DEX Swap 计算:正向推导与反向推导
最近在看 DEX 相关的代码,发现 AMM 的核心计算其实就围绕一个问题:怎么根据池子里的储备量,算出交易的输入和输出。
这个问题可以拆成两个方向:
- 正向推导:我手里有 100 个代币 A,丢进池子能换多少代币 B?
- 反向推导:我就想拿到 200 个代币 B,得往池子里塞多少代币 A?
两个方向的计算本质上是一回事,都是基于恒定乘积公式,只是求解的变量不同。但在实际开发中,搞清楚这个区别还是挺重要的,因为前端交互和合约逻辑都会用到不同的方向。
恒定乘积公式
Uniswap 这类 AMM 的核心就是一个简单的公式:
- :池子里代币 A 的数量
- :池子里代币 B 的数量
- :乘积常数
交易的本质就是:你往池子里放进一些代币 A,拿走一些代币 B,但交易前后的 值要保持不变(暂时忽略手续费)。听起来简单,但用起来有不少细节。
正向推导:已知卖出量,求买入量
一个具体例子
假设池子里现在有:
- 代币 A:1000 个
- 代币 B:2000 个
你想卖出 100 个代币 A,能拿到多少代币 B?
计算过程其实就四步:
第一步,算出当前的 :
第二步,你往池子里加了 100 个代币 A,池子里 A 变成:
第三步,根据 不变,算出池子里 B 应该剩下多少:
第四步,池子里原来有 2000 个 B,现在剩 1818.18 个,差的就是你拿走的:
所以你卖出 100 个 A,能拿到约 181.82 个 B。
提炼成通用公式
把上面的过程抽象一下:
化简后就是:
这个公式很简洁,实际写代码也就一行:
function getAmountOut(reserveA, reserveB, amountIn) {
return (reserveB * amountIn) / (reserveA + amountIn);
}
// 用刚才的数字验证一下
const amountOut = getAmountOut(1000, 2000, 100);
console.log(amountOut); // 181.818181...
这就是 DEX 前端最常见的"交易预览"功能背后的计算逻辑。你在输入框填一个数字,它实时算出你能拿到多少。
反向推导:已知目标买入量,求需要卖出多少
换个角度想问题
还是同一个池子:
- 代币 A:1000 个
- 代币 B:2000 个
但现在场景变了——你就想精确拿到 200 个代币 B,得卖出多少代币 A?
这个场景在套利、清算等操作中很常见,因为你通常知道自己的目标,需要反推成本。
计算过程
思路其实和正向推导一样,只是反过来:
第一步, 还是那个数:
第二步,你想拿走 200 个 B,池子里 B 剩下:
第三步,根据 不变,算出池子里 A 应该变成多少:
第四步,池子里原来只有 1000 个 A,现在变成 1111.11 个,多出来的就是你塞进去的:
所以你得卖出约 111.11 个代币 A。
通用公式
同样的,抽象一下:
注意这里分母是 ,也就是交易后池子里 B 的剩余量。这也意味着一个限制:你想拿走的 B 不能等于或超过池子里现有的 B,不然分母为零或者负数,公式就不成立了。
代码实现:
function getAmountIn(reserveA, reserveB, amountOut) {
return (reserveA * amountOut) / (reserveB - amountOut);
}
const amountIn = getAmountIn(1000, 2000, 200);
console.log(amountIn); // 111.111111...
加上手续费
上面都是理想情况,实际交易中 DEX 是要收手续费的。以 Uniswap V2 为例,手续费率是 0.3%。
手续费的逻辑很简单:你卖出 100 个代币 A,但不是全部进入池子参与计算,有 0.3% 被扣下了。所以实际进入池子的只有 个。
正向推导(含手续费)
function getAmountOutWithFee(reserveA, reserveB, amountIn, feeRate = 0.003) {
const amountInAfterFee = amountIn * (1 - feeRate);
return (reserveB * amountInAfterFee) / (reserveA + amountInAfterFee);
}
反向推导(含手续费)
反向推导加手续费稍微绕一点。因为手续费是从你卖出的代币里扣的,所以你得卖更多才能弥补这个损耗:
function getAmountInWithFee(reserveA, reserveB, amountOut, feeRate = 0.003) {
return (reserveA * amountOut) / ((reserveB - amountOut) * (1 - feeRate));
}
可以对比一下,含手续费的版本比分母多了一个 ,结果就是你需要卖出的量会比不考虑手续费时稍微多一点。
两个方向的对比
| 方向 | 已知条件 | 求解目标 | 公式 |
|---|---|---|---|
| 正向 | 卖出 | 能拿 | |
| 反向 | 想拿 | 需卖 |
直观理解:
- 正向推导是我有多少输入,看看能换来多少输出
- 反向推导是我想要多少输出,倒推得付出多少输入
两个公式的结构其实很像,只是分子分母的角色互换了。
实际开发中的应用
交易预览
这是正向推导最典型的应用。用户在 DEX 前端页面输入卖出数量,页面实时计算并展示预计获得的代币数量。每次你修改输入框的数字,前端都会重新调一次 getAmountOut。
精确交易
有些场景下你关心的是输出而非输入。比如套利策略,你知道目标池子里需要补多少代币才能把价格拉平,这时候就得用反向推导来算成本。
滑点保护
算出预期输出后,还得考虑实际执行时池子状态可能变化。所以一般会设置一个滑点容忍度:
const minAmountOut = expectedAmountOut * (1 - slippageTolerance);
如果链上实际执行时的输出低于这个最小值,交易就会回滚。这也是为什么你在 DEX 上交易时总会看到一个"最少获得"的提示。
精度问题
实际写代码时还有一个坑:JavaScript 的浮点数精度。上面的计算都用了小数,但链上合约用的是整数(带 decimals),所以在前端和合约之间传递数字时要注意单位转换。
小结
AMM 的计算核心就是恒定乘积公式 ,所有的 swap 逻辑都是从这个公式推出来的:
- 正向推导:已知输入求输出,代入公式直接算
- 反向推导:已知输出求输入,先算池子新状态再反推
- 手续费:在输入端打个折扣,公式稍微调整一下
这两个方向的计算是理解 DEX 运作机制的基础。不管是写前端交互、做链上套利,还是单纯想搞清楚自己 trades 背后的数学,都绕不开它们。
本文仅供技术学习参考,不构成任何投资建议。