Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Overview

This book showcases practical usage of CCIP v2.0 in this Hardhat 3 starter kit.

The goal is not to re-teach CCIP fundamentals. It is to provide runnable example flows you can execute and adapt.

Before You Run Any Example

**Important: Complete the 3 steps below before proceeding.

1. Keystore — Use a local keystore account for task and script execution:

npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_RPC_URL
npx hardhat keystore set AMOY_RPC_URL

The values from your local keystore are used at runtime (e.g. by hardhat.config.ts for network accounts and RPC URLs).

2. Ignition parameters — Update the JSON parameter files used by Hardhat Ignition for deployment:

  • Ethereum Sepolia: ignition/paramsEthSepolia.json
  • Polygon Amoy: ignition/paramsAmoy.json

These files supply $global values (such as routerAddress, armProxy, registryModuleOwnerCustom, tokenAdminRegistry, and for some modules thresholdAmountForAdditionalCCVs) to the Ignition modules.

Each module that deploys pools or receivers reads these parameters when you run npx hardhat ignition deploy ... --parameters <file>.

Ensure the addresses in each file match the correct chain (e.g. use the CCIP Directory or your deployment context). If you use the wrong file or outdated addresses, deployments will fail or point to the wrong contracts.

3. Compile — Generate the build artifacts required for deployment and tasks:

npx hardhat compile

Run this before deploying or running examples so that contract ABIs and artifacts are available.

Chain Configuration

Always pull the latest values from the CCIP Directory before running examples.

Examples 01–10 use Sepolia → Amoy. Examples 01, 03, and 04 use faucet-issued CCIP-BnM; Examples 06–10 deploy custom tokens. Verify the lane’s OnRamp 2.0.0 before sending.

Example 01: Token Transfer + Fast Transfers (FTF)

This example sends one token transfer message using ExtraArgsV3 with Fast Transfers (blockConfirmations > 0).

Task: example01 (implementation: tasks/Example01.ts).

What You Will Do

  1. Mint 1 CCIP-BnM token to your EOA with the faucet task.
  2. (Optional) Check the executor allowed finality config with the executor-allowed-finality-config task.
  3. Send that token from source chain to destination chain with the example01 task.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_RPC_URL
npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set AMOY_RPC_URL

Step 1: Get 1 CCIP-BnM Token

Use the faucet task on the source chain. Pass the CCIP-BnM token address for that chain:

npx hardhat faucet --network <NETWORK_NAME> --ccip-bnm <CCIP_BNM_TOKEN_ADDRESS>

Step 2: (Optional) Check Executor Allowed Finality

Before picking a block depth for Fast Transfers, you can read the executor’s allowed finality (FinalityCodec bytes4) with:

npx hardhat executor-allowed-finality-config --network <NETWORK_NAME> --executor <EXECUTOR_ADDRESS>

The task prints: Allowed finality config (bytes4) 0x….

Why this matters:

  • Your requested finality in ExtraArgsV3 must be allowed by the executor and destination policy (FinalityCodec); mismatches can revert.
  • If your requested mode is stricter than needed, behavior follows CCIP / pool rules (see chain-specific docs).

Check the source pool, executor, and CCV finality settings before sending:

npx hardhat example06-check-finality-gates --network <NETWORK_NAME> \
  --source-router <SOURCE_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --token-to-send <CCIP_BNM_SOURCE_TOKEN_ADDRESS> \
  --block-confirmations <BLOCK_CONFIRMATIONS_GT_ZERO>

A quote alone does not prove ccipSend will succeed with a non-IPoolV2 pool.

Step 3: Send Token With Fast Transfers

Run the example01 task with the required and optional arguments:

npx hardhat example01 --network <NETWORK_NAME> \
  --source-router <SOURCE_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <RECEIVER_ON_DESTINATION_CHAIN> \
  --token-to-send <CCIP_BNM_SOURCE_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit <GAS_LIMIT> \
  --block-confirmations <BLOCK_CONFIRMATIONS_GT_ZERO> \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Parameter notes:

  • --amount: token amount in wei (e.g. 1000000000000000000 for 1 token with 18 decimals).
  • --gas-limit: set to 0 for token-only transfer to an EOA receiver; use e.g. 200000 for contract receivers.
  • --block-confirmations: must be > 0 and meet the source pool and executor minimums from Step 2.
  • --fee-token-address: LINK token address on the source chain to pay CCIP fees in LINK, or 0x0000000000000000000000000000000000000000 to pay in native coin.

Defaults (can be omitted): --gas-limit 200000, --block-confirmations 1, --fee-token-address 0x0000000000000000000000000000000000000000.

Verify Result

The example01 task logs the message ID and a link to monitor:

  • Monitor: https://ccip.chain.link

Use the message ID in the CCIP Explorer to track status.

Example 02: Hello World to BasicMessageReceiverWithCCVs with Fast Transfers (FTF)

This example deploys a destination BasicMessageReceiverWithCCVs contract and sends a Hello World data message from an EOA using Fast Transfers (blockConfirmations > 0).

Tasks and modules used:

  • Deploy receiver: Hardhat Ignition module ignition/modules/BasicMessageReceiverWithCCVs.ts (deploy on destination chain).
  • Configure min block depth: Hardhat task set-basic-message-receiver-with-ccvs-min-block-depth (implementation: tasks/helpers/SetBasicMessageReceiverWithCCVsMinBlockDepth.ts).
  • Send message: Hardhat task example02 (implementation: tasks/Example02.ts).
  • Read latest message: Hardhat task basic-message-receiver-latest-message (implementation: tasks/helpers/BasicMessageReceiverLatestMessage.ts).

What You Will Do

  1. Deploy BasicMessageReceiverWithCCVs on the destination chain using Ignition.
  2. Configure minimum block depth for the source chain.
  3. Send a Hello World CCIP data message from the source chain to BasicMessageReceiverWithCCVs using the example02 task.
  4. (Optional) Verify the received message with the basic-message-receiver-latest-message task.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

Receiver Contract Context

Pre-v2.0

To receive CCIP messages (data or data+tokens), contracts implemented IAny2EVMMessageReceiver:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Client} from "../libraries/Client.sol";

interface IAny2EVMMessageReceiver {
  function ccipReceive(
    Client.Any2EVMMessage calldata message
  ) external;
}

As a convenience, applications generally inherited CCIPReceiver.sol and implemented _ccipReceive.

CCIP v2.0

In v2.0, receivers expose Cross Chain Verifiers and finality requirements via getCCVsAndFinalityConfig (encoded with FinalityCodec as bytes4 allowedFinalityConfig):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Client} from "../libraries/Client.sol";

interface IAny2EVMMessageReceiverV2 {
  function ccipReceive(
    Client.Any2EVMMessage calldata message
  ) external;

  function getCCVsAndFinalityConfig(
    uint64 sourceChainSelector,
    bytes calldata sender
  )
    external
    view
    returns (
      address[] memory requiredCCVs,
      address[] memory optionalCCVs,
      uint8 optionalThreshold,
      bytes4 allowedFinalityConfig
    );
}

The receiver controls two independent dimensions:

  • requiredCCVs and optionalCCVs define additional verifiers required for acceptance.
  • allowedFinalityConfig defines which requested finality modes are accepted (see FinalityCodec in chainlink-ccip). A zero value (WAIT_FOR_FINALITY_FLAG) means only fully finalized messages are accepted.

You can combine these independently (for example, add verifiers while requiring full finality, or allow Fast Transfers when policy allows).

In this starter kit, contracts/BasicMessageReceiverWithCCVs.sol adds configurable verifier sets and stores a per-chain minimum block depth, which it exposes via FinalityCodec._encodeBlockDepth in getCCVsAndFinalityConfig.

Step 1: Deploy BasicMessageReceiverWithCCVs on Destination Chain

Deploy the receiver on the destination chain using Hardhat Ignition.

npx hardhat ignition deploy ignition/modules/BasicMessageReceiverWithCCVs.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

--parameters must be the path to the chain’s Ignition parameters JSON (e.g. ignition/paramsEthSepolia.json or ignition/paramsAmoy.json). That file supplies router and other addresses to the module.

Save the deployed address as <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS>.

By default, this receiver starts with minBlockDepth = 0 (default finality only) for all source chains until you configure it.

Step 2: Configure Minimum Block Depth for Your Source Chain

On the destination chain, run:

npx hardhat set-basic-message-receiver-with-ccvs-min-block-depth --network <NETWORK_NAME> \
  --receiver <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS> \
  --source-chain-selector <SOURCE_CHAIN_SELECTOR> \
  --min-block-depth <MIN_BLOCK_DEPTH>

For this chapter, use <MIN_BLOCK_DEPTH> = 1.

  • 0 means default finality-only behavior for that source chain.
  • > 0 enables Fast Transfers with that minimum depth.

Step 3: Send Hello World Data Message from EOA

From the source chain, run the example02 task. Pass the deployed receiver address as --receiver and your message as --message-text.

npx hardhat example02 --network <NETWORK_NAME> \
  --source-router <SOURCE_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS> \
  --message-text "Hello, World" \
  --gas-limit <GAS_LIMIT> \
  --block-confirmations <BLOCK_CONFIRMATIONS_GT_ZERO> \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Parameter notes:

  • --gas-limit must be > 0 because the destination receiver callback needs gas (e.g. 200000).
  • --block-confirmations must be > 0 for Fast Transfers.
  • --block-confirmations should be greater than or equal to <MIN_BLOCK_DEPTH>.
  • Executor may enforce a minimum block confirmations value and revert if too low.
  • If requested confirmations exceed chain finality, default finality is used.
  • --fee-token-address: LINK token address on the source chain to pay fees in LINK, or 0x0000000000000000000000000000000000000000 to pay in native coin.

Defaults (can be omitted): --gas-limit 200000, --block-confirmations 1, --fee-token-address 0x0000000000000000000000000000000000000000.

Verify Result

The example02 task logs the CCIP message ID and a link to monitor.

Monitor message status: https://ccip.chain.link

Read the latest message on the receiver (destination chain). The task decodes the stored bytes as the string that was sent:

npx hardhat basic-message-receiver-latest-message --network <NETWORK_NAME> --basic-message-receiver <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS>

Output is the decoded string (e.g. Hello, World). If no message has been received yet, the task prints (empty).

Example 03: Programmable Token Transfer with Fast Transfers (FTF)

This example sends a programmable token transfer from an EOA on Ethereum Sepolia to a receiver that supports Fast Transfers on Polygon Amoy:

  • Data payload: "Hello, World"
  • Tokens: CCIP-BnM

Task: example03 (implementation: tasks/Example03.ts).

What You Will Do

  1. Ensure a destination BasicMessageReceiverWithCCVs exists on Amoy.
  2. Mint 1 CCIP-BnM on Sepolia using the faucet task.
  3. Send one CCIP message containing both data and tokens using the example03 task.
  4. (Optional) Verify receiver state with the basic-message-receiver-latest-* tasks.

Receiver Compatibility Note

This chapter uses Fast Transfers (blockConfirmations > 0), so the destination receiver should return a non-zero minimum block depth for your source chain.

  • If your receiver is default-finality-only, this message can fail on destination.
  • Deploy BasicMessageReceiverWithCCVs before running this chapter and configure setMinBlockDepth for your source chain.
  • For default-finality programmable token transfer (blockConfirmations = 0), use Example 04.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

If you do not have a receiver deployed yet

Deploy BasicMessageReceiverWithCCVs on Amoy with Ignition (see Example 02):

npx hardhat ignition deploy ignition/modules/BasicMessageReceiverWithCCVs.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

Then configure min block depth for your source chain:

npx hardhat set-basic-message-receiver-with-ccvs-min-block-depth --network <NETWORK_NAME> \
  --receiver <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS> \
  --source-chain-selector <SOURCE_CHAIN_SELECTOR> \
  --min-block-depth <MIN_BLOCK_DEPTH>

For this Fast Transfers example, use <MIN_BLOCK_DEPTH> > 0 and check the source pool minimum too.

The user-facing input for this example is --min-block-depth <MIN_BLOCK_DEPTH> (a uint16 passed to BasicMessageReceiverWithCCVs.setMinBlockDepth). On-chain, the receiver does not return that integer directly to CCIP: getCCVsAndFinalityConfig sets allowedFinalityConfig to FinalityCodec._encodeBlockDepth(minBlockDepth) — the same bytes4 finality encoding CCIP 2.0 uses elsewhere for allowed finality (depth 0 means wait for full/default finality).

--parameters is the path to the chain’s Ignition parameters JSON (e.g. ignition/paramsAmoy.json); it supplies router and other addresses to the module.

Save the deployed receiver address and use it as --receiver in Step 2.

Step 1: Get 1 CCIP-BnM Token on Sepolia

Use the token and finality check from Example 01:

npx hardhat faucet --network <NETWORK_NAME> --ccip-bnm <CCIP_BNM_SOURCE_TOKEN_ADDRESS>

Step 2: Send Hello World + CCIP-BnM

From Sepolia, run example03 with the Amoy receiver and Sepolia CCIP-BnM token.

npx hardhat example03 --network <NETWORK_NAME> \
  --source-router <SOURCE_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS> \
  --message-text "Hello, World" \
  --token-to-send <CCIP_BNM_SOURCE_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit <GAS_LIMIT> \
  --block-confirmations <BLOCK_CONFIRMATIONS_GT_ZERO> \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Parameter notes:

  • --amount: token amount in wei (e.g. 1000000000000000000 for 1 token with 18 decimals).
  • --gas-limit must be > 0 because the receiver contract callback handles data (e.g. 200000).
  • --block-confirmations must meet the source pool and receiver minimums.
  • Executor may enforce a minimum block confirmation value and revert if too low.
  • If requested confirmations exceed chain finality, default finality is used.
  • --fee-token-address: LINK token address on the source chain to pay fees in LINK, or 0x0000000000000000000000000000000000000000 to pay in native coin.

Defaults (can be omitted): --gas-limit 200000, --block-confirmations 1, --fee-token-address 0x0000000000000000000000000000000000000000.

Verify Result

The example03 task logs the CCIP message ID and a link to monitor.

Monitor message status: https://ccip.chain.link

Optional: read receiver state on Amoy using the Hardhat tasks (receiver address = <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS>):

# Decoded latest message (string)
npx hardhat basic-message-receiver-latest-message --network <NETWORK_NAME> --basic-message-receiver <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS>

# Latest sender address
npx hardhat basic-message-receiver-latest-sender --network <NETWORK_NAME> --basic-message-receiver <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS>

# Latest source chain selector (uint64)
npx hardhat basic-message-receiver-latest-source-chain-selector --network <NETWORK_NAME> --basic-message-receiver <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS>

Example 04: Programmable Token Transfer (Default Finality + Sender Contract)

This example sends a programmable token transfer from Ethereum Sepolia to Polygon Amoy using a deployed BasicMessageSender contract:

  • Data payload: "Hello, World"
  • Tokens: CCIP-BnM
  • Finality mode: default finality (blockConfirmations = 0)

Because this chapter uses default finality, the baseline BasicMessageReceiver is sufficient on destination. If you want the Fast Transfers (FTF) variant (blockConfirmations > 0), use Example 03 with BasicMessageReceiverWithCCVs.

Tasks and modules used:

  • Deploy sender: Hardhat Ignition module ignition/modules/BasicMessageSender.ts (deploy on source chain; requires routerAddress and linkAddress in params).
  • Deploy receiver (if needed): Hardhat Ignition module ignition/modules/BasicMessageReceiver.ts (this example; default finality).
  • Mint token: Hardhat task faucet.
  • Send message: Hardhat task example04 (implementation: tasks/Example04.ts).
  • Verify receiver: Hardhat tasks basic-message-receiver-latest-message, basic-message-receiver-latest-sender, basic-message-receiver-latest-source-chain-selector.

What You Will Do

  1. Deploy BasicMessageSender on the source chain (Sepolia) using Ignition.
  2. Ensure destination BasicMessageReceiver exists on Amoy (deploy via Ignition if needed; see below).
  3. Mint 1 CCIP-BnM on Sepolia using the faucet task.
  4. Run the example04 task: it quotes the fee, funds the sender contract with that amount (when paying in native), then sends data + token.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

If you do not have a receiver deployed yet

Deploy BasicMessageReceiver on Amoy with Ignition:

npx hardhat ignition deploy ignition/modules/BasicMessageReceiver.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

--parameters is the path to the chain’s Ignition parameters JSON; it supplies router and other addresses to the module.

Save the deployed receiver address for use as --receiver in Step 4.

Step 1: Deploy BasicMessageSender on Sepolia

Deploy the sender on the source chain using Hardhat Ignition. The params file must include routerAddress and linkAddress for the BasicMessageSender module (see ignition/modules/BasicMessageSender.ts).

npx hardhat ignition deploy ignition/modules/BasicMessageSender.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

--parameters is the path to the chain’s Ignition parameters JSON (e.g. ignition/paramsEthSepolia.json); it supplies router, LINK address, and other values to the module.

Save the deployed sender address from the deployment output for use as --basic-message-sender in Step 4.

Step 2: Get 1 CCIP-BnM Token on Sepolia

Use the registered Sepolia token from Example 01:

npx hardhat faucet --network <NETWORK_NAME> --ccip-bnm <CCIP_BNM_SOURCE_TOKEN_ADDRESS>

Step 3: Fund the Sender Contract (if paying in native)

The example04 task funds the sender contract with the quoted fee when --fee-token-address is the zero address. Ensure your EOA has enough native token on Sepolia to cover that fee (the task transfers it to the sender contract before calling send).

Step 4: Send Hello World + CCIP-BnM Through Sender Contract

Run the example04 task from the source chain (Sepolia). Use default finality by setting --block-confirmations 0. Pass the deployed sender as --basic-message-sender and the receiver as --receiver.

npx hardhat example04 --network <NETWORK_NAME> \
  --basic-message-sender <DEPLOYED_BASIC_MESSAGE_SENDER_ADDRESS> \
  --source-router <SOURCE_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <DEPLOYED_BASIC_MESSAGE_RECEIVER_ADDRESS> \
  --message-text "Hello, World" \
  --token-to-send <CCIP_BNM_SOURCE_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit <GAS_LIMIT> \
  --block-confirmations 0 \
  --fee-token-address 0x0000000000000000000000000000000000000000

Parameter notes:

  • This example uses default finality by setting --block-confirmations 0.
  • --amount: token amount in wei (e.g. 1000000000000000000 for 1 token with 18 decimals).
  • --gas-limit must be > 0 because the receiver contract callback handles data (e.g. 200000).
  • The task quotes the fee, then (when paying in native) funds the sender contract with that exact amount before calling send.
  • If fee conditions change between quote and send, re-run the task to re-quote and retry.
  • --fee-token-address 0x0000000000000000000000000000000000000000 pays the CCIP fee in native token; the task transfers the quoted fee from your EOA to the sender contract.

Verify Result

The example04 task logs the CCIP message ID and a link to monitor.

Monitor message status: https://ccip.chain.link

Optional: read receiver state on Amoy using the Hardhat tasks:

npx hardhat basic-message-receiver-latest-message --network <NETWORK_NAME> --basic-message-receiver <DEPLOYED_BASIC_MESSAGE_RECEIVER_ADDRESS>
npx hardhat basic-message-receiver-latest-sender --network <NETWORK_NAME> --basic-message-receiver <DEPLOYED_BASIC_MESSAGE_RECEIVER_ADDRESS>
npx hardhat basic-message-receiver-latest-source-chain-selector --network <NETWORK_NAME> --basic-message-receiver <DEPLOYED_BASIC_MESSAGE_RECEIVER_ADDRESS>

Example 05: ExtraArgsV3 No-Execution-Tag (Manual Execution Path)

This example sends a Hello World data message from an EOA, but disables automatic execution by setting executor to NO_EXECUTION_ADDRESS inside ExtraArgsV3.

Task: example05 (implementation: tasks/Example05.ts).

What You Will Do

  1. Ensure a compatible destination receiver exists (default finality or Fast Transfers (FTF) based on your blockConfirmations).
  2. Send a CCIP data message with ExtraArgsV3 no-execution-tag using the example05 task.
  3. Observe pending execution in CCIP Explorer and execute manually.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

If you do not have a receiver deployed yet

Choose deployment based on your blockConfirmations:

npx hardhat ignition deploy ignition/modules/BasicMessageReceiver.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

Use this for default finality (blockConfirmations = 0). Save this as <BASIC_MESSAGE_RECEIVER_ADDRESS>.

npx hardhat ignition deploy ignition/modules/BasicMessageReceiverWithCCVs.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

If you use Fast Transfers (blockConfirmations > 0), configure min block depth for your source chain:

npx hardhat set-basic-message-receiver-with-ccvs-min-block-depth --network <NETWORK_NAME> \
  --receiver <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS> \
  --source-chain-selector <SOURCE_CHAIN_SELECTOR> \
  --min-block-depth <MIN_BLOCK_DEPTH>

Use <MIN_BLOCK_DEPTH> > 0 for Fast Transfers.

Use this for Fast Transfers (blockConfirmations > 0). Save this as <BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS>.

--parameters is the path to the chain’s Ignition parameters JSON; it supplies router and other addresses to the module.

Understanding ExtraArgsV3

The ExtraArgsV3 is a structured set of delivery/execution options encoded and attached to a CCIP message.

struct GenericExtraArgsV3 {
    uint32 gasLimit;
    bytes4 requestedFinalityConfig;
    address[] ccvs;
    bytes[] ccvArgs;
    address executor;
    bytes executorArgs;
    bytes tokenReceiver;
    bytes tokenArgs;
}

(On-chain this struct lives in ExtraArgsCodec from @chainlink/contracts-ccip.)

  • gasLimit: gas allocated for callback execution on destination. If 0 and message data is empty, no callback executes.
  • requestedFinalityConfig: bytes4 finality mode + parameters per FinalityCodec (not a bare uint16). All-zero means wait for default/lane finality. For block-depth style Fast Transfers, encode with FinalityCodec._encodeBlockDepth(uint16) (see CCIP / FinalityCodec NatSpec). Example05 and EncodeExtraArgsOffchain.encodeV3 / encodeV3Basic take a blockConfirmations argument only as a convenience and set this field to FinalityCodec._encodeBlockDepth(blockConfirmations).
  • ccvs: list of cross-chain verifier addresses. Empty means default verifiers.
  • ccvArgs: optional arguments for each CCV. Must match ccvs length.
  • executor: executor address on source chain. address(0) uses default executor.
  • executorArgs: chain/executor-specific args (format depends on chain family/executor implementation).
  • tokenReceiver: encoded destination token receiver. If empty, receiver address is used.
  • tokenArgs: extra token transfer args (pool-specific format).

Related helpers in scripts/CallEncodeExtraArgsOffchain.ts:

  • encodeV3Basic(gasLimit, blockConfirmations) for a minimal V3 payload.
  • getNoExecutionAddress() returns Client.NO_EXECUTION_ADDRESS for the manual execution path.

The example05 task uses these to build ExtraArgsV3 with the no-execution tag.

Step 1: Send Message With No-Execution-Tag

Run the example05 task. It sends a data-only message with executor set to NO_EXECUTION_ADDRESS, so execution stays pending until triggered manually.

npx hardhat example05 --network <NETWORK_NAME> \
  --source-router <SOURCE_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <BASIC_MESSAGE_RECEIVER_ADDRESS_OR_BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS> \
  --message-text "Hello, World" \
  --gas-limit <GAS_LIMIT> \
  --block-confirmations <BLOCK_CONFIRMATIONS> \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Parameter notes:

  • --gas-limit must be > 0 because the receiver callback needs gas (e.g. 200000).
  • --block-confirmations can be 0 (default finality) or > 0.
  • If <BLOCK_CONFIRMATIONS> > 0, destination receiver must accept Fast Transfers (minBlockDepth > 0) for this source chain.
  • If <BLOCK_CONFIRMATIONS> = 0, a default-finality receiver is sufficient.
  • --fee-token-address: LINK token address on the source chain to pay fees in LINK, or 0x0000000000000000000000000000000000000000 to pay in native coin.
  • This example sets executor to NO_EXECUTION_ADDRESS, so execution is not automatic; the message remains in a pending/manual execution state.

Defaults (can be omitted): --gas-limit 200000, --block-confirmations 1, --fee-token-address 0x0000000000000000000000000000000000000000.

Verify Result

The example05 task logs the CCIP message ID and a link to monitor.

Monitor and execute manually: https://ccip.chain.link

Expected behavior:

  • Message is delivered to a pending/manual execution state because automatic execution is disabled.
  • Use the CCIP Explorer to view the message and trigger execution manually when supported.

Example 06: CCT Burn and Mint

This example covers the full BurnMint CCT flow on Sepolia → Amoy:

  1. Deploy CrossChainToken + BurnMint pool on both chains (Hardhat Ignition).
  2. Configure pools to trust each other (example06-step1).
  3. Enable Fast Transfers (FTF) on both pools and check the source lane gates.
  4. Send at default finality or Fast Transfers (example06-step2).
  5. Verify BurnMint behavior (burn on source, mint on destination) with helper tasks.

Modules and tasks used:

  • Deploy token + pool: Hardhat Ignition module ignition/modules/BurnMintTokenPool.ts (deploy on each chain with the appropriate parameters file).
  • Configure remote lane: Hardhat task example06-step1.
  • Set pool FTF policy: Hardhat task example06-set-pool-finality.
  • Inspect FTF gates: Hardhat task example06-check-finality-gates.
  • Send CCIP token transfer: Hardhat task example06-step2.
  • Verify: Helper tasks token-admin-registry-get-pool, pool-get-remote-token, pool-get-remote-pools, erc20-balance-of, erc20-total-supply.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_RPC_URL
npx hardhat keystore set AMOY_RPC_URL

Ensure ignition/paramsAmoy.json and ignition/paramsEthSepolia.json contain the correct routerAddress, armProxy, registryModuleOwnerCustom, and tokenAdminRegistry for each chain.

Deployment Defaults

The BurnMintTokenPool Ignition module deploys:

  • Token: CrossChainToken via BaseERC20.ConstructorParams: TestToken (TEST), 18 decimals, 1_000_000 × 10¹⁸ premint, 100_000_000 × 10¹⁸ max supply. The deployer EOA is pre-mint recipient, CCIP admin, burn/mint admin, and default admin. Registration uses RegistryModuleOwnerCustom.registerAdminViaGetCCIPAdmin.
  • Pool: BurnMintTokenPool with no advanced pool hook (CCT 01).

Step 1: Deploy Token + Pool on Sepolia (source)

npx hardhat ignition deploy ignition/modules/BurnMintTokenPool.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

--parameters is the path to the chain’s Ignition parameters JSON (e.g. ignition/paramsAmoy.json or ignition/paramsEthSepolia.json); it supplies router, arm proxy, token admin registry, and other addresses to the module.

Save from the deployment output:

  • <SEPOLIA_TOKEN_ADDRESS> (CrossChainToken)
  • <SEPOLIA_POOL_ADDRESS> (BurnMintTokenPool)

Step 2: Deploy Token + Pool on Amoy (destination)

npx hardhat ignition deploy ignition/modules/BurnMintTokenPool.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

Use the parameters file for the chain you are deploying to.

Save from the deployment output:

  • <AMOY_TOKEN_ADDRESS>
  • <AMOY_POOL_ADDRESS>

Step 3: Configure Sepolia Pool With Amoy Remote

npx hardhat example06-step1 --network <NETWORK_NAME> \
  --local-pool <SEPOLIA_POOL_ADDRESS> \
  --remote-chain-selector <AMOY_CHAIN_SELECTOR> \
  --remote-token <AMOY_TOKEN_ADDRESS> \
  --remote-pool <AMOY_POOL_ADDRESS>

Step 4: Configure Amoy Pool With Sepolia Remote

npx hardhat example06-step1 --network <NETWORK_NAME> \
  --local-pool <AMOY_POOL_ADDRESS> \
  --remote-chain-selector <SEPOLIA_CHAIN_SELECTOR> \
  --remote-token <SEPOLIA_TOKEN_ADDRESS> \
  --remote-pool <SEPOLIA_POOL_ADDRESS>

Step 5: Enable Fast Transfers on Both Pools

New pools allow full finality only (0x00000000). FTF is a pool setting; configure both pools:

npx hardhat example06-set-pool-finality --network <NETWORK_NAME> --local-pool <SEPOLIA_POOL_ADDRESS> --min-block-depth <MIN_BLOCK_DEPTH>
npx hardhat example06-set-pool-finality --network <NETWORK_NAME> --local-pool <AMOY_POOL_ADDRESS> --min-block-depth <MIN_BLOCK_DEPTH>

The task reads back each value. 0 disables FTF; 1–65535 set the minimum depth. The staged wait-for-safe flag is not usable on this lane.

Step 6: Check the Finality Gates

npx hardhat example06-check-finality-gates --network <NETWORK_NAME> \
  --source-router <SEPOLIA_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --token-to-send <SEPOLIA_TOKEN_ADDRESS> \
  --block-confirmations <BLOCK_CONFIRMATIONS_GT_ZERO>

The read-only task checks the pool, executor, and CCVs. A getFee quote alone does not prove ccipSend will succeed for a non-IPoolV2 pool.

Step 7: Send BurnMint Transfer

npx hardhat example06-step2 --network <NETWORK_NAME> \
  --source-router <SEPOLIA_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <RECEIVER_ON_AMOY> \
  --token-to-send <SEPOLIA_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit <GAS_LIMIT> \
  --block-confirmations 0 \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Parameter notes:

  • --amount: token amount in wei (e.g. 1000000000000000000 for 1 token with 18 decimals).
  • For token-only transfer to an EOA receiver, use --gas-limit 0.
  • This task uses ExtraArgsV3 with blockConfirmations = 0 (default finality).
  • --fee-token-address: use 0x0000000000000000000000000000000000000000 for native fee, or the LINK token address on Sepolia for LINK fee.

For the FTF version, run the same task after Steps 5 and 6 with a permitted depth:

npx hardhat example06-step2 --network <NETWORK_NAME> \
  --source-router <SEPOLIA_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <RECEIVER_ON_AMOY> \
  --token-to-send <SEPOLIA_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit <GAS_LIMIT> \
  --block-confirmations <BLOCK_CONFIRMATIONS_GT_ZERO> \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Step 8: Verify BurnMint Behavior

Verify token→pool registration:

npx hardhat token-admin-registry-get-pool --network <NETWORK_NAME> --registry <TOKEN_ADMIN_REGISTRY> --token <TOKEN_ADDRESS>

Verify remote lane mapping (tasks decode bytes to address):

npx hardhat pool-get-remote-token --network <NETWORK_NAME> --pool <POOL_ADDRESS> --chain-selector <CHAIN_SELECTOR>
npx hardhat pool-get-remote-pools --network <NETWORK_NAME> --pool <POOL_ADDRESS> --chain-selector <CHAIN_SELECTOR>

Verify balances and supplies after transfer finalizes:

npx hardhat erc20-balance-of --network <NETWORK_NAME> --token <TOKEN_ADDRESS> --account <ACCOUNT_ADDRESS>
npx hardhat erc20-total-supply --network <NETWORK_NAME> --token <TOKEN_ADDRESS>

Monitor message status with the message ID on:

  • https://ccip.chain.link

Example 07: CCT Lock and Release

This example covers the full LockRelease CCT flow on Sepolia → Amoy:

  1. Deploy token + lock box + LockRelease pool on both chains (Hardhat Ignition).
  2. Configure pools to trust each other (example07-step1).
  3. Fund destination lock box liquidity (example07-step2).
  4. Send a token transfer across the lane (example07-step3).
  5. Verify lock/release behavior with helper tasks.

Modules and tasks used:

  • Deploy token + lock box + pool: Hardhat Ignition module ignition/modules/LockAndReleaseTokenPool.ts (deploy on each chain with the appropriate parameters file).
  • Configure remote lane: Hardhat task example07-step1.
  • Fund lock box: Hardhat task example07-step2.
  • Send CCIP token transfer: Hardhat task example07-step3.
  • Verify: Helper tasks token-admin-registry-get-pool, pool-get-remote-token, pool-get-remote-pools, erc20-balance-of.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

Ensure ignition/paramsEthSepolia.json and ignition/paramsAmoy.json contain the correct routerAddress, armProxy, registryModuleOwnerCustom, and tokenAdminRegistry for each chain.

Deployment Defaults

The LockAndReleaseTokenPool Ignition module deploys:

  • Token: CrossChainToken with the same defaults and registerAdminViaGetCCIPAdmin flow as Example 06: TestToken (TEST), 18 decimals, 1_000_000 × 10¹⁸ premint, 100_000_000 × 10¹⁸ max supply.
  • Lock box: ERC20LockBox bound to the token.
  • Pool: LockReleaseTokenPool with no advanced pool hook (CCT 02).

Step 1: Deploy Token + Lock Box + Pool on Sepolia

npx hardhat ignition deploy ignition/modules/LockAndReleaseTokenPool.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

--parameters is the path to the chain’s Ignition parameters JSON (e.g. ignition/paramsEthSepolia.json or ignition/paramsAmoy.json); it supplies router, arm proxy, token admin registry, and other addresses to the module.

Save from the deployment output:

  • <SEPOLIA_TOKEN_ADDRESS> (CrossChainToken; same defaults as Example 06)
  • <SEPOLIA_LOCKBOX_ADDRESS> (ERC20LockBox)
  • <SEPOLIA_POOL_ADDRESS> (LockReleaseTokenPool)

Step 2: Deploy Token + Lock Box + Pool on Amoy

npx hardhat ignition deploy ignition/modules/LockAndReleaseTokenPool.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

Use the parameters file for the chain you are deploying to.

Save from the deployment output:

  • <AMOY_TOKEN_ADDRESS>
  • <AMOY_LOCKBOX_ADDRESS>
  • <AMOY_POOL_ADDRESS>

Step 3: Configure Sepolia Pool With Amoy Remote

npx hardhat example07-step1 --network <NETWORK_NAME> \
  --local-pool <SEPOLIA_POOL_ADDRESS> \
  --remote-chain-selector <AMOY_CHAIN_SELECTOR> \
  --remote-token <AMOY_TOKEN_ADDRESS> \
  --remote-pool <AMOY_POOL_ADDRESS>

Step 4: Configure Amoy Pool With Sepolia Remote

npx hardhat example07-step1 --network <NETWORK_NAME> \
  --local-pool <AMOY_POOL_ADDRESS> \
  --remote-chain-selector <SEPOLIA_CHAIN_SELECTOR> \
  --remote-token <SEPOLIA_TOKEN_ADDRESS> \
  --remote-pool <SEPOLIA_POOL_ADDRESS>

Step 5: Fund Destination Lock Box Liquidity

For Sepolia → Amoy transfers, fund the Amoy lock box first:

npx hardhat example07-step2 --network <NETWORK_NAME> \
  --token <AMOY_TOKEN_ADDRESS> \
  --lock-box <AMOY_LOCKBOX_ADDRESS> \
  --amount <LIQUIDITY_AMOUNT>

Use an amount in wei (e.g. 1000000000000000000000 for 1000 tokens with 18 decimals). If you also want Amoy → Sepolia transfers, fund the Sepolia lock box the same way:

npx hardhat example07-step2 --network <NETWORK_NAME> \
  --token <SEPOLIA_TOKEN_ADDRESS> \
  --lock-box <SEPOLIA_LOCKBOX_ADDRESS> \
  --amount <LIQUIDITY_AMOUNT>

Step 6: Send LockRelease Transfer (ExtraArgsV3 + Default Finality)

npx hardhat example07-step3 --network <NETWORK_NAME> \
  --source-router <SEPOLIA_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <RECEIVER_ON_AMOY> \
  --token-to-send <SEPOLIA_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit 0 \
  --block-confirmations 0 \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Parameter notes:

  • --amount: token amount in wei (e.g. 1000000000000000000 for 1 token with 18 decimals).
  • --gas-limit 0: token-only transfer to an EOA receiver.
  • This task uses ExtraArgsV3 with blockConfirmations = 0 (default finality).
  • --fee-token-address: use 0x0000000000000000000000000000000000000000 for native fee, or the LINK token address on Sepolia for LINK fee.

Step 7: Verify LockRelease Behavior

Verify token→pool registration:

npx hardhat token-admin-registry-get-pool --network <NETWORK_NAME> --registry <TOKEN_ADMIN_REGISTRY> --token <TOKEN_ADDRESS>

Verify remote lane mapping (tasks decode bytes to address):

npx hardhat pool-get-remote-token --network <NETWORK_NAME> --pool <POOL_ADDRESS> --chain-selector <REMOTE_CHAIN_SELECTOR>
npx hardhat pool-get-remote-pools --network <NETWORK_NAME> --pool <POOL_ADDRESS> --chain-selector <REMOTE_CHAIN_SELECTOR>

Verify lock box and receiver balances after transfer finalizes:

npx hardhat erc20-balance-of --network <NETWORK_NAME> --token <TOKEN_ADDRESS> --account <ACCOUNT_ADDRESS>

Monitor message status with the message ID on:

  • https://ccip.chain.link

Example 08: CCT Burn and Mint With AdvancedPoolHooks

This example covers BurnMint CCT on Sepolia → Amoy with AdvancedPoolHooks attached to the pool.

Token Pool Hooks Development

Token pool hooks are standalone contracts used by token pools for additional checks and behavior.

At a high level:

  • pool calls hook preflightCheck(...) on source-side lock/burn
  • pool calls hook postflightCheck(...) on destination-side release/mint
  • hook can enforce allowlists and CCV requirements
  • hook can optionally call a policy engine

For custom development, the pool-level integration point is IAdvancedPoolHooks and the pool constructor receives the hook address.

AdvancedPoolHooks includes optional policy engine integration through IPolicyEngine. In this example, policy engine is intentionally disabled (address(0)) to focus on hook attachment and allowlist flow.

When building policy-engine-driven flows, Chainlink ACE provides the policy engine interfaces and implementation model.

This flow still demonstrates the correct hook wiring pattern for ACE-enabled setups.

What This Example Covers

  1. Deploy CrossChainToken + AdvancedPoolHooks + BurnMint pool on both chains (Hardhat Ignition).
  2. Configure pools to trust each other (example08-step1).
  3. Send token transfer with ExtraArgsV3 default-finality sender (example08-step2).
  4. (Optional) Update hook allowlist (example08-step3).
  5. Verify hook attachment and allowlist with helper tasks.

Modules and tasks used:

  • Deploy token + hook + pool: Hardhat Ignition module ignition/modules/BurnMintTokenPoolAdvancedPoolHook.ts (deploy on each chain with the appropriate parameters file).
  • Configure remote lane: Hardhat task example08-step1.
  • Send CCIP token transfer: Hardhat task example08-step2.
  • Update allowlist: Hardhat task example08-step3.
  • Verify: Helper tasks token-admin-registry-get-pool, get-allow-list.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

Ensure ignition/paramsEthSepolia.json and ignition/paramsAmoy.json contain the correct routerAddress, armProxy, registryModuleOwnerCustom, tokenAdminRegistry, and thresholdAmountForAdditionalCCVs for each chain.

Deployment Defaults

The BurnMintTokenPoolAdvancedPoolHook Ignition module deploys:

  • Token: CrossChainToken (same defaults and registerAdminViaGetCCIPAdmin flow as Example 06): TestToken (TEST), 18 decimals, 1_000_000 × 10¹⁸ premint, 100_000_000 × 10¹⁸ max supply.
  • Hook: AdvancedPoolHooks with allowlist seeded with deployer EOA, thresholdAmountForAdditionalCCVs from params, policy engine disabled, pool authorized as caller.
  • Pool: BurnMintTokenPool with the hook attached.

Step 1: Deploy Token + Hook + Pool on Sepolia

npx hardhat ignition deploy ignition/modules/BurnMintTokenPoolAdvancedPoolHook.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

--parameters is the path to the chain’s Ignition parameters JSON (e.g. ignition/paramsEthSepolia.json or ignition/paramsAmoy.json); it supplies router, arm proxy, token admin registry, thresholdAmountForAdditionalCCVs, and other addresses to the module.

Save from the deployment output:

  • <SEPOLIA_TOKEN_ADDRESS>
  • <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS>
  • <SEPOLIA_POOL_ADDRESS>

Step 2: Deploy Token + Hook + Pool on Amoy

npx hardhat ignition deploy ignition/modules/BurnMintTokenPoolAdvancedPoolHook.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>

Use the parameters file for the chain you are deploying to.

Save from the deployment output:

  • <AMOY_TOKEN_ADDRESS>
  • <AMOY_ADVANCED_POOL_HOOK_ADDRESS>
  • <AMOY_POOL_ADDRESS>

Step 3: Configure Sepolia Pool With Amoy Remote

npx hardhat example08-step1 --network <NETWORK_NAME> \
  --local-pool <SEPOLIA_POOL_ADDRESS> \
  --remote-chain-selector <AMOY_CHAIN_SELECTOR> \
  --remote-token <AMOY_TOKEN_ADDRESS> \
  --remote-pool <AMOY_POOL_ADDRESS>

Step 4: Configure Amoy Pool With Sepolia Remote

npx hardhat example08-step1 --network <NETWORK_NAME> \
  --local-pool <AMOY_POOL_ADDRESS> \
  --remote-chain-selector <SEPOLIA_CHAIN_SELECTOR> \
  --remote-token <SEPOLIA_TOKEN_ADDRESS> \
  --remote-pool <SEPOLIA_POOL_ADDRESS>

Step 5: Send Transfer (ExtraArgsV3 + Default Finality)

npx hardhat example08-step2 --network <NETWORK_NAME> \
  --source-router <SEPOLIA_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <RECEIVER_ON_AMOY> \
  --token-to-send <SEPOLIA_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit 0 \
  --block-confirmations 0 \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Optional: Update Hook Allowlist

To add or remove senders in the allowlist after deployment, use the example08-step3 task.

Add one sender address:

npx hardhat example08-step3 --network <NETWORK_NAME> \
  --advanced-pool-hook <ADVANCED_POOL_HOOK_ADDRESS> \
  --removes "" \
  --adds "<ADDRESS_TO_ADD>"

Remove one sender address:

npx hardhat example08-step3 --network <NETWORK_NAME> \
  --advanced-pool-hook <ADVANCED_POOL_HOOK_ADDRESS> \
  --removes "<ADDRESS_TO_REMOVE>" \
  --adds ""

To add or remove multiple addresses, use comma-separated values for --adds and --removes.

Step 6: Verify Hook and Pool State

Verify token→pool registration:

npx hardhat token-admin-registry-get-pool --network <NETWORK_NAME> --registry <TOKEN_ADMIN_REGISTRY> --token <TOKEN_ADDRESS>

Verify allowlist (addresses in the hook):

npx hardhat get-allow-list --network <NETWORK_NAME> --advanced-pool-hook <ADVANCED_POOL_HOOK_ADDRESS>

To verify the attached hook address on each pool or the allowlist-enabled flag, use your RPC and a contract read (e.g. getAdvancedPoolHooks(), getAllowListEnabled()).

Monitor message status with the message ID on:

  • https://ccip.chain.link

Example 09: Pool-Based CCV Requirements on AdvancedPoolHooks

This example covers pool-based CCV (Cross-Chain Verifier) requirements on AdvancedPoolHooks, continuing the setup from Example 08.

Pool-Based CCVs

A token pool can require specific CCVs for its transfers. When a pool requires CCVs:

  • the source OnRamp consults the pool’s getRequiredCCVs(...) outbound and calls each CCV’s getOutboundImplementation and forwardToVerifier on the source chain
  • the destination OffRamp resolves the same CCV addresses on the destination chain and calls getInboundImplementation and verifyMessage before releasing or minting tokens

Because the destination OffRamp resolves the same address, a pool-required CCV must exist at the same address on both chains. Real CCVs are CREATE2-deployed to matching addresses on each chain.

AdvancedPoolHooks exposes this through per-remote-chain CCV configs:

  • outboundCCVs / inboundCCVs: base CCVs, always required for transfers to/from that chain
  • thresholdOutboundCCVs / thresholdInboundCCVs: additional CCVs, required only when the transfer amount is at or above thresholdAmountForAdditionalCCVs
  • address(0) in a base list means “use the lane defaults alongside the other listed CCVs”

Threshold CCV lists require non-empty base lists (use address(0) in the base list if you want defaults below the threshold). Duplicates within and across lists are rejected.

What This Example Covers

  1. Deploy CrossChainToken + AdvancedPoolHooks + BurnMint pool on both chains (Example 08 steps 1-4).
  2. Set pool CCV configs on both hooks (example09-step1).
  3. Send a token transfer and observe the CCV being consulted on both sides.
  4. Adjust the threshold amount and observe threshold CCVs becoming required (example09-step2).
  5. Clear the CCV config.

Modules and tasks used:

  • Deploy token + hook + pool, configure lanes: Example 08 (Ignition module ignition/modules/BurnMintTokenPoolAdvancedPoolHook.ts and task example08-step1)
  • Set pool CCV config: Hardhat task example09-step1
  • Set threshold amount: Hardhat task example09-step2
  • Send CCIP token transfer: Hardhat task example06-step2

Note on real CCVs

This starter kit does not deploy a real CCV. To try pool-based CCVs end to end on the live testnets, deploy your own CCV with the CCV starter kit and use its resolver address as <CCV_ADDRESS> below. The short version:

  1. On-chain contracts, on both chains — use the on-chain contracts kit to deploy the CREATE2 factory, the VersionedVerifierResolver, and the committee verifier, then apply the lane and signature config. The kit’s full flow walks the exact make commands; the resolver lands at the same address on both chains by construction (make deployments-check asserts the parity).
  2. Off-chain cell — use the off-chain kit to run the verifier and aggregator that actually attest messages (a Helm chart in Kubernetes, or the local docker-compose stack for testing). Without a running cell, the CCV exists on-chain but never attests, so messages requiring it will not execute.

Use the resolver address (not the committee verifier implementation) as <CCV_ADDRESS>: a pool that lists the implementation address reverts during the fee quote.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

Step 1: Deploy Token + Hook + Pool on Both Chains

Follow Example 08 steps 1-4 to deploy the token, hook and pool on both chains and link the lanes. Save from the deployment output:

  • <SEPOLIA_TOKEN_ADDRESS>, <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS>, <SEPOLIA_POOL_ADDRESS>
  • <AMOY_TOKEN_ADDRESS>, <AMOY_ADVANCED_POOL_HOOK_ADDRESS>, <AMOY_POOL_ADDRESS>

Step 2: Set Pool CCV Config on Sepolia

Require <CCV_ADDRESS> for both outbound and inbound traffic to Amoy:

npx hardhat example09-step1 --network <NETWORK_NAME> \
  --advanced-pool-hook <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS> \
  --remote-chain-selector <AMOY_CHAIN_SELECTOR> \
  --outbound-ccvs "<CCV_ADDRESS>" \
  --inbound-ccvs "<CCV_ADDRESS>"

The four list options (--outbound-ccvs, --threshold-outbound-ccvs, --inbound-ccvs, --threshold-inbound-ccvs) take comma-separated addresses and default to empty.

Step 3: Set Pool CCV Config on Amoy

Require the same <CCV_ADDRESS> for traffic to Sepolia:

npx hardhat example09-step1 --network <NETWORK_NAME> \
  --advanced-pool-hook <AMOY_ADVANCED_POOL_HOOK_ADDRESS> \
  --remote-chain-selector <SEPOLIA_CHAIN_SELECTOR> \
  --outbound-ccvs "<CCV_ADDRESS>" \
  --inbound-ccvs "<CCV_ADDRESS>"

Step 4: Send Transfer (Reuse the Example 06 Sender)

npx hardhat example06-step2 --network <NETWORK_NAME> \
  --source-router <SEPOLIA_ROUTER> \
  --destination-chain-selector <AMOY_CHAIN_SELECTOR> \
  --receiver <RECEIVER_ON_AMOY> \
  --token-to-send <SEPOLIA_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit 0 \
  --block-confirmations 0 \
  --fee-token-address <FEE_TOKEN_ADDRESS>

The source OnRamp now consults <CCV_ADDRESS> outbound, and the destination OffRamp verifies the message with the same address before minting.

Note on execution with a self-run CCV

Until your CCV is onboarded into the CCIP indexer, the default executor cannot fetch its attestation, so the message sits at UNTOUCHED after it is sent. Execute it yourself with the CCIP CLI (1.14.0 or later), reading the attestation directly from your own aggregator:

ccip-cli manual-exec <SOURCE_TX_HASH> --verifiers grpcs://<YOUR_AGGREGATOR_HOST>:443

See Test your setup in the CCV starter kit docs for the full walkthrough.

Step 5: Adjust the Threshold Amount

Threshold CCVs are required only for transfers at or above thresholdAmountForAdditionalCCVs. Example 08 deployed the hook with the threshold from your Ignition parameters file (1 in the sample files), so threshold CCVs are effectively always required. Raise the threshold so they apply only to large transfers:

npx hardhat example09-step2 --network <NETWORK_NAME> \
  --advanced-pool-hook <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS> \
  --threshold-amount 10000000000000000000000

Repeat on Amoy for the inbound side:

npx hardhat example09-step2 --network <NETWORK_NAME> \
  --advanced-pool-hook <AMOY_ADVANCED_POOL_HOOK_ADDRESS> \
  --threshold-amount 10000000000000000000000

To use threshold CCVs, set a config with non-empty threshold lists, for example --outbound-ccvs "<CCV_ADDRESS>" --inbound-ccvs "<CCV_ADDRESS>" --threshold-outbound-ccvs "<SECOND_CCV_ADDRESS>" --threshold-inbound-ccvs "<SECOND_CCV_ADDRESS>" (duplicates across lists are rejected).

Step 6: Clear the CCV Config

Re-run example09-step1 with no lists to remove the config for that lane:

npx hardhat example09-step1 --network <NETWORK_NAME> \
  --advanced-pool-hook <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS> \
  --remote-chain-selector <AMOY_CHAIN_SELECTOR>

Step 7: Verify Hook State

The example09-step1 and example09-step2 tasks print the stored config and threshold after each update. To check them later, use your RPC and a contract read on each hook:

  • getCCVConfig(uint64 remoteChainSelector) returns the four stored lists for that lane
  • getThresholdAmount() returns the threshold amount

Monitor message status with the message ID on:

  • https://ccip.chain.link

Example 10: Policy Engine on AdvancedPoolHooks

This example covers policy engine integration on AdvancedPoolHooks, continuing the setup from Example 08.

Policy Engine

AdvancedPoolHooks optionally calls a policy engine on every transfer:

  • on the source chain, the hook’s preflightCheck calls policyEngine.run(...) before the pool locks or burns
  • on the destination chain, the hook’s postflightCheck calls policyEngine.run(...) before the pool releases or mints

The hook passes its own preflightCheck/postflightCheck calldata (minus the selector) as the payload, so the engine can decode the transfer details and enforce policies. If run reverts, the transfer is rejected: on the source side the send reverts, on the destination side the message execution fails and can be retried.

The engine is called locally on each chain, so the source and destination engines are independent instances — unlike pool-required CCVs, they do not need to share an address.

For production-grade policy engines, use Chainlink ACE. This example attaches an existing engine by address and uses a minimal example engine (contracts/ExamplePolicyEngine.sol) that enforces a simple transfer-amount cap. To use a real ACE policy engine instead of the example engine, see Use a real ACE policy engine below.

What This Example Covers

  1. Deploy CrossChainToken + AdvancedPoolHooks + BurnMint pool on both chains (Example 08 steps 1-4).
  2. Deploy the minimal example policy engine.
  3. Attach the engine to the hook (example10-step1).
  4. Send a transfer within the cap (allowed) and above the cap (rejected).
  5. Detach the engine (example10-step2).

Modules and tasks used:

  • Deploy token + hook + pool, configure lanes: Example 08 (Ignition module ignition/modules/BurnMintTokenPoolAdvancedPoolHook.ts and task example08-step1)
  • Deploy the example policy engine: Hardhat Ignition module ignition/modules/ExamplePolicyEngine.ts
  • Attach the engine: Hardhat task example10-step1
  • Detach the engine: Hardhat task example10-step2
  • Send CCIP token transfer: Hardhat task example06-step2

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set AMOY_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set AMOY_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

Step 1: Deploy Token + Hook + Pool on Both Chains

Follow Example 08 steps 1-4 to deploy the token, hook and pool on both chains and link the lanes. Save from the deployment output:

  • <SEPOLIA_TOKEN_ADDRESS>, <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS>, <SEPOLIA_POOL_ADDRESS>
  • <AMOY_TOKEN_ADDRESS>, <AMOY_ADVANCED_POOL_HOOK_ADDRESS>, <AMOY_POOL_ADDRESS>

Step 2: Deploy the Example Policy Engine on Sepolia

npx hardhat ignition deploy ignition/modules/ExamplePolicyEngine.ts --network <NETWORK_NAME>

Save from the deployment output:

  • <SEPOLIA_POLICY_ENGINE_ADDRESS>

The example engine’s default cap is 10 ether, and only its owner (the deploying account) can change it with setCap. To use a real ACE policy engine instead, skip this step and follow Use a real ACE policy engine below, then pass the ACE engine’s onchain address to Step 3.

Step 3: Attach the Engine to the Sepolia Hook

npx hardhat example10-step1 --network <NETWORK_NAME> \
  --advanced-pool-hook <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS> \
  --policy-engine <SEPOLIA_POLICY_ENGINE_ADDRESS>

The hook calls the engine’s attach(), which records the hook as an attached target.

Step 4: Send a Transfer Within the Cap

npx hardhat example06-step2 --network <NETWORK_NAME> \
  --source-router <SEPOLIA_ROUTER> \
  --destination-chain-selector <AMOY_CHAIN_SELECTOR> \
  --receiver <RECEIVER_ON_AMOY> \
  --token-to-send <SEPOLIA_TOKEN_ADDRESS> \
  --amount 1000000000000000000 \
  --gas-limit 0 \
  --block-confirmations 0 \
  --fee-token-address <FEE_TOKEN_ADDRESS>

1 ether is within the 10 ether cap, so the engine allows the transfer.

Step 5: Send a Transfer Above the Cap

Send more than the cap (for example 11 ether):

npx hardhat example06-step2 --network <NETWORK_NAME> \
  --source-router <SEPOLIA_ROUTER> \
  --destination-chain-selector <AMOY_CHAIN_SELECTOR> \
  --receiver <RECEIVER_ON_AMOY> \
  --token-to-send <SEPOLIA_TOKEN_ADDRESS> \
  --amount 11000000000000000000 \
  --gas-limit 0 \
  --block-confirmations 0 \
  --fee-token-address <FEE_TOKEN_ADDRESS>

The source-side preflightCheck calls the engine, which reverts with AmountExceedsCap, and the send reverts.

Step 6: Detach the Engine

npx hardhat example10-step2 --network <NETWORK_NAME> \
  --advanced-pool-hook <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS>

The hook calls the engine’s detach(). Transfers are no longer policy-checked.

Optional: Destination-Side Engine

Attach an engine to the Amoy hook too (deploy a second engine on Amoy, then attach it to <AMOY_ADVANCED_POOL_HOOK_ADDRESS>). A destination-side rejection does not revert the send — the message is sent, but its execution on Amoy fails; check the message status on https://ccip.chain.link.

Use a real ACE policy engine

The example engine exists so this chapter runs without any Platform setup. For real policy enforcement, use Chainlink ACE: it provides managed policy engines, a built-in CCIP-AdvancedPoolHooks contract type with a pre-built extractor for the hook calldata, and policies you configure in the Chainlink Platform. You need an ACE-enabled organization on the Platform and a completed ACE account setup first. The flow below summarizes the Protect CCIP Token Pools with ACE guide, which has the complete Platform walkthrough.

  1. Create a policy engine in the Platform’s Policy Manager, including every EVM chain where you want to enforce policies (see Create a policy engine). Wait until the engine is Active, then copy its onchain address for each chain.
  2. Attach the hook to the engine with Step 3 above, passing the ACE engine’s onchain address for that chain. The hook’s attach() call is detected on-chain by ACE, which automatically adds the hook as a target under the engine. Do not register the hook manually as a second target.
  3. Verify the detected target: in the Platform, open the engine’s Contracts tab, find the target matching your hook’s address, and assign it the built-in CCIP-AdvancedPoolHooks contract type. It ships with preflightCheck and postflightCheck and their pre-built extractor, so no custom contract type or extractor is needed.
  4. Create a policy instance and protect a hook function: create the policy under the same engine and chain as the hook function you want to protect, then create a target protection selecting preflightCheck (source-side) or postflightCheck (destination-side), mapping policy parameters to the extractor’s outputs (for example from, to, amount_post_fee).

One mapping gotcha from the ACE guide: the hook sets the payload sender to msg.sender, which is the Token Pool, not the user who initiated the transfer. To evaluate the user, map the extracted from output to the policy parameter.

Step 7: Verify Hook and Engine State

The example10-step1 and example10-step2 tasks print the engine address stored on the hook after each update. To check state later, use your RPC and a contract read:

  • on the hook: getPolicyEngine() returns the attached engine (the zero address when detached)
  • on the example engine: isAttached(address) returns whether the hook is recorded as attached, and getCap() returns the transfer cap

Monitor message status with the message ID on:

  • https://ccip.chain.link

Using the CCIP API

CCIP API is available at:

https://api.ccip.chain.link/

Documentation at:

https://docs.chain.link/ccip/tools/api/

With the CCIP REST API you can:

  • fetch one message by messageId
  • search your messages by sender, chain selectors, or source tx hash
  • paginate through large result sets with cursor
  • filter messages that are ready for manual execution
  • read lane latency (totalMs) to estimate route timing

Set your variables once

Paste this once per terminal session, replace with your values, then run the commands below. Use the same EOA and messageId you got from your Hardhat example tasks.

export MY_EOA="0xYourEoaAddressHere"
export MESSAGE_ID="0xYourMessageIdFromExample01Logs"   # optional

Get one message by ID

After Example 01 (or any send), the script logs a message ID. Fetch its status and full details:

curl -s -X 'GET' \
  "https://api.ccip.chain.link/v2/messages/${MESSAGE_ID}" \
  -H 'accept: application/json' | jq

Or open in browser:

https://api.ccip.chain.link/v2/messages/<MESSAGE_ID>

You get status, source/dest network, sendTransactionHash, receiptTransactionHash, token amounts, fees. If you didn’t set MESSAGE_ID, set it to a real ID from your logs and run again.


List your messages (search, paginate, filter)

After you run a few examples, this is the fastest way to inspect only your own CCIP traffic.

Last 10 messages:

curl -s -X 'GET' \
  "https://api.ccip.chain.link/v2/messages?sender=${MY_EOA}&limit=10" \
  -H 'accept: application/json' | jq

The response has data (array of message summaries) and pagination with limit, hasNextPage, and cursor. Each item has a messageId; use the “Get one message” curl above with that ID to see full details and status.

Next page: copy the cursor value from the response, then run:

export CURSOR="paste_the_cursor_value_here"
curl -s -X 'GET' \
  "https://api.ccip.chain.link/v2/messages?cursor=${CURSOR}&limit=10" \
  -H 'accept: application/json' | jq

Don’t mix cursor with other query params (sender, chain selectors, etc.).

Filter by:

  • Ready for manual execution (e.g. from Example 05): add &readyForManualExecOnly=true
  • Source chain or destination chain: add &sourceChainSelector=... or &destChainSelector=... (values from CCIP Directory)

Examples:

# Only messages ready for manual execution
curl -s -X 'GET' \
  "https://api.ccip.chain.link/v2/messages?sender=${MY_EOA}&readyForManualExecOnly=true&limit=10" \
  -H 'accept: application/json' | jq

# Only messages from a given source chain (selector from CCIP Directory)
curl -s -X 'GET' \
  "https://api.ccip.chain.link/v2/messages?sender=${MY_EOA}&sourceChainSelector=14767482510784806043&limit=10" \
  -H 'accept: application/json' | jq

Lane latency

To show “about X minutes” for a route, call the latency endpoint with source and destination chain selectors (CCIP Directory). This one needs no variables—paste and run:

curl -s -X 'GET' \
  'https://api.ccip.chain.link/v2/lanes/latency?sourceChainSelector=14767482510784806043&destChainSelector=16015286601757825753' \
  -H 'accept: application/json' | jq

Response includes totalMs (p90 in milliseconds). Swap the selectors for the same chains you use in your examples.

CCIP SDK: ccip-send (Data, Token, Token+Data)

CCIP SDK package

Install from NPM: npm install @chainlink/ccip-sdk

NPM: https://www.npmjs.com/package/@chainlink/ccip-sdk

Docs: https://docs.chain.link/ccip/tools/sdk

This chapter shows how to run the SDK script for the three message shapes used in CCIP v2 examples:

  1. data-only
  2. token-only
  3. token+data

The script lives in sdk-examples/src/ccip-send.ts.

Receiver Compatibility for Fast Transfers (FTF)

When using --block-confirmations > 0 (Fast Transfers), receiver compatibility matters for message modes that execute receiver callbacks.

Mode--block-confirmations 0--block-confirmations > 0
databaseline receiver worksdestination receiver must allow the requested finality (see getCCVsAndFinalityConfig / allowedFinalityConfig)
token-databaseline receiver worksdestination receiver must allow the requested finality (see getCCVsAndFinalityConfig / allowedFinalityConfig)
token with --gas-limit 0callback not executed (EOA-style token receive path)callback not executed (EOA-style token receive path)

Terminology note:

  • Sender side (ExtraArgsV3): requested finality (e.g. FinalityCodec / block depth in requestedFinalityConfig)
  • Receiver side (getCCVsAndFinalityConfig): allowedFinalityConfig (bytes4, FinalityCodec)

How This Script Uses the SDK

At a high level, the script builds EVMChain instances for source and destination, asks the SDK for a fee quote, then submits the message via the SDK send call.

import { EVMChain } from '@chainlink/ccip-sdk'

const source = await EVMChain.fromUrl(sourceRpcUrl)
const dest = await EVMChain.fromUrl(destRpcUrl)

const fee = await source.getFee({ router, destChainSelector: dest.network.chainSelector, message })
await source.sendMessage({ router, destChainSelector: dest.network.chainSelector, message: { ...message, fee }, wallet })

Prerequisites

Requires Node.js 24 or later (@chainlink/ccip-sdk 1.x uses syntax Node 22 cannot run).

From repo root:

npm --prefix sdk-examples install

For actual sends (non---dry-run), set USER_KEY (or PRIVATE_KEY) in the root .env.

RPC Configuration

ccip-send accepts RPC URLs either:

  • directly via flags (--source-rpc-url, --dest-rpc-url)
  • or from .env:
    • CCIP_SOURCE_RPC_URL, CCIP_DEST_RPC_URL
    • fallback: ETHEREUM_SEPOLIA_RPC_URL, POLYGON_AMOY_RPC_URL

1) Data-Only

Fast Transfers (blockConfirmations=1):

npm --prefix sdk-examples run ccip-send -- \
  --mode data \
  --source-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \
  --dest-rpc-url "$POLYGON_AMOY_RPC_URL" \
  --router <SOURCE_ROUTER> \
  --receiver <RECEIVER> \
  --block-confirmations 1

Default finality (blockConfirmations=0):

npm --prefix sdk-examples run ccip-send -- \
  --mode data \
  --source-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \
  --dest-rpc-url "$POLYGON_AMOY_RPC_URL" \
  --router <SOURCE_ROUTER> \
  --receiver <RECEIVER> \
  --block-confirmations 0

2) Token-Only

Use Sepolia CCIP-BnM and the finality check from Example 01.

npm --prefix sdk-examples run ccip-send -- \
  --mode token \
  --source-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \
  --dest-rpc-url "$POLYGON_AMOY_RPC_URL" \
  --router <SOURCE_ROUTER> \
  --receiver <RECEIVER> \
  --token <SEPOLIA_CCIP_BNM_TOKEN> \
  --amount 1 \
  --block-confirmations <BLOCK_CONFIRMATIONS_GT_ZERO>

3) Token + Data

Configure the Amoy receiver as in Example 03.

npm --prefix sdk-examples run ccip-send -- \
  --mode token-data \
  --source-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \
  --dest-rpc-url "$POLYGON_AMOY_RPC_URL" \
  --router <SOURCE_ROUTER> \
  --receiver <RECEIVER> \
  --token <SEPOLIA_CCIP_BNM_TOKEN> \
  --amount 1 \
  --data "hello + token" \
  --block-confirmations <BLOCK_CONFIRMATIONS_GT_ZERO>

If You Need Testnet Tokens

Mint Sepolia CCIP-BnM with the faucet task:

npx hardhat faucet --network <NETWORK_NAME> --ccip-bnm <SEPOLIA_CCIP_BNM_TOKEN>

CCIP SDK: ccip-track (Tx Hash, Message ID, Sender)

CCIP SDK package

Install from NPM: npm install @chainlink/ccip-sdk

NPM: https://www.npmjs.com/package/@chainlink/ccip-sdk

Docs: https://docs.chain.link/ccip/tools/sdk

This chapter shows how to track CCIP v2 messages from source-chain activity using the SDK script:

  • sdk-examples/src/ccip-track.ts

How This Script Uses the SDK

The tracking script uses the SDK in three paths: fetch messages from a source tx hash, fetch by message ID, and stream sender history from source-chain logs.

import { EVMChain, getMessagesForSender } from '@chainlink/ccip-sdk'

const source = await EVMChain.fromUrl(sourceRpcUrl)
const byTx = await source.getMessagesInTx(sourceTxHash)
const byId = await source.getMessageById(messageId)

for await (const msg of getMessagesForSender(source, sender, { startBlock })) {
  console.log(msg.message.messageId)
}

Prerequisites

Requires Node.js 24 or later (@chainlink/ccip-sdk 1.x uses syntax Node 22 cannot run).

From repo root:

npm --prefix sdk-examples install

RPC Configuration

ccip-track reads source RPC URL from:

  • --source-rpc-url <url> flag, or
  • .env:
    • CCIP_SOURCE_RPC_URL
    • fallback: ETHEREUM_SEPOLIA_RPC_URL
source .env

1) Track by Source Transaction Hash

Use a source tx hash from your Hardhat task run (for example example01–example08):

npm --prefix sdk-examples run ccip-track -- \
  --source-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \
  --tx-hash <SOURCE_TX_HASH> 

2) Track by Message ID

npm --prefix sdk-examples run ccip-track -- \
  --source-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \
  --message-id <MESSAGE_ID>

Output prints the same concise SDK summary for that message ID.

3) Track by Sender

npm --prefix sdk-examples run ccip-track -- \
  --source-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \
  --limit 10 \
  --sender <SENDER_ADDRESS>

Arguments:

  • --limit: max rows to print (default 10)
  • --start-block: first source block to scan (default 0)

Using the CCIP CLI

CCIP CLI package

Install globally from NPM: npm install -g @chainlink/ccip-cli

NPM: https://www.npmjs.com/package/@chainlink/ccip-cli

Docs: https://docs.chain.link/ccip/tools/cli

This chapter covers:

  1. CLI install
  2. observability commands after Hardhat/SDK sends
  3. send parity commands for data-only, token-only, and token+data

Install and Verify

npm install -g @chainlink/ccip-cli
ccip-cli --version
ccip-cli --help

Observability

1) Inspect by source transaction hash

ccip-cli show <SOURCE_TX_HASH> \
  --rpcs <SOURCE_RPC_URL> \
  --rpcs <DEST_RPC_URL>

<SOURCE_TX_HASH> is the source-chain transaction hash.
<SOURCE_RPC_URL> and <DEST_RPC_URL> are RPC endpoints for the source and destination chains.

Wait for delivery from the same command:

ccip-cli show <SOURCE_TX_HASH> \
  --rpcs <SOURCE_RPC_URL> \
  --rpcs <DEST_RPC_URL> \
  --wait

--wait keeps polling until destination execution status is available.

2) Inspect by messageId

tx-hash positional becomes messageId when --id-from-source is used:

ccip-cli show <MESSAGE_ID> \
  --id-from-source <SOURCE_CHAIN_SELECTOR> \
  --rpcs <SOURCE_RPC_URL> \
  --rpcs <DEST_RPC_URL>

<MESSAGE_ID> is the CCIP message ID.
<SOURCE_CHAIN_SELECTOR> is the source CCIP chain selector.

Send Parity (CLI)

TO DO: Adapt these examples to use ExtraArgsV3

1) Data-only

ccip-cli send -s <SOURCE_CHAIN> -d <DEST_CHAIN> -r <SOURCE_ROUTER> \
  --rpcs <SOURCE_RPC_URL> \
  --rpcs <DEST_RPC_URL> \
  --wallet <PRIVATE_KEY> \
  --receiver <RECEIVER_ADDRESS> \
  --data "hello from ccip-cli" \
  --gas-limit 200000 \
  --wait

<SOURCE_CHAIN> and <DEST_CHAIN> can be chain ID, chain selector, or chain name.
Chain ID examples: Ethereum Sepolia 11155111, Polygon Amoy 80002.
<SOURCE_ROUTER> is the source-chain router for the selected lane.

2) Token-only

ccip-cli send -s <SOURCE_CHAIN> -d <DEST_CHAIN> -r <SOURCE_ROUTER> \
  --rpcs <SOURCE_RPC_URL> \
  --rpcs <DEST_RPC_URL> \
  --wallet <PRIVATE_KEY> \
  --receiver <RECEIVER_ADDRESS> \
  --transfer-tokens <TOKEN_ADDRESS>=<TOKEN_AMOUNT> \
  --gas-limit 0 \
  --wait

<TOKEN_ADDRESS>=<TOKEN_AMOUNT> uses token=amount format.
<TOKEN_AMOUNT> is human-readable; CLI converts it using token decimals.

3) Token + data

ccip-cli send -s <SOURCE_CHAIN> -d <DEST_CHAIN> -r <SOURCE_ROUTER> \
  --rpcs <SOURCE_RPC_URL> \
  --rpcs <DEST_RPC_URL> \
  --wallet <PRIVATE_KEY> \
  --receiver <RECEIVER_ADDRESS> \
  --data "hello + token" \
  --transfer-tokens <TOKEN_ADDRESS>=<TOKEN_AMOUNT> \
  --gas-limit 200000 \
  --wait

Same chain/router placeholders as above; --transfer-tokens uses the same token=amount format.

Token Amount Format

--transfer-tokens currently uses token=amount format.
Amount is human-readable and CLI converts it using token decimals.

Examples:

  • --transfer-tokens <TOKEN_ADDRESS>=1 means 1 whole token.
  • --transfer-tokens <TOKEN_ADDRESS>=1.5 means 1.5 tokens.

Legacy 01: ExtraArgsV1 Token Transfer

This example sends a token transfer using ExtraArgsV1 from an EOA.

Task: legacy01 (implementation: tasks/Legacy01.ts).

What You Will Do

  1. Mint test token(s) to your EOA on the source chain with the faucet task.
  2. Send token(s) using ExtraArgsV1 with the legacy01 task.
  3. Observe the message on CCIP Explorer.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set FUJI_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_RPC_URL
npx hardhat keystore set FUJI_RPC_URL

You do not need to deploy a receiver contract for this token-only example. Use a destination EOA as the receiver and pass --gas-limit 0.

Step 1: Get 1 CCIP-BnM Token on Source Chain

npx hardhat faucet --network <NETWORK_NAME> --ccip-bnm <CCIP_BNM_SOURCE_TOKEN_ADDRESS>

Step 2: Send Token With ExtraArgsV1

npx hardhat legacy01 --network <NETWORK_NAME> \
  --source-router <SOURCE_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <DESTINATION_EOA_ADDRESS> \
  --token-to-send <CCIP_BNM_SOURCE_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit <GAS_LIMIT> \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Parameter notes:

  • --amount: token amount in wei (for 18 decimals, 1000000000000000000 is 1 token).
  • --gas-limit: set 0 for token-only transfer to an EOA receiver. Use > 0 for contract callback.
  • --fee-token-address: LINK token address on the source chain to pay in LINK, or 0x0000000000000000000000000000000000000000 to pay in native coin.

Verify Result

Use the logged message ID in:

  • https://ccip.chain.link

Legacy 02: ExtraArgsV2 Token Transfer (CLI allowOutOfOrderExecution)

This example sends a token transfer using ExtraArgsV2 from an EOA.

Task: legacy02 (implementation: tasks/Legacy02.ts).

You can toggle allowOutOfOrderExecution via the --allow-out-of-order-execution option.

What You Will Do

  1. Mint test token(s) to your EOA on the source chain with the faucet task.
  2. Send token(s) using ExtraArgsV2 with the legacy02 task.
  3. Set out-of-order execution with --allow-out-of-order-execution true or false.
  4. Observe the message on CCIP Explorer.

Before You Start

Important: Keystore first

Use a local keystore account for task execution:

npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore set FUJI_PRIVATE_KEY
npx hardhat keystore set SEPOLIA_RPC_URL
npx hardhat keystore set FUJI_RPC_URL

You do not need to deploy a receiver contract for this token-only example. Use a destination EOA as the receiver and pass --gas-limit 0.

Step 1: Get 1 CCIP-BnM Token on Source Chain

npx hardhat faucet --network <NETWORK_NAME> --ccip-bnm <CCIP_BNM_SOURCE_TOKEN_ADDRESS>

Step 2: Send Token With ExtraArgsV2

npx hardhat legacy02 --network <NETWORK_NAME> \
  --source-router <SOURCE_ROUTER> \
  --destination-chain-selector <DESTINATION_CHAIN_SELECTOR> \
  --receiver <DESTINATION_EOA_ADDRESS> \
  --token-to-send <CCIP_BNM_SOURCE_TOKEN_ADDRESS> \
  --amount <AMOUNT> \
  --gas-limit <GAS_LIMIT> \
  --allow-out-of-order-execution <true|false> \
  --fee-token-address <FEE_TOKEN_ADDRESS>

Parameter notes:

  • --amount: token amount in wei (for 18 decimals, 1000000000000000000 is 1 token).
  • --gas-limit: set 0 for token-only transfer to an EOA receiver. Use > 0 for contract callback.
  • --allow-out-of-order-execution: true or false.
  • --fee-token-address: LINK token address on the source chain to pay in LINK, or 0x0000000000000000000000000000000000000000 to pay in native coin.

Verify Result

Use the logged message ID in:

  • https://ccip.chain.link