메인 콘텐츠로 건너뛰기

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 자동 번역입니다. 모든 내용은 영문판을 기준으로 합니다.영문판 보기 →
CPMM의 프로그램 ID와 PDA seed는 reference/program-addresses에 정식으로 나열되어 있습니다. 이 페이지는 각 계정의 용도와 유지해야 할 불변성에 초점을 맞추며, 하드코딩된 주소는 다루지 않습니다.

CPMM 풀의 여섯 계정

모든 CPMM 풀은 CPMM 프로그램 아래에 여섯 개의 PDA(program-derived address)와 이것이 참조하는 하나의 공유 AmmConfig 계정으로 완전히 기술됩니다. 두 mint만 있으면 네트워크에 접근하지 않고도 모든 항목을 결정적으로 유도할 수 있습니다.
계정Seed소유자용도
authority"vault_and_lp_mint_auth_seed"CPMM모든 vault 이동과 모든 LP mint/burn을 서명하는 주체입니다. 모든 CPMM 풀에서 공유됩니다.
poolState"pool", ammConfig, token0Mint, token1Mint 또는 서명자가 제공한 임의의 keypairCPMM풀의 상태 struct — mint 쌍, vault 잔액, LP 공급량, 수수료 적립, observation 포인터. CPMM Initialize 명령어는 네 개의 seed에서 유도한 정식 PDA 또는 생성자가 서명한 임의의 keypair를 모두 받을 수 있습니다. 랜덤 keypair 경로는 악의적인 사용자가 mempool을 감시하다가 정당한 생성자보다 먼저 정식 PDA를 차지하려는 front-running 공격을 방어하기 위해 존재합니다.
lpMint"pool_lp_mint", poolStateSPL Token풀의 LP 토큰입니다. 공급량 = 총 유출 LP. Mint authority = CPMM authority PDA.
vault0"pool_vault", poolState, token0MintSPL Token / Token-2022풀의 token0 잔액을 보유합니다. Authority PDA가 소유합니다.
vault1"pool_vault", poolState, token1MintSPL Token / Token-2022풀의 token1 잔액을 보유합니다. Authority PDA가 소유합니다.
observation"observation", poolStateCPMMTWAP에 사용되는 가격 샘플의 ring buffer입니다. 모든 스왑에서 기록됩니다.
그리고 공유 config:
계정Seed소유자용도
ammConfig"amm_config", index: u16CPMM거래/프로토콜/펀드/생성자 수수료율과 관리자 키를 보유합니다. “수수료 계층”당 하나씩 존재합니다. Poolstate는 생성 시점에 하나에 바인딩되며 이후 변경할 수 없습니다.

두 mint만으로 풀 유도하기

import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";

const CPMM_PROGRAM_ID = new PublicKey(
  "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"
); // mainnet — see reference/program-addresses

function u16ToBytes(n: number): Buffer {
  const b = Buffer.alloc(2);
  b.writeUInt16BE(n);
  return b;
}

// token0 < token1 by byte order. Getting this wrong yields a valid PDA
// that points at a nonexistent pool.
function sortMints(a: PublicKey, b: PublicKey): [PublicKey, PublicKey] {
  return Buffer.compare(a.toBuffer(), b.toBuffer()) < 0 ? [a, b] : [b, a];
}

