使用foundry编写、测试和部署简单erc20合约
- 安装foundry
在foundry的发布页,找到linux版本 https://github.com/foundry-rs/foundry/releases
通过github proxy加速,下载压缩包后解压,添加环境变量,生效,成功
$ wget https://ghproxy.net/https://github.com/foundry-rs/foundry/releases/download/nightly-9d93694e682d0b04da7c6fe1eca28565ba299874/foundry_nightly_linux_amd64.tar.gz
2025-06-20 14:43:51 (9.63 MB/s) - 已保存 “foundry_nightly_linux_amd64.tar.gz” [70098148/70098148])
$ gzip -d foundry_nightly_linux_amd64.tar.gz
$ tar -xvf foundry_nightly_linux_amd64.tar -C foundry
forge
cast
anvil
chisel
$ nano .bashrc
$ source .bashrc
$ forge -V
forge 1.2.3-nightly (9d93694e68 2025-06-20T06:02:37.745564833Z)
$ cast -V
cast 1.2.3-nightly (9d93694e68 2025-06-20T06:02:37.745564833Z)
- 从零构建web3全栈dapp应用教程
作者的github https://github.com/Luboy23/ 按照视频教程演示步骤,和github的源代码,操作
创建项目文件夹01erc20,再创建合约文件夹contracts,当前文件夹下打开vscode
$ mkdir 01erc20
$ mkdir contracts
$ cd contracts/
$ code .
初始化foundry项目,比较快
$ forge init
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment.
Installed forge-std v1.9.7
Initialized forge project
删除3个自带的文件scripts/Counter.s.sol 和 src/Counter.sol 和 test/Counter.t.sol
在src文件夹下新建LuLuCoin.sol 一段完全空的contract代码
// SPDX-License-Identifier: MIT
// 许可证声明:指定合约的版权许可为 MIT,允许代码的自由使用和修改。
pragma solidity ^0.8.20;
// 指定 Solidity 编译器的版本。这里选择的是 0.8.20,确保合约在这一版本下编译通过。
contract LuLuCoin {}
编译通过
$ forge compile
[⠃] Compiling...
[⠊] Compiling 1 files with Solc 0.8.30
[⠒] Installing Solc version 0.8.30
[⠑] Successfully installed Solc 0.8.30
[⠘] Solc 0.8.30 finished in 4.50s
Compiler run successful!
来到OpenZeppelin官网,快速构建基础合约 https://www.openzeppelin.com/solidity-contracts
OpenZeppelin官方文档,安装库 https://docs.openzeppelin.com/contracts/5.x/ 按照提示复制指令,安装成功
$ forge install OpenZeppelin/openzeppelin-contracts
Installed openzeppelin-contracts v5.3.0
文档提示修改重映射文件,路径重映射
在foundry.toml文件中,添加
remapping = ["@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"]
编译合约,通过
$ forge compile
[⠃] Compiling...
[⠢] Compiling 7 files with Solc 0.8.30
[⠆] Solc 0.8.30 finished in 159.87ms
Compiler run successful!
为合约添加功能mint和burn,直接复制源代码
编译合约,通过
$ forge compile
[⠊] Compiling...
[⠆] Compiling 1 files with Solc 0.8.30
[⠰] Solc 0.8.30 finished in 254.44ms
Compiler run successful!
开始合约测试。测试文件一般命名方式[合约名+Test.t.sol]
在test文件夹下新建LuLuCoinTest.t.sol,直接复制源代码
引入foundry内置的test.sol,LuLuCoinTest.t.sol继承该合约
使用forge compile命令编译的时候,将编译源合约,以及测试合约
一段完全空的contract代码,编译通过
import "forge-std/Test.sol";
// 导入 Foundry 的测试库,用于提供断言和测试环境控制功能。
import {LuLuCoin} from "../src/LuLuCoin.sol";
// 导入要测试的主合约 LuLuCoin。
contract LuLuCoinTest is Test {}
$ forge compile
[⠒] Compiling...
[⠒] Compiling 20 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 4.36s
Compiler run successful!
共有4个测试用例。如果测试单一函数,使用[--mt 函数名称]参数
$ forge test --mt testRevertIfUserBurn
[⠒] Compiling...
[⠘] Compiling 1 files with Solc 0.8.30
[⠃] Solc 0.8.30 finished in 3.58s
Compiler run successful!
Ran 1 test for test/LuLuCoinTest.t.sol:LuLuCoinTest
[PASS] testRevertIfUserBurn() (gas: 76178)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 12.54ms (11.18ms CPU time)
Ran 1 test suite in 65.38ms (12.54ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
如果添加-vvvvv参数,显示详细
$ forge test --mt testRevertIfUserBurn -vvvvvv
[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/LuLuCoinTest.t.sol:LuLuCoinTest
[PASS] testRevertIfUserBurn() (gas: 76178)
Traces:........
4个测试用例全部通过
$ forge test
[⠒] Compiling...
No files changed, compilation skipped
Ran 4 tests for test/LuLuCoinTest.t.sol:LuLuCoinTest
[PASS] testRevertIfUserBurn() (gas: 76178)
[PASS] testRevertIfUserMint() (gas: 18887)
[PASS] testSuccessIfOwnerBurn() (gas: 73077)
[PASS] testSuccessIfOwnerMint() (gas: 64104)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 8.87ms (1.51ms CPU time)
Ran 1 test suite in 76.82ms (8.87ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)
测试覆盖率
$ forge coverage
[⠒] Compiling...
[⠔] Compiling 27 files with Solc 0.8.30
[⠃] Solc 0.8.30 finished in 11.36s
Compiler run successful!
Analysing contracts...
Running tests...
Ran 4 tests for test/LuLuCoinTest.t.sol:LuLuCoinTest
[PASS] testRevertIfUserBurn() (gas: 76178)
[PASS] testRevertIfUserMint() (gas: 18887)
[PASS] testSuccessIfOwnerBurn() (gas: 73077)
[PASS] testSuccessIfOwnerMint() (gas: 64104)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 8.24ms (1.46ms CPU time)
Ran 1 test suite in 62.39ms (8.24ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)
╭------------------+---------------+---------------+---------------+---------------╮
| File | % Lines | % Statements | % Branches | % Funcs |
+==================================================================================+
| src/LuLuCoin.sol | 100.00% (6/6) | 100.00% (4/4) | 100.00% (0/0) | 100.00% (2/2) |
|------------------+---------------+---------------+---------------+---------------|
| Total | 100.00% (6/6) | 100.00% (4/4) | 100.00% (0/0) | 100.00% (2/2) |
╰------------------+---------------+---------------+---------------+---------------╯
本地部署
使用anvil命令,提供10个账户[10个地址+10个私钥]
$ anvil
Available Accounts
==================
Listening on 127.0.0.1:8545
在test文件夹下新建.evn文件,用于储存账户地址和私钥
设置3个账户,owner和user1和user2,就取本地网提供的前3个即可
保持anvil测试网开启,新建窗口,生效环境变量
使用forge create命令部署合约,添加参数
所有者的私钥+合约文件路径:部署的合约名+构造函数的参数[必须在最后面]
$ forge create --private-key ${OWNER_PRIVATE_KEY} src/LuLuCoin.sol:LuLuCoin --constructor-args ${OWNER_ADDRESS}
[⠒] Compiling...
No files changed, compilation skipped
Warning: Dry run enabled, not broadcasting transaction
Contract: LuLuCoin
Transaction: {
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"to": null,
"maxFeePerGas": "0x77359401",
"maxPriorityFeePerGas": "0x1",
"gas": "0x13b263",
"input": "0x608060405260405
...}
使用to-dec函数把十六进制转为十进制
$ cast to-dec 0x13b263
1290851
添加broadcast参数广播该笔交易。[路径+构造函数的参数]一定放在最后
$ forge create --private-key ${OWNER_PRIVATE_KEY} --broadcast src/LuLuCoin.sol:LuLuCoin --constructor-args ${OWNER_ADDRESS}
[⠒] Compiling...
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0x1ab1583e8ce4725a312fe1b0dac5f1d28068cbf593d5f8a047a5024c7ea26d45
回显Deployer地址就是[.env文件里的OWNER_ADDRESS]
Deployed to地址就是[已经部署的合约地址]
合约在本地测试网部署好了,anvil窗口也回显[区块高度+交易哈希+交易时间]
Transaction: 0x1ab1583e8ce4725a312fe1b0dac5f1d28068cbf593d5f8a047a5024c7ea26d45
Contract created: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Gas used: 1290851
Block Number: 1
Block Hash: 0x38a8e4cdecb5e66f31611073b20df1f50de4cb07d84f807a22361ebc4451f5a1
Block Time: "Fri, 20 Jun 2025 08:01:35 +0000"
使用cast与合约交互。把已经部署的合约地址存入.env文件,生效环境变量
先铸造100个代币 通过https://eth-converter.com/网站,将ether转为wei
交互的合约地址+交互的函数名+参数+合约所有者私钥
$ cast send ${CONTRACT_ADDRESS} "mint(uint256)" 100000000000000000000 --private-key ${OWNER_PRIVATE_KEY}
blockHash 0x8679b12f77d11e79caf06d2c33704936cb8c044640ba77ba6fd577c0870d00a4
blockNumber 2
contractAddress
cumulativeGasUsed 71831
effectiveGasPrice 885757093
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 71831
logs [{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"],"data":"0x0000000000000000000000000000000000000000000000056bc75e2d63100000","blockHash":"0x8679b12f77d11e79caf06d2c33704936cb8c044640ba77ba6fd577c0870d00a4","blockNumber":"0x2","blockTimestamp":"0x6855170c","transactionHash":"0x25c8def432415f95b76d68b1606c803fbf8e58678cac6dc7da49b9f686688a7f","transactionIndex":"0x0","logIndex":"0x0","removed":false},{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0x07883703ed0e86588a40d76551c92f8a4b329e3bf19765e0e6749473c1a84665","0x0000000000000000000000000000000000000000000000056bc75e2d63100000"],"data":"0x","blockHash":"0x8679b12f77d11e79caf06d2c33704936cb8c044640ba77ba6fd577c0870d00a4","blockNumber":"0x2","blockTimestamp":"0x6855170c","transactionHash":"0x25c8def432415f95b76d68b1606c803fbf8e58678cac6dc7da49b9f686688a7f","transactionIndex":"0x0","logIndex":"0x1","removed":false}]
status 1 (success)
transactionHash 0x25c8def432415f95b76d68b1606c803fbf8e58678cac6dc7da49b9f686688a7f
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0x5FbDB2315678afecb367f032d93F642f64180aa3
检查账户余额。因为LuLuCoin合约继承ERC20合约,父合约的函数同样可调用
对于view可见性的函数,不需要花费gas,所以不需要加private-key。最后传入[查看的账户地址]
检查owner账户余额,确实为100个
$ cast call ${CONTRACT_ADDRESS} "balanceOf(address)" ${OWNER_ADDRESS}
0x0000000000000000000000000000000000000000000000056bc75e2d63100000
$ cast to-dec 0x0000000000000000000000000000000000000000000000056bc75e2d63100000
100000000000000000000
检查user账户余额
$ cast call ${CONTRACT_ADDRESS} "balanceOf(address)" ${USER1_ADDRESS}
0x0000000000000000000000000000000000000000000000000000000000000000
果然为零
销毁40个代币。调用burn函数
$ cast send ${CONTRACT_ADDRESS} "burn(uint256)" 40000000000000000000 --private-key ${OWNER_PRIVATE_KEY}
blockHash 0x22d5809f6ce5ec155ceca07d5b7067bec8ca71dc1edf3c13c3ba2e17179afe5b
blockNumber 3
contractAddress
cumulativeGasUsed 37543
effectiveGasPrice 775567664
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 37543
logs [{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266","0x0000000000000000000000000000000000000000000000000000000000000000"],"data":"0x0000000000000000000000000000000000000000000000022b1c8c1227a00000","blockHash":"0x22d5809f6ce5ec155ceca07d5b7067bec8ca71dc1edf3c13c3ba2e17179afe5b","blockNumber":"0x3","blockTimestamp":"0x68551962","transactionHash":"0x11a5d42da19901bd68470e4dd71a54cf4e0b3941dab1c070f8a6e344f9c01ce0","transactionIndex":"0x0","logIndex":"0x0","removed":false},{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0xb90306ad06b2a6ff86ddc9327db583062895ef6540e62dc50add009db5b356eb","0x0000000000000000000000000000000000000000000000022b1c8c1227a00000"],"data":"0x","blockHash":"0x22d5809f6ce5ec155ceca07d5b7067bec8ca71dc1edf3c13c3ba2e17179afe5b","blockNumber":"0x3","blockTimestamp":"0x68551962","transactionHash":"0x11a5d42da19901bd68470e4dd71a54cf4e0b3941dab1c070f8a6e344f9c01ce0","transactionIndex":"0x0","logIndex":"0x1","removed":false}]
status 1 (success)
transactionHash 0x11a5d42da19901bd68470e4dd71a54cf4e0b3941dab1c070f8a6e344f9c01ce0
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0x5FbDB2315678afecb367f032d93F642f64180aa3
查看owner账户余额,确实为60个
$ cast call ${CONTRACT_ADDRESS} "balanceOf(address)" ${OWNER_ADDRESS}
0x00000000000000000000000000000000000000000000000340aad21b3b700000
$ cast to-dec 0x00000000000000000000000000000000000000000000000340aad21b3b700000
60000000000000000000
从owner账户给user账户转25个代币
$ cast send ${CONTRACT_ADDRESS} "transfer(address,uint256)" ${USER1_ADDRESS} 25000000000000000000 --private-key ${OWNER_PRIVATE_KEY}
blockHash 0x0d166b09249c8fe7aefebeacfb6d943d8c349668f56c0188ce4e1a0284e98a37
blockNumber 4
contractAddress
cumulativeGasUsed 52212
effectiveGasPrice 678864349
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 52212
logs [{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266","0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"],"data":"0x0000000000000000000000000000000000000000000000015af1d78b58c40000","blockHash":"0x0d166b09249c8fe7aefebeacfb6d943d8c349668f56c0188ce4e1a0284e98a37","blockNumber":"0x4","blockTimestamp":"0x68551ab8","transactionHash":"0x30026af5185678d3a9be8cfdef7f50242388c231e6560cb7cb4b43d8b061cd7b","transactionIndex":"0x0","logIndex":"0x0","removed":false}]
status 1 (success)
transactionHash 0x30026af5185678d3a9be8cfdef7f50242388c231e6560cb7cb4b43d8b061cd7b
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0x5FbDB2315678afecb367f032d93F642f64180aa3
分别检查user和owner的账户余额,各自是25个和35个
$ cast call ${CONTRACT_ADDRESS} "balanceOf(address)" ${USER1_ADDRESS}
0x0000000000000000000000000000000000000000000000015af1d78b58c40000
$ cast to-dec 0x0000000000000000000000000000000000000000000000015af1d78b58c40000
25000000000000000000
$ cast call ${CONTRACT_ADDRESS} "balanceOf(address)" ${OWNER_ADDRESS}
0x000000000000000000000000000000000000000000000001e5b8fa8fe2ac0000
$ cast to-dec 0x000000000000000000000000000000000000000000000001e5b8fa8fe2ac0000
35000000000000000000
如果销毁代币并非owner发起,而是user则会报错
$ cast send ${CONTRACT_ADDRESS} "burn(uint256)" 40000000000000000000 --private-key ${USER1_PRIVATE_KEY}
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0x118cdaa7: 00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8, data: "0x118cdaa700000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8": OwnableUnauthorizedAccount(0x70997970C51812dc3A010C7d01b50e0d17dc79C8)
使用forge下的selectors函数选择器查看错误类型
$ forge selectors find 0x118cdaa7
Searching for selector "0x118cdaa7" in the project...
Found 1 instance(s)...
╭-------+-------------------------------------+------------+----------╮
| Type | Signature | Selector | Contract |
+=====================================================================+
| Error | OwnableUnauthorizedAccount(address) | 0x118cdaa7 | LuLuCoin |
╰-------+-------------------------------------+------------+----------╯
使用user2铸造代币,也会报错。错误代码一样,也是权限问题
$ cast send ${CONTRACT_ADDRESS} "mint(uint256)" 66000000000000000000 --private-key ${USER2_PRIVATE_KEY}
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0x118cdaa7: 0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc, data: "0x118cdaa70000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc": OwnableUnauthorizedAccount(0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC)