NFT Smart Contract Development – Road to OpenSea (Part 1)

In my last blog post I talked about how NFTs work inside. Now I will show you how to create your own NFT contract from scratch, test it and make it compatible with the biggest marketplace called OpenSea.

In this part we focus on the Remix IDE, ERC11155 Smart Contract development and Smart Contract testing.

Preview

Preview of the NFT we gonna build (Link to OpenSea)

Currently we are hosting a Customer Engagement Initiative regarding this topic.
If you are interested read the business perspective blog from by colleagues or to join us directly, please register
here.

Background

In the last blog post I explained to you that there are essentially two (Non fungible Token) NFT standards. One is ERC721 and the other is ERC1155. While the former is exclusively for NFTs i.e., unique and therefore non-exchangeable items, the ERC1155 can be used for NFTs as well as for Fungible Tokens (FTs). Since the ERC1155 is more advanced, it is perceived as the successor to the ERC721. Accordingly, in this article I will show you how to develop an ERC1155 compliant NFT contract.

Getting Started

In this multi part blog, I’ll show you step-by-step how to code your own animated NFT, test it by using the frameworks Chai and Mocha, deploy it on the Testnet Blockchain called Rinkeby, and finally see it on OpenSea. At some points in the development, I will also mention security features.

To understand the blog you should be able to read code, but it is not important to know solidity in detail, because most things should be self-explanatory.

Remix Setup

To begin coding, we first need a development environment. The official Ethereum IDE is called Remix and runs entirely in the web browser. This IDE is a good start to build our smart contract.

When you open remix, you will already see a bunch of files. We don’t really need them. Therefore, we go ahead and create a new blank workspace by clicking on the plus symbol on the upper left. We will call it BlogNFT (See figure 1). Then we create two folder contracts and tests. In the contracts folder we create a file called BlogNft.sol (See figure 2). The extension sol stands for solidity, the programming language of the smart contracts.

Create%20Remix%20Workspace

Figure 1: Create Remix Workspace

Figure%202%3A%20Workspace%20Overview

Figure 2: Workspace Overview

Begin Coding

Let’s fill our BlogNft.sol file with some code. In each solidity file, the compatible compiler version should be specified. It is important to choose a version >= 8. This is because in previous versions no numerical overflow check was performed by default. Before you had either a security hole, had to check it yourself or had to rely on external libraries like SafeMath. As of version 8, the check is now standard. So, lets stick with Version 0.8.15 as this the latest version available at the time of writing this blog.

//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

As you probably know, there are many reusable building blocks in software development. This is no different in the smart contract world. In particular, it is even advisable to use ready-made building blocks from trusted sources, since errors in the smart contract cannot be patched, since the blockchain is immutable (It is possible to simulate patching by using proxies, but this is not of relevance for this post).

One code source that is trusted a lot in smart contract development is OpenZeppelin, which basically acts as the solidity standard library. Of course, they also have something up their sleeve for our Nft contract, namely an ERC1155 base class. In addition, some pleasant security features such as the ownable modifier with which a function can be equipped so that it can only be called by the owner of the SmartContract. More about this later. Let’s go ahead and import everything we need from OpenZeppelin. Within remix we can import OpenZeppelin directly, because by default the corresponding NPM package is already installed. For local development you would have to add the packages manually at this point.

//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

Now we define the contract and let it inherit from the OpenZepplin ERC1155 implementation. Within the ERC1155 constructor a string is required to store the metadata URL. More about this in the next part.

//SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.15; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol"; contract BlogNft is ERC1155, Ownable { constructor() ERC1155("") { }
}

Minting

Minting is the process of producing coins or in our case FTs/NFTs. So let’s include a corresponding function mint(..) to produce tokens. As written in the last blog post, a mapping from TokenId to Address to Amount is maintained within the OpenZepplin implementation, accordingly the _mint(…) method provided internally by OpenZepplin requests exactly these values. As you can see the function is on the one hand public and on the other hand equipped with the onlyOwner modifier. Internally, the sender address, i.e. the creator of the contract, is automatically stored when the contract gets initialized/deployed. When using the onlyOwner modifier, it is ensured that the caller corresponds to the creator of the contract. In this case, only the contract owner is allowed to mint new tokens.

//SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.15; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol"; contract BlogNft is ERC1155, Ownable { constructor() ERC1155("") { } function mint(uint256 tokenId, uint256 amount) onlyOwner public { _mint(msg.sender, tokenId, amount, ""); }
}

VM Deployment

Using remix, we can quickly and easily deploy our contract in a virtual machine. This way there are no costs, and you can see the behavior of the contract. To deploy, we switch the tab on the left to the fourth icon, select the correct contract and click deploy (see figure 3). The contract appears at the bottom as shown in step 4. We see many methods that are provided by the OpenZeppelin ERC1155 implementation as well as our own mint method.

To test the functionality, we can now for example mint 10 tokens of token id 1 and then check the balance of the account. You can find the account address above under “account”. In the console we see that our account has 10 tokens.

Figure%203%3A%20Contract%20Deployment

Figure 3: Contract Deployment

Add useful Functionalities

A ft/nft is characterized by the fact that there is a maximum number of tokens. In the case of a nft this max amount is always 1.

To implement this functionality, we need some more code. For this we first introduce a struct which contains information of a token like the mentioned maximum amount of a token. In addition to the Struct we need a mapping so that we can retrieve the information per token id.

To keep track the current token supply and thus check if we are allowed to mint more tokens, we use an OpenZeppelin implementation.  As said before, using the standard implementations help to prevent errors. Therefore, we go ahead and import and inherit the OpenZeppelin ERC1155Supply contract. This gives us a function called totalSupply which returns the number of minted tokens for a token id. Since the ERC1155Supply contract has a _beforeTokenTransfer method just like the ERC1155 contract, solidity requires that we explicitly override it.
The reason why the _beforeTokenTransfer function is used at all is that minting is nothing else then a transfer starting from address 0. So the ERC1155Supply can track the supply in this method properly.

The last step is to add a method to register new tokens and allow them to be minted. At the same time, we need to check in our mint method that the token should exist and that the maximum number of tokens has not been exceeded. A check using the require keyword is always a good idea, because this way the transaction is reverted, and we can directly pass a revert reason.

//SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.15; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol"; contract BlogNft is ERC1155, Ownable, ERC1155Supply { struct TokenInfo { bool exists; // A solidity mapping holds a value for any key. Thus an empty struct is returned if a key actually does not exist. With the flag we make sure that a token really exists. uint256 maxAmountAllowed; // Max amount of mintable tokens } mapping(uint256 => TokenInfo) tokenInfos; // Mapping from token id to token info constructor() ERC1155("") {} function add(uint256 tokenId, uint256 maxAmountAllowed) public onlyOwner { require(!tokenInfos[tokenId].exists, "Token with given id already exists"); // Ensure we can only add and not override tokenInfos[tokenId] = TokenInfo(true, maxAmountAllowed); // Add token informations for token id } function mint(uint256 tokenId, uint256 amount) public onlyOwner { TokenInfo memory tokenInfo = tokenInfos[tokenId]; // Get token information for token id require(tokenInfo.exists, "Token with given id does not exists"); // Ensure token id is allowed to be minted uint256 minted = super.totalSupply(tokenId); // Get amount of already minted tokens from ERC1155Supply require((minted + amount) <= tokenInfo.maxAmountAllowed, "To many tokens"); // Prevent minting more than allowed _mint(msg.sender, tokenId, amount, ""); // Mint token } // Override required by solidity because ERC1155 and ERC1155Supply define this function function _beforeTokenTransfer( address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) internal override(ERC1155, ERC1155Supply) { super._beforeTokenTransfer(operator, from, to, ids, amounts, data); }
}

Afterwards we must recompile the contract so that remix notices that we want to deploy the latest changes. For this we go to the third remix tab and click on compile (see Figure 4). The deployment happens as described before.

Figure%204%3A%20Recompiling%20our%20Contract

Figure 4: Recompiling our Contract

Testing

To test the contract you can of course deploy it again in the virtual machine and test it by hand, but you don’t want to do that as the number of functions grows. So we take care of automated testing. To do so we use hardhat as environment and the well-known libs mocha and chai for testing. Because solidity tests can be developed easily in JavaScript, we create a corresponding javascript file called BlogNft.test.js in our test folder.

The usage is very straight forward. We import our libraries as we usually do in javascript. After that we can organize our tests with the describe function. The actual tests are written in the it blocks. Since we want to interact with our contract every time, we specify a beforeEach method that loads the interacting users using the ether module of the hardhat environment before each test. Then we create an instance of our contract using the factory pattern of hardhat. For this we only need to pass the name of our contract. Afterwards we can deploy the contract and store it in the variable contract which can be used to interact with the deployed contract instance during the test.

As you can see in the tests, we first connect to the contract using the “connect” keyword and can thus set the interacting account. Then we can call a function of the contract. The function name and function parameters are the same as in the solidity code. A very handy feature is that we can easily test our require statements by matching the revert reason.

const { expect } = require("chai");
const { ethers } = require("hardhat"); describe("BlogNft", function () { let contract = null; // Contract let ownerUser = null; // Owner of the contract let otherUser = null; // Some other user beforeEach(async function() { // Get interacting ether account [ownerUser, otherUser] = await ethers.getSigners(); // Load factory for our contract const contractFactory = await ethers.getContractFactory("BlogNft"); // Deploy the contract contract = await contractFactory.deploy(); // Ensure it is deployed await contract.deployed(); }); it("Prevent add token twice", async function () { // Add a token that is allowed to be minted await contract.connect(ownerUser).add(1, 10); // Ensure the same request would be reverted await expect(contract.connect(ownerUser).add(1, 10)).to.be.revertedWith('Token with given id already exists'); }); it("Add and mint multiple tokens", async function () { await contract.connect(ownerUser).add(1, 10); // Allow 10 tokens of id 1 await contract.connect(ownerUser).mint(1, 5); // Mint 5 of id 1 await contract.connect(ownerUser).add(2, 1); // Allow 1 token of id 2 await contract.connect(ownerUser).mint(2, 1); // Mint 1 of id 2 }); it("Ensure mintablity", async function () { const tokenId = 1; await contract.connect(ownerUser).add(tokenId, 10); await contract.connect(ownerUser).mint(tokenId, 5); // Mint 5 await expect(await contract.connect(ownerUser).balanceOf(ownerUser.address, tokenId)).to.equal(5); await contract.connect(ownerUser).mint(tokenId, 5); // Mint 5 more await expect(await contract.connect(ownerUser).balanceOf(ownerUser.address, tokenId)).to.equal(10); }); it("Prevent minting of non existing token", async function () { const tokenId = 1; await expect(contract.connect(ownerUser).mint(tokenId, 5)).to.be.revertedWith('Token with given id does not exists'); }); it("Prevent minting more than allowed", async function () { const tokenId = 1; await contract.connect(ownerUser).add(tokenId, 10); await expect(contract.connect(ownerUser).mint(tokenId, 10 + 1)).to.be.revertedWith('To many tokens'); }); it("Ensure only owner can mint", async function () { const tokenId = 1; await contract.connect(ownerUser).add(tokenId, 10); await expect(contract.connect(otherUser).mint(tokenId, 10)).to.be.revertedWith('caller is not the owner'); }); it("Ensure only owner can add", async function () { const tokenId = 1; await expect(contract.connect(otherUser).add(tokenId, 10)).to.be.revertedWith('caller is not the owner'); });
});

To execute the test, you press Strg+Shift+S. You can see the test results in the console (see figure 5).

Figure%205%3A%20Test%20Execution

Figure 5: Test Execution

What comes Next?

In the next part we build our animated NFT and deploy it in a decentralised way using IPFS. We also modify our contract such that we can specify the metadata on token and contract level to display the images and descriptions used by OpenSea. Afterwards i will show you how to use metamask to deploy our contract on the testnet chain.

If you are interested in part two, click on my profile to get there.