Skip to main content
Esta página foi traduzida automaticamente por IA. A versão em inglês é a fonte oficial.Ver versão em inglês →
Esta página é operacional: fornece as fórmulas, convenções de ponto fixo e procedimentos passo a passo usados pelo programa CLMM. Para o raciocínio por trás da própria curva de liquidez concentrada — por que L = sqrt(x · y) importa — veja algorithms/clmm-math. Esta página assume que você já leu aquela.

Representação de sqrt-price

O CLMM armazena o preço como sqrt_price_x64 — a raiz quadrada do preço token1-por-token0, como um número de ponto fixo Q64.64: sqrt_price_x64=⌊p⋅264⌋\text{sqrt\_price\_x64} = \lfloor \sqrt{p} \cdot 2^{64} \rfloor onde p = token1_amount / token0_amount. Trabalhar em sqrt em vez de p lineariza a matemática do swap (os deltas de quantidade de token se tornam lineares em Δsqrt_price), e o x64 de ponto fixo mantém a precisão através de swaps multi-tick. A conversão tick ↔ sqrt-price é pré-computada via uma aproximação 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 uma exponenciação baseada em lookup em tick_math::get_sqrt_price_at_tick.

Liquidity como unidade canônica

Dentro de um intervalo [sqrt_a, sqrt_b] (com sqrt_a < sqrt_b) uma posição de liquidity L mapeia para quantidades de tokens da seguinte forma. Seja sqrt_c = sqrt_price_x64 o preço atual do pool. Todas as três identidades vêm do invariante x = L / sqrt_p, y = L · sqrt_p que a liquidez concentrada satisfaz dentro de um intervalo. Integradores normalmente querem o inverso: dada uma deposição de amount0 / amount1, calcular o máximo L que cabe no intervalo. O método LiquidityMath.getLiquidityFromTokenAmounts do SDK faz isso. A fórmula para o caso dentro do intervalo: 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) Qual lado se vincula determina a razão realmente consumida; o outro lado pode ter sobra.

Passo de swap de um único tick

Um swap procede em passos. Cada passo ou (a) consome toda a entrada disponível dentro do intervalo de tick atual sem cruzar um tick, ou (b) move o preço exatamente para o próximo tick inicializado. Dado o estado atual (sqrt_c, L) e um swap para baixo (token0 entra, token1 sai, sqrt_price diminui — o preço é cotado como token1/token0, então adicionar token0 o reduz), o próximo tick inicializado abaixo fica em sqrt_t < sqrt_c. Dentro deste micro-intervalo a relação entre entrada e preço é: Δ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}} e Δamount1=L⋅(sqrt_c−sqrt_t)\Delta\text{amount1} = L \cdot (\text{sqrt\_c} - \text{sqrt\_t}) Um swap com token1 como entrada é a imagem espelhada: sqrt_price sobe em direção ao próximo tick acima, e as duas fórmulas trocam qual endpoint é subtraído. O programa codifica a direção como zero_for_one (true quando o mint de entrada é token_mint_0), e um swap zero_for_one se limita a MIN_SQRT_PRICE_X64. O programa faz uma de duas coisas:
  • A entrada inteira cabe? Se a entrada restante (após taxa) for menor que Δamount0 para alcançar sqrt_t, resolva para o novo sqrt_c' exatamente: 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 um swap exato-entrada token0 → token1). O swap se completa neste passo sem cruzar um tick.
  • A entrada excede Δamount0? Defina sqrt_c' = sqrt_t, cruze o tick (aplique liquidity_net), decremente a entrada restante por Δamount0, incremente a saída por Δamount1, e repita.
Para a direção oposta (token1 → token0, preço descendo), as fórmulas têm sqrt_c e sqrt_t trocados e a inversão no outro slot. A implementação completa em Rust fica em raydium-clmm/programs/amm/src/libraries/swap_math.rs. A lógica lá corresponde exatamente ao SwapMath.computeSwapStep do Uniswap v3.

Taxas em cada passo

As taxas de negociação são retiradas da quantidade de entrada em cada passo, mesma convenção do CPMM:
A porção LP é dividida entre a liquidez atualmente dentro do intervalo atualizando o acumulador global de crescimento de taxa: fee_growth_globalin+=lp_portion⋅264L\text{fee\_growth\_global}_{\text{in}} \mathrel{+}= \text{lp\_portion} \cdot \frac{2^{64}}{L} — ou seja, é denominada em taxas por unidade de liquidez, Q64.64, de modo que uma posição de tamanho L_i que permaneceu dentro do intervalo durante este swap posteriormente lerá L_i · Δfee_growth_global / 2^{64} tokens devidos. As porções de protocolo e fundo acumulam em PoolState.protocol_fees_token_{0,1} e PoolState.fund_fees_token_{0,1} respectivamente, idêntico ao CPMM. Elas são coletadas por CollectProtocolFee / CollectFundFee.

Crescimento de taxa fora e dentro

A parte complicada da contabilidade de taxa do CLMM: uma posição ganha taxas apenas enquanto o preço do pool está dentro de seu intervalo. O pool rastreia taxas cumulativas globalmente; a posição precisa saber as taxas cumulativas enquanto dentro de seu intervalo específico. A solução é um acumulador baseado em tick. Cada tick armazena:
No momento da inicialização do tick:
  • Se o preço do pool está acima deste tick (tick_current >= this_tick), fee_growth_outside = fee_growth_global. (Tudo ganho até agora é “fora” — ou seja, abaixo — deste tick, relativo ao preço atual.)
  • Caso contrário fee_growth_outside = 0.
Quando o preço cruza um tick, o programa inverte o fee_growth_outside daquele tick: fee_growth_outside←fee_growth_global−fee_growth_outside\text{fee\_growth\_outside} \gets \text{fee\_growth\_global} - \text{fee\_growth\_outside} O invariante que isso preserva: para qualquer tick t, fee_growth_outside(t) é igual às taxas que acumularam enquanto tick_current estava no lado oposto de t. Crescimento de taxa dentro de um intervalo [tick_lower, tick_upper] é então derivado:
Esta é a fórmula de crescimento de taxa do Uniswap-v3, inalterada.

O que uma posição armazena e o que lê

Um PersonalPositionState armazena fee_growth_inside_0_last_x64 e fee_growth_inside_1_last_x64: os valores de fee_growth_inside na última vez que a posição foi tocada. Em qualquer toque subsequente (aumentar, diminuir, coletar), o programa:
  1. Calcula o fee_growth_inside_{0,1}_x64 atual usando a fórmula acima.
  2. Calcula Δ = fee_growth_inside_now − fee_growth_inside_last (subtração modular em u128).
  3. Adiciona Δ × position.liquidity / 2^{64} a tokens_fees_owed_{0,1}.
  4. Atualiza fee_growth_inside_last para o novo valor.
Tokens realmente saem dos vaults apenas em CollectFees / DecreaseLiquidity, contra tokens_fees_owed.

Recompensas

Cada um dos até 3 fluxos de recompensa do pool usa a mesma maquinaria de crescimento-dentro, em seu próprio acumulador reward_growth_global_x64. No momento da emissão: 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} — as emissões escalam inversamente com a liquidez ativa, então um pool mais denso paga a cada posição proporcionalmente menos por segundo, mas sobre mais posições no total. A recompensa por posição devida é 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} e é paga por DecreaseLiquidity / DecreaseLiquidityV2 — não há instrução de coleta de recompensa autônoma. Veja products/clmm/fees.

Exemplo trabalhado: swap exato-entrada

Suponha:
  • tick_spacing = 60
  • sqrt_price_x64 = 1 × 2^{64} — preço = 1.0, então tick_current = 0.
  • Liquidez ativa L = 1_000_000 × 2^{64}.
  • Próximo tick inicializado abaixo: t = −60 (sqrt_price_b ≈ 0.997005 × 2^{64}). Um swap com token0 como entrada se move para baixo, então o tick acima é irrelevante aqui.
  • Taxa de taxa de negociação: 500 (0.05%).
Usuário: SwapBaseInput exato-entrada 1.000 token0. Passo 1 — taxas:
Passo 2 — 999 cabe dentro do intervalo de tick atual?
999 < 3004.4, então a entrada inteira cabe sem cruzar o tick. Passo 3 — novo preço:
ou seja, sqrt_c' fica ligeiramente abaixo de sqrt_c, que é a direção correta: um swap token0 → token1 adiciona token0 ao pool e assim reduz o preço token1/token0. Passo 4 — quantidade de saída:
Após contabilizar arredondamento, o usuário recebe ≈ 998 token1. A taxa (1 token0) é dividida entre LP, protocolo e fundo por trade_fee_rate × protocol_fee_rate / 1e6 (e similar para fundo); a porção LP flui para fee_growth_global_0_x64.

Correspondência de ordem limitada durante swap

Quando um passo de swap cruza um tick que contém ordens limitadas abertas, essas ordens consomem entrada de swap antes da curva LP, ao preço exato do tick. A correspondência é FIFO dentro do tick por cohort order_phase.

Estado por-cohort em TickState

O layout de dois-cohort existe porque novas ordens podem ser abertas em um tick enquanto um cohort mais antigo ainda está sendo preenchido. Ordens recém-abertas se juntam a orders_amount e herdam o próximo order_phase; elas não podem preencher até que o cohort anterior seja totalmente consumido.

Passo de correspondência

Pseudo-código para a correspondência que acontece em cada cruzamento de tick durante um swap:
Tokens de saída indo para os proprietários de ordens limitadas não são transferidos por swap. Eles ficam virtualmente no vault de saída do pool até que o proprietário da ordem chame SettleLimitOrder (ou DecreaseLimitOrder). O pool simplesmente rastreia quanto do cohort agora está preenchido via unfilled_ratio_x64. Cada LimitOrderState armazena seu próprio snapshot (order_phase, unfilled_ratio_x64) no tempo de abertura, então a liquidação se reduz a:
Esta liquidação O(1) é o ponto inteiro do design de cohort — um tick pode preencher arbitrariamente muitas ordens sem gás por-ordem.

Interação com a curva LP

Em um passo de swap, a correspondência de ordem limitada acontece no tick (zero Δsqrt_price); o consumo da curva LP acontece entre ticks. A ordem é portanto:
  1. Cruze o tick t_cross (aplique a mudança LP liquidity_net primeiro, já que é assim que o Uniswap-V3 faz).
  2. Preencha quaisquer ordens limitadas sentadas em t_cross.
  3. Continue ao longo da curva LP para o próximo tick inicializado ou até a exaustão de swap_input.
Ordens limitadas assim dão aos traders mais liquidez efetiva exatamente ao preço do tick da ordem (um efeito de melhoria de preço), ao custo de LPs não ganharem taxas naquela porção do volume de swap — a porção de ordem limitada da negociação é sem taxa para o swapper, já que o colocador de ordem limitada está agindo como um maker. A sobretaxa de taxa dinâmica (se habilitada) ainda se aplica à porção LP do mesmo swap.

Derivação de taxa dinâmica

PoolState.dynamic_fee_info carrega o estado de volatilidade. Cada passo de swap calcula a taxa de taxa por-passo como: fee_ratetotal=trade_fee_rateconfig+dynamic_fee_control⋅(vol_acc⋅tick_spacing)2Dctrl⋅Svol2⏟sobretaxa dinaˆmica\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{sobretaxa dinâmica}} onde:
  • Dctrl=100,000D_{\text{ctrl}} = 100{,}000 — DYNAMIC_FEE_CONTROL_DENOMINATOR
  • Svol=10,000S_{\text{vol}} = 10{,}000 — VOLATILITY_ACCUMULATOR_SCALE
  • vol_acc é o acumulador por-swap após a regra de atualização abaixo
  • tick_spacing é de PoolState.tick_spacing
O resultado é limitado em 100,000/106=10%100{,}000 / 10^6 = 10\%.

Atualização do acumulador

Duas regras são aplicadas a cada swap, em ordem: Decaimento. O piso de referência decai com base no tempo desde a última atualização: vol_ref={0se Δt>decay_periodvol_accprev⋅reduction_factor10,000se filter_period<Δt≤decay_periodvol_refprevse Δt≤filter_period\text{vol\_ref} = \begin{cases} 0 & \text{se } \Delta t > \text{decay\_period} \\ \text{vol\_acc}_{\text{prev}} \cdot \dfrac{\text{reduction\_factor}}{10{,}000} & \text{se } \text{filter\_period} < \Delta t \le \text{decay\_period} \\ \text{vol\_ref}_{\text{prev}} & \text{se } \Delta t \le \text{filter\_period} \end{cases} Acumular. O novo acumulador é a referência mais a distância de tick percorrida desde o índice de referência 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á em unidades de tick-spacing, não ticks brutos: tref=⌊tick_current/tick_spacing⌋t_{\text{ref}} = \lfloor \text{tick\_current} / \text{tick\_spacing} \rfloor.

Por que parabólico em distância de tick

Elevar ao quadrado o acumulador significa que a taxa sobe como o quadrado de quão longe o preço caminhou de seu ponto de referência. Empiricamente isso corresponde ao escalonamento de variância do preço sob pressão de passeio aleatório: uma excursão de tick 2× implica 4× a volatilidade implícita, então cobra 4× a sobretaxa. O parâmetro dynamic_fee_control calibra o nível absoluto. A janela filter_period previne pequenas oscilações sub-segundo (por exemplo, bots MEV fazendo sandwich) de inflacionar o acumulador. A janela decay_period previne um único pico passado de cobrar taxas indefinidamente após o mercado ter se acalmado.

Robustez numérica

  • Todos os produtos intermediários passam por aritmética em forma de u128 ou u256. O CLMM usa helpers U128Sqrt e padrões FullMath::mulDiv diretamente portados do Uniswap v3.
  • O arredondamento de divisão é escolhido por-passo para impor o invariante k' ≥ k localmente. SwapBaseInput arredonda a saída para baixo; SwapBaseOutput arredonda a entrada para cima.
  • Cruzamentos de tick que reduzem PoolState.liquidity a zero são permitidos (o preço pode atravessar um “buraco de liquidez”) mas o swap simplesmente avança para o próximo tick inicializado sem consumir entrada, não cobrando taxa.
  • Guarda de overflow: sqrt_price_x64 é mantido no intervalo inclusivo [MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64] correspondendo a [MIN_TICK, MAX_TICK]. Um swap que empurraria além de qualquer limite reverte com SqrtPriceLimitOverflow.

Próximos passos

Fontes: