Bored Ape Yacht Club
February 4, 2022
Bored Ape Yacht Club
Bored Ape Yacht Club (BAYC) is a collection of 10000 unique digital collectibles stored on the Ethereum blockchain. Each Bored Ape is programmatically generated from over 170 possible traits, with each combination affecting an ape's rarity.
BAYC is one of the most popular and most traded NFT collections, with a floor price currently sitting at 94.5 eth (~$280k). The collection is stored as an ERC-721 token on the Ethereum blockchain and hosted on IPFS. You can view the collection on OpenSea.
Contract Identifiers
Contract Address
In order to view the smart contract of an NFT collection you need the contract address. Most NFT collections display the contract address on their website or Twitter page. The BAYC website displays the verified contract address as follows:
0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D
Etherscan
Once we have the contract address, the easiest way to explore the contract is using a block explorer like Etherscan. Etherscan allows you to search through transactions, blocks, wallet addresses, smart contracts, and other on-chain data.
If you enter the contract address into Etherscan, you will be taken to the Bored Ape Yacht Club contract.
Scrolling through this page you will see various descriptors of the contract, including: Contract Source Code, Contract Security Audit, Contract ABI, Contract Creation Code, Constructor Arguments, Deployed ByteCode Sourcemap and Swarm Source. We are interested in the Contract ABI.
Contract ABI
An Application Binary Interface (ABI) is a low-level interface that defines the properties exposed by a smart contract. An ABI is required in order to interact with a smart contract programmatically. Using Etherscan, we can extract the ABI of the BAYC contract:
When interacting with a contract, you only need to define the functions of the ABI that you want to use. Without a block explorer it is not obvious what functions a contract exposes. However, if we know that a token follows a specific standard (the ERC-721 standard in this case), then we know that the contract ABI will always include the functions implemented by that standard.
Network Nodes
In order to interact with blockchain data, the Ethereum protocol requires a connection to a node on the network running locally or by a third-party. A node is a running piece of client software that verifies all transactions in each block of the network. Having many independent and geographically dispersed nodes helps to ensure the health, resilience, and censorship resistance of the blockchain.
Ethereum clients can run three types of nodes with varying methods of synchronization:
- full node
- stores full blockchain data
- verifies all blocks and states
- light node
- stores the header chain and requests everything else
- verifies data against the state roots in the block headers
- archive node
- stores everything kept in the full node
- builds an archive of historical states
Local nodes allow you to interact with the network in a private, self-sufficient and trustless manner. However, this requires verifying all transactions and maintaining the latest state of the blockchain. This uses up a significant amount of disk space, bandwidth and computation as well as a big up-front cost for downloading the full blockchain history.
Due to these requirements, using a third-party node is often easier, faster and more secure than running a local node.
Infura
Infura is a web3 Infrastructure-as-a-Service company that provides access to blockchain nodes and developer tools. Through the Infura API, accessing the Ethereum blockchain is quick and building decentralised apps is simple, secure and scalable.
In order to use an Infura node, create an account, create a new project and go to the project settings page. Under the Keys section, there will be a Project ID, a Project Secret and an Endpoints section. The MAINNET
endpoint (which has the Project ID embedded in it) will be used for authentication when connecting to one of Infura's nodes.
[https://mainnet.infura.io/v3/](https://mainnet.infura.io/v3/)<PROJECT_ID>
Exploring the BAYC Contract
Now that we have the contract ABI and an Ethereum node, we can connect to the blockcahin and interact with the BAYC contract through an Ethereum client.
Web3.py
Web3.py is a Python client that provides an interface to interact with Ethereum.
- inside a virtual environment, install the
web3
package:
pip install web3
- instantiate a contract object:
# import libraries
import json
from web3 import Web3
# instantiate node connection
w3 = Web3(Web3.HTTPProvider('[https://mainnet.infura.io/v3/](https://mainnet.infura.io/v3/)<PROJECT_ID>'))
# specify the contract address and ABI
contract_address = Web3.toChecksumAddress("0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D")
abi = json.loads(
'[{"inputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"},{"internalType":"uint256","name":"maxNftSupply","type":"uint256"},{"internalType":"uint256","name":"saleStart","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"BAYC_PROVENANCE","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_APES","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REVEAL_TIMESTAMP","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"apePrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"emergencySetStartingIndexBlock","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"flipSaleState","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxApePurchase","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"numberOfTokens","type":"uint256"}],"name":"mintApe","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"reserveApes","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"saleIsActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"baseURI","type":"string"}],"name":"setBaseURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"provenanceHash","type":"string"}],"name":"setProvenanceHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"revealTimeStamp","type":"uint256"}],"name":"setRevealTimestamp","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"setStartingIndex","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"startingIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"startingIndexBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenOfOwnerByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
)
# instantiate contract
contract = w3.eth.contract(address=contract_address, abi=abi)
We now have a BAYC contract object connected to Ethereum. To view the functions exposed by the contract (essentially the interface defined by the ABI):
contract.all_functions()
This returns a list of function objects that are exposed by the contract. The highlighted functions implement the ERC-721 interface, a standard created for non-fungible tokens. The remaining functions are specific to the Bored Ape Yacht Club contract, extending the ERC-721 interface to provide further functionality and customisation.
[
<Function BAYC_PROVENANCE()>,
<Function MAX_APES()>,
<Function REVEAL_TIMESTAMP()>,
<Function apePrice()>,
<Function approve(address,uint256)>,
<Function balanceOf(address)>,
<Function baseURI()>,
<Function emergencySetStartingIndexBlock()>,
<Function flipSaleState()>,
<Function getApproved(uint256)>,
<Function isApprovedForAll(address,address)>,
<Function maxApePurchase()>,
<Function mintApe(uint256)>,
<Function name()>,
<Function owner()>,
<Function ownerOf(uint256)>,
<Function renounceOwnership()>,
<Function reserveApes()>,
<Function safeTransferFrom(address,address,uint256)>,
<Function safeTransferFrom(address,address,uint256,bytes)>,
<Function saleIsActive()>,
<Function setApprovalForAll(address,bool)>,
<Function setBaseURI(string)>,
<Function setProvenanceHash(string)>,
<Function setRevealTimestamp(uint256)>,
<Function setStartingIndex()>,
<Function startingIndex()>,
<Function startingIndexBlock()>,
<Function supportsInterface(bytes4)>,
<Function symbol()>,
<Function tokenByIndex(uint256)>,
<Function tokenOfOwnerByIndex(address,uint256)>,
<Function tokenURI(uint256)>,
<Function totalSupply()>,
<Function transferFrom(address,address,uint256)>,
<Function transferOwnership(address)>,
<Function withdraw()>
]
Through Web3.py
we can now call any of the above functions and inspect the data returned by the network.
Contract Details
Mint details:
contract.functions.MAX_APES().call()
> 10000
contract.functions.apePrice().call()
> 80000000000000000 # 0.08 ETH
contract.functions.maxApePurchase().call()
> 20
contract.functions.REVEAL_TIMESTAMP().call()
> 1619820000 # 2021-05-01 00:00:00
contract.functions.startingIndex().call()
> 8853
contract.functions.startingIndexBlock().call()
> 12344391
Metadata details:
contract.functions.name().call()
> BoredApeYachtClub
contract.functions.symbol().call()
> BAYC
contract.functions.baseURI().call()
> ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
contract.functions.tokenURI(0).call()
> ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/0
The BAYC is an NFT collection, and NFTs usually store the underlying assets that they represent off-chain. As can be seen, each token points to an IPFS hash where the token metadata is stored.
IPFS
IPFS is a peer-to-peer hypermedia protocol for the Internet. It is a decentralized, distributed, immutable, and private file system. When you upload a file, it is broken down, cryptographically hashed and given a unique fingerprint called a content indentifier. When nodes look up your file, they ask peer nodes storing content referenced by the content identifier. In doing so, new nodes will cache a copy of your file and become another provider of your content.
As seen above, the BAYC contract stores contract metadata at the following IPFS base hash (for specific tokens you will need to append the token ID):
QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq
With the IPFS base hash, we can use the IPFS explorer to view the metadata associated with the Token ID of 0 (or any other valid Token ID):
[https://ipfs.io/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/0](https://ipfs.io/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/0)
This returns the following JSON file:
{
"image": "ipfs://QmRRPWG96cmgTn2qSzjwr2qvfNEuhunv6FNeMFGa9bx6mQ",
"attributes": [
{ "trait_type": "Earring", "value": "Silver Hoop" },
{ "trait_type": "Background", "value": "Orange" },
{ "trait_type": "Fur", "value": "Robot" },
{ "trait_type": "Clothes", "value": "Striped Tee" },
{ "trait_type": "Mouth", "value": "Discomfort" },
{ "trait_type": "Eyes", "value": "X Eyes" }
]
}
There are two key-value pairs returned: image
and attributes
. image
points to another IPFS hash, this time the hash of the image itself. attributes
specifies the various traits that this particular ape is made up of, all of which affect it's rarity. To view the image, we can follow the IPFS hash again:
[https://ipfs.io/ipfs/QmRRPWG96cmgTn2qSzjwr2qvfNEuhunv6FNeMFGa9bx6mQ](https://ipfs.io/ipfs/QmRRPWG96cmgTn2qSzjwr2qvfNEuhunv6FNeMFGa9bx6mQ)