Audit: Bored Ape Yacht Club

Audit: Bored Ape Yacht Club

In recent months, non-fungible tokens (NFTs) have become increasingly popular in the world of cryptocurrency. One of the most well-known NFT projects is the Bored Ape Yacht Club (BAYC), which has garnered significant attention and sales on the OpenSea marketplace.

The Bored Ape Yacht Club (BAYC) has taken the NFT world by storm, with a total volume of 699.335ETH (~$828.086.800) on OpenSea. But with great success comes scrutiny, and it's important to take a closer look at the smart contract behind this popular project.

In this article, we'll conduct a code review of the BAYC smart contract to gain a better understanding of its functionality and potential issues. We'll explore the various state variables, functions, and logic in the contract, and discuss any areas of concern. To follow along, you can view the BAYC smart contract on Etherscan at this link: https://etherscan.io/address/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d#code.

The BAYC smart contract implements the ERC721 non-fungible token standard and includes several functions for minting, reserving, and managing tokens.

The Bored Ape Yacht Club smart contract includes several state variables, each with its own purpose. These variables include a string called BAYC_PROVENANCE, which holds a provenance hash, and two uint256 variables called startingIndexBlock and startingIndex, which are used to determine the starting index for a collection. There are also various constants, such as apePrice, which holds the price of a token in wei, and maxApePurchase, which holds the maximum number of tokens that can be purchased at one time. The MAX_APES variable holds the maximum number of tokens that can be created, and the saleIsActive variable indicates whether the sale of tokens is currently active. Finally, there's the REVEAL_TIMESTAMP variable, which holds a reveal timestamp.

using SafeMath for uint256;
string public BAYC_PROVENANCE = "";
uint256 public startingIndexBlock;
uint256 public startingIndex;
uint256 public constant apePrice = 80000000000000000; //0.08 ETH
uint public constant maxApePurchase = 20;
uint256 public MAX_APES;
bool public saleIsActive = false;
uint256 public REVEAL_TIMESTAMP;
BAYC State variables

The constructor function takes four arguments, including the name and symbol of the contract, the maximum NFT supply, and the start time for the sale. This function initializes the ERC721 contract with these values, sets the MAX_APES and REVEAL_TIMESTAMP variables, and increments the REVEAL_TIMESTAMP by 9 days (set on deployment to Friday, April 30, 2021, at 10:00:00 PM).

constructor(string memory name, string memory symbol, uint256 maxNftSupply, uint256 saleStart) ERC721(name, symbol) {
    MAX_APES = maxNftSupply;
    REVEAL_TIMESTAMP = saleStart + (86400 * 9);
}
BAYC Constructor

The Bored Ape Yacht Club smart contract includes several functions, each with its own purpose. The first function we'll examine is withdraw(), which allows the owner to withdraw the ETH balance of the contract. This function is marked as onlyOwner`, meaning that only the designated owner can perform this action. When called, the function retrieves the ETH balance of the contract and transfers it to the owner's address.

function withdraw() public onlyOwner {
   uint balance = address(this).balance;
   msg.sender.transfer(balance);
}
BAYC withdraw

Another interesting function in the BAYC contract is reserveApes(). This function allows the owner to set aside 30 apes for themselves. However, upon closer inspection, it appears that the function does not check if the number of reserved apes exceeds the MAX_APES supply, meaning that the owner can keep minting 30 new Apes every time they call this function. Given the current value of each Bored Ape, this could potentially result in a significant profit for the owner. It's worth noting that the function is also marked as onlyOwner, so only the designated owner can reserve these apes.

function reserveApes() public onlyOwner {        
    uint supply = totalSupply();
    uint i;
    for (i = 0; i < 30; i++) {
        _safeMint(msg.sender, supply + i);
    }
}
BAYC reserveApes

They called the function short after they deployed the smart contract:
https://etherscan.io/tx/0xcfb197f62ec5c7f0e71a11ec0c4a0e394a3aa41db5386e85526f86c84b3f2796

In addition to withdraw() and reserveApes(), the Bored Ape Yacht Club smart contract includes several other functions. setRevealTimestamp() allows the owner to set the REVEAL_TIMESTAMP variable. This function is also marked as onlyOwner, so only the designated owner can modify this value.

The setProvenanceHash() function allows the owner of the Bored Ape Yacht Club contract to set the BAYC_PROVENANCE state variable to a unique identifier that can be used to verify the authenticity and ownership of a physical or digital item. However, the purpose of storing this value in the contract is not clear from the code provided, as it is not used in any other functions or referenced in any other way.

It is possible that this feature could be used in the future to provide additional verification and authentication for the Bored Ape Yacht Club collection, but at present it does not appear to have any practical use within the context of the contract.

setBaseURI() allows the owner to set the base URI for the contract. This function is also marked as onlyOwner, so only the designated owner can modify the base URI.

flipSaleState() allows the owner to pause or activate the sale of tokens. This function is also marked as onlyOwner, so only the designated owner can modify the sale state.

The mintApe() function is a critical function in the Bored Ape Yacht Club smart contract, as it allows users to purchase and mint new Bored Ape NFTs. This function includes several checks to ensure that the transaction is valid and complies with the rules of the contract. First, the function checks that the sale is currently active using the saleIsActive boolean. If it's not, then the transaction will fail with the message "Sale must be active to mint Ape."

The function also checks that the number of tokens being purchased does not exceed the maximum allowed, which is set to 20 tokens per transaction using the maxApePurchase constant. Additionally, the function checks that the purchase does not exceed the maximum supply of tokens, which is set by the MAX_APES variable.

Finally, the function checks that the value of ether being sent is sufficient to purchase the requested number of tokens. If all of these checks pass, the function will create the requested number of tokens and assign them to the sender using the _safeMint() function. The startingIndexBlock is also set if it hasn't already been set and the conditions are met.

function mintApe(uint numberOfTokens) public payable {
    require(saleIsActive, "Sale must be active to mint Ape");
    require(numberOfTokens <= maxApePurchase, "Can only mint 20 tokens at a time");
    require(totalSupply().add(numberOfTokens) <= MAX_APES, "Purchase would exceed max supply of Apes");
    require(apePrice.mul(numberOfTokens) <= msg.value, "Ether value sent is not correct");
        
    for(uint i = 0; i < numberOfTokens; i++) {
       uint mintIndex = totalSupply();
       if (totalSupply() < MAX_APES) {
           _safeMint(msg.sender, mintIndex);
       }
    }

    // If we haven't set the starting index and this is either 1) the last saleable token or 2) the first token to be sold after
    // the end of pre-sale, set the starting index block
    if (startingIndexBlock == 0 && (totalSupply() == MAX_APES || block.timestamp >= REVEAL_TIMESTAMP)) {
        startingIndexBlock = block.number;
    } 
}
BAYC mint 

The setStartingIndex() function is used to set the starting index for the Bored Ape Yacht Club collection. This function is called after the sale has ended, and the starting index is required for the reveal of the metadata for each token. The function first checks that the starting index has not already been set and that the startingIndexBlock variable has been set. If these conditions are not met, the function will fail.

Next, the function calculates the starting index using the block hash of the startingIndexBlock and the maximum number of tokens allowed. If the function is called too late, the calculation may use a different block hash to ensure that the starting index is not predictable. Finally, the function adjusts the starting index to ensure that it is not set to zero, which could interfere with the default sequence of the tokens.

Overall, the setStartingIndex() function ensures that the starting index for the Bored Ape Yacht Club collection is set in a secure and predictable manner to allow for the reveal of the metadata for each token.

The emergencySetStartingIndexBlock() function is included as a safety measure to allow the owner of the Bored Ape Yacht Club contract to set the starting index block if it has not been set already. This function can only be called by the owner and will fail if the starting index has already been set.

In the event that the startingIndexBlock has not been set, the owner can call this function to set the starting index block manually. This ensures that the starting index can be calculated correctly and that the reveal of the metadata for each token can proceed as expected.

The emergencySetStartingIndexBlock() function is a useful fallback option in the event that the starting index block has not been set correctly, providing an additional level of security and ensuring that the reveal of the metadata for each token proceeds smoothly.

In conclusion, the Bored Ape Yacht Club contract appears to be well-structured and follows standard practices for implementing an ERC721 contract. The state variables and functions have been designed to provide the necessary functionality for managing the collection, including the ability to mint and reserve tokens, set the base URI, and pause and activate sales.

The contract's developer(s) have taken certain precautions to ensure that the contract functions as intended, such as including checks to prevent the purchase of more tokens than the maximum supply, and setting aside a limited number of tokens for the owner. However, the reserveApes() function does not include a check to prevent the owner from setting aside more tokens than the maximum supply, which could potentially lead to the creation of unlimited tokens.