第4章 去中心化交易所(DEX)原理:AMM自动做市商机制
聊到DeFi,DEX是绕不开的核心。我个人觉得,去中心化交易所的出现,真正让「交易」这件事从中心化托管走向了链上自主。今天咱们就深入聊聊AMM自动做市商机制,看看它到底是怎么运作的。
4.1 传统订单簿 vs AMM
传统交易所用的是订单簿模式。买方挂买单,卖方挂卖单,系统撮合。但链上环境不一样,每个交易都要Gas费,速度也慢。你想想看,如果每挂一个单都要等区块确认,那体验得多糟糕?
AMM的思路完全不同。它不依赖买卖双方撮合,而是用算法定价。说白了,就是有一个资金池,你直接跟池子交易。池子里的价格由数学公式决定。
核心区别:
- 订单簿:需要对手方,流动性分散
- AMM:自动定价,流动性集中到池子
4.2 恒定乘积公式
AMM最经典的模型就是恒定乘积公式。公式很简单:x * y = k。其中x和y是池子里两种代币的数量,k是常数。
举个例子。假设池子里有100个ETH和200,000个USDC。那么k = 100 * 200,000 = 20,000,000。这个k在交易过程中保持不变(不考虑手续费)。
你想用ETH换USDC。你往池子里放1个ETH,池子里的ETH变成101个。为了保持k不变,USDC的数量必须变成:20,000,000 / 101 ≈ 198,019.8。也就是说,你能拿到的USDC是200,000 - 198,019.8 = 1,980.2个。
// 恒定乘积公式核心逻辑
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) {
require(amountIn > 0, 'INSUFFICIENT_INPUT_AMOUNT');
uint amountInWithFee = amountIn * 997; // 0.3% 手续费
uint numerator = amountInWithFee * reserveOut;
uint denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
}
这段代码我实际在Uniswap V2的合约里见过很多次。注意那个997和1000,这是0.3%手续费的实现方式。嗯,这里要注意,实际计算时先扣了手续费,再算你能拿多少。
4.3 滑点问题
滑点,说白了就是你下单时的预期价格和实际成交价格之间的差异。为什么会这样?因为你的交易改变了池子的比例。
刚才的例子,1个ETH换1,980.2 USDC,单价是1,980.2。但如果你要换10个ETH呢?
池子ETH变成110个,USDC变成20,000,000 / 110 ≈ 181,818.18。你能拿到的USDC是200,000 - 181,818.18 = 18,181.82。单价变成了1,818.18。你看,价格从1,980.2跌到了1,818.18,这就是滑点。
| 交易量 (ETH) | 获得 USDC | 实际单价 | 滑点 |
|---|---|---|---|
| 1 | 1,980.2 | 1,980.2 | 0% |
| 10 | 18,181.82 | 1,818.18 | 8.2% |
| 50 | 66,666.67 | 1,333.33 | 32.7% |
我在项目中遇到过用户投诉滑点太高的情况。其实这不是bug,是AMM的固有特性。交易量越大,滑点越严重。所以大额交易最好拆分成多笔,或者找流动性更好的池子。
避坑指南:我曾经见过一个项目,前端显示滑点0.5%,但实际交易时滑点到了5%。原因是池子流动性太浅,大额交易直接拉高了滑点。建议用户交易前一定要设置滑点容忍度,比如1%或2%,超过就回滚。
4.4 无常损失
无常损失是LP(流动性提供者)最头疼的问题。你往池子里存了ETH和USDC,结果价格涨了,你反而亏了?听起来不合理,但确实会发生。
假设你存了1个ETH和2,000 USDC,当时ETH价格是2,000 USDC。池子总价值是4,000 USDC。后来ETH涨到4,000 USDC。套利者会来池子里买ETH,直到池子价格跟外部市场一致。
根据恒定乘积公式,当ETH价格变成4,000 USDC时,池子里ETH和USDC的比例会变化。最终你取回的时候,ETH变少了,USDC变多了。总价值可能还不如你当初直接持有ETH和USDC。
// 无常损失计算示例
// 初始:1 ETH + 2000 USDC = 4000 USDC
// ETH涨到4000 USDC后:
// 池子比例变化,你取回:0.707 ETH + 2828 USDC ≈ 5656 USDC
// 如果直接持有:1 ETH + 2000 USDC = 6000 USDC
// 无常损失:5656 / 6000 - 1 ≈ -5.7%
为什么叫「无常」损失?因为如果价格又跌回来,损失就消失了。但如果你在价格高位撤出流动性,损失就变成「永久」的了。
我的建议:做LP之前,先想清楚你提供流动性的代币对。稳定币对(比如USDC/USDT)无常损失几乎为零。但如果是ETH/某个山寨币,波动大的时候损失可能很惨。我个人习惯只做主流币对的LP,或者用一些无常损失对冲工具。
4.5 实际开发中的注意事项
写AMM合约时,有几个坑我踩过,分享给大家:
- 精度问题:Solidity没有浮点数,所有计算都要用整数。Uniswap V2用112位精度,V3用更复杂的Tick机制。我建议直接用OpenZeppelin的SafeMath库,避免溢出。
- 闪电贷攻击:AMM的k值在单笔交易内是固定的,但攻击者可以利用闪电贷操纵价格。解决办法是引入预言机价格验证,或者限制单笔交易的最大滑点。
- 手续费分配:0.3%的手续费要合理分配给LP。我见过有的项目直接把手续费转给合约地址,结果LP取不出来。正确做法是每次交易时累加手续费,LP撤出时按比例分配。
// 安全示例:防止闪电贷攻击
function swap(uint amountIn, uint amountOutMin) external {
uint reserve0 = reserves[0];
uint reserve1 = reserves[1];
uint amountOut = getAmountOut(amountIn, reserve0, reserve1);
require(amountOut >= amountOutMin, 'SLIPPAGE_TOO_HIGH');
// 执行交易...
_update(reserve0 - amountIn, reserve1 + amountOut);
}
这段代码里,amountOutMin就是用户设置的滑点容忍度。如果实际输出小于这个值,交易直接回滚。嗯,这是保护用户的第一道防线。
4.6 总结
AMM机制看似简单,但背后涉及的经济学原理和工程细节不少。恒定乘积公式是基础,滑点和无常损失是每个开发者必须理解的概念。我个人觉得,做DEX开发最重要的是理解「流动性」的本质——它既是机会,也是风险。
下一章我们会聊Uniswap V3的集中流动性,那又是另一个故事了。先消化好今天的内容,有问题随时交流。