> ## 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.

# PDA와 CPI

> 프로그램 파생 주소(PDA)와 크로스 프로그램 호출(CPI) — Raydium이 상태를 결정적으로 관리하고 다른 프로그램과 구성하기 위해 사용하는 두 가지 기본 요소입니다. 시드, 범프, invoke vs invoke_signed, 그리고 Raydium 코드가 따르는 패턴을 알아봅니다.

<Info>
  **이 페이지는 AI 자동 번역입니다. 모든 내용은 영문판을 기준으로 합니다.**

  [영문판 보기 →](/solana-fundamentals/pdas-and-cpis)
</Info>

<Info>
  **PDA**(프로그램 파생 주소)와 **CPI**(크로스 프로그램 호출)는 Raydium을 가능하게 하는 두 가지 기본 요소입니다. PDA를 사용하면 프로그램이 개인 키 없이 결정적인 주소를 "소유"할 수 있습니다. 이것이 풀 권한과 볼트가 작동하는 방식입니다. CPI를 사용하면 한 프로그램이 다른 프로그램을 호출할 수 있습니다. 이것이 Raydium이 SPL Token 프로그램을 통해 토큰을 스왑하는 방식이고, 통합자가 Raydium을 자신의 흐름에 구성하는 방식입니다. Raydium의 소스 코드를 읽기 전에 둘 다 이해할 가치가 있습니다.
</Info>

## PDA: 키 없는 주소

\*\*프로그램 파생 주소(PDA)\*\*는 다음과 같은 공개 키입니다.

* ed25519 곡선 위에 있지 않습니다(개인 키가 존재하지 않음).
* 프로그램 ID와 시드 집합에서 결정적으로 파생됩니다.
* `invoke_signed`를 통해 *오직* 파생 프로그램에서만 서명할 수 있습니다.

모든 Raydium 풀 권한, 모든 풀 상태 계정, 모든 볼트, 모든 팜 상태 — 모두 PDA입니다.

### 파생

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

```ts theme={null}
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의 계정 데이터에 저장되므로 후속 트랜잭션은 이를 전달하고 (비용이 많이 드는) 파생 루프를 건너뛸 수 있습니다.

```rust theme={null}
#[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`](/ko/integration-guides/cpi-integration)을 참조하세요.

### invoke vs invoke\_signed

Solana 런타임은 두 가지 CPI 기본 요소를 제공합니다.

* **`invoke`**: 다른 프로그램을 호출합니다. 호출된 프로그램은 외부 트랜잭션의 서명자를 상속합니다.
* **`invoke_signed`**: PDA를 대신하여 다른 프로그램을 호출합니다. 런타임은 PDA의 시드를 확인하고 서명을 승인합니다.

`invoke_signed`는 프로그램이 개인 키를 관리하지 않고 계정에 대한 권한을 보유하도록 하는 마법입니다.

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

풀 볼트는 권한이 풀 프로그램의 PDA인 Token Account입니다. 스왑 중 토큰을 전송하려면 풀 프로그램이 해당 PDA로 서명해야 합니다.

```rust theme={null}
// 풀 권한의 시드
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`을 호출할 수 있습니다.

```rust theme={null}
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`](/ko/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`는 전송 훅 프로그램의 추가 필수 계정에 대해 남은 계정을 사용합니다. 클라이언트는 필요한 계정을 가져와 추가합니다.

```ts theme={null}
const swapIx = await raydium.cpmm.swap({
  /* ... */
  // SDK가 자동으로 남은 계정을 처리합니다
});
```

CPI 레벨에서 통합자는 남은 계정을 자신의 명령을 통해 전달해야 합니다.

```rust theme={null}
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 계산

```ts theme={null}
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를 파생합니다.

## 포인터

* [`solana-fundamentals/account-model`](/ko/solana-fundamentals/account-model) — PDA가 계정 모델에 어떻게 맞는지.
* [`solana-fundamentals/programs-and-anchor`](/ko/solana-fundamentals/programs-and-anchor) — PDA 선언을 위한 Anchor의 헬퍼.
* [`integration-guides/cpi-integration`](/ko/integration-guides/cpi-integration) — Raydium으로 CPI하는 통합 구축.
* [`sdk-api/rust-cpi`](/ko/sdk-api/rust-cpi) — Raydium의 Rust CPI 타입.

출처:

* [Solana PDA 문서](https://docs.solana.com/developing/programming-model/calling-between-programs).
* [Anchor PDA & CPI 문서](https://www.anchor-lang.com/docs/pdas-and-cpis).
