Skip to main content

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​

  1. Hromadte svoje operácie: Vkladajte väčšie sumy menej často
  2. Akceptujte malé straty: Považujte ich za náklady na ochranu poplatkov
  3. 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).
  • 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 stav
    return oldLiquidity;
    } else {
    assert('ERR-LIQ-DROP'); // skutočné zlyhanie integrity
    }
    }
  • Nahradil priame volania, ktoré prepočítali a slepo zapisovali Liquidity v 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 + scaleB

    kde scaleA = assetADecimalsScaleFromBase a scaleB = 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 assetADecimalsScaleFromBase a assetBDecimalsScaleFromBase.
  • 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 + scaleB ohranič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 setCurrentLiquidityNonDecreasing znamená, že nikdy neukladáme menšiu hodnotu — udržiavame Liquidity monotó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 s setCurrentLiquidityNonDecreasing, 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).

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​

  1. Zaistite, že Algorand sandbox (alebo prostredie odhaľujúce KMD dispenser účet očakávaný fixture) beží a je nakonfigurovaný.
  2. Z koreňa projektu (kde žije package.json):
# plná build + cielený test
npm run test:1:build
  1. 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 Liquidity striktne 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.