Skip to main content

9. Create and deploy ERC20 contract

For the purpose of this tutorial, players of Immutable Runner can create a new skin for their fox once they have collected three coins. This means that the Immutable Runner's coins and skins depend on each other.

In this tutorial section, you will create an ERC20 contract for Immutable Runner Token, representing the coins used in the game. The contract will also interact with the Immutable Runner Skin ERC721 contract you deployed in step 9.

Set up local contract development

You will use Hardhat to manage the smart contract development environment.

  1. Ensure that your local environment is configured to run Hardhat.
  2. Create a new Hardhat project.
    • When you run npx hardhat init, choose Create an empty hardhat.config.js.
  3. Install Hardhat’s toolbox plugin, which includes all commonly used packages when developing with Hardhat.
yarn add --dev @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-ignition @nomicfoundation/hardhat-ignition-ethers @nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-chai-matchers @nomicfoundation/hardhat-ethers @nomicfoundation/hardhat-verify chai@4 ethers hardhat-gas-reporter solidity-coverage @typechain/hardhat typechain @typechain/ethers-v6
  1. Install our smart contract preset library.
yarn add @imtbl/contracts.
  1. Install the dotenv package so that you can load environment variables from the .env file.
yarn add dotenv
  1. To get your Hardhat project working with Typescript, follow the instructions here.

Write the contract

You will create a simple ERC20 contract representing the coins the fox can collect in Immutable Runner. Although we won't be going into the details of Solidity code, there are a few essential logics that you should know about.

  • Immutable Runner Token doesn't have a fixed supply.
  • Tokens can only be minted by wallets with the minter role.
  • Tokens can only be burned by token holders (or those they have an allowance for, see ERC20Burnable.sol).
  • (Optional) The wallet address that deploys the contract can mint tokens.
  • A wallet can create an Immutable Runner Skin NFT by burning three Immutable Runner Token.
  • When creating or crafting a new skin, the Immutable Runner Token contract requires the Immutable Runner Skin minter role to mint a new skin.

First, create a new directory called contracts/ and create a file inside the directory called RunnerToken.sol. Paste the code provided below, and make sure to read the comments that explain the code's various sections.

contracts/contracts/RunnerToken.sol

// Copyright (c) Immutable Australia Pty Ltd 2018 - 2024
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@imtbl/contracts/contracts/access/MintingAccessControl.sol";
import "@imtbl/contracts/contracts/token/erc721/preset/ImmutableERC721.sol";

contract RunnerToken is ERC20, ERC20Burnable, MintingAccessControl {
// A reference to the Immutable Runner Skin contract for the craftSkin function to use.
// Note: Immutable Runner Skin contract simply extends ImmutableERC721, so we can set
// the type to ImmutableERC721
ImmutableERC721 private _skinContract;

constructor(
address skinContractAddr
) ERC20("Immutable Runner Token", "IMR") {
// Grant the contract deployer the default admin role: it will be able
// to grant and revoke any roles
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
// Save the Immutable Runner Skin contract address
_skinContract = ImmutableERC721(skinContractAddr);

// Uncomment the line below to grant minter role to contract deployer
// _grantRole(MINTER_ROLE, msg.sender);
}

// Mints the number of tokens specified
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}

// Burns three tokens and crafts a skin to the caller
function craftSkin() external {
uint256 numTokens = 3 * 10 ** decimals();
require(
balanceOf(msg.sender) >= numTokens,
"craftSkin: Caller does not have enough tokens"
);

// Burn caller's three tokens
_burn(msg.sender, numTokens);

// Mint one Immutable Runner Skin to the caller
// Note: To mint a skin, the Immutable Runner Token contract must have the
// Immutable Runner Skin contract minter role.
_skinContract.mintByQuantity(msg.sender, 1);
}
}

Open hardhat.config.ts and update the file with the following code.

hardhat.config.ts

import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';

import * as dotenv from 'dotenv';

dotenv.config();

const config: HardhatUserConfig = {
solidity: {
version: '0.8.19',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
};

export default config;

To compile the contract, run npx hardhat compile.

💡Testing
You should write tests for the contract. However, since this tutorial does not cover testing, please refer to our sample code or Hardhat’s documentation on how to do so.

Deploy the contract

To deploy your new smart contract to the Immutable zkEVM Testnet using Hardhat, you must configure the network in the hardhat.config.ts file.

hardhat.config.ts

// omitted

const config: HardhatUserConfig = {
solidity: {
version: '0.8.19',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
immutableZkevmTestnet: {
url: 'https://rpc.testnet.immutable.com',
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
};

// omitted

Rename the .env.example file to .env and add the private key of the admin wallet you created in step 9 to PRIVATE_KEY. See here for instructions on how to get your Metamask wallet private key.

.env

PRIVATE_KEY=YOUR_ADMIN_WALLET_PRIVATE_KEY

Create a new directory named scripts/ and a new file named deploy.ts for the deployment script.

scripts/deploy.ts

import { ethers } from 'hardhat';

async function main() {
// Load the Immutable Runner Tokencontract and get the contract factory
const contractFactory = await ethers.getContractFactory('RunnerToken');

// Deploy the contract to the zkEVM network
const contract = await contractFactory.deploy(
'YOUR_IMMUTABLE_RUNNER_SKIN_CONTRACT_ADDRESS' // Immutable Runner Skin contract address
);

console.log('Contract deployed to:', await contract.getAddress());
}

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

Replace YOUR_IMMUTABLE_RUNNER_SKIN_CONTRACT_ADDRESS with the contract address of your Immutable Runner Skin contract. You can get the address from the Immutable Hub.

Hub Immutable Runner Skin

Run the deployment script to deploy the contract to the Immutable zkEVM test network.

npx hardhat run --network immutableZkevmTestnet scripts/deploy.ts

When the script executes successfully, note the deployed contract address that is returned. An example is provided below.

Contract deployed to: 0xDEPLOYED_CONTRACT_ADDRESS

(Optional) Verify contract

💡Grant minter role to admin wallet
If you have uncommented the _grantRole(MINTER_ROLE, msg.sender) line in RunnerToken.sol, which grants the minter role to the contract deployer or your admin wallet, you can skip Verify contract and Grant admin wallet the minter role steps.

When you verify a contract, its source code becomes publicly available on block explorers like Etherscan and Blockscout. To do this, you will need an Etherscan API key, which you can obtain by signing up on their website. After that, update the ETHERSCAN_API_KEY field in the .env file with your API key.

Also, the Immutable zkEVM Testnet chain is unsupported by default. Therefore, you'll need to add it as a custom chain.

💡Chain IDs
You can find the full list of chain IDs here.

hardhat.config.ts

// omitted

const config: HardhatUserConfig = {
solidity: {
// omitted
},
networks: {
// omitted
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
customChains: [
{
network: 'immutableZkevmTestnet',
chainId: 13473,
urls: {
apiURL: 'https://explorer.testnet.immutable.com/api',
browserURL: 'https://explorer.testnet.immutable.com',
},
},
],
},
};

// omitted

Run the verify task by passing the address of the contract you just deployed, the Immutable zkEVM network and the constructor argument used to deploy the contract, i.e. the Immutable Runner Skin contract address.

npx hardhat verify --network immutableZkevmTestnet DEPLOYED_CONTRACT_ADDRESS "YOUR_IMMUTABLE_RUNNER_SKIN_CONTRACT_ADDRESS"

Upon successful execution, you will receive a link to view your verified contract on the block explorer.

Successfully verified contract RunnerToken on the block explorer.
https://explorer.testnet.immutable.com/address/0xDEPLOYED_CONTRACT_ADDRESS#code

(Optional) Grant admin wallet the minter role

💡Grant minter role to admin wallet
If you have uncommented the _grantRole(MINTER_ROLE, msg.sender) line in RunnerToken.sol, which grants the minter role to the contract deployer or your admin wallet, you can skip Verify contract and Grant admin wallet the minter role steps.

Once your contract is verified, grant your admin wallet minter role:

  1. Enter your contract address into the Immutable Testnet block explorer search field.
  2. Click on the Contract tab and then the Write contract button.
  3. Click Connect wallet and choose Metamask to connect your admin wallet.
Block explorer connect wallet
  1. You’ll be prompted to connect to the block explorer. Click Connect to continue.
Block explorer metamask connect walletBlock explorer connected
  1. Click grantMinterRole, enter the address of your admin wallet and click Write.
Block explorer grant minter role
  1. To grant the minter role, you need to sign a transaction with your wallet to allow your admin wallet to mint tokens in the collection. Click Confirm to proceed.
Block explorer metamask sign grant minter roleBlock explorer grant minter role

Grant Immutable Runner Token contract the Immutable Runner Skin minter role

As mentioned, when creating or crafting a new skin, the Immutable Runner Token contract requires the Immutable Runner Skin minter role to mint a new skin. To do this, the process is essentially the same as the previous step.

  1. Enter your Immutable Runner Skin contract address into the Immutable Testnet block explorer search field.
  2. Click on the Contract tab and then the Write contract button.
💡Note
The contract is automatically verified when deploying a contract via the Immutable Hub. Therefore, you do not need to verify the Immutable Runner Skin contract you deployed in step 9.
  1. Click grantMinterRole, enter the address of your Immutabler Runner Token contract address and click Write.
  2. You will be prompted to approve a transaction to grant the minter role. This step will require gas fees and enable your Immutable Runner Token contract to mint skins. Click Confirm to grant the minter role.

Your contracts are now ready to be integrated and be used as in-game assets.