Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to the first lesson of our fourth module, "Fungible Tokens & DeFi Primitives."
Introduction
In our previous module, we concluded by building a UUPS upgradeable contract, using a basic ERC-20 token as our practical example. That exercise set the stage perfectly for this new module, where we will construct the core building blocks of Decentralized Finance (DeFi).
The most fundamental of these primitives is the fungible token. Today, we will go beyond the basic shell and implement a feature-rich ERC-20 token. You will learn how to use OpenZeppelin's battle-tested, modular contracts to create a token that is not only functional but also secure and manageable.
Learning Outcome: By the end of this lesson, you will be able to implement an ERC-20 token with mint, burn, and pausable functionality.
This lesson will guide you through inheriting the standard ERC-20 functionality and then extending it with capabilities for creating new tokens (minting), destroying existing ones (burning), and temporarily halting all token activity (pausing).
1. The ERC-20 Token Standard
Before we write any code, let's solidify our understanding of what an ERC-20 token is. It's not a piece of software you run, but rather a standard interface that a smart contract must implement. This standardization ensures that any application, from wallets to decentralized exchanges, can interact with any ERC-20 token in a predictable way.
To get a clear overview, let's start with a short video.
ERC20 Token Tutorial | Create Your Own Cryptocurrency
The video 'ERC20 Token Tutorial' from Block Explorer provides an excellent introduction. It clarifies the difference between a coin and a token and walks through the core functions defined by the ERC-20 standard.
Please watch the section 'ERC-20 Token Overview and OpenZeppelin' from 01:04 to 03:33. Focus on the list of functions the standard provides, such as transfer, balanceOf, and approve.
As the video mentioned, the key to modern and secure ERC-20 development is not to reinvent the wheel. We will use the OpenZeppelin library, which provides audited, gas-optimized, and community-vetted implementations of these standards.
For a quick reference, the article "Ultimate Guide to Creating and Deploying BASE..." provides a concise summary of the key features.
Ultimate Guide to Creating and Deploying BASE ...
This article, though focused on the BASE blockchain, offers a clear, textual summary of the ERC-20 standard's features.
Read the section '3.1 Overview of ERC-20 Token Standard'. Pay attention to the bullet points: Uniformity, Transferability, Approval Mechanism, and Events. This reinforces what was covered in the video.
2. Building the Basic Token with Hardhat
We will now create our token contract within the Hardhat environment you're already familiar with. Our goal is to create a contract that inherits from OpenZeppelin's ERC20.sol.
Let's start by creating a new file in your contracts directory named MyToken.sol.
The basic structure involves:
- Importing
ERC20.solfrom the OpenZeppelin library. - Declaring your contract to inherit from
ERC20. - Creating a
constructorto set the token'snameandsymboland to mint an initial supply.
The following video segment walks through this initial implementation. While it starts in the Remix IDE, the core Solidity code is what's important. We will adapt it for our Hardhat project.
ERC20 Token Tutorial | Create Your Own Cryptocurrency
Let's return to the 'ERC20 Token Tutorial' video. This part demonstrates writing the minimal code for a functional ERC-20 token.
Watch from 03:33 to 09:43, and then from 11:28 to 13:52. (03:33 - 09:43): Focus on the Solidity code itself: the import statement, the contract definition is ERC20, the constructor, and the call to _mint. You can disregard the Remix-specific UI interactions. (11:28 - 13:52): This part is crucial. Pay close attention to the explanation of decimals. Understanding that Solidity does not handle floating-point numbers and that token amounts are represented as large integers is fundamental to working with ERC-20s.
Here is the code for our initial MyToken.sol, adapted for a local Hardhat project and incorporating the concepts from the video:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
// Mint the initial supply to the contract deployer
// Note: The amount needs to account for decimals.
// For a token with 18 decimals, 1 token is represented as 1 * 10**18.
_mint(msg.sender, initialSupply * (10 ** decimals()));
}
}
This is a complete, albeit basic, ERC-20 token. It has a fixed total supply minted entirely to the deployer's address upon creation.
3. Adding Mint and Burn Functionality
A fixed supply isn't always desirable. Many token models require the ability to create new tokens (minting) or destroy existing ones (burning).
- Minting: Increases the total supply. This is often a privileged action, restricted to the contract owner, used for things like inflation, rewards, or funding.
- Burning: Decreases the total supply. This can be a public function allowing any user to destroy their own tokens, or a privileged one. It's often used to create deflationary pressure.
OpenZeppelin provides modular extensions for these features. We'll use ERC20Burnable and add a custom mint function controlled by an owner.
The following article provides excellent, self-contained examples of how to integrate these features.
DeFi concepts for beginners: Mint, Burn, Liquidate, and ...
The article 'DeFi concepts for beginners' on Medium clearly explains how to add minting and burning capabilities to an ERC-20 token using OpenZeppelin's contracts.
Read the section 'Example: ERC-20 Token with Minting and Burning'. Note how the contract inherits from ERC20 and Ownable, and how the mint function is protected by the onlyOwner modifier while the burn function is public.
Let's update our MyToken.sol to include these features. We'll need to import Ownable.sol for access control and ERC20Burnable.sol for the burn functionality.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
// Inherit from ERC20, ERC20Burnable for burn(), and Ownable for access control
contract MyToken is ERC20, ERC20Burnable, Ownable {
constructor(string memory name, string memory symbol, address initialOwner) ERC20(name, symbol) {
// The Ownable constructor sets the deployer as the initial owner.
// We pass the desired owner address to the constructor.
_transferOwnership(initialOwner);
}
/**
* @dev Creates `amount` new tokens and assigns them to `to`, increasing
* the total supply.
*
* Requirements:
* - The caller must be the owner of the contract.
*/
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
Key changes:
- We now inherit from
ERC20BurnableandOwnable. - The
ERC20Burnableextension provides the publicburn(uint256 amount)andburnFrom(address account, uint256 amount)functions out of the box. - We removed the
initialSupplyfrom the constructor. Instead, the owner can now call themintfunction anytime to create new tokens. - The
mintfunction ispublicbut restricted by theonlyOwnermodifier fromOwnable. It calls the internal_mintfunction provided by the baseERC20contract.
4. Implementing Pausable Functionality
For security, it's often wise to include an "emergency stop" button. The Pausable pattern allows a contract owner to halt key functions in case a vulnerability is discovered, preventing further damage. When applied to an ERC-20 token, this typically means stopping all transfers.
OpenZeppelin provides the Pausable contract and a specific ERC-20 extension, ERC20Pausable, that makes this incredibly simple.
DeFi concepts for beginners: Mint, Burn, Liquidate, and ...
The same Medium article also has a perfect example for making a token pausable.
Read the section '3. Pausable Contracts' and study the 'Example: Pausing Token Transfers'. Notice how inheriting from Pausable and overriding the _beforeTokenTransfer hook automatically protects all transfer-related functions.
The ERC20Pausable extension conveniently pre-wires this for us. By inheriting from it, all transfer functions are automatically protected by the whenNotPaused modifier.
Let's integrate this into our final contract.
5. The Complete Token Contract
Here is the final version of MyToken.sol that fulfills all the requirements of our learning outcome. It is a fungible token with mint, burn, and pausable capabilities, all secured by an owner.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title MyToken
* @dev A feature-rich ERC20 token with mint, burn, and pausable functionalities.
* - Minting is restricted to the owner.
* - Burning can be done by any token holder for their own tokens.
* - Pausing/unpausing all token transfers is restricted to the owner.
*/
contract MyToken is ERC20, ERC20Burnable, ERC20Pausable, Ownable {
constructor(string memory name, string memory symbol, address initialOwner) ERC20(name, symbol) {
_transferOwnership(initialOwner);
}
/**
* @dev Creates `amount` new tokens and assigns them to `to`.
* Can only be called by the contract owner.
*/
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
/**
* @dev Pauses all token transfers.
* Can only be called by the contract owner.
*/
function pause() public onlyOwner {
_pause();
}
/**
* @dev Resumes all token transfers.
* Can only be called by the contract owner.
*/
function unpause() public onlyOwner {
_unpause();
}
/**
* @dev Hook that is called before any token transfer.
* Overridden to apply the `whenNotPaused` modifier.
*/
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal
override(ERC20, ERC20Pausable)
{
super._beforeTokenTransfer(from, to, amount);
}
}
To compile this contract, you can run npx hardhat compile in your project terminal. Thanks to the modular nature of OpenZeppelin contracts, it should compile without issues. The override(ERC20, ERC20Pausable) keyword is necessary because both parent contracts define a _beforeTokenTransfer function, and we need to explicitly state which one we are extending.
Conclusion
Excellent work! You have successfully designed and implemented a robust ERC-20 token that goes far beyond the basics. By composing modular contracts from the OpenZeppelin library, you've created a token with essential management features in a secure and gas-efficient way.
Key Takeaways:
- ERC-20 is a Standard: It defines a common interface for fungible tokens, ensuring interoperability.
- Composition over Reinvention: We built our token by inheriting from pre-built, audited OpenZeppelin contracts:
ERC20,ERC20Burnable,ERC20Pausable, andOwnable. - Minting: New tokens can be created via a privileged
mintfunction that calls the internal_mint. - Burning: The
ERC20Burnableextension provides a publicburnfunction for users to destroy their tokens. - Pausable: The
ERC20Pausableextension provides a powerful safety mechanism to halt token transfers, controlled by the owner.
Next Steps:
Our token is functional, but there's a common user-experience friction point in DeFi: the approve and transferFrom flow, which requires two separate transactions. In the next lesson, we will solve this by implementing ERC-2612, a standard that allows for gasless approvals via signatures, dramatically improving the user experience.