JAMMRouter 合约

JAMMRouter是JAMM DEX的用户接口层合约,为用户提供友好的交易和流动性管理功能。它封装了复杂的底层逻辑,支持多跳交换、自动路径计算、截止时间保护,并与WJU深度集成,使用户能够直接使用JU代币进行交易。

合约基本信息

contract JAMMRouter is IJAMMRouter02 {
    address public immutable override factory;
    address public immutable override WJU;
    
    constructor(address _factory, address _WJU) {
        factory = _factory;
        WJU = _WJU;
    }
}

不可变变量

  • factory: JAMMFactory合约地址

  • WJU: WJU包装代币合约地址

修饰符

截止时间保护

modifier ensure(uint deadline) {
    require(deadline >= block.timestamp, "JAMMRouter: EXPIRED");
    _;
}

所有交易函数都使用此修饰符,确保交易在指定时间内执行。

ETH接收

receive() external payable {
    assert(msg.sender == WJU); // 只接受来自WJU合约的ETH
}

Router只接受来自WJU合约的ETH,用于WJU解包装时的ETH转账。

流动性管理

内部流动性计算

function _addLiquidity(
    address tokenA,
    address tokenB,
    uint24 fee,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
    // 如果交易对不存在则创建
    if (IJAMMFactory(factory).getPair(tokenA, tokenB, fee) == address(0)) {
        IJAMMFactory(factory).createPair(tokenA, tokenB, fee);
    }
    (uint reserveA, uint reserveB) = JAMMLibrary.getReserves(
        factory,
        tokenA,
        tokenB,
        fee
    );
    if (reserveA == 0 && reserveB == 0) {
        // 首次添加流动性
        (amountA, amountB) = (amountADesired, amountBDesired);
    } else {
        // 计算最优数量
        uint amountBOptimal = JAMMLibrary.quote(
            amountADesired,
            reserveA,
            reserveB
        );
        if (amountBOptimal <= amountBDesired) {
            require(
                amountBOptimal >= amountBMin,
                "JAMMRouter: INSUFFICIENT_B_AMOUNT"
            );
            (amountA, amountB) = (amountADesired, amountBOptimal);
        } else {
            uint amountAOptimal = JAMMLibrary.quote(
                amountBDesired,
                reserveB,
                reserveA
            );
            assert(amountAOptimal <= amountADesired);
            require(
                amountAOptimal >= amountAMin,
                "JAMMRouter: INSUFFICIENT_A_AMOUNT"
            );
            (amountA, amountB) = (amountAOptimal, amountBDesired);
        }
    }
}

计算逻辑

  1. 检查交易对是否存在,不存在则创建

  2. 获取当前储备量

  3. 如果是首次添加,使用期望数量

  4. 否则计算最优比例,确保不超过最小数量要求

添加流动性

function addLiquidity(
    address tokenA,
    address tokenB,
    uint24 fee,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
)
    external
    virtual
    override
    ensure(deadline)
    returns (uint amountA, uint amountB, uint liquidity)
{
    (amountA, amountB) = _addLiquidity(
        tokenA,
        tokenB,
        fee,
        amountADesired,
        amountBDesired,
        amountAMin,
        amountBMin
    );
    address pair = JAMMLibrary.pairFor(factory, tokenA, tokenB, fee);
    TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
    TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
    liquidity = IJAMMPair(pair).mint(to);
}

添加JU流动性

function addLiquidityETH(
    address token,
    uint24 fee,
    uint amountTokenDesired,
    uint amountTokenMin,
    uint amountETHMin,
    address to,
    uint deadline
)
    external
    payable
    virtual
    override
    ensure(deadline)
    returns (uint amountToken, uint amountETH, uint liquidity)
{
    (amountToken, amountETH) = _addLiquidity(
        token,
        WJU,
        fee,
        amountTokenDesired,
        msg.value,
        amountTokenMin,
        amountETHMin
    );
    address pair = JAMMLibrary.pairFor(factory, token, WJU, fee);
    TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
    IWJU(WJU).deposit{value: amountETH}();
    assert(IWJU(WJU).transfer(pair, amountETH));
    liquidity = IJAMMPair(pair).mint(to);
    // 退还多余的JU
    if (msg.value > amountETH)
        TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}

JU处理流程

  1. 计算最优数量

  2. 转入代币到交易对

  3. 将JU包装为WJU并转入交易对

  4. 铸造LP代币

  5. 退还多余的JU

移除流动性

function removeLiquidity(
    address tokenA,
    address tokenB,
    uint24 fee,
    uint liquidity,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
)
    public
    virtual
    override
    ensure(deadline)
    returns (uint amountA, uint amountB)
{
    address pair = JAMMLibrary.pairFor(factory, tokenA, tokenB, fee);
    IJAMMPair(pair).transferFrom(msg.sender, pair, liquidity); // 发送LP代币到交易对
    (uint amount0, uint amount1) = IJAMMPair(pair).burn(to);
    (address token0, ) = JAMMLibrary.sortTokens(tokenA, tokenB);
    (amountA, amountB) = tokenA == token0
        ? (amount0, amount1)
        : (amount1, amount0);
    require(amountA >= amountAMin, "JAMMRouter: INSUFFICIENT_A_AMOUNT");
    require(amountB >= amountBMin, "JAMMRouter: INSUFFICIENT_B_AMOUNT");
}

移除JU流动性

function removeLiquidityETH(
    address token,
    uint24 fee,
    uint liquidity,
    uint amountTokenMin,
    uint amountETHMin,
    address to,
    uint deadline
)
    public
    virtual
    override
    ensure(deadline)
    returns (uint amountToken, uint amountETH)
{
    (amountToken, amountETH) = removeLiquidity(
        token,
        WJU,
        fee,
        liquidity,
        amountTokenMin,
        amountETHMin,
        address(this),
        deadline
    );
    TransferHelper.safeTransfer(token, to, amountToken);
    IWJU(WJU).withdraw(amountETH);
    TransferHelper.safeTransferETH(to, amountETH);
}

Permit支持

带Permit的流动性移除

function removeLiquidityWithPermit(
    address tokenA,
    address tokenB,
    uint24 fee,
    uint liquidity,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline,
    bool approveMax,
    uint8 v,
    bytes32 r,
    bytes32 s
) external virtual override returns (uint amountA, uint amountB) {
    IJAMMPair(JAMMLibrary.pairFor(factory, tokenA, tokenB, fee)).permit(
        msg.sender,
        address(this),
        approveMax ? type(uint).max : liquidity,
        deadline,
        v,
        r,
        s
    );
    (amountA, amountB) = removeLiquidity(
        tokenA,
        tokenB,
        fee,
        liquidity,
        amountAMin,
        amountBMin,
        to,
        deadline
    );
}

Permit优势

  • 无需预先调用approve

  • 一次交易完成授权和操作

  • 减少Gas消耗和交易步骤

代币交换

内部交换逻辑

function _swap(
    uint[] memory amounts,
    address[] memory path,
    uint24[] memory fees,
    address _to,
    address _referrer
) internal virtual {
    require(path.length - fees.length == 1, "JAMMRouter: INVALID_PATH");
    for (uint i; i < path.length - 1; i++) {
        (address input, address output) = (path[i], path[i + 1]);
        (address token0, ) = JAMMLibrary.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
            ? JAMMLibrary.pairFor(factory, output, path[i + 2], fees[i + 1])
            : _to;
        IJAMMPair(JAMMLibrary.pairFor(factory, input, output, fees[i]))
            .swap(amount0Out, amount1Out, to, new bytes(0), _referrer);
    }
}

多跳交换逻辑

  1. 验证路径和费率数组的长度关系

  2. 遍历交换路径

  3. 计算每一跳的输出数量

  4. 确定下一跳的接收地址

  5. 调用交易对的swap函数

精确输入交换

