UniswapV3 技术学习系列(九):合约部署与本地测试
系列文章导航
本文是 UniswapV3 技术学习系列的第九篇,属于"里程碑 1:第一次Swap"模块的最后一篇。在前面的文章中,我们已经完成了核心池合约和管理合约的实现,现在是时候将这些合约部署到本地以太坊网络进行测试了。本文将介绍如何选择和配置本地区块链网络、编写部署脚本、与已部署合约交互,以及理解 ABI(应用程序二进制接口)的作用。通过本文,你将掌握使用 Foundry 工具链进行智能合约部署和测试的完整流程。
原文链接: Deployment - Uniswap V3 Development Book
一、选择本地区块链网络
1.1 本地开发网络的必要性
智能合约开发需要一个本地区块链环境,让我们能够在开发和测试阶段快速迭代。一个理想的本地开发网络应该具备以下特性:
🔗 真实的区块链环境
-
• 必须是真正的以太坊网络实现,而不是模拟器 -
• 确保合约在本地网络的行为与主网完全一致 -
• 兼容以太坊的所有特性和规范
⚡ 极快的交易速度
-
• 交易立即被打包,无需等待区块确认 -
• 支持快速迭代开发 -
• 提升开发效率
💰 无限的测试以太币
-
• 可以生成任意数量的 ETH 用于支付 Gas 费用 -
• 无需从水龙头获取测试币 -
• 多个账户预充值,方便测试
🧪 强大的测试作弊码(Cheat Codes)
-
• 支持在任意地址部署合约 -
• 可以模拟任意地址发送交易(地址伪装) -
• 能够直接修改合约状态 -
• 支持时间旅行、快照回滚等高级功能
1.2 主流解决方案对比
目前有三种主流的本地开发网络解决方案:
|
|
|
|
|
|
|
| Ganache |
|
|
|
|
|
| Hardhat |
|
|
|
|
|
| Anvil |
|
|
|
|
|
1.3 为什么选择 Anvil?
我们选择 Anvil(Foundry 的一部分) 作为本地开发网络,原因如下:
-
1. 技术栈统一:测试和部署脚本都使用 Solidity 编写,无需切换到 JavaScript -
2. 性能卓越:基于 Rust 构建,编译和执行速度极快 -
3. 现代化设计:原生支持 Solidity 0.8+ 的所有特性 -
4. 强大的作弊码:提供最丰富的测试辅助功能 -
5. 无缝集成:与 Forge 测试框架完美配合
💡 开发趋势:Solidity 开发者社区正在从 Ganache → Hardhat → Foundry 逐步迁移,Foundry 代表了智能合约开发工具的未来方向。
二、启动本地区块链节点
2.1 运行 Anvil
Anvil 无需任何配置即可运行,只需一条命令:
$ anvil --code-size-limit 50000
输出示例:
_ _
(_) | |
__ _ _ __ __ __ _ | |
/ _` | | '_ \ \ \ / / | | | |
| (_| | | | | | \ V / | | | |
\__,_| |_| |_| \_/ |_| |_|
0.1.0 (d89f6af 2022-06-24T00:15:17.897682Z)
https://github.com/foundry-rs/foundry
Available Accounts
==================
(0) 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
(1) 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH)
...
Private Keys
==================
(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
...
Listening on 127.0.0.1:8545
2.2 理解启动参数
--code-size-limit 50000
这个参数非常重要,它允许部署超过以太坊标准大小限制的合约。
-
• 以太坊合约大小限制:24,576 字节(24 KB) -
• 为什么需要提高限制:在开发阶段,合约通常包含调试信息和未优化的代码 -
• 生产环境注意:主网部署前必须将合约优化到标准大小以内
2.3 Anvil 的默认配置
|
|
|
|
| 账户数量 |
|
|
| 初始余额 |
|
|
| 网络 ID |
|
|
| RPC 地址 |
|
|
| 区块时间 |
|
|
⚠️ 注意:Anvil 只运行单个节点,不是完整的网络,但这对开发测试来说完全足够。
2.4 测试 JSON-RPC 接口
Anvil 提供标准的以太坊 JSON-RPC API 接口,我们可以通过多种方式与其交互。
方式 1:使用 curl(底层方式)
# 查询链 ID
$ curl -X POST -H 'Content-Type: application/json' \
--data '{"id":1,"jsonrpc":"2.0","method":"eth_chainId"}' \
http://127.0.0.1:8545
# 返回:{"jsonrpc":"2.0","id":1,"result":"0x7a69"}
# 0x7a69 = 31337(十进制)
# 查询账户余额
$ curl -X POST -H 'Content-Type: application/json' \
--data '{"id":1,"jsonrpc":"2.0","method":"eth_getBalance","params":["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","latest"]}' \
http://127.0.0.1:8545
# 返回:{"jsonrpc":"2.0","id":1,"result":"0x21e19e0c9bab2400000"}
# 0x21e19e0c9bab2400000 = 10000 ETH
方式 2:使用 cast(推荐方式)
Cast 是 Foundry 提供的命令行工具,封装了 JSON-RPC 调用:
# 查询链 ID
$ cast chain-id
31337
# 查询账户余额(返回 Wei)
$ cast balance 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
10000000000000000000000
# 转换为 ETH 单位
$ cast balance 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 --ether
10000.000000000000000000
💡 提示:在实际开发中,优先使用
cast命令,它更简洁、易读,并且提供了丰富的辅助功能。
三、编写部署脚本
3.1 理解合约部署流程
合约部署本质上是三个步骤:
1. 编译 Solidity 源码
↓
2. 将字节码打包成交易并广播
↓
3. 节点执行构造函数并存储合约代码
在实际项目中,部署通常涉及多个步骤:
-
• 📋 准备部署参数 -
• 🔧 部署辅助合约(如代币合约) -
• 🏗️ 部署主合约 -
• ⚙️ 初始化合约状态 -
• 🔗 建立合约间的关联
3.2 使用 Solidity 编写部署脚本
Foundry 的一大特色是可以用 Solidity 编写部署脚本,保持技术栈统一。
创建部署脚本文件
在项目根目录创建 scripts/DeployDevelopment.s.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.14;
import "forge-std/Script.sol";
import "../src/UniswapV3Pool.sol";
import "../src/UniswapV3Manager.sol";
import "../test/ERC20Mintable.sol";
/**
* @title DeployDevelopment
* @notice 本地开发环境部署脚本
* @dev 部署池合约、管理合约和测试代币
*/
contract DeployDevelopment is Script {
/**
* @notice 部署脚本的主入口函数
* @dev Foundry 会自动调用这个 run() 函数
*/
function run() public {
// 部署参数定义
uint256 wethBalance = 1 ether; // 部署者的 WETH 初始余额
uint256 usdcBalance = 5042 ether; // 部署者的 USDC 初始余额
int24 currentTick = 85176; // 初始价格 Tick
uint160 currentSqrtP = 5602277097478614198912276234240; // 初始平方根价格
// 开始广播交易
vm.startBroadcast();
// 步骤 1: 部署 ERC20 代币
ERC20Mintable token0 = new ERC20Mintable("Wrapped Ether", "WETH", 18);
ERC20Mintable token1 = new ERC20Mintable("USD Coin", "USDC", 18);
// 步骤 2: 部署池合约
UniswapV3Pool pool = new UniswapV3Pool(
address(token0),
address(token1),
currentSqrtP,
currentTick
);
// 步骤 3: 部署管理合约
UniswapV3Manager manager = new UniswapV3Manager();
// 步骤 4: 为部署者铸造测试代币
// msg.sender 是发起广播交易的地址
token0.mint(msg.sender, wethBalance);
token1.mint(msg.sender, usdcBalance);
// 停止广播
vm.stopBroadcast();
// 打印部署结果
console.log("=== 部署成功 ===");
console.log("WETH 地址:", address(token0));
console.log("USDC 地址:", address(token1));
console.log("Pool 地址:", address(pool));
console.log("Manager 地址:", address(manager));
}
}
3.3 理解脚本的关键要素
Script 合约
contract DeployDevelopment is Script {
-
• 继承自 forge-std/Script.sol -
• 获得 Foundry 提供的部署能力 -
• 可以使用 vm作弊码
run() 函数
function run() public {
-
• 这是部署脚本的约定入口函数 -
• Foundry 会自动识别并执行它 -
• 函数名必须是 run
广播机制
vm.startBroadcast();
// ... 部署操作 ...
vm.stopBroadcast();
作用:
-
• startBroadcast()和stopBroadcast()之间的操作会被转换为真实交易 -
• 每个合约部署或函数调用都是一笔独立的交易 -
• 如果不使用广播,代码只会在模拟环境中执行
msg.sender 的含义:
-
• 在广播块内, msg.sender是发起交易的账户地址 -
• 通过 --private-key参数指定 -
• 所有交易的 Gas 费用由该账户支付
3.4 部署参数说明
uint256 wethBalance = 1 ether; // 1 WETH
uint256 usdcBalance = 5042 ether; // 5042 USDC
为什么是 5042 USDC?
-
• 5000 USDC:作为流动性提供到池子 -
• 42 USDC:用于测试 Swap 交易 -
• 这与我们之前的测试场景保持一致
int24 currentTick = 85176;
uint160 currentSqrtP = 5602277097478614198912276234240;
初始价格设置:
-
• 这些参数决定了池子的初始价格 -
• 对应的价格约为:1 WETH ≈ 5000 USDC -
• 与真实市场价格相近,便于理解
💡 本地 vs 主网部署:在本地网络,我们需要自己部署代币合约;在主网或测试网,代币已存在,只需要使用其地址。
四、执行部署
4.1 部署命令详解
确保 Anvil 在另一个终端窗口中运行,然后执行:
$ forge script scripts/DeployDevelopment.s.sol \
--broadcast \
--fork-url http://localhost:8545 \
--private-key $PRIVATE_KEY \
--code-size-limit 50000
参数说明
|
|
|
|
--broadcast |
|
|
--fork-url |
|
|
--private-key |
|
|
--code-size-limit |
|
|
获取私钥
从 Anvil 启动时打印的私钥列表中选择一个,例如第一个账户:
export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
⚠️ 安全警告:这是测试私钥,永远不要在主网使用!主网部署应使用硬件钱包或环境变量管理私钥。
4.2 部署过程详解
执行部署命令后,Foundry 会经历以下步骤:
1. 编译合约
├─ 编译 UniswapV3Pool.sol
├─ 编译 UniswapV3Manager.sol
├─ 编译 ERC20Mintable.sol
└─ 生成字节码和 ABI
2. 运行部署脚本
├─ 执行 run() 函数
├─ 模拟所有交易
└─ 估算 Gas 费用
3. 广播交易(因为有 --broadcast)
├─ 发送交易 1: 部署 token0
├─ 发送交易 2: 部署 token1
├─ 发送交易 3: 部署 pool
├─ 发送交易 4: 部署 manager
├─ 发送交易 5: mint WETH
└─ 发送交易 6: mint USDC
4. 等待交易确认
└─ 获取所有交易回执
5. 保存部署记录
└─ 写入 broadcast/ 目录
4.3 部署输出示例
[⠊] Compiling...
[⠒] Compiling 15 files with 0.8.30
[⠑] Solc 0.8.30 finished in 3.42s
Compiler run successful!
Script ran successfully.
Gas used: 3847592
== Logs ==
=== 部署成功 ===
WETH 地址: 0x5FbDB2315678afecb367f032d93F642f64180aa3
USDC 地址: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Pool 地址: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Manager 地址: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
## Setting up 1 EVM.
==========================
Chain 31337
Estimated gas price: 1 gwei
Estimated total gas used for script: 4256203
Estimated amount required: 0.004256203 ETH
==========================
##### anvil-hardhat
✅ [Success] Hash: 0x7a8c...1f2b
Contract Address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Block: 1
Paid: 0.000847293 ETH (847293 gas * 1 gwei)
[... 更多交易记录 ...]
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Total Paid: 0.003847592 ETH (3847592 gas * avg 1 gwei)
4.4 查看部署记录
部署成功后,交易回执会保存在 broadcast/ 目录:
broadcast/
└── DeployDevelopment.s.sol/
└── 31337/
├── run-latest.json # 最新部署记录
└── run-1234567890.json # 带时间戳的历史记录
这些文件包含:
-
• 所有交易的哈希和回执 -
• 部署的合约地址 -
• Gas 消耗统计 -
• 完整的部署流程
💡 版本控制:建议将
broadcast/目录加入.gitignore,避免提交本地部署记录。
4.5 在 Anvil 中查看交易
切换到运行 Anvil 的终端窗口,你会看到大量日志输出:
eth_sendRawTransaction
eth_getTransactionByHash
eth_getTransactionReceipt
eth_call
...
这些是 Forge 和 Anvil 之间的 RPC 通信:
-
• eth_sendRawTransaction:发送签名后的交易 -
• eth_getTransactionByHash:查询交易状态 -
• eth_getTransactionReceipt:获取交易回执 -
• eth_call:执行只读调用(不产生交易)
五、与已部署合约交互
部署完成后,我们可以通过多种方式与合约交互。让我们学习如何调用合约函数、读取状态。
5.1 理解函数选择器(Function Selector)
为什么需要函数选择器?
在以太坊中:
-
• 合约被编译为字节码存储在链上 -
• 函数名在编译后会丢失,不存储在区块链上 -
• 需要用一个固定长度的标识符来识别函数
函数选择器的计算方式
函数选择器 = keccak256(函数签名)[0:4]
示例 1:transfer 函数
// 函数签名(不包含参数名,只有类型)
transfer(address,uint256)
// 计算过程
keccak256("transfer(address,uint256)") = 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
// 取前 4 字节
函数选择器 = 0xa9059cbb
示例 2:balanceOf 函数
$ cast keccak "balanceOf(address)" | cut -b 1-10
0x70a08231
💡 注意事项:
• 函数签名中不能有空格 • 参数只写类型,不写变量名 • 返回值不包含在签名中
为什么是 4 字节?
-
• 4 字节 = 32 位 = 2³² = 4,294,967,296 种可能 -
• 碰撞概率极低(生日攻击概率约为 2⁻¹⁶) -
• 平衡了安全性和 Gas 成本
5.2 方法一:使用 curl 进行底层调用
这是最底层的合约交互方式,帮助理解 EVM 的工作原理。
场景:查询 USDC 余额
步骤 1:计算函数选择器
$ cast keccak "balanceOf(address)" | cut -b 1-10
0x70a08231
步骤 2:编码参数
查询地址:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
calldata = 函数选择器 + 参数(左填充到 32 字节)
0x70a08231
000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
完整的 calldata:
0x70a08231000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
步骤 3:发送 eth_call 请求
$ params='{"from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","to":"0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512","data":"0x70a08231000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"}'
$ curl -X POST -H 'Content-Type: application/json' \
--data '{"id":1,"jsonrpc":"2.0","method":"eth_call","params":['"$params"',"latest"]}' \
http://127.0.0.1:8545
返回结果:
{
"jsonrpc":"2.0",
"id":1,
"result":"0x00000000000000000000000000000000000000000000011153ce5e56cf880000"
}
⚠️ 注意:这里的 "to" 地址(
0xe7f1...)是 USDC 代币合约地址,从部署输出中获取。
步骤 4:解析返回值
# 转换为十进制
$ cast --to-dec 0x00000000000000000000000000000000000000000000011153ce5e56cf880000
5042000000000000000000
# 转换为 ETH 单位(实际上是 USDC,但单位相同)
$ cast --from-wei 5042000000000000000000
5042.000000000000000000
✅ 结果正确!我们铸造了 5042 USDC。
eth_call vs eth_sendTransaction
|
|
|
|
|
|
eth_call |
|
|
|
|
eth_sendTransaction |
|
|
|
|
5.3 方法二:使用 cast 进行高级调用(推荐)
Cast 封装了底层细节,提供更友好的接口。
查询当前价格和 Tick
$ cast call 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 "slot0()" | \
xargs cast --abi-decode "a()(uint160,int24)"
5602277097478614198912276234240
85176
输出解释:
-
• 第一个值:当前的 sqrtPriceX96 = 5602277097478614198912276234240 -
• 第二个值:当前的 tick = 85176
💡 技巧:
--abi-decode需要完整的函数签名,即使我们只关心返回值。这里用"a()"作为占位符。
查询流动性仓位
# 查询特定 Tick 范围的仓位信息
$ cast call 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 \
"positions(bytes32)" \
0x0000000000000000000000000000000000000000000000000000000000000001 | \
xargs cast --abi-decode "a()(uint128)"
查询代币余额(更简洁)
# 直接查询,自动解码
$ cast call 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 \
"balanceOf(address)" \
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
0x00000000000000000000000000000000000000000000011153ce5e56cf880000
发送交易(修改状态)
如果要发送真正的交易(而非只读调用),使用 cast send:
$ cast send 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 \
"transfer(address,uint256)" \
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
1000000000000000000 \
--private-key $PRIVATE_KEY
六、理解 ABI(应用程序二进制接口)
6.1 什么是 ABI?
ABI(Application Binary Interface) 是合约的"使用说明书",它是一个 JSON 文件,描述了:
-
• 📋 所有公共函数的签名 -
• 📤 函数的输入参数(名称、类型) -
• 📥 函数的返回值(类型) -
• 📡 所有事件的定义 -
• ❌ 所有自定义错误的定义
6.2 为什么需要 ABI?
问题:字节码难以理解
部署的合约字节码:
0x608060405234801561001057600080fd5b506040516108...
没有 ABI:
-
• ❌ 不知道有哪些函数 -
• ❌ 不知道函数参数类型 -
• ❌ 不知道如何编码调用数据 -
• ❌ 不知道如何解码返回值
有了 ABI:
-
• ✅ 自动生成函数选择器 -
• ✅ 自动编码参数 -
• ✅ 自动解码返回值 -
• ✅ 类型安全的合约调用
6.3 生成 ABI
使用 Forge 生成合约 ABI:
$ forge inspect UniswapV3Pool abi
输出示例(节选):
[
{
"type": "function",
"name": "mint",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
},
{
"name": "lowerTick",
"type": "int24",
"internalType": "int24"
},
{
"name": "upperTick",
"type": "int24",
"internalType": "int24"
},
{
"name": "amount",
"type": "uint128",
"internalType": "uint128"
},
{
"name": "data",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [
{
"name": "amount0",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "swap",
"inputs": [
{
"name": "recipient",
"type": "address",
"internalType": "address"
},
{
"name": "zeroForOne",
"type": "bool",
"internalType": "bool"
},
{
"name": "amountSpecified",
"type": "int256",
"internalType": "int256"
},
{
"name": "sqrtPriceLimitX96",
"type": "uint160",
"internalType": "uint160"
},
{
"name": "data",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [
{
"name": "amount0",
"type": "int256",
"internalType": "int256"
},
{
"name": "amount1",
"type": "int256",
"internalType": "int256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "event",
"name": "Mint",
"inputs": [
{
"name": "sender",
"type": "address",
"indexed": true
},
{
"name": "owner",
"type": "address",
"indexed": true
},
{
"name": "tickLower",
"type": "int24",
"indexed": true
},
{
"name": "tickUpper",
"type": "int24",
"indexed": false
},
{
"name": "amount",
"type": "uint128",
"indexed": false
},
{
"name": "amount0",
"type": "uint256",
"indexed": false
},
{
"name": "amount1",
"type": "uint256",
"indexed": false
}
]
}
]
6.4 ABI 的组成部分
函数条目
{
"type": "function",
"name": "mint", // 函数名
"inputs": [...], // 输入参数
"outputs": [...], // 返回值
"stateMutability": "nonpayable" // 状态可变性
}
状态可变性(stateMutability)类型:
|
|
|
|
|
|
pure |
|
|
|
|
view |
|
|
|
|
nonpayable |
|
|
|
|
payable |
|
|
|
|
事件条目
{
"type": "event",
"name": "Mint",
"inputs": [
{
"name": "sender",
"type": "address",
"indexed": true // 索引字段,可用于过滤
},
...
]
}
indexed 的作用:
-
• 最多 3 个参数可以标记为 indexed -
• 索引参数存储在日志的 topics中,便于快速检索 -
• 非索引参数存储在日志的 data中
错误条目(Solidity 0.8.4+)
{
"type": "error",
"name": "InsufficientLiquidity",
"inputs": [
{
"name": "available",
"type": "uint256"
},
{
"name": "required",
"type": "uint256"
}
]
}
6.5 ABI 的使用场景
1. 前端应用
使用 ethers.js 或 web3.js 与合约交互:
import { ethers } from 'ethers';
import UniswapV3PoolABI from './UniswapV3Pool.json';
const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545');
const pool = new ethers.Contract(poolAddress, UniswapV3PoolABI, provider);
// 调用 slot0() 函数
const [sqrtPriceX96, tick] = await pool.slot0();
console.log('Current price:', sqrtPriceX96.toString());
console.log('Current tick:', tick);
// 调用 mint() 函数
const tx = await pool.mint(owner, lowerTick, upperTick, amount, data);
await tx.wait();
2. 其他合约调用
在 Solidity 中导入接口:
import "./interfaces/IUniswapV3Pool.sol";
contract MyContract {
function getPoolPrice(address poolAddress) public view returns (uint160, int24) {
IUniswapV3Pool pool = IUniswapV3Pool(poolAddress);
return pool.slot0();
}
}
3. 工具和脚本
使用 cast 或其他工具自动解析:
# cast 会自动读取 ABI 文件(如果在 out/ 目录中)
$ cast call $POOL_ADDRESS "slot0()"
6.6 保存 ABI 到文件
# 保存为 JSON 文件
$ forge inspect UniswapV3Pool abi > abi/UniswapV3Pool.json
# 或者使用 build 命令,ABI 会自动生成到 out/ 目录
$ forge build
生成的文件结构:
out/
└── UniswapV3Pool.sol/
└── UniswapV3Pool.json
├── abi # ABI 定义
├── bytecode # 部署字节码
├── deployedBytecode # 运行时字节码
└── metadata # 元数据
七、完整的部署与测试流程
现在让我们把所有步骤串联起来,完成一个完整的部署和测试流程。
7.1 准备工作
# 1. 确保在项目根目录
cd /path/to/uniswapv3_tech
# 2. 编译合约
forge build
# 3. 运行测试(可选,确保合约正确)
forge test
7.2 启动本地节点
在一个终端窗口中:
$ anvil --code-size-limit 50000
保持这个窗口打开,记下第一个账户的私钥。
7.3 设置环境变量
在另一个终端窗口中:
# 设置私钥(使用 Anvil 提供的测试私钥)
export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# 设置 RPC URL(可选,默认就是这个)
export RPC_URL=http://localhost:8545
7.4 执行部署
$ forge script scripts/DeployDevelopment.s.sol \
--broadcast \
--fork-url $RPC_URL \
--private-key $PRIVATE_KEY \
--code-size-limit 50000 \
-vvv
添加 -vvv 可以看到详细的执行日志。
7.5 记录部署地址
从部署输出中复制合约地址并保存:
export WETH=0x5FbDB2315678afecb367f032d93F642f64180aa3
export USDC=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
export POOL=0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
export MANAGER=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
export USER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
7.6 验证部署
# 检查代币余额
$ cast call $WETH "balanceOf(address)" $USER | cast --from-wei
1.0
$ cast call $USDC "balanceOf(address)" $USER | cast --from-wei
5042.0
# 检查池子状态
$ cast call $POOL "slot0()" | xargs cast --abi-decode "a()(uint160,int24)"
5602277097478614198912276234240 [5.602e30]
85176 [8.517e4]
# 检查池子的代币地址
$ cast call $POOL "token0()"
0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3 # WETH
$ cast call $POOL "token1()"
0x000000000000000000000000e7f1725e7734ce288f8367e1bb143e90bb3f0512 # USDC
7.7 测试提供流动性
现在我们可以通过 Manager 合约提供流动性:
# 首先授权 Manager 使用我们的代币
$ cast send $WETH "approve(address,uint256)" $MANAGER $(cast --to-wei 1) \
--private-key $PRIVATE_KEY
$ cast send $USDC "approve(address,uint256)" $MANAGER $(cast --to-wei 5000) \
--private-key $PRIVATE_KEY
# 编码回调数据(非常重要!)
# Manager 需要这些信息来转移代币
$ DATA=$(cast abi-encode "f(address,address,address)" $WETH $USDC $USER)
# 通过 Manager 提供流动性
# 注意:使用正确的流动性值(通过 unimath.py 计算得出)
$ cast send $MANAGER "mint(address,int24,int24,uint128,bytes)" \
$POOL \
84222 \
86129 \
1517882343751509868544 \
$DATA \
--private-key $PRIVATE_KEY
⚠️ 重要提示:
1. bytes参数不能为空的0x,必须编码正确的回调数据2. 回调数据包含:token0 地址、token1 地址、支付者地址 3. 流动性值需要使用 scripts/unimath.py计算得出,不能随意填写4. 授权的代币额度会被本次交易消耗,Swap 前需要重新授权
7.8 测试 Swap
# 重要:重新授权 USDC(之前的额度已被 mint 消耗)
$ cast send $USDC "approve(address,uint256)" $MANAGER $(cast --to-wei 42) \
--private-key $PRIVATE_KEY
# 执行 Swap:用 42 USDC 买入 WETH
# 注意:Manager 的 swap 函数只有两个参数
$ cast send $MANAGER "swap(address,bytes)" \
$POOL \
$DATA \
--private-key $PRIVATE_KEY
# 检查余额变化(注意:cast call 不支持 --ether,需要用管道)
$ cast call $WETH "balanceOf(address)" $USER | cast --from-wei
0.009420095894737164
$ cast call $USDC "balanceOf(address)" $USER | cast --from-wei
0.000000000000000000
7.9 监听事件
查看池子发出的事件:
# 获取最新区块号
$ cast block-number
# 查询 Mint 事件
$ cast logs --from-block 0 --to-block latest \
--address $POOL \
"Mint(address indexed,address indexed,int24 indexed,int24,uint128,uint256,uint256)"
# 查询 Swap 事件
$ cast logs --from-block 0 --to-block latest \
--address $POOL \
"Swap(address indexed,address indexed,int256,int256,uint160,uint128,int24)"
八、常见问题与调试
8.1 部署失败的常见原因
问题 1:合约大小超限
错误信息:
Error: contract size exceeds 24576 bytes
解决方案:
# 在部署命令中添加 --code-size-limit
forge script ... --code-size-limit 50000
# 同时确保 Anvil 也设置了相同的限制
anvil --code-size-limit 50000
问题 2:Gas 不足
错误信息:
Error: insufficient funds for gas * price + value
解决方案:
-
• 确保使用的账户有足够的 ETH -
• Anvil 默认每个账户有 10,000 ETH,应该足够 -
• 检查是否使用了错误的私钥
问题 3:Nonce 不匹配
错误信息:
Error: nonce too low
解决方案:
# 重置 Anvil(重启节点)
# Ctrl+C 停止 Anvil,然后重新启动
anvil --code-size-limit 50000
8.2 交互失败的常见原因
问题 1:地址不正确
症状:
-
• 调用返回空数据 -
• 交易失败但没有明确错误
解决方案:
-
• 仔细核对合约地址 -
• 确保使用部署脚本输出的地址 -
• 使用环境变量避免复制粘贴错误
问题 2:函数签名错误
症状:
Error: function selector was not recognized
解决方案:
-
• 检查函数名拼写 -
• 确认参数类型正确 -
• 不要在函数签名中添加空格 -
• 使用 forge inspect查看准确的函数签名
问题 3:参数类型不匹配
症状:
-
• 交易 revert -
• 返回数据解码失败
解决方案:
# 使用 cast 的类型转换功能
$ cast --to-wei 1 # 转换为 wei
$ cast --to-uint256 42 # 转换为 uint256
$ cast --to-bytes32 "0x123..." # 转换为 bytes32
问题 4:环境变量未设置
症状:
error: invalid value 'balanceOf(address)' for '[TO]': invalid string length
原因:
-
• 环境变量 $WETH、$USDC等为空 -
• cast 命令无法识别地址
解决方案:
# 检查环境变量
$ echo "WETH: $WETH"
$ echo "USDC: $USDC"
# 如果为空,需要从部署记录中提取地址
$ cat broadcast/DeployDevelopment.s.sol/31337/run-latest.json | \
python3 -c "import sys, json; data = json.load(sys.stdin); \
[print(f\"{tx.get('contractName', 'Unknown'):20s} {tx.get('contractAddress', 'N/A')}\") \
for tx in data.get('transactions', []) if tx.get('transactionType') == 'CREATE']"
# 然后设置环境变量
$ export WETH=0x5fbdb2315678afecb367f032d93f642f64180aa3
$ export USDC=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512
# ... 设置其他变量
问题 5:cast call 使用 --ether 参数报错
症状:
error: unexpected argument '--ether' found
原因:
-
• cast call命令不支持--ether参数 -
• --ether只能用于cast balance等命令
解决方案:
# 错误用法
$ cast call $WETH "balanceOf(address)" $USER --ether
# 正确用法:使用管道传递给 cast --from-wei
$ cast call $WETH "balanceOf(address)" $USER | cast --from-wei
问题 6:mint/swap 执行失败,提示 execution reverted
症状:
Error: Failed to estimate gas: server returned an error response:
error code 3: execution reverted, data: "0x"
常见原因及解决方案:
原因 1:未授权代币
# 检查授权额度
$ cast call $WETH "allowance(address,address)" $USER $MANAGER | cast --from-wei
# 如果为 0,需要授权
$ cast send $WETH "approve(address,uint256)" $MANAGER $(cast --to-wei 1) \
--private-key $PRIVATE_KEY
原因 2:授权额度已用完
# mint 操作会消耗授权额度
# swap 前需要重新授权
$ cast send $USDC "approve(address,uint256)" $MANAGER $(cast --to-wei 42) \
--private-key $PRIVATE_KEY
原因 3:回调数据编码错误
# 错误:使用空的 bytes
$ cast send $MANAGER "mint(...)" ... 0x ...
# 正确:编码回调数据
$ DATA=$(cast abi-encode "f(address,address,address)" $WETH $USDC $USER)
$ cast send $MANAGER "mint(...)" ... $DATA ...
原因 4:流动性值太小或不正确
# 错误:随意使用小数值
$ cast send $MANAGER "mint(...)" ... 1000000000000000000 ...
# 正确:使用计算得出的精确值
$ python3 scripts/unimath.py # 获取正确的流动性值
$ cast send $MANAGER "mint(...)" ... 1517882343751509868544 ...
8.3 调试技巧
使用详细输出
# -v: 基本输出
# -vv: 更详细
# -vvv: 非常详细(包括调用栈)
# -vvvv: 最详细(包括操作码)
$ forge script ... -vvvv
使用 Foundry 的调试器
# 调试特定交易
$ cast run 0x交易哈希 --debug
检查交易回执
$ cast receipt 0x交易哈希
模拟调用(不发送交易)
# 使用 cast call 模拟执行
$ cast call $POOL "mint(...)" [参数]
九、下一步:前端集成
完成本地部署后,下一步是开发前端应用与合约交互。我们将:
-
1. 搭建 React 前端 -
• 使用 ethers.js 连接到 Anvil -
• 读取合约状态并显示 -
• 提供用户友好的交互界面 -
2. 实现核心功能 -
• 连接 MetaMask 钱包 -
• 提供流动性功能 -
• 执行 Swap 功能 -
• 实时显示价格和流动性 -
3. 优化用户体验 -
• 交易确认提示 -
• 加载状态显示 -
• 错误处理和提示 -
• Gas 费用估算
十、本章小结
在本章中,我们完成了以下内容:
核心知识点
✅ 本地开发网络的选择和配置
-
• 了解 Ganache、Hardhat、Anvil 的优劣 -
• 掌握 Anvil 的启动和配置 -
• 理解本地网络的特性和限制
✅ 使用 Solidity 编写部署脚本
-
• 创建 Script 合约 -
• 使用广播机制发送交易 -
• 编写可复用的部署逻辑
✅ 执行合约部署
-
• 使用 forge script 命令部署 -
• 理解部署参数的含义 -
• 查看和保存部署记录
✅ 与已部署合约交互
-
• 理解函数选择器的原理 -
• 使用 curl 进行底层调用 -
• 使用 cast 进行高级调用 -
• 区分只读调用和状态修改
✅ 理解和使用 ABI
-
• 掌握 ABI 的结构和作用 -
• 生成和导出 ABI 文件 -
• 在前端和合约中使用 ABI
技能提升
🚀 开发工具链熟练度
-
• Foundry 生态系统的完整使用 -
• cast 工具的各种用法 -
• JSON-RPC 接口的理解
🧪 本地测试能力
-
• 快速部署和迭代 -
• 交易和状态的验证 -
• 问题诊断和调试
📚 智能合约工程化
-
• 合约部署的最佳实践 -
• 网络特定的部署策略 -
• 版本管理和记录
关键要点
💡 部署脚本的重要性
使用 Solidity 编写部署脚本的优势:
• 与合约代码使用相同语言 • 可以复用测试中的代码 • 更容易维护和版本控制 • 支持复杂的部署逻辑
⚠️ 本地 vs 生产环境
本地部署和生产部署的区别:
• 本地:快速迭代,无限 ETH,可以调试 • 测试网:真实网络,有限资源,公开可访问 • 主网:真实资产,高昂 Gas,不可逆操作
🔐 安全注意事项
• 测试私钥永远不要用于主网 • 使用环境变量管理敏感信息 • 在主网部署前进行充分测试 • 考虑使用多签钱包部署关键合约
实践建议
-
1. 建立部署流程文档 -
• 记录所有部署步骤 -
• 记录合约地址和交易哈希 -
• 建立网络特定的配置文件 -
2. 自动化测试流程 # 创建一键部署和测试脚本
forge build && \
forge test && \
forge script scripts/DeployDevelopment.s.sol --broadcast -
3. 使用多网络配置 # 在 foundry.toml 中配置不同网络
[rpc_endpoints]
local = "http://localhost:8545"
goerli = "${GOERLI_RPC_URL}"
mainnet = "${MAINNET_RPC_URL}" -
4. 集成 CI/CD -
• 在 GitHub Actions 中自动运行测试 -
• 自动化部署到测试网络 -
• 生成部署报告
通过本章的学习,我们完成了 "里程碑 1:第一次Swap" 的所有内容。我们从零开始构建了一个可工作的 UniswapV3 池合约,实现了流动性提供和代币交换功能,并成功部署到本地网络。
在下一个里程碑中,我们将增强合约功能,支持更复杂的场景:计算输出金额、使用 Tick Bitmap 优化查找、实现通用化的 Minting 和 Swapping 等。
项目仓库
本系列教程的完整代码已开源在 GitHub:
UniswapV3 技术学习项目:
https://github.com/RyanWeb31110/uniswapv3_tech
系列学习项目:
-
• UniswapV1 技术学习:https://github.com/RyanWeb31110/uniswapv1_tech -
• UniswapV2 技术学习:https://github.com/RyanWeb31110/uniswapv2_tech -
• UniswapV3 技术学习:https://github.com/RyanWeb31110/uniswapv3_tech
欢迎 Star、Fork 和提交 Issue!让我们一起深入理解 DeFi 的核心技术。

