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
permit
Function ExplainedAdvantage 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
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:
Standard Compliance: Fully compliant with the ERC-20 standard.
Permit Support: Offers a better user experience and gas efficiency through EIP-2612.
Secure Design: Multiple security mechanisms, including re-entrancy guards, overflow protection, and signature verification.
Flexible Use: Transferable and tradable liquidity credentials.
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.