Summary
Missing validation check in the Soulmate.sol::readMessageInSharedSpace()
function will allow the ones without soulmate to read messages in the shared space reserved for holders of the NFT with id 0
Vulnerability Details
The Soulmate.sol::readMessageInSharedSpace()
is implemented as follows:
function readMessageInSharedSpace() external view returns (string memory) {
return
string.concat(
sharedSpace[ownerToId[msg.sender]],
", ",
niceWords[block.timestamp % niceWords.length]
);
}
ownerToId[msg.sender]
will return 0
if msg.sender
does not have a soulmate, resulting in a reading message in the shared space for holders of NFT with ID 0
.
Impact
Users without soulmates can read messages in the shared space of soulmates who hold NFT with id 0
.
Proof of Concept (PoC)
Add the following test in SoulmateTest.t.sol
:
function test_NFTNonHoldersCanReadMessagesInTheSharedSpaceReservedForHoldersOfTokenWithIdZero(address random) public {
vm.assume(random != address(soulmateContract) && random != address(loveToken) &&
random != address(stakingContract) && random != address(airdropContract) &&
random != address(airdropVault) && random != address(stakingVault) &&
random != address(soulmate1) && random != address(soulmate2)
);
_mintOneTokenForBothSoulmates();
vm.prank(soulmate1);
soulmateContract.writeMessageInSharedSpace("Buy some eggs");
address random = makeAddr("random");
vm.prank(random);
string memory message = soulmateContract.readMessageInSharedSpace();
string[4] memory possibleText = [
"Buy some eggs, sweetheart",
"Buy some eggs, darling",
"Buy some eggs, my dear",
"Buy some eggs, honey"
];
bool found;
for (uint i; i < possibleText.length; i++) {
if (compare(possibleText[i], message)) {
found = true;
break;
}
}
assertTrue(found);
}
Run a test with forge test --mt test_NFTNonHoldersCanReadMessagesInTheSharedSpaceReservedForHoldersOfTokenWithIdZero
.
Tools Used
Recommendations
The transaction should be reverted if the user who doesn't have a soulmate tries to read a message in the shared space.
Recommended changes to the Soulmate.sol::mintSoulmateToken()
function:
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error Soulmate__alreadyHaveASoulmate(address soulmate);
error Soulmate__SoulboundTokenCannotBeTransfered();
+error Soulmate__CannotReadMessageInSharedSpaceOfOtherSoulmates();
function readMessageInSharedSpace() external view returns (string memory) {
if (soulmateOf[msg.sender] == address(0)) {
revert Soulmate__CannotReadMessageInSharedSpaceOfOtherSoulmates();
}
// Add a little touch of romantism
return
string.concat(
sharedSpace[ownerToId[msg.sender]],
", ",
niceWords[block.timestamp % niceWords.length]
);
}
Add the following import and test in SoulmateTest.t.sol
:
function test_readMessageInSharedSpaceRevertsCallerWhenDoesntHaveSoulmate(address random) public {
vm.assume(random != address(soulmateContract) && random != address(loveToken) &&
random != address(stakingContract) && random != address(airdropContract) &&
random != address(airdropVault) && random != address(stakingVault) &&
random != address(soulmate1) && random != address(soulmate2)
);
_mintOneTokenForBothSoulmates();
vm.prank(soulmate1);
soulmateContract.writeMessageInSharedSpace("Buy some eggs");
address random = makeAddr("random");
vm.prank(random);
vm.expectRevert(Soulmate.Soulmate__CannotReadMessageInSharedSpaceOfOtherSoulmates.selector);
string memory message = soulmateContract.readMessageInSharedSpace();
}
Run a test with forge test --mt test_readMessageInSharedSpaceRevertsCallerWhenDoesntHaveSoulmate
.