ERC-20 Smart Contracts: From Development to Production
A practical walkthrough of deploying ERC-20 tokens on Ethereum and Polygon — gas optimization, testing with Hardhat, and integrating Web3 wallet connectivity.
Deploying your first ERC-20 contract to a testnet is easy. Getting it production-ready on Ethereum mainnet and Polygon with a Node.js backend integration is another story. Here's everything I learned building this for Remit & Go.
The ERC-20 Standard
ERC-20 is a fungible token standard on Ethereum. Every DeFi protocol, exchange, and wallet understands it. You implement 6 core functions: totalSupply, balanceOf, transfer, transferFrom, approve, and allowance.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
contract RemitToken is ERC20, ERC20Burnable, Ownable {
uint256 public constant MAX_SUPPLY = 100_000_000 * 10**18; // 100M tokens
constructor(address initialOwner)
ERC20("RemitToken", "RMT")
Ownable(initialOwner)
{}
function mint(address to, uint256 amount) external onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
}
}Testing with Hardhat
Write exhaustive tests before touching mainnet. Hardhat gives you a local EVM environment, time manipulation, and gas reporting.
import { expect } from "chai";
import { ethers } from "hardhat";
describe("RemitToken", () => {
it("Should mint tokens to owner", async () => {
const [owner, user] = await ethers.getSigners();
const Token = await ethers.getContractFactory("RemitToken");
const token = await Token.deploy(owner.address);
await token.mint(user.address, ethers.parseEther("1000"));
expect(await token.balanceOf(user.address)).to.equal(
ethers.parseEther("1000")
);
});
it("Should not exceed max supply", async () => {
// ...
await expect(
token.mint(user.address, ethers.parseEther("100000001"))
).to.be.revertedWith("Exceeds max supply");
});
});Deploying to Polygon for Low Gas Fees
Polygon is EVM-compatible, so your Solidity code runs unchanged. The gas fees are ~1000x cheaper than Ethereum mainnet — ideal for high-frequency remittance transfers.
- ▸Polygon Mumbai testnet → Polygon mainnet for the deploy flow
- ▸Bridge MATIC to your deployer wallet
- ▸Use the same contract ABI on both chains — same Solidity, different RPC
- ▸Hardhat network config handles multi-chain deploys cleanly
// hardhat.config.ts
networks: {
polygon: {
url: `https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`,
accounts: [DEPLOYER_PRIVATE_KEY],
gasPrice: 30_000_000_000, // 30 gwei
},
ethereum: {
url: `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`,
accounts: [DEPLOYER_PRIVATE_KEY],
},
},Integrating with NestJS via Web3.js
From the NestJS backend, we interact with deployed contracts using Web3.js. The key pattern: load the contract ABI + deployed address, then call methods.
@Injectable()
export class BlockchainService {
private readonly web3: Web3;
private readonly contract: Contract;
constructor() {
this.web3 = new Web3(process.env.POLYGON_RPC_URL);
this.contract = new this.web3.eth.Contract(
RemitTokenABI,
process.env.CONTRACT_ADDRESS,
);
}
async getBalance(walletAddress: string): Promise<string> {
const balance = await this.contract.methods
.balanceOf(walletAddress)
.call();
return this.web3.utils.fromWei(balance as string, 'ether');
}
async transfer(from: string, to: string, amount: string): Promise<string> {
const amountWei = this.web3.utils.toWei(amount, 'ether');
const tx = await this.contract.methods.transfer(to, amountWei).send({
from,
gas: 100000,
});
return tx.transactionHash as string;
}
}Security Checklist Before Mainnet
- ▸Audit with Slither (free static analysis) before any external audit
- ▸Re-entrancy guards on any function that sends ETH
- ▸Access control: use Ownable or AccessControl, not hardcoded addresses
- ▸Integer overflow: Solidity 0.8+ has built-in overflow protection
- ▸Test on testnet for at least 2 weeks with real user flows
- ▸Verify contract source on Etherscan/Polygonscan for transparency
- ▸Never store private keys in code or environment variables — use AWS KMS or HashiCorp Vault
Enjoyed this post?
Let's connect and talk engineering.