function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
)
    external
    virtual
    override
    ensure(deadline)
    returns (uint[] memory amounts)
{
    amounts = JAMMLibrary.getAmountsOut(factory, amountIn, path, fees);
    require(
        amounts[amounts.length - 1] >= amountOutMin,
        "JAMMRouter: INSUFFICIENT_OUTPUT_AMOUNT"
    );
    TransferHelper.safeTransferFrom(
        path[0],
        msg.sender,
        JAMMLibrary.pairFor(factory, path[0], path[1], fees[0]),
        amounts[0]
    );
    _swap(amounts, path, fees, to, referrer);
}

精确输出交换

function swapTokensForExactTokens(
    uint amountOut,
    uint amountInMax,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
)
    external
    virtual
    override
    ensure(deadline)
    returns (uint[] memory amounts)
{
    amounts = JAMMLibrary.getAmountsIn(factory, amountOut, path, fees);
    require(
        amounts[0] <= amountInMax,
        "JAMMRouter: EXCESSIVE_INPUT_AMOUNT"
    );
    TransferHelper.safeTransferFrom(
        path[0],
        msg.sender,
        JAMMLibrary.pairFor(factory, path[0], path[1], fees[0]),
        amounts[0]
    );
    _swap(amounts, path, fees, to, referrer);
}

JU代币交换

用JU购买代币

function swapExactETHForTokens(
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
)
    external
    payable
    virtual
    override
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[0] == WJU, "JAMMRouter: INVALID_PATH");
    amounts = JAMMLibrary.getAmountsOut(factory, msg.value, path, fees);
    require(
        amounts[amounts.length - 1] >= amountOutMin,
        "JAMMRouter: INSUFFICIENT_OUTPUT_AMOUNT"
    );
    IWJU(WJU).deposit{value: amounts[0]}();
    assert(
        IWJU(WJU).transfer(
            JAMMLibrary.pairFor(factory, path[0], path[1], fees[0]),
            amounts[0]
        )
    );
    _swap(amounts, path, fees, to, referrer);
}

卖出代币换JU

function swapExactTokensForETH(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
)
    external
    virtual
    override
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[path.length - 1] == WJU, "JAMMRouter: INVALID_PATH");
    amounts = JAMMLibrary.getAmountsOut(factory, amountIn, path, fees);
    require(
        amounts[amounts.length - 1] >= amountOutMin,
        "JAMMRouter: INSUFFICIENT_OUTPUT_AMOUNT"
    );
    TransferHelper.safeTransferFrom(
        path[0],
        msg.sender,
        JAMMLibrary.pairFor(factory, path[0], path[1], fees[0]),
        amounts[0]
    );
    _swap(amounts, path, fees, address(this), referrer);
    IWJU(WJU).withdraw(amounts[amounts.length - 1]);
    TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}

支持转账费代币

内部交换逻辑(支持转账费)

function _swapSupportingFeeOnTransferTokens(
    address[] memory path,
    uint24[] memory fees,
    address _to,
    address _referrer
) internal virtual {
    for (uint i; i < path.length - 1; i++) {
        SwapData memory data;
        (data.input, data.output) = (path[i], path[i + 1]);
        (data.token0, ) = JAMMLibrary.sortTokens(data.input, data.output);

        IJAMMPair pair = IJAMMPair(
            JAMMLibrary.pairFor(factory, data.input, data.output, fees[i])
        );
        (uint reserve0, uint reserve1, ) = pair.getReserves();

        (uint reserveInput, uint reserveOutput) = data.input == data.token0
            ? (reserve0, reserve1)
            : (reserve1, reserve0);

        data.amountInput =
            IERC20(data.input).balanceOf(address(pair)) -
            reserveInput;
        data.amountOutput = JAMMLibrary.getAmountOut(
            data.amountInput,
            reserveInput,
            reserveOutput,
            fees[i]
        );

        (data.amount0Out, data.amount1Out) = data.input == data.token0
            ? (uint(0), data.amountOutput)
            : (data.amountOutput, uint(0));

        address to = i < path.length - 2
            ? JAMMLibrary.pairFor(
                factory,
                data.output,
                path[i + 2],
                fees[i + 1]
            )
            : _to;

        pair.swap(
            data.amount0Out,
            data.amount1Out,
            to,
            new bytes(0),
            _referrer
        );
    }
}

