5 分钟读懂 Uniswap V2:最全核心合约解析+实战代码

引言

在去中心化金融(DeFi)领域,Uniswap V2 是最经典也是最具代表性的自动化做市商(AMM)协议之一。它通过极简而高效的合约设计,实现了无需许可的代币兑换和流动性池管理。

本文将详细解析 Uniswap V2 的核心架构:Factory、Pair 和 Router 三大合约之间的关系及交互流程,并通过 Solidity 合约示例,带你了解如何基于 Router 快速实现:添加流动性、移除流动性以及代币交换等核心功能。无论你是想开发自己的 DApp,还是想深入学习 Uniswap 工作原理,这都是非常值得掌握的基础。

一、项目结构 & 合约关系

Uniswap V2 项目通常拆分为两个仓库:
uniswap-v2-core:核心合约
uniswap-v2-periphery:外围设计,用户交互方便用的合约,比如 router

主要合约:
在这里插入图片描述

Uniswap V2 的核心合约主要有三个

UniswapV2Factory
工厂合约,用来创建和管理交易对(Pair)合约。管理所有流动性池的 “工厂”
关键点:

  • 部署新交易对(如 USDC/ETH)。
  • 存储所有已部署的交易对地址。
  • 提供查询接口(如 getPair() 返回特定 token 对应的 Pair 地址)。

UniswapV2Pair
每个交易对都会有一个独立的 Pair 合约,用于真正存储资金、计算定价和执行 swap、add/remove liquidity。每个池子一个 Pair,所有交易都发生在 Pair 合约里。
关键点:

  • 保留了 token0 和 token1 的储备量。
  • 实现核心恒定乘积公式:x * y = k。
  • 实现 swap()、mint()(增加流动性)、burn()(移除流动性)等方法。

UniswapV2Router02
是一个提供更友好的交互接口的合约,让用户或 DApp 更方便地:添加/移除流动性、进行 token 之间的 swap。是 DApp/前端最常直接交互的合约。
关键点:

  • 内部帮你调用 Pair 的 swap/mint/burn。
  • 会计算 slippage、最小金额、路径等,简化操作。

典型的流程解释:
以用户通过 Router 添加流动性为例:
在这里插入图片描述

解释:
用户只和 Router 交互,不直接调用 Pair 或 Factory。
Router 内部先去 Factory 查询 Pair 地址,然后去 Pair 完成交互。
如果 Pair 不存在,先调用 Factory 创建。
Pair 是实际存储 token 和发放 LP Token 的地方。

假设场景

你想在 Uniswap V2 上添加一个 USDC/ETH 的流动性池,也就是让别人可以方便地在 USDC 和 ETH 之间交易。

✅第 1 步:你发起请求
你在 DApp/前端(网页)点了「添加流动性」按钮。这个操作其实是要调用链上的一个合约方法。

✅ 第 2 步:前端调用 Router
前端帮你调用合约 UniswapV2Router02.addLiquidity(…)。Router 是 Uniswap 官方写好的合约,帮忙处理各种计算(比如最小价格、滑点、路径等),让用户不需要自己和底层合约打交道。

✅ 第 3 步:Router 去找池子(Pair 合约)
Router 想添加流动性,必须知道这个池子的合约地址在哪里。所以 Router 会先问 UniswapV2Factory.getPair(tokenA, tokenB):
“你有没有 USDC 和 ETH 的池子?”Factory 是一个工厂合约,里面只管理所有 Pair(池子)的地址。

✅ 第 4 步:如果池子还没创建
如果这是第一个想要创建 USDC/ETH 池子的人,那 Factory 会返回一个空地址(意思是没有)。Router 就需要先让用户调用 UniswapV2Factory.createPair(tokenA, tokenB),去链上创建一个新的池子(Pair 合约)。

✅ 第 5 步:拿到 Pair 地址后,Router 把用户的 token 转进去
用户想放多少 USDC 和多少 ETH,前端把这些信息告诉 Router。Router 调用 transferFrom 等方式,把这些 token 转到 Pair 合约里。

✅ 第 6 步:Router 调用 Pair 的 mint() 函数
mint() 是 Pair 合约的方法,用来给用户铸造 LP Token(流动性凭证)。LP Token 的作用:以后当你想撤回资金时,就用 LP Token 去 Pair 合约里 burn,取回原来的 token + 一部分手续费收益。

✅ 第 7 步:用户得到 LP Token
添加流动性完成,你拿到 LP Token。别人就可以在这个池子里自由交换 USDC ↔ ETH,你也能从手续费里获得收益。

