使用 Foundry 部署 ERC20 合约
请在GitHub参阅完整的foundry-erc20代码库。
本节将展示如何使用Foundry创建新的Solidity合约,配置Berachain网络详细信息,将合约部署到Berachain,以及验证合约。
先决条件
开始之前,请确保你的本地设备上满足以下条件:
参考Foundry安装文档安装该软件。
创建ERC20合约代码设置
首先,为ERC20合约创建一个新的文件夹:
mkdir create-erc20-contract-using-foundry;
cd create-erc20-contract-using-foundry;
然后,运行以下代码,创建由Foundry定义的初始ERC20合约模板:
# 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
如果模板创建成功,会显示以下代码结构:
# FROM: ./create-erc20-contract-using-foundry
.
├── README.md
├── foundry.toml
├── lib
│ └── forge-std
├── script
│ └── Counter.s.sol
├── src
│ └── Counter.sol
└── test
└── Counter.t.sol
现在,所有代码已设置完成,运行以下代码,安装来自OpenZeppelin的ERC20合约所需的依赖项:
# 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
创建ERC20合约
开始之前,请将现有的src/Counter.sol
转换为新的BingBongToken.sol
,并将代码替换为以下 Solidity代码:
# FROM: ./create-erc20-contract-using-foundry
mv src/Counter.sol src/BingBongToken.sol;
文件位置:./src/BingBongToken.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "@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_);
}
}
运行以下代码,测试编译是否正确:
# 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"
# ...
如果出现上方显示的Expected Error Output
,原因是引用了一个不存在的文件。为了解决这个问题,需要将其重命名为BingBongToken.t.sol
,并替换一些占位符代码:
# FROM: ./create-erc20-contract-using-foundry
mv test/Counter.t.sol test/BingBongToken.t.sol;
文件位置:./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 {
}
现在,运行forge compile
时,应该显示以下结果:
# FROM: ./create-erc20-contract-using-foundry
forge compile;
# [Expected Output]:
# [⠢] Compiling...
# [⠰] Compiling 27 files with 0.8.21
# [⠃] Solc 0.8.21 finished in 6.25s
# Compiler run successful!
测试ERC20合约
使用重命名的BingBongToken.t.sol
文件,添加以下测试代码,该代码覆盖广泛的ERC20合约测试。
请检查每项测试,以便更加了解如何应对并成功处理各种情况。
文件位置:./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);
}
}
接下来,编译代码,运行测试,检查各项测试是否通过。
# 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)
为Berachain合约部署Foundry
现在,代码创建和测试都已完成,接下来创建部署BingBongToken.sol
文件所需的脚本。为此,需要将Course.s.sol
脚本文件重命名为BingBongToken.s.sol
:
# FROM: ./create-erc20-contract-using-foundry
mv script/Counter.s.sol script/BingBongToken.s.sol;
然后,添加以下代码以替换现有代码,用于导入钱包私钥和部署合约。
文件位置:./script/BingBongToken.s.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console2} from "forge-std/Script.sol";
import "../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 {
// Setup
uint256 deployerPrivateKey = vm.envUint("WALLET_PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// Deploy
BingBongToken bbt = new BingBongToken("BingBongToken", "BBT", 5678);
// Verify + End
console2.log(bbt.totalSupply());
vm.stopBroadcast();
}
}
为了验证合约是否能够正确执行,可以通过运行anvil
在本地节点上测试。请备份并保存私钥。
终端 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
# ...
使用上方代码中提供的私钥Private Key
,替换.env
文件中的WALLET_PRIVATE_KEY
。
文件位置:./.env
WALLET_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
终端 2:
在另一个终端窗口中,运行以下代码,将合约部署到本地节点RPC:
# FROM ./create-erc20-contract-using-foundry
forge script script/BingBongToken.s.sol --fork-url http://localhost:8545 --broadcast;
# [Expected Output]:
# Compiler run successful!
# Script ran successfully.
#
# == Logs ==
# 5678
# ...
# ✅ [Success]Hash: 0xc2b647051d11d8dbd88d131ff268ada417caa27e423747497b624cc3e9c75db8
# Contract Address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
# Block: 1
# ...
显示以上结果,部署成功!最后,务必使用ctrl + c
停止终端 1中的anvil
服务。
部署ERC20合约
部署之前,确保你的钱包里有足够的$BERA
代币,
以支付部署合约所需的费用,并确认已替换.env
文件中的WALLET_PRIVATE_KEY
。
使用本地节点配置,部署到Berachain测试网的流程均相似,但需要指定不同的RPC URL端点:
# FROM ./create-erc20-contract-using-foundry
forge script script/BingBongToken.s.sol --rpc-url https://bartio.rpc.berachain.com/ --broadcast;
# [Expected Output]:
# Compiler run successful!
# Script ran successfully.
#
# == Logs ==
# 5678
# ...
# ✅ [Success]Hash: 0x69aeb8ee5084c44cce00cae2fda3563bd10efb9c8c663ec7b6a6929d6d48a50e
# Contract Address: 0x01870EC5C7656723b31a884259537B183FE15Fa7
# Block: 68764
# ...
验证ERC20合约
目前,在v0.2.0
版本的forge中,合约验证存在一些问题,可能导致合约验证无法进行,尝试运行以下代码,应该有助于验证合约:
# FROM ./create-erc20-contract-using-foundry
forge verify-contract 0xYOUR_DEPLOYED_CONTRACT_ADDRESS BingBongToken \
--etherscan-api-key=xxxxx \
--watch \
--constructor-args $(cast abi-encode "constructor(string,string,uint256)" "BingBongToken" "BBT" 5678) \
--retries=2 \
--verifier-url=https://api.routescan.io/v2/network/testnet/evm/80084/etherscan/api/;
完整代码库
本节完整代码库,可在Github - Berachain Guides - foundry-erc20中查看。
最后更新于