Guide to building good ERC20 smart contracts
Alright, let’s talk about what it really means to create an ERC20 token contract. When we embark on this, we’re not just deploying code; we’re building a system that people will entrust with their assets. This carries a significant weight of responsibility, and there are fundamental concepts we should follow.
The Core Contract
First and foremost, we have to implement the ERC20 standard correctly. If we deviate or misunderstand these core functions, our token simply won’t work where people expect it to, eroding user trust.
We have to implement the following:
totalSupply()
: This provides transparency on the total supply. It’s a critical piece of economic information for anyone interacting with the token.balanceOf(address account)
: Users need to know how many tokens any given address holds. This is a fundamental requirement for ownership verification.transfer(address recipient, uint256 amount)
: The ability for a token holder to send their tokens directly to another address is the most basic utility. Implementing this securely is paramount; any vulnerability here could lead to unauthorized loss of funds.transferFrom(address sender, address recipient, uint256 amount)
: This function, coupled withapprove
, enables approved third parties to move tokens on behalf of the owner. It’s essential for interactions with decentralized exchanges and other protocols, but it introduces complexity regarding allowances and requires careful handling to prevent exploits like the “approve race condition.”approve(address spender, uint256 amount)
: This grants a specific address permission to spend a certain amount of tokens from the caller’s balance. It’s a delegation of authority, and we must ensure it’s handled with precision.allowance(address owner, address spender)
: Users need to be able to check how much allowance they have granted to a spender.
In addition to these mandatory functions, the ERC20 standard also suggests optional functions for better usability and discoverability:
decimals()
: Returns the number of decimal places the token uses (typically 18). This allows for divisibility of the token.name()
: Returns the name of the token (e.g., “Helloworld Token”).symbol()
: Returns the symbol of the token (e.g., “HT”).
For more info see https://docs.openzeppelin.com/contracts/5.x/api/token/erc20#ERC20
Controlling Access
Now that the basic contract structure is ready, we need to define which accounts (addresses) can do what functions on the contract.
Consider functions like minting new tokens or pausing transfers. If just anyone could call these, the token’s integrity would be instantly compromised. We typically restrict these powers to specific addresses, often an administrator or addresses holding a designated role.
For more info see https://docs.openzeppelin.com/contracts/5.x/access-control
Upgradability
Deployed smart contracts are immutable. This immutability is a source of trust – the code is law, and it cannot be changed arbitrarily. However, what happens if a critical bug is discovered? Or if essential new features are needed? For many projects, the inability to update the contract is an unacceptable risk, especially in places where the laws regarding token ownership will change over time.
This leads us to upgradability, typically implemented using proxy patterns. This involves separating the contract’s state (stored in a persistent proxy contract) from its logic (in an implementation contract). The proxy delegates calls to the current implementation. To upgrade, we deploy a new implementation contract and point the proxy to it. The token balances and other data remain intact.
For more info see https://docs.openzeppelin.com/contracts/5.x/upgradeable
While necessary for flexibility, upgradability fundamentally compromises the principle of immutability. It introduces a layer of indirection and requires a trusted entity (usually an admin address) to control the upgrade process. This grants immense power – the ability to alter the contract’s behavior after deployment. Different proxy patterns (Transparent, UUPS, Beacon) offer various ways to manage this, but they all share this core characteristic.
Gas Efficiency is not optional
Operating on the Ethereum network incurs costs, measured in gas. Every function call, every state change, consumes gas. For an ERC20 token, this means users will pay gas fees for every transfer, approval, or other interaction.
We have a responsibility to write gas-efficient code. Inefficient smart contracts translate directly into higher transaction costs for users. This can make the token impractical for frequent use or price it out of certain applications. Factors like excessive writes to storage, complex loops, or unoptimized data structures dramatically increase gas consumption.
Estimating and optimizing gas usage is a core part of the development process. Tools exist to help us analyze gas costs, but ultimately, it comes down to writing clean, efficient Solidity. A failed transaction due to insufficient gas is not just an inconvenience; it’s a waste of the user’s resources and a failure of our contract to execute reliably under expected conditions.
Use Forwarders to abstract gas fees (Advanced)
The idea of gasless transactions, where users don’t need ETH to pay for token transfers, is attractive from a user experience perspective. This is often achieved through meta-transactions (ERC-2771) and forwarder contracts.
For an example abstract implementation see https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/ERC2771Context.sol
In this model, a user signs a message authorizing a transaction (e.g., a token transfer) but doesn’t send it directly to the network. Instead, a third party, a relayer, picks up this signed message and submits it to a forwarder contract, paying the gas cost in ETH. The forwarder contract then verifies the user’s signature and executes the token transfer on their behalf.
While this improves accessibility, it introduces significant architectural and security complexity. We are now reliant on relayers and the forwarder contract’s logic to correctly authenticate and execute transactions. Preventing signature replay attacks and ensuring the integrity of the forwarded transactions is paramount. Implementing this securely requires deep technical understanding and meticulous auditing.
Do note that implementing this poorly can also led to misuse causing losses. See https://docs.openzeppelin.com/contracts/5.x/api/metatx#security_considerations
This is it for the basics, build something and have fun!