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:

  1. Trading Fees: Fees from each trade are distributed proportionally to LPs

  2. Referral Rewards: Additional rewards if there's a referral system

  3. 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