Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

People without soulmates can write messages in `Soulmate::writeMessageInSharedSpace` and overwrite the existing one

Summary

The Soulmate::writeMessageInSharedSpace allows couples to write messages each other in shared space associated with their token id. The function relies on the ownerToId mapping to determine the id associated with the caller's address. If a caller who does not have a soulmate attempts to write a message, the ownerToId mapping should not have an entry for his address, and it should default to 0 since mappings in Solidity return the default value for the value type when accessed with a key that does not exist.

Vulnerability Details

The function Soulmate::writeMessageInSharedSpace allows the person to write message to the soulmate. The function is public and there is no check if the caller of the function has actually a soulmate. Therefore, people who don't have a soulmate can also calls the function and write a message in shared space.

function writeMessageInSharedSpace(string calldata message) external {
@> uint256 id = ownerToId[msg.sender];
sharedSpace[id] = message;
emit MessageWrittenInSharedSpace(id, message);
}

The function Soulmate::writeMessageInSharedSpace get the id token of the caller (msg.sender) and assign a message to this id in the sharedSpace mapping.
If the caller is waiting for a soulmate, he/she will have a valid id and the message will be written to his/her id. the caller can write a message to the future soulmate.
But if the caller doesn't have a soulmate and his/her address doesn't exist in the ownerToId mapping, the return value from the ownerToId[msg.sender] will be 0. So the message will be written in the sharedSpace with id 0. The problem is that the id 0 is reserved for the first couple of the protocol. And this couple will receive message that is not intended to receive.
Also, the messages in the shared space are not saved and the new message overwrites the previous one.

Impact

Let's consider the following scenario. There is a couple who use the protocol (soulmate1 and soulmate2). Their token id is equal to 0. Then Bob who doesn't have a soulmate calls the function Soulmate::writeMessageInSharedSpace with a message and this message will be written to the sharedSpace with id equals to 0. Thus, the couple whose token id is equal to 0 will read this message and soulmate1 will think that the message is from soulmate2 and vice versa. That leads to confusion and dependency of the message to the problems in the couple.

The following test shows the possibility of writing message from a person who doesn't have a soulmate in the shared space of the existing couple. You can add the test in the test file Soulmate.t.sol and execute it with the command: forge test --match-test "test_writeMessage".

function test_writeMessage() public {
//soulmate1 and soulmate2 are already a couple
_mintOneTokenForBothSoulmates();
//Bob doesn't have a soulmate
address bob = makeAddr("bob");
vm.prank(bob);
//Bob's id is 0, the same as the first couple
uint id = soulmateContract.ownerToId(bob);
string memory message = "Buy some eggs";
//Bob successfully write a message in the shared space with id equals to 0
soulmateContract.writeMessageInSharedSpace(message);
assertEq(soulmateContract.sharedSpace(id), message);
}

Additionally, the Soulmate::writeMessageInSharedSpace writes the new message over the last one. There is no mechanism to save the all messages between soulmates. In that case the person who doesn't have a soulmate and writes a message in the shared space with id equals to 0 will overwrite the last message between soulmates.
Let's consider the following scenario. Soulmate1 and Soulmate2 are a couple. Soulmate1 writes a message to a soulmate2 in a shared space with id equals to 0 to buy apples. But at the same time Bob who doesn't have a soulmate and doesn't wait for a soulmate calls the function and overwites the message from the soulmate2. So the soulmate2 will read a message with text Buy some eggs that is not written from a soulmate2. You can add the following test that demonstrates this scenario in the test file Soulmate.t.sol and execute it with the command: forge test --match-test "test_overwriteMessage".

function test_overwriteMessage() public {
//soulmate1 and soulmate2 are a couple
_mintOneTokenForBothSoulmates();
//soulmate1 write a message to a soulmate2 to buy apples
vm.prank(soulmate1);
string memory message1 = "Buy some apples";
soulmateContract.writeMessageInSharedSpace(message1);
//Bob who doesn't have a soulmate writes also a message
address bob = makeAddr("bob");
vm.prank(bob);
//Bob's id is 0, the same as the first couple's id
uint id = soulmateContract.ownerToId(bob);
//Bob writes a message
string memory message2 = "Buy some eggs";
soulmateContract.writeMessageInSharedSpace(message2);
//Bob overwrites the message from the soulmate1
assertEq(soulmateContract.sharedSpace(id), message2);
}

Also, if the both soulmates write at the same time to each other, then they can read only the last message. That means some of the messages will be lost and that leads to a confusion in the chat between soulmates.
If the soulmate1 writes a message to the soulmate2 and the soulmate2 writes a message before reading the prevous one, the message from the soulmate1 will be lost. The following test shows this case:

function test_overwriteMessageSoulmates() public {
_mintOneTokenForBothSoulmates();
vm.prank(soulmate1);
//Soulmate1 writes a message to the soulmate2 to buy apples
string memory message1 = "Buy some apples";
soulmateContract.writeMessageInSharedSpace(message1);
vm.prank(soulmate2);
uint id = soulmateContract.ownerToId(soulmate2);
//Before soulmate2 to read the message from soulmate1, soulmate2 writes a message to soulmate1 to buy eggs
string memory message2 = "Buy some eggs";
soulmateContract.writeMessageInSharedSpace(message2);
//only the last message from soulmate2 can be read
assertEq(soulmateContract.sharedSpace(id), message2);
}

Tools Used

VS Code, Foundry

Recommendations

Add a check in the Soulmate::writeMessageInSharedSpace function that the caller has a soulmate or is waiting for a soulmate.
One possible way is to check if the id is associated with the address of the caller:

function writeMessageInSharedSpace(string calldata message) external {
uint256 id = ownerToId[msg.sender];
+ require ((idToOwners[id][0] == msg.sender || idToOwners[id][0] == msg.sender),
+ "The caller doesn't have soulmate");
sharedSpace[id] = message;
emit MessageWrittenInSharedSpace(id, message);
}

If you run the test function after applying the changes, the test will fail:
[FAIL. Reason: revert: The caller doesn't have soulmate] test_writeMessage() (gas: 216077)

In order to save the whole chat history between the soulmates you can add a mapping that stores every message:
mapping(uint256 => string[]) public sharedSpaceHistory; And in the Soulmate::writeMessageInSharedSpace to add:
sharedSpaceHistory[id].push(message);.
But this approach has a disadvantage that storing a lot of data on the blockchain can become very expensive in terms of gas costs. If the chat history grows large, it could become prohibitively expensive to continue appending messages. An alternative approach could be to store the chat messages off-chain (e.g., on IPFS) and only store a reference (like a hash or URI) on-chain.

Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
bube Submitter
over 1 year ago
0xnevi Lead Judge
over 1 year ago
0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-write-message-nft-0-id

Medium Severity, This has an indirect impact and influence on the possibility of divorce between soulmates owning the first soulmate NFT id0, leading to permanent loss of ability to earn airdrops/staking rewards.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.