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: The receiver field specifies which program on the destination chain will process the incoming CCIP message.
- Token-only transfers:
- Use:
0x0000000000000000000000000000000000000000000000000000000000000000
(32-byte zero address) - Why: No program execution needed for token-only transfers.
- Use:
- Arbitrary Messaging or Programmatic Token Transfers:
- Use: The program ID of the SVM program that implements the
ccip_receive
instruction, converted to a 32-byte hex format. - Why: The program will process the incoming CCIP message and execute the
ccip_receive
instruction.
- Use: The program ID of the SVM program that implements the
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
accounts
array - 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 hex-encoded Solana public key.
For transfers to wallets or multi-sig wallets:
- Use: The user's wallet address converted to 32-byte hex format.
- 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:
- Use: A Program Derived Address (PDA) that the program has authority over, converted to 32-byte hex format.
- Note: If the receiver program does not have authority over the provided
tokenReceiver
, the tokens will be inaccessible.
For data-only messaging (no tokens):
- Use:
0x0000000000000000000000000000000000000000000000000000000000000000
(32-byte zero address). - Why: 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: "0x0000000000000000000000000000000000000000000000000000000000000000",
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: "0x0000000000000000000000000000000000000000000000000000000000000000",
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: "0xc6ea0f22160ac820ce7cd983587acd07557c7d9c79cc15dd818f50d2c554e848", // Recipient wallet EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB encoded in 32-byte hex format
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: "0x0000000000000000000000000000000000000000000000000000000000000000",
accountIsWritableBitmap: calculatedBitmap,
accounts: [PDA, account, another program ID...]
}
}
// Identify which accounts need to be writable
const accounts = [
"0x7632c1560daece8311938a50279740e5a2307aeca65f17d251f96b57a1397d2c", // account1 (not writable) - 8xPzoFtAL8D2Vea9GZgkPkcLgEr9LEBYoJP2L6WNNjDM encoded in 32-byte hex format
"0xf43d70e10f1177756dfb75ee3a887a2b7d788c6b5161bfba053798db9ef18518", // account2 (writable) - HSPQpP9L7wgJpNYdK2Vp8nTpvbULLqB8quiUXL7AGGS3y encoded in 32-byte hex format
"0x3a74aee651d257c867d8c08013f228b099e023a978bf6b206b15c568212bb3c5" // account3 (not writable) - 4wBqpRvipCi5k5zCZ4NFPiFPEM5gitBKRiSgZ8NZh3nk encoded in 32-byte hex format
];
// 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: "0xe54c8b87d7468bcb060b16f10d7ed2d9fb7f3412df32278bae7fc1d8d7716a18", // Program ID - GS64TXeEb5qDnYK2TpcGZCXBcQpSdVMzJrB8nVTeHJKH encoded in 32-byte hex format
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 = [
"0x7632c1560daece8311938a50279740e5a2307aeca65f17d251f96b57a1397d2c", // account1 (not writable) - 8xPzoFtAL8D2Vea9GZgkPkcLgEr9LEBYoJP2L6WNNjDM encoded in 32-byte hex format
"0xf43d70e10f1177756dfb75ee3a887a2b7d788c6b5161bfba053798db9ef18518", // account2 (writable) - HSPQpP9L7wgJpNYdK2Vp8nTpvbULLqB8quiUXL7AGGS3y encoded in 32-byte hex format
"0xc6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61", // account3 (not writable) - EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v encoded in 32-byte hex format
"0x3d379922db649fe03267da8d2fa5316d9c9e44ebe5ebb9650ba0fb7748a539b3", // account4 (writable) - 57y3NXjkiAzP5Gw9WuUwzJMJbJQAHH6jUYBQfZdTE5zJ encoded in 32-byte hex format
"0xeeaf10b62e59f55bfe88c35074cd8ca9927d1bfb6489660859387f0eef4a6fc4", // account5 (not writable) - H4irvMb7oLqGRcaC2RDB7r1ynJsmHgbpQeJH8qZkDuiT encoded in 32-byte hex format
"0xc6ea0f22160ac820ce7cd983587acd07557c7d9c79cc15dd818f50d2c554e848", // account6 (writable) - EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB encoded in 32-byte hex format
"0x06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9" // account7 (not writable) - TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA encoded in 32-byte hex format
];
// 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: "0xe54c8b87d7468bcb060b16f10d7ed2d9fb7f3412df32278bae7fc1d8d7716a18", // Program ID - GS64TXeEb5qDnYK2TpcGZCXBcQpSdVMzJrB8nVTeHJKH encoded in 32-byte hex format
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: "0x3d379922db649fe03267da8d2fa5316d9c9e44ebe5ebb9650ba0fb7748a539b3", // a PDA that the program has authority over - 57y3NXjkiAzP5Gw9WuUwzJMJbJQAHH6jUYBQfZdTE5zJ encoded in 32-byte hex format
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.