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

  1. Destination Chain Selector: A unique identifier for the target blockchain
  2. Message Structure: The SVM2AnyMessage containing receiver, data, tokens, etc.
  3. Required Accounts: All accounts needed for the instruction
  4. 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);

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:

  1. User Token Account (writable) - Source of tokens
  2. CCIP Pool Chain Config (writable) - Chain configuration PDA
  3. Token Pool Lookup Table - Address Lookup Table for the token
  4. Token Admin Registry - Registry PDA for this token
  5. Pool Program - Program handling token pools
  6. Pool Config - Configuration for the pool
  7. Pool Token Account (writable) - Destination for locked tokens
  8. Pool Signer - PDA with authority for the pool
  9. Token Program - Program of the token
  10. Token Mint - The token's mint
  11. CCIP Router Pools Signer - Router's signer for the pool
  12. 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.

Handling Transaction Size Limits

SVM chains have transaction size limitations that become important when sending CCIP messages:

  1. 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
  2. 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
  3. 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

Get the latest Chainlink content straight to your inbox.