Implementing CCIP Receivers

Implementing CCIP Receivers

A CCIP Receiver is a TON smart contract (written in Tolk) that accepts incoming cross-chain messages delivered by the CCIP protocol. When a message is sent from an EVM chain to TON via CCIP, the CCIP Router on TON forwards it to your receiver contract as an internal message. Your contract must validate the delivery, acknowledge it to the protocol, and process the payload.

How Message Delivery Works

When a cross-chain message arrives on TON:

  1. The CCIP off-ramp verifies the message against a Merkle root and routes it through the CCIP Router on TON.
  2. The Router sends a Receiver_CCIPReceive internal message to your receiver contract, with enough TON attached to cover the confirmation transaction.
  3. Your contract performs three mandatory protocol steps, then executes your application logic.
  4. Your contract sends Router_CCIPReceiveConfirm back to the Router, which marks the message as successfully delivered on-chain.

Security Architecture

Three Mandatory Protocol Steps

Every TON CCIP receiver must implement all three steps in order:

Step 1 — Authorize the Router. Accept Receiver_CCIPReceive messages only from the configured CCIP Router address. Any other sender must be rejected.

assert(in.senderAddress == st.router) throw ERROR_UNAUTHORIZED;

Step 2 — Check attached value. The Router forwards TON with the message to cover the confirmation transaction. Verify the attached value meets MIN_VALUE. The Router needs at least 0.02 TON to send Router_CCIPReceiveConfirm back through the protocol chain. Use 0.03 TON as a baseline and increase it to cover your own execution costs.

assert(in.valueCoins >= MIN_VALUE) throw ERROR_LOW_VALUE;

Step 3 — Acknowledge delivery. Send Router_CCIPReceiveConfirm back to the Router using SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE to forward all remaining TON for the protocol's confirmation chain.

val receiveConfirm = createMessage({
    bounce: true,
    value: 0,
    dest: in.senderAddress,
    body: Router_CCIPReceiveConfirm { execId: msg.execId },
});
receiveConfirm.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);

Developer Responsibilities

Unlike EVM receivers, source chain and sender validation are not enforced at the protocol level on TON — only the Router address check (step 1) is a protocol requirement. Your contract is responsible for any application-layer checks:

  • Source chain validation: Check message.sourceChainSelector against an allowlist of trusted chains.
  • Sender validation: Check message.sender against trusted source-side addresses.

Without these checks, any address on any chain can send a CCIP message to your receiver and have it processed.

Message Structure

The CCIP Router delivers an Any2TVMMessage struct inside each Receiver_CCIPReceive message:

chainlink-ton/contracts/contracts/lib/receiver/types.tolk
struct Any2TVMMessage {
    messageId: uint256;          // Unique message identifier
    sourceChainSelector: uint64; // CCIP chain selector of the originating chain
    sender: CrossChainAddress;   // Encoded sender address from the source chain
    data: cell;                  // Arbitrary payload (your application data)
    tokenAmounts: cell?;         // Reserved for future token support; currently unused
}
FieldTypeDescription
messageIduint256Unique identifier — use for deduplication to prevent replay
sourceChainSelectoruint64CCIP selector of the originating chain
senderCrossChainAddressEncoded source-chain sender address; for EVM sources, these are the 20 EVM address bytes
datacellApplication payload encoded as a TON Cell
tokenAmountscell?Reserved for future token-transfer support; currently null

CrossChainAddress is a slice type. For EVM-to-TON messages, it contains the 20-byte EVM address of the sender.

Receiver Implementations

The starter kit provides three receiver contracts at different complexity levels. Choose the one that fits your use case, or use one as a starting template.

contracts/minimal_receiver.tolk — The bare-minimum CCIP receiver. All three mandatory protocol steps are implemented inline, giving you full transparency and control over each check.

When to use: When you want explicit control over every protocol step, need to customize error handling, or prefer not to depend on the early-stage Receiver library.

Entry Point

contracts/minimal_receiver.tolk
fun onInternalMessage(in: InMessage) {
    val msg = lazy MinimalReceiver_InMessage.fromSlice(in.body);
    match (msg) {
        Receiver_CCIPReceive => {
            val st = lazy Storage.load();

            // 1. Accept messages only from the authorized CCIP Router.
            assert(in.senderAddress == st.router) throw ERROR_UNAUTHORIZED;

            // 2. Verify enough value is attached to cover gas costs.
            //    Router needs ≥0.02 TON for CCIPReceiveConfirm; increase for your own costs.
            assert(in.valueCoins >= MIN_VALUE) throw ERROR_LOW_VALUE;

            // 3. Send CCIPReceiveConfirm back to the Router.
            val receiveConfirm = createMessage({
                bounce: true,
                value: 0,
                dest: in.senderAddress,
                body: Router_CCIPReceiveConfirm { execId: msg.execId },
            });
            receiveConfirm.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);

            // ★ YOUR LOGIC: call your application function(s) with msg.message
            processMessage(msg.message);
        }
        else => {
            // Ignore plain TON transfers; reject unknown opcodes.
            assert(in.body.isEmpty()) throw 0xFFFF;
        }
    }
}
minimal_receiver.tolk

Complete contract source including storage, constants, and imports.

Deploy:

Terminal
npm run deploy:ton:receiver:minimal

After Deployment

After deploying, send a test message from an EVM chain to verify delivery:

Terminal
npm run evm2ton:send -- \
  --sourceChain <evm-chain-name> \
  --tonReceiver <your-deployed-receiver-address> \
  --msg "Hello TON from EVM" \
  --feeToken native

Then confirm the message was received on TON:

Terminal
npm run utils:checkTON -- \
  --sourceChain <evm-chain-name> \
  --tonReceiver <your-deployed-receiver-address> \
  --msg "Hello TON from EVM"

Get the latest Chainlink content straight to your inbox.