转账费代币处理

  • 不预先计算数量,而是基于实际到账数量

  • 每一跳都重新计算输入数量

  • 适用于转账时收费的代币

支持转账费的交换函数

function swapExactTokensForTokensSupportingFeeOnTransferTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
) external virtual override ensure(deadline) {
    TransferHelper.safeTransferFrom(
        path[0],
        msg.sender,
        JAMMLibrary.pairFor(factory, path[0], path[1], fees[0]),
        amountIn
    );
    uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
    _swapSupportingFeeOnTransferTokens(path, fees, to, referrer);
    require(
        IERC20(path[path.length - 1]).balanceOf(to) - balanceBefore >=
            amountOutMin,
        "JAMMRouter: INSUFFICIENT_OUTPUT_AMOUNT"
    );
}

查询函数

价格查询

function quote(
    uint amountA,
    uint reserveA,
    uint reserveB
) public pure virtual override returns (uint amountB) {
    return JAMMLibrary.quote(amountA, reserveA, reserveB);
}

输出数量计算

function getAmountOut(
    uint amountIn,
    uint reserveIn,
    uint reserveOut,
    uint24 fee
) public pure virtual override returns (uint amountOut) {
    return JAMMLibrary.getAmountOut(amountIn, reserveIn, reserveOut, fee);
}

输入数量计算

function getAmountIn(
    uint amountOut,
    uint reserveIn,
    uint reserveOut,
    uint24 fee
) public pure virtual override returns (uint amountIn) {
    return JAMMLibrary.getAmountIn(amountOut, reserveIn, reserveOut, fee);
}

多跳数量计算

function getAmountsOut(
    uint amountIn,
    address[] memory path,
    uint24[] memory fees
) public view virtual override returns (uint[] memory amounts) {
    return JAMMLibrary.getAmountsOut(factory, amountIn, path, fees);
}

function getAmountsIn(
    uint amountOut,
    address[] memory path,
    uint24[] memory fees
) public view virtual override returns (uint[] memory amounts) {
    return JAMMLibrary.getAmountsIn(factory, amountOut, path, fees);
}

使用示例

代币交换

const router = new ethers.Contract(routerAddress, routerABI, signer);

// 精确输入交换
const amountIn = ethers.utils.parseEther("1.0");
const amountOutMin = ethers.utils.parseEther("0.95");
const path = [tokenA, tokenB];
const fees = [100]; // 1%
const to = signer.address;
const referrer = ethers.constants.AddressZero;
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;

const tx = await router.swapExactTokensForTokens(
    amountIn,
    amountOutMin,
    path,
    fees,
    to,
    referrer,
    deadline
);

JU代币交换

// 用JU购买代币
const tx = await router.swapExactETHForTokens(
    amountOutMin,
    [WJU_ADDRESS, tokenB],
    [100],
    to,
    referrer,
    deadline,
    { value: ethers.utils.parseEther("1.0") }
);

添加流动性

// 添加代币流动性
const tx = await router.addLiquidity(
    tokenA,
    tokenB,
    100, // 1%费率
    ethers.utils.parseEther("10"),
    ethers.utils.parseEther("10"),
    ethers.utils.parseEther("9"),
    ethers.utils.parseEther("9"),
    to,
    deadline
);

价格查询

// 查询输出数量
const amountOut = await router.getAmountOut(
    ethers.utils.parseEther("1"),
    reserve0,
    reserve1,
    100
);

// 查询多跳输出
const amounts = await router.getAmountsOut(
    ethers.utils.parseEther("1"),
    [tokenA, tokenB, tokenC],
    [100, 200]
);

Last updated