Bored Ape Yacht Club: Contract Review

Patrick Price
Northwest NFTs
Published in
10 min readOct 7, 2021

Today we’re going to review the smart contract code for the very successful virtual art drop Bored Ape Yacht Club. Non-fungible tokens are the hottest new thing right now, with drops of large sets of images raking in millions of dollars, selling out in minutes. One example of these large set NFTs is the Bored Ape Yacht Club. Assuming these tokens are mostly being purchased by lazy crypto millionaires, I think the name is pretty spot on. Each one of these bad monks sold for an initial 0.08 ETH, and the floor price currently on Opensea is a solid 40 ETH. That’s a pretty good ROI, and it’s obvious why people are jumping into this space in droves looking for similar returns.

When I see numbers like this, I wonder whether the BAYC team has set any aside for themselves. With all the money they earned on the initial minting of 10,000 of their tokens, they can afford to buy 20 of those spicy monks back. I’m sure the BAYC team purchased some of their own art during the sale, but are there any reserved apes in the contract? Two free apes for every member of the team would be a fat bonus (~$300,000.) Let’s take a look.

We take our first peek at the Etherscan page for the BAYC token, and thank god! The green check is there, they have verified their contract code and we don’t have to mess around with a decompiler.

Clicking on the “contract” tab, we can see the name of the contract and all of its build settings. The first thing that catches my eye is the “Optimization Enabled” section. The BAYC team decided not to optimize this contract before deployment. Non-optimized contracts build faster for developers, which creates a more seamless experience when you’re writing code, but they burn more gas. When you are deploying smart contracts to the main Ethereum network, you should always optimize your contracts.

Before reviewing the code of a smart contract, I like to take a look at the public views of a contract. These fields are easily accessible functions that tell us something about the current state of the contract. We can find all the public views of a contract under the “Read Contract” tab in the “Contract” section:

The first three values we see are BAYC_PROVENANCE, MAX_APES, and REVEAL_TIMESTAMP. They have been assigned names in all caps with underscores, which according to the Solidity style guide, suggests they are constants. These are a good set of constants, because they suggest a fixed image set that will be revealed at a given time. One of the many problems associated with owning digital art is the problem associated with verification. How do I verify that this art is what I paid for? The provenance hash is a unique signature that can be created from any set of images, with it you can verify that the images that the artists will later reveal (presumably at REVEAL_TIMESTAMP), are the same images that you purchased. It also solves the problem of later tampering; the host of the image set cannot change the images, or else the signature will change. MAX_APES simply suggests that this image set is of a fixed size: 10,000.

Looking down the list we see the expected ERC-721 standard views and some custom views. apePrice and saleIsActive are self apparent, and maxApePurchase looks like either the maximum number of apes a single address can purchase, or the maximum apes you can purchase in a single transaction. startingIndex and startingIndexBlock are more mysterious, but we’ll see what they do eventually.

Now let’s look at the writeable fields of the contract:

Woah! Emergency! Something has gone very wrong and needs to be reset in the starting index block! Now I’m very intrigued about this startingIndexBlock. It seems to be critically important, and very fragile!

I guess that answers my earlier curiosity, now it comes down to how many they reserved.

Huh, this smells fishy. I was under the impression that the provenance hash and the reveal timestamp were constant, but here are their setters. Maybe you can only set them once? Lockable fields aren’t really implemented by default in Solidity and it makes sense that you might want to set these fields after deploying the contract, so let’s give this a pass for now and we’ll see how things shake out in the code.

Over to the code, we see it’s a big ol’ flat file. I love that, because you can check all the imported standard templates as you scroll to the bottom. This contract implements IERC165, which is an interface (thus the I), that allows contracts to signal to other contracts what functions they expose. The only function required for a contract to meet the ERC165 standard is supportsInterface, which takes in a 4 byte interface ID, and returns a boolean yes/no answer as to whether the contract has that interface. This contract also implements the ERC721 interface, which is the standard interface for NFTs and defines a set of functions to transfer ownership of the tokens and allow verified auctions and market-houses to manage and sell products for holders. Specifically, this contract implements the ERC721Enumerable extension of the ERC721 standard which adds a powerful set of functions to track which tokens are owned by who.

After our quick trip through all those beautiful OpenZeppelin imports, it’s time to look at some monkey code.

I’m going to be a little nitpicky here, but it seems pretty silly to do any kind of unnecessary math in your constructor like this. They’re setting the reveal timestamp to 9 days after saleStart, and they’re calculating that timestamp for 9 days after saleStart on the blockchain. They might as well have passed in the reveal timestamp, they’re not using saleStart anywhere. Admittedly, if I just made $2.5 million, I wouldn’t mind giving some change to miners.

This answers the reserve question. The owner of this contract can mint 30 apes whenever they want, just pay the gas fees. I guess they probably only pushed this button once or twice, but with the apes at their current price, pushing that button creates $3.5 million.

This function does not check the current number of tokens against the MAX_APES we looked at above. That means the owner of this contract will be able to mint more apes for as long as they please. They might need to start drawing some more apes, but they can add as many tokens as they want. I for one, would pay top dollar for BAYC # 10,017. It’s not hard to add checks in that prevent the owner of a contract from breaking all the rules they set for their customers, and not adding in those checks leaves the door open for potential abuse. Even if you trust the group currently in control of this contract, do you trust them to manage their keys?

Hi Gargamel!

Unfortunately we’ve found the setters for those “constants.” The reveal timestamp and provenance hash can be set whenever by the contract owner. This is unfortunate, because it totally undermines the point of the provenance hash. Because this setter does not lock, there is no guarantee against future tampering by the owner of the contract.

Imagine you’re signing a contract, you agree to hand over 10000 pictures of monkeys, and you sign in pencil. 10000 people sign the contract in pen, give you your smooth $2.5 mil, and you walk off with the contract in your hand, and all the pictures too.

We found our mint function: this is where users can pay to create the tokens. We learn the meaning of maxApePurchase, it’s the number of apes a user can buy at one time. Checks like this are good in multi-mint functions because minting ERC721Enumerable tokens is expensive and minting many of them at once can quickly hit gas limits.

At the end of this mint function, we start to get a sense of what startingIndexBlock is. The starting index block is going to begin uninitialized, set to 0. In mintApe’s final if statement, the mint function will check if it’s sold out, or if the current time is after REVEAL_TIMESTAMP. If they are sold out or after the reveal, the contract is going to set the starting index block to the current block number (each block in the blockchain gets an ID number). This is about as close to a random number as you can get on the blockchain, and I wonder what they’re going to do with that value, “startingIndexBlock.”

Here is where statingIndexBlock is used. The contract is going to use it to set up startingIndex. The starting index is a number that must not be set for this function to be called, so the contract can only set the starting index one time. This is a great example of a locking field. There’s more in the comments here about sanity and preventing the “default sequence”, but we’re getting to the end of the contract and I can tell that none of this stuff is going to matter.

Just in case you weren’t happy with the random number you got

Here it is, the last function of the code, the emergency for when someone minted at the wrong time and the random starting index block got set wrong. You really don’t want someone to set your startingIndexBlock wrong, because you may enter into the “default sequence”, or potentially lose your sanity.

These functions are ancillary and have no actual influence! The startingIndex value is never reused in the contract, and thus it has no effect on anything. Why would the BAYC team leave these pointless functions in their code? After reviewing the BAYC page on provenance, we can learn the intention behind these values.

They are trying to randomize the tokens so that nobody knowns the order of IDs, and people with early access to the art set can’t “snipe” the rare tokens by purchasing many close to the indices of know rare tokens. This math is pretty simple, and the BAYC team failed to actualize it in their contract. At no point in time does an “Initial Sequence Index” exist. This could be implemented, by overriding the tokenURI function to point the tokens to the new index, but that is not implemented in this contract.

Looking around at other NFT contracts, many of them have the same copy-pasted functions. It’s evident these authors are blindly copying success, without understanding the things they use.

Lets overview the problems with this contract:

  • Claiming tokens reserved for the contract owner needs more checks to prevent owner abuse
  • The provenance hash cannot be locked, which means the owner can tamper with the image set
  • The starting index isn’t hooked in anywhere, and so it and all the functions supporting it are useless. This means that the Bored Ape tokens were not dealt out randomly, despites the claims on the BAYC website!

Realistically, I don’t think sniping is much of an issue with these NFT drops because the image sets are not usually known ahead of time. Having the startingIndex ancillary code only costs the BAYC team more money on deployment.

I think the key problems are the unlocked provenance hash and the open reserveApes. These fields should not be available and leaving them available undermines the entire agreement the contract is put in place to uphold. The safest thing to do would be to pass ownership of this contract to a shell contract that hides these fields, locking them. Alternatively, the owner could renounce ownership of the contract. At this point, most of the fields in the contract would be locked forever, and locking the baseURI field poses some potential problems regarding long term image hosting.

In our next blog post we will make some minor changes to the contract and see how much gas we can save. After that we will take a closer look at why the startingIndex logic, even if it was properly implemented, does not effectively solve the problems it claims to solve, and we will take a look at a contract with a much better implementation.

If you are working on an NFT project and need smart contract or web development support, contact us at NorthwestNFTs.com

--

--