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.