Esta página fue traducida automáticamente por IA. La versión en inglés es la fuente autorizada.Ver versión en inglés →
Esta página describe el esquema y rol de cada cuenta. Las semillas canónicas están listadas en reference/program-addresses. Un pool CLMM utiliza más cuentas que un pool CPMM porque la liquidez se almacena de forma dispersa a lo largo del rango de ticks; comprender esa dispersión es el tema central de esta página.
Inventario de cuentas
Un pool CLMM activo se describe mediante las siguientes familias de cuentas. Todas pertenecen al programa CLMM, excepto los dos mints y sus vaults.
| Cuenta | Propósito | Cantidad por pool |
|---|
AmmConfig | Nivel de comisión: tasa de comisión de trading, porcentaje de protocolo, porcentaje de fondo, tick-spacing por defecto. Compartida entre todos los pools de este nivel. | 1 (compartida) |
PoolState | sqrt_price_x64 actual, tick actual, liquidez total, acumuladores globales de comisiones, información de recompensas, puntero de observación. | 1 |
TickArrayState | Un bloque de TICK_ARRAY_SIZE ticks adyacentes. Solo se inicializa bajo demanda. | 0 ≤ N ≤ rango |
TickArrayBitmapExtension | Bitmap de desbordamiento que registra qué tick arrays existen más allá del bitmap inline de PoolState. | 0 o 1 |
PersonalPositionState | Una por posición de LP. Almacena el rango, la liquidez y el último crecimiento de comisiones/recompensas visto. Authority = propietario del NFT. | 1 por posición |
| NFT mint de posición | Mint con supply 1, asociada a PersonalPositionState. Transferir el NFT transfiere la posición. | 1 por posición |
ObservationState | Buffer circular de observaciones de precio para el TWAP. | 1 |
token_0_vault, token_1_vault | Cuentas de token que contienen los saldos del pool. Propiedad de la authority del pool. | 2 |
DynamicFeeConfig | Conjunto de parámetros reutilizable para el mecanismo de comisión dinámica. Los pools creados con create_customizable_pool pueden activarlo. Administrado por el admin. | compartida (por índice) |
LimitOrderState | Una por orden límite abierta. Registra el propietario, el tick, la dirección, el importe total y el snapshot del output liquidado. | 1 por orden |
LimitOrderNonce | Contador por (wallet, nonce_index) que genera PDAs de orden únicas. | 1 por (wallet, índice) |
PoolState
El estado en vivo del pool, leído en cada swap y en cada cambio de posición.
// 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 con los que interactuarás directamente:
sqrt_price_x64 y tick_current representan el estado de precio del pool. Se actualizan juntos en cada swap. tick_current es el entero inferior de log_{1.0001}(price).
liquidity es la liquidez activa: la suma de los valores L de todas las posiciones cuyo rango contiene a tick_current. Cambia cada vez que un swap cruza un tick y cada vez que se abre, cierra o redimensiona una posición.
fee_growth_global_{0,1}_x64 son las comisiones acumuladas por unidad de liquidez a lo largo de toda la historia del pool. Las posiciones los leen para calcular lo que se les debe.
tick_spacing queda fijado al AmmConfig durante la inicialización y nunca cambia. Determina qué índices de tick pueden ser puntos extremos de una posición.
tick_array_bitmap es un bitmap inline que cubre el rango de ticks más habitual alrededor del precio spot. Para pools con posiciones muy alejadas del centro, el seguimiento del desbordamiento vive en la cuenta separada TickArrayBitmapExtension.
fee_on se fija en el momento de la creación del pool. El valor 0 (FromInput) reproduce el comportamiento clásico de Uniswap V3. Los valores 1 y 2 dirigen la comisión de swap a un solo lado del libro — consulta products/clmm/fees para ver los compromisos de diseño.
dynamic_fee_info almacena el estado de volatilidad para el recargo de comisión dinámica. Cuando está habilitado, cada swap recalcula un dynamic_fee_component sobre AmmConfig.trade_fee_rate. El esquema se documenta en DynamicFeeInfo más abajo; los pools sin comisión dinámica dejan toda la estructura en cero.
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],
}
Conjunto típico de niveles de comisión CLMM publicados (verifica contra GET https://api-v3.raydium.io/main/clmm-config):
| Índice | trade_fee_rate | Tick spacing | Uso típico |
|---|
| 0 | 100 (0,01%) | 1 | Pares estables, USDC/USDT |
| 1 | 500 (0,05%) | 10 | Blue-chips correlacionados |
| 2 | 2_500 (0,25%) | 60 | Pares estándar |
| 3 | 10_000 (1,00%) | 120 | Activos volátiles o de cola larga |
protocol_fee_rate y fund_fee_rate son fracciones de la comisión de trading; misma convención que en CPMM. Consulta products/clmm/fees.
TickArrayState
El CLMM no almacena un registro por tick individual, ya que eso implicaría miles de millones de cuentas. En cambio, agrupa TICK_ARRAY_SIZE ticks adyacentes (inicializados o no, típicamente 60 u 88 según la versión del programa) en un TickArrayState que se crea de forma diferida en el primer 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],
}
Los cuatro campos de orden límite son cero en cualquier tick que nunca haya sido usado para una orden límite. Cuando se abren órdenes en un tick, el programa las gestiona como una secuencia de cohortes:
order_phase es el identificador de cohorte. Se incrementa cada vez que una cohorte pasa de «totalmente sin ejecutar» a «parcialmente ejecutada».
orders_amount es el total en tokens de entrada de la cohorte actual (la más reciente).
part_filled_orders_remaining lleva el seguimiento de la cohorte anterior que está siendo ejecutada en curso por los swaps activos.
unfilled_ratio_x64 es un multiplicador Q64.64 asociado a la cohorte: cuando un swap ejecuta el X% de la cohorte, el ratio se multiplica por (1 − X). Cada orden abierta almacena su propio snapshot de (order_phase, unfilled_ratio_x64) en el momento de apertura, por lo que la liquidación se reduce a comparar snapshots.
Reglas:
- Un tick t que sea extremo de posición debe satisfacer
t % tick_spacing == 0. El programa rechaza posiciones que no respeten el espaciado.
- El array de un tick se ubica en
floor(t / (TICK_ARRAY_SIZE * tick_spacing)) * (TICK_ARRAY_SIZE * tick_spacing).
- Un tick array se inicializa de forma diferida: la primera posición o swap que toca un array no inicializado lo crea y paga el alquiler.
- El programa nunca cierra un tick array. Una vez asignado, persiste durante toda la vida del pool, incluso después de que todos los ticks dentro de él vuelvan a
liquidity_gross == 0. Las posiciones y swaps posteriores reutilizan la cuenta existente sin coste adicional de alquiler. No existe una ruta de limpieza de tick arrays derivada de ClosePosition.
TickArrayBitmapExtension
PoolState.tick_array_bitmap (inline) cubre el rango «cercano al spot» — ±1.024 tick arrays. Fuera de ese rango (para valores de tick extremos), el programa mantiene una cuenta de extensión:
pub struct TickArrayBitmapExtension {
pub pool_id: Pubkey,
pub positive_tick_array_bitmap: [[u64; 8]; 14],
pub negative_tick_array_bitmap: [[u64; 8]; 14],
}
Si el rango de tu posición es «normal», nunca tendrás que pensar en la cuenta de extensión. Las posiciones de rango completo (p. ej., (MIN_TICK, MAX_TICK)) sí la requieren; el SDK la resuelve por ti.
Posiciones
Una posición CLMM es un conjunto de tres cuentas más un mint:
NFT mint de posición
Un mint de token SPL con supply 1. La dirección del mint es un PDA determinista; el NFT de posición en la wallet del propietario es simplemente una ATA que contiene ese único token. Transferir el NFT es el mecanismo por el que cambia de manos una posición — el programa vincula la autorización al titular actual del saldo en la ATA del NFT, no a una Pubkey almacenada en el estado.
PersonalPositionState
Una por posición abierta. Su clave se deriva del mint del 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)
Las versiones anteriores del CLMM almacenaban la contabilidad agregada por (pool, tick_lower, tick_upper) en un PDA llamado ProtocolPositionState. Las versiones más recientes ya no crean ni leen esta cuenta. El slot sigue apareciendo en las listas de cuentas de OpenPosition / IncreaseLiquidity / DecreaseLiquidity como UncheckedAccount por compatibilidad con el ABI, pero el programa no escribe en él. Las cuentas existentes en la cadena son vestigiales; el admin puede llamar a CloseProtocolPosition para recuperar el alquiler.La contabilidad agregada de rango ahora se deriva directamente de los dos ticks extremos (liquidity_gross, liquidity_net y los campos fee_growth_outside_* / reward_growths_outside_x64 por tick) en TickArrayState. La fórmula de crecimiento de comisiones dentro del rango fee_growth_inside = global − outside_lower − outside_upper sigue funcionando sin necesidad de una cuenta de posición 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],
}
El buffer de observaciones del CLMM almacena un tick acumulado, no un precio acumulado. Los consumidores externos calculan el precio de media geométrica sobre un intervalo a partir de (tick_cumulative[t1] − tick_cumulative[t0]) / (t1 − t0) y luego price = 1.0001 ** tick. Consulta algorithms/clmm-math.
DynamicFeeConfig y DynamicFeeInfo
Los parámetros de comisión dinámica viven en dos lugares. La plantilla reutilizable — DynamicFeeConfig — es administrada por el admin y compartida entre los pools que la activan. El estado de ejecución por pool — DynamicFeeInfo — está embebido en PoolState y se actualiza en 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],
}
Semilla PDA: ["dynamic_fee_config", index.to_be_bytes()]. Se crea mediante create_dynamic_fee_config (solo admin) y se modifica con update_dynamic_fee_config. Un pool creado con enable_dynamic_fee = true copia los cinco parámetros de calibración de la config (filter_period, decay_period, reduction_factor, dynamic_fee_control, max_volatility_accumulator) en su propio DynamicFeeInfo en el momento de la creación; las ediciones posteriores al DynamicFeeConfig no afectan retroactivamente a los pools existentes.
DynamicFeeInfo (embebido en 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],
}
Los cuatro campos inferiores son estado; los cinco superiores son la calibración copiada de DynamicFeeConfig. La matemática de la comisión y las reglas de decaimiento están documentadas en products/clmm/math y products/clmm/fees.
Constantes usadas por la fórmula:
| Constante | Valor | Significado |
|---|
VOLATILITY_ACCUMULATOR_SCALE | 10_000 | Granularidad del acumulador de volatilidad |
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 | Límite estricto del 10% sobre la tasa de comisión resultante |
LimitOrderState
Una cuenta por orden límite abierta.
// 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:
- Apertura — el usuario llama a
open_limit_order, deposita total_amount del token de entrada y la orden queda vinculada a una cohorte de TickState.
- (Opcional) Aumento / Reducción —
increase_limit_order añade al total_amount; decrease_limit_order devuelve los tokens sin ejecutar (y cualquier output liquidado hasta ese momento).
- Liquidación — cuando la cohorte está total o parcialmente ejecutada, el propietario o el keeper operacional llama a
settle_limit_order para enviar los tokens de salida a la ATA del propietario.
- Cierre — una vez que
unfilled_amount == 0, la cuenta puede cerrarse. El alquiler siempre retorna al owner.
Semilla PDA: [owner.as_ref(), limit_order_nonce.key().as_ref(), limit_order_nonce.order_nonce.to_be_bytes().as_ref()]. El PDA de la orden es por tanto único por (owner, nonce_index, order_nonce).
LimitOrderNonce
Contador por (wallet, nonce_index) que permite a un mismo usuario ejecutar múltiples pipelines paralelas de órdenes límite sin colisionar en los 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],
}
Semilla PDA: [user_wallet.as_ref(), &[nonce_index]]. La mayoría de los clientes usan nonce_index = 0 y dejan que order_nonce gestione la cardinalidad.
Derivar las cuentas clave
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;
}
Las cadenas de semilla exactas siempre deben verificarse contra el IDL on-chain y reference/program-addresses.
Referencia rápida del ciclo de vida
| Evento | Cuentas creadas | Cuentas destruidas |
|---|
CreatePool | poolState, observation, token_0_vault, token_1_vault | — |
OpenPosition[WithToken22Nft] | NFT mint + ATA, personalPosition, posiblemente nuevos tickArrayState(s), tickArrayBitmapExtension si no existe aún | — |
IncreaseLiquidity | Posiblemente nuevos tickArrayState(s) | — |
DecreaseLiquidity | — | Posiblemente limpia entradas de tick (pero el tickArrayState en sí no se cierra) |
ClosePosition | — | NFT mint, personalPosition |
SwapV2 | Posiblemente nuevo tickArrayState | — |
OpenLimitOrder | limitOrderState, posiblemente limitOrderNonce (init-if-needed), posiblemente nuevo tickArrayState | — |
IncreaseLimitOrder | — | — |
DecreaseLimitOrder | — | Cierra limitOrderState si la orden está totalmente consumida |
SettleLimitOrder | — | — |
CloseLimitOrder | — | limitOrderState (alquiler → owner) |
CreateDynamicFeeConfig | dynamicFeeConfig | — |
CreateCustomizablePool | poolState, observation, vaults — igual que CreatePool. Copia dynamicFeeConfig si enable_dynamic_fee = true. | — |
CollectRewards | — | — |
UpdateRewardInfos | — | — |
CloseProtocolPosition (admin) | — | protocolPositionState vestigial (alquiler → admin) |
Las cuentas TickArrayState nunca son cerradas por el programa — persisten durante toda la vida del pool. Una vez inicializado un tick array, permanece en la cadena aunque todos los ticks dentro de él vuelvan a liquidity_gross == 0. Reutilizar un tick array existente es gratuito; solo la primera posición que toca un array nunca inicializado paga su alquiler.
Dónde leer cada tema
Fuentes: