Skip to main content
Halaman ini diterjemahkan secara otomatis oleh AI. Versi bahasa Inggris adalah acuan resmi.Lihat versi bahasa Inggris →

Representasi sqrt-price

CLMM menyimpan harga sebagai sqrt_price_x64 — akar kuadrat dari harga token1-per-token0, sebagai bilangan fixed-point Q64.64: sqrt_price_x64=⌊p⋅264⌋\text{sqrt\_price\_x64} = \lfloor \sqrt{p} \cdot 2^{64} \rfloor di mana p = token1_amount / token0_amount. Bekerja dalam sqrt alih-alih p melinearkan matematika swap (delta jumlah token menjadi linear dalam Δsqrt_price), dan fixed-point x64 mempertahankan presisi melalui swap multi-tick. Konversi tick ↔ sqrt-price diperhitungkan sebelumnya melalui pendekatan log bit-by-bit: sqrt_price_x64(t)≈264⋅(1.0001)t/2\text{sqrt\_price\_x64}(t) \approx 2^{64} \cdot (1.0001)^{t/2} diimplementasikan sebagai eksponensial berbasis lookup dalam tick_math::get_sqrt_price_at_tick.

Likuiditas sebagai unit kanonik

Di dalam rentang [sqrt_a, sqrt_b] (dengan sqrt_a < sqrt_b) sebuah posisi dengan likuiditas L memetakan ke jumlah token sebagai berikut. Misalkan sqrt_c = sqrt_price_x64 adalah harga saat ini pool. Ketiga identitas berasal dari invarian x = L / sqrt_p, y = L · sqrt_p yang dipenuhi likuiditas terkonsentrasi dalam sebuah rentang. Integrator biasanya menginginkan kebalikannya: diberikan deposit amount0 / amount1, hitung L maksimum yang sesuai dalam rentang. SDK LiquidityMath.getLiquidityFromTokenAmounts melakukan ini. Rumus untuk kasus dalam-rentang: 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) Sisi mana pun yang mengikat menentukan rasio yang benar-benar dikonsumsi; sisi lain mungkin memiliki sisa.

Langkah swap satu tick

Swap berlangsung dalam langkah-langkah. Setiap langkah baik (a) mengonsumsi semua input yang tersedia dalam rentang tick saat ini tanpa melewati tick, atau (b) memindahkan harga tepat ke tick yang diinisialisasi berikutnya. Diberikan keadaan saat ini (sqrt_c, L) dan swap turun (token0 masuk, token1 keluar, sqrt_price menurun — harga dikutip sebagai token1/token0, jadi menambah token0 menurunkannya), tick yang diinisialisasi berikutnya di bawah berada di sqrt_t < sqrt_c. Di dalam micro-interval ini hubungan antara input dan harga adalah: Δ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}} dan Δamount1=L⋅(sqrt_c−sqrt_t)\Delta\text{amount1} = L \cdot (\text{sqrt\_c} - \text{sqrt\_t}) Swap token1-in adalah citra cermin: sqrt_price naik menuju tick berikutnya di atas, dan kedua rumus menukar titik akhir mana yang dikurangi. Program mengenkode arah sebagai zero_for_one (true ketika mint input adalah token_mint_0), dan swap zero_for_one menjepit menuju MIN_SQRT_PRICE_X64. Program melakukan salah satu dari dua hal:
  • Apakah seluruh input sesuai? Jika input yang tersisa (setelah biaya) kurang dari Δamount0 untuk mencapai sqrt_t, selesaikan sqrt_c' baru dengan tepat: 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}} (untuk swap exact-input token0 → token1). Swap selesai dalam langkah ini tanpa melewati tick.
  • Input melebihi Δamount0? Atur sqrt_c' = sqrt_t, lewati tick (terapkan liquidity_net), kurangi input yang tersisa dengan Δamount0, tambah output dengan Δamount1, dan ulangi.
