Building CCIP Messages from EVM to SVM
Introduction
This guide explains how to construct CCIP Messages from Ethereum Virtual Machine (EVM) chains (e.g. Ethereum) to SVM chains (e.g. Solana). We'll cover the message structure, required parameters, 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 are built using the EVM2AnyMessage struct from the Client.sol library. The EVM2AnyMessage struct is defined as follows:
struct EVM2AnyMessage {
    bytes receiver;
    bytes data;
    EVMTokenAmount[] tokenAmounts;
    address feeToken;
    bytes extraArgs;
}
receiver
- Definition:This is the receiver program ID that will execute the CCIP message
- For token-only transfers: Should be set to the default value for the Pubkeytype in Solana (11111111111111111111111111111111)
- For arbitrary messaging or programmatic token transfers: Must be the exact program ID of the SVM program that implements the ccip_receiveinstruction
data
- Definition: Contains the payload that will be passed to the receiving program
- For token-only transfers: Empty (0x)
- For arbitrary messaging or programmatic token transfers: Contains the data the receiver program will process
- Encoding requirement: Must be encoded as a hex string with 0xĀ· prefix
tokenAmounts
- Definition: An array of token addresses and amounts to transfer
- For data-only messages: Must be an empty array
- For token transfers or programmatic token transfers: Each entry specifies a token address and amount. 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
- For native gas token: Use address(0)to specify the source chain's native gas token (e.g. ETH on Ethereum)
- For ERC-20 tokens: Can specify an ERC-20 token address for fee payment. Note: Check the CCIP Directory for the list of supported fee tokens on your source chain
extraArgs
The most critical component for SVM-bound messages is the SVMExtraArgsV1
 structure:
struct SVMExtraArgsV1 {
    uint32 computeUnits;
    uint64 accountIsWritableBitmap;
    bool allowOutOfOrderExecution;
    bytes32 tokenReceiver;
    bytes32[] accounts;
}
bytes4 public constant SVM_EXTRA_ARGS_V1_TAG = 0x1f3b3aba;
Let's examine each field in detail:
computeUnits
Specifies the amount of compute units allowed for calling the ccip_receive instruction of the receiver program on SVM (similar to gas limit on EVM chains).
Understanding SVM Account Model
Unlike EVM chains where smart contracts manage their own storage, SVM blockchains (e.g. Solana) use an account-based architecture where:
- All data is stored in accounts: There's no dedicated storage for programs
- Programs are stateless: They can only read from and write to accounts passed to them
- Explicit account access: Every account a program needs to access must be explicitly provided
- Access permissions: Each account must be marked as either readable or writable
accounts
An array of 32-byte Solana public keys representing additional accounts required for execution.
accountIsWritableBitmap
A 64-bit bitmap indicating which accounts in the accounts array should be marked as writable.
Suppose your accounts array has 7 accounts and accounts at positions 1 and 2 need to be writable:
accounts[0]: Not writable (bit 0 = 0)
accounts[1]: Writable (bit 1 = 1)
accounts[2]: Writable (bit 2 = 1)
accounts[3]: Not writable (bit 3 = 0)
accounts[4]: Not writable (bit 4 = 0)
accounts[5]: Not writable (bit 5 = 0)
accounts[6]: Not writable (bit 6 = 0)
Binary: 0000110 = Decimal 6
const accountIsWritableBitmap = 6n; // or BigInt(6) in JavaScript/TypeScript
- 
What does "writable" mean? In Solana, marking an account as writable means: - Allows the program to modify the account's data
- Permits lamport balance mutations
 
- 
How the bitmap works: - Each bit corresponds to an account in the accountsarray
- Uses little endian bit ordering, where the least significant bit (rightmost) is at position 0
- Bit position matches account array index (0-indexed) - bit 0 corresponds to accounts[0], bit 1 to accounts[1], etc.
- Setting bit at position N to 1 makes the Nth account writable
- When represented as a decimal value, the binary bits are read right-to-left (e.g., binary 0000110= decimal6)
 
