Implementing Liquidity Pool Interface on Website
LP interface allows users to add and withdraw liquidity from AMM pool. More complex than staking: need to understand token ratio in pool, calculate proportions when adding, show pool share and impermanent loss. Plus — approve for two tokens, not one.
AMM Models
Two main options found in projects:
Uniswap v2 / SushiSwap (constant product x·y=k): simple proportion, ERC-20 LP tokens, fixed 0.3% fee.
Uniswap v3 (concentrated liquidity): user chooses price range, position is NFT, more complex calculation.
We cover Uniswap v2 as foundation — most custom AMMs build on this model.
Reading pool state
// lib/pool.ts
import { createPublicClient, http, parseAbi } from 'viem';
const PAIR_ABI = parseAbi([
'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address) view returns (uint256)',
'function token0() view returns (address)',
'function token1() view returns (address)',
]);
export interface PoolState {
reserve0: bigint;
reserve1: bigint;
totalSupply: bigint;
userLPBalance: bigint;
token0: `0x${string}`;
token1: `0x${string}`;
// Computed
userShare: number; // user share in pool, 0–1
userToken0: bigint; // how much token0 can withdraw
userToken1: bigint;
}
export async function getPoolState(
pairAddress: `0x${string}`,
userAddress?: `0x${string}`,
): Promise<PoolState> {
const client = createPublicClient({ transport: http() });
const [reserves, supply, t0, t1, balance] = await client.multicall({
contracts: [
{ address: pairAddress, abi: PAIR_ABI, functionName: 'getReserves' },
{ address: pairAddress, abi: PAIR_ABI, functionName: 'totalSupply' },
{ address: pairAddress, abi: PAIR_ABI, functionName: 'token0' },
{ address: pairAddress, abi: PAIR_ABI, functionName: 'token1' },
...(userAddress ? [{ address: pairAddress, abi: PAIR_ABI, functionName: 'balanceOf', args: [userAddress] }] : []),
],
});
const [r0, r1] = reserves.result as [bigint, bigint];
const totalSupply = supply.result as bigint;
const userLPBalance = balance?.result as bigint || 0n;
const userShare = totalSupply > 0n ? Number(userLPBalance * 10000n / totalSupply) / 10000 : 0;
const userToken0 = totalSupply > 0n ? r0 * userLPBalance / totalSupply : 0n;
const userToken1 = totalSupply > 0n ? r1 * userLPBalance / totalSupply : 0n;
return { reserve0: r0, reserve1: r1, totalSupply, userLPBalance, token0: t0.result as `0x${string}`, token1: t1.result as `0x${string}`, userShare, userToken0, userToken1 };
}
Calculating proportions when adding liquidity
When adding liquidity to non-empty pool, second token is calculated automatically by current pool price:
// User enters token0 amount → calculate token1
export function quoteToken1(
amount0: bigint,
reserve0: bigint,
reserve1: bigint,
): bigint {
if (reserve0 === 0n) return 0n; // empty pool
return (amount0 * reserve1) / reserve0;
}
// Reverse
export function quoteToken0(amount1: bigint, reserve0: bigint, reserve1: bigint): bigint {
if (reserve1 === 0n) return 0n;
return (amount1 * reserve0) / reserve1;
}
// Calculate LP tokens user receives
export function calcLPOut(
amount0: bigint,
amount1: bigint,
reserve0: bigint,
reserve1: bigint,
totalSupply: bigint,
): bigint {
if (totalSupply === 0n) {
// First liquidity provider
const MINIMUM_LIQUIDITY = 1000n;
return sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
}
const lp0 = (amount0 * totalSupply) / reserve0;
const lp1 = (amount1 * totalSupply) / reserve1;
return lp0 < lp1 ? lp0 : lp1;
}
Add liquidity form
export function AddLiquidityForm({ pool }: { pool: PoolState }) {
const [amount0, setAmount0] = useState('');
const [amount1, setAmount1] = useState('');
const decimals0 = 18;
const decimals1 = 6; // USDC
const handleAmount0Change = (val: string) => {
setAmount0(val);
if (!val || pool.reserve0 === 0n) return;
const wei0 = parseUnits(val, decimals0);
const wei1 = quoteToken1(wei0, pool.reserve0, pool.reserve1);
setAmount1(formatUnits(wei1, decimals1));
};
return (
<div>
<input value={amount0} onChange={(e) => handleAmount0Change(e.target.value)} placeholder="Token 0" />
<input value={amount1} onChange={(e) => setAmount1(e.target.value)} placeholder="Token 1" />
<button>Add Liquidity</button>
</div>
);
}
Key aspects: token approval, impermanent loss calculation, slippage tolerance, and position tracking.







