На этой странице описаны структура и роль каждого аккаунта. Канонические сиды перечислены в reference/program-addresses. CLMM-пул использует больше аккаунтов, чем CPMM-пул, поскольку ликвидность хранится разреженно по диапазону тиков — понимание этой разреженности составляет основу данной страницы.
Перечень аккаунтов
Активный CLMM-пул описывается следующими группами аккаунтов. Все они принадлежат программе CLMM, за исключением двух минтов и их хранилищ.
| Аккаунт | Назначение | Количество на пул |
|---|
AmmConfig | Уровень комиссии: ставка торговой комиссии, доля протокола, доля фонда, шаг тиков по умолчанию. Общий для всех пулов данного уровня. | 1 (общий) |
PoolState | Текущий sqrt_price_x64, текущий тик, общая ликвидность, глобальный прирост комиссий, информация о наградах, указатель на observation. | 1 |
TickArrayState | Блок из TICK_ARRAY_SIZE смежных тиков. Инициализируется по требованию. | 0 ≤ N ≤ диапазон |
TickArrayBitmapExtension | Расширенный bitmap, отслеживающий существующие tick-массивы за пределами встроенного bitmap в PoolState. | 0 или 1 |
PersonalPositionState | По одному на каждую LP-позицию. Хранит диапазон, ликвидность и последнее зафиксированное значение прироста комиссий/наград. Authority = владелец NFT. | 1 на позицию |
| Position NFT mint | Минт с supply = 1, связанный с PersonalPositionState. Передача NFT = передача позиции. | 1 на позицию |
ObservationState | Кольцевой буфер ценовых наблюдений для TWAP. | 1 |
token_0_vault, token_1_vault | Token-аккаунты, хранящие балансы пула. Принадлежат authority пула. | 2 |
DynamicFeeConfig | Переиспользуемый набор параметров механизма динамической комиссии. Пулы, созданные через create_customizable_pool, могут подключиться. Управляется администратором. | общий (по индексу) |
LimitOrderState | По одному на каждый открытый лимитный ордер. Содержит владельца, тик, направление, общий объём и снимок исполненного вывода. | 1 на ордер |
LimitOrderNonce | Счётчик для пары (wallet, nonce_index), обеспечивающий уникальность PDA ордеров. | 1 на (wallet, index) |
PoolState
Текущее состояние пула, считываемое при каждом swap и каждом изменении позиции.
// 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],
}
Поля, с которыми вы будете работать непосредственно:
sqrt_price_x64 и tick_current — ценовое состояние пула. Обновляются вместе при каждом swap. tick_current — это нижняя целая часть log_{1.0001}(price).
liquidity — активная ликвидность: сумма значений L для всех позиций, чей диапазон содержит tick_current. Изменяется каждый раз, когда swap пересекает тик, а также при открытии, закрытии или изменении размера позиции.
fee_growth_global_{0,1}_x64 — накопленные комиссии на единицу ликвидности за всю историю пула. Позиции используют это значение для расчёта причитающейся им суммы.
tick_spacing привязывается к AmmConfig при инициализации и никогда не меняется. Определяет, какие индексы тиков допустимы в качестве граничных точек позиций.
tick_array_bitmap — встроенный bitmap, охватывающий часто используемый диапазон тиков вблизи спот-цены. Для позиций, выходящих за эти границы, отслеживание ведётся в отдельном аккаунте TickArrayBitmapExtension.
fee_on фиксируется при создании пула. Значение 0 (FromInput) воспроизводит поведение классического Uniswap V3. Значения 1 и 2 направляют комиссию swap на одну из сторон — подробнее о компромиссах см. products/clmm/fees.
dynamic_fee_info хранит состояние волатильности для надбавки динамической комиссии. При включении каждый swap пересчитывает dynamic_fee_component поверх AmmConfig.trade_fee_rate. Структура описана ниже в разделе DynamicFeeInfo; у пулов без динамической комиссии весь struct равен нулю.
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],
}
Типичный набор уровней комиссии CLMM (сверяйтесь с GET https://api-v3.raydium.io/main/clmm-config):
| Индекс | trade_fee_rate | Шаг тиков | Типичное применение |
|---|
| 0 | 100 (0.01%) | 1 | Стабильные пары, USDC/USDT |
| 1 | 500 (0.05%) | 10 | Коррелированные blue-chip активы |
| 2 | 2_500 (0.25%) | 60 | Стандартные пары |
| 3 | 10_000 (1.00%) | 120 | Волатильные или низколиквидные активы |
protocol_fee_rate и fund_fee_rate — доли от торговой комиссии; соглашение аналогично CPMM. Подробнее: products/clmm/fees.
TickArrayState
CLMM не хранит по одной записи на каждый тик — это потребовало бы миллиардов аккаунтов. Вместо этого программа объединяет TICK_ARRAY_SIZE смежных тиков (обычно 60 или 88 в зависимости от версии программы) в TickArrayState, который создаётся лениво при первом обращении.
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],
}
Четыре поля для лимитных ордеров равны нулю для любого тика, который ни разу не использовался в лимитных ордерах. Когда ордера открываются на тике, программа отслеживает их как последовательность когорт:
order_phase — идентификатор когорты. Увеличивается каждый раз, когда когорта переходит из состояния «полностью не исполнена» в «частично исполнена».
orders_amount — суммарный объём входящего токена текущей (новейшей) когорты.
part_filled_orders_remaining — остаток предыдущей когорты, которая в данный момент исполняется текущими swap-ами.
unfilled_ratio_x64 — мультипликатор Q64.64 когорты: когда swap заполняет X% когорты, коэффициент умножается на (1 − X). Каждый открытый ордер хранит собственный снимок (order_phase, unfilled_ratio_x64) на момент открытия, поэтому расчёт расчёта сводится к сравнению снимков.
Правила:
- Граничный тик позиции t должен удовлетворять условию
t % tick_spacing == 0. Программа отклоняет позиции с несовпадающим шагом.
- Массив тика определяется как
floor(t / (TICK_ARRAY_SIZE * tick_spacing)) * (TICK_ARRAY_SIZE * tick_spacing).
- Tick-массив инициализируется лениво: первая позиция или swap, обращающиеся к неинициализированному массиву, создают его и оплачивают rent.
- Tick-массив никогда не закрывается программой. Однажды выделенный, он существует на протяжении всего жизненного цикла пула, даже если все тики внутри вернулись к
liquidity_gross == 0. Последующие позиции и swap-ы переиспользуют существующий аккаунт без дополнительной оплаты rent. Пути очистки tick-массивов через ClosePosition не существует.
TickArrayBitmapExtension
PoolState.tick_array_bitmap (встроенный) охватывает диапазон «вблизи спота» — ±1 024 tick-массива. За пределами этого диапазона (для экстремальных значений тиков) программа ведёт отдельный аккаунт расширения:
pub struct TickArrayBitmapExtension {
pub pool_id: Pubkey,
pub positive_tick_array_bitmap: [[u64; 8]; 14],
pub negative_tick_array_bitmap: [[u64; 8]; 14],
}
При «нормальном» диапазоне позиции об аккаунте расширения думать не нужно. Позиции на полный диапазон (например, (MIN_TICK, MAX_TICK)) требуют его; SDK разрешает это автоматически.
Позиции
Позиция CLMM — это связка из трёх аккаунтов и одного минта.
Position NFT mint
Минт SPL Token с supply = 1. Адрес минта — детерминированный PDA; Position NFT в кошельке владельца — это просто ATA, хранящий этот единственный токен. Передача NFT — это и есть смена владельца позиции: программа привязывает авторизацию к текущему держателю баланса ATA NFT, а не к Pubkey, хранящемуся в состоянии.
PersonalPositionState
По одному на каждую открытую позицию. Ключ — адрес NFT-минта.
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 (устарело)
В ранних версиях CLMM агрегированные данные по диапазону (pool, tick_lower, tick_upper) хранились в PDA ProtocolPositionState. Новые версии этот аккаунт не создают и не читают. В списке аккаунтов инструкций OpenPosition / IncreaseLiquidity / DecreaseLiquidity слот по-прежнему присутствует как UncheckedAccount для совместимости ABI, но программа в него не записывает. Существующие аккаунты на-чейне являются устаревшими; администратор может вызвать CloseProtocolPosition для возврата rent.Агрегированные данные диапазона теперь вычисляются напрямую из двух граничных тиков (liquidity_gross, liquidity_net и fee_growth_outside_* / reward_growths_outside_x64 по каждому тику) в TickArrayState. Формула прироста комиссии внутри диапазона — fee_growth_inside = global − outside_lower − outside_upper — работает без отдельного аккаунта агрегированной позиции.
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],
}
Буфер наблюдений CLMM хранит накопленный тик, а не накопленную цену. Внешние потребители вычисляют среднегеометрическую цену за интервал из (tick_cumulative[t1] − tick_cumulative[t0]) / (t1 − t0), а затем применяют price = 1.0001 ** tick. Подробнее: algorithms/clmm-math.
DynamicFeeConfig и DynamicFeeInfo
Параметры динамической комиссии хранятся в двух местах. Переиспользуемый шаблон — DynamicFeeConfig — управляется администратором и является общим для подключившихся пулов. Рантайм-состояние конкретного пула — DynamicFeeInfo — встроено в PoolState и обновляется при каждом 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-сид: ["dynamic_fee_config", index.to_be_bytes()]. Создаётся через create_dynamic_fee_config (только для администратора), изменяется через update_dynamic_fee_config. Пул, созданный с enable_dynamic_fee = true, копирует пять калибровочных параметров конфига (filter_period, decay_period, reduction_factor, dynamic_fee_control, max_volatility_accumulator) в собственный DynamicFeeInfo на момент создания; последующие изменения DynamicFeeConfig на уже созданные пулы не влияют.
DynamicFeeInfo (встроено в 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],
}
Четыре нижних поля — это состояние; пять верхних — калибровочные параметры, скопированные из DynamicFeeConfig. Математика комиссий и правила затухания описаны в products/clmm/math и products/clmm/fees.
Константы, используемые в формуле:
| Константа | Значение | Смысл |
|---|
VOLATILITY_ACCUMULATOR_SCALE | 10_000 | Гранулярность аккумулятора волатильности |
REDUCTION_FACTOR_DENOMINATOR | 10_000 | Знаменатель для reduction_factor |
DYNAMIC_FEE_CONTROL_DENOMINATOR | 100_000 | Знаменатель для dynamic_fee_control |
MAX_FEE_RATE_NUMERATOR | 100_000 | Жёсткий потолок ставки комиссии — 10% |
LimitOrderState
По одному аккаунту на каждый открытый лимитный ордер.
// 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],
}
Жизненный цикл:
- Открытие — пользователь вызывает
open_limit_order, вносит total_amount входящего токена; ордер привязывается к когорте TickState.
- (необязательно) Увеличение / Уменьшение —
increase_limit_order добавляет к total_amount; decrease_limit_order возвращает неисполненные токены (и любой накопленный вывод на этот момент).
- Расчёт — когда когорта исполнена полностью или частично, владелец или оперативный keeper вызывает
settle_limit_order, чтобы перечислить выходные токены в ATA владельца.
- Закрытие — когда
unfilled_amount == 0, аккаунт можно закрыть. Rent всегда возвращается owner.
PDA-сид: [owner.as_ref(), limit_order_nonce.key().as_ref(), limit_order_nonce.order_nonce.to_be_bytes().as_ref()]. Таким образом, PDA ордера уникален для каждой тройки (owner, nonce_index, order_nonce).
LimitOrderNonce
Счётчик для пары (wallet, nonce_index), позволяющий одному пользователю вести несколько параллельных цепочек лимитных ордеров без коллизий 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-сид: [user_wallet.as_ref(), &[nonce_index]]. Большинство клиентов используют nonce_index = 0, а счётчик ордеров несёт order_nonce.
Вычисление ключевых аккаунтов
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;
}
Точные строки сидов всегда следует сверять с on-chain IDL и reference/program-addresses.
Краткий справочник по жизненному циклу
| Событие | Создаваемые аккаунты | Уничтожаемые аккаунты |
|---|
CreatePool | poolState, observation, token_0_vault, token_1_vault | — |
OpenPosition[WithToken22Nft] | NFT mint + ATA, personalPosition, возможно новые tickArrayState, tickArrayBitmapExtension (если ещё не создан) | — |
IncreaseLiquidity | Возможно новые tickArrayState | — |
DecreaseLiquidity | — | Возможно очищает записи тиков (но сам tickArrayState не закрывается) |
ClosePosition | — | NFT mint, personalPosition |
SwapV2 | Возможно новый tickArrayState | — |
OpenLimitOrder | limitOrderState, возможно limitOrderNonce (init-if-needed), возможно новый tickArrayState | — |
IncreaseLimitOrder | — | — |
DecreaseLimitOrder | — | Закрывает limitOrderState, если ордер полностью исполнен |
SettleLimitOrder | — | — |
CloseLimitOrder | — | limitOrderState (rent → owner) |
CreateDynamicFeeConfig | dynamicFeeConfig | — |
CreateCustomizablePool | poolState, observation, vaults — аналогично CreatePool. Копирует dynamicFeeConfig, если enable_dynamic_fee = true. | — |
CollectRewards | — | — |
UpdateRewardInfos | — | — |
CloseProtocolPosition (admin) | — | Устаревший protocolPositionState (rent → admin) |
Аккаунты TickArrayState программой никогда не закрываются — они существуют на протяжении всего жизненного цикла пула. Однажды инициализированный tick-массив остаётся на-чейне, даже если все тики внутри вернулись к liquidity_gross == 0. Повторное использование существующего tick-массива бесплатно; rent платит только первая позиция, обращающаяся к ранее неинициализированному массиву.
Где искать дополнительную информацию
Источники: