Skip to main content

Command Palette

Search for a command to run...

使用foundry编写、测试和部署简单erc20合约

Published
5 min readView as Markdown
C
酷愛計算機技術Ardently Love Computer Technology 長期關註反洗錢反欺詐Long-term Focus on Anti-Money Laundering and Anti-Fraud 精通支付結算的技術、系統、流程和製度Proficient in the Technology, System, Process and Institution of Payment and Settlement
  • 安装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)

More from this blog

Penetration Test、Python、Weaponization

721 posts

微信 smartcat9999 反欺詐Anti-Fraud 反洗錢Anti-Money Laundering 反逃稅Anti-Tax Evasion 滲透測試Penetration Test 武器化Weaponization