Building a Web3 Dapp (Part 2) — Writing the Smart Contract

Tobias Fan
Coinmonks
7 min readNov 13, 2021

--

This is Part 2 of a 4-part series on ScratchCollective.app, an NFT-minting application. You can find the other articles here:

Introduction

Building a Web3 Dapp (Part 1) — Lessons Learned

Here I do an in-depth review the ScratchCollective smart contract, the rationale behind some of the design decisions.

In the next article, I will cover how I interact with this contract on the frontend, the “whats” and “hows” of NFT mechanics, and the NFT metadata (mentioned several times in this article)

Smart Contract structure (and why I chose ERC-1155 over ERC-721)

When it comes to NFTs there are really only two widespread standards to choose from at the time of this writing — ERC-721 & ERC-1155. For the ScratchCollective contract, I chose to follow the ERC-1155 Multi-Token standard, developed Enjin CTO Witek Radomski which allows for both fungible and non-fungible tokens within the same deployed contract. I also leverage OpenZeppelin’s Contract library, with some slight modifications to suit the app’s needs.

Full ScratchCollective Contract

Here is the contract, written in Solidity — let’s quickly run through some of the function and variable declarations:

Solidity compiler version and imports

Line 3 — Pragma Solidity tells the compiler which compiler version(s) to use.

Lines 5–7 import pre-built solidity code from OpenZeppelin’s contract libraries. Here I use the basic ERC1155 contract, Counters (safe method to increment or decrement a counter), and Ownable (provides access control mechanism to specific functions).

Declaring and constructing

Line 10 declares the contact name and inherits from the ERC1155 & Ownable contracts that were imported in lines 5–7.

Lines 11–13 invoke the Counters contract to be used in the rest of the contract and declare _tokenIdTracker to be a counter variable.

Line 15 initializes the artSupply variable at zero.

Line 16 creates a mapping [ link ] of tokenIds (uint256) to original creator addresses.

*each token ID represents a unique NFT

Line 17 creates another mapping of tokenIds (uint256) to an IFPS content identifier (CID) that hosts the NFT’s metadata.

Line 19 is the contract constructor, and initializes the IPFS URL as the baseURL.

addNewArt

The addNewArt function essentially mints a new NFT. It takes an IFPS CID as input (we’ll see how this is done in the front end in the next article).

Line 23 adds the caller’s address to the original_creator mapping for the current tokenId.

Line 24 adds the IPFS CID input to the ipfs_meta mapping for the current tokenID

*Lines 23–24 are how both the original creator and metadata are tracked for each NFT

Line 25 mints the NFT

Line 26 increments the artSupply count

Line 27 increments the current tokenID tracker for the next NFT

uri

The uri function is a view function that returns the NFT’s URL, which in this case is an IPFS address that hosts the NFT’s metadata. It takes in the tokenID to identify which NFT we are querying.

Line 32 ensures the NFT exists and returns an error message if it doesn’t.

Line 33 returns the concatenation of the base URL and the ipfs CID (e.g. “https://ipfs.io/ipfs/” + “QmShnMozJ9RB75paDCUyPWNVoQsETPb6C1aqPEadAGTNRD”) which together make a URL link to the metadata

updateBaseURI

The updateBaseUri function is only available to the owner of the contract (me in this instance) and allows the owner to change the baseURI. This is in case something something happens with the host / IPFS gateway provider and I need to change it to a working one.

strConcat

The strConcat function deserves a bit of attention. Solidity does not include a native way of concatenating strings, much like other languages. In this case I had to create my own function to be used in the uri function mentioned above. Basically we turn the strings into bytes, put them together and then turn the result back into a string.

safeTransferFrom

safeTransferFrom is a function that OpenZeppelin already provides for us in it’s standard ERC-1155 contract that was imported into this contract ( lines 5 and 10). I include it here in order to limit the number of each token to 1 (to make this a purely non-fungible token contract). The “override” keyword on Line 52 means I am overriding or modifying another function that’s already declared.

_exists

The _exists function is used in the uri function to make sure the tokenID is valid. It returns a boolean (true or false) for whether an address exists in the original_creator mapping at the specified tokenId.

Design Rationale

One of the selling points of ERC-1155 over ERC-721, besides the flexibility of asset type (fungibility), is that ERC-1155 allows for multiple operations on a single transaction (e.g. batch transfers), which is less time-consuming and resource intensive than doing a single operation for each transaction. Here is a good article on some of the differences.

My rationale here though, was a slightly different. Here is the gist of it:

  1. Satisfying my curiosity in using the ERC-1155 (newer standard)
  2. Less modification to OpenZeppelin pre-vetted code (I’ll explain why)
  3. Ability for batch transfers

Number 1 is easy enough to envision. I’ll go over the other two.

Number 2 - Less modification to the OpenZeppelin contract. Much of this stems from my decision to use IPFS to host the NFT metadata. As I will discuss in the next article, NFTs represent ownership of an asset by pointing to a hosted URL containing the metadata that pertains to that asset. In layman’s terms NFTs don’t point to the actual asset, it points to data regarding that asset. The data is usually in the form a JSON object, and looks something like this:

Example of metadata from OpenSea Creatures

Where this data is hosted is important, as you’ll see in the next article. In the spirit of crypto and decentralization, I decide to use IPFS to host both the image and the metadata (as opposed to a centralized host like AWS). I also wanted the URI to be stored on the contract.

The ERC-721 contract has built in tokenURI function to store/retrieve the URI, but uses the baseURI + the token ID as the full URI.

In this case I wanted the baseURI + the IPFS CID so I decided using ERC-1155 would be easier as it doesn’t have a tokenURI function built in. This is because the ERC-1155 standard is meant to house both fungible and non-fungible tokens, and having a token URI for each fungible token (which could number in the billions) wouldn’t be feasible. However, since I am limiting each token here to 1 (non-fungible) creating my own tokenUri / uri function in this case works.

Number 3 — Batch Transfers. I also wanted there to be a batch transfer function, in case someone decided to give their collection away or for any number of reasons. I thought having that functionality would be useful.

Conclusion

Writing contracts can be confusing at times as it is still a fairly new language and being improved on continuously. There is a good chance that issues you face while writing in Solidity are fairly new and not very well documented. So a bit of tinkering is required. I use Remix and Truffle for development and once in a while they have problems of their own as well. One thing that really helped me was really running though all the contracts on OpenZeppelin. They provide kick-ass pre-built contract templates and also have a very active and helpful community forum. Highly recommended for both problem solving and for learning.

I hope this article helps others to get started and hope it really delivers the message of “if I can do it, you can too”.

Happy coding!

Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing

Also Read

--

--

Tobias Fan
Coinmonks

Cryptos Biggest Fan | Defi + Web3 @LunarCrush