来个比喻方便理解

Factory = 房地产开发商,建楼(创建池子)
Pair = 楼本身(池子),里面放钱、交易
Router = 中介公司,帮忙跑流程、算价钱、带你找楼

简单总结

Router:帮你计算、帮你调用,不存钱
Factory:只管理/创建池子
Pair:池子本体,真正存钱,负责 swap、mint、burn

二、创建合约实例

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.0;
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";contract UniswapV2Integration {
    address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    IUniswapV2Router02 public uniswapV2Router;

    constructor() {
        uniswapV2Router = IUniswapV2Router02(ROUTER);
    }
}

三、增加流动性:成为流动性提供者

addLiquidity() 函数允许你为交易对提供流动性,并从每笔交易中赚取费用。

function addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) external returns (uint amountA, uint amountB, uint liquidity);

参数分解

  • tokenA/tokenB:要配对的代币的合约地址
  • amountADesired/amountBDesired:你期望的存款金额
  • amountAMin/amountBMin:最小金额(滑点保护)
  • to:接收 LP 代币的地址
  • deadline:交易过期时间戳

实际实现

function addLiquidityToPool(
    address tokenA,
    address tokenB,
    uint256 amountA,
    uint256 amountB
) external {
    // Approve router to spend tokens
    // 授权路由器消费代币
    IERC20(tokenA).approve(address(uniswapV2Router), amountA);
    IERC20(tokenB).approve(address(uniswapV2Router), amountB);

    // Add liquidity with 5% slippage tolerance
    // 增加流动性,滑点容忍度为 5%
    uniswapV2Router.addLiquidity(
        tokenA,
        tokenB,
        amountA,
        amountB,
        amountA * 95 / 100,  // 5% slippage protection
        amountB * 95 / 100,  // 5% slippage protection
        msg.sender,          // LP tokens to caller
        // LP 代币给调用者
        block.timestamp + 300 // 5 minute deadline
                              // 5 分钟截止时间
    );
}

内部机制:addLiquidity() 如何工作

交易对创建检查

if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
    IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}

基于储备金的计算

对于现有的交易对,该函数维护价格比率:

amountBOptimal = amountADesired.mul(reserveB) / reserveA;

if (amountBOptimal <= amountBDesired) {
    (amountA, amountB) = (amountADesired, amountBOptimal);
} else {
    amountAOptimal = amountBDesired.mul(reserveA) / reserveB;
    (amountA, amountB) = (amountAOptimal, amountBDesired);
}

CREATE2 地址计算

Uniswap V2 使用确定性地址来提高 gas 效率:

pair = address(uint(keccak256(abi.encodePacked(
    hex'ff',
    factory,
    keccak256(abi.encodePacked(token0, token1)),
    hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'
))));

移除流动性:退出你的仓位

removeLiquidity() 函数允许你通过销毁 LP 代币来提取你的代币:

function removeLiquidity(
    address tokenA,
    address tokenB,
    uint liquidity,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) external returns (uint amountA, uint amountB);

实现示例

function removeLiquidityFromPool(
    address tokenA,
    address tokenB,
    uint256 liquidityAmount
) external {
    address pair = IUniswapV2Factory(factory).getPair(tokenA, tokenB);

    // Approve router to spend LP tokens
    // 授权路由器消费 LP 代币
    IERC20(pair).approve(address(uniswapV2Router), liquidityAmount);

    // Remove liquidity
    // 移除流动性
    uniswapV2Router.removeLiquidity(
        tokenA,
        tokenB,
        liquidityAmount,
        0, // Accept any amount of tokenA
           // 接受任何数量的 tokenA
        0, // Accept any amount of tokenB
           // 接受任何数量的 tokenB
        msg.sender,
        block.timestamp + 300
    );
}

代币互换:DeFi 交易的核心

swapExactTokensForTokens() 函数能够实现精确的代币兑换:

function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external returns (uint[] memory amounts);

高级 Swap 实现

function swapTokens(
    address tokenIn,
    address tokenOut,
    uint256 amountIn,
    uint256 slippagePercent
) external {
    // Approve router
    // 授权路由器
    IERC20(tokenIn).approve(address(uniswapV2Router), amountIn);

    // Calculate minimum output with slippage
    // 通过滑点计算最小输出
    address[] memory path = new address[](2);
    path[0] = tokenIn;
    path[1] = tokenOut;

    uint[] memory amountsOut = uniswapV2Router.getAmountsOut(amountIn, path);
    uint amountOutMin = amountsOut[1] * (100 - slippagePercent) / 100;

    // Execute swap
    // 执行兑换
    uniswapV2Router.swapExactTokensForTokens(
        amountIn,
        amountOutMin,
        path,
        msg.sender,
        block.timestamp + 300
    );
}