Untuk arah sebaliknya (token1 → token0, harga turun), rumusnya memiliki sqrt_c dan sqrt_t ditukar dan inversi di slot lain. Implementasi Rust lengkap berada di raydium-clmm/programs/amm/src/libraries/swap_math.rs. Logika di sana cocok dengan SwapMath.computeSwapStep Uniswap v3 satu-ke-satu.

Biaya pada setiap langkah

Biaya perdagangan diambil dari jumlah input dalam setiap langkah, konvensi yang sama dengan CPMM:
Porsi LP dibagi di seluruh likuiditas yang saat ini dalam-rentang dengan memperbarui akumulator pertumbuhan biaya global: fee_growth_globalin+=lp_portion⋅264L\text{fee\_growth\_global}_{\text{in}} \mathrel{+}= \text{lp\_portion} \cdot \frac{2^{64}}{L} — yaitu, itu didenominasikan dalam biaya per unit likuiditas, Q64.64, sehingga posisi ukuran L_i yang tetap dalam-rentang di seluruh swap ini akan kemudian membaca kembali L_i · Δfee_growth_global / 2^{64} token yang dihutangkan. Porsi protokol dan dana terakumulasi masing-masing ke PoolState.protocol_fees_token_{0,1} dan PoolState.fund_fees_token_{0,1}, identik dengan CPMM. Mereka disapu oleh CollectProtocolFee / CollectFundFee.

Pertumbuhan biaya di luar dan di dalam

Bagian rumit dari akuntansi biaya CLMM: posisi memperoleh biaya hanya saat harga pool berada di dalam rentangnya. Pool melacak biaya kumulatif secara global; posisi perlu mengetahui biaya kumulatif saat berada di dalam rentang spesifiknya. Solusinya adalah akumulator berbasis tick. Setiap tick menyimpan:
Pada saat inisialisasi tick:
  • Jika harga pool berada di atas tick ini (tick_current >= this_tick), fee_growth_outside = fee_growth_global. (Semua yang diperoleh sejauh ini adalah “di luar” — yaitu, di bawah — tick ini, relatif terhadap harga saat ini.)
  • Jika tidak fee_growth_outside = 0.
Ketika harga melewati tick, program membalik fee_growth_outside tick itu: fee_growth_outside←fee_growth_global−fee_growth_outside\text{fee\_growth\_outside} \gets \text{fee\_growth\_global} - \text{fee\_growth\_outside} Invarian yang dipertahankan ini: untuk tick t apa pun, fee_growth_outside(t) sama dengan biaya yang terakumulasi saat tick_current berada di sisi berlawanan dari t. Pertumbuhan biaya di dalam rentang [tick_lower, tick_upper] kemudian diturunkan:
Ini adalah rumus pertumbuhan biaya Uniswap-v3, tidak berubah.

Apa yang disimpan posisi dan apa yang dibacanya

PersonalPositionState menyimpan fee_growth_inside_0_last_x64 dan fee_growth_inside_1_last_x64: nilai fee_growth_inside pada waktu terakhir posisi disentuh. Pada sentuhan berikutnya (tambah, kurangi, kumpulkan), program:
  1. Menghitung fee_growth_inside_{0,1}_x64 saat ini menggunakan rumus di atas.
  2. Menghitung Δ = fee_growth_inside_now − fee_growth_inside_last (pengurangan modular pada u128).
  3. Menambahkan Δ × position.liquidity / 2^{64} ke tokens_fees_owed_{0,1}.
  4. Memperbarui fee_growth_inside_last ke nilai baru.
Token benar-benar keluar dari vault hanya pada CollectFees / DecreaseLiquidity, terhadap tokens_fees_owed.

Reward

Setiap dari hingga 3 aliran reward pool menggunakan mesin pertumbuhan-dalam yang sama, dalam akumulatornya sendiri reward_growth_global_x64. Pada waktu emisi: 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} — emisi berskala terbalik dengan likuiditas aktif, jadi pool yang lebih padat membayar setiap posisi secara proporsional lebih sedikit per detik, tetapi di atas lebih banyak posisi total. Reward per-posisi yang dihutangkan adalah 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} dan dibayarkan oleh DecreaseLiquidity / DecreaseLiquidityV2 — tidak ada instruksi kumpul-reward mandiri. Lihat products/clmm/fees.

Contoh kerja: swap exact-input

Misalkan:
  • tick_spacing = 60
  • sqrt_price_x64 = 1 × 2^{64} — harga = 1.0, jadi tick_current = 0.
  • Likuiditas aktif L = 1_000_000 × 2^{64}.
  • Tick yang diinisialisasi berikutnya di bawah: t = −60 (sqrt_price_b ≈ 0.997005 × 2^{64}). Swap token0-in bergerak turun, jadi tick di atas tidak relevan di sini.
  • Tingkat biaya perdagangan: 500 (0,05%).
Pengguna: SwapBaseInput exact-input 1.000 token0. Langkah 1 — biaya:
Langkah 2 — apakah 999 sesuai dalam rentang tick saat ini?
999 < 3004.4, jadi seluruh input sesuai tanpa melewati tick. Langkah 3 — harga baru:
yaitu sqrt_c' mendarat sedikit di bawah sqrt_c, yang merupakan arah yang benar: swap token0 → token1 menambah token0 ke pool dan dengan demikian menurunkan harga token1/token0. Langkah 4 — jumlah keluar:
Setelah memperhitungkan pembulatan, pengguna menerima ≈ 998 token1. Biaya (1 token0) dibagi antara LP, protokol, dan dana oleh trade_fee_rate × protocol_fee_rate / 1e6 (dan serupa untuk dana); porsi LP mengalir ke fee_growth_global_0_x64.

Pencocokan limit order selama swap

Ketika langkah swap melewati tick yang memegang limit order terbuka, order tersebut mengonsumsi input swap sebelum kurva LP melakukannya, pada harga tepat tick. Pencocokan adalah FIFO dalam tick menurut kohort order_phase.

Status per-kohort pada TickState

Tata letak dua-kohort ada karena order baru dapat dibuka pada tick sementara kohort yang lebih lama masih diisi. Order yang baru dibuka bergabung dengan orders_amount dan mewarisi order_phase berikutnya; mereka tidak dapat diisi sampai kohort sebelumnya sepenuhnya dikonsumsi.

Langkah pencocokan

Pseudo-code untuk pencocokan yang terjadi pada setiap penyeberangan tick selama swap:
Token output yang pergi ke pemilik limit order tidak ditransfer per swap. Mereka duduk secara virtual di vault output pool sampai pemilik order memanggil SettleLimitOrder (atau DecreaseLimitOrder). Pool hanya melacak berapa banyak kohort yang sekarang terisi melalui unfilled_ratio_x64. Setiap LimitOrderState menyimpan snapshot (order_phase, unfilled_ratio_x64) miliknya sendiri pada waktu pembukaan, jadi penyelesaian berkurang menjadi:
Penyelesaian O(1) ini adalah seluruh poin desain kohort — tick dapat mengisi order dalam jumlah arbitrer tanpa gas per-order.

Interaksi dengan kurva LP

Dalam langkah swap, pencocokan limit order terjadi pada tick (zero Δsqrt_price); konsumsi kurva LP terjadi antara tick. Urutannya adalah:
  1. Lewati tick t_cross (terapkan perubahan LP liquidity_net terlebih dahulu, karena ini adalah cara Uniswap-V3 melakukannya).
  2. Isi limit order apa pun yang duduk di t_cross.
  3. Lanjutkan di sepanjang kurva LP ke tick yang diinisialisasi berikutnya atau ke kelelahan swap_input.
Limit order dengan demikian memberikan trader lebih banyak likuiditas efektif tepat pada harga tick order (efek peningkatan harga), dengan biaya LP tidak memperoleh biaya pada bagian volume swap itu — bagian limit-order dari perdagangan adalah bebas biaya untuk swapper, karena pemenempatnya bertindak sebagai pembuat. Surcharge biaya dinamis (jika diaktifkan) masih berlaku untuk bagian LP dari swap yang sama.

Derivasi biaya dinamis

PoolState.dynamic_fee_info membawa keadaan volatilitas. Setiap langkah swap menghitung tingkat biaya per-langkah sebagai: fee_ratetotal=trade_fee_rateconfig+dynamic_fee_control⋅(vol_acc⋅tick_spacing)2Dctrl⋅Svol2⏟surcharge dinamis\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{surcharge dinamis}} di mana:
  • Dctrl=100,000D_{\text{ctrl}} = 100{,}000 — DYNAMIC_FEE_CONTROL_DENOMINATOR
  • Svol=10,000S_{\text{vol}} = 10{,}000 — VOLATILITY_ACCUMULATOR_SCALE
  • vol_acc adalah akumulator per-swap setelah aturan pembaruan di bawah
  • tick_spacing berasal dari PoolState.tick_spacing
Hasilnya dijepit pada 100,000/106=10%100{,}000 / 10^6 = 10\%.

Pembaruan akumulator

Dua aturan diterapkan setiap swap, secara berurutan: Peluruhan. Lantai referensi meluruh berdasarkan waktu sejak pembaruan terakhir: vol_ref={0jika Δt>decay_periodvol_accprev⋅reduction_factor10,000jika filter_period<Δt≤decay_periodvol_refprevjika Δt≤filter_period\text{vol\_ref} = \begin{cases} 0 & \text{jika } \Delta t > \text{decay\_period} \\ \text{vol\_acc}_{\text{prev}} \cdot \dfrac{\text{reduction\_factor}}{10{,}000} & \text{jika } \text{filter\_period} < \Delta t \le \text{decay\_period} \\ \text{vol\_ref}_{\text{prev}} & \text{jika } \Delta t \le \text{filter\_period} \end{cases} Akumulasi. Akumulator baru adalah referensi ditambah jarak tick yang ditempuh sejak indeks referensi sebelumnya: 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}}) dalam unit tick-spacing, bukan tick mentah: tref=⌊tick_current/tick_spacing⌋t_{\text{ref}} = \lfloor \text{tick\_current} / \text{tick\_spacing} \rfloor.

Mengapa parabolik dalam jarak tick

Mengkuadratkan akumulator berarti biaya naik sebagai kuadrat seberapa jauh harga telah berjalan dari titik referensinya. Secara empiris ini cocok dengan penskalaan varians harga di bawah tekanan random-walk: eksursi tick 2× menyiratkan volatilitas tersirat 4×, jadi mengenakan surcharge 4×. Parameter dynamic_fee_control mengkalibrasi tingkat absolut. Jendela filter_period mencegah osilasi sub-detik kecil (misalnya, bot MEV sandwiching) dari menginflasi akumulator. Jendela decay_period mencegah lonjakan masa lalu tunggal dari mengenakan biaya tanpa batas setelah pasar telah tenang.

Ketangguhan numerik

  • Semua produk perantara melalui aritmetika berbentuk u128 atau u256. CLMM menggunakan pembantu U128Sqrt dan pola FullMath::mulDiv yang langsung diportir dari Uniswap v3.
  • Pembulatan pembagian dipilih per-langkah untuk menegakkan invarian k' ≥ k secara lokal. SwapBaseInput membulatkan output ke bawah; SwapBaseOutput membulatkan input ke atas.
  • Penyeberangan tick yang menjatuhkan PoolState.liquidity ke nol diizinkan (harga dapat melintasi “lubang likuiditas”) tetapi swap hanya maju ke tick yang diinisialisasi berikutnya tanpa mengonsumsi input, tidak mengenakan biaya.
  • Penjaga overflow: sqrt_price_x64 disimpan dalam rentang inklusif [MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64] yang sesuai dengan [MIN_TICK, MAX_TICK]. Swap yang akan mendorong melampaui batas mana pun kembali dengan SqrtPriceLimitOverflow.

Ke mana selanjutnya

Sumber: