메인 콘텐츠로 건너뛰기

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 자동 번역입니다. 모든 내용은 영문판을 기준으로 합니다.영문판 보기 →
PDA(프로그램 파생 주소)와 CPI(크로스 프로그램 호출)는 Raydium을 가능하게 하는 두 가지 기본 요소입니다. PDA를 사용하면 프로그램이 개인 키 없이 결정적인 주소를 “소유”할 수 있습니다. 이것이 풀 권한과 볼트가 작동하는 방식입니다. CPI를 사용하면 한 프로그램이 다른 프로그램을 호출할 수 있습니다. 이것이 Raydium이 SPL Token 프로그램을 통해 토큰을 스왑하는 방식이고, 통합자가 Raydium을 자신의 흐름에 구성하는 방식입니다. Raydium의 소스 코드를 읽기 전에 둘 다 이해할 가치가 있습니다.

PDA: 키 없는 주소

**프로그램 파생 주소(PDA)**는 다음과 같은 공개 키입니다.
  • ed25519 곡선 위에 있지 않습니다(개인 키가 존재하지 않음).
  • 프로그램 ID와 시드 집합에서 결정적으로 파생됩니다.
  • invoke_signed를 통해 오직 파생 프로그램에서만 서명할 수 있습니다.
모든 Raydium 풀 권한, 모든 풀 상태 계정, 모든 볼트, 모든 팜 상태 — 모두 PDA입니다.

파생

PDA는 프로그램 ID와 시드를 해싱하여 계산되고, 결과를 곡선 밖으로 강제하는 “범프” 바이트를 찾습니다. 곡선 밖 주소를 생성하는 첫 번째 범프(일반적으로 255에서 시작하여 감소)가 정규 범프입니다.
import { PublicKey } from "@solana/web3.js";

const [poolAuthority, bump] = PublicKey.findProgramAddressSync(
  [Buffer.from("authority"), poolId.toBuffer()],
  CPMM_PROGRAM_ID,
);
시드는 문자열, 다른 공개 키, 리틀 엔디언 바이트로 인코딩된 u64 값 등 무엇이든 될 수 있습니다. Raydium의 관례는 사람이 읽을 수 있는 접두사 다음에 고유 식별자입니다.

Raydium PDA 패턴

Raydium 프로그램의 일반적인 PDA:
PDA시드프로그램
AMM 권한(AMM v4)[b"amm authority"] + 범프AMM v4
풀 상태(CPMM)[b"pool", amm_config, mint_a, mint_b]CPMM
풀 볼트(CPMM)[b"pool_vault", pool, mint]CPMM
권한(CPMM)[b"vault_and_lp_mint_auth_seed"]CPMM
풀 상태(CLMM)[b"pool", amm_config, mint_0, mint_1]CLMM
틱 배열(CLMM)[b"tick_array", pool, start_tick_index]CLMM
관찰(CLMM)[b"observation", pool]CLMM
개인 포지션(CLMM)[b"position", position_nft_mint]CLMM
팜 상태(Farm v6)[b"pool_farm_state", farm_id]Farm v6
사용자 원장(Farm v6)[b"user_ledger", farm, user]Farm v6
사용자와 통합자는 아무것도 가져올 필요 없이 이러한 PDA를 계산할 수 있습니다 — 공개 입력(풀 ID, 팜 ID, 사용자 키)이 주어지면 PDA는 결정적입니다.

정규 범프

원칙적으로 곡선 밖 주소를 생성하는 여러 범프가 있을 수 있지만, Raydium의 프로그램은 항상 정규 범프(255에서 감소하여 발견)를 사용합니다. 이는 PDA의 계정 데이터에 저장되므로 후속 트랜잭션은 이를 전달하고 (비용이 많이 드는) 파생 루프를 건너뛸 수 있습니다.
#[account]
pub struct PoolState {
    pub bump: [u8; 1],
    // ... 풀 상태의 나머지 부분
}
후속 트랜잭션에서 범프는 풀 상태에서 읽어지며 재계산되지 않습니다.

CPI: 다른 프로그램 호출

**크로스 프로그램 호출(CPI)**을 사용하면 한 프로그램이 단일 트랜잭션 내에서 다른 프로그램의 명령을 인라인으로 호출할 수 있습니다. Raydium은 CPI를 광범위하게 사용합니다.
  • 스왑 명령은 SPL Token 프로그램을 호출하여 토큰을 이동합니다.
  • CLMM은 Metaplex를 호출하여 포지션 NFT를 민팅합니다.
  • 풀 생성은 System Program을 호출하여 계정을 할당합니다.
  • Farm v6은 SPL Token을 호출하여 보상을 전송합니다.
통합자들도 CPI를 사용하여 Raydium으로 호출합니다 — 이것이 볼트 전략, 레버리지 LP 프로토콜, 자동 복합 프로토콜이 작동하는 방식입니다. integration-guides/cpi-integration을 참조하세요.

invoke vs invoke_signed

Solana 런타임은 두 가지 CPI 기본 요소를 제공합니다.
  • invoke: 다른 프로그램을 호출합니다. 호출된 프로그램은 외부 트랜잭션의 서명자를 상속합니다.
  • invoke_signed: PDA를 대신하여 다른 프로그램을 호출합니다. 런타임은 PDA의 시드를 확인하고 서명을 승인합니다.
invoke_signed는 프로그램이 개인 키를 관리하지 않고 계정에 대한 권한을 보유하도록 하는 마법입니다.

예: Raydium이 풀 볼트에서 전송하기

풀 볼트는 권한이 풀 프로그램의 PDA인 Token Account입니다. 스왑 중 토큰을 전송하려면 풀 프로그램이 해당 PDA로 서명해야 합니다.
// 풀 권한의 시드
let authority_seeds: &[&[u8]] = &[
    b"vault_and_lp_mint_auth_seed",
    &[authority_bump],
];
let signer_seeds: &[&[&[u8]]] = &[authority_seeds];

// CPI 컨텍스트 구성
let cpi_accounts = Transfer {
    from:      input_vault.to_account_info(),
    to:        user_ata.to_account_info(),
    authority: pool_authority.to_account_info(),
};
let cpi_program = token_program.to_account_info();
let cpi_ctx     = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);

// 실행
token::transfer(cpi_ctx, amount)?;
런타임은 CPMM 프로그램에서 invoke_signed가 호출되는 것을 보고, vault_and_lp_mint_auth_seed + 범프가 CPMM 프로그램 ID와 함께 해싱될 때 pool_authority의 주소로 파생되는지 확인하고, 토큰 전송에 대한 권한 서명을 허용합니다. 개인 키는 관여하지 않습니다.

예: 통합자가 Raydium CPMM 호출하기

통합자 프로그램(예: 에스크로우)은 CPI를 통해 Raydium의 swap_base_input을 호출할 수 있습니다.
use raydium_cpmm::cpi::{self, accounts::Swap};

let cpi_accounts = Swap {
    payer:                order.to_account_info(),      // PDA, 서명할 예정
    authority:            pool_authority_info,
    amm_config:           amm_config_info,
    pool_state:           pool_info,
    input_token_account:  order_input_ata,
    output_token_account: order_output_ata,
    input_vault:          input_vault_info,
    output_vault:         output_vault_info,
    input_token_program:  input_token_program_info,
    output_token_program: output_token_program_info,
    input_token_mint:     input_mint_info,
    output_token_mint:    output_mint_info,
    observation_state:    observation_info,
};

let seeds = &[b"order", user.key.as_ref(), &[order.bump]];
let cpi_ctx = CpiContext::new_with_signer(
    cpmm_program.to_account_info(),
    cpi_accounts,
    &[seeds],
);

cpi::swap_base_input(cpi_ctx, amount_in, min_out)?;
이것은 정규 통합 패턴입니다. 전체 에스크로우 예제는 integration-guides/cpi-integration을 참조하세요.

CPI 깊이 제한

Solana는 CPI 깊이를 4 레벨로 제한합니다. 트랜잭션의 최상위 명령은 깊이 0으로 계산되며, 각 CPI 호출은 깊이를 증가시킵니다. 실제 함의: Raydium의 자체 스왑은 이미 1-2 수준의 CPI를 사용합니다(Raydium → SPL Token). 통합자가 Raydium을 호출하면 2입니다. 해당 통합자를 다른 통합자가 호출하면 3입니다. 4 레벨이 한계입니다. 대부분의 구성은 이를 쉽게 초과하지 않지만, 깊은 중첩(애그리게이터 → 라우터 → Raydium → 훅)은 한계에 도달할 수 있습니다. 깊게 하기보다는 평평하게 설계하세요.

남은 계정

Raydium 명령이 가변 개수의 계정을 필요로 할 때(예: 알 수 없는 개수의 틱 배열을 교차하는 CLMM 스왑), 추가 계정은 남은 계정으로 전달됩니다 — 고정 계정 목록에 추가되고 위치별로 해석됩니다. CPMM의 SwapV2는 전송 훅 프로그램의 추가 필수 계정에 대해 남은 계정을 사용합니다. 클라이언트는 필요한 계정을 가져와 추가합니다.
const swapIx = await raydium.cpmm.swap({
  /* ... */
  // SDK가 자동으로 남은 계정을 처리합니다
});
CPI 레벨에서 통합자는 남은 계정을 자신의 명령을 통해 전달해야 합니다.
pub struct Swap<'info> {
    // ... 고정 계정
    // 그리고 ctx.remaining_accounts를 통해 전달된 남은 계정들
}

// remaining_accounts를 CPI로 전달합니다
cpi::swap_base_input(
    cpi_ctx.with_remaining_accounts(ctx.remaining_accounts.to_vec()),
    amount_in,
    min_out,
)?;

PDA 함정

잘못된 시드 → 잘못된 주소

시드가 잘못된 순서, 잘못된 인코딩이거나 추가 바이트를 포함/제외하는 버그는 조용히 다른 PDA를 생성합니다. 트랜잭션은 모호하게 실패합니다(프로그램이 존재하지 않는 계정을 읽으려고 시도함). 항상 시드 파생을 알려진 골든 값에 대해 단위 테스트하세요.

범프를 저장하지 않기

매 트랜잭션마다 범프를 다시 파생하면 파생 루프의 계산을 지불합니다. 정규 범프를 PDA의 데이터에 저장하고 거기서 읽으세요.

정규 범프와 비정규 범프 혼동

비정규 범프(곡선 밖의 주소를 생성하는 것을 찾는 경우)는 invoke_signed에서 허용되지만 Raydium의 프로그램에서는 assert_eq!(bump, canonical_bump)를 통해 거부됩니다. 누군가 비정규 범프로 PDA를 주장하려고 하면 트랜잭션이 실패합니다.

소유 프로그램이 아닐 때 PDA를 서명자로 전달하기

PDA의 파생에 있는 ID의 프로그램만이 시드와 함께 invoke_signed할 수 있습니다. 시도하면 런타임이 거부합니다.

CPI 함정

remaining_accounts 전달 잊기

외부 명령이 remaining_accounts에서 전송 훅 계정을 전달하지만 Raydium으로의 CPI가 이를 전달하지 않으면 Raydium은 훅 계정을 찾을 수 없어 실패합니다. 항상 필요한 CPI에 with_remaining_accounts를 포함하세요.

쓰기 가능 플래그 불일치

외부 명령이 쓰기 가능으로 표시한 계정은 호출된 프로그램이 이를 쓰려고 할 때 CPI 호출에서도 쓰기 가능해야 합니다. 불일치 → 런타임 거부.

렌트 설명하지 않기

계정을 생성하는 프로그램에 대한 CPI(예: ATA 생성)는 지불자가 렌트를 위한 충분한 SOL을 가져야 합니다. 실패한 렌트 검사는 불명확한 오류로 나타납니다.

작업 예: Raydium CPMM PDA 계산

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

const CPMM_PROGRAM_ID = new PublicKey("CPMMoo8L3F4NbTegBCKVNunggL7H1Zpdmwpwh8KMoZ0F");

function computeCpmmPdas(ammConfig, mintA, mintB) {
  const [poolState, poolBump] = PublicKey.findProgramAddressSync(
    [
      Buffer.from("pool"),
      ammConfig.toBuffer(),
      mintA.toBuffer(),
      mintB.toBuffer(),
    ],
    CPMM_PROGRAM_ID,
  );

  const [authority] = PublicKey.findProgramAddressSync(
    [Buffer.from("vault_and_lp_mint_auth_seed")],
    CPMM_PROGRAM_ID,
  );

  const [vaultA] = PublicKey.findProgramAddressSync(
    [Buffer.from("pool_vault"), poolState.toBuffer(), mintA.toBuffer()],
    CPMM_PROGRAM_ID,
  );

  const [vaultB] = PublicKey.findProgramAddressSync(
    [Buffer.from("pool_vault"), poolState.toBuffer(), mintB.toBuffer()],
    CPMM_PROGRAM_ID,
  );

  const [observation] = PublicKey.findProgramAddressSync(
    [Buffer.from("observation"), poolState.toBuffer()],
    CPMM_PROGRAM_ID,
  );

  return { poolState, authority, vaultA, vaultB, observation, poolBump };
}
이는 getPoolInfoFromRpc({ poolId })를 호출할 때 Raydium SDK가 내부적으로 정확히 수행하는 것입니다 — 왕복 없이 관련 PDA를 파생합니다.

포인터

출처: