Реалізація інтерфейсу ліквідності (LP) на веб-сайті
LP-інтерфейс дозволяє користувачам додавати і виводити ліквідність з пулу AMM. Це складніше за стейкинг: потрібно розуміти співвідношення токенів у пулі, розраховувати пропорції при додаванні, показувати долю в пулі та impermanent loss. Плюс — approve для двох токенів, а не одного.
Моделі AMM
Два основні варіанти, які зустрічаються в проектах:
Uniswap v2 / SushiSwap (constant product x·y=k): простої пропорція, ERC-20 LP-токени, фіксована 0.3% комісія.
Uniswap v3 (concentrated liquidity): користувач вибирає цінову діапазон, позиція — NFT, більш складний розрахунок.
Розбираємо Uniswap v2 як основу — більшість custom AMM будується на цій моделі.
Чтення стану пулу
// 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}`;
// Вичислювані
userShare: number; // доля користувача в пулі, 0–1
userToken0: bigint; // скільки token0 можна вивести
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 };
}
Розрахунок пропорцій при додаванні ліквідності
При додаванні ліквідності в непустий пул другий токен розраховується автоматично за поточною ціною пулу:
// Користувач вводить кількість token0 → розраховуємо token1
export function quoteToken1(
amount0: bigint,
reserve0: bigint,
reserve1: bigint,
): bigint {
if (reserve0 === 0n) return 0n; // порожний пул
return (amount0 * reserve1) / reserve0;
}
// І навпаки
export function quoteToken0(amount1: bigint, reserve0: bigint, reserve1: bigint): bigint {
if (reserve1 === 0n) return 0n;
return (amount1 * reserve0) / reserve1;
}
// Розрахунок LP-токенів, які отримає користувач
export function calcLPOut(
amount0: bigint,
amount1: bigint,
reserve0: bigint,
reserve1: bigint,
totalSupply: bigint,
): bigint {
if (totalSupply === 0n) {
// Перший постачальник ліквідності
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;
}
Форма додавання ліквідності
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>Додати ліквідність</button>
</div>
);
}
Ключові аспекти: дозвіл на токени, розрахунок impermanent loss, допуск на ковзання та відслідковування позицій.







