LP 代币机制

概述

LP代币(Liquidity Provider Token)是JAMM DEX中代表流动性提供者在特定交易对中份额的凭证。每个交易对都有对应的LP代币,基于 JAMMERC20 合约实现,不仅符合ERC-20标准,还支持EIP-2612 Permit功能,为用户提供更好的交易体验。

JAMMERC20合约实现

基本信息

// 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代币特性

  • 统一名称: 所有交易对的LP代币都叫"JAMM LPs"。

  • 统一符号: "JAMM-LP"。

  • 精度: 18位小数。

  • 唯一性: 每个交易对都是一个独立的LP代币合约实例。

EIP-2612 Permit支持

JAMM DEX的LP代币支持EIP-2612标准,允许用户通过签名进行授权,无需发送额外的 approve 交易。

// 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代币的生命周期

铸造(Mint)

当用户向流动性池添加流动性时,会铸造相应的LP代币。该逻辑在 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);
    // ...
}

销毁(Burn)

当用户移除流动性时,会销毁相应的LP代币:

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

最小流动性机制

永久锁定

// 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
}

目的

  • 防止流动性池被完全清空,这会破坏价格计算。

  • 防止某些类型的恶意操纵攻击。

Permit功能详解

签名授权的优势

传统的ERC-20授权需要两步交易:approvetransferFrom。Permit功能允许用户通过一次离线签名和一次链上交易完成授权和操作,从而节省Gas并改善用户体验。

在Router中的应用

JAMMRouter 合约提供了使用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
    );
}

使用示例

查询LP代币信息

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, "%");

使用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
);

Last updated