Skip to main content
Diese Seite wurde mit KI automatisch übersetzt. Maßgeblich ist stets die englische Version.Englische Version ansehen →

Sqrt-Preis-Darstellung

CLMM speichert den Preis als sqrt_price_x64 — die Quadratwurzel des Token1-pro-Token0-Preises, als Q64.64-Festkommazahl: sqrt_price_x64=⌊p⋅264⌋\text{sqrt\_price\_x64} = \lfloor \sqrt{p} \cdot 2^{64} \rfloor wobei p = token1_amount / token0_amount. Die Arbeit mit sqrt statt p linearisiert die Swap-Mathematik (Token-Mengen-Deltas werden linear in Δsqrt_price), und die x64-Festkommadarstellung erhält die Präzision über viele-Tick-Swaps hinweg. Die Tick ↔ Sqrt-Preis-Konvertierung wird über eine Bit-für-Bit-Logarithmus-Approximation vorberechnet: sqrt_price_x64(t)≈264⋅(1.0001)t/2\text{sqrt\_price\_x64}(t) \approx 2^{64} \cdot (1.0001)^{t/2} implementiert als nachschlagbasierte Exponentiation in tick_math::get_sqrt_price_at_tick.

Liquidität als kanonische Einheit

Innerhalb eines Bereichs [sqrt_a, sqrt_b] (mit sqrt_a < sqrt_b) ordnet sich eine Position mit Liquidität L wie folgt Token-Mengen zu. Sei sqrt_c = sqrt_price_x64 der aktuelle Preis des Pools. Alle drei Identitäten folgen aus der Invariante x = L / sqrt_p, y = L · sqrt_p, die konzentrierte Liquidität innerhalb eines Bereichs erfüllt. Integratoren wünschen sich typischerweise die Umkehrung: Gegeben eine Einzahlung von amount0 / amount1, berechne das maximale L, das in den Bereich passt. Das SDK’s LiquidityMath.getLiquidityFromTokenAmounts macht dies. Die Formel für den In-Bereich-Fall: L0=amount0⋅sqrt_c⋅sqrt_bsqrt_b−sqrt_c,L1=amount1sqrt_c−sqrt_a,L=min⁡(L0,L1)L_0 = \text{amount0} \cdot \frac{\text{sqrt\_c} \cdot \text{sqrt\_b}}{\text{sqrt\_b} - \text{sqrt\_c}}, \qquad L_1 = \frac{\text{amount1}}{\text{sqrt\_c} - \text{sqrt\_a}}, \qquad L = \min(L_0, L_1) Welche Seite bindet, bestimmt das tatsächlich verbrauchte Verhältnis; die andere Seite kann Reste haben.

Single-Tick-Swap-Schritt

Ein Swap verläuft in Schritten. Jeder Schritt entweder (a) verbraucht die gesamte verfügbare Eingabe innerhalb des aktuellen Tick-Bereichs, ohne einen Tick zu kreuzen, oder (b) bewegt den Preis genau zum nächsten initialisierten Tick. Gegeben der aktuelle Zustand (sqrt_c, L) und ein Swap nach unten (Token0 rein, Token1 raus, sqrt_price sinkt — Preis wird als token1/token0 notiert, also senkt das Hinzufügen von Token0 ihn), sitzt der nächste initialisierte Tick darunter bei sqrt_t < sqrt_c. Innerhalb dieses Mikro-Intervalls ist die Beziehung zwischen Eingabe und Preis: Δamount0=L⋅(1sqrt_t−1sqrt_c)=L⋅(sqrt_c−sqrt_t)sqrt_c⋅sqrt_t\Delta\text{amount0} = L \cdot \left( \frac{1}{\text{sqrt\_t}} - \frac{1}{\text{sqrt\_c}} \right) = \frac{L \cdot (\text{sqrt\_c} - \text{sqrt\_t})}{\text{sqrt\_c} \cdot \text{sqrt\_t}} und Δamount1=L⋅(sqrt_c−sqrt_t)\Delta\text{amount1} = L \cdot (\text{sqrt\_c} - \text{sqrt\_t}) Ein Token1-Eingabe-Swap ist das Spiegelbild: sqrt_price steigt zum nächsten Tick oben, und die beiden Formeln tauschen, welcher Endpunkt subtrahiert wird. Das Programm kodiert die Richtung als zero_for_one (true, wenn die Eingabe-Mint token_mint_0 ist), und ein zero_for_one-Swap klemmt gegen MIN_SQRT_PRICE_X64. Das Programm macht eines von zwei Dingen:
  • Passt die gesamte Eingabe? Wenn die verbleibende Eingabe (nach Gebühr) kleiner als Δamount0 ist, um sqrt_t zu erreichen, löse für das neue sqrt_c' exakt: sqrt_c′=L⋅sqrt_cL+Δinput⋅sqrt_c\text{sqrt\_c}' = \frac{L \cdot \text{sqrt\_c}}{L + \Delta\text{input} \cdot \text{sqrt\_c}} (für einen exakten-Eingabe token0 → token1 Swap). Der Swap endet in diesem Schritt, ohne einen Tick zu kreuzen.
  • Eingabe übersteigt Δamount0? Setze sqrt_c' = sqrt_t, kreuze den Tick (wende liquidity_net an), dekrementiere verbleibende Eingabe um Δamount0, inkrementiere Ausgabe um Δamount1, und wiederhole.
Für die entgegengesetzte Richtung (token1 → token0, Preis geht nach unten), haben die Formeln sqrt_c und sqrt_t getauscht und die Inversion im anderen Slot. Die vollständige Rust-Implementierung lebt in raydium-clmm/programs/amm/src/libraries/swap_math.rs. Die Logik dort entspricht Uniswap v3’s SwapMath.computeSwapStep eins-zu-eins.

Gebühren bei jedem Schritt

Handelsgebühren werden von der Eingabe-Menge in jedem Schritt abgezogen, gleiche Konvention wie CPMM:
Der LP-Anteil wird über die aktuell-im-Bereich-Liquidität aufgeteilt, indem der globale Gebühren-Wachstums-Akkumulator aktualisiert wird: fee_growth_globalin+=lp_portion⋅264L\text{fee\_growth\_global}_{\text{in}} \mathrel{+}= \text{lp\_portion} \cdot \frac{2^{64}}{L} — d.h., er wird in Gebühren pro Liquiditätseinheit denominiert, Q64.64, sodass eine Position der Größe L_i, die über diesen Swap im Bereich blieb, später L_i · Δfee_growth_global / 2^{64} geschuldete Token zurückliest. Die Protokoll- und Fund-Anteile sammeln sich in PoolState.protocol_fees_token_{0,1} und PoolState.fund_fees_token_{0,1} an, identisch mit CPMM. Sie werden durch CollectProtocolFee / CollectFundFee eingezogen.

Gebühren-Wachstum außerhalb und innerhalb

Der knifflige Teil der CLMM-Gebühren-Buchhaltung: Eine Position verdient Gebühren nur, während der Pool-Preis innerhalb ihres Bereichs ist. Der Pool verfolgt kumulative Gebühren global; die Position muss die kumulativen Gebühren während sie in ihrem spezifischen Bereich ist kennen. Die Lösung ist ein Tick-basierter Akkumulator. Jeder Tick speichert:
Im Moment der Tick-Initialisierung:
  • Wenn der Pool-Preis über diesem Tick ist (tick_current >= this_tick), ist fee_growth_outside = fee_growth_global. (Alles bisher Verdiente ist „außerhalb” — d.h., unterhalb — dieses Ticks, relativ zum aktuellen Preis.)
  • Sonst fee_growth_outside = 0.
Wenn der Preis einen Tick kreuzt, flippt das Programm den fee_growth_outside dieses Ticks: fee_growth_outside←fee_growth_global−fee_growth_outside\text{fee\_growth\_outside} \gets \text{fee\_growth\_global} - \text{fee\_growth\_outside} Die Invariante, die dies bewahrt: Für jeden Tick t ist fee_growth_outside(t) gleich den Gebühren, die aufgelaufen sind, während tick_current auf der entgegengesetzten Seite von t war. Gebühren-Wachstum innerhalb eines Bereichs [tick_lower, tick_upper] wird dann abgeleitet:
Dies ist die Uniswap-v3-Gebühren-Wachstums-Formel, unverändert.

Was eine Position speichert und was sie liest

Ein PersonalPositionState speichert fee_growth_inside_0_last_x64 und fee_growth_inside_1_last_x64: die fee_growth_inside-Werte beim letzten Mal, als die Position berührt wurde. Bei jeder nachfolgenden Berührung (Erhöhung, Verringerung, Einzug) macht das Programm:
  1. Berechnet den aktuellen fee_growth_inside_{0,1}_x64 mit der obigen Formel.
  2. Berechnet Δ = fee_growth_inside_now − fee_growth_inside_last (Modular-Subtraktion auf u128).
  3. Addiert Δ × position.liquidity / 2^{64} zu tokens_fees_owed_{0,1}.
  4. Aktualisiert fee_growth_inside_last auf den neuen Wert.
Token bewegen sich tatsächlich nur aus den Tresoren bei CollectFees / DecreaseLiquidity, gegen tokens_fees_owed.

Belohnungen

Jeder der bis zu 3 Belohnungsströme des Pools verwendet die gleiche Growth-Inside-Maschinerie, in seinem eigenen reward_growth_global_x64-Akkumulator. Zur Emissionszeit: reward_growth_global+=emission_per_second⋅Δt⋅264L\text{reward\_growth\_global} \mathrel{+}= \text{emission\_per\_second} \cdot \Delta t \cdot \frac{2^{64}}{L} — Emissionen skalieren umgekehrt mit aktiver Liquidität, sodass ein dichterer Pool jeder Position proportional weniger pro Sekunde zahlt, aber über mehr Positionen insgesamt. Die pro-Position geschuldete Belohnung ist reward_owed=(reward_growth_insidenow−reward_growth_insidelast)⋅L/264\text{reward\_owed} = (\text{reward\_growth\_inside}_{\text{now}} - \text{reward\_growth\_inside}_{\text{last}}) \cdot L / 2^{64} und wird durch DecreaseLiquidity / DecreaseLiquidityV2 ausgezahlt — es gibt keine eigenständige Collect-Reward-Anweisung. Siehe /de/products/clmm/fees.

Durchgerechnetes Beispiel: Exakte-Eingabe-Swap

Angenommen:
  • tick_spacing = 60
  • sqrt_price_x64 = 1 × 2^{64} — Preis = 1,0, also tick_current = 0.
  • Aktive Liquidität L = 1_000_000 × 2^{64}.
  • Nächster initialisierter Tick darunter: t = −60 (sqrt_price_b ≈ 0.997005 × 2^{64}). Ein Token0-Eingabe-Swap bewegt sich nach unten, also ist der Tick oben hier irrelevant.
  • Handelsgebührsatz: 500 (0,05%).
Benutzer: SwapBaseInput exakte Eingabe 1.000 Token0. Schritt 1 — Gebühren:
Schritt 2 — passt 999 innerhalb des aktuellen Tick-Bereichs?
999 < 3004.4, also passt die gesamte Eingabe ohne Tick-Kreuzen. Schritt 3 — neuer Preis:
d.h. sqrt_c' landet leicht unter sqrt_c, was die richtige Richtung ist: Ein token0 → token1 Swap fügt Token0 zum Pool hinzu und senkt daher den token1/token0-Preis. Schritt 4 — Ausgabemenge:
Nach Berücksichtigung von Rundungen erhält der Benutzer ≈ 998 Token1. Die Gebühr (1 Token0) wird zwischen LP, Protokoll und Fund durch trade_fee_rate × protocol_fee_rate / 1e6 (und ähnlich für Fund) aufgeteilt; der LP-Anteil fließt in fee_growth_global_0_x64.

Limit-Order-Matching während Swap

Wenn ein Swap-Schritt einen Tick kreuzt, der offene Limit-Orders hält, verbrauchen diese Orders Swap-Eingabe vor der LP-Kurve, zum exakten Preis des Ticks. Das Matching ist FIFO innerhalb des Ticks nach order_phase-Kohorte.

Pro-Kohorte-Zustand auf TickState

Das Zwei-Kohorten-Layout existiert, weil neue Orders auf einem Tick während eine ältere Kohorte noch gefüllt wird, geöffnet werden können. Neu geöffnete Orders treten orders_amount bei und erben die nächste order_phase; sie können nicht füllen, bis die vorherige Kohorte vollständig verbraucht ist.

Matching-Schritt

Pseudo-Code für das Matching, das bei jedem Tick-Kreuzen während eines Swaps passiert:
Ausgabe-Token, die an die Limit-Order-Besitzer gehen, werden nicht pro Swap übertragen. Sie sitzen virtuell im Ausgabe-Tresor des Pools, bis der Order-Besitzer SettleLimitOrder (oder DecreaseLimitOrder) aufruft. Der Pool verfolgt einfach, wie viel der Kohorte jetzt gefüllt ist, über unfilled_ratio_x64. Jeder LimitOrderState speichert seinen eigenen (order_phase, unfilled_ratio_x64)-Snapshot zur Öffnungszeit, sodass die Abwicklung sich reduziert auf:
Diese O(1)-Abwicklung ist der ganze Punkt des Kohorten-Designs — ein Tick kann beliebig viele Orders füllen, ohne Pro-Order-Gas.

Interaktion mit der LP-Kurve

In einem Swap-Schritt passiert Limit-Order-Matching am Tick (Null Δsqrt_price); LP-Kurven-Verbrauch passiert zwischen Ticks. Die Reihenfolge ist daher:
  1. Kreuze Tick t_cross (wende LP liquidity_net-Änderung zuerst an, da dies wie Uniswap-V3 es macht).
  2. Fülle alle Limit-Orders, die bei t_cross sitzen.
  3. Fahre entlang der LP-Kurve zum nächsten initialisierten Tick oder bis swap_input-Erschöpfung fort.
Limit-Orders geben Händlern daher mehr effektive Liquidität genau zum Order-Tick-Preis (ein Preis-Verbesserungs-Effekt), auf Kosten von LPs, die keine Gebühren auf diesem Portion des Swap-Volumens verdienen — der Limit-Order-Portion des Handels ist gebührenfrei für den Swapper, da der Limit-Order-Placer als Maker agiert. Der dynamische Gebühren-Zuschlag (falls aktiviert) gilt immer noch für den LP-Portion des gleichen Swaps.

Dynamische Gebühren-Ableitung

PoolState.dynamic_fee_info trägt den Volatilitätszustand. Jeder Swap-Schritt berechnet den Pro-Schritt-Gebührensatz als: fee_ratetotal=trade_fee_rateconfig+dynamic_fee_control⋅(vol_acc⋅tick_spacing)2Dctrl⋅Svol2⏟dynamischer Zuschlag\text{fee\_rate}_{\text{total}} = \text{trade\_fee\_rate}_{\text{config}} + \underbrace{\frac{\text{dynamic\_fee\_control} \cdot (\text{vol\_acc} \cdot \text{tick\_spacing})^2} {D_{\text{ctrl}} \cdot S_{\text{vol}}^2}}_{\text{dynamischer Zuschlag}} wobei:
  • Dctrl=100,000D_{\text{ctrl}} = 100{,}000 — DYNAMIC_FEE_CONTROL_DENOMINATOR
  • Svol=10,000S_{\text{vol}} = 10{,}000 — VOLATILITY_ACCUMULATOR_SCALE
  • vol_acc ist der Pro-Swap-Akkumulator nach der Aktualisierungsregel unten
  • tick_spacing ist von PoolState.tick_spacing
Das Ergebnis wird bei 100,000/106=10%100{,}000 / 10^6 = 10\% geklemmt.

Akkumulator-Aktualisierung

Zwei Regeln werden bei jedem Swap angewendet, in Reihenfolge: Verfall. Der Referenz-Boden verfällt basierend auf der Zeit seit der letzten Aktualisierung: vol_ref={0wenn Δt>decay_periodvol_accprev⋅reduction_factor10,000wenn filter_period<Δt≤decay_periodvol_refprevwenn Δt≤filter_period\text{vol\_ref} = \begin{cases} 0 & \text{wenn } \Delta t > \text{decay\_period} \\ \text{vol\_acc}_{\text{prev}} \cdot \dfrac{\text{reduction\_factor}}{10{,}000} & \text{wenn } \text{filter\_period} < \Delta t \le \text{decay\_period} \\ \text{vol\_ref}_{\text{prev}} & \text{wenn } \Delta t \le \text{filter\_period} \end{cases} Akkumulieren. Der neue Akkumulator ist die Referenz plus Tick-Distanz, die seit dem vorherigen Referenz-Index durchquert wurde: vol_acc=min⁡(vol_ref+∣tref−tnow∣⋅Svol,max_vol_acc)\text{vol\_acc} = \min\left( \text{vol\_ref} + \left| t_{\text{ref}} - t_{\text{now}} \right| \cdot S_{\text{vol}}, \text{max\_vol\_acc} \right) tick_spacing_index_reference (treft_{\text{ref}}) ist in Tick-Spacing-Einheiten, nicht rohen Ticks: tref=⌊tick_current/tick_spacing⌋t_{\text{ref}} = \lfloor \text{tick\_current} / \text{tick\_spacing} \rfloor.

Warum parabolisch in Tick-Distanz

Das Quadrieren des Akkumulators bedeutet, dass die Gebühr als das Quadrat steigt, wie weit der Preis von seinem Referenzpunkt entfernt gelaufen ist. Empirisch entspricht dies der Varianz-Skalierung des Preises unter Random-Walk-Druck: Eine 2×-Tick-Ausflug impliziert 4× die implizite Volatilität, also berechnet 4× den Zuschlag. Der dynamic_fee_control-Parameter kalibriert das absolute Niveau. Das filter_period-Fenster verhindert, dass winzige Sub-Sekunden-Oszillationen (z.B. MEV-Bots, die Sandwiching betreiben) den Akkumulator aufblähen. Das decay_period-Fenster verhindert, dass ein einzelner vergangener Spike Gebühren auf unbestimmte Zeit berechnet, nachdem der Markt sich beruhigt hat.

Numerische Robustheit

  • Alle Zwischenprodukte gehen durch u128- oder u256-förmige Arithmetik. CLMM verwendet U128Sqrt-Helfer und FullMath::mulDiv-Muster, direkt von Uniswap v3 portiert.
  • Divisions-Rundung wird pro-Schritt gewählt, um die Invariante k' ≥ k lokal durchzusetzen. SwapBaseInput rundet Ausgabe ab; SwapBaseOutput rundet Eingabe auf.
  • Tick-Kreuze, die PoolState.liquidity auf Null senken, sind erlaubt (der Preis kann ein „Liquiditätsloch” durchqueren), aber der Swap rückt einfach zum nächsten initialisierten Tick vor, ohne Eingabe zu verbrauchen, und berechnet keine Gebühr.
  • Overflow-Schutz: sqrt_price_x64 wird im inklusiven Bereich [MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64] gehalten, entsprechend [MIN_TICK, MAX_TICK]. Ein Swap, der eine Grenze überschreiten würde, wird mit SqrtPriceLimitOverflow zurückgewiesen.

Wo es weitergeht

Quellen: