Summary
As per documentation everyone should be able to mint a soulbound NFT:
```mintSoulmateToken```: Where you'll mint a soulbound NFT. Everyone should be able to be minted a soulmate.
But in the Soulmate::mintSoulmateToken
, after the soulmates are reunited, only the soulmate2 can mint and receive the NFT. The soulmate1 doesn't mint and doesn't receive the NFT.
Vulnerability Details
In the Soulmate::mintSoulmateToken
, after the soulmates are reunited, only the second soulmate (soulmate2) can mint and receive the NFT. The soulmate1 is correctly coupled with soulmate2 as a soulbound but he/she doesn't mint and receive the NFT.
function mintSoulmateToken() public returns (uint256) {
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;
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];
}
Impact
function testOnlySoulmate2MintAndReceiveNFT() public {
uint256 tokenIdMinted = 0;
vm.startPrank(soulmate1);
soulmateContract.mintSoulmateToken();
console2.log("The solamate1 balance before is", soulmateContract.balanceOf(soulmate1));
assertTrue(soulmateContract.totalSupply() == 0);
vm.stopPrank();
vm.startPrank(soulmate2);
console2.log("The solamate2 balance before is", soulmateContract.balanceOf(soulmate2));
soulmateContract.mintSoulmateToken();
console2.log("The solamate1 balance after is", soulmateContract.balanceOf(soulmate1));
console2.log("The solamate2 balance after is", soulmateContract.balanceOf(soulmate2));
vm.stopPrank();
assertTrue(soulmateContract.totalSupply() == 1);
assertTrue(soulmateContract.soulmateOf(soulmate1) == soulmate2);
assertTrue(soulmateContract.soulmateOf(soulmate2) == soulmate1);
assertTrue(soulmateContract.ownerToId(soulmate1) == tokenIdMinted);
assertTrue(soulmateContract.ownerToId(soulmate2) == tokenIdMinted);
assertEq(soulmateContract.balanceOf(soulmate1), 0);
assertEq(soulmateContract.balanceOf(soulmate2), 1);
}
Running 1 test for test/unit/SoulmateTest.t.sol:SoulmateTest
[PASS] testOnlySoulmate2MintAndReceiveNFT() (gas: 227021)
Logs:
The solamate1 balance before is 0
The solamate2 balance before is 0
The solamate1 balance after is 0
The solamate2 balance after is 1
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.09ms
Tools Used
Manual review
Recommendations
Add the mint also for the soulmate1.
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;
soulmateOf[soulmate1] = msg.sender;
idToCreationTimestamp[nextID] = block.timestamp;
emit SoulmateAreReunited(soulmate1, soulmate2, nextID);
_mint(msg.sender, nextID++);
+. _mint(msg.soulmate1, nextID++);
}
return ownerToId[msg.sender];
}