This concise hardhat tutorial has 3 sections and this is section 2.
1 . Installation and sample project
2 . Write ERC20 token with OpenZeppelin
3 . Write ERC72 NFT token with on-chain SVG image
Hardhat is an Ethereum development tool suite to compile, unit test, debug and deploy smart contracts.
2. Write an ERC20 Token with OpenZeppelin
In the hardhat official tutorial at https://hardhat.org/tutorial/ , it write an ERC20 Token smart contract from scratch. Here we will write one using OpenZeppelin
library instead. You can find more information about OpenZeppelin at: https://docs.openzeppelin.com/contracts/4.x/ . The sample contract is adapted from OpenZeppelin documents.
Step 1: Install OpenZeppelin
First, let's install OpenZeppelin contracts.
yarn add @openzeppelin/contracts
Step 2: Write an ERC20 Token with OpenZeppelin
You can write an ERC20 Token smart contract by inheriting OpenZeppelin ERC20 implementation. OpenZeppelin Wizard is a helpful tools to setup an ERC20 token contract.
Our smart contract GLDToken.sol
goes as follows:
// contracts/GLDToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract GLDToken is ERC20 {
constructor(uint256 initialSupply) ERC20("GLDToken", "GLD") {
_mint(msg.sender, initialSupply * 10 ** decimals());
}
}
Compile the contract:
yarn hardhat compile
Step 3: Write a deploy script
Create a deploy script which is adapted from https://docs.openzeppelin.com/learn/deploying-and-interacting . You can find a detailed explanation there.
// scripts/deploy_GLDToken.ts
import { ethers } from "hardhat";
async function main() {
const initialSupply = 10000;
const GLDToken = await ethers.getContractFactory("GLDToken");
const token = await GLDToken.deploy(initialSupply);
await token.deployed();
const totalSupply = await token.totalSupply()
console.log(
`GLDToken deployed to ${token.address} with an initialSupply ${totalSupply}`
);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Using this script, we deploy GLDToken with 10000.0 GLD minted to the contract owner.
Try to deploy it to in-process local blockchain:
yarn hardhat run scripts/deploy_GLDToken.ts
//Output:
GLDToken deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3 with an initialSupply 10000000000000000000000
Step 4: Write Unit Test
In the hardhat official tutorial, there is a detailed explanation of unit test at https://hardhat.org/tutorial/testing-contracts.html . We adapted the test script in it with several necessary changes.
Create file GLDToken.test.ts
:
// We import Chai to use its asserting functions here.
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";
describe("GLDToken", function () {
// We define a fixture to reuse the same setup in every test.
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
const initialSupply = 10000;
async function deployGLDTokenFixture() {
// Contracts are deployed using the first signer/account by default
const [owner, otherAccount] = await ethers.getSigners();
const GLDToken = await ethers.getContractFactory("GLDToken");
const token = await GLDToken.deploy(initialSupply);
return { token, owner, otherAccount };
}
describe("Deployment", function () {
it("Should assign the total supply of tokens to the owner", async function () {
const { token, owner } = await loadFixture(deployGLDTokenFixture);
const total = await token.totalSupply();
expect(total).to.equal(await token.balanceOf(owner.address));
});
});
describe("Transaction", function () {
it("Should transfer tokens between accounts", async function () {
const { token, owner, otherAccount } = await loadFixture(deployGLDTokenFixture);
const ownerBalance = await token.balanceOf(owner.address);
await token.transfer(otherAccount.address, 50);
const addr1Balance = await token.balanceOf(otherAccount.address);
expect(addr1Balance).to.equal(50);
const ownerNewBalance = await token.balanceOf(owner.address);
expect(ownerNewBalance).to.equal(ownerBalance.sub(50));
});
it("Should fail if sender doesn’t have enough tokens", async function () {
const { token, owner, otherAccount } = await loadFixture(deployGLDTokenFixture);
// Transfer 10001 GLD tokens from owner to otherAccount
await expect(
token.transfer(otherAccount.address, ethers.utils.parseEther('10001'))
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
});
});
});
Run unit test:
yarn hardhat test test/GLDToken.test.ts
Output:
GLDToken
Deployment
✔ Should assign the total supply of tokens to the owner (597ms)
Transaction
✔ Should transfer tokens between accounts
✔ Should fail if sender doesn’t have enough tokens
3 passing (630ms)
Step 5: Deploy smart contract to localhost and interact with it
By compiling, testing and deploying in in-process blockchain, we now have a smart contract which works correctly. We would like to deploy it to stand-alone localhost blockchain and interact with it interactively from Hardhat console.
Open another terminal and run in the project directory:
yarn hardhat node
In your working terminal, deploy the contract to localhost by running:
yarn hardhat run scripts/deploy_GLDToken.ts --network localhost
//Output:
// GLDToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Please note that you need to set localhost network in hardhat.config.ts
.
Run hardhat console connecting to localhost:
yarn hardhat console --network localhost
Interact with GLDToken smart contract instance in hardhat console as follows. You need address and ABI to interact with a contract. The ABI(Application Binary Interface) is retrieved by Hardhat-ethers plugin's ethers.getContractAt("GLDToken", address)
using the contract name here.
Get a smart contract instance and retrieve read-only data:
fromWei = ethers.utils.formatEther;
toWei = ethers.utils.parseEther;
const address = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
const token = await ethers.getContractAt("GLDToken", address);
const accounts = await hre.ethers.getSigners();
owner = accounts[0].address;
toAddress = accounts[1].address;
await token.symbol()
//'GLD'
totalSupply = await token.totalSupply();
fromWei(totalSupply)
//10000.0
Some explanation on how to get contract instance:
We can get contract instance using Ethers.js directly(docs):
new ethers.Contract( address , abi , signerOrProvider )
. By this way, we need to get abi from the compile output (inartifacts
directory).We can use
hardhat-ethers
plugin's helper functionethers.getContractAt("GLDToken", address)
(docs) as we do in the above code snippet.
Once we get an instance of the ERC20 contract, we can call its functions (ERC20 docs by OpenZeppelin).
You can also transfer token from your current address to another address by calling transfer(recipient, amount)
. This is a state-changing function of the ERC20 contract.
await token.transfer(toAddress, toWei('100'))
ownerBalance = await token.balanceOf(owner);
fromWei(ownerBalance);
//'9900.0'
toBalance = await token.balanceOf(toAddress);
fromWei(toBalance)
//'100.0'
Note for playing with Public testnet
- Create an Alchemy Account and get API key
- Get some test ETH for Sepolia testnet from free faucet: Sepolia faucet
- Configure the network part in
hardhat.config.ts
for Sepolia testnet - Deploy the GLDToken ERC20 contract to Sepolia testnet
- Play with your GLDToken contract using Hardhat console.
Btw, Alchemy also provides some handy APIs for standard ERC20. For example, there is alchemy_getTokenBalances
link to get token balance of address.
Similarly, we can write an ERC721 token smart contract using OpenZeppelin. And in the next section, we will write a loot-like ERC721 NFT with on-chain SVG image.
If you feel this tutorial helpful and would like to know more, follow me Twitter: @fjun99 . DM is open.
Top comments (0)