8. 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 7.
Set up local contract development
You will need to use Hardhat to manage the smart contract development environment.
The following code is already included. Please find it in the /contracts
subdirectory.
But if you would like to do it yourself, refer to the collapsible section below for instructions.
How to manually set up local contract development
- Ensure that your local environment is configured to run Hardhat.
- Create a new Hardhat project.
- When you run
npx hardhat init
, choose Create an empty hardhat.config.js.
- When you run
- 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
- Install our smart contract preset library.
yarn add @imtbl/contracts.
- Install the dotenv package so that you can load environment variables from
the
.env
file.
yarn add dotenv
- 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.
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
.
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.
// 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 7 to
PRIVATE_KEY
. See
here
for instructions on how to get your Metamask wallet private key.
PRIVATE_KEY=YOUR_ADMIN_WALLET_PRIVATE_KEY
Create a new directory named scripts/
and a new file named deploy.ts
for the
deployment script.
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.
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
_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.
// 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
_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:
- Enter your contract address into the Immutable Testnet block explorer search field.
- Click on the Contract tab and then the Write contract button.
- Click Connect wallet and choose Metamask to connect your admin wallet.
- You’ll be prompted to connect to the block explorer. Click Connect to continue.
- Click grantMinterRole, enter the address of your admin wallet and click Write.
- 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.
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.
- Enter your Immutable Runner Skin contract address into the Immutable Testnet block explorer search field.
- Click on the Contract tab and then the Write contract button.
- Click grantMinterRole, enter the address of your Immutabler Runner Token contract address and click Write.
- 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.