# Building CCIP Messages from SVM to EVM
Source: https://docs.chain.link/ccip/tutorials/svm/source/build-messages
Last Updated: 2025-07-25

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

## 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 programmable token transfers (data and tokens).

## CCIP Message Structure

CCIP messages from SVM are built using the [`SVM2AnyMessage`](/ccip/api-reference/svm/v1.6.0/messages#svm2anymessage) struct from the CCIP Router program. See the [CCIP Router API Reference](/ccip/api-reference/svm/v1.6.0/router) for complete details. The `SVM2AnyMessage` struct is defined as follows:

```rust
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

> \*\*NOTE: Converting EVM Addresses for Solana\*\*
>
>
>
> EVM addresses (20 bytes) must be converted to Solana-compatible format (32 bytes) for CCIP messages. This format difference exists because Solana accounts are always 32 bytes.
>
> **The conversion process:**
>
> 1. Start with a standard EVM address (0x-prefixed, 20 bytes)
> 2. Remove the 0x prefix and convert to a byte array
> 3. Left-pad with zeros to reach 32 bytes
>
> ```javascript
> function evmAddressToSolanaBytes(evmAddress) {
>   // Remove 0x prefix if present
>   const hex = evmAddress.startsWith('0x') ? evmAddress.slice(2) : evmAddress;
>   
>   // Convert hex to bytes (assumes valid Ethereum address)
>   const addressBytes = Buffer.from(hex, 'hex');
>   
>   // Left-pad to 32 bytes
>   const paddedBytes = Buffer.alloc(32);
>   paddedBytes.set(addressBytes, 32 - addressBytes.length);
>   
>   return paddedBytes;
> }
> ```

### 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 **programmable 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.

> \*\*NOTE: Data Encoding for Cross-Chain Messages\*\*
>
>
>
> When sending data from Solana to EVM chains, there are two important steps:
>
> 1. **Format the data** for the receiving contract (optional but recommended for EVM)
> 2. **Convert to Buffer format** for the Solana CCIP Router program
>
> **Example using [ethers.js](https://www.npmjs.com/package/ethers) for [EVM ABI encoding](https://docs.soliditylang.org/en/latest/abi-spec.html):**
>
> ```javascript
> import { ethers } from 'ethers';
>
> // Step 1: Format data for EVM contract (using ABI encoding)
> const messageData = ethers.AbiCoder.defaultAbiCoder().encode(["string"], ["Hello World"]);
>
> // Step 2: Convert to Buffer format for the Solana CCIP Router
> const dataBuffer = messageDataToBuffer(messageData);
>
> // Use in your message
> const message = {
>   receiver: evmAddressToSolanaBytes(receiverAddress),
>   data: dataBuffer,
>   // ... other message fields
> };
> ```
>
> **Helper function to convert data to Buffer format:**
>
> ```javascript
> /**
>  * Converts message data to a Buffer format required by Solana
>  * This conversion is needed regardless of destination chain
>  */
> function messageDataToBuffer(messageData) {
>   // Return empty buffer for empty data
>   if (!messageData) {
>     return Buffer.alloc(0);
>   }
>   
>   // Handle hex strings (from ABI encoding) or regular strings
>   return messageData.startsWith("0x") 
>     ? Buffer.from(messageData.slice(2), "hex") 
>     : Buffer.from(messageData);
> }
> ```

### 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](/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](/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:

```rust
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

> \*\*NOTE: Setting Extra Args\*\*
>
>
>
> - **Gas limit**:
>   - For token transfers only: Gas limit must be set to 0
>   - For arbitrary messaging or programmable token transfers: gas limit must be set based on the receiving contract's complexity.
> - **Allow out of order execution**:
>   - Must always be set to true.

> \*\*NOTE: Encoding extraArgs for EVM Destinations\*\*
>
>
>
> For EVM destinations, you must encode the `extraArgs` field with gas limit and execution flag:
>
> ```javascript
> /**
>  * Creates properly encoded extraArgs buffer for EVM destinations
>  * @param {number} gasLimit - Gas limit for execution on destination chain (use 0 for token-only transfers)
>  * @returns {Buffer} - Properly encoded extraArgs buffer
>  */
> function encodeExtraArgs(gasLimit = 0) {
>   // 1. Type Tag: bytes4(keccak256("CCIP EVMExtraArgsV2")) = 0x181dcf10
>   const typeTag = Buffer.from([0x18, 0x1d, 0xcf, 0x10])
>
>   // 2. Gas Limit: Convert to 16-byte little-endian format
>   // Note: We're using a BigNumber library here (bn.js)
>   const bn = require("bn.js")
>   const gasLimitLE = new bn(gasLimit).toArrayLike(Buffer, "le", 16)
>
>   // 3. Allow Out Of Order Execution flag: Must be true (byte value 1)
>   const allowOutOfOrderFlag = Buffer.from([1]) // Always 1 (true)
>
>   // 4. Concatenate all parts to form the final buffer
>   return Buffer.concat([
>     typeTag, // 4 bytes - type identifier
>     gasLimitLE, // 16 bytes - gas limit in little-endian
>     allowOutOfOrderFlag, // 1 byte - boolean flag (always true)
>   ])
> }
>
> // Example usage:
> // For token transfers (gas limit = 0)
> const tokenTransferExtraArgs = encodeExtraArgs(0)
>
> // For arbitrary messaging (specify appropriate gas limit)
> const messagingExtraArgs = encodeExtraArgs(200000)
> ```
>
> **Buffer Structure Breakdown:**
>
> - **Byte 0-3**: Type tag (0x181dcf10) - Identifies this as EVMExtraArgsV2
> - **Byte 4-19**: Gas limit - 16 bytes in little-endian format
> - **Byte 20**: Allow out of order execution flag - Always 1 (true)

## Implementation by Message Type

### Token Transfer

Use this configuration when sending only tokens from SVM to EVM chains:

```typescript
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
  })
};
```

> **NOTE: Key Requirements**
>
> - **`gasLimit`** MUST be 0 for token-only transfers

- **`data`** must be empty
- **The receiver** must be properly formatted as a 32-byte left-padded Ethereum address
- **`allowOutOfOrderExecution`** must be true for all messages
- **Check** the [CCIP Directory](/ccip/directory) for the list of supported tokens for the lane you are using

### Arbitrary Messaging

Use this configuration when sending only data to EVM chains:

```typescript
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
  })
};
```

> **NOTE: Key Requirements**
>
> - **`gasLimit`** must be sufficient for the receiving contract to process the message

- **`data`** must be properly encoded to be compatible with the receiving contract
- **`tokenAmounts`** must be an empty array
- **`allowOutOfOrderExecution`** must be true for all messages
- **The receiver** must be a contract that implements `ccipReceive` function

### Programmable Token Transfer (Data and Tokens)

Use this configuration when sending both tokens and data in a single message:

```typescript
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
  })
};
```

> **NOTE: Key Requirements**
>
> - **`gasLimit`** must be sufficient for the receiving contract to process the message

- **`data`** must be properly encoded to be compatible with the receiving contract
- **`tokenAmounts`** must be an array of tokens to transfer
- **The receiving contract** must implement logic to handle both tokens and data
- **`allowOutOfOrderExecution`** must be true for all messages
- **Check** the [CCIP Directory](/ccip/directory) for the list of supported tokens for the lane you are using

## 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](/ccip/api-reference/svm/v1.6.0/router#how-remaining_accounts-and-token_indexes-work)

### Data Encoding for Cross-Chain Compatibility

When sending data from SVM to EVM chains, proper encoding is crucial:

### 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](/ccip/api-reference/svm/v1.6.0/router#context-accounts):

<Aside title="Account Derivation">
  For complete account derivation documentation, see the [CCIP Router API Reference](/ccip/api-reference/svm/v1.6.0/router#context-accounts).

  These accounts are Program Derived Addresses (PDAs) that must be derived with specific seeds:

  **Core Router PDAs:**

  - **Config**: `["config"]` - Derived from CCIP Router program
  - **Destination Chain**: `["dest_chain_state", destChainSelector]` - Derived from CCIP Router program
  - **Nonce**: `["nonce", destChainSelector, userPubkey]` - Derived from CCIP Router program
  - **Fee Billing Signer**: `["fee_billing_signer"]` - Derived from CCIP Router program

  **Fee Quoter PDAs:**

  - **Fee Quoter Config**: `["config"]` - Derived from Fee Quoter program
  - **Fee Quoter Dest Chain**: `["dest_chain", destChainSelector]` - Derived from Fee Quoter program
  - **Fee Quoter Billing Token Config**: `["billing_token_config", tokenMint]` - Derived from Fee Quoter program
  - **Fee Quoter Link Token Config**: `["billing_token_config", linkTokenMint]` - Derived from Fee Quoter program

  **RMN Remote PDAs:**

  - **RMN Remote Curses**: `["curses"]` - Derived from RMN Remote program
  - **RMN Remote Config**: `["config"]` - Derived from RMN Remote program

  **Token-specific PDAs:**

  - **Token Admin Registry**: `["token_admin_registry", tokenMint]` - Derived from CCIP Router program
  - **Pool Chain Config**: `["pool_chain_config", destChainSelector, tokenMint]` - Derived from Pool program
  - **Pool Signer**: `["pool_signer"]` - Derived from Pool program
  - **CCIP Router Pools Signer**: `["external_token_pools_signer", poolProgram]` - Derived from CCIP Router program

  Using incorrect seeds or deriving PDAs from the wrong program will result in account validation failures. All PDAs must be properly derived based on their respective program IDs.
</Aside>

## 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

   > \*\*NOTE: Address Lookup Tables (ALTs) in CCIP\*\*
   >
   >
   >
   > Address Lookup Tables are a critical architectural component of CCIP token transfers from SVM:
   >
   > - **Each supported token** requires a registered ALT in its Token Admin Registry
   > - **ALTs contain** essential token-specific accounts including the Pool Program
   > - **The Router program** uses ALTs to locate required accounts for token transfers
   > - **Token transfers** will fail without properly configured ALTs
   > - **When implementing transfers**, ensure your transaction includes the ALT for each token

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

- [CCIP Router API Reference](/ccip/api-reference/svm/v1.6.0/router): Complete technical details about message structure, account requirements, and token handling
- [CCIP Messages API Reference](/ccip/api-reference/svm/v1.6.0/messages): Comprehensive documentation of all message structures
- [How token\_indexes and remaining\_accounts Work](/ccip/api-reference/svm/v1.6.0/router#how-remaining_accounts-and-token_indexes-work): Step-by-step explanation with examples

> **CAUTION: Educational Example Disclaimer**
>
> This page includes an educational example to use a Chainlink system, product, or service and is provided to
> demonstrate how to interact with Chainlink's systems, products, and services to integrate them into your own. This
> template is provided "AS IS" and "AS AVAILABLE" without warranties of any kind, it has not been audited, and it may be
> missing key checks or error handling to make the usage of the system, product or service more clear. Do not use the
> code in this example in a production environment without completing your own audits and application of best practices.
> Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for unintended outputs
> that are generated due to errors in code.