메인 콘텐츠로 건너뛰기

Documentation Index

Fetch the complete documentation index at: https://docs.raydium.io/llms.txt

Use this file to discover all available pages before exploring further.

이 페이지는 AI 자동 번역입니다. 모든 내용은 영문판을 기준으로 합니다.영문판 보기 →
이 페이지는 CLMM의 유도 과정을 정리했습니다. 온체인 구현은 products/clmm/math를 참고하세요(이 페이지를 인용함). 틱 격자의 배경은 products/clmm/ticks-and-positions에서 확인할 수 있습니다.

Sqrt-price를 쓰는 이유

Uniswap v3 계열의 CLMM은 가격을 제곱근으로 표현하며, 고정 소수점 Q64.64로 저장합니다:
sqrt_price_x64 = floor(sqrt(price) · 2^64)
세 가지 이유가 있습니다:
  1. 선형 유동성 수학. 가격 범위 내 token0 또는 token1의 양은 price의 함수가 아니라 sqrt_price의 선형 함수로 표현됩니다. sqrt_price를 저장하면 스왑 스텝에서 제곱근을 계산할 필요 없이 선형 공식을 평가할 수 있습니다.
  2. 오버플로우 방지. sqrt_price · L은 모든 합리적인 매개변수에서 u256에 맞지만, price · L은 훨씬 빨리 오버플로우할 수 있습니다.
  3. 틱 수학의 균일성. 틱이 1.0001^i로 정의되므로, sqrt(price) = 1.00005^i도 정확한 1.00005의 거듭제곱입니다. 각 틱 크로스는 sqrt_price_x64 공간에서 작은 곱셈으로 변환됩니다.
가격과 sqrt-price는 일대일 대응이며, 변환은 price = (sqrt_price_x64 / 2^64)^2입니다.

틱 격자

가격은 격자로 이산화됩니다:
price(tick_i) = 1.0001^i
tick_ii32입니다. 활성 범위는 [MIN_TICK, MAX_TICK] = [−443636, 443636]이며, 가격 범위는 대략 [2^−128, 2^128]입니다. 각 풀의 tick_spacing은 수수료 등급에 따라 설정됩니다: 좁은 쌍의 경우 더 작은 간격(예: 스테이블코인 0.01% 등급은 간격 1), 변동성 높은 쌍의 경우 더 큰 간격(0.25% 등급은 60, 1% 등급은 120). 포지션의 tick_lowertick_uppertick_spacing에 정렬되어야 합니다. 풀의 활성 틱(유동성이 시작 또는 종료되는 틱)은 스왑 스텝이 관심 있는 유일한 틱입니다.

유동성에서 양으로의 변환

유동성 L과 가격 범위 [sqrt_lo, sqrt_hi](모두 sqrt_price 값)를 가진 포지션:
풀 상태Token0 양Token1 양
범위 위 가격 (sqrt_p ≥ sqrt_hi)0L · (sqrt_hi − sqrt_lo)
범위 내 가격L · (sqrt_hi − sqrt_p) / (sqrt_p · sqrt_hi)L · (sqrt_p − sqrt_lo)
범위 아래 가격 (sqrt_p ≤ sqrt_lo)L · (sqrt_hi − sqrt_lo) / (sqrt_lo · sqrt_hi)0
유도: CPMM 불변식을 로컬에서 미분합니다. 단일 틱 범위 내에서 포지션은 가상 자산 (x_v, y_v)를 사용한 CPMM처럼 동작하며, 풀의 현재 (sqrt_p, L)L = sqrt(x_v · y_v)와 일치하도록 선택됩니다. sqrt_p에서 범위 경계까지 적분하면 위의 양을 얻습니다. 역 공식(주어진 amount0 또는 amount1에 대해 포지션을 민팅할 때 사용):
L_from_amount0(amount0, sqrt_lo, sqrt_hi, sqrt_p) =
    amount0 · sqrt_p · sqrt_hi / (sqrt_hi − sqrt_p)

L_from_amount1(amount1, sqrt_lo, sqrt_hi, sqrt_p) =
    amount1 / (sqrt_p − sqrt_lo)

// 범위 내 포지션으로의 대칭 입금의 경우, 최소값을 취합니다.
L = min(L_from_amount0, L_from_amount1)

단일 틱 스왑 스텝

단일 틱 범위 내에서 풀은 CPMM처럼 동작합니다. 현재 sqrt_p와 목표 sqrt_target이 주어지면:
Δamount0_step = L · (sqrt_target − sqrt_p) / (sqrt_p · sqrt_target)     // token0으로 스왑하는 경우
Δamount1_step = L · (sqrt_target − sqrt_p)                              // token1으로 스왑하는 경우

정확한 입력 스텝

Δin_remaining이 주어질 때:
// 틱 경계까지 채운 경우 예상 새로운 sqrt_p:
sqrt_after_full = sqrt_target
amount_to_full  = Δamount_in_to_reach(sqrt_p → sqrt_target)

if Δin_remaining ≥ amount_to_full:
    // 나머지 버킷 소비
    sqrt_p'         = sqrt_target
    Δin_consumed    = amount_to_full
    Δout            = amount_out_at_boundary
else:
    // 크로스하지 않음; 최종 sqrt_p에 대해 풀이
    sqrt_p'         = L · sqrt_p / (L + Δin_remaining · sqrt_p)      // 0→1 스왑의 경우
    Δin_consumed    = Δin_remaining
    Δout            = L · (sqrt_p − sqrt_p')                          // Δsqrt에 비례
0→1 스왑은 sqrt_p를 낮춥니다(token0을 팔 때 가격이 하락). 1→0 스왑은 이를 높입니다. 공식은 sqrt_psqrt_target이 바뀌면 대칭입니다.

정확한 출력 스텝

동일한 구조로 Δin에 대해 풀이합니다.

다중 틱 스왑 루프

스왑은 입력이 소진되거나 가격 제한에 도달할 때까지 틱을 반복합니다:
while Δin_remaining > 0 and sqrt_p != sqrt_price_limit:
    next_tick = find_next_initialized_tick(pool.tick_current, direction)
    sqrt_target = min(next_tick.sqrt_price, sqrt_price_limit)       // 방향에 따라

    (Δin, Δout, sqrt_p') = single_step(sqrt_p, sqrt_target, L, Δin_remaining)

    Δin_remaining -= Δin
    accumulated_out += Δout

    if sqrt_p' == next_tick.sqrt_price:
        // 틱 크로스
        L += next_tick.liquidity_net * direction_sign
        flip_fee_growth_outside(next_tick)
        match_limit_orders_at_tick(next_tick, ...)        // products/clmm/math 참고
        pool.tick_current = next_tick.tick_index
    sqrt_p = sqrt_p'
single_step은 풀의 현재 L을 사용합니다. L초기화된 틱을 크로스할 때만 변합니다. 틱 간의 유동성은 일정하며, 이것이 스텝 수학이 닫힌 형태를 갖는 이유입니다. 틱의 liquidity_net은 그 틱에서 시작하는 포지션 유동성의 합에서 그 틱에서 끝나는 포지션 유동성을 뺀 값입니다. 위쪽으로 크로스하면 liquidity_net을 더하고, 아래쪽으로 크로스하면 뺍니다. 풀이 틱에서 제한 주문을 열고 있을 때, 틱 크로스 스텝은 또한 그 주문들을 채우기 위해 스왑 입력의 일부를 소비합니다(코호트별 FIFO). 매칭 알고리즘과 기본 스텝 위에 적용될 수 있는 동적 수수료 할증은 products/clmm/math에 문서화되어 있습니다. 위의 닫힌 형태 단일 스텝 공식은 변하지 않습니다.

수수료 성장 누적기

CLMM은 활성 유동성 단위당 수수료를 추적하며, 양쪽, 전역 및 틱별로 추적합니다:
fee_growth_global_0_x64     // Q64.64, 단조증가
fee_growth_global_1_x64
tick.fee_growth_outside_0_x64   // "이 틱이 활성 범위 밖에 있던 동안 누적된 수수료"
tick.fee_growth_outside_1_x64
single_step에서:
step_lp_fee = (Δin · fee_rate) · (1 − protocol_fraction − fund_fraction)
fee_growth_global += step_lp_fee · 2^64 / L     // 입력 측에만 해당
(다른 쪽의 fee_growth_global은 이 스텝에서 움직이지 않으므로, 그 쪽의 토큰이 입력으로 지급되지 않았습니다.) 틱을 크로스할 때 프로그램은 fee_growth_outside뒤집습니다:
tick.fee_growth_outside_0_x64 = fee_growth_global_0_x64 − tick.fee_growth_outside_0_x64
tick.fee_growth_outside_1_x64 = fee_growth_global_1_x64 − tick.fee_growth_outside_1_x64
“Outside”는 tick_current에 상대적입니다. tick_current가 틱 위에 있으면 outside는 “아래”를 의미합니다. tick_current가 아래에 있으면 outside는 “위”를 의미합니다. 뒤집기가 해석을 바꿉니다.

포지션의 fee_growth_inside

포지션 [tick_lower, tick_upper]과 현재 tick_current가 주어질 때:
if tick_current >= tick_upper:
    inside = tick_lower.fee_growth_outside − tick_upper.fee_growth_outside
else if tick_current < tick_lower:
    inside = tick_upper.fee_growth_outside − tick_lower.fee_growth_outside
else:     // 포지션이 범위 내
    inside = fee_growth_global
           − tick_lower.fee_growth_outside
           − tick_upper.fee_growth_outside
포지션의 토큰 측면 s에 대한 미수수료는:
tokens_owed_s += L · (fee_growth_inside_s − fee_growth_inside_last_s) / 2^64
fee_growth_inside_last_s = fee_growth_inside_s
이 업데이트는 포지션과의 모든 상호작용(IncreaseLiquidity, DecreaseLiquidity, CollectFees)에서 실행됩니다.

작업 예제 — 한 틱 크로스

풀(단순화):
  • sqrt_p_x64 = 2^64 · 1.0 = 2^64 (가격 = 1.0)
  • L = 1_000_000
  • tick_current = 0
  • 아래의 다음 초기화된 틱: tick = −60, sqrt_price = 1.0001^(−30) ≈ 0.99700, liquidity_net = −400_000 (이 틱은 포지션을 종료하므로 아래쪽 크로스는 400k를 제거)
  • 수수료율: 0.25%
스왑: Δin = 10_000 token0, 방향 = 0→1. 스텝 1 — sqrt_target = 0.99700 · 2^64까지:
amount_in_to_target = L · (1/sqrt_target − 1/sqrt_p)
                    = 1_000_000 · (1/0.99700 − 1/1.0)
                    ≈ 1_000_000 · 0.003009
                    ≈ 3_009
3,009 < 10,000이므로 이 스텝을 완전히 채웁니다:
Δin_step  = 3_009 / (1 − 0.0025)  = 3_017    // 수수료 포함
Δout_step = L · (sqrt_p − sqrt_target) ≈ 1_000_000 · 0.00299 ≈ 2_990
sqrt_p    = 0.99700 · 2^64
tick_current = −60
L         = 1_000_000 + (−400_000)  = 600_000         // 틱을 크로스
fee_growth_outside at tick −60 is flipped
Δin_remaining = 10_000 − 3_017 = 6_983
스텝 2 — 새로운 L = 600_000으로: 다음 초기화된 틱(예: tick = −120)은 sqrt = 0.99402에 있습니다. amount_in_to_target을 다시 계산합니다:
amount_in_to_target = 600_000 · (1/0.99402 − 1/0.99700)
                    ≈ 600_000 · 0.003010
                    ≈ 1_806
여전히 Δin_remaining보다 작습니다. 다시 크로스합니다. Δin_remaining이 0에 도달할 때까지 계속합니다. Δout의 전체 시퀀스는 최종 스왑 출력에 누적됩니다.

초기화 및 오버플로우 방지

  • MIN_SQRT_PRICE_X64MAX_SQRT_PRICE_X64tick = ±443636에 해당합니다. 이 범위 밖으로 sqrt_p를 푸시하려는 스왑은 되돌립니다.
  • 사용자의 sqrt_price_limit 매개변수는 같은 간격 내에 있어야 합니다. 프로그램이 확인합니다.
  • L · Δsqrt의 곱은 u256에서 계산된 다음 오버플로우를 피하기 위해 u128로 다시 이동됩니다.

Uniswap v3과의 차이점

  • Oracle. Raydium의 ObservationState(block_timestamp, tick_cumulative, seconds_per_liquidity_cumulative) 링 버퍼를 저장합니다. Uniswap과 약간 다른 와이어 형식이지만 같은 TWAP 수학입니다.
  • Token-2022. Raydium CLMM은 Token-2022 민트를 지원합니다. 전송 수수료 변형에는 추가 사전/사후 스왑 양 조정이 필요합니다. algorithms/token-2022-transfer-fees를 참고하세요.
  • 틱 비트맵. Raydium은 초기화된 틱 비트맵을 풀당 [u64; 16]으로 압축하여 빠른 find_next_initialized_tick을 제공합니다. Uniswap은 단어별 온체인 맵핑을 사용합니다. 트레이드오프는 렌트 대 조회 비용입니다.
  • 보상 슬롯. Raydium은 풀당 3개의 보상 스트림을 지원하며 별도의 reward_growth_global_x64 카운터를 사용합니다. 수수료 성장 누적기와 같은 구조입니다.

참고 자료

출처:
  • Uniswap v3 백서 (sqrt-price 수학의 정규 유도).
  • Raydium CLMM 프로그램 소스.