Skip to main content
ESC

Create an ERC-20 Token

This tutorial walks you through creating and deploying an ERC-20 token on Elastos Smart Chain (ESC). ESC is a geth fork: Chain ID 20 on mainnet and 21 on testnet. Standard Ethereum tooling (MetaMask, Hardhat, OpenZeppelin) works the same as on any EVM chain.

EVM compatibility

ESC is fully EVM-compatible. Patterns from general Ethereum and Solidity tutorials apply directly; only network RPC URLs, chain IDs, and explorers differ.

Prerequisites

  • Node.js (LTS recommended)
  • MetaMask or another Web3 wallet
  • Basic familiarity with the terminal

Step 1: Set Up MetaMask for ESC Testnet

Network Configuration

For MetaMask and wallet network setup, see Add Network to Wallet.

Save the network and switch to it before deploying.

For mainnet deployments, use the same fields with: Network name Elastos Smart Chain, RPC https://api.elastos.io/esc, Chain ID 20, currency ELA, explorer https://esc.elastos.io/.

Add ESC from a dApp (optional)

You can prompt MetaMask (or any EIP-1193 wallet) to add the chain without manual entry using wallet_addEthereumChain. Copy the ESC_MAINNET_PARAMS / ESC_TESTNET_PARAMS objects and the addElastosNetwork helper from Add Network to Wallet so RPC URLs, explorers, and chain metadata stay in one place. For testnet, use the testnet params from that page (chain ID 0x15, testnet RPC and explorer).

Step 2: Get Testnet ELA

  1. Open the official faucet: https://esc-faucet.elastos.io/
  2. Paste your MetaMask address (the account you will use to deploy).
  3. Request 1 tELA (or the amount the faucet allows).

You need a small amount of tELA to pay gas on ESC Testnet.

Step 3: Create a Hardhat Project

From a directory where you keep projects, run:

mkdir my-token && cd my-token
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init

When prompted, choose Create a JavaScript project and accept the defaults for the rest (or adjust paths if your tooling requires it). This gives you a minimal Hardhat layout with contracts/, scripts/, and hardhat.config.js.

Step 4: Install OpenZeppelin

Install audited ERC-20 implementations:

npm install @openzeppelin/contracts

Step 5: Write the Token Contract

Create contracts/MyToken.sol with the following full contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
constructor(uint256 initialSupply) ERC20("My Token", "MTK") Ownable(msg.sender) {
_mint(msg.sender, initialSupply * 10 ** decimals());
}

function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}

The constructor mints the initial supply to the deployer (with 18 decimals, OpenZeppelin’s default). The owner can mint additional tokens via mint.

Step 6: Configure Hardhat for ESC

Replace or merge your hardhat.config.js so it matches this configuration (tooling imports and solidity version aligned with the contract):

require("@nomicfoundation/hardhat-toolbox");

module.exports = {
solidity: "0.8.20",
networks: {
escTestnet: {
url: "https://api-testnet.elastos.io/esc",
chainId: 21,
accounts: [process.env.PRIVATE_KEY],
},
escMainnet: {
url: "https://api.elastos.io/esc",
chainId: 20,
accounts: [process.env.PRIVATE_KEY],
},
},
};
Private keys and source control

Never commit private keys, seed phrases, or .env files that contain secrets to Git or any public repository. Treat any key used on mainnet as highly sensitive; rotate it if it may have been exposed.

Step 7: Write the Deploy Script

Create scripts/deploy.js:

const { ethers } = require("hardhat");

