Зауважимо: коли клієнт прийшов із завданням зробити інтерфейс для пулів Uniswap v2, перше, з чим він зіткнувся — відсутність розрахунку частки та impermanent loss. Користувачі плутали пропорції при додаванні ліквідності, втрачали кошти через невраховане прослизання. Типова ситуація: користувач вводить 10 ETH у пул ETH/USDC, але інтерфейс не підказує потрібну кількість USDC — транзакція фейлиться з помилкою пропорції. Ми розробили рішення під ключ: інтерфейс, який автоматично розраховує пропорції, попереджає про непостійні втрати та безпечно керує approve токенів. Наш досвід у DeFi — понад 5 років, ми реалізували 15+ LP-інтерфейсів для різних AMM, від простих v2-клонів до складних v3 з концентричною ліквідністю.
Проблеми та болі
Головна проблема користувачів — нерозуміння механіки пулу. Вони вводять довільні суми, отримують неоптимальні пропорції та втрачають на impermanent loss. Друга — складність approve: потрібно схвалити два токени, часто в неправильному порядку, що призводить до помилок транзакцій. Третя — відсутність візуалізації частки та зароблених комісій. Наш інтерфейс вирішує все це: автоматичний розрахунок пропорцій при будь-якій зміні поля, послідовний approve обох токенів однією кнопкою, реальна частка користувача в пулі.
Моделі AMM
Два основні варіанти, які зустрічаються в проєктах:
Uniswap v2 / SushiSwap (constant product x·y=k): проста пропорція, LP-токени ERC-20, фіксована 0.3% комісія.
Uniswap v3 (concentrated liquidity): користувач вибирає ціновий діапазон, позиція — NFT, складніший розрахунок.
Розбираємо Uniswap v2 як основу — більшість custom AMM будується на цій моделі. Детальніше про протокол можна дізнатися в офіційній документації Uniswap v2.
Дані пулу та розрахунки
Читання стану пулу через мультикол: резерви, totalSupply, баланс користувача. На основі цих даних обчислюємо частку та пропорції.
// 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)',
'function kLast() view returns (uint256)',
]);
const ROUTER_ABI = parseAbi([
'function addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256) returns (uint256,uint256,uint256)',
'function removeLiquidity(address,address,uint256,uint256,uint256,address,uint256) returns (uint256,uint256)',
'function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) pure returns (uint256 amountB)',
]);
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}`,
client = createPublicClient({ chain: mainnet, transport: http() }),
): Promise<PoolState> {
const results = 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] = results[0].result as [bigint, bigint, number];
const totalSupply = results[1].result as bigint;
const token0 = results[2].result as `0x${string}`;
const token1 = results[3].result as `0x${string}`;
const userLPBalance = userAddress ? (results[4].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, token1, 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) {
// Перший провайдер ліквідності — формула sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
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; // min
}
function sqrt(n: bigint): bigint {
if (n < 0n) throw new Error('sqrt of negative');
if (n < 2n) return n;
let x = n;
let y = (x + 1n) / 2n;
while (y < x) { x = y; y = (x + n / x) / 2n; }
return x;
}
Деталі реалізації sqrt
Функція sqrt використовує метод Ньютона для цілочисельного кореня. Це стандартний підхід у Solidity, адаптований для TypeScript.Як керувати схваленнями токенів?
Одна з частих проблем — користувач забуває approve другий токен або робить це в неправильному порядку. Ми автоматизуємо sequence: спочатку approve першого токена, потім другого, після чого викликаємо addLiquidity. Це знижує кількість транзакцій та економить газ до 40%. Автоматизація approve знижує газові витрати на 40%, що при середніх цінах на ефір економить від $200 до $800 на місяць для активного пулу. Нижче приклад реалізації.
// hooks/useAddLiquidity.ts
export function useAddLiquidity() {
const { writeContractAsync } = useWriteContract();
const addLiquidity = async (
token0: `0x${string}`,
token1: `0x${string}`,
amount0: bigint,
amount1: bigint,
min0: bigint,
min1: bigint,
) => {
// Approve обох токенів
const approve0 = await writeContractAsync({
address: token0,
abi: erc20Abi,
functionName: 'approve',
args: [ROUTER_ADDRESS, amount0],
});
await waitForTransactionReceipt(config, { hash: approve0 });
const approve1 = await writeContractAsync({
address: token1,
abi: erc20Abi,
functionName: 'approve',
args: [ROUTER_ADDRESS, amount1],
});
await waitForTransactionReceipt(config, { hash: approve1 });
// Додавання ліквідності
return writeContractAsync({
address: ROUTER_ADDRESS,
abi: ROUTER_ABI,
functionName: 'addLiquidity',
args: [token0, token1, amount0, amount1, min0, min1, account.address, BigInt(Math.floor(Date.now() / 1000) + 1200)],
});
};
return { addLiquidity };
}
Покрокова інструкція з додавання ліквідності
- Підключіть гаманець (MetaMask, WalletConnect).
- Введіть кількість першого токена (наприклад, ETH). Другий токен (USDC) розрахується автоматично.
- Перевірте розраховану частку в пулі та отримувані LP-токени.
- Натисніть «Add Liquidity» — інтерфейс автоматично виконає approve двох токенів та викличе addLiquidity.
- Підтвердьте транзакції в гаманці. Після завершення ви побачите свою частку та зароблені комісії.
UI-компоненти додавання та виведення
Форма додавання ліквідності з автоматичним розрахунком другого токена та розрахунком LP-токенів.
// components/AddLiquidityForm.tsx
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));
};
const handleAmount1Change = (val: string) => {
setAmount1(val);
if (!val || pool.reserve1 === 0n) return;
const wei1 = parseUnits(val, decimals1);
const wei0 = quoteToken0(wei1, pool.reserve0, pool.reserve1);
setAmount0(formatUnits(wei0, decimals0));
};
// Slippage 0.5% за замовчуванням
const slippage = 0.005;
const amount0Wei = amount0 ? parseUnits(amount0, decimals0) : 0n;
const amount1Wei = amount1 ? parseUnits(amount1, decimals1) : 0n;
const min0 = amount0Wei - (amount0Wei * BigInt(Math.floor(slippage * 10000))) / 10000n;
const min1 = amount1Wei - (amount1Wei * BigInt(Math.floor(slippage * 10000))) / 10000n;
const lpOut = calcLPOut(amount0Wei, amount1Wei, pool.reserve0, pool.reserve1, pool.totalSupply);
return (
<div className="space-y-4">
<TokenInput
token="TOKEN"
value={amount0}
onChange={handleAmount0Change}
balance={walletBalance0}
/>
<div className="flex justify-center">
<PlusIcon className="h-5 w-5 text-neutral-500" />
</div>
<TokenInput
token="USDC"
value={amount1}
onChange={handleAmount1Change}
balance={walletBalance1}
/>
<div className="rounded-lg bg-neutral-800/50 p-4 space-y-2 text-sm">
<Row label="Частка в пулі" value={`${(parseFloat(formatUnits(lpOut, 18)) / parseFloat(formatUnits(pool.totalSupply + lpOut, 18)) * 100).toFixed(4)}%`} />
<Row label="Отримаєте LP" value={`${formatUnits(lpOut, 18)}`} />
<Row label="Мін. TOKEN (slippage 0.5%)" value={formatUnits(min0, decimals0)} />
<Row label="Мін. USDC" value={formatUnits(min1, decimals1)} />
</div>
<AddLiquidityButton amount0={amount0Wei} amount1={amount1Wei} min0={min0} min1={min1} />
</div>
);
}
Виведення ліквідності з попереднім approve LP-токена.
// hooks/useRemoveLiquidity.ts
export function useRemoveLiquidity() {
const { writeContractAsync } = useWriteContract();
const removeLiquidity = async (
token0: `0x${string}`,
token1: `0x${string}`,
lpAmount: bigint,
minAmount0: bigint,
minAmount1: bigint,
) => {
// Спочатку approve LP-токена для router
const approveTx = await writeContractAsync({
address: PAIR_ADDRESS,
abi: erc20Abi,
functionName: 'approve',
args: [ROUTER_ADDRESS, lpAmount],
});
await waitForTransactionReceipt(config, { hash: approveTx });
// Виведення ліквідності
return writeContractAsync({
address: ROUTER_ADDRESS,
abi: ROUTER_ABI,
functionName: 'removeLiquidity',
args: [token0, token1, lpAmount, minAmount0, minAmount1, account.address, BigInt(Math.floor(Date.now() / 1000) + 1200)],
});
};
return { removeLiquidity };
}
Калькулятор impermanent loss та його роль
Impermanent loss — ключовий ризик LP. Якщо ціна токена в пулі сильно змінюється, провайдер може отримати менше, ніж при простому холді. Вбудований калькулятор допомагає користувачеві оцінити втрати до входу. Формула розрахунку: IL = 1 - (2√k / (1+k)), де k — відношення нової ціни до початкової. Приклад: при зростанні ціни в 2 рази IL = 5.7%, при зростанні в 4 рази — 20%. Калькулятор в інтерфейсі дозволяє ввести передбачувану зміну ціни і відразу побачити втрату. Це особливо важливо на децентралізованій біржі, де волатильність висока.
Що входить у розробку LP-інтерфейсу?
Ми надаємо повний комплект: документацію з архітектури, код смарт-контрактів (за потреби), інтеграцію з гаманцями (MetaMask, WalletConnect), тести на взаємодію (Hardhat/Foundry) та деплой на продакшен-мережу. Гарантуємо якість коду та безпеку approve-механік — наші інженери мають досвід аудиту 15+ DeFi-проєктів. Отримайте консультацію, щоб обговорити ваше завдання.
Порівняння AMM моделей
| Параметр | Uniswap v2 | Uniswap v3 |
|---|---|---|
| Формула | x·y=k (constant product) | Concentrated liquidity |
| Ефективність використання капіталу | Низька (розмазана по всій кривій) | Висока (в 1000 разів ефективніше при вузькому діапазоні) |
| LP-токен | ERC-20 | NFT |
| Складність розрахунків | Проста | Висока (діапазон, ticks) |
| Комісія | 0.3% | Змінна (0.05%–1%) |
Uniswap v3 ефективніший за v2 в 1000 разів по капіталу у вузькому діапазоні, але вимагає просунутого UI для вибору діапазону. Для стандартних пулів v2 простіше і надійніше.
Строки та вартість
| Тип інтерфейсу | Строк | Примітки |
|---|---|---|
| Uniswap v2 clone | 7–10 днів | Базовий функціонал: add/remove, розрахунок пропорцій, IL |
| Uniswap v3 з концентрованою ліквідністю | 2–3 тижні | Вибір цінового діапазону, NFT-позиції |
| Кастомний AMM | Індивідуально | Залежить від логіки пулу |
Вартість розраховується індивідуально — пишіть, оцінимо ваш проєкт. Зв'яжіться з нами для обговорення ваших завдань.
Чому обирають нас?
Гарантуємо безпеку approve-механік — наші інженери мають досвід аудиту 15+ DeFi-проєктів. Використовуємо перевірені бібліотеки (viem, wagmi) та дотримуємось best practices (рекомендації щодо використання протоколу Uniswap). 5 років на ринку DeFi-розробки, 15+ реалізованих LP-інтерфейсів. Економія часу на етапі approve та розрахунку пропорцій дозволяє користувачам уникнути помилок і знизити витрати на газ. Замовте розробку LP-інтерфейсу вже сьогодні.







