目录
引言
在去中心化金融(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 项目打下坚实的基础