How We Built the Buildspace NFT Contract ✨

David Barrick
5 min readDec 22, 2021

--

Inspiration

Traditionally, when you complete a class / college degree / training etc, you receive a nice confirmation email, or a certificate at best.

Pretty boring right? At buildspace, we had to spice things up. So naturally, we turned to the best crypto native certificate, ✨ NFTs

Excellent. Now that we got our meme out of the way courtesy of the buildspace discord meme channel, let’s dive in, shall we?

NFTs and smart contracts in general require a lot of thought and planning upfront because of their immutable nature, so here was our spec:

  • ⛔️ Non-Transferable: Users shouldn’t be able to transfer their proof of completion to someone else.
  • 🔮 Multiple Cohorts: Each cohort in buildspace has a unique number of users enrolled. So the mint limit would also need to be unique per cohort on the contract.
  • ✈️ Airdrop Support: $MATIC is pretty expensive to bridge, so we needed a way to mint and airdrop NFTs to users without requiring them to have $MATIC in their wallets.
  • 🔒 Access Control: We needed a way to grant certain people (buildspace team members and teaching assistants) privilege to mint and airdrop NFTs.

We used the OpenZepplin ERC721 contract as our base, and modified it to fit our needs. Initially, we deployed our NFT contract to the ethereum mainnet at a cost of around $850 at the time, and paid the gas fees to mint on behalf of our users. But at $40-$60 per mint, this quickly became cost prohibitive at our scale. Enter Polygon. Polygon is an EVM compatible layer 2 chain. Once we deployed the *same* contract on Polygon, we saw our mints go down to $0.01-$0.02 each. Much better!

Note: We use merkle trees extensively in this contract. Essentially, merkle trees are an easy way to whitelist a bunch of addresses without storing an endlessly growing array of ‘allowed’ addresses on the contract. You can read more about them in relation to crypto here.

The Contract

Below is the full contract, but don’t worry I’ll break it down section by section:

Initially, buildspace users would manually claim their token from the project page via the claimToken function on line 134. We found the merkle root update during the claim process to be pretty confusing to users, so we switched to an airdrop method and abandoned the claimToken function. Alright let’s look at the first spec requirement:

⛔️ Non-Transferrable

Making NFTs non-transferrable turns out to be pretty easy. Before a transfer event occurs, and internal ERC721 _beforeTokenTransfer function is called. So all we need to do is override this function on the contract and add in our requirements (line 181). Essentially, we only wan’t to allow mints: from address(0), burning an NFT: to address(0), or if a buildspace admin explicitly allowsTransfers in the future. We kept the allowsTransfers override option just in case we saw a large amount of incorrect wallet addresses linked to user’s profiles and wanted to let them transfer to their correct wallet.

🔮 Multiple Cohorts

Let’s take a look at line 13. Our cohorts are all a mapping of cohort_id as the key, and a Cohort struct defined on line 21 as the value. Whenever we create a new cohort for a buildspace project, a new cohort is also created on the contract by calling the createCohort function on line 152. There are 3 params in this function: cohortId, limit, and merkleRoot. The cohortId param corresponds to the cohort ID on buildspace, the limit params sets a max number of mints per cohort, and the merkleRoot params allows us to pass in an initial ‘whitelist’ of users who should be able to mint against a cohort.

✈️ Airdrop Support

Every hour, a cron job built by @alec_dilanchian runs and fetches all the cohort IDs that have started within the past 7 days. For each cohort, we check to see if a completion event has happened. In our case, this means you’ve completed the last lesson in the project. Once a project completion is found for a user, and that user has linked their ETH wallet to buildspace, we generate a merkle tree in our backend, update the contract with the new merkle root with the setMerkleroot function on line 166, then run the adminClaimToken function on line 126 from the cron job with the merkle proof related to the address that is receiving the NFT. After that, we fire off a confirmation email to the users that an NFT was just delivered to their address! Here’s what this looks like on our stack:

🔒 Access Control

Initially, we believed we needed a way for the buildspace team to mint and airdrop NFTs to users. In order to do this, we needed an admin map on line 12 to store our team wallet addresses, and a way to update these admin addresses over time via the updateAdmin function on line 177. We haven’t really used this feature in the contract though, and probably won’t in the future because we now have the cron job detailed above. That turned out to be a lot easier!

🖼 Bonus: Metadata

It’s recommended to use something like IPFS for a more permanent storage solutions for NFTs. Here’s a great post on why this is important. However under our time constraints, we opted to use a Cloudfront CDN solution backed by an S3 bucket for all of our metadata needs. Our contractBaseUri is https://tokens.buildspace.so/assets/{COHORT_ID}-{COHORT_TOKEN_NUMBER}/metadata.json, and serves up all relevant metadata to wallets like MetaMask, and marketplaces like OpenSea.

And that’s it! You can checkout the contract’s activity on Polygon here. If you have any questions on our method of implementing NFTs, or just web3 stuff in general, feel free to reach out to me on twitter: @DavBarrick

PS: Huge shoutout to @jake_loo, @monkeymeaning, and the team at thirdweb for helping us build and audit this contract :)

--

--

David Barrick
David Barrick

Written by David Barrick

Building the most valuable web3 engineering community at buildspace

Responses (1)