Liquidity Pools
What is a Liquidity Pool?
A liquidity pool is a core component of JAMM DEX, consisting of a smart contract that holds reserves of two tokens. Each liquidity pool is an instance of the JAMMPair
contract, responsible for managing the trading and liquidity of a specific token pair.
Pool Creation
Creation via Factory
All liquidity pools are created through the JAMMFactory
contract:
// 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);
}
Deterministic Address Generation
JAMM DEX uses CREATE2 to generate deterministic pool addresses. This means that for the same token pair and fee, the pool address will always be the same. This calculation is performed in 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
)))));
}
Pool State
Reserve Management
Each pool maintains reserves of two tokens:
// 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;
}
Reserve Updates
Reserves are updated after every trade or liquidity change:
// 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);
}
Liquidity Tokens (LP Tokens)
The Role of LP Tokens
When a user provides liquidity to a pool, they receive LP tokens as a receipt:
LP tokens represent the user's share in the pool.
They can be redeemed at any time for the corresponding token pair.
LP tokens are themselves ERC-20 tokens and can be transferred.
Minting LP Tokens
// 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);
}
Burning LP Tokens
// 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);
}
Trade Execution
The swap
Function
swap
FunctionThe core function of the pool is to execute token swaps:
// 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);
}
Security Mechanisms
Re-entrancy Guard
All critical functions are protected with the lock
modifier to prevent re-entrancy attacks:
// from JAMMPair.sol
uint private unlocked = 1;
modifier lock() {
require(unlocked == 1, "JAMM: LOCKED");
unlocked = 0;
_;
unlocked = 1;
}
Minimum Liquidity Lock
To prevent the pool from being completely drained, a minimum amount of LP tokens is permanently locked on the first liquidity provision:
// from JAMMPair.sol
uint public constant MINIMUM_LIQUIDITY = 10**3;
Overflow Protection
Reserves use the uint112
type, and updates are checked for overflow, leveraging the built-in security features of Solidity 0.8.x:
// from JAMMPair.sol
require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "JAMM: OVERFLOW");
Utility Functions
skim
Function
skim
FunctionUsed to remove any excess tokens that were accidentally sent to the pool and not accounted for in the reserves:
// 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
Function
sync
FunctionUsed to forcibly synchronize the reserves with the contract's actual token balances. This can be used to recover the pool state in certain edge cases:
// from JAMMPair.sol
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
Summary
Liquidity pools are the core components of JAMM DEX, and they:
Are created and managed centrally by the Factory contract.
Use deterministic address generation for easy calculation and lookup.
Implement full AMM functionality, including liquidity management and token swaps.
Have built-in security mechanisms and fee distribution logic.
Provide price oracle functionality.
Understanding how liquidity pools work is essential for effectively using JAMM DEX.