Building a web3 RPG Part 1: Deploy an ERC-1155 Multi-Token Smart Contract

Learn how to deploy a simple ERC-1155 smart contract using MetaMask, Infura, Linea and Hardhat to power a web3 RPG.

Building a web3 RPG Part 1: Deploy an ERC-1155 Multi-Token Smart Contract

Learn how to deploy a simple ERC-1155 smart contract using MetaMask, Infura, Linea and Hardhat to power a web3 RPG.

Heyo, this is Emily, your friendly developer advocate from Linea, here to get you excited about web3 gaming! In this tutorial, we’ll be talking about the ERC-1155 Multi-Token standard and how it gives developers the flexibility of supporting multiple fungible, semi-fungible, and non-fungible tokens in a single contract: allowing builders to gas-efficiently create new use cases to build out the Linea ecosystem. At the end, we’ll deploy a straightforward ERC-1155 contract using MetaMask, Infura, Linea and Hardhat to power the beginning of a cat-centric Web3 RPG.

Prerequisites:

The What’s and Why’s of ERC-1155

In order to explain ERC-1155, it’s easier to start by explaining the limitations of ERC-721s and ERC-20s in the context of Web3 games.

In a standard RPG, you might have different types of fungible tokens - gold, silver, and copper - as well as hundreds of types of non-fungible tokens - swords, armor, and shields. In a world without ERC-1155, each one of these tokens would have to be a separate contract. Then, if you wanted to trade items and money, you’d have to initiate a separate transfer transaction for each individual token contract. Imagine how expensive that would be!

So, to trade 1 horse for 2 gold, 1 silver, and your finest sword, you’d have to:

  • Execute a transfer of 2 gold on the gold contract.
  • Execute a transfer of 1 silver on the silver contract.
  • Execute a transfer of the finest sword on the sword contract.
  • Execute a transfer of 1 horse from the horse contract.

That’s 4 transactions for conceptually just one trade (potentially 8 depending on how you decide to execute smart contract approvals for transfers)! When gas prices are high and transaction times are slow on Ethereum, imagine how expensive and painful that would be.

This is where ERC-1155 comes into play, allowing multiple tokens to be defined in a single contract and multiple tokens to be exchanged through a batch transfer in a single transaction.

WOW.

But wait, what’s this about semi-fungible tokens?

ERC-1155 is cool because it allows for the creation of semi-fungible tokens, which at its core is a token that has fungible and non-fungible properties.

For example, entry tickets to an event are often cited as semi-fungible tokens. Before the event date, entry tickets are essentially fungible - there are no unique properties as one entry ticket could be exchanged for another. However, once used it has the potential to be turned into some type of memorabilia.

In the game we’ll be building, one example of a semi-fungible token is what we’re calling a Schroedginer box. Initially, there is nothing unique about any of the boxes - they’re completely fungible. However, once opened, they can take on non-fungible properties - potentially becoming incredibly rare or incredibly common NFT (but I promise no dead cats).

How does metadata work?

If you’ve created an NFT before, you might be used to creating a new URI on IPFS with metadata for each NFT. In the case of ERC-1155, there’s an optional metadata extension, which relies on a substitution mechanism to use the same URI for all token types. Because we are only storing on URI, we can save significant gas costs. This substitution mechanism allows clients to replace an {id} property in order to represent all the different metadata for each NFT.

We’ll go over different strategies and how to upload data to IPFS for multiple token types in part 2. For now, this serves as the end of the introduction, and we’ll get on to deploying a very simple contract!

Let’s get building!

In this tutorial, we’ll be deploying a contract on Linea, an EVM-equivalent ZK-rollup that helps lower gas fees and speed up transaction times. Because it’s EVM-equivalent, the process is no different from deploying on Ethereum!

You can find more information on the Linea website.

In order to deploy a smart contract, you’ll need to do a few things:

Create a Hardhat project

In order to get started, open up your terminal and create a new folder called “kitten-collect”. Navigate into this folder and run the following command to create a new Hardhat project.

npm init
npm install --save-dev hardhat
npx hardhat init

Select “Create an empty hardhat.config.js” with your keyboard and hit enter.

Write the smart contract

Rather than implement an ERC-1155 contract from scratch, we can use OpenZeppelin’s ERC-1155 contract as a base. To do so, go ahead and install OpenZeppelin’s contract library:

npm install @openzeppelin/contracts

Create a new directory called “contracts” and create a file inside the directory called “KittenCollectItems.sol”.

Now, to utilize the OpenZeppelin contract, add the import statement at the top of the file and have your contract extend ERC-1155 like so:

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";


// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;


contract KittenCollectItems is ERC1155{
constructor() public {
}
}

In this game, we’ll have three types of token assets:

  • CAT_NIP: This is a fungible token, representing our in-game currency that can be used to buy non-fungible and semi-fungible tokens in the game.
  • CAT_TOY: This is a non-fungible token, representing toys like scratching posts and laser pointers.
  • CAT: This is a non-fungible token, representing different cats you can own.
  • SCHROEDINGER_BOX: This a semi-fungible token, representing interchangeable, indistinguishable loot boxes that once opened, contain cat toys, cats (all living), or nothing at all.

