Esta página descreve o layout e a função de cada conta. As seeds canônicas estão listadas em reference/program-addresses. Um pool CLMM utiliza mais contas do que um pool CPMM porque a liquidez é armazenada de forma esparsa ao longo do intervalo de ticks; entender essa esparsidade é o tema principal desta página.
Inventário de contas
Um pool CLMM ativo é descrito pelas seguintes famílias de contas. Todas pertencem ao programa CLMM, exceto as duas mints e seus vaults.
| Conta | Finalidade | Quantidade por pool |
|---|
AmmConfig | Fee tier: taxa de negociação, participação do protocolo, participação do fundo, tick-spacing padrão. Compartilhada entre todos os pools desse tier. | 1 (compartilhada) |
PoolState | sqrt_price_x64 atual, tick atual, liquidez total, crescimento global de taxas, informações de recompensa, ponteiro de observação. | 1 |
TickArrayState | Um bloco de TICK_ARRAY_SIZE ticks adjacentes. Inicializado apenas sob demanda. | 0 ≤ N ≤ intervalo |
TickArrayBitmapExtension | Bitmap de overflow que rastreia quais arrays de tick existem além do bitmap inline em PoolState. | 0 ou 1 |
PersonalPositionState | Um por posição de LP. Armazena o intervalo, a liquidez e o crescimento de taxas/recompensas visto pela última vez. Autoridade = dono do NFT. | 1 por posição |
| NFT mint da posição | Mint com supply 1, associada ao PersonalPositionState. Transferir o NFT transfere a posição. | 1 por posição |
ObservationState | Buffer circular de observações de preço para o TWAP. | 1 |
token_0_vault, token_1_vault | Contas de token que guardam os saldos do pool. Pertencentes à autoridade do pool. | 2 |
DynamicFeeConfig | Conjunto de parâmetros reutilizável para o mecanismo de taxa dinâmica. Pools criados via create_customizable_pool podem aderir. Gerenciado pelo admin. | compartilhada (por índice) |
LimitOrderState | Um por ordem limitada aberta. Registra o dono, tick, direção, valor total e snapshot da saída liquidada. | 1 por ordem |
LimitOrderNonce | Contador por (wallet, nonce_index) que deriva PDAs únicas para cada ordem. | 1 por (wallet, índice) |
PoolState
O estado ativo do pool, lido a cada swap e a cada alteração de posição.
// 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],
}
Campos que você realmente utilizará:
sqrt_price_x64 e tick_current representam o estado de preço do pool. São atualizados juntos a cada swap. tick_current é o piso de log_{1.0001}(price).
liquidity é a liquidez ativa — a soma dos valores L de todas as posições cujo intervalo contém tick_current. Ela muda sempre que um swap cruza um tick e sempre que uma posição é aberta, fechada ou redimensionada.
fee_growth_global_{0,1}_x64 são as taxas acumuladas por unidade de liquidez ao longo de todo o histórico do pool. As posições consultam esses valores para calcular o que lhes é devido.
tick_spacing é fixado no AmmConfig na inicialização e nunca muda. Ele determina quais índices de tick podem ser endpoints de posição.
tick_array_bitmap é um bitmap inline que cobre o intervalo de tick mais utilizado em torno do preço spot. Para pools cujas posições se estendem muito além desse intervalo, o rastreamento de overflow fica na conta separada TickArrayBitmapExtension.
fee_on é definido na criação do pool. 0 (FromInput) reproduz o comportamento clássico do Uniswap-V3. 1 e 2 direcionam a taxa de swap para um único lado do livro — veja products/clmm/fees para as implicações.
dynamic_fee_info carrega o estado de volatilidade para o acréscimo de taxa dinâmica. Quando habilitado, cada swap recalcula um dynamic_fee_component sobre o AmmConfig.trade_fee_rate. O layout está documentado em DynamicFeeInfo abaixo; pools sem taxa dinâmica mantêm toda a struct zerada.
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],
}
Um conjunto típico de fee tiers CLMM publicados (confirme em GET https://api-v3.raydium.io/main/clmm-config):
| Índice | trade_fee_rate | Tick spacing | Uso típico |
|---|
| 0 | 100 (0,01%) | 1 | Pares estáveis, USDC/USDT |
| 1 | 500 (0,05%) | 10 | Blue-chips correlacionadas |
| 2 | 2_500 (0,25%) | 60 | Pares padrão |
| 3 | 10_000 (1,00%) | 120 | Ativos voláteis ou de cauda longa |
protocol_fee_rate e fund_fee_rate são frações da taxa de negociação; mesma convenção do CPMM. Veja products/clmm/fees.
TickArrayState
O CLMM não armazena um registro por tick — isso resultaria em bilhões de contas. Em vez disso, ele agrupa TICK_ARRAY_SIZE ticks adjacentes (tipicamente 60 ou 88, dependendo da versão do programa), sejam eles inicializados ou não, em um TickArrayState criado de forma lazy no primeiro uso.
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],
}
Os quatro campos de ordem limitada são zero em qualquer tick que nunca foi utilizado para uma ordem limitada. Quando ordens são abertas em um tick, o programa as rastreia como uma sequência de coortes:
order_phase é o id da coorte. Ele incrementa toda vez que uma coorte transita de “totalmente não preenchida” para “parcialmente preenchida.”
orders_amount é o total do token de entrada da coorte atual (a mais recente).
part_filled_orders_remaining rastreia a coorte anterior que está sendo preenchida pelos swaps em andamento.
unfilled_ratio_x64 é um multiplicador Q64.64 mantido na coorte: quando um swap preenche X% da coorte, a razão é multiplicada por (1 − X). Cada ordem aberta armazena seu próprio snapshot de (order_phase, unfilled_ratio_x64) no momento da abertura, de modo que o cálculo de liquidação se reduz a comparar snapshots.
Regras:
- Um tick de endpoint de posição t deve satisfazer
t % tick_spacing == 0. O programa rejeita posições fora do espaçamento.
- O array do tick está localizado em
floor(t / (TICK_ARRAY_SIZE * tick_spacing)) * (TICK_ARRAY_SIZE * tick_spacing).
- Um tick array é inicializado de forma lazy: a primeira posição ou swap que toca um array não inicializado o cria, pagando o rent.
- Um tick array nunca é fechado pelo programa. Uma vez alocado, ele persiste durante toda a vida do pool, mesmo após todos os ticks dentro dele retornarem a
liquidity_gross == 0. Posições e swaps subsequentes reutilizam a conta existente sem custo adicional de rent. Não há caminho de limpeza via ClosePosition para tick arrays.
TickArrayBitmapExtension
PoolState.tick_array_bitmap (inline) cobre o intervalo “próximo ao spot” — ±1.024 tick arrays. Além desse intervalo (para valores extremos de tick), o programa mantém uma conta de extensão:
pub struct TickArrayBitmapExtension {
pub pool_id: Pubkey,
pub positive_tick_array_bitmap: [[u64; 8]; 14],
pub negative_tick_array_bitmap: [[u64; 8]; 14],
}
Se o intervalo da sua posição for “normal”, você nunca precisará pensar na conta de extensão. Posições de intervalo completo (p. ex., (MIN_TICK, MAX_TICK)) exigem ela; o SDK resolve isso por você.
Posições
Uma posição CLMM é um conjunto de três contas mais uma mint:
NFT mint da posição
Uma mint SPL Token com supply 1. O endereço da mint é um PDA determinístico; o NFT da posição na carteira do dono é simplesmente uma ATA que guarda esse único token. Transferir o NFT é como uma posição muda de titular — o programa vincula a autorização ao portador atual do saldo da ATA do NFT, não a uma Pubkey armazenada no estado.
PersonalPositionState
Um por posição aberta. Indexado pela mint do 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 (obsoleto)
Versões anteriores do CLMM armazenavam bookkeeping agregado por (pool, tick_lower, tick_upper) em um PDA ProtocolPositionState. As versões mais recentes não criam nem leem mais essa conta. O slot ainda aparece nas listas de contas de OpenPosition / IncreaseLiquidity / DecreaseLiquidity como UncheckedAccount por compatibilidade de ABI, mas o programa não escreve nela. As contas existentes on-chain são vestigiais; o admin pode chamar CloseProtocolPosition para recuperar o rent delas.O bookkeeping agregado de intervalos agora é derivado diretamente dos dois ticks de endpoint (liquidity_gross, liquidity_net e os fee_growth_outside_* / reward_growths_outside_x64 por tick) no TickArrayState. A fórmula de crescimento de taxa interna fee_growth_inside = global − outside_lower − outside_upper continua funcionando sem uma conta de posição agregada.
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],
}
O buffer de observação do CLMM armazena um tick cumulativo, não um preço cumulativo. Consumidores externos calculam o preço médio geométrico em um intervalo a partir de (tick_cumulative[t1] − tick_cumulative[t0]) / (t1 − t0) e depois price = 1.0001 ** tick. Veja algorithms/clmm-math.
DynamicFeeConfig e DynamicFeeInfo
Os parâmetros de taxa dinâmica residem em dois lugares. O template reutilizável — DynamicFeeConfig — é gerenciado pelo admin e compartilhado entre os pools que aderem. O estado de execução por pool — DynamicFeeInfo — está embutido em PoolState e atualizado a cada 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],
}
Seed do PDA: ["dynamic_fee_config", index.to_be_bytes()]. Criado via create_dynamic_fee_config (restrito ao admin) e modificado via update_dynamic_fee_config. Um pool criado com enable_dynamic_fee = true copia os cinco parâmetros de calibração da config (filter_period, decay_period, reduction_factor, dynamic_fee_control, max_volatility_accumulator) para seu próprio DynamicFeeInfo no momento da criação; edições posteriores ao DynamicFeeConfig não afetam retroativamente pools já existentes.
DynamicFeeInfo (embutido em 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],
}
Os quatro campos inferiores são estado; os cinco superiores são parâmetros de calibração copiados do DynamicFeeConfig. O cálculo da taxa e as regras de decaimento estão documentados em products/clmm/math e products/clmm/fees.
Constantes utilizadas pela fórmula:
| Constante | Valor | Significado |
|---|
VOLATILITY_ACCUMULATOR_SCALE | 10_000 | Granularidade do acumulador de volatilidade |
REDUCTION_FACTOR_DENOMINATOR | 10_000 | Denominador de reduction_factor |
DYNAMIC_FEE_CONTROL_DENOMINATOR | 100_000 | Denominador de dynamic_fee_control |
MAX_FEE_RATE_NUMERATOR | 100_000 | Limite máximo de 10% sobre a taxa resultante |
LimitOrderState
Uma conta por ordem limitada aberta.
// 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],
}
Ciclo de vida:
- Abrir — o usuário chama
open_limit_order, deposita total_amount do token de entrada; a ordem é vinculada a uma coorte do TickState.
- (opcional) Aumentar / Diminuir —
increase_limit_order adiciona a total_amount; decrease_limit_order devolve os tokens não preenchidos (e qualquer saída liquidada até aquele ponto).
- Liquidar — quando a coorte é total ou parcialmente preenchida, o dono ou o keeper operacional chama
settle_limit_order para enviar os tokens de saída para a ATA do dono.
- Fechar — assim que
unfilled_amount == 0, a conta pode ser fechada. O rent sempre retorna ao owner.
Seed do PDA: [owner.as_ref(), limit_order_nonce.key().as_ref(), limit_order_nonce.order_nonce.to_be_bytes().as_ref()]. O PDA da ordem é, portanto, único por (owner, nonce_index, order_nonce).
LimitOrderNonce
Contador por (wallet, nonce_index) que permite a um único usuário executar múltiplos pipelines paralelos de ordens limitadas sem colisão de PDAs.
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],
}
Seed do PDA: [user_wallet.as_ref(), &[nonce_index]]. A maioria dos clientes usa nonce_index = 0 e deixa o order_nonce controlar a cardinalidade.
Derivando as contas principais
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;
}
As strings de seed exatas devem sempre ser verificadas contra o IDL on-chain e reference/program-addresses.
Referência rápida do ciclo de vida
| Evento | Contas criadas | Contas destruídas |
|---|
CreatePool | poolState, observation, token_0_vault, token_1_vault | — |
OpenPosition[WithToken22Nft] | NFT mint + ATA, personalPosition, possivelmente novos tickArrayState(s), tickArrayBitmapExtension se ainda não existir | — |
IncreaseLiquidity | Possivelmente novos tickArrayState(s) | — |
DecreaseLiquidity | — | Possivelmente limpa entradas de tick (mas o tickArrayState em si não é fechado) |
ClosePosition | — | NFT mint, personalPosition |
SwapV2 | Possivelmente novo tickArrayState | — |
OpenLimitOrder | limitOrderState, possivelmente limitOrderNonce (init-if-needed), possivelmente novo tickArrayState | — |
IncreaseLimitOrder | — | — |
DecreaseLimitOrder | — | Fecha limitOrderState se a ordem for totalmente consumida |
SettleLimitOrder | — | — |
CloseLimitOrder | — | limitOrderState (rent → owner) |
CreateDynamicFeeConfig | dynamicFeeConfig | — |
CreateCustomizablePool | poolState, observation, vaults — igual a CreatePool. Copia dynamicFeeConfig se enable_dynamic_fee = true. | — |
CollectRewards | — | — |
UpdateRewardInfos | — | — |
CloseProtocolPosition (admin) | — | protocolPositionState vestigial (rent → admin) |
As contas TickArrayState nunca são fechadas pelo programa — elas persistem durante toda a vida do pool. Uma vez que um tick array tenha sido inicializado, ele permanece on-chain mesmo quando todos os ticks dentro dele retornam a liquidity_gross == 0. Reutilizar um tick array existente é gratuito; apenas a primeira posição a tocar um array nunca inicializado paga seu rent.
O que ler e onde
Fontes: