JAMMPair Contract

Overview

JAMMPair is the core trading pair contract of JAMM DEX, implementing specific AMM liquidity pool functionality. Each trading pair is an independent JAMMPair contract instance, responsible for managing reserves of two tokens, executing swap logic, handling liquidity operations, and maintaining price oracle data.

Contract Inheritance Structure

contract JAMMPair is JAMMERC20 {
    using UQ112x112 for uint224;
    // ...
}

JAMMPair inherits from JAMMERC20, meaning each trading pair is itself an ERC-20 token (LP token).

Core Constants

uint public constant MINIMUM_LIQUIDITY = 10 ** 3;  // 1000
bytes4 private constant SELECTOR = bytes4(keccak256(bytes("transfer(address,uint256)")));
  • MINIMUM_LIQUIDITY: Permanently locked minimum liquidity amount

  • SELECTOR: ERC-20 transfer function selector for safe transfers

State Variables

Basic Information

address public factory;    // Factory contract address
address public token0;     // First token address (smaller address)
address public token1;     // Second token address (larger address)
uint24 public fee;         // Trading fee rate

Reserve Management

uint112 private reserve0;           // token0 reserves
uint112 private reserve1;           // token1 reserves
uint32 private blockTimestampLast;  // Last update block timestamp

Storage Optimization: Three variables packed in one storage slot:

  • uint112 + uint112 + uint32 = 256 bits

Price Oracle

uint public price0CumulativeLast;  // token0 cumulative price
uint public price1CumulativeLast;  // token1 cumulative price
uint public kLast;                 // Last k value (reserve0 * reserve1)

Reentrancy Protection

uint private unlocked = 1;
modifier lock() {
    require(unlocked == 1, "JAMM: LOCKED");
    unlocked = 0;
    _;
    unlocked = 1;
}

Initialization

Constructor

constructor() {
    factory = msg.sender;
}

Constructor only sets factory address, actual initialization is done through initialize function.

Initialize Function

function initialize(
    address _token0,
    address _token1,
    uint24 _fee
) external {
    require(msg.sender == factory, "JAMM: FORBIDDEN");
    token0 = _token0;
    token1 = _token1;
    fee = _fee;
}

Security: Only Factory contract can call the initialize function.

Core Functions

Reserve Query

function getReserves()
    public
    view
    returns (
        uint112 _reserve0,
        uint112 _reserve1,
        uint32 _blockTimestampLast
    )
{
    _reserve0 = reserve0;
    _reserve1 = reserve1;
    _blockTimestampLast = blockTimestampLast;
}

This is the most commonly used query function, returning current reserves and last update time.

Reserve Update

function _update(
    uint balance0,
    uint balance1,
    uint112 _reserve0,
    uint112 _reserve1
) private {
    require(
        balance0 <= type(uint112).max && balance1 <= type(uint112).max,
        "JAMM: OVERFLOW"
    );
    uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32);
    uint32 timeElapsed = blockTimestamp - blockTimestampLast;
    if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
        // Update cumulative prices
        price0CumulativeLast +=
            uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) *
            timeElapsed;
        price1CumulativeLast +=
            uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) *
            timeElapsed;
    }
    reserve0 = uint112(balance0);
    reserve1 = uint112(balance1);
    blockTimestampLast = blockTimestamp;
    emit Sync(reserve0, reserve1);
}

Update Logic:

  1. Check balances won't overflow uint112

  2. Calculate time elapsed

  3. Update cumulative prices (if time elapsed and reserves are non-zero)

  4. Update reserves and timestamp

  5. Emit Sync event

Liquidity Management

Add Liquidity (Mint)

function mint(address to) external lock returns (uint liquidity) {
    (uint112 _reserve0, uint112 _reserve1, ) = getReserves();
    uint balance0 = JAMMERC20(token0).balanceOf(address(this));
    uint balance1 = JAMMERC20(token1).balanceOf(address(this));
    uint amount0 = balance0 - _reserve0;
    uint amount1 = balance1 - _reserve1;

    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply;
    if (_totalSupply == 0) {
        liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
        _mint(address(0), MINIMUM_LIQUIDITY); // Permanently lock
    } else {
        liquidity = Math.min(
            (amount0 * _totalSupply) / _reserve0,
            (amount1 * _totalSupply) / _reserve1
        );
    }
    require(liquidity > 0, "JAMM: INSUFFICIENT_LIQUIDITY_MINTED");
    _mint(to, liquidity);

    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0) * uint(reserve1);
    emit Mint(msg.sender, amount0, amount1);
}

Minting Process:

  1. Get current reserves and balances

  2. Calculate newly added token amounts

  3. Handle protocol fees

  4. Calculate LP tokens to mint

  5. Mint LP tokens to specified address

  6. Update reserves

  7. Update k value (if protocol fees enabled)

  8. Emit Mint event

Remove Liquidity (Burn)

function burn(
    address to
) external lock returns (uint amount0, uint amount1) {
    (uint112 _reserve0, uint112 _reserve1, ) = getReserves();
    address _token0 = token0;
    address _token1 = token1;
    uint balance0 = JAMMERC20(_token0).balanceOf(address(this));
    uint balance1 = JAMMERC20(_token1).balanceOf(address(this));
    uint liquidity = balanceOf[address(this)];

    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply;
    amount0 = (liquidity * balance0) / _totalSupply;
    amount1 = (liquidity * balance1) / _totalSupply;
    require(
        amount0 > 0 && amount1 > 0,
        "JAMM: INSUFFICIENT_LIQUIDITY_BURNED"
    );
    _burn(address(this), liquidity);
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);
    balance0 = JAMMERC20(_token0).balanceOf(address(this));
    balance1 = JAMMERC20(_token1).balanceOf(address(this));

    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0) * uint(reserve1);
    emit Burn(msg.sender, amount0, amount1, to);
}

Burning Process:

  1. Get current state

  2. Handle protocol fees

  3. Calculate proportional token amounts to return

  4. Burn LP tokens

  5. Transfer tokens to specified address

  6. Update reserves

  7. Emit Burn event

Token Swapping

Main Swap Function

