CCIP v1.6.0 SVM Receiver API Reference
Receiver
Below is a complete API reference for the ccip_receive instruction that must be implemented by any Solana program wishing to receive CCIP messages.
ccip_receive
This instruction is the entry point for receiving a cross-chain message on an SVM-based blockchain from any source blockchain.
pub fn ccip_receive(
    ctx: Context<CcipReceive>,
    message: Any2SVMMessage
) -> Result<()>;
Parameters
| Name | Type | Description | 
|---|---|---|
| message | Any2SVMMessage | The cross-chain message being delivered. See Message Structure for details. | 
Context (Accounts)
These are the required accounts that must be passed to implement a secure CCIP Receiver. The first three accounts form the critical security validation chain and must be implemented exactly as shown.
| Field | Type | Writable? | Description | 
|---|---|---|---|
| authority | Signer<'info> | No | The Offramp CPI signer PDA. This must be the first account. Derivation: [EXTERNAL_EXECUTION_CONFIG_SEED, receiver_program_id]under theofframp_program. | 
| offramp_program | UncheckedAccount<'info> | No | The Offramp program ID. This exists only to derive the allowed offramp PDA and must be the second account. | 
| allowed_offramp | UncheckedAccount<'info> | No | PDA owned by the Router program that verifies this Offramp is allowed. Derivation: [ALLOWED_OFFRAMP, source_chain_selector, offramp_program_key]under the Router program. Must be the third account. | 
| Additional accounts | Various | Varies | The receiver program can define additional accounts as needed for its specific logic (state accounts, token accounts, etc.). These are application-specific. | 
Implementation Requirements
- 
Instruction Name and Discriminator: - If using Anchor, the instruction name must be exactly ccip_receive.
- If not using Anchor, the instruction discriminator must be [0x0b, 0xf4, 0x09, 0xf9, 0x2c, 0x53, 0x2f, 0xf5].
 
- If using Anchor, the instruction name must be exactly 
- 
Security Pattern: - The first three accounts in the CcipReceivecontext must follow the exact pattern shown above.
- Your program must store the Router address (typically in a state account) to verify the allowed_offrampPDA.
 
- The first three accounts in the 
- 
Account Validation: - The authoritymust be validated as a PDA derived from the offramp program.
- The allowed_offrampmust be validated as a PDA owned by the router program with the correct seeds.
 
- The 
- 
State Management: - The receiver program should maintain state that includes at minimum the router address.
- Optionally track processed message IDs to prevent replay attacks.
 
Example
Below is a minimal example of a secure CcipReceive context implementation:
#[derive(Accounts, Debug)]
#[instruction(message: Any2SVMMessage)]
pub struct CcipReceive<'info> {
    // Offramp CPI signer PDA must be first.
    #[account(
        seeds = [EXTERNAL_EXECUTION_CONFIG_SEED, crate::ID.as_ref()],
        bump,
        seeds::program = offramp_program.key(),
    )]
    pub authority: Signer<'info>,
    /// CHECK offramp program: exists only to derive the allowed offramp PDA
    pub offramp_program: UncheckedAccount<'info>,
    /// CHECK PDA of the router program verifying the signer is an allowed offramp.
    #[account(
        owner = state.router @ CcipReceiverError::InvalidCaller,
        seeds = [
            ALLOWED_OFFRAMP,
            message.source_chain_selector.to_le_bytes().as_ref(),
            offramp_program.key().as_ref()
        ],
        bump,
        seeds::program = state.router,
    )]
    pub allowed_offramp: UncheckedAccount<'info>,
    // Program-specific state account - must contain router address
    #[account(
        seeds = [STATE],
        bump,
    )]
    pub state: Account<'info, BaseState>,
    // Additional program-specific accounts...
}
And a minimal implementation of the ccip_receive instruction:
pub fn ccip_receive(ctx: Context<CcipReceive>, message: Any2SVMMessage) -> Result<()> {
    // Process message data
    if !message.data.is_empty() {
        // Process arbitrary data payload
    }
    // Process token transfers
    if !message.token_amounts.is_empty() {
        // Handle received tokens
    }
    // Emit event for tracking
    emit!(MessageReceived {
        message_id: message.message_id
    });
    Ok(())
}
Token Handling
When implementing a CCIP Receiver that needs to handle token transfers, you must create a PDA that will serve as the token administrator. This PDA will have the authority to sign token transfer instructions.
Token Admin PDA
Create a dedicated PDA to manage tokens within your program:
// During program initialization
#[account(
    init,
    seeds = [TOKEN_ADMIN_SEED],
    bump,
    payer = authority,
    space = ANCHOR_DISCRIMINATOR,
)]
/// CHECK: CPI signer for tokens
pub token_admin: UncheckedAccount<'info>,
Using remaining_accounts for Token Transfers
When handling token transfers, the number of accounts passed depends on the specific token being handled. The ccip_receive handler should use remaining_accounts to access these token accounts.
Below is an example of a typical token transfer implementation:
// Example of token-related accounts in remaining_accounts
// For each token transfer:
// 1. token_mint: Account<Mint>
// 2. source_token_account: Account<TokenAccount> (owned by program with token_admin authority)
// 3. token_admin: UncheckedAccount (the PDA with authority)
// 4. recipient_token_account: Account<TokenAccount>
// 5. token_program: Program<Token>
// Example token transfer logic
pub fn handle_token_transfer(ctx: Context<CcipReceive>, message: Any2SVMMessage) -> Result<()> {
    // Check if we have sufficient remaining accounts for token handling
    if ctx.remaining_accounts.len() < 5 {
        return Err(ErrorCode::InvalidRemainingAccounts.into());
    }
    // Extract account references from the remaining_accounts
    let token_mint_info = &ctx.remaining_accounts[0];
    let source_token_account = &ctx.remaining_accounts[1];
    let token_admin_info = &ctx.remaining_accounts[2];
    let recipient_account_info = &ctx.remaining_accounts[3];
    let token_program_info = &ctx.remaining_accounts[4];
    // Verify the token_admin is the expected PDA
    let (expected_token_admin, admin_bump) =
        Pubkey::find_program_address(&[TOKEN_ADMIN_SEED], &crate::ID);
    if token_admin_info.key() != expected_token_admin {
        return Err(ErrorCode::InvalidTokenAdmin.into());
    }
    // Create and execute the token transfer instruction
    let seeds = &[TOKEN_ADMIN_SEED, &[admin_bump]];
    let signer_seeds = &[&seeds[..]];
    // Transfer tokens using CPI with the PDA as signer
    // ... token transfer code ...
    Ok(())
}