Building CCIP Messages from EVM to TON
Introduction
This guide explains how to construct CCIP messages from Ethereum Virtual Machine (EVM) chains (e.g., Ethereum Sepolia, Arbitrum Sepolia) to the TON blockchain. It covers the message structure, required parameters, and implementation details for sending arbitrary data payloads to a Tolk smart contract on TON.
CCIP Message Structure
CCIP messages from EVM 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 encoded TON address of the smart contract on TON that will receive and process the CCIP message.
- Encoding: TON addresses must be encoded into a 36-byte format: 4 bytes for the workchain identifier (big-endian signed int32) followed by 32 bytes for the account hash. Use the
encodeTONAddresshelper shown below.
data
- Definition: The raw bytes payload delivered to the
ccipReceiveentry point on the TON destination contract. - For arbitrary messaging: Contains the custom data payload your receiver will process.
- Encoding: Pass raw bytes directly — do not use
hexlify. Useethers.toUtf8Bytes()for string payloads.
tokenAmounts
- Definition: An array of token addresses and amounts to transfer.
- Current support: EVM-to-TON currently supports arbitrary messaging only. Set
tokenAmountsto an empty array ([]).
feeToken
- Definition: Specifies which token to use for paying CCIP fees.
- Native gas token: Use
ethers.ZeroAddress(address(0)) to pay with the source chain's native token (e.g., ETH on Ethereum Sepolia). - LINK: Specify the LINK token address on your source chain. See the CCIP Directory for token addresses.
extraArgs
For TON-bound messages, the extraArgs parameter is a byte string composed of the 4-byte GenericExtraArgsV2 tag (0x181dcf10) prepended to the ABI-encoded values (uint256 gasLimit, bool allowOutOfOrderExecution). This format matches the GenericExtraArgsV2 specification.
gasLimit
- Definition: The amount of nanoTON reserved for execution on the TON destination chain.
- Units: This value is denominated in nanoTON, not EVM gas units. 1 TON = 1,000,000,000 nanoTON. A starting value of
100_000_000n(0.1 TON) covers most receive operations. Any unused nanoTON is returned to the contract. - Usage: Increase this value if your receiver performs heavy computation. Determine the right amount through testing.
allowOutOfOrderExecution
- Definition: A boolean required by the TON lane.
- Usage: Must be set to
truewhen TON is the destination chain.
Implementation by Message Type
Arbitrary Messaging
Use this configuration when sending a data payload to a custom smart contract on TON.
{
destinationChainSelector: TON_TESTNET_CHAIN_SELECTOR,
receiver: encodedTONAddress, // 36-byte encoded TON contract address
tokenAmounts: [], // Empty — token transfers not yet supported
feeToken: feeTokenAddress, // address(0) for native, LINK address for LINK
data: rawBytesPayload, // ethers.toUtf8Bytes(...) — do NOT hexlify
extraArgs: {
gasLimit: 100_000_000n, // 0.1 TON in nanoTON
allowOutOfOrderExecution: true
}
}
import { Address } from "@ton/core"
import { ethers } from "ethers"
const TON_TESTNET_CHAIN_SELECTOR = 1399300952838017768n
const tonContractAddr = "EQB9QIw22sgwNKMfqsMKGepkhnjXYJmXlzCgcBSAlaiF9VCj"
const receiver = encodeTONAddress(Address.parse(tonContractAddr))
const data = ethers.toUtf8Bytes("Hello TON!")
const message = {
receiver,
data,
tokenAmounts: [],
feeToken: ethers.ZeroAddress, // pay fee in native ETH
extraArgs: buildExtraArgsForTON(100_000_000n, true)
}
const fee = await router.getFee(TON_TESTNET_CHAIN_SELECTOR, message)
const feeWithBuffer = (fee * 110n) / 100n // add 10% buffer
const tx = await router.ccipSend(TON_TESTNET_CHAIN_SELECTOR, message, {
value: feeWithBuffer
})
import { Address } from "@ton/core"
import { ethers } from "ethers"
const TON_TESTNET_CHAIN_SELECTOR = 1399300952838017768n
const LINK_ADDRESS = "0x779877A7B0D9E8603169DdbD7836e478b4624789" // LINK on Sepolia
const tonContractAddr = "EQB9QIw22sgwNKMfqsMKGepkhnjXYJmXlzCgcBSAlaiF9VCj"
const receiver = encodeTONAddress(Address.parse(tonContractAddr))
const data = ethers.toUtf8Bytes("Hello TON!")
const message = {
receiver,
data,
tokenAmounts: [],
feeToken: LINK_ADDRESS, // pay fee in LINK
extraArgs: buildExtraArgsForTON(100_000_000n, true)
}
const fee = await router.getFee(TON_TESTNET_CHAIN_SELECTOR, message)
const feeWithBuffer = (fee * 110n) / 100n // add 10% buffer
await linkToken.approve(await router.getAddress(), feeWithBuffer)
const tx = await router.ccipSend(TON_TESTNET_CHAIN_SELECTOR, message)
Related Tutorials
To see these concepts in action with a step-by-step implementation guide, check out the following tutorial:
- Arbitrary Messaging: EVM to TON — Learn how to send data messages from an EVM chain to a TON smart contract.