Securing Your Mint Smart Contract Function

NFTs are all the rage and thanks to standards like ERC-721 and libraries OpenZeppellin anyone can get an NFT smart contract deployed to the Ethereum mainnet with relative ease.

A key part of any NFT drop is the minting function. This is the function that is used by outside users to generate their NFT from the ERC-721 compliant contract. This is usually a payable function that takes in a number of parameters like number of NFTs to mint and the address to send the NFT to.

Developers include a number of restrictions on these minting functions to make sure that the user has payed enough ETH for the number of NFTs they are trying to mint, as well as restrict the number of NFTs a user can mint per transaction. Developers have also tried things like using block time to prevent users from minting too many NFTs over a period of time.

Here is an example minting function from the Party Penguins NFT project contract:

function mint(address _to, uint256 num) public payable {
    uint256 supply = totalSupply();

    if(msg.sender != owner()) {
        require(!_paused, "Sale Paused");
        require( num < (_maxMint+1),"You can adopt a maximum of _maxMint Penguins" );
        require( msg.value >= _price * num,"Ether sent is not correct" );
    }

    require( supply + num < MAX_ENTRIES, "Exceeds maximum supply" );

    for(uint256 i; i < num; i++){
        _safeMint( _to, supply + i );
    }
}

   
The require statements are placing restrictions on the ability of the user to mint. When a require statement fails the entire transaction fails and is reverted, and none of the changes are made on the blockchain. This specific function restricts the number of NFTs the caller can mint as well as verifies the price the user has sent along with the call, and makes sure the caller isn't trying to mint more than the total supply of the collection. In this case the owner of the contract can bypass many of the restrictions.

Developers also usually provide a front end UI that lets users connect their wallet and enter the number of NFTs to mint. The UI will then calculate the amount of ETH the transaction will cost and send the transaction to be signed and executed by the user's wallet. Restrictions on NFT minting can also be enforced by the UI, BUT users can also interact with the mint function directly from the contract page on etherscan, so any restrictions need to be built into the mint function.

Of course attackers have taken advantage of a number of NFT minting functions to mint more than their fair share of NFTs.

One attack vector is by calling a mint function multiple times from another smart contract. All public functions, which minting functions are, can be called by any contract that knows the function signature and address of the contract. Attackers can bundle a bunch of mint function calls together and then use things like flashbots to push their exploiting transactions through with low gas prices and avoiding any mitigating efforts by the NFT contract owner, like "pausing" the mint as the Party Penguins contract can do.

Here is an exampe attacker contract that I created for testing:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


interface TargetInterface {
    function mint(address _to, uint256 num) external payable;
}
contract MintAttacker {
    TargetInterface _target;
    constructor(address toAttack) {
        _target = TargetInterface(toAttack);
    }

    function attackMint(uint256 count) public payable {
        for(uint256 i = 0;i<count;i++){
            _target.mint{value: msg.value/count}(msg.sender,1);
        }
    }

}

   
It takes in the address of the victim contract and calls the mint function multiple times. This can be used to get around mint count and time restrictions in the victim contract, as well as save gas by executing these from a contract.

We can protect our mint function by adding a modifier that prevents the function from being called by contracts. A solidity modifier augments another function. Normal functions can be annotated with a modifier which can then execute code either before or after the annotated function:

Here is the onlyOwner modifier provided by the OpenZeppelin library:

 
modifier onlyOwner() {
    require(owner() == _msgSender(), "Ownable: caller is not the owner");
    _;
}

We can then annotate any function with onlyOwner and it will verify that the caller of the annotated function is the owner of the contract.

To help protect our mint function from being attacked by other contracts I created a validAddress modifier that checks that the origin of the transaction is the sender of the function call.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

abstract contract MintSecure {

modifier validAddress() {
require(tx.origin == msg.sender, "MintSecure: caller cannot be a smart contract");
        _;
}

}

   
tx.origin is the caller of the initial function, it will be passed down to each function call within the initiating function. In our attacker's case this will be the wallet address calling attackMint. msg.sender is the sender of the function call which in the case of _target.mint{value: msg.value/count}(msg.sender,1); is the address of the attacker contract.
validAddress simply makres sure the function caller is the same as the transaction origin. This prevents smart contracts from calling functions on other smart contracts!

To use this modifier you simply annotate your mint function with: validAddress:

function mint(address _to, uint256 num) public payable validAddress() {
   //your mint logic
}

Of course there are many ways around this modifier. An attacker can simply run the mint() function loop code through javascript scripting locally instead of using a smart contract.

I may expand on this post in the future with other techniques to prevent exploitation of mint functions