Build a Smart Contract
The ERC-20 token standard provides a common interface for tokens on Berachain. ERC-20s follow a standard interface such that other applications can easily interact with them on-chain, but can be extended to do much, much more. They power everything from simple value transfers to complex DeFi interactions.
In this guide, we'll walk through how to create an ERC-20 token using Solidity and deploy it to the Berachain Testnet.
Pre-requisites
Before you start, make sure you have the following:
- Foundry or Hardhat installed
- A text editor of your choice
Initialize Repository
First, we'll create a new project using Forge's init
command:
forge init my_token
forge init my_token
This will create a new directory called my_token
with a basic forge project structure and example contracts. If using VS Code as your text editor, you can add the --vscode
flag like so to initialize some extra defaults.
forge init my_token --vscode
forge init my_token --vscode
Now you can cd
into the directory so you're ready to run commands later:
cd my_token
cd my_token
Feel free to delete the generated files:
src/Counter.sol
test/Counter.t.sol
script/Counter.s.sol
They serve as a good example of how to write contracts and tests in Forge Foundry's format, but we won't need them for this guide.
Install Dependencies
OpenZeppelin ERC-20
OpenZeppelin provides commonly used interfaces & implementations of various ERC standards, including ERC-20. We'll use their ERC-20 implementation to create our token as these are audited and battle-tested. It also makes it much easier to get up and running quickly without reinventing the wheel.
Foundry
If using Foundry, install the OpenZeppelin library using the following command:
$ forge install openzeppelin/openzeppelin-contracts
$ forge install openzeppelin/openzeppelin-contracts
This pulls the OpenZeppelin
library, stages the .gitmodules file in git and makes a commit with the message "Installed openzeppelin-contracts".
In order to use the library, edit the remappings.txt
file at the root of your project to include the following line:
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
@openzeppelin/=lib/openzeppelin-contracts/
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
@openzeppelin/=lib/openzeppelin-contracts/
This tells Foundry where to find the @openzeppelin
library when compiling your contracts.
Create the Token Contract
Now we can start creating our token contract. We'll create a new file called MyToken.sol
inside the src/
folder and import the OpenZeppelin ERC-20 contract.
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
This imports the ERC20
contract from the OpenZeppelin library, which includes basic implementations of all of the functions in the ERC-20 standard. We'll use this as the base for our token contract.
Next, we'll create the actual contract which extends the ERC-20 contract we imported. With this token we will:
- Set the name to "MyToken"
- Set the symbol to "MT"
- Set the initial supply to 1,000,000 tokens
pragma solidity ^0.8.13;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
uint256 public constant INITIAL_SUPPLY = 1_000_000 * 1 ether;
constructor() ERC20("MyToken", "MT"){
_mint(msg.sender, INITIAL_SUPPLY);
}
}
pragma solidity ^0.8.13;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
uint256 public constant INITIAL_SUPPLY = 1_000_000 * 1 ether;
constructor() ERC20("MyToken", "MT"){
_mint(msg.sender, INITIAL_SUPPLY);
}
}
TIP
The 1 ether
is an easy way to make a unit conversion. The default decimals
for the ERC-20 token standard is 18, so this will mint 1 million tokens with 18 decimal places. To learn more, check out this article on decimals.
Technically, this is all you need to create your own token contract! If satisfied, you could take this contract and deploy it. It would then mint 1 million tokens to your wallet that deployed the contract (msg.sender
). This would allow you to do whatever with the supply that you want, for example pairing those tokens with another token in the Berachain BEX so others can acquire it.
However, we usually want to make a token that's a little more interesting so it stands out. Let's add some more functionality to our token contract.
Let's make it so our token burns a small fee on every transfer. To do so, we'll add a few more constants and then override the default _transfer
function in OpenZeppelin's ERC20.sol
like so:
pragma solidity ^0.8.13;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
uint256 public constant INITIAL_SUPPLY = 1_000_000 * 1 ether;
uint256 public constant BURN_PERCENTAGE = 1; // 1%
address public constant BURN_ADDRESS = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF;
constructor() ERC20("MyToken", "MT"){
_mint(msg.sender, INITIAL_SUPPLY);
}
function _transfer(address sender, address recipient, uint256 amount) internal override {
uint256 burnAmount = (amount * BURN_PERCENTAGE) / 100;
super._transfer(sender, recipient, amount - burnAmount);
super._transfer(sender, BURN_ADDRESS, burnAmount);
}
}
pragma solidity ^0.8.13;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
uint256 public constant INITIAL_SUPPLY = 1_000_000 * 1 ether;
uint256 public constant BURN_PERCENTAGE = 1; // 1%
address public constant BURN_ADDRESS = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF;
constructor() ERC20("MyToken", "MT"){
_mint(msg.sender, INITIAL_SUPPLY);
}
function _transfer(address sender, address recipient, uint256 amount) internal override {
uint256 burnAmount = (amount * BURN_PERCENTAGE) / 100;
super._transfer(sender, recipient, amount - burnAmount);
super._transfer(sender, BURN_ADDRESS, burnAmount);
}
}
As you can see, we override the parent class's _transfer
function by redefining _transfer
in our MyToken
contract with the override
modifier. We can still call the default _transfer
function from the parent class using super._transfer
and do so to handle the actual token transfer logic after the burn has been calculated.