Skip to main content
Trang này được dịch tự động bằng AI. Phiên bản tiếng Anh là bản chính thức.Xem bản tiếng Anh →

Biểu diễn sqrt-price

CLMM lưu trữ giá dưới dạng sqrt_price_x64 — căn bậc hai của giá token1-trên-token0, được biểu diễn dưới dạng số điểm cố định Q64.64: sqrt_price_x64=⌊p⋅264⌋\text{sqrt\_price\_x64} = \lfloor \sqrt{p} \cdot 2^{64} \rfloor trong đó p = token1_amount / token0_amount. Làm việc với sqrt thay vì p tuyến tính hóa toán học swap (các delta lượng token trở thành tuyến tính trong Δsqrt_price), và điểm cố định x64 duy trì độ chính xác qua các swap nhiều tick. Chuyển đổi Tick ↔ sqrt-price được tính toán trước thông qua xấp xỉ 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} được triển khai dưới dạng phép lũy thừa dựa trên bảng tra cứu trong tick_math::get_sqrt_price_at_tick.

Liquidity như một đơn vị chính tắc

Bên trong một phạm vi [sqrt_a, sqrt_b] (với sqrt_a < sqrt_b), một vị trí có liquidity L ánh xạ tới các lượng token như sau. Gọi sqrt_c = sqrt_price_x64 là giá hiện tại của pool. Cả ba đẳng thức đều xuất phát từ bất biến x = L / sqrt_p, y = L · sqrt_p mà liquidity tập trung thỏa mãn trong một phạm vi. Các nhà tích hợp thường muốn phép nghịch đảo: cho một khoản gửi amount0 / amount1, tính toán L tối đa phù hợp với phạm vi. SDK của Raydium LiquidityMath.getLiquidityFromTokenAmounts thực hiện điều này. Công thức cho trường hợp trong phạm vi: 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) Bên nào ràng buộc sẽ xác định tỷ lệ thực tế được tiêu thụ; bên kia có thể có phần dư.

Bước swap đơn tick

Một swap diễn ra theo các bước. Mỗi bước hoặc (a) tiêu thụ tất cả input có sẵn trong phạm vi tick hiện tại mà không vượt qua một tick, hoặc (b) di chuyển giá chính xác tới tick được khởi tạo tiếp theo. Cho trạng thái hiện tại (sqrt_c, L) và một swap xuống (token0 vào, token1 ra, sqrt_price giảm — giá được trích dẫn là token1/token0, vì vậy thêm token0 làm giảm nó), tick được khởi tạo tiếp theo bên dưới nằm ở sqrt_t < sqrt_c. Bên trong khoảng vi mô này, mối quan hệ giữa input và giá là: Δ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}} và Δamount1=L⋅(sqrt_c−sqrt_t)\Delta\text{amount1} = L \cdot (\text{sqrt\_c} - \text{sqrt\_t}) Một swap token1-vào là hình ảnh phản chiếu: sqrt_price tăng hướng tới tick tiếp theo ở trên, và hai công thức hoán đổi điểm cuối nào bị trừ. Chương trình mã hóa hướng dưới dạng zero_for_one (true khi mint input là token_mint_0), và một swap zero_for_one kẹp hướng tới MIN_SQRT_PRICE_X64. Chương trình thực hiện một trong hai điều:
  • Toàn bộ input có vừa không? Nếu input còn lại (sau phí) nhỏ hơn Δamount0 để đạt sqrt_t, giải quyết cho sqrt_c' chính xác: 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}} (cho một swap exact-input token0 → token1). Swap hoàn thành trong bước này mà không vượt qua một tick.
  • Input vượt quá Δamount0? Đặt sqrt_c' = sqrt_t, vượt qua tick (áp dụng liquidity_net), giảm input còn lại bằng Δamount0, tăng output bằng Δamount1, và lặp lại.
Cho hướng ngược lại (token1 → token0, giá đi xuống), các công thức có sqrt_c và sqrt_t hoán đổi và phép nghịch đảo ở vị trí khác. Triển khai Rust đầy đủ nằm trong raydium-clmm/programs/amm/src/libraries/swap_math.rs. Logic ở đó khớp với SwapMath.computeSwapStep của Uniswap v3 một-một.

Phí trên mỗi bước

Phí giao dịch được lấy từ lượng input trong mỗi bước, cùng quy ước với CPMM:
Phần LP được chia sẻ trên liquidity hiện đang trong phạm vi bằng cách cập nhật bộ tích lũy phí toàn cầu: fee_growth_globalin+=lp_portion⋅264L\text{fee\_growth\_global}_{\text{in}} \mathrel{+}= \text{lp\_portion} \cdot \frac{2^{64}}{L} — tức là, nó được tính bằng phí trên mỗi đơn vị liquidity, Q64.64, sao cho một vị trí có kích thước L_i nằm trong phạm vi qua swap này sẽ sau đó đọc lại L_i · Δfee_growth_global / 2^{64} token nợ. Các phần protocol và fund tích lũy tới PoolState.protocol_fees_token_{0,1} và PoolState.fund_fees_token_{0,1} tương ứng, giống hệt CPMM. Chúng được quét bởi CollectProtocolFee / CollectFundFee.

Phí tăng trưởng bên ngoài và bên trong

Phần phức tạp của kế toán phí CLMM: một vị trí chỉ kiếm phí khi giá pool bên trong phạm vi của nó. Pool theo dõi phí tích lũy toàn cầu; vị trí cần biết phí tích lũy khi bên trong phạm vi cụ thể của nó. Giải pháp là một bộ tích lũy dựa trên tick. Mỗi tick lưu trữ:
Tại thời điểm khởi tạo tick:
  • Nếu giá pool trên tick này (tick_current >= this_tick), fee_growth_outside = fee_growth_global. (Tất cả kiếm được cho đến nay là “bên ngoài” — tức là, bên dưới — tick này, tương đối với giá hiện tại.)
  • Nếu không fee_growth_outside = 0.
Khi giá vượt qua một tick, chương trình lật fee_growth_outside của tick đó: fee_growth_outside←fee_growth_global−fee_growth_outside\text{fee\_growth\_outside} \gets \text{fee\_growth\_global} - \text{fee\_growth\_outside} Bất biến này bảo toàn: với bất kỳ tick t nào, fee_growth_outside(t) bằng phí tích lũy khi tick_current ở phía đối diện của t. Phí tăng trưởng bên trong một phạm vi [tick_lower, tick_upper] sau đó được suy ra:
Đây là công thức phí tăng trưởng của Uniswap-v3, không thay đổi.

Vị trí lưu trữ gì và nó đọc gì

Một PersonalPositionState lưu trữ fee_growth_inside_0_last_x64 và fee_growth_inside_1_last_x64: các giá trị fee_growth_inside tại lần cuối cùng vị trí được chạm vào. Tại bất kỳ lần chạm tiếp theo nào (tăng, giảm, thu thập), chương trình:
  1. Tính toán fee_growth_inside_{0,1}_x64 hiện tại bằng công thức trên.
  2. Tính toán Δ = fee_growth_inside_now − fee_growth_inside_last (phép trừ mô-đun trên u128).
  3. Thêm Δ × position.liquidity / 2^{64} vào tokens_fees_owed_{0,1}.
  4. Cập nhật fee_growth_inside_last thành giá trị mới.
Token thực sự chỉ di chuyển ra khỏi các vault khi CollectFees / DecreaseLiquidity, chống lại tokens_fees_owed.

Phần thưởng

Mỗi trong số tối đa 3 luồng phần thưởng của pool sử dụng cùng một cơ chế growth-inside, trong bộ tích lũy reward_growth_global_x64 riêng của nó. Tại thời điểm phát hành: 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} — phát hành tỷ lệ nghịch với liquidity hoạt động, vì vậy một pool dày đặc trả cho mỗi vị trí ít hơn theo tỷ lệ mỗi giây, nhưng trên nhiều vị trí hơn tổng cộng. Phần thưởng nợ trên mỗi vị trí là 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} và được trả bởi DecreaseLiquidity / DecreaseLiquidityV2 — không có lệnh thu thập phần thưởng độc lập. Xem products/clmm/fees.

Ví dụ đã làm: swap exact-input

Giả sử:
  • tick_spacing = 60
  • sqrt_price_x64 = 1 × 2^{64} — giá = 1.0, vì vậy tick_current = 0.
  • Liquidity hoạt động L = 1_000_000 × 2^{64}.
  • Tick được khởi tạo tiếp theo bên dưới: t = −60 (sqrt_price_b ≈ 0.997005 × 2^{64}). Một swap token0-vào di chuyển xuống, vì vậy tick ở trên không liên quan ở đây.
  • Tỷ lệ phí giao dịch: 500 (0.05%).
Người dùng: SwapBaseInput exact-input 1,000 token0. Bước 1 — phí:
Bước 2 — 999 có vừa trong phạm vi tick hiện tại không?
999 < 3004.4, vì vậy toàn bộ input vừa mà không vượt qua tick. Bước 3 — giá mới:
tức là sqrt_c' hạ cánh hơi dưới sqrt_c, đó là hướng chính xác: một swap token0 → token1 thêm token0 vào pool và do đó làm giảm giá token1/token0. Bước 4 — lượng ra:
Sau khi tính đến làm tròn, người dùng nhận được ≈ 998 token1. Phí (1 token0) được chia giữa LP, protocol, và fund bằng trade_fee_rate × protocol_fee_rate / 1e6 (và tương tự cho fund); phần LP chảy vào fee_growth_global_0_x64.

Khớp lệnh giới hạn trong swap

Khi một bước swap vượt qua một tick chứa các lệnh giới hạn mở, những lệnh đó tiêu thụ input swap trước đường cong LP, ở giá chính xác của tick. Khớp là FIFO trong tick theo cohort order_phase.

Trạng thái mỗi cohort trên TickState

Bố cục hai cohort tồn tại vì các lệnh mới có thể được mở trên một tick trong khi một cohort cũ hơn vẫn đang được điền. Các lệnh mới được mở tham gia orders_amount và kế thừa order_phase tiếp theo; chúng không thể điền cho đến khi cohort trước được tiêu thụ hoàn toàn.

Bước khớp

Mã giả cho khớp xảy ra tại mỗi lần vượt qua tick trong một swap:
Token đầu ra đi tới chủ sở hữu lệnh giới hạn không được chuyển giao mỗi swap. Chúng nằm ảo trong vault đầu ra của pool cho đến khi chủ sở hữu lệnh gọi SettleLimitOrder (hoặc DecreaseLimitOrder). Pool chỉ theo dõi bao nhiêu của cohort hiện được điền thông qua unfilled_ratio_x64. Mỗi LimitOrderState lưu trữ snapshot (order_phase, unfilled_ratio_x64) riêng của nó tại thời điểm mở, vì vậy giải quyết rút gọn thành:
Giải quyết O(1) này là toàn bộ điểm của thiết kế cohort — một tick có thể điền một số lượng tùy ý các lệnh mà không cần gas mỗi lệnh.

Tương tác với đường cong LP

Trong một bước swap, khớp lệnh giới hạn xảy ra tại tick (zero Δsqrt_price); tiêu thụ đường cong LP xảy ra giữa các tick. Thứ tự do đó là:
  1. Vượt qua tick t_cross (áp dụng thay đổi LP liquidity_net trước, vì đây là cách Uniswap-V3 làm).
  2. Điền bất kỳ lệnh giới hạn nào ngồi tại t_cross.
  3. Tiếp tục dọc theo đường cong LP tới tick được khởi tạo tiếp theo hoặc tới sự cạn kiệt swap_input.
Các lệnh giới hạn do đó cung cấp cho các nhà giao dịch nhiều liquidity hiệu quả hơn chính xác ở giá tick của lệnh (hiệu ứng cải thiện giá), với chi phí là LP không kiếm phí trên phần đó của khối lượng swap — phần lệnh giới hạn của giao dịch không có phí cho người swap, vì người đặt lệnh giới hạn đang hoạt động như một maker. Khoản phụ phí phí động (nếu được bật) vẫn áp dụng cho phần LP của cùng một swap.

Suy ra phí động

PoolState.dynamic_fee_info mang trạng thái biến động. Mỗi bước swap tính toán tỷ lệ phí mỗi bước là: fee_ratetotal=trade_fee_rateconfig+dynamic_fee_control⋅(vol_acc⋅tick_spacing)2Dctrl⋅Svol2⏟dynamic surcharge\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{dynamic surcharge}} trong đó:
  • Dctrl=100,000D_{\text{ctrl}} = 100{,}000 — DYNAMIC_FEE_CONTROL_DENOMINATOR
  • Svol=10,000S_{\text{vol}} = 10{,}000 — VOLATILITY_ACCUMULATOR_SCALE
  • vol_acc là bộ tích lũy mỗi swap sau quy tắc cập nhật bên dưới
  • tick_spacing là từ PoolState.tick_spacing
Kết quả được kẹp ở 100,000/106=10%100{,}000 / 10^6 = 10\%.

Cập nhật bộ tích lũy

Hai quy tắc được áp dụng mỗi swap, theo thứ tự: Phân rã. Sàn tham chiếu phân rã dựa trên thời gian kể từ lần cập nhật cuối cùng: vol_ref={0if Δt>decay_periodvol_accprev⋅reduction_factor10,000if filter_period<Δt≤decay_periodvol_refprevif Δt≤filter_period\text{vol\_ref} = \begin{cases} 0 & \text{if } \Delta t > \text{decay\_period} \\ \text{vol\_acc}_{\text{prev}} \cdot \dfrac{\text{reduction\_factor}}{10{,}000} & \text{if } \text{filter\_period} < \Delta t \le \text{decay\_period} \\ \text{vol\_ref}_{\text{prev}} & \text{if } \Delta t \le \text{filter\_period} \end{cases} Tích lũy. Bộ tích lũy mới là tham chiếu cộng với khoảng cách tick đã duyệt kể từ chỉ số tham chiếu trước: 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}}) tính bằng đơn vị tick-spacing, không phải tick thô: tref=⌊tick_current/tick_spacing⌋t_{\text{ref}} = \lfloor \text{tick\_current} / \text{tick\_spacing} \rfloor.

Tại sao parabol trong khoảng cách tick

Bình phương bộ tích lũy có nghĩa là phí tăng lên khi bình phương của khoảng cách giá đã đi từ điểm tham chiếu của nó. Theo kinh nghiệm, điều này phù hợp với tỷ lệ phương sai của giá dưới áp lực bước ngẫu nhiên: một sự vượt quá tick 2× ngụ ý 4× biến động ngụ ý, vì vậy tính phí 4× khoản phụ phí. Tham số dynamic_fee_control hiệu chỉnh mức tuyệt đối. Cửa sổ filter_period ngăn chặn các dao động nhỏ dưới một giây (ví dụ: bot MEV sandwich) làm tăng bộ tích lũy. Cửa sổ decay_period ngăn chặn một spike quá khứ duy nhất tính phí vô thời hạn sau khi thị trường đã bình tĩnh.

Độ mạnh mẽ số học

  • Tất cả các sản phẩm trung gian đi qua số học hình dạng u128 hoặc u256. CLMM sử dụng các trợ giúp U128Sqrt và các mẫu FullMath::mulDiv được chuyển cổng trực tiếp từ Uniswap v3.
  • Làm tròn phép chia được chọn mỗi bước để thực thi bất biến k' ≥ k cục bộ. SwapBaseInput làm tròn đầu ra xuống; SwapBaseOutput làm tròn input lên.
  • Các lần vượt qua tick làm giảm PoolState.liquidity xuống 0 được phép (giá có thể duyệt qua một “lỗ liquidity”) nhưng swap chỉ cần tiến tới tick được khởi tạo tiếp theo mà không tiêu thụ input, không tính phí.
  • Bảo vệ tràn: sqrt_price_x64 được giữ trong phạm vi bao gồm [MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64] tương ứng với [MIN_TICK, MAX_TICK]. Một swap sẽ đẩy quá một trong hai giới hạn hoàn nguyên với SqrtPriceLimitOverflow.

Tiếp theo đi đâu

Nguồn: