הערה: כאשר לקוח הגיע עם משימה של בניית ממשק עבור בריכות Uniswap v2, הדבר הראשון שנתקלו בו היה היעדר חישוב חלקים והערכת הפסד בלתי-קבוע (impermanent loss). משתמשים ערבבו פרופורציות בעת הוספת נזילות, ואיבדו כספים עקב החלקה (slippage) שלא נלקחה בחשבון. תרחיש טיפוסי: משתמש מזין 10 ETH לבריכת ETH/USDC, אך הממשק לא מציין את הכמות הנדרשת של USDC — העסקה נכשלת עם שגיאת פרופורציה. פיתחנו פתרון סוהר: ממשק שמחשב אוטומטית פרופורציות, מזהיר מפני הפסד בלתי-קבוע, ומנהל בבטחה אישורי אסימונים (token approvals). הניסיון שלנו ב-DeFi עולה על 5 שנים, עם 15+ ממשקי LP שיושמו עבור AMM שונים, מקלונים פשוטים של v2 ועד v3 מורכבים עם נזילות מרוכזת.
בעיות ונקודות כאב
הבעיה העיקרית של המשתמש היא חוסר הבנה של מכניקת הבריכה. הם מזינים סכומים שרירותיים, מקבלים פרופורציות לא אופטימליות, ומפסידים עקב הפסד בלתי-קבוע. השנייה היא מורכבות האישורים: הם צריכים לאשר שני אסימונים, לעיתים קרובות בסדר הלא נכון, מה שמוביל לשגיאות עסקה. השלישית היא היעדר ויזואליזציה של החלק שלהם והעמלות שנצברו. הממשק שלנו פותר את כל זה: חישוב פרופורציות אוטומטי בכל שינוי שדה, אישור רציף של שני האסימונים בלחיצה אחת, והצגת חלק המשתמש בזמן אמת.
מודלי AMM
שני וריאנטים עיקריים שנתקלים בהם בפרויקטים:
Uniswap v2 / SushiSwap (מכפלה קבועה x·y=k): פרופורציה פשוטה, אסימוני LP מסוג ERC-20, עמלה קבועה של 0.3%.
Uniswap v3 (נזילות מרוכזת): המשתמש בוחר טווח מחירים, הפוזיציה היא NFT, חישוב מורכב יותר.
נתמקד ב-Uniswap v2 כבסיס — רוב ה-AMM המותאמים אישית בנויים על מודל זה. מידע נוסף על הפרוטוקול ניתן למצוא ב-תיעוד הרשמי של Uniswap v2.
נתוני בריכה וחישובים
קריאת מצב הבריכה באמצעות multicall: רזרבות, 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.איך לנהל אישורי אסימונים?
בעיה נפוצה אחת היא שמשתמשים שוכחים לאשר את האסימון השני או עושים זאת בסדר הלא נכון. אנו ממכנים את הרצף: קודם מאשרים את האסימון הראשון, אחר כך את השני, ואז קוראים ל-addLiquidity. זה מפחית את מספר העסקאות וחוסך בגז עד 40%. אוטומציה של אישורים מפחיתה עלויות גז ב-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 שיתקבלו.
- לחץ על 'הוסף נזילות' — הממשק יאשר אוטומטית את שני האסימונים ויקרא ל-addLiquidity.
- אשר את העסקאות בארנק שלך. לאחר השלמתן, תראה את החלק שלך ואת העמלות שנצברו.
רכיבי ממשק להוספה והסרה
טופס הוספת נזילות עם חישוב אוטומטי של האסימון השני והערכת אסימוני 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>
);
}הסרת נזילות עם אישור מקדים של אסימוני 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 };
} מחשבון הפסד בלתי-קבוע ותפקידו
הפסד בלתי-קבוע הוא סיכון מרכזי עבור ספקי נזילות. אם מחיר האסימון בבריכה משתנה באופן דרמטי, הספק עשוי לקבל פחות מאשר בהחזקה פשוטה. המחשבון המובנה עוזר למשתמשים להעריך הפסדים לפני הכניסה. הנוסחה: IL = 1 - (2√k / (1+k)), כאשר k הוא היחס בין המחיר החדש למקורי. דוגמה: אם המחיר מוכפל, IL = 5.7%; אם הוא מוכפל פי ארבעה, IL = 20%. המחשבון בתוך הממשק מאפשר למשתמשים להזין את שינוי המחיר המשוער ולראות את ההפסד מיד. זה חשוב במיוחד בבורסות מבוזרות שבהן התנודתיות גבוהה.
מה כלול בפיתוח ממשק LP?
אנו מספקים חבילה מלאה: תיעוד ארכיטקטורה, קוד חוזה חכם (אם נדרש), אינטגרציית ארנק (MetaMask, WalletConnect), בדיקות אינטראקציה (Hardhat/Foundry), ופריסה לרשת הייצור. אנו מבטיחים איכות קוד ואבטחה של מנגנוני האישור — למהנדסים שלנו יש ניסיון בביקורת מ-15+ פרויקטי DeFi. קבל ייעוץ כדי לדון בפרויקט שלך.
השוואת מודלי AMM
| פרמטר | Uniswap v2 | Uniswap v3 |
|---|---|---|
| נוסחה | x·y=k (מכפלה קבועה) | נזילות מרוכזת |
| יעילות הון | נמוכה (מפוזרת על פני כל העקומה) | גבוהה (עד פי 1000 יותר יעילה בטווח צר) |
| אסימון LP | ERC-20 | NFT |
| מורכבות חישוב | פשוטה | גבוהה (טווח, ticks) |
| עמלה | 0.3% | משתנה (0.05%–1%) |
Uniswap v3 יעיל פי 1000 בהון בטווח צר, אך דורש ממשק מתקדם לבחירת טווח. עבור בריכות סטנדרטיות, v2 פשוט ויציב יותר.
לוח זמנים ועלות
| סוג ממשק | לוח זמנים | הערות |
|---|---|---|
| קלון Uniswap v2 | 7–10 ימים | תכונות בסיסיות: הוספה/הסרה, חישוב פרופורציות, IL |
| Uniswap v3 עם נזילות מרוכזת | 2–3 שבועות | בחירת טווח מחירים, פוזיציות NFT |
| AMM מותאם אישית | משתנה | תלוי בלוגיקת הבריכה |
העלות מחושבת באופן אישי — צור קשר כדי לקבל הערכה לפרויקט שלך. פנה אלינו כדי לדון בצרכים שלך.
למה לבחור בנו?
אנו מבטיחים את אבטחת מנגנוני האישור — למהנדסים שלנו יש ניסיון בביקורת מ-15+ פרויקטי DeFi. אנו משתמשים בספריות מוכחות (viem, wagmi) ועוקבים אחרי שיטות עבודה מומלצות (המלצות לשימוש בפרוטוקול Uniswap). 5 שנים בשוק פיתוח DeFi, 15+ ממשקי LP שיושמו. חיסכון בזמן בשלב האישור וחישוב הפרופורציות מאפשר למשתמשים להימנע משגיאות ולהפחית עלויות גז. הזמן את פיתוח ממשק ה-LP שלך היום.







