Отметим: когда клиент пришёл с задачей сделать интерфейс для пулов 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-интерфейса уже сегодня.







