Zaokrúhľovanie likvidity a neznižujúci sa invariant — vyšetrovanie a oprava
Dátum: 2025-10-25
Repozitár: BiatecCLAMM (projects/BiatecCLAMM)
Primárny súbor: contracts/BiatecClammPool.algo.ts
Súhrn pre používateľov
Dôležité: Pri pridávaní likvidity do poolov s existujúcimi poplatkami môžete dostať mierne menej LP tokenov, ako by naznačovala priama proporcia. Toto je zámerné a chráni pool pred únikom hodnoty prostredníctvom chýb zaokrúhľovania.
Čo by mali používatelia vedieť
- Typický dopad: Strata je < 0.0001% vášho vkladu
- Maximálna pozorovaná: ~10 základných jednotiek na operáciu (často menej ako $0.000001)
- Nezhromažďuje sa: Strata je lineárna s počtom operácií, nie exponenciálna
- Hodnota poolu sa zvyšuje: Napriek tomuto zaokrúhľovaniu vaše LP tokeny získavajú hodnotu z obchodných poplatkov
Prečo sa to deje
Pri mintovaní LP tokenov pool zohľadňuje nahromadené poplatky: vklad likvidity delta namintuje delta * distributedLp / (distributedLp + usersFeeLiquidity) LP tokenov, zaokrúhlených nadol na desatinné miesta LP tokenu (6). Podiel riedenia poplatkov delta - X sa pripíše existujúcim držiteľom LP (LiquidityUsersFromFees); zvyšok zo zaokrúhlenia pod úrovňou mikro-LP zostáva v poole, takže zaokrúhľovanie vždy uprednostňuje pool pred jednotlivým vkladateľom. To zabraňuje útočníkom odčerpávať hodnotu prostredníctvom opakovaných malých operácií. Odvodenie nájdete v časti Ochrana likviditných poplatkov.
Stratégie zmiernenia
- Hromadte svoje operácie: Vkladajte väčšie sumy menej často
- Akceptujte malé straty: Považujte ich za náklady na ochranu poplatkov
- Dlhodobý pohľad: Hodnota poolu sa stále zvyšuje z obchodných poplatkov v priebehu času
Príklad scenára
Vklad: 1,000,000 tokenov
Okamžité výber: 999,999.99 tokenov
Strata: 0.01 tokenov (0.000001%)
Táto malá strata je prijateľná, pretože:
- Zabraňuje poolu stratiť hodnotu
- Strata je deterministická a ohraničená
- Vaše LP tokeny sa oceňujú z obchodných poplatkov
- Alternatíva (únik poolu) by bola horšia pre všetkých LP
Technické hlboké ponorenie
Cieľ
Zabezpečiť, že uložená likvidita poolu (globálny stav Liquidity) sa nikdy nezmenší v dôsledku normálnych operácií (swapy, pridanie likvidity, distribúcia prebytočných aktív). Akákoľvek pozorovaná malá strata musí byť artefaktom celočíselného zaokrúhľovania, nie ekonomickou stratou. Odmietnuť iba skutočné, väčšie poklesy.
Súhrn problému
- Jest test pre scenár swapu s nulovým poplatkom zlyhal s tvrdením ako: "Likvidita sa musí zvýšiť po swap".
- Hlavná príčina: celočíselná aritmetika kombinovaná s škálovaním desatinných miest aktív a viacerými deleniami/odmocninami produkovala malé dolné zaokrúhľovacie posuny pri prepočítaní likvidity z bilancií. Toto sú algoritmické chyby skracovania (zaokrúhľovanie nadol) a nie ekonomické straty.
Čo som zmenil
V contracts/BiatecClammPool.algo.ts som zaviedol pomocné funkcie a malé zmeny riadenia toku, aby som zabezpečil, že uložená likvidita je neznižujúca sa pre swap a vkladové toky, pričom zachoval prísne kontroly pre väčšie poklesy:
-
Nová pomocná funkcia:
calculateCurrentLiquidity()- Počíta projekčnú likviditu z
assetABalanceBaseScale,assetBBalanceBaseScale, cenových rozsahov a vracia vypočítanéuint256(nezapisuje stav).
- Počíta projekčnú likviditu z
-
Nová pomocná funkcia:
setCurrentLiquidityNonDecreasing(oldLiquidity: uint256): uint256// Pseudokód pre logiku:projectedLiquidity = calculateCurrentLiquidity();if (projectedLiquidity >= oldLiquidity) {Liquidity = projectedLiquidity; // akceptovať a zapísaťreturn projectedLiquidity;} else {liquidityDrop = oldLiquidity - projectedLiquidity;if (liquidityDrop <= allowance) {Liquidity = oldLiquidity; // zachovať monotónny stavreturn oldLiquidity;} else {assert('ERR-LIQ-DROP'); // skutočné zlyhanie integrity}} -
Nahradil priame volania, ktoré prepočítali a slepo zapisovali
Liquidityv týchto cestách kódu, aby používali nový neznižujúci setter:- swap cesta (miesto predtým tvrdiace "Likvidita sa nesmie zmenšiť po swap")
- cesta spracovania pridania likvidity
- cesta distribúcie prebytočných aktív
-
Pomocná funkcia
getLiquidityRoundingAllowance()vracia povolenie vypočítané ako:allowance = scaleA * scaleB + scaleA + scaleBkde
scaleA = assetADecimalsScaleFromBaseascaleB = assetBDecimalsScaleFromBase.(Toto je konzervatívny obal, ktorý ohraničuje šírené chyby celočíselného zaokrúhľovania cez zmiešanú škálovú aritmetiku a krok odmocniny.)
Prečo je vôbec potrebné povolenie
- Všetka matematika likvidity používa celočíselnú aritmetiku a konverziu škály: bilancie sa konvertujú na pevnú základnú škálu (1e9) pomocou
assetADecimalsScaleFromBaseaassetBDecimalsScaleFromBase. - Pri kombinovaní škálovaných celých čísel s deleniami a operáciami odmocniny môže každé zaokrúhľovanie/skracovanie stratiť takmer jednu jednotku relatívne k deliteľovi. Keď sa tieto malé skracovania neskôr vynásobia, môžu sa mapovať na stratu meranú v základných škálových jednotkách, ktorá je proporcionálna k produktu škál na aktívum.
- Malý celočíselný pokles (niekoľko základných jednotiek) nie je ekonomická strata; je to numerický šum. Bez tolerovania tohto môžu správne swapy s nulovým poplatkom neočakávane zlyhať.
Odôvodnenie pre zvolený vzorec
- Konzervatívny najhorší prípad odôvodnenia (neformálny): maximá skracovania na os môžu interagovať multiplikatívne cez neskoršie produkty/delenia. Vzorec
scaleA*scaleB + scaleA + scaleBohraničuje hornú hranicu jednokrokového šíreného krížového člena plus aditívne skracovania na os. Použili sme mierne uvoľnenejší (voľnejší) obal ako tesná algebraická −3 simplifikácia pre jednoduchosť a bezpečnosť. - Politika kombinovaná s
setCurrentLiquidityNonDecreasingznamená, že nikdy neukladáme menšiu hodnotu — udržiavameLiquiditymonotónnu na reťazci, ale stále tvrdenie (vrátenie späť), keď pokles prekročí obal (indikujúci skutočný problém).
Súbory a zmenené symboly (rýchla referencia)
contracts/BiatecClammPool.algo.ts- Pridané:
calculateCurrentLiquidity() - Pridané:
setCurrentLiquidityNonDecreasing(oldLiquidity: uint256): uint256 - Pridané:
getLiquidityRoundingAllowance(): uint256 - Nahradil priame použitia
setCurrentLiquidity()v swap, pridanie likvidity a distribučných tokoch ssetCurrentLiquidityNonDecreasing, kde je potrebná monotónnosť. - Zachoval
setCurrentLiquidity()ako pohodlie, ktoré zapisuje surovú vypočítanú likviditu (stále používané na niekoľkých ďalších miestach, kde nie je potrebná monotónna politika).
- Pridané:
Testy a overenie
- Repozitár poskytuje Jest testy, ktoré cvičia AMM logiku a edge case s nulovým poplatkom. Spustenie cieleného testu sa vykonáva pomocou:
npm run test:1:build
- Poznámka: Test harness vyžaduje Algorand sandbox alebo nakonfigurované testovacie prostredie (KMD/localnet dispenser účet). V mojom prostredí test run zlyhal, keď miestny KMD dispenser nebol dostupný; to je problém nastavenia prostredia, nie chyba kontraktu.
Ako reprodukovať pôvodný zlyhávajúci symptóm
- Zaistite, že Algorand sandbox (alebo prostredie odhaľujúce KMD dispenser účet očakávaný fixture) beží a je nakonfigurovaný.
- Z koreňa projektu (kde žije
package.json):
# plná build + cielený test
npm run test:1:build
- Pred opravou test swapu s nulovým poplatkom
Extreme-No-Fees - ASASR EURUSD - 0.9 - 1.1, LP fee 0, Biatec fee 0%produkoval zlyhanie tvrdenia:Liquidity must increase after swap. Po tejto oprave uložená likvidita zostáva monotónna a test by mal prejsť, keď je prostredie dostupné.
Denník rozhodnutí / kompromisy
- Udržiavanie uloženej
Liquiditystriktne monotónnej vyhýba sa zlyhávaniu downstream invariantov a testovacích očakávaní kvôli numerickému šumu. - Kompromis je pridanie permisívneho obalu: malé zaokrúhľovacie poklesy sú maskované zachovaním predchádzajúcej hodnoty namiesto zápisu mierne menšej vypočítanej hodnoty. Toto je bezpečné, pretože maskované rozdiely sú v rámci deterministických hraníc a neekonomické. Väčšie poklesy stále vracajú späť nahlas.
- Vzorec povolenia je konzervatívny a zámerne jednoduchý na lacné výpočítanie v TEALScript/TEALScript-generovanom kóde.
Možné vylepšenia
- Zúžiť povolenie: použiť (scaleA * scaleB + scaleA + scaleB - 3) alebo vypočítať dynamické hranice odvodené z presne toho, ktoré delenia/odmocniny boli vykonané v aktuálnej ceste.
- Použiť aritmetiku s vyššou presnosťou (ak podporované), takže intermediálne zaokrúhľovania sú menej škodlivé; toto môže vyžadovať viac opcode rozpočtu alebo širšie typy celých čísel.
- Pridať unit testy, ktoré zámerne vytvárajú najhoršie scenáre zaokrúhľovania na potvrdenie, že povolenie je dostatočné a nie príliš permisívne.
Navrhované následné kroky
- Spustiť plnú Jest sadu v prostredí s Algorand sandbox/KMD správne nakonfigurovaným:
npm run test. - Pridať vyhradený test, ktorý simuluje veľa swapnutých malých množstiev so zmesou desatinných miest aktív na cvičenie hraníc zaokrúhľovania.
- Zvážiť následnú zmenu na vytvorenie povolenia dynamického na cestu, ak rozpočet gas/opcode umožňuje.
Ak chcete, môžem:
- implementovať tesnejšiu −3 variantu vzorca povolenia a pushnúť ju ako malú záplatu, alebo
- pridať vyhradený unit test, ktorý konštruuje explicitný najhorší prípad zaokrúhľovania a overuje správanie
setCurrentLiquidityNonDecreasing.
Povedzte mi, ktorý follow-up by ste chceli a implementujem ho ďalej.