Add Liquidity
Overview
Adding liquidity is the process of providing token reserves for JAMM DEX trading pairs. Liquidity providers (LPs) deposit two types of tokens to receive LP tokens, which represent their share in the pool and allow them to earn trading fee rewards.
Basic Liquidity Concepts
What is Liquidity
Liquidity refers to the amount of tokens available for trading in a trading pair. Higher liquidity means:
Smaller price impact
Lower slippage
Better trading experience
LP Tokens
When you add liquidity, you receive LP tokens as proof:
LP token amount represents your share in the pool
You can redeem corresponding token pairs with LP tokens at any time
LP tokens themselves are ERC-20 tokens and can be transferred
Liquidity Mining Rewards
Sources of income for liquidity providers:
Trading Fees: Fees from each trade are distributed proportionally to LPs
Referral Rewards: Additional rewards if there's a referral system
Price Appreciation: If token prices rise, LP value also increases
Ways to Add Liquidity
1. Token Pair Liquidity
The most common way to add liquidity, requiring two types of tokens:
async function addLiquidity(
tokenA,
tokenB,
fee,
amountADesired,
amountBDesired,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
// 1. Calculate minimum amounts (slippage protection)
const slippageBps = Math.floor(slippagePercent * 100);
const amountAMin = amountADesired.mul(10000 - slippageBps).div(10000);
const amountBMin = amountBDesired.mul(10000 - slippageBps).div(10000);
// 2. Check and approve tokens
await ensureTokenApproval(tokenA, ROUTER_ADDRESS, amountADesired, signer);
await ensureTokenApproval(tokenB, ROUTER_ADDRESS, amountBDesired, signer);
// 3. Set deadline
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes
// 4. Add liquidity
const tx = await router.addLiquidity(
tokenA,
tokenB,
fee,
amountADesired,
amountBDesired,
amountAMin,
amountBMin,
userAddress,
deadline
);
console.log("Add liquidity transaction submitted:", tx.hash);
const receipt = await tx.wait();
// 5. Parse result
const result = await parseLiquidityResult(receipt);
return result;
}
// Helper function: Ensure token approval
async function ensureTokenApproval(tokenAddress, spenderAddress, amount, signer) {
const token = new ethers.Contract(tokenAddress, erc20ABI, signer);
const userAddress = await signer.getAddress();
const currentAllowance = await token.allowance(userAddress, spenderAddress);
if (currentAllowance.lt(amount)) {
console.log(`Approving ${tokenAddress}...`);
const approveTx = await token.approve(spenderAddress, amount);
await approveTx.wait();
console.log("Approval completed");
}
}
2. JU + Token Liquidity
Adding liquidity for JU and ERC-20 tokens:
async function addLiquidityETH(
token,
fee,
amountTokenDesired,
juAmount,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
// 1. Calculate minimum amounts
const slippageBps = Math.floor(slippagePercent * 100);
const amountTokenMin = amountTokenDesired.mul(10000 - slippageBps).div(10000);
const amountETHMin = juAmount.mul(10000 - slippageBps).div(10000);
// 2. Approve token
await ensureTokenApproval(token, ROUTER_ADDRESS, amountTokenDesired, signer);
// 3. Check JU balance
const juBalance = await signer.getBalance();
if (juBalance.lt(juAmount)) {
throw new Error(`Insufficient JU balance. Required: ${ethers.utils.formatEther(juAmount)}, Have: ${ethers.utils.formatEther(juBalance)}`);
}
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
// 4. Add liquidity
const tx = await router.addLiquidityETH(
token,
fee,
amountTokenDesired,
amountTokenMin,
amountETHMin,
userAddress,
deadline,
{ value: juAmount } // Send JU
);
console.log("Add JU liquidity transaction submitted:", tx.hash);
return tx;
}
// Usage example
await addLiquidityETH(
TOKEN_ADDRESS,
100, // 1% fee rate
ethers.utils.parseEther("1000"), // 1000 tokens
ethers.utils.parseEther("1"), // 1 JU
1, // 1% slippage
userAddress,
signer
);
First-time Liquidity Addition
Creating New Trading Pairs
If a trading pair doesn't exist, it will be automatically created when adding liquidity:
async function createPairAndAddLiquidity(
tokenA,
tokenB,
fee,
amountA,
amountB,
userAddress,
signer
) {
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, signer);
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
// 1. Check if pair exists
let pairAddress = await factory.getPair(tokenA, tokenB, fee);
if (pairAddress === ethers.constants.AddressZero) {
console.log("Pair does not exist, will be created automatically");
// Router will automatically create pair when adding liquidity
} else {
console.log("Pair already exists:", pairAddress);
}
// 2. Add liquidity (will automatically create pair if it doesn't exist)
const result = await addLiquidity(
tokenA,
tokenB,
fee,
amountA,
amountB,
1, // 1% slippage
userAddress,
signer
);
// 3. Get newly created pair address
pairAddress = await factory.getPair(tokenA, tokenB, fee);
console.log("Pair address:", pairAddress);
return { ...result, pairAddress };
}
Initial Price Setting
When adding liquidity for the first time, the token ratio you set determines the initial price:
function calculateInitialPrice(amountA, amountB, decimalsA = 18, decimalsB = 18) {
// Normalize to 18 decimals
const normalizedA = amountA.mul(ethers.BigNumber.from(10).pow(18 - decimalsA));
const normalizedB = amountB.mul(ethers.BigNumber.from(10).pow(18 - decimalsB));
// Calculate price: 1 TokenA = ? TokenB
const priceAtoB = normalizedB.mul(ethers.utils.parseEther("1")).div(normalizedA);
const priceBtoA = normalizedA.mul(ethers.utils.parseEther("1")).div(normalizedB);
return {
priceAtoB: ethers.utils.formatEther(priceAtoB),
priceBtoA: ethers.utils.formatEther(priceBtoA)
};
}
// Usage example
const prices = calculateInitialPrice(
ethers.utils.parseEther("1000"), // 1000 TokenA
ethers.utils.parseUnits("2000", 6) // 2000 USDC (6 decimals)
);
console.log("Initial price - 1 TokenA =", prices.priceAtoB, "USDC");
console.log("Initial price - 1 USDC =", prices.priceBtoA, "TokenA");
Subsequent Liquidity Addition
Adding According to Existing Ratio
For trading pairs with existing liquidity, you need to add according to the existing ratio:
async function addLiquidityToExistingPair(
tokenA,
tokenB,
fee,
amountADesired,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
// 1. Get current reserves
const pairAddress = await factory.getPair(tokenA, tokenB, fee);
if (pairAddress === ethers.constants.AddressZero) {
throw new Error("Pair does not exist");
}
const pair = new ethers.Contract(pairAddress, pairABI, provider);
const reserves = await pair.getReserves();
// 2. Determine token order
const token0 = await pair.token0();
const [reserveA, reserveB] = tokenA.toLowerCase() === token0.toLowerCase()
? [reserves.reserve0, reserves.reserve1]
: [reserves.reserve1, reserves.reserve0];
// 3. Calculate optimal TokenB amount
const amountBOptimal = await router.quote(amountADesired, reserveA, reserveB);
console.log("Recommended amounts to add:");
console.log("- TokenA:", ethers.utils.formatEther(amountADesired));
console.log("- TokenB:", ethers.utils.formatEther(amountBOptimal));
// 4. Add liquidity
const result = await addLiquidity(
tokenA,
tokenB,
fee,
amountADesired,
amountBOptimal,
1, // 1% slippage
userAddress,
signer
);
return result;
}
Single-sided Addition Optimization
When you only have one type of token, you can first swap part of it to get the other token:
async function addLiquidityWithSingleToken(
tokenIn,
tokenOut,
fee,
amountIn,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
// 1. Calculate amount to swap (usually half)
const swapAmount = amountIn.div(2);
const remainingAmount = amountIn.sub(swapAmount);
// 2. First perform swap
console.log("Swapping partial tokens...");
const swapTx = await router.swapExactTokensForTokens(
swapAmount,
0, // Accept any amount of output
[tokenIn, tokenOut],
[fee],
userAddress,
ethers.constants.AddressZero, // No referrer
Math.floor(Date.now() / 1000) + 60 * 20
);
await swapTx.wait();
// 3. Query balance after swap
const tokenOutContract = new ethers.Contract(tokenOut, erc20ABI, provider);
const tokenOutBalance = await tokenOutContract.balanceOf(userAddress);
// 4. Add liquidity
console.log("Adding liquidity...");
const result = await addLiquidity(
tokenIn,
tokenOut,
fee,
remainingAmount,
tokenOutBalance,
2, // 2% slippage
userAddress,
signer
);
return result;
}
Advanced Features
Adding Liquidity with Permit
Avoid pre-approval and add liquidity directly through signatures:
async function addLiquidityWithPermit(
tokenA,
tokenB,
fee,
amountADesired,
amountBDesired,
userAddress,
signer
) {
// Note: This feature requires LP tokens to support Permit, used for removing liquidity
// Adding liquidity itself still requires pre-approval of input tokens
// For adding liquidity, still need standard approve process
await ensureTokenApproval(tokenA, ROUTER_ADDRESS, amountADesired, signer);
await ensureTokenApproval(tokenB, ROUTER_ADDRESS, amountBDesired, signer);
// Then add liquidity normally
return await addLiquidity(
tokenA,
tokenB,
fee,
amountADesired,
amountBDesired,
1,
userAddress,
signer
);
}
Batch Liquidity Addition
Add liquidity to multiple trading pairs simultaneously:
async function batchAddLiquidity(liquidityParams, signer) {
const results = [];
for (const params of liquidityParams) {
try {
console.log(`Adding liquidity: ${params.tokenA} / ${params.tokenB}`);
const result = await addLiquidity(
params.tokenA,
params.tokenB,
params.fee,
params.amountA,
params.amountB,
params.slippage || 1,
params.userAddress,
signer
);
results.push({
success: true,
pair: `${params.tokenA}/${params.tokenB}`,
...result
});
// Wait to avoid nonce conflicts
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
results.push({
success: false,
pair: `${params.tokenA}/${params.tokenB}`,
error: error.message
});
}
}
return results;
}
// Usage example
const liquidityParams = [
{
tokenA: TOKEN_A_ADDRESS,
tokenB: TOKEN_B_ADDRESS,
fee: 100,
amountA: ethers.utils.parseEther("100"),
amountB: ethers.utils.parseEther("100"),
userAddress: userAddress
},
{
tokenA: TOKEN_C_ADDRESS,
tokenB: TOKEN_D_ADDRESS,
fee: 50,
amountA: ethers.utils.parseEther("50"),
amountB: ethers.utils.parseEther("50"),
userAddress: userAddress
}
];
const results = await batchAddLiquidity(liquidityParams, signer);
Liquidity Calculations
LP Token Amount Calculation
function calculateLPTokens(amountA, amountB, reserveA, reserveB, totalSupply) {
if (totalSupply.eq(0)) {
// First liquidity addition: geometric mean - minimum liquidity
const liquidity = sqrt(amountA.mul(amountB)).sub(ethers.BigNumber.from(1000));
return liquidity;
} else {
// Subsequent additions: proportional calculation
const liquidityA = amountA.mul(totalSupply).div(reserveA);
const liquidityB = amountB.mul(totalSupply).div(reserveB);
return liquidityA.lt(liquidityB) ? liquidityA : liquidityB;
}
}
// Square root calculation (simplified version)
function sqrt(value) {
if (value.eq(0)) return ethers.BigNumber.from(0);
let z = value.add(1).div(2);
let y = value;
while (z.lt(y)) {
y = z;
z = value.div(z).add(z).div(2);
}
return y;
}
Value Calculation
async function calculateLiquidityValue(pairAddress, lpAmount, userAddress) {
const pair = new ethers.Contract(pairAddress, pairABI, provider);
// Get basic information
const [token0, token1, reserves, totalSupply] = await Promise.all([
pair.token0(),
pair.token1(),
pair.getReserves(),
pair.totalSupply()
]);
// Calculate user share
const share = lpAmount.mul(ethers.utils.parseEther("1")).div(totalSupply);
// Calculate redeemable token amounts
const amount0 = reserves.reserve0.mul(lpAmount).div(totalSupply);
const amount1 = reserves.reserve1.mul(lpAmount).div(totalSupply);
return {
token0: token0,
token1: token1,
amount0: amount0,
amount1: amount1,
share: ethers.utils.formatEther(share), // Percentage
totalSupply: totalSupply
};
}
Monitoring and Analysis
Liquidity Event Monitoring
async function monitorLiquidityEvents(userAddress) {
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
// Listen for new pair creation
factory.on("PairCreated", (token0, token1, fee, pair, length) => {
console.log("New pair created:", {
token0: token0.slice(0, 6) + '...',
token1: token1.slice(0, 6) + '...',
fee: fee,
pair: pair.slice(0, 6) + '...',
totalPairs: length.toString()
});
});
// Listen for liquidity additions (need to monitor Mint events from all pairs)
// Simplified here to monitor specific pair
const pairAddress = await factory.getPair(TOKEN_A_ADDRESS, TOKEN_B_ADDRESS, 100);
if (pairAddress !== ethers.constants.AddressZero) {
const pair = new ethers.Contract(pairAddress, pairABI, provider);
pair.on("Mint", (sender, amount0, amount1) => {
console.log("Liquidity added:", {
sender: sender.slice(0, 6) + '...',
amount0: ethers.utils.formatEther(amount0),
amount1: ethers.utils.formatEther(amount1)
});
});
}
}
Yield Analysis
async function analyzeLiquidityReturns(pairAddress, userAddress, fromBlock) {
const pair = new ethers.Contract(pairAddress, pairABI, provider);
// Get user's liquidity events
const mintFilter = pair.filters.Mint(null, null, null);
const burnFilter = pair.filters.Burn(null, null, null, userAddress);
const [mintEvents, burnEvents] = await Promise.all([
pair.queryFilter(mintFilter, fromBlock),
pair.queryFilter(burnFilter, fromBlock)
]);
// Analyze added liquidity
let totalAdded0 = ethers.BigNumber.from(0);
let totalAdded1 = ethers.BigNumber.from(0);
for (const event of mintEvents) {
// Need to further filter user-related events
totalAdded0 = totalAdded0.add(event.args.amount0);
totalAdded1 = totalAdded1.add(event.args.amount1);
}
// Analyze removed liquidity
let totalRemoved0 = ethers.BigNumber.from(0);
let totalRemoved1 = ethers.BigNumber.from(0);
for (const event of burnEvents) {
totalRemoved0 = totalRemoved0.add(event.args.amount0);
totalRemoved1 = totalRemoved1.add(event.args.amount1);
}
// Calculate current liquidity value held
const currentLP = await pair.balanceOf(userAddress);
const currentValue = await calculateLiquidityValue(pairAddress, currentLP, userAddress);
return {
totalAdded: { amount0: totalAdded0, amount1: totalAdded1 },
totalRemoved: { amount0: totalRemoved0, amount1: totalRemoved1 },
currentHolding: currentValue,
events: { mint: mintEvents.length, burn: burnEvents.length }
};
}
Best Practices
1. Risk Management
const liquidityRiskChecks = {
// Check impermanent loss risk
checkImpermanentLoss: (tokenA, tokenB) => {
// Stablecoin pairs: low risk
const stablecoins = ['USDC', 'USDT', 'DAI'];
const isStablePair = stablecoins.includes(tokenA) && stablecoins.includes(tokenB);
if (isStablePair) {
return { risk: 'LOW', message: 'Stablecoin pair, low impermanent loss risk' };
}
// Correlated assets: medium risk
const correlatedPairs = [['ETH', 'WETH'], ['BTC', 'WBTC']];
const isCorrelated = correlatedPairs.some(pair =>
(pair.includes(tokenA) && pair.includes(tokenB))
);
if (isCorrelated) {
return { risk: 'MEDIUM', message: 'Correlated assets, medium impermanent loss risk' };
}
// Other cases: high risk
return { risk: 'HIGH', message: 'Uncorrelated assets, high impermanent loss risk' };
},
// Check liquidity depth
checkLiquidityDepth: async (pairAddress) => {
const pair = new ethers.Contract(pairAddress, pairABI, provider);
const reserves = await pair.getReserves();
const totalLiquidity = reserves.reserve0.add(reserves.reserve1);
if (totalLiquidity.lt(ethers.utils.parseEther("1000"))) {
return { risk: 'HIGH', message: 'Low liquidity, may face high slippage' };
} else if (totalLiquidity.lt(ethers.utils.parseEther("10000"))) {
return { risk: 'MEDIUM', message: 'Medium liquidity' };
} else {
return { risk: 'LOW', message: 'Sufficient liquidity' };
}
}
};
2. Optimal Addition Strategy
function calculateOptimalLiquidityAmount(
tokenABalance,
tokenBBalance,
reserveA,
reserveB,
targetRatio = 0.5 // Target to use 50% of balance
) {
// Calculate current price ratio
const priceRatio = reserveB.mul(ethers.utils.parseEther("1")).div(reserveA);
// Calculate maximum addable amounts
const maxAmountA = tokenABalance.mul(targetRatio * 10000).div(10000);
const maxAmountB = tokenBBalance.mul(targetRatio * 10000).div(10000);
// Calculate optimal amounts based on price ratio
const requiredB = maxAmountA.mul(priceRatio).div(ethers.utils.parseEther("1"));
const requiredA = maxAmountB.mul(ethers.utils.parseEther("1")).div(priceRatio);
if (requiredB.lte(maxAmountB)) {
return { amountA: maxAmountA, amountB: requiredB };
} else {
return { amountA: requiredA, amountB: maxAmountB };
}
}
3. Gas Optimization
async function optimizeGasForLiquidity(liquidityParams, gasPrice) {
// Batch approvals to save gas
const approvals = [];
for (const params of liquidityParams) {
approvals.push(
ensureTokenApproval(params.tokenA, ROUTER_ADDRESS, params.amountA, signer),
ensureTokenApproval(params.tokenB, ROUTER_ADDRESS, params.amountB, signer)
);
}
// Execute approvals in parallel
await Promise.all(approvals);
// Calculate optimal gas price
const optimalGasPrice = await calculateOptimalGasPrice(gasPrice);
// Sort liquidity additions by gas efficiency
const sortedParams = liquidityParams.sort((a, b) => {
// Prioritize large liquidity amounts (higher gas efficiency)
const valueA = a.amountA.add(a.amountB);
const valueB = b.amountA.add(b.amountB);
return valueB.gt(valueA) ? 1 : -1;
});
return { sortedParams, optimalGasPrice };
}
Last updated