Appearance
去中心化交易所(DEX)与自动化做市商(AMM)详解
导航目录
本文面向“想把 Swap 跑通并理解原理”的读者,按 概念 → 交易流程 → 手续费与收益 → 风险 → 工程实现 的顺序讲清 DEX/AMM。
读完你应该能掌握
- DEX 和 CEX 的关键差异(托管、撮合方式、上币机制)
- 一次 Swap 在链上发生了什么(报价、滑点、路由、结算)
- 手续费如何分配(LP 收益、协议抽成)
- 主要风险点(价格冲击、无常损失、MEV/三明治)
1 DEX 与 AMM 概述
与传统中心化交易所(CEX)不同,DEX 允许用户直接从自己的钱包中进行点对点的资产兑换,无需注册、KYC 或将资产托管给第三方。其核心优势在于:
- 自我托管:用户始终掌握资产私钥,从根本上消除了交易所被盗或跑路的风险。
- 无许可上币:任何项目都可以无需审核地创建交易对,提供了极大的开放性。
- 抗审查性:任何人都无法单方面阻止某个地址进行交易。
而 AMM(Automated Market Maker,自动化做市商)是当今 DEX 的核心技术引擎:用资金池(Liquidity Pool)+ 定价公式取代订单簿撮合。用户不再与“对手方订单”成交,而是直接与池子的流动性交互。
关键术语
- Router(路由合约):帮你找路径并把多跳 swap 串起来执行(如 USDC→WETH→UNI)。
- Pair/Pool(交易对/池子合约):真正持有两种代币储备金并执行定价/转账的合约。
- Slippage(滑点):你期望成交价与最终成交价的允许偏差;通常用
amountOutMin保护。
2 一次 Swap 的完整流程
一次典型的 DEX 兑换涉及用户、前端界面和多个智能合约的复杂交互。其核心流程和资金流向如下图所示:

2.1 用户交互与查询
- 连接钱包:用户首先通过 Web3 钱包(如 MetaMask)连接至 DEX 前端界面。
- 选择交易对和数量:用户选择想要卖出的代币(Input)和想要买入的代币(Output),并输入卖出数量。
- 前端查询报价:前端界面会向智能合约查询这笔交易预计可得到的输出代币数量。这个过程是只读的,不消耗 Gas。
- 前端会调用路由合约的
getAmountsOut函数,传入路径和输入数量,计算出输出数量。 - 同时,前端会从链上或自己的数据库中获取当前交易的滑点容忍度(Slippage Tolerance)和交易截止时间(Deadline)的默认值。
- 前端会调用路由合约的
2.2 交易执行与结算
- 用户确认:用户确认报价,并可以调整滑点容忍度和 Gas 费。较低的滑点容忍度可以防止大幅的价格波动,但也可能导致交易失败。
- 交易签名:用户点击“Swap”,钱包会弹出请求签名的提示。用户签名后,交易被广播到区块链网络。
- 智能合约执行:
- 路由合约(Router):这是处理复杂交易的核心。它负责找到最佳兑换路径(例如,直接 USDC->ETH,或通过中间池 USDC->USDT->ETH)。
- 资金池合约(Pair):路由合约最终会调用目标资金池的
swap函数。该函数会执行以下操作:- 验证:检查交易是否满足预设条件(如未超过截止时间、输出代币数量大于用户设置的最小值)。
- 计算:根据恒定乘积公式 (x * y = k) 计算用户应收到的代币数量,并扣除手续费。
- 更新储备金:减少输出代币的储备量,增加输入代币的储备量。
- 转账:将输出代币发送到用户地址。
- 交易完成:区块确认后,用户的钱包中会收到输出代币,前端界面会更新显示交易成功的状态。
常见失败原因(前端需要提示清楚)
- 滑点设置过低:
amountOutMin过高,最终输出达不到而回滚(用户常看到INSUFFICIENT_OUTPUT_AMOUNT)。 - 授权未完成 / 授权额度不足:ERC20
approve未做或 allowance 不够。 - Deadline 过短:拥堵时可能超时回滚。
3 手续费、收益与分配机制
在 AMM 模型中,收益主要来自于交易手续费,并由流动性提供者和协议平台共享。
3.1 用户的收益(流动性提供者 - LPs)
普通交易用户没有直接收益,反而需要支付手续费。收益主要来自于为资金池提供流动性的用户,即流动性提供者(LPs)。
- 收益来源:交易手续费。每笔交易都会收取一定比例的费用(常见为 0.3%),这部分费用会直接添加到资金池的储备金中。
- 收益计算:LP 的收益是其按份额占有的那部分手续费。
- 当你注入流动性时,你会获得LP 代币(如 UNI-V2),代表你在该资金池中的份额。
- 你的份额 =
(你提供的流动性数量) / (池中总流动性数量) - 你的收益 =
总手续费收入 * 你的份额
- 无常损失(Impermanent Loss):这是提供流动性时的主要风险。当两种代币的市场价格比率发生变化时,与简单持有代币相比,LP 可能会遭受损失。手续费收益就是对冲这一风险和激励 LP 提供流动性的补偿。
3.2 平台的收益
协议的收益(佣金)是通过从总手续费中抽成来实现的。
- 手续费抽成(Fee Take):这是最主流的方式。协议不会额外收费,而是从 LP 应得的手续费中抽取一部分。例如:
- 总交易手续费率为:0.3%
- 其中,0.25% 分配给所有 LP。
- 另外 0.05% 则转入协议的国库地址(Treasury) 作为平台收入。
- 其他模式:
- 原生代币回购与销毁:部分协议(如 PancakeSwap)会使用部分平台收入在公开市场上回购并销毁其原生代币(如 CAKE),创造通缩压力。
- 治理与激励:平台收入可以用于资助生态开发、市场推广,或作为激励分配给质押了平台治理代币的用户。
3.3 佣金分配案例
假设一个 USDC/ETH 池:
- 总流动性:$1,000,000
- 每日交易量:$10,000,000
- 总手续费率:0.3%
- 协议抽成率:0.05% (即总手续费的 1/6)
- LP 手续费率:0.25%
每日手续费总收入: $10,000,000 * 0.3% = $30,000
平台每日佣金收入: $10,000,000 * 0.05% = $5,000 (或 $30,000 * (1/6) = $5,000)
LP 每日总收益: $10,000,000 * 0.25% = $25,000 (或 $30,000 * (5/6) = $25,000)
如果一个 LP 提供了该池总流动性 1%的资产(价值$10,000),那么他每日可以分得: $25,000 * 1% = $25 的手续费收益。
4 你需要特别理解的风险点(重要)
4.1 价格冲击(Price Impact)
AMM 的价格来自池子储备金比例。你的交易会改变储备金比例,从而造成价格冲击;交易越大、池子越小,冲击越大。前端通常会展示 priceImpact,并在过高时给出强提示。
4.2 无常损失(Impermanent Loss, IL)
LP 面对的核心风险是 IL:当池内两种资产的相对价格变化时,LP 的持仓组合会被动再平衡,可能导致相对“直接持有”更亏。手续费收益是对 IL 风险的一种补偿,但不保证覆盖。
4.3 MEV 与三明治攻击(Sandwich)
公开内存池(mempool)里的 swap 容易被机器人“夹击”:
- 前置交易:机器人先买入推高价格
- 你的交易:以更差的价格成交(但仍满足你的滑点)
- 后置交易:机器人卖出获利
工程建议(非常关键)
- 不要把滑点默认值设置太大;交易量/波动大时需要更明显的风险提示。
- 尽可能支持 私有交易 或 MEV 保护通道(取决于链/钱包/基础设施能力)。
5 技术实现:合约、后端与前端
5.1 智能合约(以 Uniswap V2 风格为例)
DEX 的核心是经过审计的、Gas 优化的智能合约。工程上常见做法是基于 Uniswap V2 / V3 或其分叉实现,并叠加自己的费率、路由和治理逻辑。
5.1.1 核心合约结构(示意)
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 核心配对合约(代表一个交易对,如USDC/ETH)
contract UniswapV2Pair {
address public factory;
address public token0;
address public token1;
uint112 private reserve0; // 代币0的储备量
uint112 private reserve1; // 代币1的储备量
uint32 private blockTimestampLast; // 最后更新时间戳
uint public totalSupply; // LP代币总供应量
mapping(address => uint) public balanceOf; // LP代币余额
// 恒定乘积公式计算
function getAmountOut(uint amountIn, address tokenIn) public view returns (uint amountOut) {
uint amountInWithFee = amountIn * 997; // 扣除0.3%手续费
uint numerator = amountInWithFee * (tokenIn == token0 ? reserve1 : reserve0);
uint denominator = (tokenIn == token0 ? reserve0 : reserve1) * 1000 + amountInWithFee;
amountOut = numerator / denominator;
}
// 兑换函数(外部由路由合约调用)
function swap(uint amount0Out, uint amount1Out, address to) external {
// 1. 安全检查...
// 2. 从合约中转出输出代币给用户 `to`
// 3. 更新储备金
// 注意:真实实现中 balance0/balance1 来自 token.balanceOf(address(this))
// _update(balance0, balance1);
}
// 更新储备金
function _update(uint balance0, uint balance1) private {
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = uint32(block.timestamp);
emit Sync(reserve0, reserve1); // 触发同步事件
}
}
// 工厂合约(创建和管理所有交易对)
contract UniswapV2Factory {
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
address public feeTo; // 协议收费地址
function createPair(address tokenA, address tokenB) external returns (address pair) {
// 创建新的Pair合约
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(tokenA, tokenB));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
UniswapV2Pair(pair).initialize(tokenA, tokenB);
getPair[tokenA][tokenB] = pair;
getPair[tokenB][tokenA] = pair;
allPairs.push(pair);
}
}
// 路由合约(处理复杂交易和路由)
contract UniswapV2Router {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin, // 最小输出数量(防滑点)
address[] calldata path, // 兑换路径
address to,
uint deadline
) external ensure(deadline) returns (uint[] memory amounts) {
// 1. 计算路径中每一步的预期输出量
amounts = getAmountsOut(amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'Router: INSUFFICIENT_OUTPUT_AMOUNT');
// 2. 将用户的输入代币转入第一个Pair合约
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
// 3. 循环调用路径上的每一个Pair合约进行兑换
_swap(amounts, path, to);
}
function _swap(uint[] memory amounts, address[] memory path, address _to) internal {
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
}说明
上面代码用于帮助理解 结构与关键参数,并非可直接部署的完整实现。生产环境请直接参考/复用经过审计的成熟实现(Uniswap、Sushi、Pancake 等)并进行二次审计。
5.1.2 安全与优化考量
- 重入攻击防护:在关键函数中使用
nonReentrant修饰符(如来自 OpenZeppelin 的ReentrancyGuard)。 - 精度处理:妥善处理不同代币的
decimals差异,使用足够的精度进行数学运算(通常乘以1e18)。 - 闪电贷:AMM 本身支持闪电贷。
swap函数不需要特殊的闪电贷代码,但需确保在所有外部调用之前完成状态更新。 - Gas 优化:使用
create2部署 Pair 合约、使用uint112存储储备金以优化存储布局。
5.2 后端(索引与聚合)
后端主要负责提供链下数据索引和 API,以增强前端体验。
- 技术栈:Node.js + Express/NestJS, PostgreSQL, Redis。
- 核心任务:
- 事件监听与索引:使用
Ethers.js监听链上事件(Swap,Mint,Burn,Sync),将交易、池子、用户余额等数据存入数据库。 - 计算聚合数据:计算并缓存全局数据,如总锁仓量(TVL)、24 小时交易量、热门交易对、代币价格等。
- 提供 API:
GET /pairs: 返回所有交易对列表及其数据。GET /pairs/:address: 返回特定交易对的详细数据。GET /pairs/:address/transactions: 返回交易对的最近交易记录。
- 获取代币元数据:从链上或第三方服务获取代币的图标、名称、符号等信息。
- 事件监听与索引:使用
5.3 前端(交互与交易构造)
前端是用户直接交互的界面,需要直观、响应快且安全。
- 技术栈:React + Vite, Ethers.js, Web3Modal (用于钱包连接), Chart.js (用于显示价格图表)。
- 核心功能:
- 钱包连接:集成多种钱包(MetaMask, WalletConnect, Coinbase Wallet)。
- 数据展示:从后端 API 和链上合约获取数据,展示代币价格、池子流动性、24 小时成交量、历史图表等。
- 兑换界面:
- 实时获取和显示报价。
- 允许用户设置滑点容忍度和交易截止时间。
- 显示预估的 Gas 费用。
- 流动性管理界面:允许用户添加/移除流动性,并直观地显示他们的头寸和累计费用。
- 交易状态跟踪:显示交易状态,并提供区块链浏览器的链接。
5.3.1 ethers 示例:构造 swapExactTokensForTokens
下面示例展示最常见的 V2 Router 调用方式:先 approve,再带上 amountOutMin 与 deadline 发起 swap。
ts
import { ethers } from "ethers";
// 你需要替换为目标链的地址
const ROUTER = "0xRouterAddress";
const TOKEN_IN = "0xTokenIn";
const TOKEN_OUT = "0xTokenOut";
const erc20Abi = [
"function approve(address spender, uint256 amount) external returns (bool)",
"function allowance(address owner, address spender) external view returns (uint256)",
"function decimals() external view returns (uint8)",
];
const routerAbi = [
"function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)",
"function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)",
];
export async function swapExactTokensForTokensExample() {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const user = await signer.getAddress();
const tokenIn = new ethers.Contract(TOKEN_IN, erc20Abi, signer);
const router = new ethers.Contract(ROUTER, routerAbi, signer);
const decimals = await tokenIn.decimals();
const amountIn = ethers.parseUnits("100", decimals); // 卖出 100 tokenIn
const path = [TOKEN_IN, TOKEN_OUT];
// 1) 读报价(不花 Gas)
const amounts = await router.getAmountsOut(amountIn, path);
const quotedOut = amounts[amounts.length - 1];
// 2) 设置滑点保护:例如 0.5%
const slippageBps = 50n; // 0.50% = 50 bps
const amountOutMin = (quotedOut * (10_000n - slippageBps)) / 10_000n;
// 3) 设置 deadline:例如 10 分钟后过期
const deadline = BigInt(Math.floor(Date.now() / 1000) + 10 * 60);
// 4) approve(若 allowance 不足)
const allowance = await tokenIn.allowance(user, ROUTER);
if (allowance < amountIn) {
const txApprove = await tokenIn.approve(ROUTER, amountIn);
await txApprove.wait();
}
// 5) 发起 swap
const tx = await router.swapExactTokensForTokens(amountIn, amountOutMin, path, user, deadline);
const receipt = await tx.wait();
return receipt;
}