# Implementing CCIP Receivers
Source: https://docs.chain.link/ccip/tutorials/svm/receivers
Last Updated: 2025-07-25


> **NOTE: Prerequisites**
>
> This reference guide assumes familiarity with:

- [Rust](https://www.rust-lang.org/) and the [Anchor
  framework](https://www.anchor-lang.com/)
- [Solana program
  development](https://solana.com/docs)
- [CCIP architecture](/ccip/concepts/architecture/overview)

# Implementing CCIP Receivers for Solana

This reference guide explains the key components and security patterns required for building Solana programs that can receive cross-chain messages via Chainlink's Cross-Chain Interoperability Protocol (CCIP).

## Introduction

A CCIP Receiver is a Solana program that implements the [`ccip_receive`](/ccip/api-reference/svm/v1.6.0/receiver#ccip_receive) instruction, allowing it to process incoming cross-chain messages. It can handle both arbitrary data payloads and/or token transfers, serving as the destination endpoint for CCIP messages.

## Security Architecture

To build a secure CCIP receiver program, you need to understand how it interacts with the core CCIP programs.

Your receiver program interacts with two external CCIP components:

## Core Components of a CCIP Receiver

A complete CCIP Receiver implementation contains several key components, each serving a specific purpose in the cross-chain messaging system.

### Message Structure

CCIP messages follow a standardized structure that your program must be prepared to receive:

```rust
#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
pub struct Any2SVMMessage {
    pub message_id: [u8; 32],
    pub source_chain_selector: u64,
    pub sender: Vec<u8>,
    pub data: Vec<u8>,
    pub token_amounts: Vec<SVMTokenAmount>,
}

#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default)]
pub struct SVMTokenAmount {
    pub token: Pubkey,
    pub amount: u64, // solana local token amount
}
```

These structures contain:

- `message_id`: A unique identifier for the message
- `source_chain_selector`: The chain ID of the source chain
- `sender`: The address of the sender on the source chain
- `data`: The arbitrary data payload
- `token_amounts`: An array of tokens and amounts being transferred

### The CcipReceive Context

The account context for the `ccip_receive` instruction is the most critical security component of your program. It must follow this exact pattern:

```rust
#[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 accounts follow...
    #[account(
        seeds = [STATE],
        bump,
    )]
    pub state: Account<'info, BaseState>,

    // Additional program accounts as needed...
}
```

> **CAUTION: Critical Security Pattern**
>
> The account structure above implements the required security pattern for CCIP Receivers:

1. The first three accounts
   (`authority`, `offramp_program`, and `allowed_offramp`) are provided by the CCIP Offramp program
2. These three accounts form the security validation chain
3. The `state` account and any additional accounts are provided by the message sender and are specific to your program's needs
4. It's critical to have the router stored in a state PDA so it can be used to verify the `allowed_offramp` PDA
5. Depending on your program's functionality, the context can include additional accounts that are specific to your use case

### Program State

Your program needs state accounts to store configuration information, most importantly the router address:

```rust
#[account]
#[derive(InitSpace, Default, Debug)]
pub struct BaseState {
    pub owner: Pubkey,
    pub proposed_owner: Pubkey,
    pub router: Pubkey,
}
```

This is a basic example of state storage, but your program can have more complex state structures with additional data. The critical requirement is that the state must store the router address for verification of the `allowed_offramp` PDA.

### Extending the CcipReceive Context

While the first three accounts in the CcipReceive context are mandatory and must follow the exact security pattern shown above, **you must extend this structure with additional program-specific accounts** based on your specific use case.

These additional accounts will be provided when your program is called through the CCIP Offramp. In your `ccip_receive` instruction, you should validate these accounts according to your application's security requirements before using them.

### The `ccip_receive` Instruction

The core instruction that implements the CCIP receiver interface:

```rust
/// This instruction is called by the CCIP Offramp to execute the CCIP message.
/// The method name needs to be ccip_receive with Anchor encoding.
/// If not using Anchor, the discriminator needs to be [0x0b, 0xf4, 0x09, 0xf9, 0x2c, 0x53, 0x2f, 0xf5]
pub fn ccip_receive(ctx: Context<CcipReceive>, message: Any2SVMMessage) -> Result<()> {
    // Process message data
    if !message.data.is_empty() {
        // Custom data processing logic here
    }

    // Process token transfers
    if !message.token_amounts.is_empty() {
        // Custom token handling logic here
    }

    // Emit event for tracking
    emit!(MessageReceived {
        message_id: message.message_id,
        source_chain_selector: message.source_chain_selector,
        sender: message.sender.clone(),
    });

    Ok(())
}
```

> \*\*NOTE: Instruction Name and Discriminator\*\*
>
>
>
> When 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]`. This exact name/discriminator is required
> for the CCIP Offramp to identify your program as a valid receiver.

> **NOTE: Additional Program Instructions**
>
> While this guide focuses on the `ccip_receive` instruction, a comprehensive CCIP Receiver should also implement
> several administrative instructions:

- `initialize`: Set up initial state with router address and owner
- `update_router`: Update the router address (restricted to owner)
- `transfer_ownership`: Begin ownership transfer process
- `accept_ownership`: Complete ownership transfer
- `withdraw_tokens`: Allow controlled withdrawal of received tokens

These administrative functions are not directly related to receiving messages but are important for proper program
management and security.

## Security Considerations

Building secure CCIP Receivers requires attention to several key areas:

### Caller Validation

The most critical security aspect is validating that the caller is a legitimate CCIP Offramp. This is handled by the account constraints in the `CcipReceive` context:

```rust
// 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>,
```

This constraint ensures that the transaction is signed by a PDA derived from the offramp program using a specific seed. Only the legitimate CCIP Offramp can produce this signature.

### Router Authorization

The second critical validation is ensuring that the offramp is authorized by the CCIP Router:

```rust
#[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>,
```

This validates that:

1. The `allowed_offramp` PDA exists and is owned by the router program
2. The PDA is derived using the correct seeds that include the source chain and offramp program ID
3. This proves the router has authorized this specific offramp for this specific source chain

### Optional Sender Validation

For additional security, you can implement sender validation:

```rust
// Optional additional validation in ccip_receive
pub fn ccip_receive(ctx: Context<CcipReceive>, message: Any2SVMMessage) -> Result<()> {
    // Verify sender is approved (if implementing allowlist)
    let is_approved = is_sender_approved(
        ctx.accounts.state,
        message.source_chain_selector,
        &message.sender
    );

    require!(is_approved, CcipReceiverError::InvalidChainAndSender);

    // Continue with message processing...
    Ok(())
}
```

### Message Deduplication

To prevent replay attacks, consider tracking processed message IDs:

```rust
// In your ccip_receive instruction:

// Check if message has already been processed
let is_duplicate = ctx.accounts.processed_messages
    .messages
    .contains(&message.message_id);

require!(!is_duplicate, CcipReceiverError::DuplicateMessage);

// Record the message ID to prevent reprocessing
ctx.accounts.processed_messages.messages.push(message.message_id);
if ctx.accounts.processed_messages.messages.len() > MAX_STORED_MESSAGES {
    ctx.accounts.processed_messages.messages.remove(0);
}
```

## Token Handling for Receivers

CCIP receivers that handle tokens need to understand how tokens are delivered and how to properly manage them.

### Token Delivery Process

When tokens are sent via CCIP to a Solana program:

1. The tokens are initially delivered to a token account specified as the `tokenReceiver` in the CCIP message
2. For programmable token transfers, this `tokenReceiver` must be a PDA that your program has authority over
3. Your program must implement the logic to handle the received tokens

> **CAUTION: Token Ownership**
>
> The `tokenReceiver` PDA must be controlled by your program, or the tokens will be inaccessible.

### Token Admin PDA

Create a dedicated PDA to serve as your program's token administrator:

```rust
#[account(
    init,
    seeds = [TOKEN_ADMIN_SEED],
    bump,
    payer = authority,
    space = ANCHOR_DISCRIMINATOR,
)]
/// CHECK: CPI signer for tokens
pub token_admin: UncheckedAccount<'info>,
```

This token\_admin PDA should:

1. Be initialized during program setup
2. Be used as the authority for token accounts that will receive CCIP tokens
3. Sign token transfer instructions (e.g., for forwarding tokens to their final destination)

### remaining\_accounts

For each token being transferred, your `remaining_accounts` typically needs:

1. `token_mint`: The mint account of the token
2. `source_token_account`: The account that received tokens from CCIP
3. `token_admin`: Your program's PDA with authority over the source account
4. `recipient_token_account`: The final destination for the tokens
5. `token_program`: The SPL Token program (Token or Token-2022)

**Note**: The pattern may vary depending on your specific implementation needs.

## Best Practices

When implementing CCIP Receivers, follow these best practices:

1. **Follow the Security Pattern**: Always use the exact account validation pattern shown in the `CcipReceive` context

2. **Store the Router Address**: Store and validate the router address to ensure only allowed offramps can call your program

3. **Handle Token Security**: Use PDAs with proper token authority for receiving and managing tokens

4. **Consider Message Deduplication**: Track message IDs to prevent replaying the same message

5. **Implement Proper Error Handling**: Use specific error codes and messages for better debugging and security

6. **Use Events for Tracking**: Emit events when processing messages to facilitate off-chain tracking and indexing

7. **Test Thoroughly**: Test your receiver with various message types, token amounts, and edge cases

## Example Implementation

For a complete, audited reference implementation of a CCIP Receiver, you can examine the [example-ccip-receiver](https://github.com/smartcontractkit/chainlink-ccip/tree/solana-v1.6.0/chains/solana/contracts/programs/example-ccip-receiver) in the Chainlink CCIP repository. This example demonstrates all the security patterns and best practices covered in this guide and can serve as a starting point for your own implementation.

> **CAUTION: Educational Example Disclaimer**
>
> This page includes an educational example to use a Chainlink system, product, or service and is provided to
> demonstrate how to interact with Chainlink's systems, products, and services to integrate them into your own. This
> template is provided "AS IS" and "AS AVAILABLE" without warranties of any kind, it has not been audited, and it may be
> missing key checks or error handling to make the usage of the system, product or service more clear. Do not use the
> code in this example in a production environment without completing your own audits and application of best practices.
> Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for unintended outputs
> that are generated due to errors in code.