Ochrona akumulacji opłat gdy dołączają nowi LP
Data: 2025-10-26, zaktualizowano: 2026-09-26
Repozytorium: BiatecCLAMM (projects/BiatecCLAMM)
Główny plik: contracts/BiatecClammPool.algo.ts
Tło
Gdy pula zbiera opłaty ze swapów, stan on-chain śledzi je jako dodatkową płynność (LiquidityUsersFromFees, Lu) bez mintowania dodatkowych tokenów LP. Pierwotny przepływ dodawania płynności mintował nowe tokeny LP bezpośrednio z surowego przyrostu płynności (newLiquidity - oldLiquidity). W rezultacie nowy uczestnik mógł dodać płynność i natychmiast ją wycofać, przejmując proporcjonalny udział w historycznych opłatach, które powinny należeć do dotychczasowych dostawców płynności (LP).
Regresja ujawniła się w teście puli „new liquidity provider does not scoop pre-existing fees”, w którym konto C dodaje płynność po scenariuszu z opłatami swap i natychmiast ją wycofuje. Oczekiwanym zachowaniem jest to, że konto otrzymuje dokładnie tyle, ile wpłaciło (zerowy zysk netto).
Niezmiennik księgowy
Każda jednostka płynności puli musi mieć właściciela:
Liquidity == distributedLp + LiquidityUsersFromFees + LiquidityBiatecFromFees
distributedLp(D) to podaż LP będąca w obiegu poza kontem puli, przeskalowana do precyzji bazowej,Luto płynność z opłat należąca proporcjonalnie (pro rata) do wszystkich posiadaczy tokenów LP (wypłacana przezremoveLiquidity),Lbto płynność z opłat należąca do Biatec (wypłacana przezremoveLiquidityAdmin).
Metoda swap zachowuje ten niezmiennik, ponieważ cały przyrost płynności jest dzielony pomiędzy Lu oraz Lb.
Podsumowanie poprawki
Każdy będący w obiegu token LP ma pokrycie w (D + Lu) / D jednostkach płynności, ponieważ płynność opłat użytkowników Lu jest dzielona proporcjonalnie przez wszystkie tokeny LP przy wywołaniu removeLiquidity. Deponent dodający płynność delta otrzymuje zatem
X = delta * D / (D + Lu)
tokenów LP (zaokrąglonych w dół do miejsc dziesiętnych tokenu LP), a część odpowiadająca rozwodnieniu opłat delta - X zostaje zaksięgowana w Lu (wynosi ona dokładnie 0, dopóki pula nie zebrała żadnych opłat). Przy Lu' = Lu + delta - X:
- roszczenie nowego uczestnika przy wyjściu:
X * (D + Lu + delta) / (D + X) == delta— dokładnie tyle, ile wpłacono, bez historycznych opłat, - dotychczasowi LP:
D * (D + Lu + delta) / (D + X) == D + Lu— zachowują wszystkie historyczne opłaty, - niezmiennik:
D + X + Lu' == D + Lu + delta— żadna płynność nie pozostaje bez właściciela.
Gdy pula nie ma naliczonych opłat, wzór redukuje się do pierwotnego zachowania „wymintuj przyrost płynności”.
Dlaczego zastąpiono wariant kwadratowy
Między 2025-10-26 a 2026-09-26 kontrakt mintował min(delta, root), gdzie root był pierwiastkiem równania X^2 + X(D + Lu - Q) - Q * D = 0, a Q = depositA * newLiquidity / assetABalance (lub udział aktywa B, gdy nie wpłacono aktywa A). Powodowało to dwa problemy:
Qjest oszacowaniem jednostronnym. Dla depozytu, który nie jest w bieżącej proporcji puli (co jest dozwolone i przesuwa cenę puli), wartośćQjest mniejsza od faktycznie dodanej płynności, przez co deponent otrzymywał mniej tokenów LP niż wniesiona przez niego płynność.- Różnica
delta - Xnie była nigdzie księgowana. Zwiększała onaLiquidity, ale nie należała ani do posiadaczy LP, ani do Biatec, przez co nigdy nie mogła zostać wypłacona.
Pokazuje to pula mainnetowa 3720188642 (Gold/GoldDAO, zakres 128–256, opłata LP 0,1%, udział Biatec 20%): po trzech depozytach, 238 swapach i umorzeniu 100% tokenów LP w puli pozostało L = 57.3 (około 1.04 Gold i 61.6 GoldDAO), podczas gdy udział opłat Biatec wynosił zaledwie Lb = 0.045. Pełna historia jest odtwarzana w __test__/pool/mainnet-replay-3720188642.test.ts z pliku __test__/test-data/mainnet-pool-3720188642.json; po zastosowaniu poprawionego wzoru umorzenie zwraca całą pozycję, a w puli pozostaje wyłącznie Lb.
Dalsze zmiany w wersji 01-06-05
- Depozyt, który nie osiąga jednej mikrojednostki LP (
1e-6LP), jest odrzucany z błędemLP-ZERO-ERR. Wcześniej był zaokrąglany w górę do jednego tokenu LP, co pozwalało wpłacającemu 1 jednostkę bazową wypłacić więcej niż wpłacił (audyt 2026-09-07, H-01). - Metoda
reconcileLiquidity(tylko dla wykonawcy opłat) księguje płynność, którą starsze pule pozostawiły bez właściciela; zobacz Naprawa działających pul. - Metody
removeLiquidityorazremoveLiquidityAdminwspółdzielą jedną procedurę wypłaty (payOutLiquidity); ich zachowanie nie uległo zmianie.
Oczekiwania dotyczące zaokrąglania
- Tokeny LP są zaokrąglane w dół do 6 miejsc dziesiętnych; reszta poniżej mikro-LP (
< 1e-6LP na depozyt) pozostaje w puli jako zaokrąglenie na korzyść puli. Celowo nie jest ona księgowana jako przychód z opłat, dzięki czemuliquidityUsersFromFeespozostaje równe 0 aż do pierwszego swapu; metodareconcileLiquiditymoże przekazać zakumulowane reszty zaokrągleń posiadaczom LP. - Metoda
removeLiquiditynadal zaokrągla w dół wysyłane kwoty aktywów, więc wypłaty mogą być mniejsze od wpłat o kilka jednostek bazowych, które pozostają w puli. - Zestaw testów Jest weryfikuje, że saldo nowego uczestnika nigdy nie wzrasta i toleruje jedynie minimalny deficyt, a odtworzenie historii z mainnetu sprawdza niezmiennik po każdej operacji płynnościowej z tolerancją jednej mikrojednostki LP na operację.
Uwagi operacyjne
- Każda zmiana kontraktu wymaga ponownej kompilacji artefaktów TEAL (
npm run compile-contract) i wygenerowania klientów (npm run generate-clientlubnpm run build) przed publikacją pakietów. - Funkcje pomocnicze off-chain lub symulacje szacujące liczbę mintowanych tokenów LP muszą używać wzoru
delta * D / (D + Lu), aby zachować zgodność z łańcuchem. - Pule wdrożone w wersji
BIATEC-CLAMM-01-06-04lub wcześniejszej powinny zostać zaktualizowane; płynność osierocona wcześniej w takich pulach nie jest odzyskiwana automatycznie przez samą aktualizację. - Pełny wieloepokowy dowód matematyczny, konkretne przykłady liczbowe oraz pokrycie w zestawach testów dla wielu podmiotów (
__test__/pool/multi-party-fee-aggregation.test.ts) znajdują się w dokumencie Agregacja płynności wielu podmiotów i dowód uczciwości opłat.