LP Token Mechanism

Overview

LP (Liquidity Provider) tokens are credentials that represent a provider's share in a specific trading pair within JAMM DEX. Each trading pair has a corresponding LP token, implemented based on the JAMMERC20 contract. It not only complies with the ERC-20 standard but also supports the EIP-2612 permit function, providing a better user experience.

JAMMERC20 Contract Implementation

Basic Information

// from JAMMERC20.sol
contract JAMMERC20 is IJAMMERC20 {
    string public constant name = "JAMM LPs";
    string public constant symbol = "JAMM-LP";
    uint8 public constant decimals = 18;
    uint public totalSupply;
    mapping(address => uint) public balanceOf;
    mapping(address => mapping(address => uint)) public allowance;
    // ...
}

LP Token Characteristics:

  • Uniform Name: All LP tokens for all pairs are named "JAMM LPs".

  • Uniform Symbol: "JAMM-LP".

  • Precision: 18 decimals.

  • Uniqueness: Each trading pair is a separate instance of the LP token contract.

EIP-2612 Permit Support

JAMM DEX's LP tokens support the EIP-2612 standard, allowing users to grant approvals via a signature, eliminating the need for a separate approve transaction.

// from JAMMERC20.sol
bytes32 public DOMAIN_SEPARATOR;
bytes32 public constant PERMIT_TYPEHASH =
    0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;

constructor() {
    uint chainId;
    assembly {
        chainId := chainid()
    }
    DOMAIN_SEPARATOR = keccak256(abi.encode(
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
        keccak256(bytes(name)),
        keccak256(bytes("1")),
        chainId,
        address(this)
    ));
}

function permit(
    address owner,
    address spender,
    uint value,
    uint deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    require(deadline >= block.timestamp, "JAMM: EXPIRED");
    bytes32 digest = keccak256(abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR,
        keccak256(abi.encode(
            PERMIT_TYPEHASH,
            owner,
            spender,
            value,
            nonces[owner]++,
            deadline
        ))
    ));
    address recoveredAddress = ecrecover(digest, v, r, s);
    require(recoveredAddress != address(0) && recoveredAddress == owner, "JAMM: INVALID_SIGNATURE");
    _approve(owner, spender, value);
}

LP Token Lifecycle

Minting

When a user adds liquidity to a pool, corresponding LP tokens are minted. This logic is implemented in JAMMPair.sol:

// from JAMMPair.sol
function mint(address to) external lock returns (uint liquidity) {
    (uint112 _reserve0, uint112 _reserve1, ) = getReserves();
    uint balance0 = IERC20(token0).balanceOf(address(this));
    uint balance1 = IERC20(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);
    } else {
        liquidity = Math.min((amount0 * _totalSupply) / _reserve0, (amount1 * _totalSupply) / _reserve1);
    }
    require(liquidity > 0, "JAMM: INSUFFICIENT_LIQUIDITY_MINTED");
    _mint(to, liquidity);
    // ...
}

Burning

When a user removes liquidity, the corresponding LP tokens are burned:

// from JAMMPair.sol
function burn(address to) external lock returns (uint amount0, uint amount1) {
    (uint112 _reserve0, uint112 _reserve1, ) = getReserves();
    uint balance0 = IERC20(token0).balanceOf(address(this));
    uint balance1 = IERC20(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);
    // ...
}

Minimum Liquidity Mechanism

Permanent Lock

// from JAMMPair.sol
uint public constant MINIMUM_LIQUIDITY = 10**3;

// inside mint()
if (_totalSupply == 0) {
    liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
    _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first tokens
}

Purpose:

  • To prevent the liquidity pool from being completely drained, which would break price calculations.

  • To prevent certain types of malicious manipulation attacks.

permit Function Explained

Advantage of Signature Approval

Traditional ERC-20 approvals require two transactions: approve and transferFrom. The permit function allows users to complete the approval and the action in a single on-chain transaction, preceded by an offline signature. This saves gas and improves user experience.

Application in the Router

The JAMMRouter contract provides a function for removing liquidity using permit:

// from JAMMRouter.sol
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 returns (uint amountA, uint amountB) {
    // Use permit to approve LP token spending
    IJAMMPair(JAMMLibrary.pairFor(factory, tokenA, tokenB, fee)).permit(
        msg.sender, address(this), approveMax ? type(uint).max : liquidity, deadline, v, r, s
    );
    // Then remove liquidity
    (amountA, amountB) = removeLiquidity(
        tokenA, tokenB, fee, liquidity, amountAMin, amountBMin, to, deadline
    );
}

Usage Example

Querying LP Token Information

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

// Get LP token balance
const lpBalance = await pair.balanceOf(userAddress);
console.log("LP Balance:", ethers.utils.formatEther(lpBalance));

// Get total supply of LP tokens
const totalSupply = await pair.totalSupply();
console.log("Total Supply:", ethers.utils.formatEther(totalSupply));

// Calculate user's share of the pool
const userShare = lpBalance.mul(10000).div(totalSupply);
console.log("User Share:", userShare.toNumber() / 100, "%");

Removing Liquidity with permit

// Generate permit signature
const nonce = await pair.nonces(signer.address);
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes

const domain = {
    name: 'JAMM LPs',
    version: '1',
    chainId: await signer.getChainId(),
    verifyingContract: pairAddress
};

const types = {
    Permit: [
        { name: 'owner', type: 'address' },
        { name: 'spender', type: 'address' },
        { name: 'value', type: 'uint256' },
        { name: 'nonce', type: 'uint256' },
        { name: 'deadline', type: 'uint256' }
    ]
};

const value = {
    owner: signer.address,
    spender: routerAddress,
    value: liquidity,
    nonce: nonce,
    deadline: deadline
};

const signature = await signer._signTypedData(domain, types, value);
const { v, r, s } = ethers.utils.splitSignature(signature);

// Remove liquidity using the signature
const tx = await router.removeLiquidityWithPermit(
    tokenA, tokenB, fee, liquidity,
    amountAMin, amountBMin, to, deadline,
    false, v, r, s
);

Summary

The LP token mechanism in JAMM DEX has the following features:

  1. Standard Compliance: Fully compliant with the ERC-20 standard.

  2. Permit Support: Offers a better user experience and gas efficiency through EIP-2612.

  3. Secure Design: Multiple security mechanisms, including re-entrancy guards, overflow protection, and signature verification.

  4. Flexible Use: Transferable and tradable liquidity credentials.

  5. Transparent Value: The value corresponds directly to the share of assets in the pool.

LP tokens are a vital part of the JAMM DEX ecosystem, providing liquidity providers with a flexible way to manage their assets.