const { useState: ilUseState, useMemo: ilUseMemo } = React; const { Card: IlCard, useMobile: ilUseMobile, TokenPair: IlTokenPair, usePairs: ilUsePairs } = window; // Volatile pools: x·y = k. Stable pools (Solidly): x³y + xy³ = k, with x, y in 1e18 units of the same numeraire. function ilVolatile(r) { return (2 * Math.sqrt(r)) / (1 + r) - 1; } function stableCurve(x, y) { return x * x * x * y + x * y * y * y; } function stablePrice(x, y) { return (3 * x * x * y + y * y * y) / (x * x * x + 3 * x * y * y); } // Start from a balanced pool (x = y = 1, price 1), find the point on the same curve where the marginal price equals r. function ilStable(r) { if (r === 1) return 0; const k = stableCurve(1, 1); let lo = 1e-6, hi = 1e6; for (let i = 0; i < 200; i++) { const x = Math.sqrt(lo * hi); const y = solveY(x, k); if (stablePrice(x, y) > r) lo = x; else hi = x; } const x = Math.sqrt(lo * hi), y = solveY(x, k); const lp = x * r + y, hold = r + 1; return lp / hold - 1; } function solveY(x, k) { let lo = 0, hi = Math.max(2, k / (x * x * x)); for (let i = 0; i < 200; i++) { const y = (lo + hi) / 2; if (stableCurve(x, y) < k) lo = y; else hi = y; } return (lo + hi) / 2; } function ilPct(r, stable) { return (stable ? ilStable(r) : ilVolatile(r)) * 100; } const ilNum = (n, dp = 2) => n.toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp }); const ilSigned = (n, dp = 2) => (n > 0 ? '+' : '') + ilNum(n, dp); function IlLabel({ children, accent }) { return
{children}
; } function IlField({ label, value, onChange, unit, accent, hint }) { return (
{label}
onChange(e.target.value.replace(/[^0-9.\-]/g, ''))} inputMode="decimal" style={{ flex:1, background:'none', border:'none', outline:'none', fontFamily:'var(--font-m)', fontSize:20, fontWeight:700, color:'var(--ink)', minWidth:0 }} /> {unit}
{hint &&
{hint}
}
); } function IlStat({ label, value, sub, tone }) { const color = tone === 'bad' ? '#ef4444' : tone === 'good' ? '#4ade80' : tone === 'warn' ? '#ffd47c' : 'var(--ink)'; return (
{label}
{value}
{sub &&
{sub}
}
); } function IlCurve({ stable, r, yieldPct, accent }) { const W = 640, H = 220, padL = 44, padR = 12, padT = 12, padB = 28; const xs = []; for (let i = 0; i <= 120; i++) xs.push(Math.pow(2, -4 + (8 * i) / 120)); const ils = xs.map(x => ilPct(x, stable)); const minY = Math.min(-1, ...ils, -yieldPct - 1), maxY = Math.max(1, yieldPct + 1); const px = x => padL + ((Math.log2(x) + 4) / 8) * (W - padL - padR); const py = y => padT + ((maxY - y) / (maxY - minY)) * (H - padT - padB); const path = xs.map((x, i) => `${i ? 'L' : 'M'}${px(x).toFixed(1)},${py(ils[i]).toFixed(1)}`).join(' '); const ticks = [0.0625, 0.25, 0.5, 1, 2, 4, 16]; const rc = Math.min(16, Math.max(0.0625, r)); return ( {yieldPct > 0 && } {ticks.map(t => ( {t < 1 ? `${ilNum(t * 100, 0)}%` : `${t}x`} ))} {[minY, minY / 2, 0].map((y, i) => ( {ilNum(y, 0)}% ))} {yieldPct > 0 && fees + emissions {ilNum(yieldPct, 1)}%} ); } function ILScreen({ accent }) { const isMobile = ilUseMobile(); const { pairs } = ilUsePairs(); const [pairAddr, setPairAddr] = ilUseState(null); const [stableManual, setStableManual] = ilUseState(false); const [deposit, setDeposit] = ilUseState('10000'); const [pA, setPA] = ilUseState('0'); const [pB, setPB] = ilUseState('0'); const [days, setDays] = ilUseState('30'); const [feeApr, setFeeApr] = ilUseState('0'); const [emitApr, setEmitApr] = ilUseState('0'); const list = pairs ?? []; const pair = list.find(p => p.pair_address === pairAddr) ?? null; const stable = pair ? !!pair.stable : stableManual; const symA = pair ? pair.token0_symbol : 'TOKEN A'; const symB = pair ? pair.token1_symbol : 'TOKEN B'; const out = ilUseMemo(() => { const dep = parseFloat(deposit) || 0; const a = 1 + (parseFloat(pA) || 0) / 100, b = 1 + (parseFloat(pB) || 0) / 100; if (a <= 0 || b <= 0) return null; const r = a / b; const il = (stable ? ilStable(r) : ilVolatile(r)); const hold = dep * (a + b) / 2; const lp = hold * (1 + il); const t = Math.max(0, parseFloat(days) || 0) / 365; const yieldPct = ((parseFloat(feeApr) || 0) + (parseFloat(emitApr) || 0)) * t; const earned = lp * yieldPct / 100; const net = lp + earned - hold; let breakeven = null; if (yieldPct > 0) { let lo = 1, hi = 1e6; for (let i = 0; i < 100; i++) { const m = Math.sqrt(lo * hi); if (-ilPct(m, stable) < yieldPct) lo = m; else hi = m; } breakeven = Math.sqrt(lo * hi); } return { r, il: il * 100, hold, lp, ilUsd: lp - hold, yieldPct, earned, net, netPct: hold ? (net / hold) * 100 : 0, breakeven }; }, [deposit, pA, pB, days, feeApr, emitApr, stable]); const presets = [-50, -25, 0, 25, 50, 100, 300]; return (
RISK

IMPERMANENT LOSS

What an LP position is worth against simply holding the two tokens, after a price move. Volatile pools follow x·y = k. Stable pools follow x³y + xy³ = k, which keeps quoting near 1:1, so the same price move rebalances you harder and a broken peg costs more than it would in a volatile pool.

POOL TYPE
{[[false,'VOLATILE · x·y = k'],[true,'STABLE · x³y + xy³ = k']].map(([s, l]) => ( ))}
{list.length > 0 && (
{list.map(p => { const on = pair?.pair_address === p.pair_address; return ( ); })}
)}
POSITION & PRICE MOVE
{symA} QUICK {presets.map(v => ( ))}
WHAT YOU EARN MEANWHILE
{out && ( RESULT
0 ? 'good' : 'neutral'} />
IL BY RELATIVE PRICE ({symA} / {symB}) · MARKER = YOUR INPUT ({ilNum(out.r, 3)}x)
IL depends only on the ratio between the two prices, not on the direction of the market. It is realised when you withdraw; fees and emissions accrue regardless. Emissions are counted at their current price and do not compound.
)}
); } Object.assign(window, { ILScreen });