- Each bit corresponds to an account in the 
allowOutOfOrderExecution
MUST be set to true for SVM as a destination chain.
tokenReceiver
The Solana account that will initially receive tokens, represented as a 32-byte Solana public key.
For transfers to wallets or multi-sig wallets:
- Set to the user's wallet address
- Note: Do not use an Associated Token Account (ATA) as the tokenReceiver. Instead, use the user's wallet address directly. The ATA will be automatically derived by the CCIP nodes.
For transfers to programs:
- Set to a Program Derived Address (PDA) that the program has authority over
- Note: If the receiver program does not have authority over the provided tokenReceiver, the tokens will be inaccessible.
For data-only messaging (no tokens):
- MUST set to the default Solana PublicKey (11111111111111111111111111111111)
- This is required even though no tokens are being transferred
Message Encoding Requirements
When implementing CCIP from EVM to SVM, proper encoding of various elements is crucial for successful message delivery and processing.
Implementation by Message Type
Token Transfer
Use this configuration when sending only tokens from EVM to Solana:
{
  destinationChainSelector: SVM_CHAIN_SELECTOR,
  receiver: DEFAULT_PUBKEY,
  tokenAmounts: [{ token: tokenAddress, amount: tokenAmount }],
  feeToken: feeTokenAddress,
  data: "0x",
  extraArgs: {
    computeUnits: 0,
    allowOutOfOrderExecution: true,
    tokenReceiver: recipientAddress,
    accountIsWritableBitmap: 0,
    accounts: []
  }
}
const message = {
  destinationChainSelector: 16423721717087811551,  // Solana Devnet
  receiver: "11111111111111111111111111111111",  // Default PubKey
  tokenAmounts: [{ 
    token: "0x779877A7B0D9E8603169DdbD7836e478b4624789", // LINK on Ethereum Sepolia
    amount: "1000000000000000000"  // 1 LINK (18 decimals)
  }],
  feeToken: "0x779877A7B0D9E8603169DdbD7836e478b4624789",  // LINK for fees
  data: "0x",  // No data for token-only transfer
  extraArgs: encodeExtraArgs({
    computeUnits: 0,
    allowOutOfOrderExecution: true,
    tokenReceiver: "EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB",  // Recipient wallet
    accountIsWritableBitmap: 0,
    accounts: []
  })
};
Arbitrary Messaging
Use this configuration when sending only data messages to SVM:
{
  destinationChainSelector: SVM_CHAIN_SELECTOR,
  receiver: receiverProgramId,
  tokenAmounts: [],
  feeToken: feeTokenAddress,
  data: messageData,
  extraArgs: {
    computeUnits: determinedComputeUnits,
    allowOutOfOrderExecution: true,
    tokenReceiver: DEFAULT_PUBKEY,
    accountIsWritableBitmap: calculatedBitmap,
    accounts: [PDA, account, another program ID...]
  }
}
// Identify which accounts need to be writable
const accounts = [
  "8xPzoFtAL8D2Vea9GZgkPkcLgEr9LEBYoJP2L6WNNjDM",  // account1 (not writable)
  "HSQpP9L7wgJpNYdK2Vp8nTpvbULLqB8quiUXL7AGGS3y",  // account2 (writable)
  "4wBqpRvipCi5k5zCZ4NFPiFPEM5gitBKRiSgZ8NZh3nk"   // account3 (not writable)
];
// account 1 at index0 , not writable --> bit 0 = 0
// account 2 at index1 , writable --> bit 1 = 1
// account 3 at index2 , not writable --> bit 2 = 0
// bitmap = 00000010
// bitmap in decimal = 2
const bitmap = 2;
const message = {
  destinationChainSelector: 16423721717087811551,  // Solana Devnet
  receiver: "GS64TXeEb5qDnYK2TpcGZCXBcQpSdVMzJrB8nVTeHJKH",  // Program ID
  tokenAmounts: [],  // No tokens for data-only message
  feeToken: "0x779877A7B0D9E8603169DdbD7836e478b4624789",  // LINK for fees
  data: "0x68656c6c6f", // "hello" in hex
  extraArgs: encodeExtraArgs({
    computeUnits: 200000,  // compute units required by the program for executing the ccip_receive instruction
    allowOutOfOrderExecution: true,
    tokenReceiver: "0x0000000000000000000000000000000000000000000000000000000000000000",  // Default PubKey encoded in 32-byte hex format
    accountIsWritableBitmap: 2,  // Decimal representation of bitmap
    accounts: accounts // accounts required by the program for executing the ccip_receive instruction
  })
};
Programmatic Token Transfer (Data and Tokens)
Use this configuration when sending both tokens and data in a single message:
{
  destinationChainSelector: SVM_CHAIN_SELECTOR,
  receiver: receiverProgramId,
  tokenAmounts: [{ token: tokenAddress, amount: tokenAmount }],
  feeToken: feeTokenAddress,
  data: messageData,
  extraArgs: {
    computeUnits: determinedComputeUnits,
    allowOutOfOrderExecution: true,
    tokenReceiver: PDA,
    accountIsWritableBitmap: calculatedBitmap,
    accounts: [PDA, account, another program ID...]
  }
}
// Identify all required accounts
const accounts = [
  "8xPzoFtAL8D2Vea9GZgkPkcLgEr9LEBYoJP2L6WNNjDM",  // account1 (not writable)
  "HSQpP9L7wgJpNYdK2Vp8nTpvbULLqB8quiUXL7AGGS3y",  // account2 (writable)
  "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",  // account3 (not writable)
  "57y3NXjkiAzP5Gw9WuUwzJMJbJQAHH6jUYBQfZdTE5zJ",  // account4 (writable)
  "H4irvMb7oLqGRcaC2RDB7r1ynJsmHgbpQeJH8qZkDuiT",  // account5 (not writable)
  "EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB",  // account6 (writable)
  "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"     // account7 (not writable)
];
// account 1 at index0 , not writable --> bit 0 = 0
// account 2 at index1 , writable --> bit 1 = 1
// account 3 at index2 , not writable --> bit 2 = 0
// account 4 at index3 , writable --> bit 3 = 1
// account 5 at index4 , not writable --> bit 4 = 0
// account 6 at index5 , writable --> bit 5 = 1
// account 7 at index6 , not writable --> bit 6 = 0
// bitmap = 00101010
// bitmap in decimal = 42
const bitmap = 42;
const message = {
  destinationChainSelector: 16423721717087811551,  // Solana Devnet
  receiver: "GS64TXeEb5qDnYK2TpcGZCXBcQpSdVMzJrB8nVTeHJKH",  // Program ID
  tokenAmounts: [{
    token: "0x779877A7B0D9E8603169DdbD7836e478b4624789",
    amount: "1000000000000000000" // 1 LINK (18 decimals)
  }],
  feeToken: "0x0000000000000000000000000000000000000000",  // Native ETH for fees
  data: "0x68656c6c6f", // "hello" in hex
  extraArgs: encodeExtraArgs({
    computeUnits: 300000,  // compute units required by the program for executing the ccip_receive instruction
    allowOutOfOrderExecution: true,
    tokenReceiver: "57y3NXjkiAzP5Gw9WuUwzJMJbJQAHH6jUYBQfZdTE5zJ", // a PDA that the program has authority over
    accountIsWritableBitmap: 42,  // Decimal representation of bitmap
    accounts: accounts
  })
};
Related Tutorials
To see these concepts in action with step-by-step implementation guides, check out the following tutorials:
- Token Transfers: EVM to SVM - Learn how to implement token-only transfers from EVM chains to Solana wallets
- Arbitrary Messaging: EVM to SVM - Learn how to send data messages from EVM chains to Solana programs
These tutorials provide complete, working examples using the concepts covered in this guide.