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

> For the complete documentation index, see [llms.txt](/llms.txt).

> **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/v1/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/v1/svm/api-reference/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:

### Architectural Components

The CCIP security architecture consists of:

1. **Receiver Program** (your SVM program)

   - Implements the [`ccip_receive`](/ccip/v1/svm/api-reference/v1.6.0/receiver#ccip_receive) instruction
   - Validates that messages come from an authorized OffRamp program

2. **CCIP Router**

   - Central coordinator of cross-chain communication
   - Manages allowed OffRamps
   - Creates PDAs that verify OffRamp authorization

3. **CCIP Offramp**

   - Delivers messages to the receiver program
   - Uses a specific PDA to sign transactions calling your program

### Security Model

The security model relies on three critical validations:

1. **Authority Validation**

   - Verify the calling authority
   - is a legitimate Offramp PDA
   - This prevents unauthorized programs from sending fake messages

2. **Router Validation**

   - Verify the Offramp is authorized by the Router
   - This ensures only trusted Offramps approved by the Router can deliver messages

3. **Optional Sender Validation**

   - Optionally validate the source chain and sender address
   - This provides an additional layer of security by restricting which sources can send messages

## 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**
>
> Please note, this page contains community examples only — these are not Chainlink products or services and are not
> supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is
> intended for demonstration and educational purposes only. It is provided "AS IS" and "AS AVAILABLE" without warranties
> of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this
> example code does so entirely at their own risk and must perform its own audits, security and code review, key
> management, and testing before any production deployment and ensure the operation and performance of such code matches
> expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses
> any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no
> commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely
> on this code without first conducting your own technical, engineering, and security review. This code is also outside
> the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node
> operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for
> any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of
> Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you
> acknowledge and agree to these terms.