Solidity进阶与Uniswap Router合约解读
学习Solidity继承、接口、错误处理,深入解析Uniswap V2 Router合约和Swap流程
核心概念
今天学什么?
- Solidity进阶语法(继承、接口、错误处理)
- Uniswap架构(Factory/Router/Pair)
- Router核心函数解析
- 完整Swap流程
为什么要读Uniswap合约?
- Uniswap是DeFi基础设施,几乎所有DEX都参考其设计
- 理解Router/Pair分离架构
- 学习生产级合约的代码风格
- 面试高频考点
Solidity 进阶语法
接口 (Interface)
接口定义合约必须实现的函数,不包含实现代码。
// 定义接口
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
}
// 使用接口调用其他合约
contract MyContract {
function checkBalance(address token, address user) external view returns (uint256) {
return IERC20(token).balanceOf(user);
}
function sendToken(address token, address to, uint256 amount) external {
IERC20(token).transfer(to, amount);
}
}继承 (Inheritance)
// 基础合约
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
// 单继承
contract MyToken is Ownable {
function mint() public onlyOwner {
// 自动拥有owner和onlyOwner
}
}
// 多重继承
contract MyContract is Ownable, Pausable, ReentrancyGuard {
// 继承多个合约的功能
}抽象合约 (Abstract)
// 有未实现函数的合约
abstract contract Animal {
function speak() public virtual returns (string memory); // 未实现
function eat() public pure returns (string memory) {
return "eating"; // 已实现
}
}
// 子合约必须实现所有抽象函数
contract Dog is Animal {
function speak() public pure override returns (string memory) {
return "woof";
}
}特殊函数
receive 和 fallback
contract Wallet {
// 接收纯ETH转账 (msg.data为空)
receive() external payable {
emit Received(msg.sender, msg.value);
}
// 调用不存在的函数 或 带data的ETH转账
fallback() external payable {
// 兜底处理
}
}调用逻辑流程
发送ETH/调用到合约
│
▼
msg.data 为空?
┌───┴───┐
是 否
▼ ▼
receive() 函数存在?
存在? ┌──┴──┐
┌─┴─┐ 是 否
是 否 ▼ ▼
▼ ▼ 执行 fallback()
执行 fallback 存在?
存在? ┌─┴─┐
┌─┴─┐ 是 否
是 否 ▼ ▼
▼ ▼ 执行 revert
执行 revert错误处理
require / revert / assert
contract ErrorHandling {
// require - 检查条件,失败退还剩余Gas
function transfer(address to, uint256 amount) public {
require(to != address(0), "Invalid address");
require(balances[msg.sender] >= amount, "Insufficient balance");
}
// revert - 手动回滚,适合复杂条件判断
function withdraw(uint256 amount) public {
if (amount > balance) {
revert("Amount too large");
}
if (paused) {
revert("Contract paused");
}
}
// assert - 检查内部错误(不应该发生的情况)
function divide(uint256 a, uint256 b) public pure returns (uint256) {
assert(b != 0); // 失败说明代码有bug
return a / b;
}
}自定义错误 (Custom Errors) - 更省Gas
// 定义错误类型
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized(address caller);
error DeadlineExpired(uint256 deadline, uint256 currentTime);
contract MyContract {
function withdraw(uint256 amount) public {
if (balances[msg.sender] < amount) {
revert InsufficientBalance(balances[msg.sender], amount);
}
}
}Gas对比
| 方式 | Gas消耗 | 说明 |
|---|---|---|
| require("message") | ~500 | 存储字符串 |
| revert CustomError() | ~100 | 只存4字节选择器 |
Uniswap V2 架构
核心合约关系
Uniswap V2 架构图:
┌─────────────────┐
│ 用户 │
└────────┬────────┘
│ 调用
▼
┌─────────────────┐ 查询Pair ┌─────────────────┐
│ Router │ ←───────────────→│ Factory │
│ 路由合约 │ │ 工厂合约 │
│ 0x7a250d56... │ │ 0x5C69bEe7... │
└────────┬────────┘ └────────┬────────┘
│ │
│ 调用swap │ 创建
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Pair │ ←─────────────────│ Pair │
│ USDC/WETH │ │ DAI/WETH │
│ 交易对合约 │ │ │
└─────────────────┘ └─────────────────┘
用户只和 Router 交互,Router 处理所有复杂逻辑各合约职责
| 合约 | 职责 | 主网地址 |
|---|---|---|
| Factory | 创建和注册所有交易对 | 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f |
| Router | 用户入口,多跳/ETH处理 | 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D |
| Pair | 单个交易对,持有流动性,执行swap | 每对不同地址 |
| WETH | 包装ETH,使ETH兼容ERC20 | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 |
Router 核心函数
1. swapExactTokensForTokens
用精确数量的输入Token换取至少数量的输出Token
function swapExactTokensForTokens(
uint amountIn, // 输入数量(精确值)
uint amountOutMin, // 最少输出(滑点保护)
address[] calldata path, // 交换路径
address to, // 接收地址
uint deadline // 截止时间戳
) external returns (uint[] memory amounts);使用场景: "我要卖掉100 USDC,至少换回0.04 ETH"
2. swapTokensForExactTokens
用最多数量的输入Token换取精确数量的输出Token
function swapTokensForExactTokens(
uint amountOut, // 输出数量(精确值)
uint amountInMax, // 最多输入(滑点保护)
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);使用场景: "我要买0.1 ETH,最多花250 USDC"
3. ETH相关Swap
// ETH → Token (发送ETH调用)
function swapExactETHForTokens(
uint amountOutMin,
address[] calldata path, // path[0] = WETH
address to,
uint deadline
) external payable returns (uint[] memory amounts);
// Token → ETH
function swapExactTokensForETH(
uint amountIn,
uint amountOutMin,
address[] calldata path, // path[末尾] = WETH
address to,
uint deadline
) external returns (uint[] memory amounts);
// ETH → 精确数量Token
function swapETHForExactTokens(
uint amountOut,
address[] calldata path,
address to,
uint deadline
) external payable returns (uint[] memory amounts);Path 路径详解
什么是Path?
Path是交换路径数组,表示从TokenA到TokenB的中间步骤。
直接交换 (存在直接交易对):
USDC → WETH
path = [USDC地址, WETH地址]
多跳交换 (无直接交易对或流动性差):
SHIB → LINK
path = [SHIB地址, WETH地址, LINK地址]
SHIB → WETH → LINK
三跳交换:
path = [A, B, C, D]
A → B → C → D为什么需要多跳?
场景: 交换小众Token
直接路径:
SHIB → LINK ❌ 可能无此池或流动性极差
通过WETH:
SHIB → WETH → LINK ✅ 两个池都流动性好
Router自动执行多跳,用户无感知Path 在 Input Data 中的编码
path = [USDC, WETH] 编码为:
0000000000000000000000000000000000000000000000000000000000000002 ← 长度=2
000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 ← USDC
000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 ← WETH完整 Swap 流程
用户视角
1. approve: 授权Router使用你的Token
USDC.approve(Router地址, 数量)
2. swap: 调用Router执行交换
Router.swapExactTokensForETH(...)
3. 收到ETH合约内部流程
用户调用: swapExactTokensForETH(100 USDC → ETH)
┌──────────────────────────────────────────────────────┐
│ 1. Router 检查 │
│ ├── deadline 未过期? │
│ └── 预计算输出 >= amountOutMin? │
├──────────────────────────────────────────────────────┤
│ 2. Router 调用 USDC.transferFrom(用户, Pair, 100e6) │
│ └── 需要用户提前 approve │
├──────────────────────────────────────────────────────┤
│ 3. Router 调用 Pair.swap() │
│ ├── Pair 计算输出数量 (x*y=k) │
│ └── Pair 发送 WETH 给 Router │
├──────────────────────────────────────────────────────┤
│ 4. Router 调用 WETH.withdraw() │
│ └── WETH 解包为 ETH │
├──────────────────────────────────────────────────────┤
│ 5. Router 发送 ETH 给用户 │
│ └── 交易完成! │
└──────────────────────────────────────────────────────┘
触发事件:
├── Transfer(用户, Pair, 100 USDC)
├── Sync(reserve0, reserve1)
├── Swap(sender, amount0In, amount1Out, to)
├── Transfer(Pair, Router, WETH)
└── Withdrawal(Router, ETH amount)AMM 核心公式
恒定乘积公式
x * y = k
x = Token A 储备量
y = Token B 储备量
k = 常数(交易前后保持不变)
交易时:
(x + Δx) * (y - Δy) = k
Δy = y - k/(x + Δx)价格影响示例
池子状态: 1000 ETH + 2,000,000 USDC
k = 1000 * 2000000 = 2,000,000,000
用 10,000 USDC 买 ETH:
新USDC储备 = 2,000,000 + 10,000 = 2,010,000
新ETH储备 = k / 2,010,000 = 995.02 ETH
获得ETH = 1000 - 995.02 = 4.98 ETH
实际价格: 10000 / 4.98 = 2008 USDC/ETH
池子价格: 2000 USDC/ETH
滑点: (2008-2000)/2000 = 0.4%链上实操记录
在 Etherscan 查看 Router
1. 打开: https://etherscan.io/address/0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
2. Contract → Read Contract:
factory()→ 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6fWETH()→ 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
3. 查看交易列表,找一笔 Swap
解析 Swap 交易
函数选择器: 0x38ed1739 = swapExactTokensForTokens
Input Data 解析:
├── amountIn: 要卖的Token数量
├── amountOutMin: 最少获得数量(滑点保护)
├── path: [TokenA, TokenB, ...]
├── to: 接收地址
└── deadline: 截止时间戳
Logs 事件:
├── Transfer: Token转移记录
├── Sync: 池子储备更新
└── Swap: 交换详情Uniswap V2 vs V3
| 特性 | V2 | V3 |
|---|---|---|
| 流动性 | 全范围均匀分布 | 集中在指定价格区间 |
| 资本效率 | 1x | 最高4000x |
| LP凭证 | ERC20 Token | NFT (ERC721) |
| 费率 | 固定0.3% | 0.05%/0.3%/1%可选 |
| 复杂度 | 简单 | 复杂 |
| 适用场景 | 长尾资产 | 主流交易对 |
今日思考
问题1: 为什么Router和Pair要分开?
- 关注点分离: Pair专注于AMM逻辑,Router处理用户交互
- 可升级性: Router可以升级而不影响流动性
- 复杂逻辑: 多跳、ETH包装等逻辑不应在Pair中
- Gas优化: Pair代码精简,部署成本低
问题2: deadline参数有什么作用?
- 防止交易长时间pending后以过时价格成交
- 保护用户免受价格大幅波动影响
- 通常设置为当前时间+10~30分钟
- MEV保护的一部分
问题3: 为什么要用WETH而不是ETH?
- ETH不是ERC20,无法用统一接口处理
- WETH是ETH的ERC20包装版本,1:1兑换
- 简化合约逻辑,所有Token用同一套接口
- Router自动处理ETH↔WETH转换
学习资源
视频教程
| 资源 | 语言 | 说明 |
|---|---|---|
| Uniswap V2 详解 - Smart Contract Programmer | 英文 | 代码级讲解 |
| AMM原理 - Finematics | 英文 | 动画讲解 |
文档阅读
| 资源 | 说明 |
|---|---|
| Uniswap V2 Docs | 官方文档 |
| Uniswap V2 白皮书 | 10页,必读 |
| Uniswap V2 Core | 核心合约源码 |
| Uniswap V2 Periphery | Router源码 |
工具网站
| 工具 | 用途 |
|---|---|
| Etherscan | 查看合约和交易 |
| Uniswap Info | 池子数据分析 |
| DEX Screener | 多链DEX数据 |
面试题准备
Q: Uniswap的Router和Pair有什么区别?
30秒版本:
Router是用户交互入口,处理多跳交换、ETH包装、deadline检查等复杂逻辑;Pair是实际持有流动性的合约,每个交易对一个Pair,只负责核心的swap和流动性操作。用户调用Router,Router内部调用Pair完成实际交易。
追问: 为什么这样设计?
- 关注点分离,Pair代码精简
- Router可升级而不影响流动性
- 降低Pair部署成本
Q: 什么是滑点?如何保护?
30秒版本:
滑点是预期价格和实际成交价格的差异,由价格波动和交易对池子的影响造成。Uniswap通过amountOutMin参数保护:用户设置可接受的最低输出数量,实际输出低于此值则交易回滚。前端通常提供0.5%-1%的滑点设置。
追问: 大额交易滑点大怎么办?
- 拆分成多笔小交易
- 使用聚合器(1inch)路由到多个池
- 选择流动性更深的池
Q: 为什么swap前需要approve?
30秒版本:
ERC20标准规定合约不能直接转移用户代币。用户需要先调用Token合约的approve函数,授权Router合约一定额度,Router才能调用transferFrom把用户的代币转给Pair。这是DeFi的基础安全模式,确保用户对资产有完全控制权。
Q: Uniswap的x*y=k公式是什么意思?
30秒版本:
这是恒定乘积做市商公式。x和y是池中两种Token的储备量,k是常数。交易时,一种Token增加,另一种必须减少以保持k不变。这自动形成价格发现机制:买入越多,价格越贵;卖出越多,价格越低。
明日预告
Day 7: 复习 + 完善Portfolio组件
- Week 1 知识点复盘
- 在Uniswap测试网Swap实操
- 优化项目Portfolio组件
- 代码提交