JAMMRouter Contract

Overview

JAMMRouter is the user interface layer contract of JAMM DEX, providing user-friendly trading and liquidity management functions. It encapsulates complex underlying logic, supports multi-hop swaps, automatic path calculation, deadline protection, and is deeply integrated with WJU, enabling users to trade directly using JU tokens.

Contract Basic Information

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

Immutable Variables:

  • factory: JAMMFactory contract address

  • WJU: WJU wrapped token contract address

Modifiers

Deadline Protection

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

All trading functions use this modifier to ensure transactions execute within the specified time.

ETH Reception

receive() external payable {
    assert(msg.sender == WJU); // Only accept ETH from WJU contract
}

Router only accepts ETH from WJU contract, used for ETH transfers during WJU unwrapping.

Liquidity Management

Internal Liquidity Calculation

function _addLiquidity(
    address tokenA,
    address tokenB,
    uint24 fee,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
    // Create pair if it doesn't exist
    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) {
        // First liquidity addition
        (amountA, amountB) = (amountADesired, amountBDesired);
    } else {
        // Calculate optimal amounts
        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);
        }
    }
}

Calculation Logic:

  1. Check if trading pair exists, create if not

  2. Get current reserves

  3. If first addition, use desired amounts

  4. Otherwise calculate optimal ratio, ensuring minimum amount requirements

Add Liquidity

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

Add JU Liquidity

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);
    // Refund excess JU
    if (msg.value > amountETH)
        TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}

JU Processing Flow:

  1. Calculate optimal amounts

  2. Transfer tokens to trading pair

  3. Wrap JU to WJU and transfer to trading pair

  4. Mint LP tokens

  5. Refund excess JU

Remove Liquidity

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); // Send LP tokens to pair
    (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");
}

Remove JU Liquidity

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 Support

Remove Liquidity with 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 Advantages:

  • No need to pre-call approve

  • Complete authorization and operation in one transaction

  • Reduce gas consumption and transaction steps

Token Swapping

Internal Swap Logic

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

Multi-hop Swap Logic:

  1. Validate relationship between path and fee array lengths

  2. Iterate through swap path

  3. Calculate output amount for each hop

  4. Determine recipient address for next hop

  5. Call trading pair's swap function

Exact Input 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);
}

Exact Output Swap

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 Token Swapping

Buy Tokens with 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);
}

Sell Tokens for 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]);
}

Fee-on-Transfer Token Support

Internal Swap Logic (Fee-on-Transfer Support)

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

Fee-on-Transfer Token Handling:

  • Don't pre-calculate amounts, base on actual received amounts

  • Recalculate input amount for each hop

  • Suitable for tokens that charge fees on transfer

Fee-on-Transfer Swap Functions

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"
    );
}

Query Functions

Price Query

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

Output Amount Calculation

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

Input Amount Calculation

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

Multi-hop Amount Calculation

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

Usage Examples

Token Swapping

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

// Exact input swap
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 Token Swapping

// Buy tokens with JU
const tx = await router.swapExactETHForTokens(
    amountOutMin,
    [WJU_ADDRESS, tokenB],
    [100],
    to,
    referrer,
    deadline,
    { value: ethers.utils.parseEther("1.0") }
);

Add Liquidity

// Add token liquidity
const tx = await router.addLiquidity(
    tokenA,
    tokenB,
    100, // 1% fee rate
    ethers.utils.parseEther("10"),
    ethers.utils.parseEther("10"),
    ethers.utils.parseEther("9"),
    ethers.utils.parseEther("9"),
    to,
    deadline
);

Price Query

// Query output amount
const amountOut = await router.getAmountOut(
    ethers.utils.parseEther("1"),
    reserve0,
    reserve1,
    100
);

// Query multi-hop output
const amounts = await router.getAmountsOut(
    ethers.utils.parseEther("1"),
    [tokenA, tokenB, tokenC],
    [100, 200]
);

Last updated