Uniswap 是以太坊上最知名的去中心化交易所(DEX),首创 AMM 自动做市商模式,不靠订单簿,依靠「流动性池 + 恒定乘积公式 」实现代币自动兑换。
恒定乘积自动做市商公式
英文 constant product automarket maker 即 AMM
- 是恒定的值,即流动性
- 表示要换出的代币token的数量
- 表示要换入的代币token的数量 这个 在交换的时候是不变的
一个用户想要卖出 的 token,用 来表示。对于 来讲相当于用户把 数量的代币注入到 池子中 一个用户想要买入 的 token,用 来表示。对于 来讲相当于用户把 数量的代币从 池子中取出 因为要保持恒定值 不变,所以
案例:
假定一个交易对 ETH / USDT,10 ETH <==> 20000 USDT,那么流动性 = 10 × 20000 = 200000
① 用户卖出5个ETH
得:
② 用户持有 ,想买入ETH
得:
③ 有1个疯子持有10万个ETH,能换多少个USDT?这10万个ETH能卖出吗?
得:
即:可以卖出,得到的 会无限接近 池的总数 20000
结论 这个公式可以支撑token在任何价格上的买卖,也体现了价格是由市场决定的
价格问题
由上面的案例可以看到一个问题。
在交易开始时:
ETH 和 USDT 兑换的初始价格是:1个ETH 兑换 2000 USDT
USDT 和 ETH 兑换的初始价格是: 2000 个ETH 兑换 1 USDT
然后可以看到上面的案例,真实交易的结果是亏了,我们把这个情况称做为滑点,之所以出现这个情况是因为池子太浅,假如是使用 0.001ETH来兑换USDT,这时的滑点和预期价格一致
滑点(Slippage)
定义:下单时看到的预期价格 ≠ 订单实际成交价格,两者之间的差价,就叫滑点。
通俗理解:你点击下单那一刻的行情,等指令传到市场,价格已经动了,成交价“滑走了”。
两种滑点
1)负滑点(最常见、对你不利)
✅ 做多(买入):预期100,实际成交 100.5 → 买贵了
✅ 做空(卖出):预期100,实际成交 99.5 → 卖便宜了
2)正滑点(对你有利,比较少见)
✅ 做多:预期100,实际 99.7,买到更低价
✅ 做空:预期100,实际 100.4,卖出更高价
注意:止损单触发后会变成市价单,极易出现滑点! 行情跳空时,你的止损价位直接被跳过,会在更远价位成交。
滑点为什么会发生?
- 流动性不足(最重要) 买卖挂单少,你大单进场,当前价位承接盘不够,只能逐级寻找下一档价格成交。冷门品种、深夜交易滑点普遍更大。
- 行情剧烈波动(数据、突发消息、开盘跳空) 价格毫秒级跳动,订单还没送达交易所,价格已经变动。
- 网络/服务器延迟
- 市价单机制:市价单追求立刻成交,不锁死价格,天然自带滑点风险; 👉 限价单可以锁定价格,不会产生滑点,但有可能无法成交。
流动性(Liquidity)
有很多地方讲流动性的公式并不是用的 ,而是使用的
为什么要开根号❓ 比如 x和y 的单位是米,那么 的单位就是 ,即 是一个指数的关系,为了表示一个线性的关系,所以需要对 开根号
对于任何一个交易对 流动性提供者,叫 Liquidity Provider (LP)
LP给的凭证叫 Liquidity Provider Token (LPT)
初始流动性
决定了token价格
添加流动性
要求:添加流动性时不能影响市场价格
移除流动性
同样也不可以影响价格
| 操作 | ETH | USDT | ETH池总币数 | USDT池总币数 | ETH 兑换 USDT 的比例 | 备注 | 概念要点 |
|---|---|---|---|---|---|---|---|
| 初始流动性 | 5.00 | 10000.00 | 5 | 10000 | 2000 | 初始流动性:决定了TOKEN的价格 | |
| 添加流动性 | 5.00 | 10000.00 | 10 | 20000 | 2000 | ① 要保持系数不变 | 添加流动性:不可以影响价格 |
| 第一笔交易 | 15.00 | -6666.67 | 25 | 13333.33 | 533.3332 | ||
| 移除流动性 | -12.50 | -6666.67 | 12.5 | 6666.665 | 533.3332 | ① 要保持系数不变 ② 因为添加流动性时, 占用的正好是初始流动性的一半 所以移出的ETH: | 移除流动性:不可以影响价格 项目方跑路了,把币池给撤了,就是指移除了流动性 |
UniswapV2
包含的模块以及关键的合约如下:
- V2-core模块:UniswapV2Pair.sol、UniswapV2Factory.sol
- V2-pheriphery模块:UniswapV2Router02.sol
路由合约
关键函数:
- 我有确定的代币数量要换,使用
swapExactTokensForTokens - 我知道想要换出多少代币,但不我不知道要用多少来换,使用
swapTokensForExactTokens
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
案例
案例1:DAI / USDT 交换
flowchart LR
User["👤 用户"]
Router["Router02.sol"]
Pair["Pair<br/>DAI/USDT"]
User --"① swapExactTokensForTokens<br/>swapTokensForExactTokens" --> Router
Router --"② transferFrom"--> Pair
Router --> Pair
Pair --"③ swap(内部执行兑换计算) "--> Pair
Pair --"④ transfer" --> User
案例2:DAI / MKR 交换,但是没有DAI 直接交换 MKR的路径,需要经过多跳才能转换: DAI -> USDT -> MKR
flowchart LR
User["👤 用户"]
Router["Router02.sol"]
Pair1["Pair1<br/>DAI/USDT"]
Pair2["Pair2<br/>USDT/MKR"]
User --"① swapExactTokensForTokens(path=[DAI,USDT,MKR])"--> Router
Router --"② transferFrom DAI 用户→Pair1 "--> Pair1
Router --> Pair1
Pair1 --"③ swap 内部计算 DAI→USDT"--> Pair1
Pair1 --".中转USDT."--> Pair2
Router --> Pair2
Pair2 --"④ swap 内部计算 USDT→MKR"--> Pair2
Pair2 --"⑤ transfer MKR"--> User
// given an output amount of an asset and pair reserves, returns a required input amount of the other asset
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint i = path.length - 1; i > 0; i--) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
每次执行 swap 交易都会产生一笔手续费,因此普通用户通常倾向选择兑换路径最短的交易路由。但套利机器人有所不同,一旦监测到多个交易池之间存在价差失衡,即便路径更长,也会采用多跳路由完成套利。
以swapExactTokensForTokens 为例,参数详解(UniswapV2Router02)
功能:确定输入代币数量,换取尽可能多的输出代币 路径示例:path = [DAI, USDT, MKR],用固定数量 DAI 兑换 MKR
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts)
uint amountIn你愿意付出的确切代币数量uint amountOutMin最少能接受拿到多少目标代币(滑点保护)
例:预期收到 990 MKR,设置
amountOutMin = 980 * 1e18- 如果行情波动,最终能兑换到的代币 <amountOutMin→ 交易直接回滚,不成交 - 作用:抵御套利、币价剧烈波动,防止滑点亏太多
-
address[] calldata path
**兑换路径数组,决定多跳路由** 示例[DAI, USDT, MKR]` 规则:path[0]= 付出的代币path[path.length-1]= 最终收到的代币- 数组相邻两个地址构成一个 Pair:
DAI+USDT为第一池,USDT+MKR第二池 - Router 自动循环 path,串联多个 Pair 完成多跳交换
-
address to` 最终兑换出来的代币转给谁
-
uint deadline
**交易有效截止时间戳(秒,unix时间)** 搭配修饰器ensure(deadline)` 逻辑:
如果区块执行时间 ≥ deadline → revert 拒绝执行 典型用法:
block.timestamp + 600(10分钟有效期) 防止长时间挂单,行情大变后被动成交。
// given an output amount of an asset and pair reserves, returns a required input amount of the other asset
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint numerator = reserveIn.mul(amountOut).mul(1000);
uint denominator = reserveOut.sub(amountOut).mul(997);
amountIn = (numerator / denominator).add(1);
}
// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
理解997、1000这些魔法数字的含义:
公式推导
reserveIn= (输入代币储备)reserveOut= (输出代币储备)amountOut= (想要拿到的输出数量)amountIn= (需要付出的输入数量)
| 推导出等于多少❓ getAmountOut | 推导出等于多少❓ getAmountIn | ||
|---|---|---|---|
| 展开左侧乘积 | |||
| 两边消去 | |||
| 移项 | |||
| 右侧提取公因子 | 右侧提取公因子 | ||
| 两边同除以 | 两边除以 | ||
| 因为还需要考虑到收取手续费的情况 | |||
| 手续费是千3,即 | |||
| 因为计算我们不考虑小数 | |||
| 结果 |
补充关键细节
getAmountIn 代码末尾 .add(1)
amountIn = (numerator / denominator).add(1);
这是整数向上取整:Solidity 除法是向下取整,+1 保证实际输入数量足够拿到目标 amountOut,避免精度不足导致交易失败。
Swap之下的手续费增长
Uniswap V2:为什么持续 Swap 会让池子流动性(储备)持续增长
无手续费理想AMM(理论模型) 交易完成后 不变,池子总代币价值不变。 单纯交易,池子资产不会增加。
Uniswap V2(0.3%交易手续费模型) 用户转入全部输入代币 进池子; 但合约只用 参与恒定乘积公式计算兑换数量: 交易结束后池子新储备:
差额 ,也就是0.3%手续费,直接留在池子里,不发给任何人。
于是:
✅ 结论: 每一笔swap,都会有一部分代币以手续费形式沉淀在池中。 不断有人交易,储备 持续累积,池子总资产持续上涨。
这里说的“流动性增长”≠有人添加流动性(addLiquidity)。 不需要LP充币,仅靠用户反复swap,池子储备本身就能内生增长。
案例: 初始池子:DAI 100 / USDT 100 普通交易者用 DAI 兑换 USDT,池内储备变为 DAI 104、USDT 96.77,池子价格失衡; 套利机器人捕捉价差机会,快速执行反向交易,最终池内储备回归 DAI 101 / USDT 101,价格重新恢复均衡。
- 套利机器人的作用:修复 AMM 池子价格 普通交易者的单笔大额 swap 会打破(xy=k)形成的市场价格;套利机器人捕捉价格偏离,通过反向交易抹平滑点,让池子价格和外部市场保持一致。
- 收益来源 机器人低买高卖:在 DAI 相对便宜的时候买入 DAI,完成一轮操作后赚取差额利润。
关键约束:套利毛利必须覆盖 swap 手续费与 gas 成本,否则机器人不会启动。
如何让 LP (流动性提供者)受益于手续费
每一笔交易产生的手续费不会单独发放,直接留在池子中,让池内代币总量不断增加。 LP 代币总数量不变,因此每一枚 LP 凭证能兑换到的代币持续变多。LP 想要拿到手续费收益,只能销毁 LP 代币、撤出流动性赎回资产,增量部分就是手续费收益。 LP 向交易池注入代币获得 LP 凭证;交易手续费持续沉淀在池内抬高资产总值,最终 LP 撤出流动性时,取回包含本金与手续费的代币。
作为Uniswap项目方是如何受益的
Uniswap项目方会收取 protocol fee(协议费),是从 0.3% 交易手续费 里切分出一部分给项目方。
- 关闭协议费(
feeTo = 0):0.3%全部归属LP; - 开启协议费(配置
feeTo接收地址):约定比例分给协议方,剩余归LP。 V2硬编码比例:协议费占总手续费的1/6。 也就是总费率0.3%:
协议费: LP获得:
关键设计:协议费不在swap时直接转账
和LP手续费一样,swap产生的手续费先全部留在池子储备里,抬高K值。
只有触发 mint(添加流动性) / burn(移除流动性) 的瞬间,合约才会计算K的增量,铸造新的LP代币发给feeTo地址。
协议费不随每笔swap实时扣费,依靠K值增长核算;仅在增减流动性时铸造LP分给项目地址。项目方想要兑现收益,同样需要销毁收到的LP代币提取池内资产。
协议费从0.3%交易手续费中划分0.05%,手续费先存入池子推高K;仅当用户添加或移除流动性时,合约自动铸造LP代币分配给项目方地址,项目方赎回LP完成收益兑现。
的计算公式推导: 已知等式:
交叉相乘
展开右侧
把含 项移到左边
整理得到通式
带入 Uniswap V2 标准条件
V2协议费固定:
当涨幅不大时近似(工程常用简化)
,可以近似: 也就是常看到的教科书简化公式。
- :蛋糕新增增量(也就是常说的把蛋糕做大的含义)
- :约定从增量里抽取的比例(V2为1/6)
- 等式意义: 新铸造份额 占铸造后总份额 的比例 = 协议方分得价值占新蛋糕 的比例
核心思想:协议方不是切旧蛋糕,只瓜分池子膨胀出来的新增部分;铸造之后稀释全体LP,但稀释来源仅仅是手续费带来的资产增量。
Uniswap V2 白皮书 app.uniswap.org/whitepaper.…
【2.4 Protocol fee】
添加流动性
池子储备 ,流动性公式定义:
同一条双曲线 固定不变。
也就是说在 这个时刻,流动性 ,而【即是的流动性】为
理清概念:
-
LP Token 是流动性Token,是链上ERC20代币载体
-
LP Share 是流动性份额,代表你占整个池子总流动性的所有权比例
-
它是抽象权益:代表你将来赎回时,可以按比例瓜分池子里所有
x,y资产; -
数学公式里我们写的 ,全部指 LP Share。 类比: LP Share(S)= 持有的公司股份 LP Token = 记录你持有多少股份的股权证书(ERC20 代币) 举个例子 公司总共发行 100 份股权凭证(LP Token 总量 = 100) 你手里持有 10 枚 LP Token → 你拥有 10% LP Share(10% 股份)
-
池子资产价值 1000U,你理论可赎回价值 100U;
-
一天手续费赚 50U,你分到 5U。
所以用户新增流动性,应该的计算公式:
- :增资前池子总流动性
- :用户投入资产后的新总流动性
- :用户新增带来的流动性增量
- :增资前 LP Token 总发行量 totalSupply(原有全部股份总和)
- :用户本次新增、将要铸造得到的 LP Token(新股份)
等式文字翻译:新增流动性占原有流动性的比例 = 用户新拿到的LP份额 ÷ 池子原先全部LP总份额
池子 = 公司
= 公司当前总资产体量
= 公司已经发行的全部股票
用户增资注入资产,让公司体量涨到
= 用户新投入对应的资产体量
规则:你投入资产占原有公司体量的比值 = 你获得新股占原有总股本的比值
举例: ,原有总股本
- 公司原来100体量,1000股;
- 你带来20体量资产;
- 给你新发 200股。 增资之后: 总股本变为 你持股比例:
⚠️ 重要提醒: 不是你最终持股比例! 最终持股比例是 公式只是用来计算本次应该铸造多少新LP。
💡
因为:
因为:,r 是放大倍数。
所以设:
带放上面的公式
因为:
前面定义了
所以最终得到公式,即用户本次添加流动性,合约新铸造、发放给用户的 LP Token 数量:
理论上:
但是在合约计算中,会存在精度的丢失。所以在合约代码中会写成
Uniswap V2 里添加流动性的实现
flowchart LR
User["👤 用户"]
Router["UniswapV2 Router"]
Factory["UniswapV2 Factory"]
Pair["DAI/WETH Pair 池子"]
%% 1. 用户发起添加流动性
User -- "1.addLiquidity" --> Router
%% 2. Router 查询/请求创建交易对
Router -- "2.getPair / createPair" --> Factory
%% 3. Factory 使用CREATE2部署Pair(首次创建才执行)
Factory -- "3.CREATE2 部署Pair合约" --> Pair
%% 4. 转账用户代币到池子
User -- "4.transferFrom(DAI/WETH)" --> Pair
%% 5. Router调用Pair铸币
Router -- "5. mint()" --> Pair
%% 6. Pair把新铸造的LP Token转给用户
Pair -- "6.transfer LP Token" --> User
Step1: 用户调用 addLiquidity(发起交易)
用户钱包发起交易,调用 UniswapV2Router02.addLiquidity(tokenA, tokenB, amountA, amountB,...)
用户想要存入 DAI + WETH,提供流动性,换取 LP Token。 用户不会直接调用 Pair 合约,全部走 Router 做中转。
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}
// **** ADD LIQUIDITY ****
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
// 省略..........
}
Step2:Router 调用 Factory:getPair / createPair
Router 先去 Factory 查询:DAI-WETH 这个交易对是否已经存在
- ✅ 已有 Pair:直接拿到 Pair 合约地址,继续下一步
- ❌ 不存在 Pair:触发创建逻辑,执行 Step 3
Step3:Factory 通过 CREATE2 部署全新 Pair 合约
Uniswap V2 使用 CREATE2 部署 Pair,
好处:交易对地址可以离线预先算出,不需要上链查询。
部署成功后 Factory 会记录:(tokenA,tokenB) → pairAddress 的映射关系。
只有第一次创建交易对时,才会执行 Step 3;后续再加流动性直接跳过 Step 3。
Step4:transferFrom:把用户的代币转到 Pair 池子
Router 触发 token.transferFrom(用户地址, Pair地址, 数量)
前提:用户提前授权(approve) Router 操作自己的 DAI/WETH。
执行完成:用户钱包里的 DAI、WETH,转入 DAI/WETH Pair 合约内部,成为池子储备 x,y。
Step 5:Router 调用 Pair 的 mint() 铸币函数
// this low-level function should be called from a contract which performs important safety checks
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}
这里就是前面推导出来的公式:
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
mint
是最关键一步! Pair 合约拿到新增的两种代币后:
- 读取池子旧储备量
x0,y0 - 计算本次应该铸造多少 LP Token(就是公式里的 S)
- 内部执行铸币逻辑
⚠️ mint 函数铸造出来的 LP,先铸给 Pair 合约自身,不会直接发给用户
Step 6:Pair 将新铸造的 LP Token transfer 还给用户
mint 完成后,Pair 把刚生成的 LP Token 转账到用户钱包。 至此用户拿到 LP 凭证,整个 addLiquidity 流程结束。
移除流动性
移除流动性的时候, 因为:
代入到上面的公式中
因为:,r 是缩小倍数。
设:
因为: 所以:
即:
reserve0:
reserve1:
// **** REMOVE LIQUIDITY ****
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
Uniswap V2 里移除流动性的实现
flowchart LR
User["👤 用户"]
Router["UniswapV2 Router"]
Pair["DAI/WETH Pair 池子"]
%% 1. 用户发起移除流动性
User -- "1. removeLiquidity" --> Router
%% 2. transferFrom:把用户的LP Token转入Pair合约
User -- "2. transferFrom LP Token" --> Pair
%% 3. Router调用Pair.burn()销毁LP
Router -- "3. burn()" --> Pair
%% 4. Pair转出对应的两种代币返还用户
Pair -- "4. transfer DAI / WETH" --> User
Step 1: 用户调用 Router.removeLiquidity 用户发起交易,指定要销毁多少 LP Token,设置最小收到 DAI、WETH 滑点保护。
Step 2: transferFrom LP Token
用户必须提前授权 Router 操作自己的 LP;Router 执行 transferFrom,将用户钱包里的 LP Token 转移到 Pair 合约地址。
⚠️ burn 函数要求 LP 先转入 Pair,不能直接销毁用户账户上的 LP。
Step 3: Router 调用 Pair.burn ()
核心逻辑就在 burn:
- 读取当前池子储备 (x_0,y_0)、当前 LP 总供给 T
- 根据转入的 LP 数量 S,计算份额 (r=\dfrac{S}{T})
- 算出用户可取走代币:(dx=r\cdot x_0,\ dy=r\cdot y_0)
- 销毁转入的 LP Token(减少 Pair 的 totalSupply)
Step 4: Pair 把 DAI、WETH transfer 还给用户 池子扣除对应储备,将等值两种代币转账回用户地址,流程结束。
和 addLiquidity 做对称对比
- addLiquidity:代币进池子 →
mint铸造 LP 发给用户 - removeLiquidity:LP 进池子 →
burn销毁 LP,代币还给用户