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

The function `Soulmate::mintSoulmateToken` allows `self` to be their own soulmate. This leads to illegitimate earning and staking of love tokens.

Summary

The soulmate::mintSoulmateToken public function allows anybody to assign themselves as their own soulmate, which completely undermines the intended functionality of the contract. This essentially enables anyone to "get divorced" from themselves.

function mintSoulmateToken() public returns (uint256) {
// Check if people already have a soulmate, which means already have a token
address soulmate = soulmateOf[msg.sender];
if (soulmate != address(0)) {
revert Soulmate__alreadyHaveASoulmate(soulmate);
}
address soulmate1 = idToOwners[nextID][0];
address soulmate2 = idToOwners[nextID][1];
if (soulmate1 == address(0)) {
idToOwners[nextID][0] = msg.sender;
ownerToId[msg.sender] = nextID;
emit SoulmateIsWaiting(msg.sender);
} else if (soulmate2 == address(0)) {
idToOwners[nextID][1] = msg.sender;
// Once 2 soulmates are reunited, the token is minted
ownerToId[msg.sender] = nextID;
//---------------------------------------
// ------------ ||-------- ||
// ------ \/---- \/
@> soulmateOf[msg.sender] = soulmate1; // msg.sender and soulmate1 both are same person.
@> soulmateOf[soulmate1] = msg.sender;
idToCreationTimestamp[nextID] = block.timestamp;
// --------------------------------------------------
// -------------------- ||----- ||
// The bias in the `soulmate::mintSoulmateToken` function causes biased log records, making the frontend DAPP ambiguous.
// ---------- \/--- \/
@> emit SoulmateAreReunited(soulmate1, soulmate2, nextID);
_mint(msg.sender, nextID++);
}
return ownerToId[msg.sender];
}

Vulnerability Details

  1. Alice, an Ethereum account holder with the address 0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916, initiates the minting process for a soulmate token using the soulmate::mintSoulmateToken public function. At the time of initiation, Alice does not have an assigned soulmate. Consequently, the initial require check passes, and Alice is assigned tokenId or nextID 0, indicating the first soulmate slot.

  2. Recognizing a vulnerability within the contract, Alice attempts to designate herself as her own soulmate by invoking the minting function again with her EOA (Externally Owned Account) Ethereum account address 0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916. Since Alice does not have an assigned soulmate yet, the initial require check is satisfied once more. However, the subsequent logic check fails due to her ownership of tokenId 0. As a result, the else if condition succeeds as soulmate2, corresponding to nextID, remains unclaimed.

  3. By exploiting this vulnerability, Alice inadvertently assigns herself as her own soulmate. As she already owns slot 1 with nextID 0, the logic flow allows her to claim ownership of slot 2 at index 1 using the statements idToOwners[nextID][1] = msg.sender and ownerToId[msg.sender] = nextID.

  4. Consequently, Alice is effectively reunited with herself within the contract, as evidenced by the assignment statements soulmateOf[msg.sender] = soulmate1 and soulmateOf[soulmate1] = msg.sender.

  5. Furthermore, the soulmate::mintSoulmateToken function emits an event named SoulmateAreReunited upon execution, with both soulmate1 and soulmate2 referencing Alice's Ethereum account address 0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916, and nextID indicating 0.

Take a look at this Poc Foundry Test...

PoC Self Reunit:
  1. Place the following test code snippet into the test/unit/soulmateTest.t.sol file. Put it at the very bottom but before the last closing semicolon }.

function test_mintSoulmateTokenToSelf() public {
address alice = makeAddr("ALICE");
vm.startPrank(alice);
uint256 alice_ownerId = soulmateContract.mintSoulmateToken();
uint256 soulmateOfAlice_ownerId = soulmateContract.mintSoulmateToken();
vm.stopPrank();
address soulmateOfAlice = soulmateContract.soulmateOf(alice);
console2.log("Alice's ownerId: ", alice_ownerId);
console2.log("soulmate Of Alice's ownerId: ", soulmateOfAlice_ownerId);
console2.log("Alice: ", alice);
console2.log("soulmate of Alice: ", soulmateOfAlice);
assertEq(soulmateOfAlice, alice);
}
  1. Open Your Bash Terminal and execute the following command...

forge test --mt "test_mintSoulmateTokenToSelf" -vv --via-ir
  1. Some output might appear upon executing the above command. Take a look at that output.

Impact

People can mint, earn, and stake loveTokens by assigning themselves as their own soulmates. They can even initiate a "divorce" from themselves. This capability allows anyone to disrupt Valentine's day and compromise the integrity of the entire Soulmate Contract | Protocol.

Tools Used

Foundry Framework (Solidity, Rust)

Recommendations

The easiest mitigation could be:

Implement an if check to verify that the second person isn't the first person attempting to make themselves their soulmate.

Make the following changes in src/Soulmate.sol:

contract Soulmate is ERC721 {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error Soulmate__alreadyHaveASoulmate(address soulmate);
error Soulmate__SoulboundTokenCannotBeTransfered();
+ error Soulmate__CannotBeSelfSoulmate(address soulmate);
...
...
...
function mintSoulmateToken() public returns (uint256) {
// Check if people already have a soulmate, which means already have a token
address soulmate = soulmateOf[msg.sender];
if (soulmate != address(0)) {
revert Soulmate__alreadyHaveASoulmate(soulmate);
}
address soulmate1 = idToOwners[nextID][0];
address soulmate2 = idToOwners[nextID][1];
+ if (soulmate1 == msg.sender) {
+ revert Soulmate__CannotBeSelfSoulmate(soulmate1);
+ }
if (soulmate1 == address(0)) {
idToOwners[nextID][0] = msg.sender;
ownerToId[msg.sender] = nextID;
emit SoulmateIsWaiting(msg.sender);
} else if (soulmate2 == address(0)) {
idToOwners[nextID][1] = msg.sender;
// Once 2 soulmates are reunited, the token is minted
ownerToId[msg.sender] = nextID;
soulmateOf[msg.sender] = soulmate1;
soulmateOf[soulmate1] = msg.sender;
idToCreationTimestamp[nextID] = block.timestamp;
emit SoulmateAreReunited(soulmate1, soulmate2, nextID);
_mint(msg.sender, nextID++);
}
return ownerToId[msg.sender];
}
...
...
...
}
Mitigation Poc:
  1. After implementing & Updating Soulmate.sol with the above mitigation code. Place the following test code snippet into the test/unit/soulmateTest.t.sol file. Put it at the very bottom but before the last closing semicolon }.

function test_mintSoulmateTokenToSelfProtect() public {
address alice = makeAddr("ALICE");
address bob = makeAddr("BOB");
vm.startPrank(alice);
uint256 alice_ownerId = soulmateContract.mintSoulmateToken();
vm.expectRevert();
soulmateContract.mintSoulmateToken();
vm.stopPrank();
vm.startPrank(bob);
uint256 soulmateOfAlice_ownerId = soulmateContract.mintSoulmateToken();
vm.expectRevert();
soulmateContract.mintSoulmateToken();
vm.stopPrank();
address soulmateOfAlice = soulmateContract.soulmateOf(alice);
address soulmateOfBob = soulmateContract.soulmateOf(bob);
console2.log("Alice's ownerId: ", alice_ownerId);
console2.log("soulmate Of Alice's ownerId: ", soulmateOfAlice_ownerId);
console2.log("Alice: ", alice);
console2.log("soulmate of Alice: ", soulmateOfAlice);
console2.log("soulmate of Bob: ", soulmateOfBob);
assertEq(soulmateOfAlice, bob);
assertEq(soulmateOfBob, alice);
}
  1. Open Your Bash Terminal and execute the following command...

forge test --mt "test_mintSoulmateTokenToSelfProtect" -vv --via-ir
  1. Ouput should indicate that test Passed Successfully.

Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-self-soulmate

- Given the native anonymous nature of blockchain in general, this issue cannot be avoided unless an explicit whitelist is implemented. Even then we can only confirm soulmates are distinct individuals via kyc. I believe finding a soulmate is intended to be permisionless. - However, even though sufficient (500_000_000e18 in each vault) tokens are minted to claim staking and airdrop rewards, it would take 500_000_000 / 2 combined weeks for airdrop vault to be drained which is not unreasonable given there are [80+ million existing wallets](https://coinweb.com/trends/how-many-crypto-wallets-are-there/). Given there is no option to mint new love tokens, this would actually ruin the functionality of the protocol of finding soulmates and shift the focus to abusing a sybil attack to farming airdrops instead. Assigning medium severity for now but am open for appeals otherwise, since most if not all issues lack indepth analysis of the issue.

theirrationalone 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-self-soulmate

- Given the native anonymous nature of blockchain in general, this issue cannot be avoided unless an explicit whitelist is implemented. Even then we can only confirm soulmates are distinct individuals via kyc. I believe finding a soulmate is intended to be permisionless. - However, even though sufficient (500_000_000e18 in each vault) tokens are minted to claim staking and airdrop rewards, it would take 500_000_000 / 2 combined weeks for airdrop vault to be drained which is not unreasonable given there are [80+ million existing wallets](https://coinweb.com/trends/how-many-crypto-wallets-are-there/). Given there is no option to mint new love tokens, this would actually ruin the functionality of the protocol of finding soulmates and shift the focus to abusing a sybil attack to farming airdrops instead. Assigning medium severity for now but am open for appeals otherwise, since most if not all issues lack indepth analysis of the issue.

Support

FAQs

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