返回博客列表

DEX Swap 计算:正向推导与反向推导

web3
DEXAMMSwapUniswap

⚠️ 免责声明:本文内容仅供技术学习与研究参考,不构成任何投资建议。请读者独立思考,谨慎决策。

DEX Swap 计算:正向推导与反向推导

最近在看 DEX 相关的代码,发现 AMM 的核心计算其实就围绕一个问题:怎么根据池子里的储备量,算出交易的输入和输出

这个问题可以拆成两个方向:

  1. 正向推导:我手里有 100 个代币 A,丢进池子能换多少代币 B?
  2. 反向推导:我就想拿到 200 个代币 B,得往池子里塞多少代币 A?

两个方向的计算本质上是一回事,都是基于恒定乘积公式,只是求解的变量不同。但在实际开发中,搞清楚这个区别还是挺重要的,因为前端交互和合约逻辑都会用到不同的方向。


恒定乘积公式

Uniswap 这类 AMM 的核心就是一个简单的公式:

xy=kx \cdot y = k
  • xx:池子里代币 A 的数量
  • yy:池子里代币 B 的数量
  • kk:乘积常数

交易的本质就是:你往池子里放进一些代币 A,拿走一些代币 B,但交易前后的 kk 值要保持不变(暂时忽略手续费)。听起来简单,但用起来有不少细节。


正向推导:已知卖出量,求买入量

一个具体例子

假设池子里现在有:

  • 代币 A:1000 个
  • 代币 B:2000 个

你想卖出 100 个代币 A,能拿到多少代币 B?

计算过程其实就四步:

第一步,算出当前的 kk

k=1000×2000=2000000k = 1000 \times 2000 = 2000000

第二步,你往池子里加了 100 个代币 A,池子里 A 变成:

x1=1000+100=1100x_1 = 1000 + 100 = 1100

第三步,根据 kk 不变,算出池子里 B 应该剩下多少:

y1=kx1=200000011001818.18y_1 = \frac{k}{x_1} = \frac{2000000}{1100} \approx 1818.18

第四步,池子里原来有 2000 个 B,现在剩 1818.18 个,差的就是你拿走的:

Δy=20001818.18=181.82\Delta y = 2000 - 1818.18 = 181.82

所以你卖出 100 个 A,能拿到约 181.82 个 B。

提炼成通用公式

把上面的过程抽象一下:

Δy=y0x0y0x0+Δx\Delta y = y_0 - \frac{x_0 \cdot y_0}{x_0 + \Delta x}

化简后就是:

Δy=y0Δxx0+Δx\Delta y = \frac{y_0 \cdot \Delta x}{x_0 + \Delta x}

这个公式很简洁,实际写代码也就一行:

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?

这个场景在套利、清算等操作中很常见,因为你通常知道自己的目标,需要反推成本。

计算过程

思路其实和正向推导一样,只是反过来:

第一步,kk 还是那个数:

k=2000000k = 2000000

第二步,你想拿走 200 个 B,池子里 B 剩下:

y1=2000200=1800y_1 = 2000 - 200 = 1800

第三步,根据 kk 不变,算出池子里 A 应该变成多少:

x1=ky1=200000018001111.11x_1 = \frac{k}{y_1} = \frac{2000000}{1800} \approx 1111.11

第四步,池子里原来只有 1000 个 A,现在变成 1111.11 个,多出来的就是你塞进去的:

Δx=1111.111000=111.11\Delta x = 1111.11 - 1000 = 111.11

所以你得卖出约 111.11 个代币 A。

通用公式

同样的,抽象一下:

Δx=x0Δyy0Δy\Delta x = \frac{x_0 \cdot \Delta y}{y_0 - \Delta y}

注意这里分母是 y0Δyy_0 - \Delta y,也就是交易后池子里 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% 被扣下了。所以实际进入池子的只有 100×(10.003)=99.7100 \times (1 - 0.003) = 99.7 个。

正向推导(含手续费)

Δy=y0(1r)Δxx0+(1r)Δx\Delta y = \frac{y_0 \cdot (1-r) \cdot \Delta x}{x_0 + (1-r) \cdot \Delta x}
function getAmountOutWithFee(reserveA, reserveB, amountIn, feeRate = 0.003) {
  const amountInAfterFee = amountIn * (1 - feeRate);
  return (reserveB * amountInAfterFee) / (reserveA + amountInAfterFee);
}

反向推导(含手续费)

反向推导加手续费稍微绕一点。因为手续费是从你卖出的代币里扣的,所以你得卖更多才能弥补这个损耗:

Δx=x0Δy(y0Δy)(1r)\Delta x = \frac{x_0 \cdot \Delta y}{(y_0 - \Delta y) \cdot (1-r)}
function getAmountInWithFee(reserveA, reserveB, amountOut, feeRate = 0.003) {
  return (reserveA * amountOut) / ((reserveB - amountOut) * (1 - feeRate));
}

可以对比一下,含手续费的版本比分母多了一个 (1r)(1-r),结果就是你需要卖出的量会比不考虑手续费时稍微多一点。


两个方向的对比

方向已知条件求解目标公式
正向卖出 Δx\Delta x能拿 Δy\Delta yΔy=y0Δxx0+Δx\Delta y = \frac{y_0 \cdot \Delta x}{x_0 + \Delta x}
反向想拿 Δy\Delta y需卖 Δx\Delta xΔx=x0Δyy0Δy\Delta x = \frac{x_0 \cdot \Delta y}{y_0 - \Delta y}

直观理解:

  • 正向推导是我有多少输入,看看能换来多少输出
  • 反向推导是我想要多少输出,倒推得付出多少输入

两个公式的结构其实很像,只是分子分母的角色互换了。


实际开发中的应用

交易预览

这是正向推导最典型的应用。用户在 DEX 前端页面输入卖出数量,页面实时计算并展示预计获得的代币数量。每次你修改输入框的数字,前端都会重新调一次 getAmountOut

精确交易

有些场景下你关心的是输出而非输入。比如套利策略,你知道目标池子里需要补多少代币才能把价格拉平,这时候就得用反向推导来算成本。

滑点保护

算出预期输出后,还得考虑实际执行时池子状态可能变化。所以一般会设置一个滑点容忍度:

const minAmountOut = expectedAmountOut * (1 - slippageTolerance);

如果链上实际执行时的输出低于这个最小值,交易就会回滚。这也是为什么你在 DEX 上交易时总会看到一个"最少获得"的提示。

精度问题

实际写代码时还有一个坑:JavaScript 的浮点数精度。上面的计算都用了小数,但链上合约用的是整数(带 decimals),所以在前端和合约之间传递数字时要注意单位转换。


小结

AMM 的计算核心就是恒定乘积公式 xy=kx \cdot y = k,所有的 swap 逻辑都是从这个公式推出来的:

  1. 正向推导:已知输入求输出,代入公式直接算
  2. 反向推导:已知输出求输入,先算池子新状态再反推
  3. 手续费:在输入端打个折扣,公式稍微调整一下

这两个方向的计算是理解 DEX 运作机制的基础。不管是写前端交互、做链上套利,还是单纯想搞清楚自己 trades 背后的数学,都绕不开它们。


本文仅供技术学习参考,不构成任何投资建议。