返回知识库
Day 6

Solidity进阶与Uniswap Router合约解读

学习Solidity继承、接口、错误处理,深入解析Uniswap V2 Router合约和Swap流程

2025-01-15
SolidityUniswapRouterSwapAMMDeFi

核心概念

今天学什么?

  • 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兼容ERC200xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

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() → 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
  • WETH() → 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

特性V2V3
流动性全范围均匀分布集中在指定价格区间
资本效率1x最高4000x
LP凭证ERC20 TokenNFT (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 PeripheryRouter源码

工具网站

工具用途
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组件
  • 代码提交