As mentioned above, the metadata for each token needs to be associated with an ID. We’ll go over actually uploading and structuring your metadata in part 2, but for now, we’ll just put in a dummy string. Again, since this is the simplest iteration of our web3 RPG, we’ll also just associate a single unique ID with each token type. Change your code to look like this:

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";


// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;


contract KittenCollectItems is ERC1155{
uint256 public constant CATNIP = 0;
uint256 public constant CAT_TOY = 1;
uint256 public constant CAT = 2;
uint256 public constant SCHROEDINGER_BOX = 3;


constructor() ERC1155("https://kittencollect-example/api/item/{id}.json"){
}
}

Note that we removed the public visibility modifier on the constructor. If we were to leave it there, we would get a compilation error Warning: Visibility for the constructor is ignored. If you want the contract to be non-deployable, making it "abstract" is sufficient.

This is because constructors are only called once on contract deployment, so there’s no need to add a visibility modifier to the function.

In this tutorial, we’ll just be sending assets directly to the contract deployer by minting tokens upon contract deployment - specifically, 1000 catnip, 1 cat toy, 1 cat, and 5 Schroedinger boxes. In future iterations, we’ll move the minting mechanism into separate functions to mint specific tokens to player addresses as well.

In our case, since we’re minting multiple token types, OpenZeppelin offers a _mintBatch function in their implementation of ERC-1155 structured as follows:

_mintBatch(address to, uint256[] ids, uint256[] amounts, bytes data)

So, to mint our tokens, we’ll match our designated IDs to the amount we want to dispense to the address of the deployer:

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";


// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;


contract KittenCollectItems is ERC1155{
uint256 public constant CAT_NIP = 0;
uint256 public constant CAT_TOY = 1;
uint256 public constant CAT = 2;
uint256 public constant SCHROEDINGER_BOX = 3;


constructor() ERC1155("https://kittencollect-example/api/item/{id}.json"){
uint256[] memory ids = new uint256[](4);
ids[0] = CAT_NIP;
ids[1] = CAT_TOY;
ids[2] = CAT;
ids[3] = SCHROEDINGER_BOX;


uint256[] memory amounts = new uint256[](4);
amounts[0] = 1000;
amounts[1] = 1;
amounts[2] = 1;
amounts[3] = 5;
		
_mintBatch(msg.sender, ids, amounts, "");
}
}

Now that we’ve got the basics of an ERC-1155 contract down, let’s actually deploy it!

Write the migration script

In order for Hardhat to understand how to deploy a contract, we need to give it instructions under the script folder. Create a file called deploy.js and add the following code:

async function main() {
  const [deployer] = await ethers.getSigners();


  console.log("Deploying contracts with the account:", deployer.address);


  const token = await ethers.deployContract("Token");


  console.log("Token address:", await token.getAddress());
}


main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Deploy your smart contract

In order to deploy to a live network, you’ll need to add the Linea RPC URL to your hardhat.config.js. But, how do you get that?

First, you’ll need to sign up for an Infura account:

Then, create an API key, choosing Web3 API for your network type, and copy the API key - you’ll need that for later:

In order to deploy, you’ll need to add this API key and your wallet’s secret recovery phase into your hardhat.config.js. However, this is incredibly dangerous, as you don’t want to expose that information by accidentally uploading it to GitHub. Start by creating a .env file and a .gitignore file in your root directory.

Add the appropriate information to the .env file, inputting your secret recovery phrase as the mnemonic:

SECRET_RECOVERY_PHRASE=<YOUR SECRET RECOVERY PHRASE>
INFURA_API_KEY=<YOUR INFURA API KEY>

Then, make sure to add this line to your .gitignore:

.env

In order to use these environment variables in your hardhat.config.js and deploy, you’ll need to install:

npm i -D dotenv

Then, in your hardhat.config.js, add the following code:

require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config();

const { SECRET_RECOVERY_PHRASE, INFURA_API_KEY } = process.env;

module.exports = {
//...cont

Then, you’ll want to configure the Linea network under networks, using the new environment variables you’ve configured:

module.exports = {
solidity: "0.8.19",
networks: {
	linea: {
		url: `https://linea-goerli.infura.io/v3/${INFURA_API_KEY}`,
              accounts: [SECRET_RECOVERY_PHRASE]
	} 
     } 
};

Finally, we can deploy your contract! Remember that you’ll need to fund your wallet with Linea ETH, using either a faucet or bridging ETH from Goerli to Linea Goerli. Call this from the CLI:

npx hardhat run scripts/deploy.js --network linea

Start building on Linea with Infura

Congrats! You’ve deployed an ERC-1155 contract on Linea using Infura! If you get stuck or have questions, please find us in Discord. Also, don’t forget to follow us on Twitter to find out about part 2 of building a web3 RPG.