Skip to main content

V2 Add Liquidity Example

A complete example of adding liquidity to a V2 constant-product pool.

Full Code

import { ethers } from 'ethers';

// === Configuration (Mainnet) ===
const RPC_URL = 'https://evmrpc.kasplex.org';
const V2_FACTORY = '0x4373b7Fcf5059A785843cD224129e01d243Aef71';
const V2_ROUTER = '0xC7ca845B8302346e1C7227f03bb9EFb35ecD51fe';

// === ABIs ===
const ERC20_ABI = [
  'function approve(address,uint256) returns (bool)',
  'function allowance(address,address) view returns (uint256)',
  'function balanceOf(address) view returns (uint256)',
  'function symbol() view returns (string)',
  'function decimals() view returns (uint8)',
];

const FACTORY_ABI = [
  'function getPair(address,address) view returns (address)',
];

const PAIR_ABI = [
  'function getReserves() view returns (uint112, uint112, uint32)',
  'function token0() view returns (address)',
  'function totalSupply() view returns (uint256)',
  'function balanceOf(address) view returns (uint256)',
];

const ROUTER_ABI = [
  'function addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256) returns (uint256,uint256,uint256)',
  'function addLiquidityETH(address,uint256,uint256,uint256,address,uint256) payable returns (uint256,uint256,uint256)',
];

/**
 * Add liquidity to a V2 pool.
 *
 * @param signer   - Connected ethers Signer
 * @param tokenA   - First token address
 * @param tokenB   - Second token address
 * @param amountA  - Desired amount of tokenA (raw units)
 * @param slippage - Slippage tolerance (e.g., 1 for 1%)
 */
async function addLiquidityV2(
  signer: ethers.Signer,
  tokenA: string,
  tokenB: string,
  amountA: bigint,
  slippage: number = 1
) {
  const provider = signer.provider!;
  const userAddress = await signer.getAddress();
  const deadline = Math.floor(Date.now() / 1000) + 1200;
  const slippageFactor = BigInt(Math.floor((100 - slippage) * 100)) ; // e.g., 9900 for 1%

  // --- Check if pool exists and calculate amountB ---
  const factory = new ethers.Contract(V2_FACTORY, FACTORY_ABI, provider);
  const pairAddress = await factory.getPair(tokenA, tokenB);

  let amountB: bigint;

  if (pairAddress === ethers.ZeroAddress) {
    // New pool — user sets the initial price
    console.log('Pool does not exist. You are setting the initial price.');
    console.log('Enter amountB manually (this example uses 1:1 ratio):');
    amountB = amountA; // Change this to your desired ratio
  } else {
    // Existing pool — calculate proportional amountB
    const pair = new ethers.Contract(pairAddress, PAIR_ABI, provider);
    const [reserve0, reserve1] = await pair.getReserves();
    const token0 = await pair.token0();

    const isAToken0 = tokenA.toLowerCase() === token0.toLowerCase();
    const reserveA = isAToken0 ? reserve0 : reserve1;
    const reserveB = isAToken0 ? reserve1 : reserve0;

    amountB = (amountA * reserveB) / reserveA;
    console.log(`Pool exists. Calculated amountB: ${amountB}`);
  }

  // --- Approve both tokens to V2 Router ---
  console.log('Approving tokens...');
  const tokenAContract = new ethers.Contract(tokenA, ERC20_ABI, signer);
  const tokenBContract = new ethers.Contract(tokenB, ERC20_ABI, signer);

  const allowanceA = await tokenAContract.allowance(userAddress, V2_ROUTER);
  const allowanceB = await tokenBContract.allowance(userAddress, V2_ROUTER);

  if (allowanceA < amountA) {
    const tx = await tokenAContract.approve(V2_ROUTER, ethers.MaxUint256);
    await tx.wait();
  }
  if (allowanceB < amountB) {
    const tx = await tokenBContract.approve(V2_ROUTER, ethers.MaxUint256);
    await tx.wait();
  }

  // --- Add liquidity ---
  console.log('Adding liquidity...');
  const router = new ethers.Contract(V2_ROUTER, ROUTER_ABI, signer);

  const tx = await router.addLiquidity(
    tokenA,
    tokenB,
    amountA,
    amountB,
    (amountA * slippageFactor) / 10000n,
    (amountB * slippageFactor) / 10000n,
    userAddress,
    deadline
  );

  const receipt = await tx.wait();
  console.log(`Liquidity added. Tx: ${receipt!.hash}`);

  // --- Check LP balance ---
  const newPairAddress = pairAddress === ethers.ZeroAddress
    ? await factory.getPair(tokenA, tokenB)
    : pairAddress;

  const pair = new ethers.Contract(newPairAddress, PAIR_ABI, provider);
  const lpBalance = await pair.balanceOf(userAddress);
  console.log(`LP token balance: ${lpBalance}`);
}

// === Usage ===
async function main() {
  const provider = new ethers.JsonRpcProvider(RPC_URL);
  const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);

  const TOKEN_A = '0xB190a6A7fC2873f1Abf145279eD664348d5Ef630';
  const TOKEN_B = '0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220';

  await addLiquidityV2(
    signer,
    TOKEN_A,
    TOKEN_B,
    ethers.parseEther('10'), // 10 tokens
    1                         // 1% slippage
  );
}

main().catch(console.error);

With Native KAS

To add liquidity with native KAS, use addLiquidityETH:
const tx = await router.addLiquidityETH(
  tokenAddress,           // The ERC-20 token (not WKAS)
  amountTokenDesired,
  amountTokenMin,
  amountKASMin,
  userAddress,
  deadline,
  { value: amountKASDesired }  // Send KAS as value
);

Removing Liquidity

const REMOVE_ABI = [
  'function removeLiquidity(address,address,uint256,uint256,uint256,address,uint256) returns (uint256,uint256)',
];

// Approve LP token to router
const lpToken = new ethers.Contract(pairAddress, ERC20_ABI, signer);
await (await lpToken.approve(V2_ROUTER, lpAmount)).wait();

// Remove
const router = new ethers.Contract(V2_ROUTER, REMOVE_ABI, signer);
const tx = await router.removeLiquidity(
  tokenA,
  tokenB,
  lpAmount,
  0, // amountAMin
  0, // amountBMin
  userAddress,
  deadline
);