Halaman ini menjelaskan tata letak dan peran setiap akun. Seed yang kanonik tercantum di reference/program-addresses. Pool CLMM memiliki lebih banyak akun dibanding pool CPMM karena likuiditas disimpan secara jarang (sparse) di sepanjang rentang tick; memahami kejarangan tersebut adalah inti dari halaman ini.
Inventaris akun
Sebuah pool CLMM yang aktif terdiri dari keluarga akun berikut. Semuanya dimiliki oleh program CLMM, kecuali dua mint dan vault-nya.
| Akun | Tujuan | Jumlah per pool |
|---|
AmmConfig | Tingkat biaya: trade-fee rate, bagian protokol, bagian dana, default tick-spacing. Dibagi bersama semua pool dalam tier ini. | 1 (bersama) |
PoolState | sqrt_price_x64 saat ini, tick saat ini, total likuiditas, global pertumbuhan biaya, info reward, pointer observasi. | 1 |
TickArrayState | Blok berisi TICK_ARRAY_SIZE tick yang berdekatan. Hanya diinisialisasi sesuai kebutuhan. | 0 ≤ N ≤ range |
TickArrayBitmapExtension | Bitmap overflow yang melacak array tick mana yang ada di luar bitmap inline pada PoolState. | 0 atau 1 |
PersonalPositionState | Satu per posisi LP. Menyimpan rentang, likuiditas, dan pertumbuhan biaya/reward terakhir yang diamati. Authority = pemilik NFT. | 1 per posisi |
| Position NFT mint | Mint dengan supply 1, terkait dengan PersonalPositionState. Memindahkan NFT berarti memindahkan posisi. | 1 per posisi |
ObservationState | Ring buffer dari observasi harga untuk TWAP. | 1 |
token_0_vault, token_1_vault | Akun token yang menyimpan saldo pool. Dimiliki oleh pool authority. | 2 |
DynamicFeeConfig | Set parameter yang dapat digunakan ulang untuk mekanisme dynamic-fee. Pool yang dibuat melalui create_customizable_pool dapat ikut serta. Dikelola admin. | bersama (per indeks) |
LimitOrderState | Satu per limit order yang terbuka. Mencatat pemilik, tick, sisi, jumlah total, snapshot output yang diselesaikan. | 1 per order |
LimitOrderNonce | Counter per (wallet, nonce_index) yang menghasilkan PDA order yang unik. | 1 per (wallet, indeks) |
PoolState
State live pool, dibaca pada setiap swap dan setiap perubahan posisi.
// programs/amm/src/states/pool.rs
pub struct PoolState {
pub bump: [u8; 1],
pub amm_config: Pubkey, // fee tier binding
pub owner: Pubkey, // admin (multisig)
pub token_mint_0: Pubkey,
pub token_mint_1: Pubkey,
pub token_vault_0: Pubkey,
pub token_vault_1: Pubkey,
pub observation_key: Pubkey,
pub mint_decimals_0: u8,
pub mint_decimals_1: u8,
pub tick_spacing: u16, // inherited from amm_config at init
pub liquidity: u128, // total active (in-range) liquidity
pub sqrt_price_x64: u128, // Q64.64 of sqrt(price)
pub tick_current: i32, // current tick index
pub padding3: u16,
pub padding4: u16,
// Global fee growth per unit of liquidity, Q64.64.
pub fee_growth_global_0_x64: u128,
pub fee_growth_global_1_x64: u128,
// Accrued-but-not-swept protocol fees (per mint).
pub protocol_fees_token_0: u64,
pub protocol_fees_token_1: u64,
// Reserved padding for future upgrades.
pub padding5: [u128; 4],
// Status bitmask. Bits 0-5: open-position, decrease-liquidity,
// collect-fee, collect-reward, swap, limit-order. A set bit disables
// the corresponding operation.
pub status: u8,
// Fee-collection mode (CollectFeeOn).
// 0 = FromInput (deduct fee from the swap input — Uniswap-V3 default)
// 1 = Token0Only (always deduct fee from token0 vault)
// 2 = Token1Only (always deduct fee from token1 vault)
pub fee_on: u8,
pub padding: [u8; 6],
// Live reward streams (up to REWARD_NUM = 3).
pub reward_infos: [RewardInfo; 3],
// Inline bitmap tracking initialized tick-arrays in the primary range.
pub tick_array_bitmap: [u64; 16],
// Reserved padding for future upgrades.
pub padding6: [u64; 4],
pub fund_fees_token_0: u64,
pub fund_fees_token_1: u64,
pub open_time: u64, // currently disabled by the program
pub recent_epoch: u64,
// Per-pool dynamic-fee state. Zero-valued unless the pool was
// created with `enable_dynamic_fee = true` via create_customizable_pool.
pub dynamic_fee_info: DynamicFeeInfo,
// Reserved for future upgrades.
pub padding1: [u64; 14],
pub padding2: [u64; 32],
}
Field yang akan sering Anda gunakan:
sqrt_price_x64 dan tick_current adalah state harga pool. Keduanya diperbarui bersama pada setiap swap. tick_current adalah floor dari log_{1.0001}(price).
liquidity adalah likuiditas aktif — jumlah nilai L dari semua posisi yang rentangnya mencakup tick_current. Nilainya berubah setiap kali swap melintasi sebuah tick dan setiap kali posisi dibuka/ditutup/diubah ukurannya.
fee_growth_global_{0,1}_x64 adalah biaya kumulatif yang diperoleh per unit likuiditas sepanjang seluruh riwayat pool. Posisi membaca nilai ini untuk menghitung jumlah yang terutang kepada mereka.
tick_spacing dikunci ke AmmConfig saat inisialisasi dan tidak pernah berubah. Nilai ini menentukan indeks tick mana saja yang diizinkan menjadi endpoint posisi.
tick_array_bitmap adalah bitmap inline yang mencakup rentang tick yang umum digunakan di sekitar harga spot. Untuk pool dengan posisi yang mencapai jangkauan jauh, pelacakan overflow disimpan di akun TickArrayBitmapExtension yang terpisah.
fee_on ditetapkan saat pool dibuat. 0 (FromInput) mereproduksi perilaku klasik Uniswap-V3. 1 dan 2 mengarahkan biaya swap ke satu sisi buku — lihat products/clmm/fees untuk pertimbangannya.
dynamic_fee_info menyimpan state volatilitas untuk surcharge dynamic-fee. Jika diaktifkan, setiap swap menghitung ulang dynamic_fee_component di atas AmmConfig.trade_fee_rate. Tata letak didokumentasikan di bawah DynamicFeeInfo; pool tanpa dynamic fee membiarkan seluruh struct bernilai nol.
AmmConfig
pub struct AmmConfig {
pub bump: u8,
pub index: u16, // uses "amm_config"+u16 seed
pub owner: Pubkey, // admin
pub protocol_fee_rate: u32, // fraction of trade fee to protocol, denom 1e6
pub trade_fee_rate: u32, // trade fee in 1e6ths of volume
pub tick_spacing: u16, // default spacing for pools using this config
pub fund_fee_rate: u32, // fraction of trade fee to fund, denom 1e6
pub padding_u32: u32,
pub fund_owner: Pubkey,
pub padding: [u64; 3],
}
Set tier biaya CLMM yang umum dipublikasikan (konfirmasi melalui GET https://api-v3.raydium.io/main/clmm-config):
| Indeks | trade_fee_rate | Tick spacing | Penggunaan umum |
|---|
| 0 | 100 (0.01%) | 1 | Pasangan stabil, USDC/USDT |
| 1 | 500 (0.05%) | 10 | Blue-chip yang berkorelasi |
| 2 | 2_500 (0.25%) | 60 | Pasangan standar |
| 3 | 10_000 (1.00%) | 120 | Aset volatil atau long-tail |
protocol_fee_rate dan fund_fee_rate adalah fraksi dari trade fee; konvensinya sama dengan CPMM. Lihat products/clmm/fees.
TickArrayState
CLMM tidak menyimpan satu record per tick — pendekatan itu akan menghasilkan miliaran akun. Sebagai gantinya, TICK_ARRAY_SIZE tick yang berdekatan (umumnya 60 atau 88 tergantung versi program), baik yang sudah diinisialisasi maupun belum, dikelompokkan ke dalam sebuah TickArrayState yang dibuat secara lazy saat pertama kali digunakan.
pub const TICK_ARRAY_SIZE: usize = 60;
pub const TICK_ARRAY_SIZE_USIZE: usize = 60;
pub struct TickArrayState {
pub pool_id: Pubkey,
pub start_tick_index: i32, // lowest tick in this array
pub ticks: [TickState; TICK_ARRAY_SIZE], // 60 entries
pub initialized_tick_count: u8,
pub recent_epoch: u64,
pub padding: [u8; 107],
}
pub struct TickState {
pub tick: i32,
pub liquidity_net: i128, // ΔL when crossing this tick upward
pub liquidity_gross: u128, // total L referencing this tick
pub fee_growth_outside_0_x64: u128, // see math.mdx
pub fee_growth_outside_1_x64: u128,
pub reward_growths_outside_x64: [u128; 3],
// Limit-order bookkeeping. All zero for ticks that have never carried
// a limit order. See products/clmm/math for the matching algorithm.
pub order_phase: u64, // monotonic FIFO cohort id
pub orders_amount: u64, // unfilled tokens in current cohort
pub part_filled_orders_remaining: u64, // remaining tokens of partially-filled cohort
pub unfilled_ratio_x64: u128, // Q64.64; starts at 1.0 and shrinks as fills occur
pub padding: [u32; 3],
}
Keempat field limit-order bernilai nol pada tick yang belum pernah digunakan untuk limit order. Ketika order dibuka pada sebuah tick, program melacaknya sebagai urutan kohort:
order_phase adalah id kohort. Nilainya bertambah setiap kali sebuah kohort bertransisi dari “semua belum terisi” menjadi “sebagian terisi.”
orders_amount adalah total token input dari kohort saat ini (terbaru).
part_filled_orders_remaining melacak kohort sebelumnya yang sedang diisi oleh swap yang sedang berjalan.
unfilled_ratio_x64 adalah multiplier Q64.64 yang dibawa pada kohort: ketika swap mengisi X% dari kohort, rasionya dikalikan dengan (1 − X). Setiap order yang terbuka menyimpan snapshot (order_phase, unfilled_ratio_x64) saat dibuka, sehingga matematika penyelesaian cukup membandingkan snapshot.
Aturan:
- Tick endpoint posisi t harus memenuhi
t % tick_spacing == 0. Program menolak posisi yang tidak sesuai spacing.
- Array dari sebuah tick berada pada
floor(t / (TICK_ARRAY_SIZE * tick_spacing)) * (TICK_ARRAY_SIZE * tick_spacing).
- Array tick diinisialisasi secara lazy: posisi atau swap pertama yang menyentuh array yang belum diinisialisasi akan membuatnya sekaligus membayar rent.
- Array tick tidak pernah ditutup oleh program. Setelah dialokasikan, akun ini bertahan selama pool ada, bahkan setelah setiap tick di dalamnya kembali ke
liquidity_gross == 0. Posisi dan swap berikutnya menggunakan kembali akun yang sudah ada tanpa biaya rent tambahan. Tidak ada jalur pembersihan berbasis ClosePosition untuk array tick.
TickArrayBitmapExtension
PoolState.tick_array_bitmap (inline) mencakup rentang “dekat spot” — ±1.024 array tick. Di luar rentang tersebut (untuk nilai tick yang ekstrem), program memelihara akun extension:
pub struct TickArrayBitmapExtension {
pub pool_id: Pubkey,
pub positive_tick_array_bitmap: [[u64; 8]; 14],
pub negative_tick_array_bitmap: [[u64; 8]; 14],
}
Jika rentang posisi Anda “normal”, Anda tidak perlu memikirkan akun extension ini. Posisi full-range (misalnya (MIN_TICK, MAX_TICK)) membutuhkannya; SDK akan menanganinya untuk Anda.
Posisi
Sebuah posisi CLMM adalah gabungan dari tiga akun ditambah satu mint:
Position NFT mint
Mint SPL Token dengan supply 1. Alamat mint adalah PDA deterministik; NFT posisi di wallet pemilik hanyalah ATA yang menyimpan satu token tersebut. Memindahkan NFT adalah cara suatu posisi berganti pemilik — program mengaitkan otorisasi ke pemegang saldo ATA NFT saat ini, bukan ke Pubkey yang tersimpan dalam state.
PersonalPositionState
Satu per posisi yang terbuka. Dikunci berdasarkan NFT mint.
pub struct PersonalPositionState {
pub bump: [u8; 1],
pub nft_mint: Pubkey, // this position's NFT mint
pub pool_id: Pubkey,
pub tick_lower_index: i32,
pub tick_upper_index: i32,
pub liquidity: u128, // this position's L
// Fee-growth snapshots at last time the position was touched.
pub fee_growth_inside_0_last_x64: u128,
pub fee_growth_inside_1_last_x64: u128,
pub token_fees_owed_0: u64, // accrued since last collect
pub token_fees_owed_1: u64,
pub reward_infos: [PositionRewardInfo; 3],
pub recent_epoch: u64,
pub padding: [u64; 7],
}
pub struct PositionRewardInfo {
pub growth_inside_last_x64: u128,
pub reward_amount_owed: u64,
}
ProtocolPositionState (tidak digunakan lagi)
Rilis CLMM yang lebih lama menyimpan pembukuan agregat per (pool, tick_lower, tick_upper) dalam PDA ProtocolPositionState. Rilis terbaru tidak lagi membuat atau membaca akun ini. Slot tersebut masih muncul dalam daftar akun OpenPosition / IncreaseLiquidity / DecreaseLiquidity sebagai UncheckedAccount untuk kompatibilitas ABI, tetapi program tidak menulisi akun tersebut. Akun yang ada di on-chain bersifat vestigial; admin dapat memanggil CloseProtocolPosition untuk mengklaim kembali rent-nya.Pembukuan rentang agregat kini diturunkan langsung dari dua tick endpoint (liquidity_gross, liquidity_net, dan fee_growth_outside_* / reward_growths_outside_x64 per tick) dalam TickArrayState. Formula fee-growth-inside fee_growth_inside = global − outside_lower − outside_upper tetap berfungsi tanpa akun posisi agregat.
Observation
pub const OBSERVATION_NUM: usize = 100;
pub struct Observation {
pub block_timestamp: u32,
pub tick_cumulative: i64, // Σ tick_current × Δt
pub padding: [u64; 4],
}
pub struct ObservationState {
pub initialized: bool,
pub recent_epoch: u64,
pub observation_index: u16,
pub pool_id: Pubkey,
pub observations: [Observation; OBSERVATION_NUM], // 100 entries
pub padding: [u64; 4],
}
Buffer observasi CLMM menyimpan tick kumulatif, bukan harga kumulatif. Konsumen eksternal menghitung harga rata-rata geometris dalam suatu interval dari (tick_cumulative[t1] − tick_cumulative[t0]) / (t1 − t0), kemudian price = 1.0001 ** tick. Lihat algorithms/clmm-math.
DynamicFeeConfig dan DynamicFeeInfo
Parameter dynamic fee berada di dua tempat. Template yang dapat digunakan ulang — DynamicFeeConfig — dikelola admin dan dibagi bersama pool yang ikut serta. State runtime per pool — DynamicFeeInfo — tertanam dalam PoolState dan diperbarui oleh setiap swap.
DynamicFeeConfig
// programs/amm/src/states/pool_fee.rs
pub struct DynamicFeeConfig {
pub index: u16, // identifier; PDA seed component
pub filter_period: u16, // seconds — within this window the volatility reference is held
pub decay_period: u16, // seconds — beyond this window the reference fully decays
pub reduction_factor: u16, // fixed-point in [1, 10_000); applied at decay
pub dynamic_fee_control: u32, // fixed-point in (0, 100_000); fee-rate gain
pub max_volatility_accumulator: u32, // ceiling on the volatility accumulator
pub padding: [u64; 8],
}
PDA seed: ["dynamic_fee_config", index.to_be_bytes()]. Dibuat melalui create_dynamic_fee_config (dibatasi admin) dan dimodifikasi melalui update_dynamic_fee_config. Pool yang dibuat dengan enable_dynamic_fee = true menyalin lima parameter kalibrasi konfigurasi (filter_period, decay_period, reduction_factor, dynamic_fee_control, max_volatility_accumulator) ke dalam DynamicFeeInfo-nya sendiri saat pembuatan; pengeditan selanjutnya pada DynamicFeeConfig tidak berlaku surut bagi pool yang sudah ada.
DynamicFeeInfo (tertanam dalam PoolState)
pub struct DynamicFeeInfo {
pub filter_period: u16,
pub decay_period: u16,
pub reduction_factor: u16,
pub dynamic_fee_control: u32,
pub max_volatility_accumulator: u32,
pub tick_spacing_index_reference: i32, // tick-spacing-units; reference for next swap
pub volatility_reference: u32, // running floor for the accumulator
pub volatility_accumulator: u32, // current cumulative volatility (capped)
pub last_update_timestamp: u64,
pub padding: [u8; 46],
}
Empat field terbawah adalah state; lima field teratas adalah kalibrasi yang disalin dari DynamicFeeConfig. Matematika biaya dan aturan decay didokumentasikan di products/clmm/math dan products/clmm/fees.
Konstanta yang digunakan dalam formula:
| Konstanta | Nilai | Makna |
|---|
VOLATILITY_ACCUMULATOR_SCALE | 10_000 | Granularitas dari volatility accumulator |
REDUCTION_FACTOR_DENOMINATOR | 10_000 | Penyebut untuk reduction_factor |
DYNAMIC_FEE_CONTROL_DENOMINATOR | 100_000 | Penyebut untuk dynamic_fee_control |
MAX_FEE_RATE_NUMERATOR | 100_000 | Batas keras 10% pada fee rate yang dihasilkan |
LimitOrderState
Satu akun per limit order yang terbuka.
// programs/amm/src/states/limit_order.rs
pub struct LimitOrderState {
pub pool_id: Pubkey,
pub owner: Pubkey,
pub tick_index: i32,
pub zero_for_one: bool, // direction: true sells token0 for token1
pub order_phase: u64, // snapshot of TickState.order_phase at open time
pub total_amount: u64, // input-token amount placed
pub filled_amount: u64, // informational; computed precisely on settle
pub settle_base: u64, // unfilled remainder at last settle/decrease
pub settled_output: u64, // cumulative output-token paid to owner
pub open_time: u64,
pub unfilled_ratio_x64: u128, // Q64.64 snapshot of TickState.unfilled_ratio_x64 at open
pub padding: [u64; 4],
}
Siklus hidup:
- Buka — pengguna memanggil
open_limit_order, menyetor total_amount token input, order terikat ke kohort TickState.
- (opsional) Tambah / Kurangi —
increase_limit_order menambah total_amount; decrease_limit_order mengembalikan token yang belum terisi (dan output yang sudah diselesaikan hingga saat itu).
- Selesaikan — ketika kohort terisi penuh atau sebagian, pemilik atau keeper operasional memanggil
settle_limit_order untuk mendorong token output ke ATA pemilik.
- Tutup — setelah
unfilled_amount == 0, akun dapat ditutup. Rent selalu dikembalikan ke owner.
PDA seed: [owner.as_ref(), limit_order_nonce.key().as_ref(), limit_order_nonce.order_nonce.to_be_bytes().as_ref()]. PDA order oleh karena itu unik per (owner, nonce_index, order_nonce).
LimitOrderNonce
Counter per (wallet, nonce_index) yang memungkinkan satu pengguna menjalankan beberapa pipeline limit order secara paralel tanpa bentrokan pada PDA.
pub struct LimitOrderNonce {
pub user_wallet: Pubkey,
pub nonce_index: u8, // user-chosen, 0..255
pub order_nonce: u64, // monotonic, incremented every time a new order is opened
pub padding: [u64; 4],
}
PDA seed: [user_wallet.as_ref(), &[nonce_index]]. Sebagian besar klien menggunakan nonce_index = 0 dan membiarkan order_nonce menangani kardinalitas.
import { PublicKey } from "@solana/web3.js";
const CLMM_PROGRAM_ID = new PublicKey(
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"
); // see reference/program-addresses
function i32ToBytes(n: number): Buffer {
const b = Buffer.alloc(4);
b.writeInt32BE(n);
return b;
}
export function deriveClmmAccounts(
ammConfig: PublicKey,
token0Mint: PublicKey, // must already be sorted
token1Mint: PublicKey,
) {
const [poolState] = PublicKey.findProgramAddressSync(
[Buffer.from("pool"), ammConfig.toBuffer(),
token0Mint.toBuffer(), token1Mint.toBuffer()],
CLMM_PROGRAM_ID,
);
const [observation] = PublicKey.findProgramAddressSync(
[Buffer.from("observation"), poolState.toBuffer()],
CLMM_PROGRAM_ID,
);
const [tickArrayBitmapExtension] = PublicKey.findProgramAddressSync(
[Buffer.from("pool_tick_array_bitmap_extension"), poolState.toBuffer()],
CLMM_PROGRAM_ID,
);
return { poolState, observation, tickArrayBitmapExtension };
}
export function deriveTickArray(
pool: PublicKey,
startTickIndex: number,
) {
const [tickArray] = PublicKey.findProgramAddressSync(
[Buffer.from("tick_array"), pool.toBuffer(), i32ToBytes(startTickIndex)],
CLMM_PROGRAM_ID,
);
return tickArray;
}
export function deriveDynamicFeeConfig(index: number) {
const idx = Buffer.alloc(2);
idx.writeUInt16BE(index);
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("dynamic_fee_config"), idx],
CLMM_PROGRAM_ID,
);
return pda;
}
export function deriveLimitOrderNonce(
wallet: PublicKey,
nonceIndex: number,
) {
const [pda] = PublicKey.findProgramAddressSync(
[wallet.toBuffer(), Buffer.from([nonceIndex & 0xff])],
CLMM_PROGRAM_ID,
);
return pda;
}
export function deriveLimitOrder(
wallet: PublicKey,
nonceAccount: PublicKey,
orderNonce: bigint,
) {
const nonceBytes = Buffer.alloc(8);
nonceBytes.writeBigUInt64BE(orderNonce);
const [pda] = PublicKey.findProgramAddressSync(
[wallet.toBuffer(), nonceAccount.toBuffer(), nonceBytes],
CLMM_PROGRAM_ID,
);
return pda;
}
export function derivePersonalPosition(nftMint: PublicKey) {
const [personalPosition] = PublicKey.findProgramAddressSync(
[Buffer.from("position"), nftMint.toBuffer()],
CLMM_PROGRAM_ID,
);
return personalPosition;
}
String seed yang tepat harus selalu diverifikasi ulang terhadap IDL on-chain dan reference/program-addresses.
Referensi cepat siklus hidup
| Event | Akun yang dibuat | Akun yang dihancurkan |
|---|
CreatePool | poolState, observation, token_0_vault, token_1_vault | — |
OpenPosition[WithToken22Nft] | NFT mint + ATA, personalPosition, kemungkinan tickArrayState baru, tickArrayBitmapExtension jika belum ada | — |
IncreaseLiquidity | Kemungkinan tickArrayState baru | — |
DecreaseLiquidity | — | Kemungkinan membersihkan entri tick (tetapi tickArrayState itu sendiri tidak ditutup) |
ClosePosition | — | NFT mint, personalPosition |
SwapV2 | Kemungkinan tickArrayState baru | — |
OpenLimitOrder | limitOrderState, kemungkinan limitOrderNonce (init-if-needed), kemungkinan tickArrayState baru | — |
IncreaseLimitOrder | — | — |
DecreaseLimitOrder | — | Menutup limitOrderState jika order sudah habis terisi |
SettleLimitOrder | — | — |
CloseLimitOrder | — | limitOrderState (rent → owner) |
CreateDynamicFeeConfig | dynamicFeeConfig | — |
CreateCustomizablePool | poolState, observation, vault — sama seperti CreatePool. Menyalin dynamicFeeConfig jika enable_dynamic_fee = true. | — |
CollectRewards | — | — |
UpdateRewardInfos | — | — |
CloseProtocolPosition (admin) | — | protocolPositionState vestigial (rent → admin) |
Akun TickArrayState tidak pernah ditutup oleh program — akun ini bertahan selama pool ada. Setelah sebuah array tick diinisialisasi, akun tersebut tetap ada di on-chain bahkan ketika setiap tick di dalamnya kembali ke liquidity_gross == 0. Menggunakan kembali array tick yang sudah ada tidak dikenakan biaya; hanya posisi pertama yang menyentuh array yang belum pernah diinisialisasi yang membayar rent-nya.
Panduan referensi selanjutnya
Sumber: