This is not a walkthrough of every contract or code of the challenge. I am sharing my notes and resources I have used to complete this challenge, as well as some lessons I think are useful to take away after completing the challenge. I highly recommend you finish the challenge yourself first and only use this as additional content.
BaseLaunchpegNFT
is basically an ERC721 ownable token that uses the counters contract to track the number of NFTs for functions that return the total supply or for easily minting unique id NFTs.
The contract declares a bunch of state variables that later get initialized in the constructor, nothing of concern there except maybe the salePrice
state variable that is never initialized, which means that its 0 so we get a free mint.
The contract uses a modifier isEOA()
that checks if msg.sender
is an Externally Owned Account and not a smart contract. The modifier uses an opcode extcodesize()
that takes in a 20 byte address and returns the byte size of the code. EOAs have empty code regions so the extcodesize()
will return 0. However this modifier can easily be bypassed by using a smart contract that has all its logic in the constructor since extcodesize()
returns 0 on a contract in construction. Usually if a function uses this kind of modifier there could be a vulnerability there.
IMPORTANT: Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract constructor.
FlatLaunchpeg.sol
acts as a wrapper for BaseLaunchpegNFT
. The contract uses the suspicious modifier isEOA()
on an external payable mint function publicSaleMint()
which can be exploited with a smart contract.
FlatLaunchpeg
contract is deployed with collectionSize = 69
, maxBatchSize = 5
, and maxPerAddressDuringMint = 5
. Which means the attack contract must mint 69 NFTs, 5 NFTs at a time.
The attack contract must do the following:
Contain all attack logic in the constructor
Mint the whole collection of 69 NFTs
Must keep on minting while the condition nft.totalSupply() < nft.collectionSize()
If the amountToMint + nft.totalSupply() >= nft.collectionSize()
it must decrease the amountToMint
and then proceed to mint
If the two above conditions are satisfied we can call the publicSaleMint(amountToMint)
Since publicSaleMint()
checks if our address already has 5 NFTs we must immediately transfer them to another address (attacker address) so that we can mint 5 more again
Complete contract can be found here