Uniswap V2 交易价格冲击:AMM 兑换结算价的计算
目录
- 一、问题的提出:换 X 能拿到多少 Y
- 二、恒定乘积公式
- 三、合约里的实际校验方式
- 四、为什么 25 个 ETH 换不到 25 个 USDC
- 五、推导正确的兑换数量公式
- 六、加入 0.3% 手续费
- 七、关键结论
- 八、动手练习项目:链上报价器 QuoteLens
一、问题的提出:换 X 能拿到多少 Y
当你在 Uniswap 上把 ETH 换成 USDC 时,前端会显示一个”预计获得”的数字。这个数字是怎么算出来的?本文回答的就是这个核心问题:
向 AMM 存入 Δx 个代币 X,最多能取走多少个代币 Y?
理解了这个计算,你就理解了滑点、价格冲击、手续费的全部数学本质。
二、恒定乘积公式
2.1 公式与直觉
AMM 的基础原则:
x × y ≥ k
x:池中代币 X 的数量y:池中代币 Y 的数量k:恒定乘积
这个公式天然构造了两种代币之间的反比关系:一种代币被存入(变多),另一种就可以被取出(变少),完美模拟供需——某种东西越稀缺越贵。
把 y = k / x 画出来是一条双曲线,所有合法的池子状态都必须落在这条曲线上方或曲线上。
2.2 具体示例池
本文通篇使用这个例子:
- ETH/USDC 交易对
- 池中有 100 ETH 和 100 USDC(为了数字简单,不代表真实价格)
- k = 100 × 100 = 10,000
三、合约里的实际校验方式
合约并不是”先算好你能拿多少再给你”,而是采用更通用的校验:
交易前的 k ≤ 交易后的 k
也就是先让你存币、取币,最后检查一下乘积没有变小。只要不变小,交易就合法。这种”先斩后奏再验证”的方式正是 Uniswap V2 支持闪电贷(flash swap)的基础。
四、为什么 25 个 ETH 换不到 25 个 USDC
直觉上池子里 ETH 和 USDC 数量相等(100:100),“汇率”是 1:1,那存 25 ETH 取 25 USDC 行不行?
算一下就知道不行:
| 项目 | 数值 |
|---|---|
| 交易后池中 ETH | 100 + 25 = 125 |
| 交易后池中 USDC | 100 − 25 = 75 |
| 交易后乘积 | 125 × 75 = 9,375 |
| 交易前乘积 | 100 × 100 = 10,000 |
9,375 < 10,000,违反了不变量,AMM 会直接拒绝(revert)这笔交易。
这说明:池子比例给出的 1:1 只是边际价格,真实成交时你的大额买入本身会把价格推高。
五、推导正确的兑换数量公式
5.1 公式推导
设存入 Δx,取出 Δy,要求交易后乘积不小于交易前:
(x + Δx)(y − Δy) ≥ x·y
展开求解 Δy:
y − Δy ≥ x·y / (x + Δx)
Δy ≤ y − x·y / (x + Δx)
这就是最多能取走的 Y 数量公式。
5.2 代入示例逐步计算
存入 25 ETH:
- 交易后池中 ETH:100 + 25 = 125
- 为保持 k = 10,000,池中 USDC 至少要剩:10,000 / 125 = 80
- 所以最多取走:100 − 80 = 20 USDC
- 验证:125 × 80 = 10,000 ✓
结论:25 ETH 最多换 20 USDC,而不是 25 USDC。
如果你少取了(比如只取 15 USDC),交易也合法,但你吃亏了——没有最大化这次兑换的所得。
5.3 这就是滑点
- 边际价格说 1 ETH = 1 USDC;
- 实际成交均价:20 / 25 = 0.8 USDC/ETH;
- 这 20% 的差距就是这笔交易的价格冲击(滑点)。
交易量相对池子越大,滑点越严重,且关系是非线性的(双曲线越走越陡)。这也是为什么大额交易要拆单或者寻找深度更好的池子。
六、加入 0.3% 手续费
6.1 手续费只对存入的代币收取
Uniswap V2 收取 0.3% 手续费,规则是:
- 手续费只从你存入的代币里扣;
- 只有 99.7% 的存入量参与兑换计算;
- 被扣下的 0.3% 留在池子里,使 k 略微变大——这就是 LP 的收入。
6.2 含手续费的完整计算示例
还是存入 25 ETH:
- 手续费:25 × 0.3% = 0.075 ETH
- 参与兑换的有效数量:25 − 0.075 = 24.925 ETH
- 用有效数量算可取出的 USDC:100 − 10,000 / (100 + 24.925) = 100 − 10,000 / 124.925 ≈ 100 − 80.048 ≈ 19.952 USDC
- 但注意:那 0.075 ETH 手续费也留在池子里,所以交易后池子实际是 125 ETH × 80.048 USDC ≈ 10,006 > 10,000——乘积变大了,LP 们的份额升值了。
对比无手续费时的 20 USDC,你少拿了约 0.048 USDC,这正是手续费的代价。
6.3 含手续费的通用公式
Δy ≤ y − x·y / (x + 0.997·Δx)
这正是后面 UniswapV2Library.getAmountOut 的数学原型(合约里用整数运算写成 amountIn * 997 / 1000 的形式以避免小数)。
七、关键结论
- AMM 不按”标价”成交,每笔交易都沿着双曲线移动价格;
- 最大可取出量公式:
Δy = y − xy/(x + Δx),取少了是你的损失; - 手续费只从输入侧扣 0.3%,永久留在池中推高 k,是 LP 收益的来源;
- 交易越大滑点越大,且非线性恶化。
八、动手练习项目:链上报价器 QuoteLens
项目目标
亲手实现并验证本文的两个公式(无手续费版 / 0.3% 手续费版),做一个可部署到 Sepolia 的报价 + 模拟成交合约,搭配一个最小池子验证理论值与实际值完全一致。
合约要求
1. SimplePool.sol(最小恒定乘积池)
- 持有两种自部署的测试 ERC20(TokenX / TokenY,各 mint 1,000,000 给部署者);
seed(uint amountX, uint amountY):部署者一次性注入初始流动性(transferFrom 拉入),只允许调用一次;swapXForY(uint amountXIn, uint minYOut):transferFrom拉入 X;- 按含手续费公式计算
amountYOut = reserveY - (reserveX * reserveY) / (reserveX + amountXIn * 997 / 1000)(注意整数运算顺序:先乘后除); - 校验
amountYOut >= minYOut,否则自定义错误SlippageExceeded(uint expected, uint actual); - 转出 Y,并在函数末尾断言
新X余额 * 新Y余额 >= 旧X余额 * 旧Y余额(自定义错误KInvariantViolated())——这是本文第三节”事后校验”思想的直接落地;
getReserves() view。
2. QuoteLens.sol(纯函数报价库合约)
quoteNoFee(uint amountIn, uint reserveIn, uint reserveOut) pure returns (uint):实现Δy = y − xy/(x+Δx);quoteWithFee(uint amountIn, uint reserveIn, uint reserveOut) pure returns (uint):实现 0.3% 手续费版本;priceImpactBps(uint amountIn, uint reserveIn, uint reserveOut) pure returns (uint):返回这笔交易的价格冲击(基点)。提示:边际价 = reserveOut/reserveIn,成交均价 = amountOut/amountIn,冲击 = (1 − 均价/边际价) × 10000。
测试要求(Foundry)
用本文的数字做断言,让理论和代码互相印证:
- 池子 100e18 X / 100e18 Y,
quoteNoFee(25e18, ...)必须精确返回20e18; quoteWithFee(25e18, ...)返回值在19.95e18 ~ 19.96e18区间(assertApproxEqRel);swapXForY实际转出的 Y ==quoteWithFee的报价(报价器与池子逻辑一致性测试);- 直接构造”存 25 取 25”的攻击调用(可写一个恶意测试直接给池子转账后取款)验证 K 校验会 revert;
- 模糊测试:
testFuzz_KNeverDecreases(uint96 amountIn),任意输入下交易后 k 不减小。
Sepolia 部署与验证步骤
- 部署 TokenX、TokenY、SimplePool、QuoteLens;
- approve 后调用
seed(100e18, 100e18); - 在 Etherscan 上先调用
quoteWithFee(25e18, 100e18, 100e18)读到报价 ≈ 19.952e18; - approve 25e18 X,调用
swapXForY(25e18, 19.9e18),对比实际收到的 Y 与第 3 步报价一致; - 再读
getReserves,手算乘积确认 k 从 10,000e36 变成了约 10,006e36——亲眼看到手续费让 k 变大。
进阶挑战(可选)
- 给 QuoteLens 增加
quoteIn(反向问题:想取出 Δy 至少要存多少 Δx),推导公式Δx = (x·Δy / (y − Δy)) / 0.997 + 1; - 连续做 5 笔 5 ETH 的小额 swap,对比一笔 25 ETH 的大额 swap,验证”拆单不能绕过滑点”(结果应几乎相同,差异仅来自手续费的复利效应)。