async function main() {
const initialSupply = 1000000;
const MyToken = await ethers.getContractFactory("MyToken");
const token = await MyToken.deploy(initialSupply);
await token.waitForDeployment();
console.log(`MyToken deployed to: ${await token.getAddress()}`);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

initialSupply is in whole tokens; the contract multiplies by 10 ** decimals() before minting.

Compile before deploying

Hardhat compiles automatically when you run scripts, but you can compile explicitly to catch errors early:

npx hardhat compile

You should see artifacts under artifacts/ and cache/ when compilation succeeds.

Step 8: Deploy to ESC Testnet

Export your deployer account’s private key (the 0x-prefixed hex string from MetaMask: Account details → Show private key). Do this only on a machine you trust, then run:

PRIVATE_KEY=your_private_key npx hardhat run scripts/deploy.js --network escTestnet

Replace your_private_key with your actual key. The script prints the deployed contract address when it succeeds.

Checklist before you run deploy:

  • MetaMask account matches the key you export (same address as funded on the faucet).
  • Network name in MetaMask is ESC Testnet (Chain ID 21).
  • You have enough tELA for gas (a simple deployment typically needs only a small fraction of 1 tELA).

If deployment fails with insufficient funds, return to Step 2 and request more tELA. If you see wrong network or chain ID errors, confirm chainId: 21 in hardhat.config.js and that your RPC URL is exactly https://api-testnet.elastos.io/esc.

Using a .env file

For local development, install dotenv (npm install --save-dev dotenv), add require("dotenv").config(); at the top of hardhat.config.js, and put PRIVATE_KEY=... in a .env file. Add .env to .gitignore so the key never leaves your machine in version control.

Example .gitignore entries:

node_modules
.env
.env.*
!.env.example

You can commit a .env.example with placeholder keys (no real secrets) so teammates know which variables to set.

Step 9: Verify on Blockscout

  1. Open the ESC Testnet explorer: https://esc-testnet.elastos.io/
  2. Search for your contract address from the deploy output.
  3. Open the contract page and use Verify & Publish (or the explorer’s equivalent) to submit the Solidity source and compiler settings so others can read verified code on-chain.

After verification, users can see your token name, symbol, and source in the explorer UI.

When filling the verification form, use Solidity version 0.8.20, match the optimizer settings to your Hardhat defaults (if you did not enable the optimizer, leave it off in the form), and paste the flattened source or the multi-file layout the explorer requests; many explorers accept standard JSON input exported from Hardhat for an exact compiler match.

Verify from the Hardhat CLI (optional)

If you use Hardhat’s verify task, configure Blockscout as a custom chain (no API key required). Example hardhat.config.ts excerpt:

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

const config: HardhatUserConfig = {
solidity: {
version: "0.8.20",
settings: {
optimizer: { enabled: true, runs: 200 },
},
},
networks: {
esc: {
url: "https://api.elastos.io/esc",
chainId: 20,
accounts: [process.env.DEPLOYER_PRIVATE_KEY!],
gasPrice: 1000000000, // 1 gwei — ESC often uses low gas prices
},
escTestnet: {
url: "https://api-testnet.elastos.io/esc",
chainId: 21,
accounts: [process.env.DEPLOYER_PRIVATE_KEY!],
},
},
etherscan: {
apiKey: {
esc: "no-api-key-needed",
escTestnet: "no-api-key-needed",
},
customChains: [
{
network: "esc",
chainId: 20,
urls: {
apiURL: "https://esc.elastos.io/api",
browserURL: "https://esc.elastos.io",
},
},
{
network: "escTestnet",
chainId: 21,
urls: {
apiURL: "https://esc-testnet.elastos.io/api",
browserURL: "https://esc-testnet.elastos.io",
},
},
],
},
};

export default config;

Then run (constructor args must match deploy — here one uint256 initial supply in whole tokens):

npx hardhat verify --network escTestnet <DEPLOYED_ADDRESS> 1000000

For mainnet, use --network esc and the mainnet explorer URL if you open verification in the browser: https://esc.elastos.io/address/<ADDRESS>/contract-verifications/new.

Call the contract from a script

After deployment, you can interact with the token using ethers and the contract address:

import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://api-testnet.elastos.io/esc");
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

const abi = [
"function mint(address to, uint256 amount)",
"function balanceOf(address) view returns (uint256)",
];
const contract = new ethers.Contract("0xYOUR_CONTRACT_ADDRESS", abi, wallet);

const tx = await contract.mint("0xRecipientAddress", ethers.parseEther("100"));
await tx.wait();

const balance = await contract.balanceOf("0xRecipientAddress");
console.log("Balance:", ethers.formatEther(balance));

Use the mainnet RPC URL when calling contracts on Chain ID 20.

Next steps

  • Transfer tokens: From MetaMask, send MTK to another address you control to confirm balances update on the explorer.
  • Import the token in MetaMask: Import tokens → Custom token, paste your contract address; symbol and decimals should resolve after the contract is deployed (decimals 18).
  • Mainnet: Deploy with --network escMainnet only after a security review and production key handling. Mainnet uses ELA for gas and Chain ID 20; double-check the RPC https://api.elastos.io/esc and your wallet balance before broadcasting.
  • Ownership: The deployer owns MyToken and can call mint. If you transfer ownership, use OpenZeppelin’s transferOwnership (exposed via Ownable) or extend the contract; never leave admin functions callable by arbitrary addresses in production without a clear trust model.

Summary: You configured ESC Testnet in MetaMask, funded with tELA, built a Hardhat project with OpenZeppelin ERC20 and Ownable, compiled and deployed to Chain ID 21, and verified the contract on Blockscout. The same workflow applies to other ERC-20 patterns from the Ethereum ecosystem by swapping in your contract and network names.