1. 恒定乘积公式实现
任务目标
实现Uniswap V2风格的恒定乘积自动做市商核心逻辑:给定两种代币的储备量,完成swap(兑换)与getAmountOut(输出金额计算)功能,并验证价格与储备量的关系满足x * y = k。
核心公式
- 恒定乘积:
x * y = k,其中x、y为两种代币的储备量,k为常数。 - 输出计算(Uniswap V2):
amountOut = (amountIn * 0.997 * reserveOut) / (reserveIn + amountIn * 0.997),其中0.997为0.3%手续费后的留存比例。 - 边际价格:
price = reserveOut / reserveIn(以输入代币计价输出代币)。
实现代码
// constant_product_amm.js
// 运行环境:Node.js 12+,无第三方依赖
class ConstantProductAMM {
/**
* @param {bigint} reserve0 代币0的储备量(最小单位)
* @param {bigint} reserve1 代币1的储备量(最小单位)
*/
constructor(reserve0, reserve1) {
if (reserve0 <= 0n || reserve1 <= 0n) {
throw new Error('Reserves must be positive');
}
this.reserve0 = reserve0;
this.reserve1 = reserve1;
this.FEE_NUMERATOR = 997n; // 0.3% 手续费,保留 99.7%
this.FEE_DENOMINATOR = 1000n;
}
/**
* 计算兑换输出金额(Uniswap V2 公式)
* @param {bigint} amountIn 输入代币数量
* @param {boolean} isInputToken0 true=输入代币0,false=输入代币1
* @returns {bigint} 输出代币数量
*/
getAmountOut(amountIn, isInputToken0) {
if (amountIn <= 0n) throw new Error('Input amount must be positive');
const reserveIn = isInputToken0 ? this.reserve0 : this.reserve1;
const reserveOut = isInputToken0 ? this.reserve1 : this.reserve0;
const amountInWithFee = amountIn * this.FEE_NUMERATOR;
const numerator = amountInWithFee * reserveOut;
const denominator = reserveIn * this.FEE_DENOMINATOR + amountInWithFee;
return numerator / denominator;
}
/**
* 执行兑换,更新储备量
* @param {bigint} amountIn 输入数量
* @param {boolean} isInputToken0 true=输入代币0
* @returns {bigint} 实际输出数量
*/
swap(amountIn, isInputToken0) {
const amountOut = this.getAmountOut(amountIn, isInputToken0);
if (isInputToken0) {
this.reserve0 += amountIn;
this.reserve1 -= amountOut;
} else {
this.reserve1 += amountIn;
this.reserve0 -= amountOut;
}
// 防御性检查:恒定乘积必须不减少
const kBefore = this.reserve0 * this.reserve1;
// 由于整数除法,k可能略微减少(最多损失1个最小单位),此处仅做合理性校验
if (this.reserve0 <= 0n || this.reserve1 <= 0n) {
throw new Error('Reserve cannot be zero after swap');
}
return amountOut;
}
/** 获取当前边际价格(以代币0计价代币1) */
getPriceToken0InToken1() {
return this.reserve1 / this.reserve0;
}
}
// ---------- 最小验证 ----------
function runVerification() {
// 初始储备:1000 Token0,2000 Token1
const amm = new ConstantProductAMM(1000n, 2000n);
const kInitial = amm.reserve0 * amm.reserve1;
console.log('初始 k =', kInitial.toString());
// 兑换 100 Token0
const amountIn = 100n;
const amountOut = amm.swap(amountIn, true);
console.log(`输入 ${amountIn} Token0,输出 ${amountOut} Token1`);
// 验证恒定乘积不减少(允许整数除法损失1)
const kAfter = amm.reserve0 * amm.reserve1;
console.log('兑换后 k =', kAfter.toString());
console.log('k 未减少:', kAfter >= kInitial - 1n);
// 验证价格计算
console.log('当前价格(1 Token0 值多少 Token1):', amm.getPriceToken0InToken1().toString());
// 验证输出金额与预期公式一致
const expectedOut = (amountIn * 997n * 2000n) / (1000n * 1000n + amountIn * 997n);
console.log('输出金额与公式一致:', amountOut === expectedOut);
}
runVerification();
运行与验证
- 将上述代码保存为
constant_product_amm.js。 - 在终端执行:
node constant_product_amm.js - 观察输出:
- 初始
k = 2000000(1000 × 2000) - 输入100 Token0后,输出约为
181Token1(精确值由整数除法决定) - 兑换后
k不小于初始值减1(因整数除法精度损失最多1) - 最后一行输出
true,表示代码实现与公式完全一致
通过条件:程序无异常退出,且控制台打印的验证项均为 true 或满足上述数值范围。
关键说明
- 使用
BigInt(n后缀)避免JavaScript浮点精度问题,所有金额均为整数最小单位。 - 手续费模型:每笔交易收取0.3%手续费,实际进入储备的输入为
amountIn * 997 / 1000,公式中已体现。 - 实际Uniswap V2合约中还需处理流动性添加/移除、闪兑等逻辑,本案例聚焦核心兑换公式。