Building CCIP Messages from SVM to EVM
Introduction
This guide explains how to construct CCIP Messages from SVM chains (e.g. Solana) to EVM chains (e.g. Ethereum, Arbitrum, Avalanche, etc.). We'll cover the message structure, required parameters, account management, and implementation details for different message types including token transfers, arbitrary data messaging, and programmatic token transfers (data and tokens).
CCIP Message Structure
CCIP messages from SVM are built using the SVM2AnyMessage
struct from the CCIP Router program. See the CCIP Router API Reference for complete details. The SVM2AnyMessage
struct is defined as follows:
pub struct SVM2AnyMessage {
pub receiver: Vec<u8>,
pub data: Vec<u8>,
pub token_amounts: Vec<SVMTokenAmount>,
pub fee_token: Pubkey, // pass zero address if native SOL
pub extra_args: Vec<u8>,
}
receiver
- For EVM destinations:
- This is the address of the contract or wallet that will receive the message
- Target either smart contracts that implement
ccipReceive
function or user wallets for token-only transfers - Must be properly formatted as a 32-byte Solana-compatible byte array
data
- Definition: Contains the payload that will be passed to the receiving contract on the destination chain
- For token-only transfers: Must be empty
- For arbitrary messaging or programmatic token transfers: Contains the data the receiver contract will process
- Encoding consideration: The receiver on the destination chain must be able to correctly decode this data.
tokenAmounts
- Definition: An array of token addresses and amounts to transfer
- Each token is represented by a
SVMTokenAmount
struct that contains:- token: The Solana token mint public key
- amount: The amount to transfer in the token's native denomination
- For data-only messages: Must be an empty array
- Note: Check the CCIP Directory for the list of supported tokens on each lane
feeToken
- Definition: Specifies which token to use for paying CCIP fees
- Use
Pubkey::default()
to pay fees with native SOL - Alternatively, specify a token mint address for fee payment with that token
- Note: Check the CCIP Directory for the list of supported fees tokens for the SVM chain you are using
extraArgs
For EVM-bound messages, the extraArgs
field must include properly encoded parameters for:
struct GenericExtraArgsV2 {
gas_limit: u256,
allow_out_of_order_execution: bool,
}
- gas_limit: Specifies the amount of gas allocated for calling the receiving contract on the destination chain
- allow_out_of_order_execution: Flag that determines if messages can be executed out of order
Implementation by Message Type
Token Transfer
Use this configuration when sending only tokens from SVM to EVM chains:
const message = {
receiver: evmAddressToSolanaBytes(evmReceiverAddress), // 32-byte padded EVM address
data: new Uint8Array(), // Empty data for token-only transfer
tokenAmounts: [{ token: tokenMint, amount: amount }],
feeToken: feeTokenMint, // Or Pubkey.default() for native SOL
extraArgs: encodeExtraArgs({
gasLimit: 0, // Must be 0 for token-only transfers
allowOutOfOrderExecution: true // Must be true for all messages
})
};
Arbitrary Messaging
Use this configuration when sending only data to EVM chains:
const message = {
receiver: evmAddressToSolanaBytes(evmReceiverAddress), // 32-byte padded EVM address
data: messageData, // Encoded data to send
tokenAmounts: [], // Empty array for data-only messages
feeToken: feeTokenMint, // Or Pubkey.default() for native SOL
extraArgs: encodeExtraArgs({
gasLimit: 200000, // Appropriate gas limit for the receiving contract
allowOutOfOrderExecution: true // Must be true for all messages
})
};
Programmatic Token Transfer (Data and Tokens)
Use this configuration when sending both tokens and data in a single message:
const message = {
receiver: evmAddressToSolanaBytes(evmReceiverAddress), // 32-byte padded EVM address
data: messageData, // Encoded data to send
tokenAmounts: [{ token: tokenMint, amount: amount }],
feeToken: feeTokenMint, // Or Pubkey.default() for native SOL
extraArgs: encodeExtraArgs({
gasLimit: 300000, // Higher gas limit for complex operations
allowOutOfOrderExecution: true // Must be true for all messages
})
};
Understanding the ccip_send
Instruction
The core of sending CCIP messages from SVM is the ccip_send
instruction in the CCIP Router program. This instruction requires several key components to be prepared correctly:
Core Components for ccip_send
- Destination Chain Selector: A unique identifier for the target blockchain
- Message Structure: The
SVM2AnyMessage
containing receiver, data, tokens, etc. - Required Accounts: All accounts needed for the instruction
- Token Indices: For token transfers, indices marking where each token's accounts begin. See detailed explanation in the API Reference
Data Encoding for Cross-Chain Compatibility
When sending data from SVM to EVM chains, proper encoding is crucial:
// Example: Encoding a string message for an EVM receiver
// Import ethers ABI coder
import {ethers} from 'ethers';
// Create an ABI coder instance
const abiCoder = new ethers.utils.AbiCoder();
// Encode a string message
// Note: abiCoder.encode returns a hex string with '0x' prefix
const encodedHex = abiCoder.encode(['string'], ['Hello from SVM ']);
// Convert to Buffer format for Solana
const messageData = messageDataToBuffer(encodedHex);
When sending messages across chains:
- Ensure the data payload uses an encoding format that's compatible with the receiver contract on the destination chain.
- For EVM-based destinations, implement Ethereum ABI encoding to guarantee proper data interpretation upon receipt.
Account Requirements
Unlike EVM chains which use a simple mapping storage model, SVM account model requires explicit specification of all accounts needed for an operation. When sending CCIP messages, several account types are needed. For complete account specifications, see the CCIP Router API Reference:
For each token being transferred, the following accounts must be provided:
- User Token Account (writable) - Source of tokens
- CCIP Pool Chain Config (writable) - Chain configuration PDA
- Token Pool Lookup Table - Address Lookup Table for the token
- Token Admin Registry - Registry PDA for this token
- Pool Program - Program handling token pools
- Pool Config - Configuration for the pool
- Pool Token Account (writable) - Destination for locked tokens
- Pool Signer - PDA with authority for the pool
- Token Program - Program of the token
- Token Mint - The token's mint
- CCIP Router Pools Signer - Router's signer for the pool
- Additional Pool-specific Accounts - Plus any additional accounts the pool implementation requires
These accounts must be provided in exactly this order in the remaining_accounts
parameter.
Note: The tokenIndexes
parameter must be provided in the ccip_send
instruction parameter, containing the starting indices in the remaining accounts array where each token's accounts begin.
-
Core Accounts
- Config PDA (index 0): The CCIP Router configuration account - Stores router configuration settings.
- Destination Chain (index 1): State account for the target chain - Stores information about the destination chain and is writable because it's updated during message sending.
- Nonce (index 2): Per-user sequence tracking - Keeps track of message sequence numbers for each user and destination chain pair, and is writable because it's incremented with each message.
- Authority (signer) (index 3): Transaction signer/fee payer - The account that signs the transaction and pays for fees, must be a signer and is writable.
- System Program (index 4): For account creation if needed - Required for account creation operations.
-
Fee Payment Accounts
- Fee Token Program (index 5): Token program for the fee token - Either the standard SPL Token or Token-2022 program.
- Fee Token Mint (index 6): The mint of the token used for fees - Defines which token is used to pay for CCIP fees.
- User's Fee Token (index 7): Token account of fee payer - The user's token account which is debited for fees, is writable.
- Fee Receiver (index 8): Destination for fees - The account where fees are sent, is writable.
- Fee Billing Signer (index 9): PDA with authority over fee receiver - Has authority to manage fee-related operations.
-
Fee Quoter Accounts
- Fee Quoter (index 10): The Fee Quoter program account - Program responsible for calculating fees.
- Fee Quoter Config (index 11): Configuration account for the Fee Quoter - Stores global configuration for the Fee Quoter.
- Fee Quoter Dest Chain (index 12): Destination chain configuration for the Fee Quoter - Specific fee configuration for the target chain.
- Fee Quoter Billing Token Config (index 13): Fee token configuration for billing - Specific configuration for the token used to pay fees.
- Fee Quoter Link Token Config (index 14): LINK token configuration for the Fee Quoter - Configuration for LINK token as a reference.
-
RMN Remote Accounts
- RMN Remote (index 15): The Remote Monitoring Network program - Responsible for cross-chain monitoring and validation.
- RMN Remote Curses (index 16): Curse records for the Remote Monitoring Network - Stores validation information.
- RMN Remote Config (index 17): Configuration for the Remote Monitoring Network - Stores settings for the RMN program.
These accounts must be provided in exactly this order in the remaining_accounts
parameter. See structure of token sub-slices in the API reference.
Note: The tokenIndexes
parameter must be provided in the ccip_send
instruction parameter, containing the starting indices in the remaining accounts array where each token's accounts begin. See examples of single and multi-token transfers in the API reference.
Handling Transaction Size Limits
SVM chains have transaction size limitations that become important when sending CCIP messages:
-
Account Reference Limit:
- SVM transactions have a limit on how many accounts they can reference
- Each token transfer adds approximately 11-12 accounts to your transaction
-
Address Lookup Tables (ALTs):
- ALTs allow transactions to reference accounts without including full public keys
- The CCIP Router requires each token to have an ALT in its Token Admin Registry
- The CCIP Router relies on these ALTs to locate the correct Pool Program and other token-specific accounts
-
Transaction Serialized Size:
- Even with ALTs, SVM transactions have a maximum serialized size (1232 bytes)
- Each token transfer increases the transaction size
- If your transaction exceeds this limit, you'll need to split it into multiple transactions
Tracking Messages with Transaction Logs
After sending a CCIP message, the CCIP Router emits a CCIPMessageSent
event in the transaction logs containing key tracking information:
Program log: Event: {
"name": "CCIPMessageSent",
"data": {
"dest_chain_selector": [destination chain ID],
"sequence_number": [sequence number],
"message": {
"header": {
"message_id": "0x123...",
...
}
}
}
}
The message_id
in the event header serves as the unique cross-chain identifier that:
- Links transactions between source and destination chains
- Provides confirmation of successful message execution
Store this identifier in your application for transaction tracking and reconciliation.
Further Resources
- CCIP Router API Reference: Complete technical details about message structure, account requirements, and token handling
- CCIP Messages API Reference: Comprehensive documentation of all message structures
- How token_indexes and remaining_accounts Work: Step-by-step explanation with examples