function swap(
    uint amount0Out,
    uint amount1Out,
    address to,
    bytes calldata data,
    address _referrer
) external lock {
    require(
        amount0Out > 0 || amount1Out > 0,
        "JAMM: INSUFFICIENT_OUTPUT_AMOUNT"
    );
    (uint112 _reserve0, uint112 _reserve1, ) = getReserves();
    require(
        amount0Out < _reserve0 && amount1Out < _reserve1,
        "JAMM: INSUFFICIENT_LIQUIDITY"
    );

    uint balance0;
    uint balance1;
    {
        require(to != token0 && to != token1, "JAMM: INVALID_TO");
        if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
        if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
        if (data.length > 0)
            IJAMMCallee(to).jammCall(
                msg.sender,
                amount0Out,
                amount1Out,
                data,
                _referrer
            );
        balance0 = JAMMERC20(token0).balanceOf(address(this));
        balance1 = JAMMERC20(token1).balanceOf(address(this));
    }

    uint amount0In = balance0 > _reserve0 - amount0Out
        ? balance0 - (_reserve0 - amount0Out)
        : 0;
    uint amount1In = balance1 > _reserve1 - amount1Out
        ? balance1 - (_reserve1 - amount1Out)
        : 0;
    require(
        amount0In > 0 || amount1In > 0,
        "JAMM: INSUFFICIENT_INPUT_AMOUNT"
    );
    _collectFee(amount0In, amount1In, _referrer);
    {
        balance0 = JAMMERC20(token0).balanceOf(address(this));
        balance1 = JAMMERC20(token1).balanceOf(address(this));
        uint balance0Adjusted = (balance0 *
            10000 -
            (amount0In * fee * 4) /
            5);
        uint balance1Adjusted = (balance1 *
            10000 -
            (amount1In * fee * 4) /
            5);
        require(
            balance0Adjusted * balance1Adjusted >=
                uint(_reserve0) * uint(_reserve1) * (10000 ** 2),
            "JAMM: K"
        );
    }

    _update(balance0, balance1, _reserve0, _reserve1);
    emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

Swap Process:

  1. Validate output amounts and liquidity sufficiency

  2. Optimistic transfer (transfer tokens first)

  3. Support flash loan callback

  4. Calculate actual input amounts

  5. Collect trading fees

  6. Verify constant product formula

  7. Update reserves

  8. Emit Swap event

Fee Collection

function _collectFee(
    uint amount0In,
    uint amount1In,
    address _referrer
) private {
    address referrer = IJAMMFactory(factory).referrer(tx.origin);
    address feeTo = IJAMMFactory(factory).feeTo();

    if (_referrer == address(0) && referrer == address(0)) {
        // No referrer
        _safeTransferFee(token0, feeTo, (amount0In * fee) / 50000);
        _safeTransferFee(token1, feeTo, (amount1In * fee) / 50000);
    } else {
        // Has referrer
        _safeTransferFee(token0, feeTo, (amount0In * fee) / 100000);
        _safeTransferFee(token1, feeTo, (amount1In * fee) / 100000);
        if (referrer != address(0)) {
            _safeTransferFee(token0, referrer, (amount0In * fee) / 100000);
            _safeTransferFee(token1, referrer, (amount1In * fee) / 100000);
        } else {
            IJAMMFactory(factory).setReferrer(_referrer);
            _safeTransferFee(token0, _referrer, (amount0In * fee) / 100000);
            _safeTransferFee(token1, _referrer, (amount1In * fee) / 100000);
        }
    }
}

Fee Distribution Logic:

  • No referrer: Protocol collects fee/50000 of fees

  • Has referrer: Protocol and referrer each collect fee/100000 of fees

Protocol Fee Minting

function _mintFee(
    uint112 _reserve0,
    uint112 _reserve1
) private returns (bool feeOn) {
    address mintTo = IJAMMFactory(factory).mintTo();
    feeOn = mintTo != address(0);
    uint _kLast = kLast;
    if (feeOn) {
        if (_kLast != 0) {
            uint rootK = Math.sqrt(uint(_reserve0) * uint(_reserve1));
            uint rootKLast = Math.sqrt(_kLast);
            if (rootK > rootKLast) {
                uint numerator = totalSupply * (rootK - rootKLast) * 8;
                uint denominator = rootK * 17 + rootKLast * 8;
                uint liquidity = numerator / denominator;
                if (liquidity > 0) _mint(mintTo, liquidity);
            }
        }
    } else if (_kLast != 0) {
        kLast = 0;
    }
}

Protocol Fee Calculation:

  • Based on k value growth to calculate protocol fees

  • Formula: liquidity = totalSupply * (√k - √kLast) * 8 / (√k * 17 + √kLast * 8)

Safe Transfer

Safe Transfer Function

function _safeTransfer(address token, address to, uint value) private {
    if (value == 0) {
        return;
    }
    (bool success, bytes memory data) = token.call(
        abi.encodeWithSelector(SELECTOR, to, value)
    );
    require(
        success && (data.length == 0 || abi.decode(data, (bool))),
        "JAMM: TRANSFER_FAILED"
    );
}

Fee Transfer Function

function _safeTransferFee(address token, address to, uint value) private {
    _safeTransfer(token, to, value);
    if (value > 0) {
        emit Fee(tx.origin, to, token, value);
    }
}

Features:

  • Handles non-standard ERC-20 tokens

  • Zero value transfers return directly

  • Fee transfers emit Fee events

Utility Functions

Skim Function

function skim(address to) external lock {
    address _token0 = token0;
    address _token1 = token1;
    _safeTransfer(
        _token0,
        to,
        JAMMERC20(_token0).balanceOf(address(this)) - reserve0
    );
    _safeTransfer(
        _token1,
        to,
        JAMMERC20(_token1).balanceOf(address(this)) - reserve1
    );
}

Purpose: Remove excess token balances beyond reserves.

Sync Function

function sync() external lock {
    _update(
        JAMMERC20(token0).balanceOf(address(this)),
        JAMMERC20(token1).balanceOf(address(this)),
        reserve0,
        reserve1
    );
}

Purpose: Force synchronization of reserves with actual balances.

Event System

Core Events

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(address indexed sender, uint amount0In, uint amount1In, uint amount0Out, uint amount1Out, address indexed to);
event Sync(uint112 reserve0, uint112 reserve1);
event Fee(address indexed sender, address indexed referrer, address token, uint amount);

Usage Examples

Query Trading Pair Information

const pair = new ethers.Contract(pairAddress, pairABI, provider);

// Get basic information
const token0 = await pair.token0();
const token1 = await pair.token1();
const fee = await pair.fee();

console.log("Token0:", token0);
console.log("Token1:", token1);
console.log("Fee rate:", fee);

// Get reserves
const reserves = await pair.getReserves();
console.log("Reserve0:", ethers.utils.formatEther(reserves.reserve0));
console.log("Reserve1:", ethers.utils.formatEther(reserves.reserve1));
console.log("Last updated:", new Date(reserves.blockTimestampLast * 1000));

Listen to Swap Events

pair.on("Swap", (sender, amount0In, amount1In, amount0Out, amount1Out, to) => {
    console.log("Swap event:");
    console.log("- Sender:", sender);
    console.log("- Input0:", ethers.utils.formatEther(amount0In));
    console.log("- Input1:", ethers.utils.formatEther(amount1In));
    console.log("- Output0:", ethers.utils.formatEther(amount0Out));
    console.log("- Output1:", ethers.utils.formatEther(amount1Out));
    console.log("- Recipient:", to);
});

Listen to Fee Events

pair.on("Fee", (sender, referrer, token, amount) => {
    console.log("Fee event:");
    console.log("- Trader:", sender);
    console.log("- Referrer:", referrer);
    console.log("- Token:", token);
    console.log("- Amount:", ethers.utils.formatEther(amount));
});

Last updated