从19.1的数学建模到19.7的链上部署,我们已经完成了一个简易DEX的全链路工程。然而,合约一旦部署即不可更改,任何经济逻辑上的漏洞都将造成真实的资产损失。19.8节将回到测试的视角,用Hardhat测试套件从数学上严格验证三项核心假设:恒定乘积是否成立、Swap输出是否精确、无常损失是否可被公式量化。这是整个第19章的最后一道质量关卡。
19.8.1 为什么要写测试套件
智能合约的测试不仅是"代码是否能跑通",更是"经济学假设是否成立"。在传统Web开发中,我们可以通过热补丁(hotfix)修复线上bug;在区块链上,合约状态不可篡改,任何数学漏洞都会导致真实资金损失。
测试分为三个层次:
- 单元测试(Unit Test):验证单个函数的边界行为,如
getAmountOut(0)应返回0,addLiquidity在首次注入时LP Token总量是否正确。 - 集成测试(Integration Test):验证多个合约之间的交互,如Factory创建Pair后Router是否能正确路由。
- 端到端测试(E2E Test):模拟真实用户场景,从部署到Swap到移除流动性的完整资产流动闭环。
Hardhat测试框架提供 loadFixture 与 evm_snapshot / evm_revert,使我们能在每个测试用例之间快速重置链状态,同时又能构造复杂的时间序列场景。
19.8.2 测试用例一:初始流动性后k值验证
场景:向空池子注入10,000 TTA + 10,000 TTB,验证储备量与LP Token总量。
const { expect } = require("chai");
const { ethers } = require("hardhat");
it("初始流动性注入后储备与LP总量验证", async () => {
const [owner] = await ethers.getSigners();
const amount = ethers.parseUnits("10000", 18);
// 授权并添加流动性
await tokenA.approve(router.target, amount);
await tokenB.approve(router.target, amount);
await router.addLiquidity(tokenA.target, tokenB.target, amount, amount, 0, 0, owner.address, deadline);
const [reserve0, reserve1] = await pair.getReserves();
expect(reserve0).to.equal(amount);
expect(reserve1).to.equal(amount);
const totalSupply = await pair.totalSupply();
const expectedLP = ethers.parseUnits("10000", 18); // sqrt(10^22 * 10^22) / 10^18 = 10^22 / 10^18 = 10^4 (scaled by 10^18)
expect(totalSupply).to.equal(expectedLP);
const k = reserve0 * reserve1;
const expectedK = amount * amount;
expect(k).to.equal(expectedK);
});要点:首次注入时LP Token按 铸造。当 (含18位精度)时,,以18位精度表示即为 。
19.8.3 测试用例二:精确Swap计算验证
场景:向已有池子(10000, 10000)输入1,000 TTA,验证链上输出与数学公式是否一致。
含手续费的精确输出公式:
代入数值:
it("Swap输出与数学公式一致", async () => {
// ... 已添加初始流动性
const amountIn = ethers.parseUnits("1000", 18);
const reserveBefore = await pair.getReserves();
// 链上执行swap(无滑点保护,仅用于验证)
await tokenA.approve(router.target, amountIn);
await router.swapExactTokensForTokens(
amountIn, 0, [tokenA.target, tokenB.target], owner.address, deadline
);
const reserveAfter = await pair.getReserves();
const actualOut = reserveBefore[1] - reserveAfter[1];
// 数学计算(BigInt整数运算,允许1 wei舍入误差)
const amountInWithFee = amountIn * 997n / 1000n;
const numerator = reserveBefore[0] * reserveBefore[1];
const denominator = reserveBefore[0] + amountInWithFee;
const expectedOut = reserveBefore[1] - numerator / denominator;
// 允许最多2 wei的舍入差异
const diff = actualOut > expectedOut ? actualOut - expectedOut : expectedOut - actualOut;
expect(diff).to.be.lte(2n);
});这一测试的意义在于:合约的Solidity实现与我们的数学模型至少在数值上是等价的。如果测试失败,说明合约中的fee处理或除法舍入策略存在问题。
19.8.4 测试用例三:无常损失量化测试
场景:LP以1:1价格注入流动性(10,000 TTA + 10,000 TTB),随后大额交易将池内价格从1:1推至1:2,LP移除全部流动性并计算损失。
无常损失公式推导
当价格从 变为 时,定义价格比率 。LP提取时的资产数量为:
其中 为LP Token总量(保持守恒)。无常损失定义为LP资产价值与HODL策略的价值差异百分比:
当 (价格翻倍)时:
这意味着:如果LP单纯"持有不动",资产价值会比HODL策略低约5.71%。注意这是"相对于HODL的机会成本",而非绝对亏损。如果手续费收益超过5.71%,LP仍然是盈利的。
Hardhat 测试脚本
it("无常损失量化:价格1→2时损失约5.71%", async () => {
const [owner, trader] = await ethers.getSigners();
const tenK = ethers.parseUnits("10000", 18);
// 1. LP注入流动性(1:1)
await tokenA.approve(router.target, tenK);
await tokenB.approve(router.target, tenK);
await router.addLiquidity(tokenA.target, tokenB.target, tenK, tenK, 0, 0, owner.address, deadline);
// 2. 交易者用大量TTA买入TTB,将价格从1:1推至1:2
// 根据 AMM 公式,要使价格变为1:2,需要在池中有大约7071 TTA和14142 TTB(含手续费修正)
// 这里通过反复swap使池到达目标比例,实际数值由脚本迭代确定
const largeAmount = ethers.parseUnits("100000", 18);
await tokenA.connect(trader).approve(router.target, largeAmount);
// 实际测试中会找到使价格比恰好达到1:2的输入量,此处示意
// ...
// 3. LP移除流动性
const lpBalance = await pair.balanceOf(owner.address);
await pair.approve(router.target, lpBalance);
const beforeA = await tokenA.balanceOf(owner.address);
const beforeB = await tokenB.balanceOf(owner.address);
await router.removeLiquidity(tokenA.target, tokenB.target, lpBalance, 0, 0, owner.address, deadline);
const afterA = await tokenA.balanceOf(owner.address);
const afterB = await tokenB.balanceOf(owner.address);
const retrievedA = afterA - beforeA;
const retrievedB = afterB - beforeB;
// 4. 计算价值(假设外部市场仍为1:2,以TTB为计价单位)
const lpValue = retrievedA * 2n + retrievedB; // 1 TTA = 2 TTB
const hodlValue = tenK * 2n + tenK; // 如果LP从未提供流动性,仍持有10kTTA+10kTTB
// 允许1%容差
const ilPercent = Number(lpValue - hodlValue) / Number(hodlValue);
expect(Math.abs(ilPercent + 0.0571)).to.be.lessThan(0.01);
});19.8.5 Python/JS 计算脚本:IL公式数值验证
为了直观理解无常损失的严重程度,我们用Python绘制不同价格比率下的IL曲线:
import numpy as np
import matplotlib.pyplot as plt
def impermanent_loss(rho):
"""rho = P1 / P0,价格比率"""
return 2 * np.sqrt(rho) / (1 + rho) - 1
rho = np.linspace(0.1, 10, 500)
il = impermanent_loss(rho) * 100 # 转换为百分比
plt.figure(figsize=(10, 6))
plt.plot(rho, il, 'b-', linewidth=2)
plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
plt.axvline(1, color='gray', linestyle='--', alpha=0.5)
plt.xlabel('Price Ratio (ρ = P₁ / P₀)')
plt.ylabel('Impermanent Loss (%)')
plt.title('Impermanent Loss vs Price Ratio')
plt.grid(True, alpha=0.3)
# 标注几个关键点
for r in [0.5, 1, 2, 4]:
loss = impermanent_loss(r) * 100
plt.annotate(f'ρ={r}: {loss:.1f}%', xy=(r, loss), xytext=(r, loss-3),
arrowprops=dict(arrowstyle='->', lw=1, color='red'))
plt.tight_layout()
plt.savefig('impermanent_loss.png', dpi=150)
plt.show()关键数值速查
| 价格比率 ρ | 无常损失 |
|---|---|
| 0.5 (跌50%) | -5.72% |
| 1.0 (不变) | 0% |
| 2.0 (涨100%) | -5.71% |
| 4.0 (涨300%) | -20.0% |
| 10.0 (涨900%) | -41.9% |
无常损失曲线关于 对称。价格偏离越远,损失增速递减但绝对值快速扩大。对于流动性提供者而言,理解IL是风险管理的第一步。
19.8.6 第19章完整工程闭环回顾
让我们站到一个更高的视角,回顾从19.1到19.8的全过程:
flowchart LR
A[19.1-19.2<br/>AMM数学模型<br/>恒定乘积公式] --> B[19.3<br/>Swap手续费<br/>滑点保护]
B --> C[19.4<br/>价格预言机<br/>闪电贷防御]
C --> D[19.5<br/>前端UI<br/>钱包+交易面板]
D --> E[19.6<br/>端到端测试<br/>全流程验证]
E --> F[19.7<br/>合约部署<br/>源码验证]
F --> G[19.8<br/>数学验证<br/>无常损失测试]
G --> A
这七个小节构成了一个完整的DEX开发闭环:
- 数学模型定义了系统的不变式()。
- 合约实现将数学转化为链上可执行代码。
- 前端产品让普通用户无需理解Solidity即可交互。
- 测试验证确保代码实现与数学模型等价。
- 部署运维将产品推向真实环境。
一个成功的DEX项目,绝不仅是几行Solidity代码——它是数学、工程、产品与安全的系统工程。
要点总结:
- 智能合约测试不仅是验证代码正确性,更是验证经济学假设的数值准确性。
- Swap输出测试应精确到
wei级别,确保合约实现与数学公式等价。- 无常损失公式 是LP风险管理的核心工具,必须通过单元测试在代码层面验证。
- DEX开发的闭环是:数学模型 → 合约实现 → 前端产品 → 测试验证 → 部署运维。
本章小结:带走的3个关键认知
- 测试是合约安全的最后防线:在不可篡改的链上环境中,Hardhat测试框架提供的快照、时间操纵与精确数值断言能力,是发现和预防经济漏洞的唯一手段。
- 数学公式必须在代码中验证:恒定乘积、手续费处理、无常损失——所有这些经济模型的正确性,最终都应体现为通过/失败的测试用例,而非仅停留在纸面推导。
- DEX是一个系统工程的闭环:从数学直觉到Solidity合约,从React前端到Hardhat测试,从本地开发到链上部署——每一步都不可或缺。理解这个闭环,是走向Web3产品开发者的必经之路。
评论
0评论加载中…