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();

运行与验证

  1. 将上述代码保存为 constant_product_amm.js。
  2. 在终端执行:node constant_product_amm.js
  3. 观察输出:
  • 初始 k = 2000000(1000 × 2000)
  • 输入100 Token0后,输出约为 181 Token1(精确值由整数除法决定)
  • 兑换后 k 不小于初始值减1(因整数除法精度损失最多1)
  • 最后一行输出 true,表示代码实现与公式完全一致

通过条件:程序无异常退出,且控制台打印的验证项均为 true 或满足上述数值范围。

关键说明

  • 使用 BigInt(n后缀)避免JavaScript浮点精度问题,所有金额均为整数最小单位。
  • 手续费模型:每笔交易收取0.3%手续费,实际进入储备的输入为 amountIn * 997 / 1000,公式中已体现。
  • 实际Uniswap V2合约中还需处理流动性添加/移除、闪兑等逻辑,本案例聚焦核心兑换公式。