Building CCIP Messages from Aptos to EVM

Introduction

This guide explains how to construct CCIP Messages from the Aptos blockchain to EVM chains (e.g., Ethereum, Arbitrum, Base, etc.). We'll cover the message structure by examining the ccip_send entry function from the ccip_router::router module, its required parameters, and the implementation details for different message types including token transfers, arbitrary data messaging, and programmatic token transfers (data and tokens).

CCIP Message Structure on Aptos

CCIP messages from Aptos are initiated by calling the ccip_send entry function in the CCIP Router Move module. This function serves as the single entry point for all cross-chain messages, and internally routes the request to the correct On-Ramp contract version based on the destination chain. See the CCIP Router API Reference for complete details.

As defined in the ccip_router::router module, the ccip_send function has the following signature:

public entry fun ccip_send(
    caller: &signer,
    dest_chain_selector: u64,
    receiver: vector<u8>,
    data: vector<u8>,
    token_addresses: vector<address>,
    token_amounts: vector<u64>,
    token_store_addresses: vector<address>,
    fee_token: address,
    fee_token_store: address,
    extra_args: vector<u8>
)

receiver

  • Definition: The address of the contract or wallet on the destination EVM chain that will receive the message.
  • Formatting: EVM addresses are 20 bytes long, but Aptos requires this parameter to be a 32-byte array. You must left-pad the 20-byte EVM address with 12 zero-bytes to create a valid 32-byte array.

data

  • Definition: The payload that will be executed by the receiving contract on the destination chain.
  • Usage:
    • For token-only transfers: This must be an empty vector<u8>.
    • For arbitrary messaging or programmatic token transfers: This contains the function selectors and arguments for the receiver contract, typically ABI-encoded.
  • Encoding: The receiver contract on the destination EVM chain must be able to decode this data. Standard EVM ABI-encoding is the recommended approach.

token_addresses

A vector of Aptos token type addresses to be transferred.

token_amounts

A vector of amounts to transfer for each corresponding token, in the token's smallest denomination.

token_store_addresses

A vector of addresses representing the Fungible Asset store from which the tokens will be withdrawn. You could just use 0x0 as the token_store_address, because when using 0x0, it would retrieve the primary_store_address (using primary_fungible_store::primary_store_address) of the token corresponding to the sender's account.

For data-only messages, the token_addresses, token_amounts, and token_store_addresses vectors must all be empty.

fee_token

The address of the token to be used for paying CCIP fees. This can be native APT (0xa) or a supported token like LINK.

fee_token_store

The address of the Fungible Asset store from which the fee will be paid. You can use 0x0 here as well, which will resolve to the primary store for the fee token in the sender's account.

extra_args

For messages going to an EVM chain, the extra_args parameter is a vector<u8> that must be encoded according to a specific format for compatibility. While there is no literal GenericExtraArgsV2 struct in the Aptos modules, the byte vector must be encoded to match the format that EVM-family chains expect.

This format consists of a 4-byte tag (0x181dcf10) followed by the BCS-encoded parameters:

  • gas_limit: (u256) The gas limit for the execution of the transaction on the destination EVM chain.
  • allow_out_of_order_execution: (bool) A flag that must always be set to true for Aptos-to-EVM messages.

The ccip::client module provides a helper view function, encode_generic_extra_args_v2, to perform this encoding on-chain. Off-chain scripts replicate this logic to construct the byte vector correctly.

Estimating Fees

Before sending a transaction, you must calculate the required fee. The ccip_router::router module provides a get_fee view function for this purpose. It takes the exact same arguments as ccip_send, allowing you to get a precise fee quote for your intended message.

import { Aptos } from "@aptos-labs/ts-sdk"

async function getCcipFee(aptos: Aptos, messagePayload: any) {
  const fee = await aptos.view({
    payload: {
      function: `${ccipRouterModuleAddr}::router::get_fee`,
      functionArguments: messagePayload.functionArguments, // Reuse the same arguments as ccip_send
    },
  })
  return fee[0]
}

// You would call this before submitting your ccip_send transaction.
const feeAmount = await getCcipFee(aptosClient, transactionPayload)
console.log(`Required CCIP Fee: ${feeAmount}`)

Implementation by Message Type

Token Transfer

Use this configuration when sending only tokens from Aptos to an EVM chain:

import { MoveVector } from "@aptos-labs/ts-sdk"

const transactionPayload = {
  function: `${ccipRouterModuleAddr}::router::ccip_send`,
  functionArguments: [
    destinationChainSelector, // e.g., "16015286601757825753" for Ethereum Sepolia
    MoveVector.U8(evmAddressToAptos(receiverAddress)), // Padded 32-byte receiver
    MoveVector.U8([]), // Empty data vector
    [ccipBnmTokenAddress], // Vector of token addresses
    MoveVector.U64([10000000n]), // Vector of token amounts
    [tokenStoreAddress], // Vector of token store addresses
    feeTokenAddress, // e.g., LINK or native APT token address
    feeTokenStore, // Fee token store address
    encodeGenericExtraArgsV2(0n, true), // extraArgs with gasLimit = 0
  ],
}

Arbitrary Messaging

Use this configuration when sending only data to EVM chains.

import { MoveVector, Hex } from "@aptos-labs/ts-sdk"
import { ethers } from "ethers"

// ABI-encode the data for the receiver contract
const encodedData = ethers.AbiCoder.defaultAbiCoder().encode(["string"], ["Hello World"])
const dataBytes = Hex.hexInputToUint8Array(encodedData)

const transactionPayload = {
  function: `${ccipRouterModuleAddr}::router::ccip_send`,
  functionArguments: [
    destinationChainSelector,
    MoveVector.U8(evmAddressToAptos(receiverContractAddress)),
    MoveVector.U8(dataBytes), // The encoded data payload
    [], // Empty token addresses vector
    MoveVector.U64([]), // Empty token amounts vector
    [], // Empty token store addresses vector
    feeTokenAddress,
    feeTokenStore,
    encodeGenericExtraArgsV2(200000n, true), // extraArgs with appropriate gasLimit
  ],
}

Programmatic Token Transfer (Data and Tokens)

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

import { MoveVector, Hex } from "@aptos-labs/ts-sdk"
import { ethers } from "ethers"

// ABI-encode the data for the receiver contract
const encodedData = ethers.AbiCoder.defaultAbiCoder().encode(["string"], ["Tokens attached"])
const dataBytes = Hex.hexInputToUint8Array(encodedData)

const transactionPayload = {
  function: `${ccipRouterModuleAddr}::router::ccip_send`,
  functionArguments: [
    destinationChainSelector,
    MoveVector.U8(evmAddressToAptos(receiverContractAddress)),
    MoveVector.U8(dataBytes), // The encoded data payload
    [ccipBnmTokenAddress], // Vector of token addresses
    MoveVector.U64([10000000n]), // Vector of token amounts
    [tokenStoreAddress], // Vector of token store addresses
    feeTokenAddress,
    feeTokenStore,
    encodeGenericExtraArgsV2(200000n, true), // extraArgs with appropriate gasLimit
  ],
}

Tracking Messages with Transaction Events

After a successful ccip_send transaction, the CCIP Router module emits an event containing the unique identifier for the cross-chain message. On Aptos, this event is emitted by the On-Ramp module (e.g., CCIPMessageSent) and can be found in the Events tab of the executed transaction on the Aptos Explorer.

// Example of a CCIPMessageSent event from an Aptos transaction receipt
{
  "Type": "0xe9dbf...ac0dd3::onramp::CCIPMessageSent",
  "Data": {
    "dest_chain_selector": "16015286601757825753",
    "message": {
      "header": {
        "message_id": "0x06859...2afefea",
        "nonce": "0",
        "sequence_number": "14",
        "source_chain_selector": "743186221051783445"
      },
      "receiver": "0x00000...bb14ca",
      "sender": "0xd0e22...d2fad4",
      "token_amounts": [
        {
          "amount": "10000000",
          "dest_token_address": "0x00000...fe82a05"
        }
      ],
      "fee_token": "0x8c208...fa3542",
      "data": "0x",
      "extra_args": "0x181dc...00000..."
    },
    "sequence_number": "14"
  }
}

The message_id is the critical piece of information that links the source Aptos transaction to the destination EVM transaction.

Further Resources

Get the latest Chainlink content straight to your inbox.