NFT: Multi-Token

NFT: Multi-Token

Wednesday Feb 21, 2024

Multi-Token Standard (ERC-1155)

In this course, we will learn about the ERC-1155 standard, which is a multi-token standard that allows for creation of multiple tokens within a single smart contract.

What and why ERC-1155?

When deploying an NFT in the form of an ERC721 token, you can only have one "set" of tokens per contract. For instance, if you created a game where you had 10 different types of weapons, you would need to deploy 10 different contracts to represent each weapon type.

Now you might think that 10 contracts isn't that bad, but what if you had 1000 different types of weapons? Or 10,000? You can see how this can quickly become a problem and how it can become very expensive to deploy and maintain all of these contracts.

So the ERC-1155 standard was created to solve this problem. It allows you to create multiple tokens within a single contract. This means that you can have 10,000 different types of weapons within a single contract. This is a huge improvement over the ERC-721 standard.

ERC-1155 vs ERC-721

A simple diagram to show the difference between the ERC-1155 and ERC-721 standards:

ERC721 vs ERC1155 by Timo Klaasee

Setting up our project

Before we start, we need to create a few files.

In the contracts folder:

  1. Create a new file called ERC1155.sol.

In the test folder:

  1. Create a new file called ERC1155.ts.

Writing our smart contract

In the ERC1155.sol file, add the following code:

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.18;
3
4import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
5import "@openzeppelin/contracts/access/Ownable.sol";
6
7contract ERC1155Token is ERC1155, Ownable {
8    /**
9     * The token contstructor
10     * @param baseURI The base token URI
11     * @dev The token URI is following the ERC-1155 standard 
12     * in which we return the URI with `{id}` as a placeholder for the token ID.
13     * Example: https://token-cdn-domain/{id}.json
14     * It is up to the client to replace the `{id}` placeholder with the actual token ID
15     */
16    constructor(address owner, string memory baseURI) ERC1155(baseURI) {
17        _transferOwnership(owner);
18    }
19
20    /**
21     * Mint a new token with the given token ID
22     * @param owner_ The owner of the token
23     * @param tokenId_ The token ID (Different from the token ID in ERC721)
24     * @param amount_ The amount of tokens to mint
25     * @param data_ Any extra data to pass to the token
26     */
27    function mint(
28        address owner_,
29        uint256 tokenId_,
30        uint256 amount_,
31        bytes memory data_
32    ) public onlyOwner {
33        _mint(owner_, tokenId_, amount_, data_);
34    }
35}

We're inheriting from the ERC1155 and Ownable contracts. The ERC1155 contract is the main contract that implements the ERC-1155 standard. The Ownable contract is used to make sure that only the owner of the contract can mint new tokens.

In terms of what exactly the contract does, you're welcome to read the ERC1155 contract now, but we will explore it more in future courses.

Writing our tests

In the ERC1155.ts file, add the following code:

1import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
2import { expect } from "chai";
3import { ethers } from "hardhat";
4
5describe("ERC721", function () {
6	async function deployERC1155Fixture() {
7		const [owner, account_1] = await ethers.getSigners();
8		const ERC1155Token = await ethers.getContractFactory("ERC1155Token");
9		const erc1155Token = await ERC1155Token.deploy(owner.address, "https://example.com/{id}.json");
10		return { erc1155Token, owner, account_1 };
11	}
12
13	describe("Deployment Tests", function () {
14		it("Should set the right owner", async function () {
15			const { erc1155Token, owner } = await loadFixture(deployERC1155Fixture);
16			expect(await erc1155Token.owner()).to.equal(owner.address);
17		});
18	});
19
20  describe("Minting Tests", function () {
21    it("Should mint a new token with the correct URI", async function () {
22			const { erc1155Token, owner } = await loadFixture(deployERC1155Fixture);
23      await erc1155Token.mint(owner.address, 0, 1, "0x00");
24			expect(await erc1155Token.balanceOf(owner.address, 0)).to.equal(1);
25			expect(await erc1155Token.uri(0)).to.equal("https://example.com/{id}.json");
26		});
27  });
28});

So let's have a look at what we did here:

  1. We created a fixture called deployERC1155Fixture that deploys our ERC1155 contract. It sets the owner of the contract to the first signer and sets the base URI to https://example.com/{id}.json.

  2. We created a test that checks if the owner of the contract is set correctly.

  3. We created a test that checks if we can mint a new token and if the URI is set correctly.

And that's it. We created a very basic ERC1155 token that can create multiple tokens within a single contract!

Compiling and running our smart contract

To run our tests and make sure everything works, run the following command in your terminal:

1npx hardhat test

Now we can deploy our smart contract to the testnet.

Deploying our smart contract

To deploy our smart contract, we need to create a deployment script. In the scripts folder, create a new file called deploy-erc1155.ts and add the following code:

1// We require the Hardhat Runtime Environment explicitly here. This is optional
2// but useful for running the script in a standalone fashion through `node <script>`.
3//
4// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
5// will compile your contracts, add the Hardhat Runtime Environment's members to the
6// global scope, and execute the script.
7import hre from "hardhat";
8
9const config = {
10  owner_address: "your address here",
11  base_uri: "ipfs://QmfAJoYp518dvdRg87jMeTMtzzs7AzXS4XjasfhTTLcWyB/{id}.json",
12}
13
14async function main() {
15  const { owner_address, base_uri } = config;
16
17  console.log(`Deploying ERC-1155 to owner: ${owner_address}...`);
18
19  const Token = await hre.ethers.getContractFactory("ERC1155Token");
20  const token = await Token.deploy(
21    owner_address,
22    base_uri
23  );
24
25  console.log(`Deployed token. Owner: ${owner_address}`);
26
27  console.log("Waiting 1 minute for Etherscan to index the contract...");
28  await new Promise((r) => setTimeout(r, 60000));
29
30  console.log("Verifying contract...");
31  await hre.run("verify:verify", {
32    address: token.getAddress(),
33    constructorArguments: [
34      owner_address,
35      base_uri
36    ],
37  });
38
39  console.log("Contract verified! 🎉");
40  console.log(
41    `Please find the verified contract on Etherscan: https://sepolia.etherscan.io/address/${token.getAddress()}`
42  );
43}
44
45// We recommend this pattern to be able to use async/await everywhere
46// and properly handle errors.
47main().catch((error) => {
48  console.error(error);
49  process.exitCode = 1;
50});

happy

Congratulations! You've just created your first ERC1155 token!

1npx hardhat run scripts/deploy-erc1155.ts --network sepolia

Conclusion

In this course, we learned about the ERC-1155 standard and how it can be used to create multiple tokens within a single contract. We also learned how to create a basic ERC1155 token and how to deploy it to the testnet.