流动性池

什么是流动性池

流动性池是JAMM DEX的核心组件,它是一个包含两种代币储备的智能合约。每个流动性池都是一个 JAMMPair 合约实例,负责管理特定代币对的交易和流动性。

池子的创建

通过Factory创建

所有流动性池都通过 JAMMFactory 合约创建:

// from JAMMFactory.sol
function createPair(
    address tokenA,
    address tokenB,
    uint24 fee
) external override returns (address pair) {
    require(tokenA != tokenB, "JAMMFactory: IDENTICAL_ADDRESSES");
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), "JAMMFactory: ZERO_ADDRESS");
    require(
        fee == FEE_0_5_PERCENT ||
        fee == FEE_1_PERCENT ||
        fee == FEE_2_PERCENT ||
        fee == FEE_3_PERCENT,
        "JAMMFactory: INVALID_FEE"
    );
    require(getPair[token0][token1][fee] == address(0), "JAMMFactory: PAIR_EXISTS");
    
    // deploy the pair using CREATE2
    bytes memory bytecode = type(JAMMPair).creationCode;
    bytes32 salt = keccak256(abi.encodePacked(token0, token1, fee));
    assembly {
        pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
    }
    
    // initialize the pair
    JAMMPair(pair).initialize(token0, token1, fee);
    getPair[token0][token1][fee] = pair;
    getPair[token1][token0][fee] = pair;
    allPairs.push(pair);
    emit PairCreated(token0, token1, fee, pair, allPairs.length);
}

确定性地址生成

JAMM DEX使用CREATE2来生成确定性的池子地址,这意味着给定相同的代币对和费率,池子地址总是相同的。这个计算在 JAMMLibrary.sol 中完成:

// from JAMMLibrary.sol
function pairFor(
    address factory,
    address tokenA,
    address tokenB,
    uint24 fee
) internal pure returns (address pair) {
    (address token0, address token1) = sortTokens(tokenA, tokenB);
    pair = address(uint160(uint256(keccak256(abi.encodePacked(
        hex"ff",
        factory,
        keccak256(abi.encodePacked(token0, token1, fee)),
        hex"2e599419feff8382bdfc47abf847537b06e96b0525db3802b2a9fb0bfe068ed8" // init code hash
    )))));
}

池子的状态

储备量管理

每个池子维护两种代币的储备量:

// from JAMMPair.sol
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;

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

储备量更新

储备量在每次交易或流动性变更后都会更新:

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

流动性代币 (LP代币)

LP代币的作用

当用户向池子提供流动性时,会收到LP代币作为凭证:

  • LP代币代表用户在池子中的份额。

  • 可以随时用LP代币赎回相应的代币对。

  • LP代币本身也是ERC-20代币,可以转让。

LP代币的铸造

// 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) {
        // first liquidity provider
        liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
        _mint(address(0), MINIMUM_LIQUIDITY);
    } else {
        // subsequent liquidity providers
        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);
}

LP代币的销毁

// from JAMMPair.sol
function burn(address to) external lock returns (uint amount0, uint amount1) {
    (uint112 _reserve0, uint112 _reserve1, ) = getReserves();
    address _token0 = token0;
    address _token1 = token1;
    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; // prorated distribution
    amount1 = (liquidity * balance1) / _totalSupply; // prorated distribution
    require(amount0 > 0 && amount1 > 0, "JAMM: INSUFFICIENT_LIQUIDITY_BURNED");
    _burn(address(this), liquidity);
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);
    balance0 = IERC20(_token0).balanceOf(address(this));
    balance1 = IERC20(_token1).balanceOf(address(this));

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

交易执行

Swap函数

池子的核心功能是执行代币交换:

// from JAMMPair.sol
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 = IERC20(token0).balanceOf(address(this));
        balance1 = IERC20(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);
    
    // K-value check
    {
        balance0 = IERC20(token0).balanceOf(address(this));
        balance1 = IERC20(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);
}

安全机制

重入保护

所有关键函数都使用 lock 修饰符保护,防止重入攻击:

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

最小流动性锁定

为防止池子被完全清空,首次添加流动性时会永久锁定最小数量的LP代币:

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

溢出保护

储备量使用 uint112 类型,并在更新时检查溢出,利用了 Solidity 0.8.x 的内置安全特性:

// from JAMMPair.sol
require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "JAMM: OVERFLOW");

实用函数

Skim函数

用于移除意外转入池子且未被计入储备量的多余代币:

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

Sync函数

用于强制同步储备量与合约的实际代币余额,在某些异常情况下可以用来恢复池子状态:

// from JAMMPair.sol
function sync() external lock {
    _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}

Last updated