이 페이지는 AI 자동 번역입니다. 모든 내용은 영문판을 기준으로 합니다.영문판 보기 →
틱이 존재하는 이유
CLMM의 유동성은 가격 범위에 집중됩니다. 범위를 온체인에서 다루기 쉽게 하기 위해 가격은 정수 틱으로 양자화되며, 각 틱은 이전 틱의 상수배입니다: 틱은 0.01% 가격 변동, 즉 약 1 베이시스 포인트에 해당합니다. 매핑은 다음과 같습니다:MIN_TICK과 MAX_TICK은 양쪽 끝에서 sqrt_price_x64가 u128에 맞도록 선택됩니다. 모든 풀은 tick_lower >= MIN_TICK과 tick_upper <= MAX_TICK을 강제합니다. 실제로 웹 UI는 사용자가 도달 불가능한 틱에 유동성을 잠그는 것을 방지하기 위해 범위를 훨씬 더 좁게 제한합니다.
틱 간격
풀의AmmConfig는 틱 간격을 고정합니다. 이는 포지션이 끝점으로 사용할 수 있는 유일한 틱입니다. tick_spacing = 60이면 …, −120, −60, 0, 60, 120, … 틱만 유효합니다. 끝점 31로 포지션을 열려는 시도는 InvalidTickIndex로 되돌립니다.
일반적인 공개 간격:
간격이 클수록 초기화할 틱 배열이 적고, 넓은 포지션을 열기가 저렴하며, 가격 경계가 더 흐릿합니다. 변동성이 높은 쌍은 일반적으로 120 간격 계층에 있고, 스테이블 코인은 1 간격 계층에 있습니다.
틱 배열
풀은 틱별 상태를 별도 계정에 저장하지 않습니다. 대신 인접한TICK_ARRAY_SIZE개의 틱(현재 Raydium CLMM에서는 60개)이 단일 TickArrayState로 압축됩니다. 배열의 첫 번째 틱은 start_tick_index이며, 정확히 TICK_ARRAY_SIZE * tick_spacing 정수 틱 단위를 포함합니다.
tick_spacing = 60과 TICK_ARRAY_SIZE = 60의 경우:
- 각 틱 배열은
60 × 60 = 3600정수 틱을 포함합니다. start_tick_index는 3600의 배수입니다:…, -7200, -3600, 0, 3600, 7200, ….
tick_spacing = 60에서 포지션 끝점 t = 2040은 start_tick_index = 0인 틱 배열에 있습니다. 포지션 끝점 t = 4200은 start_tick_index = 3600인 배열에 있습니다.
배열이 생성되는 시점
틱 배열은 지연 생성됩니다: 내부의 모든 틱을 참조하는 첫 번째 포지션이 배열을 초기화하고 렌트를 지불합니다. 스왑은 틱 배열을 초기화하지 않습니다. 비트맵을 사용하여 초기화되지 않은 배열을 건너뜁니다. SDK의 포지션 열기 흐름은 선택한 범위를 검사하고, 접하는 틱 배열 목록을 계산하고, 누락된 경우OpenPosition과 동일한 트랜잭션에 init_tick_array 명령을 추가합니다.
틱 배열은 닫히지 않습니다
틱 배열이 초기화되면 풀의 수명 동안 유지됩니다. 프로그램은initialized_tick_count가 0으로 돌아온 후에도 틱 배열을 닫을 수 있는 경로를 노출하지 않습니다. 틱 배열에 대한 렌트 복구는 없습니다. 배열에 접하는 첫 번째 포지션이 지불한 렌트는 해당 계정에 영구적으로 잠깁니다. 이는 의도적인 트레이드오프입니다: 기존 틱 배열을 재사용하는 것은 이후의 모든 포지션에 무료이므로, 거래량이 많은 풀은 변동에 관계없이 (pool, start_tick_index) 슬롯당 한 번만 렌트 비용을 지불합니다.
비트맵
“현재 틱의 왼쪽/오른쪽에서 다음 초기화된 틱 찾기”는 빨라야 합니다. 스왑은 많은 틱을 교차할 수 있습니다. 풀은PoolState에 틱 0 주변 ±1,024 배열 범위에 대한 1비트당 틱 배열 비트맵을 인라인으로 저장합니다. 그 범위 밖(전체 범위 포지션, 특이한 설정)에서는 TickArrayBitmapExtension이 오버플로우를 제공합니다.
스왑은 비트맵을 따릅니다: lowest_set_bit_above(tick_current_array_index)는 스왑이 교차하는 쪽에 초기화된 틱이 있는 다음 배열을 제공합니다. 해당 배열 내에서 유사한 비트 스캔이 다음 초기화된 틱을 찾습니다.
liquidity_gross와 liquidity_net
모든 초기화된 틱은 두 개의 유동성 값을 저장합니다:
liquidity_gross— 이 틱을 끝점으로 참조하는 모든 포지션의L합계입니다.liquidity_gross가 0에 도달하면 틱은 초기화되지 않으며 비트맵에서 제거될 수 있습니다.liquidity_net— 가격이 이 틱을 위쪽으로 교차할 때(틱 공간에서 왼쪽에서 오른쪽으로) 풀 수준liquidity에 대한 부호가 있는 변화입니다. 이 틱이 크기L인 포지션의 하한이면+L을 기여합니다. 해당 포지션의 상한이면−L을 기여합니다.
- 포지션 A:
tick_lower = -120,tick_upper = 0, 유동성L_A = 100. - 포지션 B:
tick_lower = -60,tick_upper = 60, 유동성L_B = 50.
다양한
tick_current 값에 대한 풀 수준 liquidity:
tick_current = -180:liquidity = 0(모든 포지션 이전)tick_current = -90:liquidity = 100(A만 내부)tick_current = -30:liquidity = 150(A와 B 내부)tick_current = 30:liquidity = 50(B만 내부)tick_current = 90:liquidity = 0(둘 다 지남)
liquidity_net(음수일 수 있음)을 PoolState.liquidity에 추가합니다. 이는 정확한 Uniswap v3 메커니즘입니다.
NFT로서의 포지션
Raydium CLMM 포지션은 NFT입니다. 포지션을 열면 공급량이 1인 새로운 민트를 호출자의 지갑에 민팅하고, 민트의 권한은 CLMM 프로그램입니다. 프로그램은 포지션 소유권을 CPI 시점에 해당 민트의 ATA에서 잔액을 보유하는 사람에게 연결합니다. 결과:- 포지션은 일반적으로 양도 가능합니다. 지갑은 NFT를 전송하여 포지션을 판매하거나 에어드롭할 수 있습니다. 새 보유자는
CollectRewards,IncreaseLiquidity등을 호출할 수 있습니다. 예외는 아래의 제한된 발행자 경로에서 동결된 포지션입니다. - 포지션은 CLMM 외부에서 주소 지정 가능합니다. 마켓플레이스와 지갑은 다른 NFT처럼 포지션을 표시합니다. SDK는 민트 메타데이터에 합리적인
name/symbol을 설정합니다. - 포지션의 PDA는 NFT 민트에서 파생됩니다. 현재 보유자를 알지 못해도
PersonalPositionState를 찾을 수 있습니다.
제한된 발행자 포지션
2026-08 업그레이드 이후 생성된 모든 포지션 NFT 민트는 CLMMpool_state를 동결 권한으로 기록합니다. 이것이 모든 새 포지션이 동결된다는 의미는 아닙니다. 일반 풀과 모든 비매칭 포지션의 경우 NFT 토큰 계정은 동결되지 않고 양도 가능합니다. 풀 PDA는 CLMM 프로그램 외부에서 서명할 수 없으며, CLMM은 범용 동결 명령을 노출하지 않습니다.
동결하려면 다음 두 조건이 모두 필요합니다:
- 포지션이
OpenPositionV2또는OpenPositionWithToken22Nft를 통해 열립니다. - 최소한 하나의 풀 볼트 민트가 프로그램의 하드코딩된 제한된 발행자 목록의 동결 권한을 가집니다.
OpenPosition V1은 이 필터를 적용하지 않습니다. 현재 목록은 reference/program-addresses를 참조하세요.
동결된 포지션:
- NFT를 다른 토큰 계정으로 전송할 수 없습니다.
- NFT 토큰 계정의 소유자를 변경할 수 없습니다.
- 기록된 소유자가 서명할 때 유동성을 증가 또는 감소시키고 수수료 또는 보상을 수집할 수 있습니다.
- 여전히 닫을 수 있습니다.
ClosePosition은 풀 PDA를 사용하여 NFT 계정을 해동한 다음 NFT를 소각하고 동일한 명령에서 포지션 계정을 닫습니다.
Token-2022 포지션
CLMM은OpenPositionV2를 통해 클래식 SPL Token에서 포지션 NFT를 민팅하거나, OpenPositionWithToken22Nft를 통해 Token-2022에서 민팅할 수 있습니다. 두 V2 경로 모두 풀 볼트 민트를 검사하고 동일한 제한된 발행자 동결 규칙을 적용합니다. OpenPosition V1은 레거시 클래식 토큰 경로이며 Token-2022 볼트 민트가 있는 풀을 제공할 수 없습니다. 지갑 및 마켓플레이스 호환성은 다릅니다. Raydium의 UI는 두 NFT 프로그램을 모두 추적합니다.
허용 범위 규칙
OpenPosition 시점에 프로그램은 다음을 강제합니다:
tick_lower < tick_upper.tick_lower % tick_spacing == 0및tick_upper % tick_spacing == 0.MIN_TICK <= tick_lower및tick_upper <= MAX_TICK.- 호출자가
tick_lower와tick_upper를 포함하는 틱 배열을 제공했습니다. 이미 초기화되었거나 동일한 트랜잭션의init_tick_array를 통해. - 이 포지션이 확장 범위로 확장되는 경우 비트맵 확장 계정.
InvalidTickIndex, NotApproved 또는 InsufficientLiquidity로 되돌립니다. reference/error-codes를 참조하세요.
”범위 내” vs “범위 외”
포지션은tick_lower <= tick_current < tick_upper일 때 범위 내입니다. 범위 내 포지션만 PoolState.liquidity에 기여하므로 스왑 수수료를 얻습니다.
범위 외 포지션:
- 한 토큰의 100%를 보유합니다(범위가 지나간 토큰). 구체적으로,
tick_current < tick_lower이면 포지션은 token1만 보유합니다(가격이 멀어지면서 이미 “판매”됨).tick_current >= tick_upper이면 token0만 보유합니다. - 스왑 수수료를 얻지 않습니다.
- 풀의 보상 스트림이 범위 외 유동성에 방출하는 경우 계속 보상을 적립합니다. 그러나 Raydium의 기본 동작은 “범위 내에만 방출”이며, Uniswap v3 규칙과 일치합니다.
products/clmm/fees를 참조하세요.
일반적인 통합 함정
- 간격을 벗어난 끝점. 목표 가격에서 틱을 계산하는 코드는
OpenPosition에 전달하기 전에tick_spacing의 배수로 스냅해야 합니다. SDK 헬퍼(TickUtils.getTickWithPriceAndTickspacing)는 이를 수행합니다. 직접 작성한 수학은 종종 그렇지 않습니다. - 누락된 틱 배열. 넓은 포지션을 열려면 여러 틱 배열을 초기화해야 할 수 있습니다. 쓰기 가능한 계정으로 전달하지 않으면 되돌립니다. SDK의
openPositionFromBase는 목록을 반환합니다. - 스왑 후 오래된 틱.
tick_current는 한 번의 스왑에서 많은 틱을 교차할 수 있습니다. UX가 한 RPC 호출에서 “현재 틱”을 표시한 다음 나중에 포지션을 열면 라이브 가격 대비 상대적 포지션이 수십 틱 떨어질 수 있습니다. 서명하기 직전에 다시 가져오세요. - 추가 메타데이터가 있는 포지션 NFT. Raydium 포지션을 인식하는 지갑을 구축하는 경우 하드코딩된 메타데이터 필드가 아닌 포지션 PDA/프로그램 데이터를 사용하세요. 새 포지션 민트는 생성 시 풀 PDA를 민트 및 동결 권한으로 사용합니다. 민트 권한은 단일 NFT가 민팅된 후 제거됩니다.
- 모든 포지션이 양도 가능하다고 가정. 전송, 마켓플레이스, 에스크로 또는 Burn & Earn 작업을 표시하기 전에 NFT 토큰 계정의
isFrozen상태를 읽으세요.
다음 단계
- 수학 — 틱 경계가 참여하는 스왑 단계별 설명 및 수수료 성장 유도.
- 계정 —
TickArrayState및PositionState레이아웃. - 수수료 및 보상 — 범위 내 여부가 수수료 적립을 제어하는 방식.
algorithms/clmm-math— 집중 유동성 공식의 공유 유도.
raydium-io/raydium-clmm—tick_array,tick,position모듈- “Uniswap v3 Core” 백서, §6 (틱), §7 (수수료 성장)

