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.
- For token-only transfers: This must be an empty
- 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 totrue
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
- CCIP Router API Reference: Complete technical details about the router's functions, parameters, and view functions like
get_fee
. - CCIP Messages API Reference: Comprehensive documentation of all CCIP message and event structures for Aptos.
- Aptos TS-SDK Docs: For more information on building transactions and interacting with the Aptos blockchain, refer to the official Aptos TS-SDK docs.