Create ERC-20 Contract Using Foundry
See the full GitHub Project Code Repository.
This developer guide will walk you through setting up a new Solidity contract, configuring the Berachain network details, deploying to Berachain, and verifying the contract, all with Foundry.
Requirements
Before beginning, make sure you have the following installed or setup on your computer before hand.
- Foundry
v1.3.1
or greater
WARNING
NOTE: forge v1.3.1 is absolutely needed to take advantage of Etherscan's V2 contract verification. Ensure that you have the latest version installed with foundryup
.
Creating ERC20 Project Code Setup
Let's start by creating the project folder for the entire code.
mkdir create-erc20-contract-using-foundry;
cd create-erc20-contract-using-foundry;
Next, start by creating the initial template project defined by Foundry by running the following command:
# FROM: ./create-erc20-contract-using-foundry
forge init; # forge init --force; # if there is already an existing .git repository associated
# [Expected Output]:
# ...
# Resolving deltas: 100% (129/129), done.
# Installed forge-std v1.7.1
# Initialized forge project
If templated correctly, we should see the following structure:
# FROM: ./create-erc20-contract-using-foundry
.
├── README.md
├── foundry.lock
├── foundry.toml
├── lib
│ └── forge-std
├── script
│ └── Counter.s.sol
├── src
│ └── Counter.sol
└── test
└── Counter.t.sol
Now that all the code has been setup, install the dependencies needed for an ERC20 contract from OpenZeppelin, by funning the following:
# FROM: ./create-erc20-contract-using-foundry
forge install OpenZeppelin/openzeppelin-contracts;
# If existing git setup run:
# forge install OpenZeppelin/openzeppelin-contracts --no-commit;
# [Expected Output]:
# ...
# Resolving deltas: 100% (129/129), done.
# Installed openzeppelin-contracts v5.0.0
INFO
NOTE: You may get the following error in your IDE Source "@openzeppelin/contracts/token/ERC20/ERC20.sol" not found: File import callback not supported
. Run the following to help fix this error:
# FROM: ./create-erc20-contract-using-foundry
echo 'remappings = ["@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"]' >> foundry.toml;
Creating the ERC20 Contract
To create our contract, convert the existing src/Counter.sol
to a new BingBongToken.sol
and replace the code with the following Solidity code:
# FROM: ./create-erc20-contract-using-foundry
mv src/Counter.sol src/BingBongToken.sol;
File: ./src/BingBongToken.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract BingBongToken is ERC20 {
/**
* @dev Init constructor for setting token name and symbol
*/
constructor(string memory name_, string memory symbol_, uint256 mintedTokens_) ERC20(name_, symbol_) {
_mint(msg.sender, mintedTokens_);
}
}
Confirm that this compiles correctly by running the following:
# FROM: ./create-erc20-contract-using-foundry
forge compile;
# [Expected Error Output]:
# [⠊] Compiling...
# [⠒] Unable to resolve imports:
# "../src/Counter.sol" in "/path/to/create-erc20-contract-using-foundry/test/Counter.t.sol"
# ...
This error happens because it references a file that no longer exists. To fix this, we'll rename it to BingBongToken.t.sol
and replace it some placeholder code.
# FROM: ./create-erc20-contract-using-foundry
mv test/Counter.t.sol test/BingBongToken.t.sol;
File: ./test/BingBongToken.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console2} from "forge-std/Test.sol";
import {BingBongToken} from "../src/BingBongToken.sol";
contract BingBongTokenTest is Test {
}
We'll also need to fix the script file to point the correct file as well.
# FROM: ./create-erc20-contract-using-foundry
mv script/Counter.s.sol script/BingBongToken.s.sol;
Next, replace the existing code the following to handle importing the wallet private key, and deploying the contract.
File: ./script/BingBongToken.s.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console2} from "forge-std/Script.sol";
import {BingBongToken} from "../src/BingBongToken.sol";
contract BingBongTokenScript is Script {
/**
* @dev Relevant source part starts here and spans across multiple lines
*/
function setUp() public {
}
/**
* @dev Main deployment script
*/
function run() public {
// Deploy
vm.startBroadcast();
BingBongToken bbt = new BingBongToken("BingBongToken", "BBT", 5678);
vm.stopBroadcast();
// Verify + End
console2.log('address:', address(bbt));
console2.log('totalSupply:', bbt.totalSupply());
}
}
Now when running forge compile
the following results should show up:
# FROM: ./create-erc20-contract-using-foundry
forge compile;
# [Expected Output]:
# [⠊] Compiling...
# [⠔] Compiling 1 files with Solc 0.8.26
# [⠒] Solc 0.8.26 finished in 461.24ms
Testing the ERC20 Contract
With our newly renamed BingBongToken.t.sol
file, add the following tests that cover a wide range of ERC20 tests.
Feel free to look at each individual test to get a better idea on how revert and successful scenarios are handled.
File: ./test/BingBongToken.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console2, stdError} from "forge-std/Test.sol";
import {BingBongToken} from "../src/BingBongToken.sol";
contract BingBongTokenTest is Test {
// Variables
BingBongToken public token;
address supplyOwnerAddress = makeAddr("BerachainWalletUser"); // 0xE3284cB941608AA9E65F7EDdbb50c461D936622f
address randomWalletAddress = makeAddr("GiveMeTokens"); // 0x187A660c372Fa04D09C1A71f2927911e62e98a89
address anotherWalletAddress = makeAddr("AnotherAddress"); // 0x0F3B9cC98eef350B12D5b7a338D8B76c2F9a92CC
error ERC20InvalidReceiver(address receiver);
// Initial Read Tests
// ========================================================
/**
* @dev Initial contract setup
*/
function setUp() public {
vm.prank(supplyOwnerAddress);
token = new BingBongToken("BingBong Token", "BBT", 10000);
}
/**
* @dev Test initiatted token name
*/
function test_name() public {
assertEq(token.name(), "BingBong Token");
}
/**
* @dev Test initiatted token symbol
*/
function test_symbol() public {
assertEq(token.symbol(), "BBT");
}
/**
* @dev Test default decimals
*/
function test_decimals() public {
assertEq(token.decimals(), 18);
}
/**
* @dev Test initial total token supply
*/
function test_totalSupply() public {
assertEq(token.totalSupply(), 10000);
}
/**
* @dev Test initial random account balance
*/
function test_balanceOfAddress0() public {
assertEq(token.balanceOf(address(0)), 0);
}
/**
* @dev Test account balance of original deployer
*/
function test_balanceOfAddressSupplyOwner() public {
assertEq(token.balanceOf(supplyOwnerAddress), 10000);
}
/**
* @dev Test Revert transfer to sender as 0x0
*/
function test_transferRevertInvalidSender() public {
vm.prank(address(0));
vm.expectRevert(abi.encodeWithSignature("ERC20InvalidSender(address)", address(0)));
token.transfer(randomWalletAddress, 100);
}
/**
* @dev Test Revert transfer to receiver as 0x0
*/
function test_transferRevertInvalidReceiver() public {
vm.prank(supplyOwnerAddress);
vm.expectRevert(abi.encodeWithSignature("ERC20InvalidReceiver(address)", address(0)));
token.transfer(address(0), 100);
}
/**
* @dev Test Revert transfer to sender with insufficient balance
*/
function test_transferRevertInsufficientBalance() public {
vm.prank(randomWalletAddress);
// NOTE: Make sure to keep this string for `encodeWithSignature` free of spaces for the string (" ")
vm.expectRevert(abi.encodeWithSignature("ERC20InsufficientBalance(address,uint256,uint256)", randomWalletAddress, 0, 100));
token.transfer(supplyOwnerAddress, 100);
}
/**
* @dev Test transfer to receiver from sender with sufficient balance
*/
function test_transfer() public {
vm.prank(supplyOwnerAddress);
assertEq(token.transfer(randomWalletAddress, 100), true);
assertEq(token.balanceOf(randomWalletAddress), 100);
assertEq(token.balanceOf(supplyOwnerAddress), 10000 - 100);
}
/**
* @dev Test allowance of random address for supplyOwner
*/
function test_allowance() public {
assertEq(token.allowance(supplyOwnerAddress, randomWalletAddress), 0);
}
/**
* @dev Test Revert approve of owner as 0x0
*/
function test_approveRevertInvalidApprover() public {
vm.prank(address(0));
vm.expectRevert(abi.encodeWithSignature("ERC20InvalidApprover(address)", address(0)));
token.approve(randomWalletAddress, 100);
}
/**
* @dev Test Revert approve of spender as 0x0
*/
function test_approveRevertInvalidSpender() public {
vm.prank(supplyOwnerAddress);
vm.expectRevert(abi.encodeWithSignature("ERC20InvalidSpender(address)", address(0)));
token.approve(address(0), 100);
}
/**
* @dev Test approve of spender for 0 and 50
*/
function test_approve() public {
vm.prank(supplyOwnerAddress);
assertEq(token.approve(randomWalletAddress, 0), true);
assertEq(token.approve(randomWalletAddress, 50), true);
}
/**
* @dev Test Revert transferFrom of spender with 0 approveed
*/
function test_transferFromRevertInsufficientAllowanceFor0x0() public {
vm.prank(supplyOwnerAddress);
vm.expectRevert(abi.encodeWithSignature("ERC20InsufficientAllowance(address,uint256,uint256)", supplyOwnerAddress, 0, 100));
token.transferFrom(randomWalletAddress, address(0), 100);
}
/**
* @dev Test Revert transferFrom of spender transferring to 0x0
*/
function test_transferFromRevertInvalidReceiver() public {
// Setup
vm.prank(supplyOwnerAddress);
token.approve(randomWalletAddress, 30);
// Test
vm.prank(randomWalletAddress);
vm.expectRevert(abi.encodeWithSignature("ERC20InvalidReceiver(address)", address(0)));
token.transferFrom(supplyOwnerAddress, address(0), 30);
}
/**
* @dev Test Revert transferFrom of spender transferring 50/30 approved
*/
function test_transferFromRevertInsufficientAllowance() public {
// Setup
vm.prank(supplyOwnerAddress);
token.approve(randomWalletAddress, 30);
// Test
vm.prank(randomWalletAddress);
vm.expectRevert(abi.encodeWithSignature("ERC20InsufficientAllowance(address,uint256,uint256)", randomWalletAddress, 30, 50));
token.transferFrom(supplyOwnerAddress, anotherWalletAddress, 50);
}
/**
* @dev Test transferFrom of spender 10/30 approved
*/
function test_transferFrom() public {
// Setup
vm.prank(supplyOwnerAddress);
token.approve(randomWalletAddress, 30);
// Test
vm.prank(randomWalletAddress);
assertEq(token.transferFrom(supplyOwnerAddress, anotherWalletAddress, 10), true);
assertEq(token.balanceOf(anotherWalletAddress), 10);
assertEq(token.balanceOf(supplyOwnerAddress), 10000 - 10);
assertEq(token.allowance(supplyOwnerAddress, randomWalletAddress), 30 - 10);
}
}
Compile the code and then run a test to see the different tests pass.
# FROM: ./create-erc20-contract-using-foundry
forge test -vvv; # v stands for verbose and multiple vvv allow for more details for tests
# [Expected Output]:
# [⠰] Compiling...
# No files changed, compilation skipped
#
# Running 18 tests for test/BingBongToken.t.sol:BingBongTokenTest
# [PASS] test_allowance() (gas: 12341)
# [PASS] test_approve() (gas: 42814)
# [PASS] test_approveRevertInvalidApprover() (gas: 11685)
# [PASS] test_approveRevertInvalidSpender() (gas: 11737)
# [PASS] test_balanceOfAddress0() (gas: 7810)
# [PASS] test_balanceOfAddressSupplyOwner() (gas: 9893)
# [PASS] test_decimals() (gas: 5481)
# [PASS] test_name() (gas: 9541)
# [PASS] test_symbol() (gas: 9650)
# [PASS] test_totalSupply() (gas: 7546)
# [PASS] test_transfer() (gas: 44880)
# [PASS] test_transferFrom() (gas: 75384)
# [PASS] test_transferFromRevertInsufficientAllowance() (gas: 42626)
# [PASS] test_transferFromRevertInsufficientAllowanceFor0x0() (gas: 16597)
# [PASS] test_transferFromRevertInvalidReceiver() (gas: 28334)
# [PASS] test_transferRevertInsufficientBalance() (gas: 16477)
# [PASS] test_transferRevertInvalidReceiver() (gas: 11796)
# [PASS] test_transferRevertInvalidSender() (gas: 11746)
# Test result: ok. 18 passed; 0 failed; 0 skipped; finished in 2.07ms
#
# Ran 1 test suites: 18 tests passed, 0 failed, 0 skipped (18 total tests)
Configuring Foundry for Berachain Contract Deployment
To verify that the contract can actually be deployed, test it with a local node by running anvil
. Take note of the private key.
Terminal 1:
# FROM: ./create-erc20-contract-using-foundry
anvil;
# [Expected Output]:
#
#
# _ _
# (_) | |
# __ _ _ __ __ __ _ | |
# / _` | | '_ \ \ \ / / | | | |
# | (_| | | | | | \ V / | | | |
# \__,_| |_| |_| \_/ |_| |_|
#
# 0.2.0 (f5b9c02 2023-10-28T00:16:04.060987000Z)
# https://github.com/foundry-rs/foundry
#
# Available Accounts
# ==================
#
# (0) "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" (10000.000000000000000000 ETH)
# ...
#
# Private Keys
# ==================
#
# (0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# ...
With the provided private key, we're going to import it into foundry's encrypted keystore system.
# FROM ./create-erc20-contract-using-foundry
# If `Error: Keystore file already exists` you'll need to remove the file via `rm ~/path/to/.foundry/keystores/MyBeraAccount`
cast wallet import MyBeraAccount --interactive;
# [Expected Similar Output]:
# Enter private key:
# Enter password:
# `MyBeraAccount` keystore was saved successfully. Address: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Terminal 2:
In other terminal window, run the following command to deploy the contract to the local node rpc.
# FROM ./create-erc20-contract-using-foundry
forge script script/BingBongToken.s.sol --account MyBeraAccount --fork-url http://localhost:8545 --broadcast;
# [Expected Similar Output]:
# [⠊] Compiling...
# No files changed, compilation skipped
# Enter keystore password:
# Script ran successfully.
#
# == Logs ==
# address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
# totalSupply: 5678
#
# ## Setting up 1 EVM.
# ==========================
#
# Chain 31337
# Estimated gas price: 2.000000001 gwei
# Estimated total gas used for script: 1233780
# Estimated amount required: 0.00246756000123378 ETH
# ==========================
#
# ##### anvil-hardhat
# ✅ [Success] Hash: 0xd7e542342832543915f8f409135ba929bbe7de268d2c0ba29a4b7e3c6ccfe607
# Contract Address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
# Block: 1
# Paid: 0.000949062000949062 ETH (949062 gas * 1.000000001 gwei)
#
# ✅ Sequence #1 on anvil-hardhat | Total Paid: 0.000949062000949062 ETH (949062 gas * avg 1.000000001 gwei)
# ==========================
#
# ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
#
# Transactions saved to: /path/to/create-erc20-contract-using-foundry/broadcast/BingBongToken.s.sol/31337/run-latest.json
#
# Sensitive values saved to: /path/to/create-erc20-contract-using-foundry/cache/BingBongToken.s.sol/31337/run-latest.json
Success! Make sure to stop the anvil
service in Terminal 1 by using ctrl + c
.
Deploying ERC20 Contract
NOTE: For this step, make sure to have a wallet that contains
BERA
tokens to pay for the transaction and make sure to change the wallet set viacast wallet import MyBeraAccount --interactive
.
With a local node configure, the deployment to Berachain Testnet should be the same process, but with a specified RPC URL endpoint.
# FROM ./create-erc20-contract-using-foundry
forge script script/BingBongToken.s.sol --account MyBeraAccount --rpc-url https://bepolia.rpc.berachain.com/ --broadcast;
# [Expected Output]:
# [⠊] Compiling...
# [⠑] Compiling 1 files with Solc 0.8.26
# [⠘] Solc 0.8.26 finished in 706.50ms
# Compiler run successful!
# Enter keystore password:
# Script ran successfully.
#
# == Logs ==
# address: 0x93aE8591e2013Aa50D68b9a4C5005b6440EE729C <-- 0xYOUR_DEPLOYED_CONTRACT_ADDRESS
# totalSupply: 5678
#
# ## Setting up 1 EVM.
# ==========================
#
# Chain 80069
#
# Estimated gas price: 20.000000094 gwei
#
# Estimated total gas used for script: 1233780
#
# Estimated amount required: 0.02467560011597532 BERA
# ==========================
#
# ##### berachain-bepolia
# ✅ [Success] Hash: 0x4863bd410073886419247125c9fb2729d31c400878dc4d64ec145efd0b3cbd47
# Contract Address: 0x93aE8591e2013Aa50D68b9a4C5005b6440EE729C
# Block: 10306287
# Paid: 0.018981240044605914 BERA (949062 gas * 20.000000047 gwei)
#
# ✅ Sequence #1 on berachain-bepolia | Total Paid: 0.018981240044605914 BERA (949062 gas * avg 20.000000047 gwei)
#
# ==========================
#
# ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
#
# Transactions saved to: /path/to/create-erc20-contract-using-foundry/broadcast/BingBongToken.s.sol/80069/run-latest.json
#
# Sensitive values saved to:/path/to/create-erc20-contract-using-foundry/cache/BingBongToken.s.sol/80069/run-latest.json
Verifying ERC20 Contract
NOTE: In order for verification to work with Etherscan's V2 contract verification API, make sure that you have the latest version installed with
foundryup
.
Make sure to get an Etherscan API Key from https://etherscan.io/register.
# FROM ./create-erc20-contract-using-foundry
forge verify-contract \
--watch \
--chain berachain-bepolia \
<0xYOUR_DEPLOYED_CONTRACT_ADDRESS> \
src/BingBongToken.sol:BingBongToken \
--verifier etherscan \
--etherscan-api-key <YOUR_ETHERSCAN_API_V2_KEY>;
# [Expected Similar Output]:
# Start verifying contract `0xYOUR_DEPLOYED_CONTRACT_ADDRESS` deployed on berachain-bepolia
#
# Submitting verification for [src/BingBongToken.sol:BingBongToken] 0xYOUR_DEPLOYED_CONTRACT_ADDRESS.
# Submitted contract for verification:
# Response: `OK`
# GUID: `xtecz3j...`
# URL: https://testnet.berascan.com/address/0xYOUR_DEPLOYED_CONTRACT_ADDRESS
# Contract verification status:
# Response: `NOTOK`
# Details: `Pending in queue`
# Warning: Verification is still pending...; waiting 15 seconds before trying again (7 tries remaining)
# Contract verification status:
# Response: `OK`
# Details: `Pass - Verified`
# Contract successfully verified
Full Code Repository
The full github code repository can be found in the guides section of this repository under Create ERC20 Contract Using Foundry.