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_URLThe values from your local keystore are used at runtime (e.g. by
hardhat.config.tsfor 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.jsonThese files supply
$globalvalues (such asrouterAddress,armProxy,registryModuleOwnerCustom,tokenAdminRegistry, and for some modulesthresholdAmountForAdditionalCCVs) 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 compileRun 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
- Mint 1 CCIP-BnM token to your EOA with the
faucettask. - (Optional) Check the executor allowed finality config with the
executor-allowed-finality-configtask. - Send that token from source chain to destination chain with the
example01task.
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
ExtraArgsV3must 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.1000000000000000000for 1 token with 18 decimals).--gas-limit: set to0for token-only transfer to an EOA receiver; use e.g.200000for contract receivers.--block-confirmations: must be> 0and 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, or0x0000000000000000000000000000000000000000to 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
- Deploy
BasicMessageReceiverWithCCVson the destination chain using Ignition. - Configure minimum block depth for the source chain.
- Send a Hello World CCIP data message from the source chain to
BasicMessageReceiverWithCCVsusing theexample02task. - (Optional) Verify the received message with the
basic-message-receiver-latest-messagetask.
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:
requiredCCVsandoptionalCCVsdefine additional verifiers required for acceptance.allowedFinalityConfigdefines which requested finality modes are accepted (seeFinalityCodecin 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.
0means default finality-only behavior for that source chain.> 0enables 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-limitmust be> 0because the destination receiver callback needs gas (e.g.200000).--block-confirmationsmust be> 0for Fast Transfers.--block-confirmationsshould 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, or0x0000000000000000000000000000000000000000to 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
- Ensure a destination
BasicMessageReceiverWithCCVsexists on Amoy. - Mint 1
CCIP-BnMon Sepolia using thefaucettask. - Send one CCIP message containing both data and tokens using the
example03task. - (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
BasicMessageReceiverWithCCVsbefore running this chapter and configuresetMinBlockDepthfor 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
BasicMessageReceiverWithCCVson 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> > 0and check the source pool minimum too.The user-facing input for this example is
--min-block-depth <MIN_BLOCK_DEPTH>(auint16passed toBasicMessageReceiverWithCCVs.setMinBlockDepth). On-chain, the receiver does not return that integer directly to CCIP:getCCVsAndFinalityConfigsetsallowedFinalityConfigtoFinalityCodec._encodeBlockDepth(minBlockDepth)— the samebytes4finality encoding CCIP 2.0 uses elsewhere for allowed finality (depth0means wait for full/default finality).
--parametersis 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
--receiverin 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.1000000000000000000for 1 token with 18 decimals).--gas-limitmust be> 0because the receiver contract callback handles data (e.g.200000).--block-confirmationsmust 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, or0x0000000000000000000000000000000000000000to 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; requiresrouterAddressandlinkAddressin 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
- Deploy
BasicMessageSenderon the source chain (Sepolia) using Ignition. - Ensure destination
BasicMessageReceiverexists on Amoy (deploy via Ignition if needed; see below). - Mint 1
CCIP-BnMon Sepolia using thefaucettask. - Run the
example04task: 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
BasicMessageReceiveron Amoy with Ignition:npx hardhat ignition deploy ignition/modules/BasicMessageReceiver.ts --network <NETWORK_NAME> --parameters <PARAMETERS_FILE>
--parametersis 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
--receiverin 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.1000000000000000000for 1 token with 18 decimals).--gas-limitmust be> 0because 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 0x0000000000000000000000000000000000000000pays 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
- Ensure a compatible destination receiver exists (default finality or Fast Transfers (FTF) based on your
blockConfirmations). - Send a CCIP data message with
ExtraArgsV3no-execution-tag using theexample05task. - 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> > 0for Fast Transfers.Use this for Fast Transfers (
blockConfirmations > 0). Save this as<BASIC_MESSAGE_RECEIVER_WITH_CCVS_ADDRESS>.
--parametersis 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. If0and message data is empty, no callback executes.requestedFinalityConfig:bytes4finality mode + parameters perFinalityCodec(not a bareuint16). All-zero means wait for default/lane finality. For block-depth style Fast Transfers, encode withFinalityCodec._encodeBlockDepth(uint16)(see CCIP /FinalityCodecNatSpec). Example05 andEncodeExtraArgsOffchain.encodeV3/encodeV3Basictake ablockConfirmationsargument only as a convenience and set this field toFinalityCodec._encodeBlockDepth(blockConfirmations).ccvs: list of cross-chain verifier addresses. Empty means default verifiers.ccvArgs: optional arguments for each CCV. Must matchccvslength.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()returnsClient.NO_EXECUTION_ADDRESSfor 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-limitmust be> 0because the receiver callback needs gas (e.g.200000).--block-confirmationscan be0(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, or0x0000000000000000000000000000000000000000to 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:
- Deploy
CrossChainToken+ BurnMint pool on both chains (Hardhat Ignition). - Configure pools to trust each other (
example06-step1). - Enable Fast Transfers (FTF) on both pools and check the source lane gates.
- Send at default finality or Fast Transfers (
example06-step2). - 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_URLEnsure
ignition/paramsAmoy.jsonandignition/paramsEthSepolia.jsoncontain the correctrouterAddress,armProxy,registryModuleOwnerCustom, andtokenAdminRegistryfor 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 usesRegistryModuleOwnerCustom.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.1000000000000000000for 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: use0x0000000000000000000000000000000000000000for 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:
- Deploy token + lock box + LockRelease pool on both chains (Hardhat Ignition).
- Configure pools to trust each other (
example07-step1). - Fund destination lock box liquidity (
example07-step2). - Send a token transfer across the lane (
example07-step3). - 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_URLEnsure
ignition/paramsEthSepolia.jsonandignition/paramsAmoy.jsoncontain the correctrouterAddress,armProxy,registryModuleOwnerCustom, andtokenAdminRegistryfor each chain.
Deployment Defaults
The LockAndReleaseTokenPool Ignition module deploys:
- Token: CrossChainToken with the same defaults and
registerAdminViaGetCCIPAdminflow 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.1000000000000000000for 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: use0x0000000000000000000000000000000000000000for 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.
Chainlink ACE Context
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
- Deploy CrossChainToken +
AdvancedPoolHooks+ BurnMint pool on both chains (Hardhat Ignition). - Configure pools to trust each other (
example08-step1). - Send token transfer with ExtraArgsV3 default-finality sender (
example08-step2). - (Optional) Update hook allowlist (
example08-step3). - 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_URLEnsure
ignition/paramsEthSepolia.jsonandignition/paramsAmoy.jsoncontain the correctrouterAddress,armProxy,registryModuleOwnerCustom,tokenAdminRegistry, andthresholdAmountForAdditionalCCVsfor each chain.
Deployment Defaults
The BurnMintTokenPoolAdvancedPoolHook Ignition module deploys:
- Token: CrossChainToken (same defaults and
registerAdminViaGetCCIPAdminflow 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,
thresholdAmountForAdditionalCCVsfrom 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’sgetOutboundImplementationandforwardToVerifieron the source chain - the destination OffRamp resolves the same CCV addresses on the destination chain and calls
getInboundImplementationandverifyMessagebefore 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 chainthresholdOutboundCCVs/thresholdInboundCCVs: additional CCVs, required only when the transfer amount is at or abovethresholdAmountForAdditionalCCVsaddress(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
- Deploy CrossChainToken +
AdvancedPoolHooks+ BurnMint pool on both chains (Example 08 steps 1-4). - Set pool CCV configs on both hooks (
example09-step1). - Send a token transfer and observe the CCV being consulted on both sides.
- Adjust the threshold amount and observe threshold CCVs becoming required (
example09-step2). - Clear the CCV config.
Modules and tasks used:
- Deploy token + hook + pool, configure lanes: Example 08 (Ignition module
ignition/modules/BurnMintTokenPoolAdvancedPoolHook.tsand taskexample08-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:
- 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 exactmakecommands; the resolver lands at the same address on both chains by construction (make deployments-checkasserts the parity).- 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
UNTOUCHEDafter 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>:443See 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 lanegetThresholdAmount()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
preflightCheckcallspolicyEngine.run(...)before the pool locks or burns - on the destination chain, the hook’s
postflightCheckcallspolicyEngine.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
- Deploy CrossChainToken +
AdvancedPoolHooks+ BurnMint pool on both chains (Example 08 steps 1-4). - Deploy the minimal example policy engine.
- Attach the engine to the hook (
example10-step1). - Send a transfer within the cap (allowed) and above the cap (rejected).
- Detach the engine (
example10-step2).
Modules and tasks used:
- Deploy token + hook + pool, configure lanes: Example 08 (Ignition module
ignition/modules/BurnMintTokenPoolAdvancedPoolHook.tsand taskexample08-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.
- 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.
- 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. - 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-AdvancedPoolHookscontract type. It ships withpreflightCheckandpostflightCheckand their pre-built extractor, so no custom contract type or extractor is needed. - 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) orpostflightCheck(destination-side), mapping policy parameters to the extractor’s outputs (for examplefrom,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, andgetCap()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:
Documentation at:
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
This chapter shows how to run the SDK script for the three message shapes used in CCIP v2 examples:
- data-only
- token-only
- 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 |
|---|---|---|
data | baseline receiver works | destination receiver must allow the requested finality (see getCCVsAndFinalityConfig / allowedFinalityConfig) |
token-data | baseline receiver works | destination receiver must allow the requested finality (see getCCVsAndFinalityConfig / allowedFinalityConfig) |
token with --gas-limit 0 | callback 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 inrequestedFinalityConfig) - 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
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 (default10)--start-block: first source block to scan (default0)
Using the CCIP CLI
CCIP CLI package
Install globally from NPM:
npm install -g @chainlink/ccip-cli
This chapter covers:
- CLI install
- observability commands after Hardhat/SDK sends
- 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>=1means 1 whole token.--transfer-tokens <TOKEN_ADDRESS>=1.5means 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
- Mint test token(s) to your EOA on the source chain with the
faucettask. - Send token(s) using
ExtraArgsV1with thelegacy01task. - 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,1000000000000000000is 1 token).--gas-limit: set0for token-only transfer to an EOA receiver. Use> 0for contract callback.--fee-token-address: LINK token address on the source chain to pay in LINK, or0x0000000000000000000000000000000000000000to 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
- Mint test token(s) to your EOA on the source chain with the
faucettask. - Send token(s) using
ExtraArgsV2with thelegacy02task. - Set out-of-order execution with
--allow-out-of-order-execution trueorfalse. - 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,1000000000000000000is 1 token).--gas-limit: set0for token-only transfer to an EOA receiver. Use> 0for contract callback.--allow-out-of-order-execution:trueorfalse.--fee-token-address: LINK token address on the source chain to pay in LINK, or0x0000000000000000000000000000000000000000to pay in native coin.
Verify Result
Use the logged message ID in:
- https://ccip.chain.link