DatingDapp

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

Reentrancy Risk in `mintProfile` Due to Improper State Update Order

Summary

In src/SoulboundProfileNFT.sol, the mintProfile function first checks the validity, then mints the NFT, which may trigger an external call via the onERC721Received function, and finally updates the contract's internal state. This violates the Checks-Effects-Interactions (CEI) principle, potentially introducing a reentrancy risk.

Vulnerability Details

The function implementation is as follows:

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); // External call before updating contract state
// Store metadata on-chain
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}

In this function, _safeMint(msg.sender, tokenId) is executed before updating _profiles[tokenId] and profileToToken[msg.sender]. Since _safeMint can trigger onERC721Received, which allows external contracts to execute arbitrary logic, an attacker could exploit this reentrancy window to manipulate contract state in an unintended way.

Impact

  • Potential reentrancy attack, leading to unauthorized multiple profile minting

  • Inconsistent contract state if execution is reverted unexpectedly during the external call

Tools Used

Manual code review

Recommendations

Reorder the function logic to follow the CEI principle:

  1. Update the contract's internal state before performing any external interactions

  2. Call _safeMint only after modifying _profiles[tokenId] and profileToToken[msg.sender]

The corrected implementation should be:

function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
uint256 tokenId = ++_nextTokenId;
// Store metadata on-chain before external interaction
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
_safeMint(msg.sender, tokenId); // External call after state update
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}

This modification ensures that any external contract interaction occurs only after the contract's internal state is securely updated, reducing the risk of reentrancy attacks.

Updates

Appeal created

n0kto Lead Judge 6 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.