This guide provides a step-by-step walkthrough for creating and deploying a custom ERC20 token on the Ethereum blockchain using the Hardhat development environment. Hardhat offers a robust toolkit for compiling, testing, and deploying smart contracts efficiently.
Setting Up the Development Environment
To begin, ensure you have Node.js and npm installed. Open your terminal and navigate to your project directory. Initialize a new Node.js project by running:
npm init -yThis command generates a package.json file with default settings. Next, install Hardhat as a development dependency:
npm install --save-dev hardhatAfter installation, initialize Hardhat in your project folder:
npx hardhatSelect the default options when prompted. This generates essential files and directories:
- contracts/: Stores Solidity smart contracts.
- test/: Contains unit test scripts.
- scripts/: Includes deployment scripts.
- hardhat.config.js: Configures network and compiler settings.
Install additional dependencies for secure contract templates and testing:
npm install @openzeppelin/contracts chaiOpenZeppelin provides audited, secure smart contract implementations, while Chai is used for writing assertion-based tests.
Creating the ERC20 Token Contract
In the contracts/ directory, delete the default Lock.sol file and create a new file named MyToken.sol. Paste the following Solidity code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
contract MyToken is ERC20Capped, ERC20Burnable {
address payable public owner;
uint256 public blockReward;
constructor(uint256 cap, uint256 reward)
ERC20("MyToken", "MYT")
ERC20Capped(cap * (10 ** decimals()))
{
owner = payable(msg.sender);
_mint(owner, 50000000 * (10 ** decimals()));
blockReward = reward * (10 ** decimals());
}
function _mintMinerReward() internal {
_mint(block.coinbase, blockReward);
}
function _beforeTokenTransfer(address from, address to, uint256 value) internal virtual override {
if (from != block.coinbase && to != block.coinbase && block.coinbase != address(0)) {
_mintMinerReward();
}
super._beforeTokenTransfer(from, to, value);
}
function _mint(address account, uint256 amount) internal virtual override(ERC20Capped, ERC20) {
require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded");
super._mint(account, amount);
}
function setBlockReward(uint256 reward) public onlyOwner {
blockReward = reward * (10 ** decimals());
}
function destroyContract() public onlyOwner {
selfdestruct(owner);
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
}Understanding the Contract Structure
This contract creates a capped, burnable ERC20 token named "MyToken" (symbol: MYT). Key features include:
- Inheritance: Uses OpenZeppelin’s
ERC20,ERC20Capped, andERC20Burnablefor standard functionality, supply cap, and token burning. - Constructor: Sets the token cap, mints an initial supply to the deployer, and defines a block reward for miners.
- Miner Rewards: Automatically mints tokens to the miner of a block containing a transfer transaction.
- Access Control: Restricts critical functions to the contract owner.
Configuring the Solidity Compiler
Specify the Solidity version in hardhat.config.js to avoid compilation errors:
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.17",
};Compile the contract using:
npx hardhat compileSuccessful compilation generates an artifacts/ directory containing the contract’s ABI and bytecode.
Writing Unit Tests for the Token
In the test/ directory, replace the default test file with MyToken.js. Use the following code to test token functionality:
const { expect } = require("chai");
const hre = require("hardhat");
describe("MyToken Contract", function () {
let Token, myToken, owner, addr1, addr2;
const tokenCap = 100000000;
const tokenBlockReward = 50;
beforeEach(async function () {
Token = await ethers.getContractFactory("MyToken");
[owner, addr1, addr2] = await hre.ethers.getSigners();
myToken = await Token.deploy(tokenCap, tokenBlockReward);
});
describe("Deployment", function () {
it("Should set the correct owner", async function () {
expect(await myToken.owner()).to.equal(owner.address);
});
it("Should assign the total supply to the owner", async function () {
const ownerBalance = await myToken.balanceOf(owner.address);
expect(await myToken.totalSupply()).to.equal(ownerBalance);
});
it("Should set the correct capped supply", async function () {
const cap = await myToken.cap();
expect(Number(hre.ethers.utils.formatEther(cap))).to.equal(tokenCap);
});
});
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
await myToken.transfer(addr1.address, 50);
expect(await myToken.balanceOf(addr1.address)).to.equal(50);
await myToken.connect(addr1).transfer(addr2.address, 50);
expect(await myToken.balanceOf(addr2.address)).to.equal(50);
});
it("Should fail if sender has insufficient balance", async function () {
await expect(
myToken.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
});
});
});Run the tests with:
npx hardhat testAll tests should pass, confirming the contract works as intended.
Configuring Deployment Settings
Create a .env file to store sensitive data:
PRIVATE_KEY=your_metamask_private_key
INFURA_SEPOLIA_ENDPOINT=your_infura_sepolia_urlUpdate hardhat.config.js to use the Sepolia testnet:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.17",
networks: {
sepolia: {
url: process.env.INFURA_SEPOLIA_ENDPOINT,
accounts: [process.env.PRIVATE_KEY],
},
},
};Deploying the Contract to Sepolia
Create a deployment script in scripts/deploy.js:
const hre = require("hardhat");
async function main() {
const MyToken = await hre.ethers.getContractFactory("MyToken");
const myToken = await MyToken.deploy(100000000, 50);
await myToken.deployed();
console.log("Token deployed to:", myToken.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Deploy using:
npx hardhat run --network sepolia scripts/deploy.jsNote the deployed contract address for verification and interaction.
Verifying the Contract on Etherscan
- Visit Sepolia Etherscan.
- Paste the contract address into the search bar.
- Confirm the contract details and transaction history.
Importing the Token into MetaMask
- Open MetaMask and click "Import Tokens".
- Paste the contract address. The token symbol and decimals should auto-populate.
- Click "Add Custom Token" to view your balance.
To transfer tokens:
- Switch to another MetaMask account.
- Use the "Send" function to transfer tokens to the new address.
- Import the token in the recipient account to view the balance.
Frequently Asked Questions
What is an ERC20 token?
ERC20 is a technical standard for fungible tokens on the Ethereum blockchain. It defines a common set of rules for token transactions, balances, and supply, ensuring compatibility across wallets and exchanges.
Why use Hardhat for deployment?
Hardhat simplifies smart contract development with built-in testing, debugging, and deployment tools. It supports multiple networks and offers extensive plugin integration for streamlined workflows.
How do I get test ETH for deployment?
Use faucets like Sepolia Faucet to obtain free test ETH for deploying and testing contracts on the Sepolia network.
What is the purpose of a capped supply?
A capped supply ensures a maximum number of tokens can ever be created, preventing inflation and promoting scarcity. This is common in utility or governance tokens.
How can I make my contract more secure?
Use audited libraries like OpenZeppelin, implement access controls, and conduct thorough testing. Always verify contracts on Etherscan for transparency.
Can I deploy this on the Ethereum mainnet?
Yes, but replace the Sepolia configuration with a mainnet RPC endpoint and ensure you have sufficient ETH for gas fees. Always test extensively on testnets first.
Conclusion
This guide covered the end-to-end process of deploying a custom ERC20 token using Hardhat. From environment setup and contract creation to testing and deployment, these steps provide a foundation for Ethereum smart contract development. For further learning, explore more strategies on advanced contract optimization and security practices.