Skip to content

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:

bash
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.

bash
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:

bash
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:

bash
$ 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:

txt
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.

solidity
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
solidity
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:

solidity
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.