多跳兑换

对于没有直接交易对的代币,Uniswap 通过中间代币进行路由:

function multiHopSwap(uint256 amountIn) external {
    address[] memory path = new address[](3);
    path[0] = USDC_ADDRESS;  // Start with USDC
                               // 从 USDC 开始
    path[1] = WETH_ADDRESS;  // Route through WETH
                               // 通过 WETH 路由
    path[2] = DAI_ADDRESS;   // End with DAI
                               // 以 DAI 结束

    IERC20(USDC_ADDRESS).approve(address(uniswapV2Router), amountIn);

    uniswapV2Router.swapExactTokensForTokens(
        amountIn,
        0, // Calculate proper minimum in production
           // 在生产中计算适当的最小值
        path,
        msg.sender,
        block.timestamp + 300
    );
}

四、完整的集成示例

这是一个全面的合约,演示了所有主要的 Uniswap V2 交互:

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.0;
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";

contract UniswapV2Manager {
    IUniswapV2Router02 public immutable uniswapV2Router;
    IUniswapV2Factory public immutable uniswapV2Factory;

    constructor() {
        uniswapV2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
        uniswapV2Factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f);
    }

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountA,
        uint256 amountB,
        uint256 slippage
    ) external returns (uint256, uint256, uint256) {
        IERC20(tokenA).transferFrom(msg.sender, address(this), amountA);
        IERC20(tokenB).transferFrom(msg.sender, address(this), amountB);

        IERC20(tokenA).approve(address(uniswapV2Router), amountA);
        IERC20(tokenB).approve(address(uniswapV2Router), amountB);

        uint256 amountAMin = amountA * (100 - slippage) / 100;
        uint256 amountBMin = amountB * (100 - slippage) / 100;

        return uniswapV2Router.addLiquidity(
            tokenA,
            tokenB,
            amountA,
            amountB,
            amountAMin,
            amountBMin,
            msg.sender,
            block.timestamp + 300
        );
    }

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint256 liquidity,
        uint256 slippage
    ) external returns (uint256, uint256) {
        address pair = uniswapV2Factory.getPair(tokenA, tokenB);
        IERC20(pair).transferFrom(msg.sender, address(this), liquidity);
        IERC20(pair).approve(address(uniswapV2Router), liquidity);

        // Get current reserves to calculate minimums
        // 获取当前储备金以计算最小值
        (uint256 reserveA, uint256 reserveB,) = IUniswapV2Pair(pair).getReserves();
        uint256 totalSupply = IERC20(pair).totalSupply();

        uint256 amountAMin = (reserveA * liquidity / totalSupply) * (100 - slippage) / 100;
        uint256 amountBMin = (reserveB * liquidity / totalSupply) * (100 - slippage) / 100;

        return uniswapV2Router.removeLiquidity(
            tokenA,
            tokenB,
            liquidity,
            amountAMin,
            amountBMin,
            msg.sender,
            block.timestamp + 300
        );
    }

    function swapExactTokensForTokens(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 slippage
    ) external returns (uint256[] memory) {
        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
        IERC20(tokenIn).approve(address(uniswapV2Router), amountIn);

        address[] memory path = new address[](2);
        path[0] = tokenIn;
        path[1] = tokenOut;

        uint256[] memory amountsOut = uniswapV2Router.getAmountsOut(amountIn, path);
        uint256 amountOutMin = amountsOut[1] * (100 - slippage) / 100;

        return uniswapV2Router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            path,
            msg.sender,
            block.timestamp + 300
        );
    }
}

总结

Uniswap V2 之所以能够成为 DeFi 世界的里程碑,核心在于其简洁而强大的合约设计:
Factory 合约:只负责创建和管理所有交易对(Pair)的地址。
Pair 合约:是真正存储资金、执行 swap、mint、burn 的地方,也是 LP Token 的发行者。
Router 合约:是用户最常接触的界面,封装了复杂计算与合约交互,让开发者和用户都能方便使用。

在实际开发中,只需与 Router 交互,就能完成大部分需求:添加/移除流动性,进行代币兑换,甚至多跳兑换等复杂操作。

希望通过本篇解析和示例,你能对 Uniswap V2 的底层逻辑、合约关系以及实际代码实现都有更清晰的认识,也为你后续开发 DeFi 项目打下坚实的基础

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yoona1020

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值