export function deriveCpmmAccounts(
  mintA: PublicKey,
  mintB: PublicKey,
  ammConfigIndex = 0,
) {
  const [token0Mint, token1Mint] = sortMints(mintA, mintB);

  const [ammConfig] = PublicKey.findProgramAddressSync(
    [Buffer.from("amm_config"), u16ToBytes(ammConfigIndex)],
    CPMM_PROGRAM_ID,
  );
  const [authority] = PublicKey.findProgramAddressSync(
    [Buffer.from("vault_and_lp_mint_auth_seed")],
    CPMM_PROGRAM_ID,
  );
  const [poolState] = PublicKey.findProgramAddressSync(
    [
      Buffer.from("pool"),
      ammConfig.toBuffer(),
      token0Mint.toBuffer(),
      token1Mint.toBuffer(),
    ],
    CPMM_PROGRAM_ID,
  );
  const [lpMint] = PublicKey.findProgramAddressSync(
    [Buffer.from("pool_lp_mint"), poolState.toBuffer()],
    CPMM_PROGRAM_ID,
  );
  const [vault0] = PublicKey.findProgramAddressSync(
    [Buffer.from("pool_vault"), poolState.toBuffer(), token0Mint.toBuffer()],
    CPMM_PROGRAM_ID,
  );
  const [vault1] = PublicKey.findProgramAddressSync(
    [Buffer.from("pool_vault"), poolState.toBuffer(), token1Mint.toBuffer()],
    CPMM_PROGRAM_ID,
  );
  const [observation] = PublicKey.findProgramAddressSync(
    [Buffer.from("observation"), poolState.toBuffer()],
    CPMM_PROGRAM_ID,
  );

  return {
    ammConfig,
    authority,
    poolState,
    lpMint,
    token0Mint,
    token1Mint,
    vault0,
    vault1,
    observation,
  };
}
풀 PDA를 유도하기 전에 항상 mint를 정렬합니다. Seed는 사용자 순서가 아닌 바이트 순서로 두 mint를 해싱합니다. (A, B)(B, A)를 가진 두 풀은 온체인에서 충돌합니다. 정렬은 프로그램이 매핑을 정식으로 만드는 방법입니다.
풀 ID가 항상 정식 PDA는 아닙니다. Initialize는 위의 PDA 외에도 임의의 서명자 keypair를 pool_state로 받습니다. 전달된 계정이 정식 PDA와 일치하지 않으면 프로그램은 이것이 서명자여야 합니다. 즉, 생성자가 서명할 새로운 keypair를 전달합니다. 이것이 front-run 방어입니다: 정식 PDA를 확보하려는 제3자는 정당한 생성자가 대신 랜덤 keypair를 사용함으로써 우회될 수 있습니다. 하위 PDA(lpMint, vault0, vault1, observation)는 여전히 poolState.key()에서 유도되므로 어떤 주소가 사용되었든 고유하게 유지됩니다. 풀을 인덱싱할 때는 정식 PDA를 유도하지 말고 항상 온체인 상태에서 풀 ID를 발견합니다(예: CPMM 프로그램 아래의 PoolState 계정). 후자는 랜덤 keypair 풀을 놓칩니다.

계정 레이아웃

전체 Rust 정의는 raydium-cp-swap 소스에 있습니다. 아래 필드는 통합할 때 읽을 필드들입니다.

PoolState

// programs/cp-swap/src/states/pool.rs
pub struct PoolState {
    pub amm_config: Pubkey,               // binds this pool to an AmmConfig
    pub pool_creator: Pubkey,             // who ran initialize
    pub token_0_vault: Pubkey,            // == vault0 PDA
    pub token_1_vault: Pubkey,            // == vault1 PDA

    pub lp_mint: Pubkey,
    pub token_0_mint: Pubkey,
    pub token_1_mint: Pubkey,

    pub token_0_program: Pubkey,          // SPL Token or Token-2022 program
    pub token_1_program: Pubkey,

    pub observation_key: Pubkey,          // == observation PDA
    pub auth_bump: u8,
    pub status: u8,                       // bitmask: deposit | withdraw | swap
    pub lp_mint_decimals: u8,
    pub mint_0_decimals: u8,
    pub mint_1_decimals: u8,

    pub lp_supply: u64,                   // mirrors lp_mint supply
    pub protocol_fees_token_0: u64,
    pub protocol_fees_token_1: u64,
    pub fund_fees_token_0: u64,
    pub fund_fees_token_1: u64,

    pub open_time: u64,                   // unix; swaps rejected before this
    pub recent_epoch: u64,

    // Creator-fee state (added after the original layout):
    pub creator_fee_on: u8,               // 0=BothToken, 1=OnlyToken0, 2=OnlyToken1
    pub enable_creator_fee: bool,
    pub padding1: [u8; 6],
    pub creator_fees_token_0: u64,
    pub creator_fees_token_1: u64,

    pub padding: [u64; 28],
}
실제로 읽어야 할 것:
  • lp_supply — LP mint의 총 공급량을 반영하는 풀의 내부 미러입니다. LP 공유 계산에 사용합니다. 값은 mint의 온체인 공급량과 일치해야 하지만 PoolState에서 읽으면 추가 계정 fetch를 피할 수 있습니다.
  • protocol_fees_token{0,1}, fund_fees_token{0,1} — 아직 수거되지 않은 적립된 수수료입니다. 이들은 스왑 가격 책정에 영향을 주지 않습니다. CollectProtocolFee / CollectFundFee가 호출될 때까지 vault에 남아있습니다.
  • statusSwap, Deposit, Withdraw가 허용되는지 여부를 제어하는 비트마스크입니다. 관리자가 UpdatePoolStatus를 통해 업데이트합니다. SDK는 트랜잭션을 구축하기 전에 이를 확인합니다. CPI로 직접 이동하는 경우 직접 확인합니다.
  • token0_program / token1_program — 각 vault에 대해 CPI할 토큰 프로그램입니다. 하나는 classic SPL Token이고 다른 하나는 Token-2022일 수 있으며, 독립적입니다.
  • open_time — Unix 타임스탬프입니다. 이 시간 이전의 스왑은 실패합니다. Deposit은 풀을 시드할 수 있도록 open_time 전에 허용됩니다.
  • creator_fee_on / enable_creator_fee — 함께 선택 창작자 수수료가 이 풀에 대해 활성화되어 있는지, 그리고 스왑의 어느 쪽에서 수거되는지를 제어합니다. enable_creator_fee == false는 creator-fee 경로를 완전히 제로화합니다. 활성화되면 creator_fee_on은 다음을 선택합니다: 0 = 스왑 입력인 토큰 중 어느 것이든 수수료를 가져갑니다 (BothToken); 1 = token_0에서만 수수료를 가져갑니다 (token_1 → token_0 스왑은 건너뜀); 2 = token_1에서만 수수료를 가져갑니다. 풀 생성 시 InitializeWithPermission을 통해 설정됩니다. 나중에 변경할 수 없습니다.
  • creator_fees_token_{0,1} — 적립된 creator 수수료는 CollectCreatorFee로 수거됩니다.

AmmConfig

pub struct AmmConfig {
    pub bump: u8,
    pub disable_create_pool: bool,
    pub index: u16,                       // matches the seed
    pub trade_fee_rate: u64,              // e.g., 2500 = 0.25%
    pub protocol_fee_rate: u64,           // fraction of trade fee to protocol
    pub fund_fee_rate: u64,               // fraction of trade fee to fund
    pub create_pool_fee: u64,             // paid once at init (in SOL or token)
    pub protocol_owner: Pubkey,           // can call CollectProtocolFee
    pub fund_owner: Pubkey,               // can call CollectFundFee
    pub creator_fee_rate: u64,            // optional pool-creator fee rate (1/1_000_000 of volume)
    pub padding: [u64; 15],
}
세 가지 주의할 점:
  1. trade_fee_ratecreator_fee_rate은 거래량의 분수이며, 둘 다 1/1_000_000 단위로 표시됩니다. 2500은 거래량의 0.25%를 의미합니다. protocol_fee_ratefund_fee_rate거래 수수료의 분수(거래량이 아님), 같은 1/1_000_000 분모입니다. Creator 수수료는 거래 수수료의 분수가 아닙니다. 자체 독립적인 비율입니다. 전체 산술은 products/cpmm/fees에 있습니다.
  2. **indexu16**이므로 seed 해시는 2바이트 big-endian을 사용합니다. 바이트 순서의 off-by-one은 일반적인 통합 버그입니다.
  3. AmmConfig는 풀 수준에서 불변입니다. 풀은 생성 시 하나의 AmmConfig를 가리키며 절대 전환되지 않습니다. 수수료 변경은 풀이 각 스왑마다 config를 읽기 때문에 전파됩니다. 하지만 풀은 수수료 계층 간에 이동할 수 없습니다.
Creator 수수료에 대한 참고: 비율 자체(creator_fee_rate)는 AmmConfig에 있으며 수수료 계층 전체에서 공유됩니다. 특정 풀이 실제로 이를 청구하는지(enable_creator_fee)와 스왑의 어느 쪽에 착지하는지(creator_fee_on)는 PoolState에 있습니다. Creator 수수료는 거래 수수료와 독립적입니다. 자체 비율이며, 자체 카운터(creator_fees_token_{0,1})에 적립되며, LP / 프로토콜 / 펀드 거래 수수료 공유를 절대 줄이지 않습니다. 수거는 CollectCreatorFee를 통해 수행됩니다. 전체 메커니즘은 products/cpmm/fees를 참조합니다.

Permission

InitializeWithPermission이 사용하는 작은 액세스 제어 계정입니다. CPMM 프로그램은 다른 프로그램(예: 토큰을 CPMM으로 승격시킬 때 LaunchLab)이 주어진 AmmConfig에 대해 풀을 생성할 자격이 있음을 증명할 수 있도록 권한이 있는 풀 생성 경로를 지원합니다.
pub struct Permission {
    pub authority: Pubkey,    // who is allowed to call InitializeWithPermission
    pub padding: [u64; 8],
}
Permission PDA는 CPMM 관리자가 CreatePermissionPda를 통해 생성하고 ClosePermissionPda를 통해 취소합니다. 최종 사용자는 이 계정과 직접 상호작용하지 않습니다. 이는 크로스 프로그램 흐름을 위한 기반입니다.

Vault와 Token-2022

vault0vault1은 CPMM authority PDA가 소유하며, 토큰 프로그램 소유자(token_program)는 풀 생성 시 mint의 프로그램으로 결정되는 SPL Token 또는 Token-2022입니다. 풀은 두 경우를 투명하게 처리합니다. Swap / Deposit / Withdraw 명령어 계정에서 각 쪽에 올바른 토큰 프로그램 ID를 전달합니다. CPMM은 풀 생성 시 엄격한 확장 허용 목록을 적용합니다 (utils/token.rsis_supported_mint). Token-2022 mint는 모든 확장이 이 목록에 있을 경우에만 CPMM 풀에서 사용할 수 있습니다:
  • TransferFeeConfig. 모든 이전에서 mint가 적용합니다. 풀은 SwapBaseInput 입금의 수신 쪽이고 출금의 전송 쪽입니다. 프로그램은 vault에 도착하는 금액을 계산하고 그에 따라 곡선을 설정합니다. algorithms/token-2022-transfer-fees를 참조합니다.
  • **MetadataPointerTokenMetadata. Mint의 표준 메타데이터입니다. 스왑 수학에 영향을 주지 않습니다.
  • InterestBearingConfig. Mint의 UI 금액이 이자를 적립합니다. Vault는 원시 금액을 저장합니다. 곡선은 원시 금액에서만 작동합니다. APR을 표시하는 UI는 Token-2022 헬퍼를 호출하여 UI 금액을 렌더링해야 합니다.
  • ScaledUiAmount. UI 표시 스케일링 확장입니다. InterestBearingConfig과 동일한 처리입니다. 곡선은 원시 금액을 사용합니다.
다른 확장(PermanentDelegate, TransferHook, DefaultAccountState, NonTransferable, ConfidentialTransfer, Group/GroupMember, MintCloseAuthority 등)은 InitializeNotSupportMint로 거부합니다. 예외는 확장 검사를 우회하는 프로그램의 작은 하드코딩된 mint 화이트리스트(소수의 특정 pubkey)입니다. 특정 mint를 사건별로 온보딩하는 데 사용됩니다. 검증된 확장 목록과 mint 화이트리스트는 programs/cp-swap/src/utils/token.rs 아래의 CP-Swap 소스에 있으며 향후 프로그램 업그레이드와 함께 변경될 수 있습니다.

Observation

Observation 계정은 각각 block_timestamp누적 가격을 저장하는 ObservationState 항목의 ring buffer입니다. 모든 스왑에서 프로그램은 마지막 항목 이후 충분한 시간이 지났으면 새로운 observation을 추가합니다. TWAP는 두 개의 observation을 읽고 Δcumulative / Δtime을 나누어 계산합니다.
// OBSERVATION_NUM is hardcoded in the program to 100.
pub const OBSERVATION_NUM: usize = 100;

pub struct Observation {
    pub block_timestamp:              u64,
    pub cumulative_token_0_price_x32: u128,   // Q32.32, top 64 bits left for overflow
    pub cumulative_token_1_price_x32: u128,
}

pub struct ObservationState {
    pub initialized:           bool,
    pub observation_index:     u16,                            // circular index
    pub pool_id:               Pubkey,
    pub observations:          [Observation; OBSERVATION_NUM], // 100 entries
    pub last_update_timestamp: u64,                            // timestamp of the most recent append
    pub padding:               [u64; 3],
}
Ring buffer는 100 observation에 맞게 크기가 조정되었습니다. 각 observation은 40바이트이므로 배열만 4,000바이트입니다. 완전한 ObservationState PDA는 주변 필드와 discriminator 이후 약 4,100바이트입니다. 두 개의 소비자 규칙:
  • 단일 observation을 가격으로 사용하지 마십시오. 이는 현물 가격이 아닌 누적입니다. TWAP를 계산하려면 두 개를 사용합니다.
  • 최소한 하나 블록 떨어진 observation을 선택합니다. 동일 블록 내의 스왑은 새로운 observation을 생성하지 않을 수 있습니다. 연속으로 읽으면 동일 기록을 반환할 수 있습니다.
더 많은 수학은 products/clmm/accounts에 있습니다.

계정 생명주기

이벤트생성된 계정삭제된 계정
InitializepoolState, lpMint, vault0, vault1, observation
Deposit— (사용자 LP ATA 생성 가능)
Withdraw
Swap— (사용자 대상 ATA 생성 가능)
CollectProtocolFee
CollectFundFee
UpdatePoolStatus
CPMM 풀과 그 PDA는 절대 닫히지 않습니다. 0 유동성에서도 poolState는 유지됩니다. 이는 의도적입니다: 나중에 동일 풀을 다시 시드하면 역사적 observation buffer를 보존하고 PDA 유도가 안정적으로 유지됩니다.

어디서 무엇을 읽을지

출처: