DEX测试完全指南 - 去中心化交易所测试实战
版本: v1.0
最后更新: 2026-03-14
目标: 让你看完后拥有DEX测试的实战经验
📚 目录
第一部分:DEX基础理论
第二部分:DEX功能详解
第三部分:测试实战
第四部分:高级主题
第五部分:精通之路
第零部分:DEX核心术语详解
0. DEX常用名词解释与案例
0.1 基础概念类
0.1.1 AMM (Automated Market Maker - 自动做市商)
定义: 使用数学公式自动为交易定价的算法,无需传统订单簿。
详细解释:
传统交易所使用订单簿匹配买卖双方,而AMM使用固定公式(如x*y=k)根据池中资产比例自动计算价格。
真实案例:
// Uniswap案例
池子初始状态: 1000 ETH, 2000000 USDT
k = 1000 * 2000000 = 2,000,000,000
用户买入100 ETH:
新状态: 1100 ETH, ? USDT
根据 k 值: 1100 * y = 2,000,000,000
y = 1,818,181.82 USDT
用户支付: 2,000,000 - 1,818,181.82 = 181,818.18 USDT
平均价格: 181,818.18 / 100 = 1,818.18 USDT/ETH
代码示例:
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
public pure returns (uint256)
{
uint256 amountInWithFee = amountIn * 997; // 扣除0.3%手续费
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
return numerator / denominator;
}
0.1.2 Liquidity Pool (流动性池)
定义: 存放两种代币储备的智能合约,用于提供交易流动性。
详细解释:
流动性池是DEX的核心,包含两种代币的储备。用户可以随时与池子交易,池子根据AMM公式自动调整价格。
真实案例 - Uniswap V2 ETH/USDT池:
2024年某日数据:
- ETH储备: 45,234 ETH
- USDT储备: 90,123,456 USDT
- 价格: 90,123,456 / 45,234 ≈ 1,992 USDT/ETH
- 总流动性: 约$180M
- 24小时交易量: $1.2B
在Etherscan上的实际表现:
// 查询流动性池储备
const pair = await ethers.getContractAt("IUniswapV2Pair", pairAddress);
const [reserve0, reserve1] = await pair.getReserves();
console.log("ETH Reserve:", ethers.formatEther(reserve0));
console.log("USDT Reserve:", ethers.formatUnits(reserve1, 6));
console.log("Price:", Number(reserve1) / Number(reserve0));
0.1.3 LP Token (Liquidity Provider Token)
定义: 流动性提供者的凭证代币,代表其在池中的份额。
详细解释:
当用户向流动性池添加代币时,会收到LP Token作为回执。LP Token数量代表用户在池中的所有权比例,可随时赎回本金+赚取的手续费。
真实案例:
// 用户A添加流动性
初始池子: 1000 ETH, 2000000 USDT
总LP供应: 44,721 LP (sqrt(1000*2000000))
用户A添加: 10 ETH + 20,000 USDT
获得LP = min(
10 * 44721 / 1000 = 447.21,
20000 * 44721 / 2000000 = 447.21
)
用户A持有: 447.21 LP
占比: 447.21 / (44721 + 447.21) = 0.99%
SushiSwap真实数据:
某用户在USDC/ETH池:
- 添加流动性: 50,000 USDC + 25 ETH
- 收到LP Token: 1,234.56 SLP
- 6个月后:
- 池子增长到 $200M
- 收取手续费: $1,245
- 赎回: 51,245 USDC + 25.62 ETH
- 收益率: 4.8% (手续费) + 资产升值
0.1.4 Slippage (滑点)
定义: 预期价格与实际成交价格的差异。
详细解释:
在AMM中,大额交易会改变池子储备比例,导致价格变动。滑点越大,说明交易对价格影响越大。
真实案例 - 不同交易额的滑点:
池子: 1000 ETH, 2000000 USDT (价格: 2000 USDT/ETH)
// 小额交易 (1 ETH)
输入: 1 ETH
输出: 1994.01 USDT
实际价格: 1994.01 USDT/ETH
滑点: (2000 - 1994.01) / 2000 = 0.3%
// 中额交易 (10 ETH)
输入: 10 ETH
输出: 19,740 USDT
实际价格: 1974 USDT/ETH
滑点: (2000 - 1974) / 2000 = 1.3%
// 大额交易 (100 ETH)
输入: 100 ETH
输出: 181,818 USDT
实际价格: 1818.18 USDT/ETH
滑点: (2000 - 1818.18) / 2000 = 9.1%
PancakeSwap实际用户体验:
用户交易BNB->BUSD:
- 输入: 10 BNB
- 预期输出: 3,000 BUSD (300 BUSD/BNB)
- 设置滑点: 0.5%
- 最少接受: 2,985 BUSD
- 实际获得: 2,992 BUSD
- 实际滑点: 0.27% ✅ 通过
0.1.5 Impermanent Loss (无常损失/无偿损失)
定义: 流动性提供者因价格变动导致的潜在损失(相对于单纯持有)。
详细解释:
当池中代币价格发生变化时,AMM会自动重新平衡,导致LP持有的代币数量变化。如果价格大幅波动,LP可能会比单纯持有代币损失更多。
真实案例 - ETH价格上涨50%:
初始状态:
用户添加: 1 ETH + 2000 USDT (ETH = 2000 USDT)
总价值: $4,000
价格变化: ETH涨到 3000 USDT (+50%)
如果单纯持有:
1 ETH + 2000 USDT = 3000 + 2000 = $5,000
如果提供流动性:
根据 x * y = k
新储备: 0.816 ETH + 2,449 USDT
总价值: 0.816 * 3000 + 2449 = $4,898
无常损失: (4898 - 5000) / 5000 = -2.04%
计算公式:
function calculateImpermanentLoss(priceRatio) {
// priceRatio = 新价格 / 初始价格
const IL = 2 * Math.sqrt(priceRatio) / (1 + priceRatio) - 1;
return IL * 100; // 百分比
}
// 价格变化 vs 无常损失
1.25x -> -0.6%
1.5x -> -2.0%
2x -> -5.7%
3x -> -13.4%
4x -> -20.0%
5x -> -25.5%
Uniswap V3真实数据:
用户在ETH/USDC池 (价格区间: $1800-$2200):
- 初始: $10,000 流动性
- 3个月后ETH从$2000涨到$2800
- 头寸移出价格区间,停止赚取手续费
- 无常损失: -18.2%
- 赚取手续费: +5.3%
- 净损失: -12.9%
教训: 价格区间设置太窄导致损失
0.2 交易机制类
0.2.1 Swap (交换)
定义: 在DEX上用一种代币交换另一种代币的操作。
详细解释:
Swap通过智能合约自动执行,根据AMM公式计算输出金额,扣除手续费后转账给用户。
真实案例 - Uniswap交易截图分析:
交易哈希: 0xabc123...
用户操作: Swap 5 ETH for USDT
交易详情:
{
from: "0x742d35...",
to: "UniswapV2Router02",
value: 5 ETH,
// 调用函数
function: "swapExactETHForTokens",
params: {
amountIn: 5 ETH,
amountOutMin: 9,900 USDT, // 滑点保护 (0.5%)
path: [WETH, USDT],
to: user,
deadline: 1699999999
},
// 实际结果
result: {
amountOut: 9,925 USDT,
gasUsed: 123,456 gas,
gasFee: 0.0025 ETH ($5)
}
}
// 用户实际收益
输入: 5 ETH ($10,000)
输出: 9,925 USDT
Gas: -$5
净收入: $9,920
0.2.2 Route (路由)
定义: 从源代币到目标代币的交易路径,可能经过多个中间代币。
详细解释:
当两个代币没有直接交易对时,需要通过中间代币(如WETH、USDT)完成交易。Router合约会自动计算最优路径。
真实案例 - 最优路径选择:
目标: 用SHIB换DAI
// 路径1: 直接路径 (流动性不足)
SHIB -> DAI
池子: $500K 流动性
输出: 985 DAI (滑点: 1.5%)
// 路径2: 通过WETH (最优)
SHIB -> WETH -> DAI
池子1: $50M 流动性 (SHIB/WETH)
池子2: $200M 流动性 (WETH/DAI)
输出: 997 DAI (滑点: 0.3%) ✅
// 路径3: 通过USDT
SHIB -> USDT -> DAI
输出: 994 DAI (滑点: 0.6%)
Router选择: 路径2
1inch聚合器真实优化:
// 1inch比较多个DEX的路径
输入: 1000 USDC -> LINK
Option 1 (Uniswap):
USDC -> LINK
输出: 142.3 LINK
Option 2 (SushiSwap):
USDC -> LINK
输出: 142.1 LINK
Option 3 (1inch Split):
- 60% 通过 Uniswap: USDC -> WETH -> LINK
- 40% 通过 Curve: USDC -> USDT -> WETH -> LINK
输出: 143.2 LINK ✅ (多赚0.9 LINK)
节省: $12 (0.63%)
0.2.3 Price Impact (价格影响)
定义: 单笔交易对市场价格造成的影响幅度。
详细解释:
大额交易会显著改变池子储备比例,导致后续交易者面临不同价格。价格影响是衡量交易规模相对于流动性大小的指标。
真实案例 - 流动性深度对比:
// 小池子 ($1M流动性)
输入: 50,000 USDT买ETH
价格影响: 4.88%
实际损失: $2,440
// 大池子 ($100M流动性)
输入: 50,000 USDT买ETH
价格影响: 0.05%
实际损失: $25
教训: 大额交易应该选择流动性深的池子
Balancer真实警告:
用户界面显示:
┌───────────────────────────────┐
│ ⚠️ High Price Impact │
│ │
│ Your trade will move the │
│ market price by 8.2% │
│ │
│ Expected: 100 tokens │
│ You get: 91.8 tokens │
│ Lost to PI: 8.2 tokens │
│ │
│ [Cancel] [Trade Anyway] │
└───────────────────────────────┘
0.3 安全与攻击类
0.3.1 Front-running (抢跑/抢先交易)
定义: 攻击者监控mempool,发现有利可图的交易后,通过支付更高gas抢先执行。
详细解释:
以太坊交易在被打包前会先进入mempool(交易池),任何人都能看到。攻击者可以发现大额交易,然后用更高的gas price让自己的交易先被打包。
真实案例 - MEV Bot攻击:
// 2023年真实MEV攻击案例
时间轴:
14:32:15.234 - 用户A提交: 买入100 ETH (gas: 50 gwei)
14:32:15.567 - MEV Bot发现交易
14:32:15.789 - Bot提交: 买入50 ETH (gas: 200 gwei) ← 抢先
14:32:16.123 - 区块打包顺序:
1. MEV Bot交易 ✅ (高gas)
2. 用户A交易 (价格已被推高)
结果:
- Bot买入价: $1,995/ETH
- 用户A买入价: $2,015/ETH (被推高)
- Bot卖出价: $2,010/ETH
- Bot获利: (2010-1995) * 50 = $750
- 用户A损失: (2015-2000) * 100 = $1,500
Etherscan上的证据:
Block #18,234,567
Tx 1: 0xaaa111... (Gas: 200 gwei, Priority: 50 gwei)
MEV Bot: Buy 50 ETH
Tx 2: 0xbbb222... (Gas: 50 gwei, Priority: 2 gwei)
Victim: Buy 100 ETH
Tx 3: 0xccc333... (Gas: 180 gwei)
MEV Bot: Sell 50 ETH
MEV Bot Profit: 0.375 ETH ($750)
0.3.2 Sandwich Attack (三明治攻击)
定义: 攻击者在目标交易前后各插入一笔交易,从价格变动中获利。
详细解释:
三明治攻击是Front-running的特殊形式:攻击者先买入推高价格(front-run),让受害者以高价成交,然后立即卖出获利(back-run)。
真实案例 - Jaredfromsubway.eth:
// 2023年最臭名昭著的MEV Bot
攻击案例 #12,345:
时间: Block 18,567,234
[1] Front-run (Tx 0)
Bot买入: 100 ETH -> PEPE
Gas: 500 gwei (优先级极高)
花费: 200,000 USDT
获得: 10B PEPE
价格推高: 1.8%
[2] Victim (Tx 1)
用户交易: 50 ETH -> PEPE
Gas: 30 gwei (正常)
花费: 100,000 USDT
获得: 4.82B PEPE (少了1.8%)
[3] Back-run (Tx 2)
Bot卖出: 10B PEPE -> ETH
Gas: 450 gwei
获得: 103,600 USDT
Bot净利润: 3,600 USDT
Gas成本: -400 USDT
实际利润: 3,200 USDT
受害者损失: 1,800 USDT
统计:
- Jaredfromsubway.eth总获利: $34M (6个月)
- 平均每次攻击: $1,200
- 成功率: 87%
- Gas花费: $8.5M
防护案例 - Flashbots保护:
// 使用Flashbots私密交易池
用户配置:
{
rpc: "https://rpc.flashbots.net",
maxPriorityFeePerGas: 3 gwei, // 不公开gas price
type: "flashbots",
// 交易不进入公共mempool
// MEV Bot看不到
// 直接打包进区块
}
结果:
✅ 避免三明治攻击
✅ 节省gas(不需要竞价)
✅ 保证执行价格
0.3.3 Flash Loan (闪电贷)
定义: 在单笔交易中借款并归还的无抵押贷款。
详细解释:
闪电贷允许用户在一个交易中借出大量资金,只要在交易结束前归还即可。如果还款失败,整个交易回滚。常被用于套利,也被用于攻击。
真实案例 - 2020年bZx攻击:
// 史上第一起闪电贷攻击
攻击流程 (Block 9,484,688):
1. 从dYdX闪电贷: 10,000 ETH
2. 用5,500 ETH在Compound借WBTC
操纵Compound价格预言机
3. 用剩余4,500 ETH在Uniswap买入WBTC
推高WBTC价格
4. 在bZx用操纵后的高价格借出更多ETH
5. 还原市场并归还闪电贷
攻击者获利: 1,193 ETH ($350,000)
bZx损失: $350,000
关键代码:
```solidity
function attack() external {
// 1. 闪电贷
dydx.flashLoan(10000 ether);
}
function callFunction(bytes calldata data) external {
// 2. 攻击逻辑
compound.borrow(wbtc);
uniswap.swapETHForWBTC(4500 ether);
bzx.borrow(eth);
// 3. 归还
dydx.repay(10000 ether + fee);
}
攻击分析:
- 利用了价格预言机依赖单一DEX
- 闪电贷放大了攻击规模
- 单笔交易完成,原子性保证
**合法套利案例**:
```javascript
// 闪电贷套利(合法)
机会: ETH在Uniswap ($2000) 比 Sushiswap ($2020) 便宜
套利流程:
1. Aave闪电贷: 1000 ETH
- 手续费: 0.09% = 0.9 ETH
2. Uniswap买入: 1000 ETH
- 花费: 2,000,000 USDT
3. Sushiswap卖出: 1000 ETH
- 获得: 2,020,000 USDT
4. 用USDT买回1000.9 ETH
5. 归还闪电贷: 1000.9 ETH
利润计算:
收入: 20,000 USDT
手续费: -0.09% * 2M = -1,800 USDT
Gas: -500 USDT
净利润: 17,700 USDT ($17,700)
0.3.4 MEV (Maximal Extractable Value)
定义: 通过重新排序、插入或审查区块中的交易所能提取的最大价值。
详细解释:
矿工/验证者可以控制区块中交易的顺序,通过这种权力可以提取额外价值。MEV包括:套利、清算、三明治攻击等。
真实案例 - Flashbots统计数据:
// 2023年以太坊MEV数据
总MEV提取: $1.38B
按类型分类:
- DEX套利: $687M (49.8%)
- 三明治攻击: $384M (27.8%)
- 清算: $241M (17.5%)
- NFT套利: $68M (4.9%)
Top MEV Searcher:
地址: 0x0000001234567890...
提取总额: $127M
成功率: 94.3%
平均利润/交易: $3,200
最大单笔MEV:
Block: 18,234,567
MEV: $4.2M
类型: USDC脱钩套利
详情: USDC脱钩到$0.88,机器人在多个DEX套利
MEV保护对比:
// 无保护交易
用户交易: Swap 100 ETH for USDT
预期: 200,000 USDT
实际: 195,600 USDT
MEV损失: 4,400 USDT (2.2%) ❌
// 使用Flashbots Protect
用户交易: 通过私密RPC提交
预期: 200,000 USDT
实际: 199,800 USDT
MEV损失: 200 USDT (0.1%) ✅
节省: 4,200 USDT
0.4 价格与预言机类
0.4.1 TWAP (Time-Weighted Average Price)
定义: 时间加权平均价格,通过一段时间内的价格累积计算平均价格。
详细解释:
TWAP通过累积价格随时间的积分来计算平均价格,可以抵御单笔交易的价格操纵。Uniswap V2/V3内置TWAP预言机。
真实案例 - 价格操纵防护:
// Uniswap V2 TWAP实现
// 攻击者尝试操纵
时刻 T0: 价格 = $2000/ETH
时刻 T1: 攻击者大额买入,价格 = $3000/ETH (操纵)
时刻 T2: 价格恢复 = $2000/ETH
// 即时价格预言机(易被操纵)
getSpotPrice() = $3000 ❌ 被操纵
// TWAP预言机(抗操纵)
getTWAP(period = 1小时) = $2,008 ✅ 仅轻微影响
计算过程:
TWAP = Σ(价格 * 时长) / 总时长
= (2000*3540 + 3000*60 + 2000*0) / 3600
= $2,016.67
即使价格被操纵1分钟,TWAP变化< 1%
Compound使用TWAP案例:
// Compound V2价格预言机
contract PriceOracle {
function getUnderlyingPrice(CToken cToken)
external view returns (uint256)
{
// 使用Uniswap V3 TWAP
return uniswapV3Pair.observe(
secondsAgo: [1800, 0] // 30分钟TWAP
);
}
}
// 实际数据
2023-10-15 案例:
- 闪电贷攻击尝试将ETH价格从$1,650推到$2,200
- Compound TWAP价格: $1,652 (仅+$2)
- 攻击失败,无法低价清算
- 攻击者损失: gas费 $15,000
0.4.2 Oracle (预言机)
定义: 为智能合约提供链外数据(如价格)的服务。
详细解释:
DEX本身可以作为价格预言机(如Uniswap TWAP),也可以使用专业预言机(如Chainlink)。预言机质量直接影响DeFi协议安全。
真实案例对比:
// 案例1: 单一DEX预言机(不安全)
contract VulnerableLending {
function getPrice() public view returns (uint256) {
// ❌ 只依赖单个DEX即时价格
return uniswap.getSpotPrice();
}
}
攻击:
1. 闪电贷借10,000 ETH
2. 在Uniswap大量买入操纵价格
3. 在借贷协议以高价借款
4. 还原价格归还闪电贷
损失: $2.3M (真实案例: Harvest Finance)
// 案例2: Chainlink预言机(安全)
contract SecureLending {
AggregatorV3Interface priceFeed;
function getPrice() public view returns (uint256) {
// ✅ 使用多节点聚合价格
(, int256 price,,,) = priceFeed.latestRoundData();
return uint256(price);
}
}
Chainlink特点:
- 数据来自多个交易所
- 多个节点共识
- 历史数据可验证
- 更新延迟: ~10秒
MakerDAO预言机设计:
// MakerDAO OSM (Oracle Security Module)
特性:
1. 价格延迟1小时生效
2. 使用多个数据源中位数
3. 紧急暂停机制
代码逻辑:
```solidity
contract OSM {
uint256 public currentPrice;
uint256 public nextPrice;
uint256 public lastUpdateTime;
function poke() external {
// 获取新价格
nextPrice = median.read();
// 1小时后生效
if (block.timestamp >= lastUpdateTime + 1 hours) {
currentPrice = nextPrice;
lastUpdateTime = block.timestamp;
}
}
}
真实防护案例:
- 2020年3月12日黑色星期四
- ETH价格暴跌30%
- 1小时延迟让MakerDAO有时间应对
- 避免了恐慌性清算
---
### 0.5 Gas与成本类
#### 0.5.1 Gas Fee (Gas费用)
**定义**: 在以太坊上执行交易需要支付的计算费用。
**详细解释**:
Gas是以太坊计量计算工作量的单位。每个操作消耗一定Gas,用户支付 Gas * GasPrice 的ETH作为手续费给矿工/验证者。
**真实案例 - 不同操作的Gas成本**:
```javascript
// 2024年以太坊主网实际数据 (Gas Price = 30 gwei)
操作 Gas消耗 成本(ETH) 成本(USD)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ETH转账 21,000 0.00063 $1.26
ERC20转账 65,000 0.00195 $3.90
Uniswap Swap (单跳) 120,000 0.0036 $7.20
Uniswap Swap (多跳) 180,000 0.0054 $10.80
添加流动性 150,000 0.0045 $9.00
移除流动性 140,000 0.0042 $8.40
NFT Mint 80,000 0.0024 $4.80
高峰期Gas战争:
// 2023年Arbitrum空投事件
时间: 2023-03-23 12:00 UTC
事件: Arbitrum开放ARB代币领取
Gas Price变化:
12:00 - 正常: 20 gwei
12:05 - 开始上涨: 150 gwei
12:10 - 疯狂: 500 gwei
12:15 - 峰值: 2,000 gwei (100倍!)
实际成本:
简单领取交易:
- 正常: $2
- 高峰: $200
有用户花费:
- Gas: 0.15 ETH ($300)
- 领取: 1,250 ARB ($1,500)
- 净利润: $1,200
L2解决方案对比:
交易: Swap 1 ETH for USDT
以太坊主网:
- Gas: 120,000
- Gas Price: 30 gwei
- 成本: $7.20
Arbitrum (L2):
- Gas: 120,000
- Gas Price: 0.1 gwei
- 成本: $0.024 (节省99.7%)
Optimism (L2):
- Gas: 120,000
- Gas Price: 0.001 gwei
- 成本: $0.00024 (节省99.99%)
0.5.2 Pair (交易对)
定义: 两种代币的流动性池,是DEX交易的基本单位。
详细解释:
每个Pair合约管理两种代币的储备,实现了ERC20接口(LP Token),并提供swap、mint、burn等功能。
真实案例 - Uniswap V2 Pair生命周期:
// ETH/USDT Pair创建到运营
1. 创建阶段 (Block 15,000,000)
Factory.createPair(WETH, USDT)
→ 部署新Pair合约: 0xabc123...
→ 初始状态: 0 ETH, 0 USDT, 0 LP
2. 首次添加流动性 (Block 15,000,100)
用户A添加: 100 ETH + 200,000 USDT
→ LP Token: sqrt(100 * 200000) = 4,472 LP
→ 用户A收到: 4,472 LP
→ 初始价格: 2,000 USDT/ETH
3. 第二次添加流动性 (Block 15,000,500)
用户B添加: 10 ETH + 20,000 USDT
→ LP Token: min(10*4472/100, 20000*4472/200000) = 447 LP
→ 用户B收到: 447 LP
→ 总LP: 4,919 LP
4. 交易活动 (Block 15,001,000)
交易1: 5 ETH → USDT
交易2: 10,000 USDT → ETH
交易3: 2 ETH → USDT
... (累计100笔交易)
→ 累积手续费: 0.5 ETH + 1,000 USDT
→ k值从 20,000,000 增长到 20,100,000
5. 移除流动性 (Block 15,010,000)
用户A赎回: 4,472 LP (91% 份额)
→ 获得: 100.45 ETH + 200,900 USDT
→ 盈利: 0.45 ETH + 900 USDT (手续费收入)
→ ROI: 0.95% (10,000区块 ≈ 1.3天)
Pair合约关键状态:
contract Pair {
// 状态变量
address public token0; // 0x000...111 (WETH)
address public token1; // 0x000...222 (USDT)
uint112 private reserve0; // 100.45 ETH
uint112 private reserve1; // 200,900 USDT
uint32 private blockTimestampLast;
// LP Token信息
string public name = "Uniswap V2";
string public symbol = "UNI-V2";
uint256 public totalSupply = 4919e18;
// 用户余额
mapping(address => uint256) public balanceOf;
// 用户A: 4,472e18 LP
// 用户B: 447e18 LP
}
Etherscan上的实际Pair分析:
合约: 0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 (ETH/USDT)
统计数据 (2024-01-15):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总流动性: $187,234,567
24h交易量: $1,234,567,890
24h交易笔数: 23,456
手续费收入: $3,703,703 (0.3% * 交易量)
LP持有者: 12,345
Top LP Provider:
地址: 0x123...abc
份额: 12.3%
价值: $23M
0.6 总结:术语关联图
┌──────────────┐
│ Liquidity │
│ Pool │
└──────┬───────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────▼─────┐ ┌────▼────┐ ┌─────▼─────┐
│ AMM │ │ Pair │ │ LP Token │
└─────┬─────┘ └────┬────┘ └─────┬─────┘
│ │ │
┌─────▼─────┐ ┌────▼────┐ ┌─────▼─────┐
│ Swap │ │ Reserve │ │ Impermanent│
│ │ │ │ │ Loss │
└─────┬─────┘ └────┬────┘ └───────────┘
│ │
┌─────▼─────┐ ┌────▼────┐
│ Slippage │ │ Price │
│ │ │ Impact │
└───────────┘ └────┬────┘
│
┌─────▼─────┐
│ Oracle │
│ TWAP │
└─────┬─────┘
│
┌─────────────┼─────────────┐
│ │ │
┌─────▼─────┐ ┌────▼────┐ ┌─────▼─────┐
│Front-run │ │Sandwich │ │Flash Loan │
└───────────┘ └─────────┘ └───────────┘
│ │ │
└─────────────┼─────────────┘
│
┌─────▼─────┐
│ MEV │
└───────────┘
第一部分:DEX基础理论
1. DEX工作原理
1.1 什么是DEX?
DEX (Decentralized Exchange) 是去中心化交易所,与传统中心化交易所的区别:
| 特性 | 中心化交易所 (CEX) | 去中心化交易所 (DEX) |
|---|---|---|
| 资产托管 | 交易所托管 | 用户自己掌控 |
| 订单撮合 | 中心化订单簿 | 智能合约 / AMM |
| KYC | 需要 | 不需要 |
| 交易速度 | 快 | 取决于区块链 |
| 手续费 | 低 | 相对高 (Gas) |
| 安全风险 | 中心化风险 | 智能合约风险 |
1.2 DEX的三种模型
1.2.1 订单簿模型 (Order Book)
传统模型,类似CEX:
- 用户提交限价单/市价单
- 订单簿匹配买卖双方
- 链上/链下订单簿
代表: dYdX, Loopring
优点: 价格发现精确
缺点: 流动性碎片化,Gas成本高
1.2.2 自动做市商模型 (AMM)
使用数学公式自动定价:
- 恒定乘积 (Uniswap): x * y = k
- 恒定总和 (mStable): x + y = k
- 混合曲线 (Curve): 稳定币优化
代表: Uniswap, SushiSwap, PancakeSwap, Curve
优点: 流动性集中,Gas成本低
缺点: 滑点,无常损失
1.2.3 混合模型
结合订单簿和AMM:
- PMM (Proactive Market Maker)
- vAMM (Virtual AMM)
代表: DODO, Perpetual Protocol
优点: 兼具两者优势
缺点: 复杂度高
1.3 DEX架构图
┌─────────────────────────────────────────────────────────┐
│ DEX 前端界面 │
│ (用户交互、钱包连接、交易确认) │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Router 合约 │
│ • 路径计算 │
│ • 多跳交易 │
│ • 滑点保护 │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Pair 合约 │ │ Pair 合约 │ │ Pair 合约 │
│ ETH/USDT │ │ USDT/USDC │ │ USDC/DAI │
│ │ │ │ │ │
│ • swap │ │ • swap │ │ • swap │
│ • addLiq │ │ • addLiq │ │ • addLiq │
│ • removeLiq │ │ • removeLiq │ │ • removeLiq │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Factory 合约 │
│ • 创建交易对 │
│ • 注册交易对 │
│ • 管理费用 │
└─────────────────────────────────────────────────────────┘
2. AMM算法详解
2.1 恒定乘积公式 (Constant Product)
核心公式: x * y = k
x: Token A 储备量
y: Token B 储备量
k: 恒定乘积常数
特点:
- Uniswap V2 使用
- 价格随供需变化
- 适用于所有代币对
2.1.1 价格计算
// 现货价格 (Spot Price)
price_B_in_A = reserveA / reserveB
// 例子:
// 池子: 1000 ETH, 2000000 USDT
// ETH价格 = 2000000 / 1000 = 2000 USDT
// 实际交易价格包含滑点:
amountOut = (reserveB * amountIn) / (reserveA + amountIn)
2.1.2 滑点计算
// 滑点 (Price Impact)
priceImpact = (spotPrice - executionPrice) / spotPrice * 100%
// 例子:
// 买入 100 ETH
// 现货价: 2000 USDT
// 实际价: 2020 USDT
// 滑点: (2020 - 2000) / 2000 = 1%
2.2 恒定总和公式 (Constant Sum)
核心公式: x + y = k
特点:
- 零滑点
- 适用于稳定币对
- 流动性容易耗尽
2.3 混合曲线 (Curve)
核心公式: 结合恒定乘积和恒定总和
// Curve StableSwap公式
A * n^n * sum(x_i) + D = A * D * n^n + D^(n+1) / (n^n * prod(x_i))
其中:
A: 放大系数 (Amplification Coefficient)
n: 代币数量
D: 不变量 (Invariant)
特点:
- 低滑点稳定币交易
- 平衡点附近接近恒定总和
- 偏离平衡点接近恒定乘积
2.4 算法对比
| 算法 | 适用场景 | 滑点 | 无常损失 | 资本效率 |
|---|---|---|---|---|
| 恒定乘积 | 所有代币对 | 中等 | 高 | 低 |
| 恒定总和 | 稳定币 | 无 | 低 | 低 |
| Curve | 稳定币/相似资产 | 极低 | 低 | 高 |
| Uniswap V3 | 所有代币对 | 可控 | 高 | 极高 |
3. DEX核心组件
3.1 Factory合约
职责: 创建和管理交易对
// contracts/Factory.sol
pragma solidity ^0.8.20;
import "./Pair.sol";
contract Factory {
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
event PairCreated(
address indexed token0,
address indexed token1,
address pair,
uint256
);
// 创建交易对
function createPair(address tokenA, address tokenB)
external
returns (address pair)
{
require(tokenA != tokenB, "Identical addresses");
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA);
require(token0 != address(0), "Zero address");
require(getPair[token0][token1] == address(0), "Pair exists");
// 创建新Pair合约
bytes memory bytecode = type(Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
// 初始化Pair
Pair(pair).initialize(token0, token1);
// 注册
getPair[token0][token1] = pair;
getPair[token1][token0] = pair;
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
function allPairsLength() external view returns (uint256) {
return allPairs.length;
}
}
3.2 Pair合约 (核心)
职责: 执行交易、管理流动性
// contracts/Pair.sol
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Pair is ERC20, ReentrancyGuard {
address public token0;
address public token1;
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;
uint256 public kLast; // reserve0 * reserve1
event Mint(address indexed sender, uint256 amount0, uint256 amount1);
event Burn(address indexed sender, uint256 amount0, uint256 amount1);
event Swap(
address indexed sender,
uint256 amount0In,
uint256 amount1In,
uint256 amount0Out,
uint256 amount1Out
);
event Sync(uint112 reserve0, uint112 reserve1);
constructor() ERC20("LP Token", "LP") {}
function initialize(address _token0, address _token1) external {
require(token0 == address(0), "Already initialized");
token0 = _token0;
token1 = _token1;
}
// ========== 查询函数 ==========
function getReserves()
public
view
returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)
{
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
// ========== 添加流动性 ==========
function mint(address to) external nonReentrant returns (uint256 liquidity) {
(uint112 _reserve0, uint112 _reserve1, ) = getReserves();
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
uint256 amount0 = balance0 - _reserve0;
uint256 amount1 = balance1 - _reserve1;
uint256 _totalSupply = totalSupply();
if (_totalSupply == 0) {
// 首次添加流动性
liquidity = Math.sqrt(amount0 * amount1);
} else {
// 后续添加流动性
liquidity = Math.min(
(amount0 * _totalSupply) / _reserve0,
(amount1 * _totalSupply) / _reserve1
);
}
require(liquidity > 0, "Insufficient liquidity minted");
_mint(to, liquidity);
_update(balance0, balance1);
emit Mint(msg.sender, amount0, amount1);
}
// ========== 移除流动性 ==========
function burn(address to)
external
nonReentrant
returns (uint256 amount0, uint256 amount1)
{
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
uint256 liquidity = balanceOf(address(this));
uint256 _totalSupply = totalSupply();
amount0 = (liquidity * balance0) / _totalSupply;
amount1 = (liquidity * balance1) / _totalSupply;
require(amount0 > 0 && amount1 > 0, "Insufficient liquidity burned");
_burn(address(this), liquidity);
IERC20(token0).transfer(to, amount0);
IERC20(token1).transfer(to, amount1);
balance0 = IERC20(token0).balanceOf(address(this));
balance1 = IERC20(token1).balanceOf(address(this));
_update(balance0, balance1);
emit Burn(msg.sender, amount0, amount1);
}
// ========== Swap交易 ==========
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to
) external nonReentrant {
require(amount0Out > 0 || amount1Out > 0, "Insufficient output amount");
(uint112 _reserve0, uint112 _reserve1, ) = getReserves();
require(
amount0Out < _reserve0 && amount1Out < _reserve1,
"Insufficient liquidity"
);
// 转出代币
if (amount0Out > 0) IERC20(token0).transfer(to, amount0Out);
if (amount1Out > 0) IERC20(token1).transfer(to, amount1Out);
// 获取新余额
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
// 计算输入金额
uint256 amount0In = balance0 > _reserve0 - amount0Out
? balance0 - (_reserve0 - amount0Out)
: 0;
uint256 amount1In = balance1 > _reserve1 - amount1Out
? balance1 - (_reserve1 - amount1Out)
: 0;
require(amount0In > 0 || amount1In > 0, "Insufficient input amount");
// 验证 k 值 (扣除0.3%手续费)
uint256 balance0Adjusted = (balance0 * 1000) - (amount0In * 3);
uint256 balance1Adjusted = (balance1 * 1000) - (amount1In * 3);
require(
balance0Adjusted * balance1Adjusted >=
uint256(_reserve0) * uint256(_reserve1) * (1000**2),
"K value check failed"
);
_update(balance0, balance1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out);
}
// ========== 内部函数 ==========
function _update(uint256 balance0, uint256 balance1) private {
require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "Overflow");
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = uint32(block.timestamp);
emit Sync(reserve0, reserve1);
}
}
library Math {
function sqrt(uint256 y) internal pure returns (uint256 z) {
if (y > 3) {
z = y;
uint256 x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
function min(uint256 x, uint256 y) internal pure returns (uint256) {
return x < y ? x : y;
}
}
3.3 Router合约
职责: 简化用户交互、多跳交易
// contracts/Router.sol
pragma solidity ^0.8.20;
import "./interfaces/IFactory.sol";
import "./interfaces/IPair.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Router {
address public immutable factory;
address public immutable WETH;
constructor(address _factory, address _WETH) {
factory = _factory;
WETH = _WETH;
}
// ========== 添加流动性 ==========
function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
)
external
ensure(deadline)
returns (uint256 amountA, uint256 amountB, uint256 liquidity)
{
(amountA, amountB) = _addLiquidity(
tokenA,
tokenB,
amountADesired,
amountBDesired,
amountAMin,
amountBMin
);
address pair = IFactory(factory).getPair(tokenA, tokenB);
IERC20(tokenA).transferFrom(msg.sender, pair, amountA);
IERC20(tokenB).transferFrom(msg.sender, pair, amountB);
liquidity = IPair(pair).mint(to);
}
function _addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin
) internal returns (uint256 amountA, uint256 amountB) {
address pair = IFactory(factory).getPair(tokenA, tokenB);
if (pair == address(0)) {
pair = IFactory(factory).createPair(tokenA, tokenB);
}
(uint256 reserveA, uint256 reserveB) = getReserves(tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint256 amountBOptimal = quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, "Insufficient B amount");
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint256 amountAOptimal = quote(amountBDesired, reserveB, reserveA);
require(amountAOptimal <= amountADesired, "Amount A exceeded");
require(amountAOptimal >= amountAMin, "Insufficient A amount");
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
// ========== Swap交易 ==========
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external ensure(deadline) returns (uint256[] memory amounts) {
amounts = getAmountsOut(amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, "Insufficient output");
IERC20(path[0]).transferFrom(
msg.sender,
pairFor(path[0], path[1]),
amounts[0]
);
_swap(amounts, path, to);
}
function _swap(
uint256[] memory amounts,
address[] memory path,
address _to
) internal {
for (uint256 i = 0; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0, ) = sortTokens(input, output);
uint256 amountOut = amounts[i + 1];
(uint256 amount0Out, uint256 amount1Out) = input == token0
? (uint256(0), amountOut)
: (amountOut, uint256(0));
address to = i < path.length - 2
? pairFor(output, path[i + 2])
: _to;
IPair(pairFor(input, output)).swap(amount0Out, amount1Out, to);
}
}
// ========== 辅助函数 ==========
function getAmountsOut(uint256 amountIn, address[] memory path)
public
view
returns (uint256[] memory amounts)
{
require(path.length >= 2, "Invalid path");
amounts = new uint256[](path.length);
amounts[0] = amountIn;
for (uint256 i = 0; i < path.length - 1; i++) {
(uint256 reserveIn, uint256 reserveOut) = getReserves(
path[i],
path[i + 1]
);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
function getAmountOut(
uint256 amountIn,
uint256 reserveIn,
uint256 reserveOut
) public pure returns (uint256 amountOut) {
require(amountIn > 0, "Insufficient input amount");
require(reserveIn > 0 && reserveOut > 0, "Insufficient liquidity");
uint256 amountInWithFee = amountIn * 997; // 0.3% fee
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = (reserveIn * 1000) + amountInWithFee;
amountOut = numerator / denominator;
}
function quote(
uint256 amountA,
uint256 reserveA,
uint256 reserveB
) public pure returns (uint256 amountB) {
require(amountA > 0, "Insufficient amount");
require(reserveA > 0 && reserveB > 0, "Insufficient liquidity");
amountB = (amountA * reserveB) / reserveA;
}
function getReserves(address tokenA, address tokenB)
public
view
returns (uint256 reserveA, uint256 reserveB)
{
(address token0, ) = sortTokens(tokenA, tokenB);
address pair = pairFor(tokenA, tokenB);
(uint256 reserve0, uint256 reserve1, ) = IPair(pair).getReserves();
(reserveA, reserveB) = tokenA == token0
? (reserve0, reserve1)
: (reserve1, reserve0);
}
function pairFor(address tokenA, address tokenB)
internal
view
returns (address pair)
{
pair = IFactory(factory).getPair(tokenA, tokenB);
}
function sortTokens(address tokenA, address tokenB)
internal
pure
returns (address token0, address token1)
{
require(tokenA != tokenB, "Identical addresses");
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), "Zero address");
}
modifier ensure(uint256 deadline) {
require(deadline >= block.timestamp, "Expired");
_;
}
}
4. Swap交易功能
4.1 Swap流程图
用户发起Swap
│
▼
Router.swapExactTokensForTokens()
│
├─> 1. 计算输出金额 (getAmountsOut)
│
├─> 2. 检查滑点 (amountOut >= amountOutMin)
│
├─> 3. 转入token到第一个Pair
│
▼
Pair.swap()
│
├─> 4. 验证输出金额
│
├─> 5. 转出token到用户/下一个Pair
│
├─> 6. 验证 k 值 (考虑手续费)
│
├─> 7. 更新储备量
│
▼
交易完成,emit Swap事件
4.2 价格计算详解
单跳交易
// 输入金额计算输出
function getAmountOut(amountIn, reserveIn, reserveOut) {
const amountInWithFee = amountIn * 997; // 0.3% 手续费
const numerator = amountInWithFee * reserveOut;
const denominator = reserveIn * 1000 + amountInWithFee;
return numerator / denominator;
}
// 例子:
// 池子: 1000 ETH, 2000000 USDT
// 输入: 10 ETH
// 输出 = (10 * 997 * 2000000) / (1000 * 1000 + 10 * 997)
// = 19739400000 / 1009970
// = 19543 USDT
多跳交易
// ETH -> USDT -> DAI
path = [ETH, USDT, DAI]
// 第一跳: ETH -> USDT
amountOut1 = getAmountOut(amountIn, reserveETH, reserveUSDT)
// 第二跳: USDT -> DAI
amountOut2 = getAmountOut(amountOut1, reserveUSDT, reserveDAI)
// 最终输出: amountOut2
5. 流动性管理
5.1 添加流动性
5.1.1 首次添加
// 首次添加流动性
// LP Token数量 = sqrt(amount0 * amount1)
例子:
用户添加: 100 ETH + 200000 USDT
LP Token = sqrt(100 * 200000) = sqrt(20000000) ≈ 4472
价格: 1 ETH = 2000 USDT
5.1.2 后续添加
// 后续添加流动性
// LP Token = min(
// amount0 * totalSupply / reserve0,
// amount1 * totalSupply / reserve1
// )
例子:
当前池子: 1000 ETH, 2000000 USDT, 44721 LP
用户添加: 10 ETH, 20000 USDT
LP = min(
10 * 44721 / 1000 = 447.21,
20000 * 44721 / 2000000 = 447.21
) = 447.21
5.2 移除流动性
// 移除流动性
// amount0 = liquidity * balance0 / totalSupply
// amount1 = liquidity * balance1 / totalSupply
例子:
燃烧 447.21 LP
池子: 1000 ETH, 2000000 USDT, 44721 LP
ETH = 447.21 * 1000 / 44721 = 10
USDT = 447.21 * 2000000 / 44721 = 20000
5.3 无常损失 (Impermanent Loss)
// 无常损失计算公式
IL = 2 * sqrt(price_ratio) / (1 + price_ratio) - 1
例子:
初始价格: 1 ETH = 2000 USDT
现在价格: 1 ETH = 3000 USDT (上涨50%)
price_ratio = 3000 / 2000 = 1.5
IL = 2 * sqrt(1.5) / (1 + 1.5) - 1
= 2 * 1.2247 / 2.5 - 1
= 0.9798 - 1
= -2.02%
// 持有LP损失2.02% (相对于单独持有)
// 但会获得交易手续费补偿
6. 路由和价格发现
6.1 路径查找算法
// 寻找最优交易路径
function findBestPath(tokenIn, tokenOut, amountIn) {
// 1. 直接路径
const directPath = [tokenIn, tokenOut];
const directAmount = getAmountsOut(amountIn, directPath);
// 2. 通过中间token (如WETH, USDT)
const middleTokens = [WETH, USDT, USDC, DAI];
let bestPath = directPath;
let bestAmount = directAmount[1];
for (const middle of middleTokens) {
if (middle === tokenIn || middle === tokenOut) continue;
const path = [tokenIn, middle, tokenOut];
try {
const amounts = getAmountsOut(amountIn, path);
if (amounts[2] > bestAmount) {
bestPath = path;
bestAmount = amounts[2];
}
} catch {
// 路径不存在
}
}
return { path: bestPath, amountOut: bestAmount };
}
6.2 价格预言机
// TWAP (Time-Weighted Average Price)
contract PriceOracle {
struct Observation {
uint32 timestamp;
uint256 price0Cumulative;
uint256 price1Cumulative;
}
mapping(address => Observation[]) public observations;
// 更新价格累积值
function update(address pair) external {
(uint112 reserve0, uint112 reserve1, uint32 blockTimestamp) =
IPair(pair).getReserves();
uint256 price0 = uint256(reserve1) * 1e18 / reserve0;
uint256 price1 = uint256(reserve0) * 1e18 / reserve1;
Observation memory last = observations[pair][observations[pair].length - 1];
uint32 timeElapsed = blockTimestamp - last.timestamp;
observations[pair].push(Observation({
timestamp: blockTimestamp,
price0Cumulative: last.price0Cumulative + price0 * timeElapsed,
price1Cumulative: last.price1Cumulative + price1 * timeElapsed
}));
}
// 获取TWAP
function getTWAP(address pair, uint32 period)
external
view
returns (uint256 price0, uint256 price1)
{
Observation[] storage obs = observations[pair];
require(obs.length >= 2, "Insufficient data");
Observation memory latest = obs[obs.length - 1];
Observation memory oldest = obs[obs.length - 1];
// 找到period前的观察点
for (uint256 i = obs.length - 1; i > 0; i--) {
if (latest.timestamp - obs[i].timestamp >= period) {
oldest = obs[i];
break;
}
}
uint32 timeElapsed = latest.timestamp - oldest.timestamp;
require(timeElapsed >= period, "Insufficient time");
price0 = (latest.price0Cumulative - oldest.price0Cumulative) / timeElapsed;
price1 = (latest.price1Cumulative - oldest.price1Cumulative) / timeElapsed;
}
}
7. 功能测试实战
7.1 完整测试套件
// test/DEX.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("DEX Complete Test Suite", function() {
let factory;
let router;
let tokenA;
let tokenB;
let pair;
let owner;
let user1;
let user2;
beforeEach(async function() {
[owner, user1, user2] = await ethers.getSigners();
// 部署Factory
const Factory = await ethers.getContractFactory("Factory");
factory = await Factory.deploy();
// 部署WETH (mock)
const WETH = await ethers.getContractFactory("WETH9");
const weth = await WETH.deploy();
// 部署Router
const Router = await ethers.getContractFactory("Router");
router = await Router.deploy(await factory.getAddress(), await weth.getAddress());
// 部署测试代币
const Token = await ethers.getContractFactory("ERC20Token");
tokenA = await Token.deploy("Token A", "TKA", ethers.parseEther("1000000"));
tokenB = await Token.deploy("Token B", "TKB", ethers.parseEther("1000000"));
// 创建交易对
await factory.createPair(await tokenA.getAddress(), await tokenB.getAddress());
const pairAddress = await factory.getPair(
await tokenA.getAddress(),
await tokenB.getAddress()
);
pair = await ethers.getContractAt("Pair", pairAddress);
});
describe("1. Factory Tests", function() {
it("Should create pair correctly", async function() {
const Token = await ethers.getContractFactory("ERC20Token");
const tokenC = await Token.deploy("Token C", "TKC", ethers.parseEther("1000000"));
await factory.createPair(await tokenA.getAddress(), await tokenC.getAddress());
const pairAddress = await factory.getPair(
await tokenA.getAddress(),
await tokenC.getAddress()
);
expect(pairAddress).to.not.equal(ethers.ZeroAddress);
});
it("Should revert on duplicate pair creation", async function() {
await expect(
factory.createPair(await tokenA.getAddress(), await tokenB.getAddress())
).to.be.revertedWith("Pair exists");
});
it("Should track all pairs", async function() {
expect(await factory.allPairsLength()).to.equal(1);
const Token = await ethers.getContractFactory("ERC20Token");
const tokenC = await Token.deploy("Token C", "TKC", ethers.parseEther("1000000"));
await factory.createPair(await tokenA.getAddress(), await tokenC.getAddress());
expect(await factory.allPairsLength()).to.equal(2);
});
});
describe("2. Add Liquidity Tests", function() {
it("Should add initial liquidity correctly", async function() {
const amountA = ethers.parseEther("1000");
const amountB = ethers.parseEther("2000");
// 授权
await tokenA.approve(await router.getAddress(), amountA);
await tokenB.approve(await router.getAddress(), amountB);
// 添加流动性
const tx = await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
amountA,
amountB,
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
const receipt = await tx.wait();
// 检查LP token
const lpBalance = await pair.balanceOf(owner.address);
expect(lpBalance).to.be.gt(0);
// 检查储备量
const [reserve0, reserve1] = await pair.getReserves();
expect(reserve0).to.equal(amountA);
expect(reserve1).to.equal(amountB);
});
it("Should calculate liquidity for subsequent adds", async function() {
// 首次添加
const amountA1 = ethers.parseEther("1000");
const amountB1 = ethers.parseEther("2000");
await tokenA.approve(await router.getAddress(), amountA1);
await tokenB.approve(await router.getAddress(), amountB1);
await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
amountA1,
amountB1,
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
const lpBalance1 = await pair.balanceOf(owner.address);
// 第二次添加
const amountA2 = ethers.parseEther("100");
const amountB2 = ethers.parseEther("200");
await tokenA.approve(await router.getAddress(), amountA2);
await tokenB.approve(await router.getAddress(), amountB2);
await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
amountA2,
amountB2,
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
const lpBalance2 = await pair.balanceOf(owner.address);
// LP增加应该是按比例的
const lpIncrease = lpBalance2 - lpBalance1;
const expectedIncrease = (lpBalance1 * amountA2) / amountA1;
expect(lpIncrease).to.be.closeTo(expectedIncrease, ethers.parseEther("0.1"));
});
it("Should revert on insufficient liquidity", async function() {
await tokenA.approve(await router.getAddress(), 1000);
await tokenB.approve(await router.getAddress(), 1000);
await expect(
router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
1000,
1000,
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
)
).to.be.revertedWith("Insufficient liquidity minted");
});
});
describe("3. Swap Tests", function() {
beforeEach(async function() {
// 添加流动性
const amountA = ethers.parseEther("1000");
const amountB = ethers.parseEther("2000");
await tokenA.approve(await router.getAddress(), amountA);
await tokenB.approve(await router.getAddress(), amountB);
await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
amountA,
amountB,
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
});
it("Should swap tokens correctly", async function() {
const amountIn = ethers.parseEther("10");
await tokenA.approve(await router.getAddress(), amountIn);
const path = [await tokenA.getAddress(), await tokenB.getAddress()];
const amounts = await router.getAmountsOut(amountIn, path);
const balanceBefore = await tokenB.balanceOf(owner.address);
await router.swapExactTokensForTokens(
amountIn,
0,
path,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
const balanceAfter = await tokenB.balanceOf(owner.address);
expect(balanceAfter - balanceBefore).to.equal(amounts[1]);
});
it("Should respect slippage protection", async function() {
const amountIn = ethers.parseEther("10");
await tokenA.approve(await router.getAddress(), amountIn);
const path = [await tokenA.getAddress(), await tokenB.getAddress()];
const amounts = await router.getAmountsOut(amountIn, path);
const minOut = amounts[1] + 1n; // 要求更多输出
await expect(
router.swapExactTokensForTokens(
amountIn,
minOut,
path,
owner.address,
Math.floor(Date.now() / 1000) + 3600
)
).to.be.revertedWith("Insufficient output");
});
it("Should calculate price impact correctly", async function() {
const amountIn = ethers.parseEther("100"); // 大额交易
const [reserve0, reserve1] = await pair.getReserves();
const spotPrice = reserve1 * ethers.parseEther("1") / reserve0;
await tokenA.approve(await router.getAddress(), amountIn);
const path = [await tokenA.getAddress(), await tokenB.getAddress()];
const amounts = await router.getAmountsOut(amountIn, path);
const executionPrice = amounts[1] * ethers.parseEther("1") / amountIn;
const priceImpact = (spotPrice - executionPrice) * 10000n / spotPrice;
console.log("Spot Price:", ethers.formatEther(spotPrice));
console.log("Execution Price:", ethers.formatEther(executionPrice));
console.log("Price Impact:", priceImpact.toString(), "bps");
expect(priceImpact).to.be.gt(0); // 应该有滑点
});
it("Should handle multi-hop swaps", async function() {
// 创建第三个token和pair
const Token = await ethers.getContractFactory("ERC20Token");
const tokenC = await Token.deploy("Token C", "TKC", ethers.parseEther("1000000"));
await factory.createPair(await tokenB.getAddress(), await tokenC.getAddress());
// 添加B-C流动性
const amountB = ethers.parseEther("2000");
const amountC = ethers.parseEther("4000");
await tokenB.approve(await router.getAddress(), amountB);
await tokenC.approve(await router.getAddress(), amountC);
await router.addLiquidity(
await tokenB.getAddress(),
await tokenC.getAddress(),
amountB,
amountC,
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
// 执行A->B->C多跳交易
const amountIn = ethers.parseEther("10");
await tokenA.approve(await router.getAddress(), amountIn);
const path = [
await tokenA.getAddress(),
await tokenB.getAddress(),
await tokenC.getAddress()
];
const amounts = await router.getAmountsOut(amountIn, path);
await router.swapExactTokensForTokens(
amountIn,
0,
path,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
const balanceC = await tokenC.balanceOf(owner.address);
expect(balanceC).to.equal(amounts[2]);
});
});
describe("4. Remove Liquidity Tests", function() {
let lpAmount;
beforeEach(async function() {
// 添加流动性
const amountA = ethers.parseEther("1000");
const amountB = ethers.parseEther("2000");
await tokenA.approve(await router.getAddress(), amountA);
await tokenB.approve(await router.getAddress(), amountB);
await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
amountA,
amountB,
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
lpAmount = await pair.balanceOf(owner.address);
});
it("Should remove liquidity correctly", async function() {
// 转移LP到pair(用于burn)
await pair.transfer(await pair.getAddress(), lpAmount);
const balanceABefore = await tokenA.balanceOf(owner.address);
const balanceBBefore = await tokenB.balanceOf(owner.address);
await pair.burn(owner.address);
const balanceAAfter = await tokenA.balanceOf(owner.address);
const balanceBAfter = await tokenB.balanceOf(owner.address);
expect(balanceAAfter - balanceABefore).to.be.gt(0);
expect(balanceBAfter - balanceBBefore).to.be.gt(0);
});
it("Should return proportional amounts", async function() {
const [reserve0, reserve1] = await pair.getReserves();
const totalSupply = await pair.totalSupply();
const expectedA = (lpAmount * reserve0) / totalSupply;
const expectedB = (lpAmount * reserve1) / totalSupply;
await pair.transfer(await pair.getAddress(), lpAmount);
await pair.burn(owner.address);
const balanceA = await tokenA.balanceOf(owner.address);
const balanceB = await tokenB.balanceOf(owner.address);
expect(balanceA).to.be.closeTo(expectedA, ethers.parseEther("0.1"));
expect(balanceB).to.be.closeTo(expectedB, ethers.parseEther("0.1"));
});
});
describe("5. Edge Cases", function() {
it("Should handle very small amounts", async function() {
const amountA = ethers.parseEther("0.001");
const amountB = ethers.parseEther("0.002");
await tokenA.approve(await router.getAddress(), amountA);
await tokenB.approve(await router.getAddress(), amountB);
await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
amountA,
amountB,
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
expect(await pair.balanceOf(owner.address)).to.be.gt(0);
});
it("Should handle imbalanced adds after trading", async function() {
// 添加初始流动性
await tokenA.approve(await router.getAddress(), ethers.parseEther("1000"));
await tokenB.approve(await router.getAddress(), ethers.parseEther("2000"));
await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
ethers.parseEther("1000"),
ethers.parseEther("2000"),
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
// 进行交易改变比例
await tokenA.approve(await router.getAddress(), ethers.parseEther("100"));
await router.swapExactTokensForTokens(
ethers.parseEther("100"),
0,
[await tokenA.getAddress(), await tokenB.getAddress()],
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
// 尝试添加不按新比例的流动性
await tokenA.approve(await router.getAddress(), ethers.parseEther("100"));
await tokenB.approve(await router.getAddress(), ethers.parseEther("200"));
// Router应该自动调整
await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
ethers.parseEther("100"),
ethers.parseEther("200"),
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
});
});
describe("6. Fee Tests", function() {
it("Should collect 0.3% fee on swaps", async function() {
// 添加流动性
await tokenA.approve(await router.getAddress(), ethers.parseEther("1000"));
await tokenB.approve(await router.getAddress(), ethers.parseEther("2000"));
await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
ethers.parseEther("1000"),
ethers.parseEther("2000"),
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
const [reserve0Before, reserve1Before] = await pair.getReserves();
const kBefore = reserve0Before * reserve1Before;
// 执行交易
await tokenA.approve(await router.getAddress(), ethers.parseEther("10"));
await router.swapExactTokensForTokens(
ethers.parseEther("10"),
0,
[await tokenA.getAddress(), await tokenB.getAddress()],
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
const [reserve0After, reserve1After] = await pair.getReserves();
const kAfter = reserve0After * reserve1After;
// k应该增加(因为手续费)
expect(kAfter).to.be.gt(kBefore);
});
});
});
8. 安全测试实战
8.1 重入攻击测试
// contracts/ReentrancyAttacker.sol
pragma solidity ^0.8.20;
import "./interfaces/IPair.sol";
import "./interfaces/IERC20.sol";
contract ReentrancyAttacker {
IPair public pair;
IERC20 public token0;
IERC20 public token1;
uint256 public attackCount;
bool public attacking;
constructor(address _pair) {
pair = IPair(_pair);
token0 = IERC20(pair.token0());
token1 = IERC20(pair.token1());
}
function attack() external {
attacking = true;
attackCount = 0;
// 尝试重入swap
pair.swap(1000, 0, address(this));
}
// 接收token时尝试重入
function onERC20Received(address, uint256) external returns (bytes4) {
if (attacking && attackCount < 5) {
attackCount++;
pair.swap(1000, 0, address(this));
}
return this.onERC20Received.selector;
}
}
// test/Security.test.js
describe("Security Tests", function() {
it("Should prevent reentrancy attack", async function() {
// 添加流动性
await addLiquidity();
// 部署攻击合约
const Attacker = await ethers.getContractFactory("ReentrancyAttacker");
const attacker = await Attacker.deploy(await pair.getAddress());
// 攻击应该失败
await expect(attacker.attack()).to.be.reverted;
});
});
8.2 整数溢出测试
describe("Integer Overflow Protection", function() {
it("Should prevent overflow in swap calculation", async function() {
// 尝试导致溢出的交易
const maxUint112 = 2n ** 112n - 1n;
await expect(
pair.swap(maxUint112, 0, owner.address)
).to.be.revertedWith("Overflow");
});
it("Should handle max liquidity", async function() {
const maxAmount = ethers.MaxUint256 / 2n;
await tokenA.mint(owner.address, maxAmount);
await tokenB.mint(owner.address, maxAmount);
await tokenA.approve(await router.getAddress(), maxAmount);
await tokenB.approve(await router.getAddress(), maxAmount);
// 应该能处理大额流动性
await router.addLiquidity(
await tokenA.getAddress(),
await tokenB.getAddress(),
maxAmount,
maxAmount,
0,
0,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
});
});
9. 价格操纵测试
9.1 闪电贷价格操纵
// contracts/FlashLoanAttacker.sol
pragma solidity ^0.8.20;
contract FlashLoanAttacker {
IPair public pair;
ILendingPool public lendingPool;
address public targetProtocol;
function attack() external {
// 1. 闪电贷借出大量token
lendingPool.flashLoan(address(this), token, amount, data);
}
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address,
bytes calldata
) external returns (bool) {
// 2. 用借来的token在DEX大量买入
// 操纵价格
pair.swap(amount, 0, address(this));
// 3. 在其他协议利用被操纵的价格
// (例如:低价清算、套利等)
targetProtocol.exploit();
// 4. 卖出token,还原价格
pair.swap(0, amount, address(this));
// 5. 归还闪电贷
IERC20(asset).transfer(msg.sender, amount + premium);
return true;
}
}
// test/PriceManipulation.test.js
describe("Price Manipulation Attack", function() {
it("Should demonstrate flash loan price manipulation", async function() {
// 创建小池子(容易操纵)
await addLiquidity(ethers.parseEther("100"), ethers.parseEther("200"));
// 记录初始价格
const [reserve0Before, reserve1Before] = await pair.getReserves();
const priceBefore = reserve1Before * ethers.parseEther("1") / reserve0Before;
// 模拟闪电贷大额买入
const largeAmount = ethers.parseEther("1000");
await tokenA.mint(owner.address, largeAmount);
await tokenA.approve(await router.getAddress(), largeAmount);
await router.swapExactTokensForTokens(
largeAmount,
0,
[await tokenA.getAddress(), await tokenB.getAddress()],
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
// 价格被严重操纵
const [reserve0After, reserve1After] = await pair.getReserves();
const priceAfter = reserve1After * ethers.parseEther("1") / reserve0After;
const priceChange = (priceAfter - priceBefore) * 10000n / priceBefore;
console.log("Price manipulation:", priceChange.toString(), "bps");
expect(Math.abs(Number(priceChange))).to.be.gt(1000); // >10%变化
});
it("Should use TWAP to resist manipulation", async function() {
// 使用时间加权平均价格(TWAP)可以抵御单次交易操纵
// 这需要价格预言机合约
const Oracle = await ethers.getContractFactory("PriceOracle");
const oracle = await Oracle.deploy();
// 初始化观察点
await oracle.update(await pair.getAddress());
// 等待一段时间
await ethers.provider.send("evm_increaseTime", [3600]); // 1小时
// 更新观察点
await oracle.update(await pair.getAddress());
// 进行大额交易
await router.swapExactTokensForTokens(
ethers.parseEther("1000"),
0,
[await tokenA.getAddress(), await tokenB.getAddress()],
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
// TWAP不应该被单次交易影响太多
const [twapPrice0, twapPrice1] = await oracle.getTWAP(
await pair.getAddress(),
3600
);
console.log("TWAP Price:", ethers.formatEther(twapPrice0));
});
});
10. MEV攻击测试
10.1 三明治攻击 (Sandwich Attack)
攻击流程:
1. 攻击者监控mempool
2. 发现用户大额交易
3. 抢先以高gas发送买入交易 (front-run)
4. 用户交易执行(价格被推高)
5. 攻击者卖出获利 (back-run)
// test/MEV.test.js
describe("MEV Attack Tests", function() {
it("Should demonstrate sandwich attack", async function() {
// 添加流动性
await addLiquidity(ethers.parseEther("1000"), ethers.parseEther("2000"));
// 受害者准备交易
const victimAmount = ethers.parseEther("100");
await tokenA.transfer(user1.address, victimAmount);
await tokenA.connect(user1).approve(await router.getAddress(), victimAmount);
// 攻击者监控到这笔交易
// 步骤1: 攻击者抢先买入 (front-run)
const attackerAmount = ethers.parseEther("50");
await tokenA.approve(await router.getAddress(), attackerAmount);
const path = [await tokenA.getAddress(), await tokenB.getAddress()];
// 攻击者交易(高gas)
await router.swapExactTokensForTokens(
attackerAmount,
0,
path,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
const attackerTokenBAfterBuy = await tokenB.balanceOf(owner.address);
// 步骤2: 受害者交易执行(价格已被推高)
const balanceBefore = await tokenB.balanceOf(user1.address);
await router.connect(user1).swapExactTokensForTokens(
victimAmount,
0,
path,
user1.address,
Math.floor(Date.now() / 1000) + 3600
);
const balanceAfter = await tokenB.balanceOf(user1.address);
const victimReceived = balanceAfter - balanceBefore;
// 步骤3: 攻击者卖出获利 (back-run)
await tokenB.approve(await router.getAddress(), attackerTokenBAfterBuy);
const reversePath = [await tokenB.getAddress(), await tokenA.getAddress()];
await router.swapExactTokensForTokens(
attackerTokenBAfterBuy,
0,
reversePath,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
const attackerTokenAAfterSell = await tokenA.balanceOf(owner.address);
// 攻击者获利
const attackerProfit = attackerTokenAAfterSell - (ethers.parseEther("1000000") - attackerAmount);
console.log("Attacker profit:", ethers.formatEther(attackerProfit));
console.log("Victim received:", ethers.formatEther(victimReceived));
expect(attackerProfit).to.be.gt(0);
});
it("Should use slippage protection to mitigate sandwich attack", async function() {
await addLiquidity(ethers.parseEther("1000"), ethers.parseEther("2000"));
const victimAmount = ethers.parseEther("100");
await tokenA.transfer(user1.address, victimAmount);
await tokenA.connect(user1).approve(await router.getAddress(), victimAmount);
// 计算预期输出
const path = [await tokenA.getAddress(), await tokenB.getAddress()];
const expectedOut = await router.getAmountsOut(victimAmount, path);
// 设置1%滑点保护
const minOut = (expectedOut[1] * 99n) / 100n;
// 攻击者先买入
await router.swapExactTokensForTokens(
ethers.parseEther("50"),
0,
path,
owner.address,
Math.floor(Date.now() / 1000) + 3600
);
// 受害者交易应该失败(滑点保护)
await expect(
router.connect(user1).swapExactTokensForTokens(
victimAmount,
minOut,
path,
user1.address,
Math.floor(Date.now() / 1000) + 3600
)
).to.be.revertedWith("Insufficient output");
});
});
11. 闪电贷攻击测试
11.1 闪电贷重入攻击
// contracts/FlashLoanReentrancy.sol
pragma solidity ^0.8.20;
contract FlashLoanReentrancy {
IPair public pair;
bool public inFlashLoan;
function attack() external {
// 发起闪电swap
pair.swap(1000, 0, address(this), abi.encode("attack"));
}
// Uniswap V2 callback
function uniswapV2Call(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external {
require(msg.sender == address(pair), "Unauthorized");
require(!inFlashLoan, "Reentrancy detected");
inFlashLoan = true;
// 尝试在回调中重入swap
try pair.swap(100, 0, address(this), "") {
// 如果成功,说明有重入漏洞
} catch {
// 重入被阻止
}
// 归还闪电贷
IERC20(pair.token0()).transfer(address(pair), amount0 * 1004 / 1000);
inFlashLoan = false;
}
}
// test/FlashLoan.test.js
describe("Flash Loan Attack Tests", function() {
it("Should prevent flash loan reentrancy", async function() {
await addLiquidity(ethers.parseEther("1000"), ethers.parseEther("2000"));
const Attacker = await ethers.getContractFactory("FlashLoanReentrancy");
const attacker = await Attacker.deploy(await pair.getAddress());
// 给攻击合约一些初始资金(用于支付手续费)
await tokenA.transfer(await attacker.getAddress(), ethers.parseEther("10"));
// 攻击应该失败
await expect(attacker.attack()).to.be.reverted;
});
it("Should handle flash swap correctly", async function() {
await addLiquidity(ethers.parseEther("1000"), ethers.parseEther("2000"));
// 正常的闪电swap用法
const FlashBorrower = await ethers.getContractFactory("FlashBorrower");
const borrower = await FlashBorrower.deploy();
const borrowAmount = ethers.parseEther("100");
// 执行闪电借贷
await borrower.flashBorrow(
await pair.getAddress(),
await tokenA.getAddress(),
borrowAmount
);
// 验证归还
const [reserve0, reserve1] = await pair.getReserves();
expect(reserve0).to.be.gte(ethers.parseEther("1000"));
});
});
12. 多链DEX测试
12.1 跨链流动性测试
describe("Multi-Chain DEX Tests", function() {
let l1Factory, l1Router;
let l2Factory, l2Router;
beforeEach(async function() {
// 部署L1 (主网)
l1Factory = await deployFactory();
l1Router = await deployRouter(l1Factory);
// 部署L2 (Arbitrum/Optimism)
l2Factory = await deployFactory();
l2Router = await deployRouter(l2Factory);
});
it("Should maintain price consistency across chains", async function() {
// 在两条链上创建相同的交易对
await createPair(l1Factory, tokenA, tokenB);
await createPair(l2Factory, tokenA, tokenB);
// 添加流动性
await addLiquidity(l1Router, ethers.parseEther("1000"), ethers.parseEther("2000"));
await addLiquidity(l2Router, ethers.parseEther("1000"), ethers.parseEther("2000"));
// 获取两条链的价格
const l1Price = await getPrice(l1Factory, tokenA, tokenB);
const l2Price = await getPrice(l2Factory, tokenA, tokenB);
// 价格应该相近(考虑套利机会)
const priceDiff = Math.abs(Number(l1Price - l2Price)) * 10000 / Number(l1Price);
expect(priceDiff).to.be.lt(100); // <1% 差异
});
it("Should handle cross-chain arbitrage", async function() {
// 在L1和L2创建价格差异
await createPriceDifference(l1Router, l2Router);
// 模拟套利者
const arbitrager = user1;
// 在便宜的链买入
const buyAmount = ethers.parseEther("100");
await router1.connect(arbitrager).swapExactTokensForTokens(/*...*/);
// 跨链转移(通过桥)
await bridge.transfer(tokenA, l2, buyAmount);
// 在贵的链卖出
await router2.connect(arbitrager).swapExactTokensForTokens(/*...*/);
// 验证套利利润
const profit = await calculateProfit(arbitrager);
expect(profit).to.be.gt(0);
});
});
13. 性能和Gas优化
13.1 Gas优化测试
describe("Gas Optimization Tests", function() {
it("Should compare gas costs for different operations", async function() {
const operations = [];
// 测试1: 单跳 vs 多跳
const singleHopTx = await router.swapExactTokensForTokens(
ethers.parseEther("10"),
0,
[tokenA, tokenB],
owner.address,
deadline
);
const singleHopReceipt = await singleHopTx.wait();
operations.push({
name: "Single Hop Swap",
gas: singleHopReceipt.gasUsed
});
const multiHopTx = await router.swapExactTokensForTokens(
ethers.parseEther("10"),
0,
[tokenA, tokenC, tokenB],
owner.address,
deadline
);
const multiHopReceipt = await multiHopTx.wait();
operations.push({
name: "Multi Hop Swap",
gas: multiHopReceipt.gasUsed
});
// 测试2: 添加流动性
const addLiqTx = await router.addLiquidity(/*...*/);
const addLiqReceipt = await addLiqTx.wait();
operations.push({
name: "Add Liquidity",
gas: addLiqReceipt.gasUsed
});
// 测试3: 移除流动性
const removeLiqTx = await router.removeLiquidity(/*...*/);
const removeLiqReceipt = await removeLiqTx.wait();
operations.push({
name: "Remove Liquidity",
gas: removeLiqReceipt.gasUsed
});
// 打印gas报告
console.log("\n=== Gas Usage Report ===");
operations.forEach(op => {
console.log(`${op.name}: ${op.gas.toString()} gas`);
});
});
it("Should optimize for batch operations", async function() {
// 单独操作 vs 批量操作
const individualGas = [];
for (let i = 0; i < 5; i++) {
const tx = await router.swapExactTokensForTokens(/*...*/);
const receipt = await tx.wait();
individualGas.push(receipt.gasUsed);
}
const totalIndividual = individualGas.reduce((a, b) => a + b, 0n);
// 批量操作(如果支持)
const batchTx = await router.batchSwap(/*...*/);
const batchReceipt = await batchTx.wait();
console.log("Individual operations gas:", totalIndividual.toString());
console.log("Batch operation gas:", batchReceipt.gasUsed.toString());
console.log("Savings:", (totalIndividual - batchReceipt.gasUsed).toString());
});
});
14. 真实案例分析
14.1 Uniswap V2攻击案例
案例1: The DAO黑客利用Uniswap
时间: 2020年4月
损失: $300,000
攻击流程:
1. 黑客发现token价格预言机使用单一DEX价格
2. 使用闪电贷操纵Uniswap价格
3. 在其他协议利用错误价格清算/借贷
4. 归还闪电贷,获利离场
测试复现:
it("Should reproduce oracle manipulation attack", async function() {
// 1. 创建依赖DEX价格的借贷协议
const lending = await deployLendingProtocol(pair);
// 2. 正常用户抵押借款
await lending.connect(user).deposit(collateral);
await lending.connect(user).borrow(amount);
// 3. 攻击者操纵价格
const flashLoan = ethers.parseEther("10000");
await flashLoanProvider.loan(attacker, flashLoan);
// 大量买入,推高价格
await router.connect(attacker).swap(flashLoan, /*...*/);
// 4. 利用错误价格清算
const canLiquidate = await lending.checkLiquidation(user.address);
expect(canLiquidate).to.be.true; // 错误地可以清算
await lending.connect(attacker).liquidate(user.address);
// 5. 还原价格,归还闪电贷
await router.connect(attacker).swap(/*...*/);
});
防护措施:
// 使用TWAP而不是即时价格
contract SecureLending {
IPriceOracle public oracle;
function checkLiquidation(address user) public view returns (bool) {
// 使用时间加权平均价格
uint256 price = oracle.getTWAP(pair, 3600); // 1小时TWAP
// 而不是即时价格
// uint256 price = oracle.getSpotPrice(pair);
uint256 collateralValue = userCollateral[user] * price;
uint256 borrowValue = userDebt[user];
return collateralValue < borrowValue * liquidationThreshold / 100;
}
}
14.2 Sushiswap Vampire Attack
案例描述: Sushiswap通过激励迁移Uniswap流动性
测试模拟:
it("Should simulate liquidity migration", async function() {
// Uniswap pool
const uniPair = await deployPair();
await addLiquidity(uniPair, ethers.parseEther("1000"), ethers.parseEther("2000"));
// Sushiswap pool (提供额外奖励)
const sushiPair = await deployPair();
const sushiRewards = await deploySushiRewards();
// 用户迁移流动性
const lpBalance = await uniPair.balanceOf(user.address);
// 1. 从Uniswap移除流动性
await uniPair.connect(user).approve(router.address, lpBalance);
const [amountA, amountB] = await router.connect(user).removeLiquidity(/*...*/);
// 2. 添加到Sushiswap
await router.connect(user).addLiquidity(sushiPair, amountA, amountB);
// 3. 质押LP获取SUSHI奖励
const sushiLP = await sushiPair.balanceOf(user.address);
await sushiRewards.connect(user).stake(sushiLP);
// 验证迁移
expect(await uniPair.balanceOf(user.address)).to.equal(0);
expect(await sushiRewards.stakedBalance(user.address)).to.be.gt(0);
});
15. 测试最佳实践
15.1 测试组织结构
test/
├── unit/
│ ├── Factory.test.js
│ ├── Pair.test.js
│ └── Router.test.js
├── integration/
│ ├── SwapFlow.test.js
│ ├── LiquidityFlow.test.js
│ └── MultiHop.test.js
├── security/
│ ├── Reentrancy.test.js
│ ├── PriceManipulation.test.js
│ └── FlashLoan.test.js
├── mev/
│ ├── Sandwich.test.js
│ ├── Arbitrage.test.js
│ └── Liquidation.test.js
└── gas/
└── Optimization.test.js
15.2 测试工具推荐
# 主要框架
npm install --save-dev hardhat
npm install --save-dev @nomicfoundation/hardhat-toolbox
# Fork测试
npm install --save-dev @nomicfoundation/hardhat-network-helpers
# Gas报告
npm install --save-dev hardhat-gas-reporter
# 覆盖率
npm install --save-dev solidity-coverage
# 安全分析
pip install slither-analyzer
pip install mythril
15.3 持续集成配置
# .github/workflows/test.yml
name: DEX Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Compile contracts
run: npx hardhat compile
- name: Run unit tests
run: npx hardhat test test/unit/**/*.test.js
- name: Run integration tests
run: npx hardhat test test/integration/**/*.test.js
- name: Run security tests
run: npx hardhat test test/security/**/*.test.js
- name: Run coverage
run: npx hardhat coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
- name: Run Slither
run: slither contracts/
- name: Gas report
run: REPORT_GAS=true npx hardhat test
16. DEX测试Checklist
✅ 功能测试清单
## Factory
- [ ] 创建交易对
- [ ] 防止重复创建
- [ ] 正确排序token地址
- [ ] 追踪所有交易对
- [ ] 设置费用接收地址
## Pair
- [ ] 初始化正确
- [ ] 添加流动性(首次)
- [ ] 添加流动性(后续)
- [ ] 移除流动性
- [ ] Swap交易(A->B)
- [ ] Swap交易(B->A)
- [ ] 正确计算k值
- [ ] 收取手续费
- [ ] emit正确事件
- [ ] 更新储备量
- [ ] 处理零金额
- [ ] 处理最大金额
## Router
- [ ] 单跳swap
- [ ] 多跳swap
- [ ] 滑点保护
- [ ] 截止时间检查
- [ ] 路径验证
- [ ] 最优路径选择
- [ ] 批量操作
✅ 安全测试清单
## 漏洞测试
- [ ] 重入攻击防护
- [ ] 整数溢出检查
- [ ] 访问控制验证
- [ ] 前端运行防护
- [ ] 时间戳依赖
- [ ] 闪电贷攻击
- [ ] 价格操纵防护
- [ ] DoS攻击防护
## MEV测试
- [ ] 三明治攻击
- [ ] 抢跑交易
- [ ] 套利机会
- [ ] 清算机器人
## 经济安全
- [ ] 无常损失计算
- [ ] 手续费分配
- [ ] 滑点计算
- [ ] 价格影响
- [ ] 流动性激励
✅ 性能测试清单
## Gas优化
- [ ] 单跳swap gas
- [ ] 多跳swap gas
- [ ] 添加流动性 gas
- [ ] 移除流动性 gas
- [ ] 批量操作优化
## 压力测试
- [ ] 大额交易
- [ ] 高频交易
- [ ] 深度流动性
- [ ] 多用户并发
总结:精通DEX测试的关键
✅ 核心知识
-
DEX原理
- AMM算法(恒定乘积、恒定总和、Curve)
- 价格发现机制
- 流动性管理
- 无常损失
-
核心组件
- Factory: 创建管理
- Pair: 交易执行
- Router: 用户接口
-
安全风险
- 重入攻击
- 价格操纵
- 闪电贷攻击
- MEV攻击
-
测试方法
- 功能测试
- 安全测试
- 价格测试
- Gas测试
✅ 实践路径
-
基础阶段(1个月)
- 理解AMM原理
- 部署简单DEX
- 写基础测试
-
进阶阶段(2-3个月)
- 完整功能测试
- 安全漏洞测试
- 价格操纵测试
-
高级阶段(3-6个月)
- MEV攻击研究
- 多链DEX测试
- 性能优化
-
专家阶段(持续)
- 参与真实审计
- 发现新攻击向量
- 贡献开源项目
✅ 推荐资源
最后更新: 2026-03-14
作者: DeFi Security Team
版权: MIT License
看完这份文档并完成所有测试实践,你就具备了专业的DEX测试经验!
评论区