Skip to main content
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 es operativa: proporciona las fórmulas, convenciones de punto fijo y procedimientos utilizados por el programa CLMM. Para entender el razonamiento detrás de la curva de liquidez concentrada en sí — por qué L = sqrt(x · y) importa — consulta algorithms/clmm-math. Esta página asume que ya has leído esa sección.

Representación de sqrt-price

CLMM almacena el precio como sqrt_price_x64 — la raíz cuadrada del precio token1-por-token0, como un número de punto fijo Q64.64: sqrt_price_x64=⌊p⋅264⌋\text{sqrt\_price\_x64} = \lfloor \sqrt{p} \cdot 2^{64} \rfloor donde p = token1_amount / token0_amount. Trabajar en sqrt en lugar de p linealiza las matemáticas del swap (los deltas de montos de tokens se vuelven lineales en Δsqrt_price), y el punto fijo x64 mantiene la precisión a través de swaps multi-tick. La conversión tick ↔ sqrt-price se precomputa mediante una aproximación logarítmica bit-por-bit: sqrt_price_x64(t)≈264⋅(1.0001)t/2\text{sqrt\_price\_x64}(t) \approx 2^{64} \cdot (1.0001)^{t/2} implementada como una exponenciación basada en búsqueda en tick_math::get_sqrt_price_at_tick.

Liquidez como unidad canónica

Dentro de un rango [sqrt_a, sqrt_b] (con sqrt_a < sqrt_b) una posición de liquidez L se mapea a montos de tokens de la siguiente manera. Sea sqrt_c = sqrt_price_x64 el precio actual del pool. Las tres identidades provienen del invariante x = L / sqrt_p, y = L · sqrt_p que la liquidez concentrada satisface dentro de un rango. Los integradores típicamente quieren lo inverso: dado un depósito de amount0 / amount1, calcula el máximo L que cabe en el rango. El método LiquidityMath.getLiquidityFromTokenAmounts del SDK hace esto. La fórmula para el caso dentro del rango: 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) Cualquiera de los dos lados que sea restrictivo determina la proporción realmente consumida; el otro lado puede tener sobrante.

Paso de swap de un solo tick

Un swap procede en pasos. Cada paso ya sea (a) consume toda la entrada disponible dentro del rango de tick actual sin cruzar un tick, o (b) mueve el precio exactamente al siguiente tick inicializado. Dado el estado actual (sqrt_c, L) y un swap hacia abajo (token0 entra, token1 sale, sqrt_price disminuye — el precio se cotiza como token1/token0, así que agregar token0 lo baja), el siguiente tick inicializado por debajo se encuentra en sqrt_t < sqrt_c. Dentro de este micro-intervalo la relación entre entrada y precio es: Δ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}} y Δamount1=L⋅(sqrt_c−sqrt_t)\Delta\text{amount1} = L \cdot (\text{sqrt\_c} - \text{sqrt\_t}) Un swap con token1 de entrada es la imagen especular: sqrt_price sube hacia el siguiente tick por encima, y las dos fórmulas intercambian cuál extremo se resta. El programa codifica la dirección como zero_for_one (true cuando el mint de entrada es token_mint_0), y un swap zero_for_one se limita hacia MIN_SQRT_PRICE_X64. El programa hace una de dos cosas:
  • ¿Cabe toda la entrada? Si la entrada restante (después de comisión) es menor que Δamount0 para alcanzar sqrt_t, resuelve el nuevo sqrt_c' exactamente: 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}} (para un swap exacto-entrada token0 → token1). El swap se completa en este paso sin cruzar un tick.
  • ¿La entrada excede Δamount0? Establece sqrt_c' = sqrt_t, cruza el tick (aplica liquidity_net), decrementa la entrada restante por Δamount0, incrementa la salida por Δamount1, y repite.
Para la dirección opuesta (token1 → token0, precio bajando), las fórmulas tienen sqrt_c y sqrt_t intercambiados y la inversión en el otro lugar. La implementación completa en Rust vive en raydium-clmm/programs/amm/src/libraries/swap_math.rs. La lógica allí coincide uno-a-uno con SwapMath.computeSwapStep de Uniswap v3.

Comisiones en cada paso

Las comisiones comerciales se toman de la cantidad de entrada en cada paso, la misma convención que CPMM:
La porción LP se divide entre la liquidez actualmente dentro del rango actualizando el acumulador global de crecimiento de comisiones: fee_growth_globalin+=lp_portion⋅264L\text{fee\_growth\_global}_{\text{in}} \mathrel{+}= \text{lp\_portion} \cdot \frac{2^{64}}{L} — es decir, se denomina en comisiones por unidad de liquidez, Q64.64, de modo que una posición de tamaño L_i que se mantuvo dentro del rango durante este swap posteriormente leerá L_i · Δfee_growth_global / 2^{64} tokens adeudados. Las porciones de protocolo y fondo se acumulan en PoolState.protocol_fees_token_{0,1} y PoolState.fund_fees_token_{0,1} respectivamente, idéntico a CPMM. Se barren mediante CollectProtocolFee / CollectFundFee.

Crecimiento de comisiones fuera y dentro

La parte complicada de la contabilidad de comisiones de CLMM: una posición gana comisiones solo mientras el precio del pool está dentro de su rango. El pool rastrea comisiones acumulativas globalmente; la posición necesita saber las comisiones acumulativas mientras está dentro de su rango específico. La solución es un acumulador basado en ticks. Cada tick almacena:
En el momento de la inicialización del tick:
  • Si el precio del pool está por encima de este tick (tick_current >= this_tick), fee_growth_outside = fee_growth_global. (Todo lo ganado hasta ahora está “fuera” — es decir, por debajo — de este tick, relativo al precio actual.)
  • Si no, fee_growth_outside = 0.
Cuando el precio cruza un tick, el programa invierte el fee_growth_outside de ese tick: fee_growth_outside←fee_growth_global−fee_growth_outside\text{fee\_growth\_outside} \gets \text{fee\_growth\_global} - \text{fee\_growth\_outside} El invariante que esto preserva: para cualquier tick t, fee_growth_outside(t) es igual a las comisiones que se acumularon mientras tick_current estaba en el lado opuesto de t. Crecimiento de comisiones dentro de un rango [tick_lower, tick_upper] se deriva entonces:
Esta es la fórmula de crecimiento de comisiones de Uniswap-v3, sin cambios.

Qué almacena una posición y qué lee

Un PersonalPositionState almacena fee_growth_inside_0_last_x64 y fee_growth_inside_1_last_x64: los valores de fee_growth_inside en la última vez que se tocó la posición. En cualquier toque posterior (aumentar, disminuir, cobrar), el programa:
  1. Calcula el fee_growth_inside_{0,1}_x64 actual usando la fórmula anterior.
  2. Calcula Δ = fee_growth_inside_now − fee_growth_inside_last (resta modular en u128).
  3. Suma Δ × position.liquidity / 2^{64} a tokens_fees_owed_{0,1}.
  4. Actualiza fee_growth_inside_last al nuevo valor.
Los tokens realmente se mueven fuera de los vaults solo en CollectFees / DecreaseLiquidity, contra tokens_fees_owed.

Recompensas

Cada uno de los hasta 3 flujos de recompensas del pool utiliza la misma maquinaria de crecimiento-dentro, en su propio acumulador reward_growth_global_x64. En el momento de emisión: 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} — las emisiones se escalan inversamente con la liquidez activa, así que un pool más denso paga a cada posición proporcionalmente menos por segundo, pero sobre más posiciones en total. La recompensa adeudada por posición es 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} y se paga mediante DecreaseLiquidity / DecreaseLiquidityV2 — no hay instrucción independiente de cobro de recompensas. Consulta products/clmm/fees.

Ejemplo trabajado: swap de entrada exacta

Supongamos:
  • tick_spacing = 60
  • sqrt_price_x64 = 1 × 2^{64} — precio = 1.0, así que tick_current = 0.
  • Liquidez activa L = 1_000_000 × 2^{64}.
  • Siguiente tick inicializado por debajo: t = −60 (sqrt_price_b ≈ 0.997005 × 2^{64}). Un swap con token0 de entrada se mueve hacia abajo, así que el tick por encima es irrelevante aquí.
  • Tasa de comisión comercial: 500 (0.05%).
Usuario: SwapBaseInput entrada exacta 1,000 token0. Paso 1 — comisiones:
Paso 2 — ¿caben 999 dentro del rango de tick actual?
999 < 3004.4, así que toda la entrada cabe sin cruzar el tick. Paso 3 — nuevo precio:
es decir, sqrt_c' cae ligeramente por debajo de sqrt_c, que es la dirección correcta: un swap token0 → token1 agrega token0 al pool y por lo tanto baja el precio token1/token0. Paso 4 — monto de salida:
Después de contabilizar el redondeo, el usuario recibe ≈ 998 token1. La comisión (1 token0) se divide entre LP, protocolo y fondo por trade_fee_rate × protocol_fee_rate / 1e6 (y similar para fondo); la porción LP fluye hacia fee_growth_global_0_x64.

Coincidencia de órdenes limitadas durante swap

Cuando un paso de swap cruza un tick que contiene órdenes limitadas abiertas, esas órdenes consumen entrada de swap antes de que lo haga la curva LP, al precio exacto del tick. La coincidencia es FIFO dentro del tick por cohorte order_phase.

Estado por cohorte en TickState

El diseño de dos cohortes existe porque nuevas órdenes pueden abrirse en un tick mientras una cohorte más antigua aún se está llenando. Las órdenes recién abiertas se unen a orders_amount y heredan el siguiente order_phase; no pueden llenarse hasta que la cohorte anterior se consume completamente.

Paso de coincidencia

Pseudo-código para la coincidencia que ocurre en cada cruce de tick durante un swap:
Los tokens de salida que van a los propietarios de órdenes limitadas no se transfieren por swap. Se sientan virtualmente en el vault de salida del pool hasta que el propietario de la orden llama a SettleLimitOrder (o DecreaseLimitOrder). El pool simplemente rastrea cuánto de la cohorte ahora está lleno mediante unfilled_ratio_x64. Cada LimitOrderState almacena su propia instantánea (order_phase, unfilled_ratio_x64) en el momento de apertura, así que la liquidación se reduce a:
Esta liquidación O(1) es todo el punto del diseño de cohortes — un tick puede llenar arbitrariamente muchas órdenes sin gas por orden.

Interacción con la curva LP

En un paso de swap, la coincidencia de órdenes limitadas ocurre en el tick (cero Δsqrt_price); el consumo de la curva LP ocurre entre ticks. El orden es por lo tanto:
  1. Cruza el tick t_cross (aplica el cambio de LP liquidity_net primero, ya que así es como Uniswap-V3 lo hace).
  2. Llena cualquier orden limitada sentada en t_cross.
  3. Continúa a lo largo de la curva LP al siguiente tick inicializado o al agotamiento de swap_input.
Las órdenes limitadas por lo tanto dan a los traders más liquidez efectiva exactamente al precio del tick de la orden (un efecto de mejora de precio), al costo de que los LP no ganen comisiones en esa porción del volumen de swap — la porción de orden limitada del trade es sin comisión para el swapper, ya que el colocador de orden limitada actúa como creador de mercado. El recargo de comisión dinámica (si está habilitado) aún se aplica a la porción LP del mismo swap.

Derivación de comisión dinámica

PoolState.dynamic_fee_info lleva el estado de volatilidad. Cada paso de swap calcula la tasa de comisión por paso como: fee_ratetotal=trade_fee_rateconfig+dynamic_fee_control⋅(vol_acc⋅tick_spacing)2Dctrl⋅Svol2⏟recargo dinaˊmico\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{recargo dinámico}} donde:
  • Dctrl=100,000D_{\text{ctrl}} = 100{,}000 — DYNAMIC_FEE_CONTROL_DENOMINATOR
  • Svol=10,000S_{\text{vol}} = 10{,}000 — VOLATILITY_ACCUMULATOR_SCALE
  • vol_acc es el acumulador por swap después de la regla de actualización a continuación
  • tick_spacing es de PoolState.tick_spacing
El resultado se limita en 100,000/106=10%100{,}000 / 10^6 = 10\%.

Actualización del acumulador

Se aplican dos reglas en cada swap, en orden: Decaimiento. El piso de referencia decae basado en el tiempo desde la última actualización: vol_ref={0si Δt>decay_periodvol_accprev⋅reduction_factor10,000si filter_period<Δt≤decay_periodvol_refprevsi Δt≤filter_period\text{vol\_ref} = \begin{cases} 0 & \text{si } \Delta t > \text{decay\_period} \\ \text{vol\_acc}_{\text{prev}} \cdot \dfrac{\text{reduction\_factor}}{10{,}000} & \text{si } \text{filter\_period} < \Delta t \le \text{decay\_period} \\ \text{vol\_ref}_{\text{prev}} & \text{si } \Delta t \le \text{filter\_period} \end{cases} Acumular. El nuevo acumulador es la referencia más la distancia de tick recorrida desde el índice de referencia anterior: 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}}) está en unidades de tick-spacing, no ticks crudos: tref=⌊tick_current/tick_spacing⌋t_{\text{ref}} = \lfloor \text{tick\_current} / \text{tick\_spacing} \rfloor.

Por qué parabólico en distancia de tick

Elevar al cuadrado el acumulador significa que la comisión sube como el cuadrado de cuán lejos ha caminado el precio desde su punto de referencia. Empíricamente esto coincide con el escalado de varianza del precio bajo presión de caminata aleatoria: una excursión de tick 2× implica 4× la volatilidad implícita, así que cobra 4× el recargo. El parámetro dynamic_fee_control calibra el nivel absoluto. La ventana filter_period previene pequeñas oscilaciones sub-segundo (por ejemplo, bots MEV sándwich) de inflar el acumulador. La ventana decay_period previene que un único pico pasado cobre comisiones indefinidamente después de que el mercado se haya calmado.

Robustez numérica

  • Todos los productos intermedios pasan por aritmética de forma u128 o u256. CLMM usa ayudantes U128Sqrt y patrones FullMath::mulDiv directamente portados de Uniswap v3.
  • El redondeo de división se elige por paso para hacer cumplir el invariante k' ≥ k localmente. SwapBaseInput redondea la salida hacia abajo; SwapBaseOutput redondea la entrada hacia arriba.
  • Los cruces de tick que dejan PoolState.liquidity en cero están permitidos (el precio puede atravesar un “agujero de liquidez”) pero el swap simplemente avanza al siguiente tick inicializado sin consumir entrada, sin cobrar comisión.
  • Guardia de desbordamiento: sqrt_price_x64 se mantiene en el rango inclusivo [MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64] correspondiente a [MIN_TICK, MAX_TICK]. Un swap que empujaría más allá de cualquiera de los límites revierte con SqrtPriceLimitOverflow.

Dónde ir a continuación

Fuentes: