Manual Execution
This tutorial extends the Transfer Tokens with Data example. It uses CCIP to send tokens + arbitrary data from one chain to another in a single transaction, and then
demonstrates manual execution by intentionally causing the destination execution to fail.
The example shows execution using CCIP Explorer (UI) or ccip-cli (terminal)
Before you begin
- You should understand how to write, compile, deploy, and fund a smart contract. Go through this tutorial to get started.
- Your wallet must have:
ETHon Ethereum Sepolia (deploy + send)ETHon Arbitrum Sepolia (deploy + configure + manual execution transaction gas)CCIP-BnMon Ethereum Sepolia (token transfer)
- If you plan to pay CCIP fees in LINK, you also need
LINKon Ethereum Sepolia. Learn how to Acquire testnet LINK. - Check the CCIP Directory if you want to configure a different set of source and destination chains/tokens.
Examine the code
1 Initializing the contracts
The example deploys ProgrammableTokenTransfers on both chains:
- Ethereum Sepolia (sender)
- Arbitrum Sepolia (receiver)
Some key things to note:
- The contracts inherit
CCIPReceiverand accept the Router address at deploy time. - Access control is enforced through allowlisting:
- outbound messages are restricted by destination chain selector (
allowlistDestinationChain) - inbound delivery is restricted by source chain selector + sender contract address (
allowlistChainSender)
- outbound messages are restricted by destination chain selector (
- The receiver advertises its allowed finality modes per source chain via
getCCVsAndFinalityConfig. The configure scripts set this policy, and the send scripts validateBLOCK_DEPTHrequests against it.
contract ProgrammableTokenTransfers is CCIPReceiver, OwnerIsCreator {
mapping(uint64 => bool) public allowlistedDestinationChains;
mapping(uint64 => mapping(address => bool)) public allowlistedChainSenders;
mapping(uint64 => bytes4) private s_allowedFinalityConfig;
constructor(address _router) CCIPReceiver(_router) {}
function allowlistDestinationChain(uint64 _destinationChainSelector, bool allowed) external onlyOwner { /* ... */ }
function allowlistChainSender(uint64 _sourceChainSelector, address _sender, bool allowed) external onlyOwner { /* ... */ }
function getCCVsAndFinalityConfig(uint64 sourceChainSelector, bytes calldata sender)
external
view
override
returns (address[] memory, address[] memory, uint8, bytes4 allowedFinalityConfig)
{
// ... allowlist enforcement ...
allowedFinalityConfig = s_allowedFinalityConfig[sourceChainSelector];
}
}
2 Build transaction payload (EVM2AnyMessage + extraArgs)
The sender builds a Client.EVM2AnyMessage payload and passes it
to the Router. The important part for this tutorial is extraArgs:
extraArgsis abytesfield that carries message execution parameters.- On EVM chains, this includes a
gasLimitfield (for example, seeGenericExtraArgsV2.gasLimit). - In CCIP 2.0 tutorials, the scripts encode
extraArgsoff-chain (V2 or V3 depending on lane support) and pass the encoded bytes to the contract. The contract treats it as opaque bytes and forwards it into the message payload.
function _buildCCIPMessage(
address _receiver,
string calldata _text,
address _token,
uint256 _amount,
address _feeTokenAddress,
bytes calldata _extraArgs
) private pure returns (Client.EVM2AnyMessage memory) {
return Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: abi.encode(_text),
tokenAmounts: tokenAmounts,
extraArgs: _extraArgs,
feeToken: _feeTokenAddress
});
}
Where does GAS_LIMIT come from? The tutorial scripts read GAS_LIMIT from the environment and encode it into
extraArgs:
params.gasLimit = uint32(vm.envOr("GAS_LIMIT", uint256(200_000)));
bytes memory extraArgs = buildExtraArgs(/* ... */, params.gasLimit, params.requestedFinalityConfig);
// Hardhat send script (snippet)
const GAS_LIMIT_OVERRIDE = process.env.GAS_LIMIT ? Number(process.env.GAS_LIMIT) : undefined
// ... if unset, the script estimates dynamically; if set, it forces the override.
3 Receiver execution path (why `GAS_LIMIT=20000` fails)
On the destination chain, the Router calls ccipReceive, which enforces Router-only delivery and delegates into your
contract’s _ccipReceive handler.
_ccipReceive performs work that costs gas (decoding + state writes + emitting an event). In this tutorial, we
intentionally set GAS_LIMIT=20000 to make that execution run out of gas. When receiver execution fails, the delivery
reverts, and the message can become eligible for manual execution.
function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage)
internal
override
onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address)))
{
s_lastReceivedMessageId = any2EvmMessage.messageId;
s_lastReceivedSender = abi.decode(any2EvmMessage.sender, (address));
s_lastReceivedText = abi.decode(any2EvmMessage.data, (string));
emit MessageReceived(/* ... */);
}
4 Faster-Than-Finality (FTF): how this tutorial uses it
In this tutorial we use a numeric block depth for FTF:
- Configure step:
ALLOWED_FINALITY_CONFIG=BLOCK_DEPTHandALLOWED_BLOCK_DEPTH=32 - Send step:
BLOCK_DEPTH=32 - Default finalized finality: omit
BLOCK_DEPTHor useBLOCK_DEPTH=DEFAULT
Manual execution is triggered by execution failure (gas), not by which finality mode you choose. We include FTF
here because it is part of the standard CCIP 2.0 tutorial workflow and affects how the scripts encode extraArgs.
5 Manual execution (CCIP Explorer or CCIP CLI)
Manual execution is a retry path when the message has been committed but destination execution failed. CCIP execution is atomic: if the receiver callback reverts (for example, due to an insufficient callback gas limit), tokens and data are not delivered. The message can become Ready for manual execution, and any user can retry execution by submitting a destination-chain execution transaction (optionally overriding the receiver callback gas limit for that single retry).
In this tutorial you can retry execution using either:
- CCIP Explorer (UI): locate the message by
messageId, override the gas limit, and trigger manual execution. - CCIP CLI (terminal): use
ccip-cli:showto confirm the message is stuck / eligible for manual executionmanual-execto retry execution with a higher--gas-limit
Check out the complete contract code on Github.
Tutorial
Let's get started! Choose your preferred development environment below.
Foundry
Best for Solidity-native workflows that prefer a modular, powerful scripting framework.
1 Bootstrap a new project
Clone the Foundry Starter Kit for a smoother setup.
Working directory: Run all commands from the
docs-cciprepository root (the folder that containsfoundry.toml).
- Clone the CCIP 2.0 template repository, and open a terminal inside the project directory:
git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip
- Create an encrypted Foundry keystore:
cast wallet import your_keystore_name --interactive
- Install dependencies:
npm install
- Install/Update
ccip-cliand verify the installed version:
npm install -g @chainlink/ccip-cli
ccip-cli --version
- Create a
.envfile by copying the example file, and fill in your values:
cp .env.example .env
Set KEYSTORE_NAME to the name of the keystore entry you created above, and provide RPC endpoints for the chains you will use:
# Keystore name
KEYSTORE_NAME=your_keystore_name
# RPC URLs
ETHEREUM_SEPOLIA_RPC_URL=
ARBITRUM_SEPOLIA_RPC_URL=
# Etherscan API key (required only if you pass --verify to deployment scripts)
ETHERSCAN_API_KEY=
- Load the environment variables:
source .env
- Compile the contracts:
forge build
2 Deploy your contracts
- Deploy
ProgrammableTokenTransferson both Ethereum Sepolia (source) and Arbitrum Sepolia (destination):
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
forge script foundry/scripts/tutorials/programmable-token-transfers/deploy/Deploy.s.sol:Deploy \
--account $KEYSTORE_NAME --broadcast -vv
The deploy script prints the deployed contract addresses and follow-up commands:
========================================
🚀 Deploy CCIP Contracts on Both Chains
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
========================================
[Step 1] Deploying ProgrammableTokenTransfers on Ethereum Sepolia
Contract deployed at: 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B
https://sepolia.etherscan.io/address/0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B
========================================
✅ Deployment Complete on Ethereum Sepolia!
========================================
Source Contract Address: 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B
[Step 2] Deploying ProgrammableTokenTransfers on Arbitrum Sepolia
Contract deployed at: 0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c
https://sepolia.arbiscan.io/address/0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c
========================================
✅ Deployment Complete on Arbitrum Sepolia!
========================================
Destination Contract Address: 0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c
========================================
✅ All Deployments Complete!
========================================
Source Chain: Ethereum Sepolia
Source Contract: 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B
Destination Chain: Arbitrum Sepolia
Destination Contract: 0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c
Run this command to set both environment variables:
export ETHEREUM_SEPOLIA_CONTRACT=0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B && export ARBITRUM_SEPOLIA_CONTRACT=0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c
========================================
Check out the complete script code on Github.
3 Configure allowlists and finality
As a best practice, configure allowlists before sending messages. This script:
- allowlists the destination chain on the sender contract
- allowlists the chain-sender pair on the receiver contract
- configures the receiver to allow numeric faster than finality requests (
BLOCK_DEPTH) with a minimum depth of32
Finality configuration
- Export the addresses of the deployed contracts so they are available in your terminal:
export ETHEREUM_SEPOLIA_CONTRACT=<Sender Contract Address> && export ARBITRUM_SEPOLIA_CONTRACT=<Receiver Contract Address>
- Run the configure script:
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \
ALLOWED_BLOCK_DEPTH=32 \
forge script foundry/scripts/tutorials/programmable-token-transfers/configure/Configure.s.sol:Configure \
--account $KEYSTORE_NAME --broadcast -vv
========================================
⚙️ Configure CCIP Contracts on Both Chains
========================================
Source Chain: Ethereum Sepolia
Source Contract: 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B
Destination Chain: Arbitrum Sepolia
Destination Contract: 0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c
========================================
[Step 1] Configuring sender on Ethereum Sepolia
Allowlisting Arbitrum Sepolia as destination chain...
✅ Destination chain allowlisted: Arbitrum Sepolia
========================================
✅ Configuration Complete on Ethereum Sepolia!
========================================
[Step 2] Configuring receiver on Arbitrum Sepolia
Allowlisting sender 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B from Ethereum Sepolia...
✅ Chain-sender pair allowlisted: Ethereum Sepolia -> 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B
Setting allowed finality config to 0x00000020 (BLOCK_DEPTH=32)...
✅ Allowed finality config set to 0x00000020 (BLOCK_DEPTH=32) for Ethereum Sepolia
========================================
✅ Configuration Complete on Arbitrum Sepolia!
========================================
========================================
✅ All Configurations Complete!
========================================
Ethereum Sepolia can send messages to Arbitrum Sepolia
Arbitrum Sepolia can receive messages from Ethereum Sepolia
========================================
Check out the complete script code on Github.
4 Fund your wallet with test tokens and gas
Before sending a CCIP message, your wallet needs CCIP-BnM on Ethereum Sepolia. You also need ETH (Sepolia) and
ETH (Arbitrum Sepolia) to pay transaction gas.
Use the faucet script included in the starter kit to drip CCIP-BnM tokens to your wallet:
CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS=<your-wallet-address> \
forge script foundry/scripts/faucet/DripBnMToken.s.sol:DripBnMToken \
--account $KEYSTORE_NAME --broadcast -vv
========================================
💰 Drip CCIP-BnM Token
========================================
Chain: Ethereum Sepolia
CCIP-BnM Address: 0x9a97F119cFE1D5Ea77c264441C0A0aBC9B34E119
Recipient: 0x1A2b3C4d5E6f708192A3b4C5d6E7f8091A2b3C4D
========================================
✅ CCIP-BnM tokens dripped successfully!
Check out the faucet script on Github.
5 Trigger failure: send with low `GAS_LIMIT` and `BLOCK_DEPTH=32`
This step forces a destination execution failure by sending a programmable token transfer with:
GAS_LIMIT=20000(too low; destination execution should run out of gas)BLOCK_DEPTH=32(faster than finality example)
Faster Than Finality (block depth)
Run the send script:
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
BLOCK_DEPTH=32 \
GAS_LIMIT=20000 \
TOKEN_AMOUNT=1000000000000000 \
MESSAGE='Hello from manual execution tutorial!' \
forge script foundry/scripts/tutorials/programmable-token-transfers/interact/SendMessage.s.sol:SendMessage \
--account $KEYSTORE_NAME --broadcast -vv
The send script prints the messageId. Record it : you will use it for the next steps.
========================================
📡 CCIP Message Transfer - Pay with Native
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B
Receiver: 0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c
Fee Token: Native (ETH)
========================================
[Pre-validation] Detecting lane version and building extraArgs...
Gas limit (override): 20000
Token pool ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s))
Receiver contract ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s))
✅ Using V3 extraArgs with FTF (gasLimit=20000, finalityConfig=0x00000020 (BLOCK_DEPTH: 32 block(s))).
[Step 1] Approving contract to spend CCIP-BnM...
✅ Contract approved to spend CCIP-BnM
[Step 2] Sending CCIP message with native token fee ( ETH )...
Required CCIP fee (in WEI): 140525448541409
========================================
✅ Message sent successfully!
========================================
CCIP messageId: 0x8f3a1c9e5b7d2f4a6c8e0b3d5f7a9c1e3b5d7f9a2c4e6b8d0f3a5c7e9b1d3f5a
CCIP Explorer:
https://ccip.chain.link/#/side-drawer/msg/0x8f3a1c9e5b7d2f4a6c8e0b3d5f7a9c1e3b5d7f9a2c4e6b8d0f3a5c7e9b1d3f5a
Check out the complete script code on Github.
6 Option A : Manual execute in CCIP Explorer (confirm + trigger)
Use CCIP Explorer to confirm the message is Ready for manual execution and trigger manual execution.
- Open CCIP Explorer and locate your message by
messageId. You can paste themessageIdinto the Explorer search bar, or open the message page directly athttps://ccip.chain.link/#/side-drawer/msg/<messageId>(replace<messageId>). - Wait for the status to become Ready for manual execution.
- Connect your wallet, set the gas limit override to
200000, and trigger manual execution. Manual execution is a destination-chain transaction, so the executing wallet must have enough Arbitrum Sepolia ETH to pay gas. - Wait for the message status to become Success. Then skip Option B and continue to Verify receipt after manual execution.
7 Option B : Manual execute with CCIP CLI (show + manual-exec)
Use ccip-cli to confirm eligibility and retry execution from the terminal.
- Inspect the message status and confirm it is Ready for manual execution:
ccip-cli show <messageId> \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
- Manually execute with a higher receiver callback gas limit override:
ccip-cli manual-exec <messageId> \
--wallet foundry:$KEYSTORE_NAME \
--gas-limit 200000 \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
Lane:
┌────────────────┬──────────────────────────────────────────────┬───────────────────────────────────────┐
│ (index) │ source │ dest │
├────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┤
│ name │ 'ethereum-testnet-sepolia' │ 'ethereum-testnet-sepolia-arbitrum-1' │
│ chainId │ 11155111 │ 421614 │
│ chainSelector │ 16015286601757825753n │ 3478487238524512106n │
└────────────────┴──────────────────────────────────────────────┴───────────────────────────────────────┘
Request (source):
┌─────────────────────────┬──────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├─────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ messageId │ '0x8f3a1c9e5b7d2f4a6c8e0b3d5f7a9c1e3b5d7f9a2c4e6b8d0f3a5c7e9b1d3f5a' │
│ sender │ '0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B' │
│ receiver │ '0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c' │
│ finality │ 32n │
│ finalityType │ 'BLOCK_DEPTH' │
│ status │ 'FAILED' │
│ readyForManualExecution │ true │
└─────────────────────────┴──────────────────────────────────────────────────────────────────────┘
[FAILED] Message execution failed on destination chain
┌──────────────────┬──────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├──────────────────┼──────────────────────────────────────────────────────────────────────┤
│ state │ '❌ failed' │
│ returnData.error │ 'ReceiverError(bytes err)' │
│ returnData.err │ '0x [likely out-of-gas]' │
└──────────────────┴──────────────────────────────────────────────────────────────────────┘
Receipt (dest):
┌─────────────────┬──────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ state │ '✅ success' │
│ gasUsed │ 185432 │
│ contract │ '0x5F2b8D4a6C1e9A3f7B0d5E8c2A4f6B9d1E3a7C5b' │
│ transactionHash │ '0x2d4f6a8c0e1b3d5f7a9c1e2b4d6f8a0c2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d0f' │
│ blockNumber │ 39404012 │
└─────────────────┴──────────────────────────────────────────────────────────────────────┘
8 Verify receipt after manual execution
After manual execution (Option A or Option B), re-run the destination receipt query. This time, the receiver should report a received message:
CHAIN=ARBITRUM_SEPOLIA \
forge script foundry/scripts/tutorials/programmable-token-transfers/interact/GetLastReceivedMessageDetails.s.sol:GetLastReceivedMessageDetails
========================================
🔍 Verify Received Message
========================================
Chain: Arbitrum Sepolia
Receiver Address: 0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c
========================================
Checking for received message...
========================================
✅ Message Received Successfully!
========================================
Message ID: 0x8f3a1c9e5b7d2f4a6c8e0b3d5f7a9c1e3b5d7f9a2c4e6b8d0f3a5c7e9b1d3f5a
Sender: 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B
Received Text: "Hello from manual execution tutorial!"
Received Token: 0x686325E21F55c64Bf724047E0fe7C454D6faD37D
Received Token Amount: 1000000000000000
========================================
Check out the complete script code on Github.
Hardhat
Best for developers who want a mature, TypeScript-based smart contract development framework.
1 Bootstrap a new project
Clone the starter kit (contains both Hardhat and Foundry code) for a smoother setup.
- Clone the CCIP 2.0 template repository, and open a terminal inside the project directory:
git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip
- Copy the example environment file and fill in your values:
cp .env.example .env
Set KEYSTORE_NAME to the keystore alias you will create later in this section, and provide RPC endpoints for the chains you will use:
# Keystore name
KEYSTORE_NAME=your_keystore_name
# RPC URLs
ETHEREUM_SEPOLIA_RPC_URL=
ARBITRUM_SEPOLIA_RPC_URL=
# Etherscan API key (required only if you pass --verify to deployment scripts)
ETHERSCAN_API_KEY=
- Install dependencies and compile:
npm install && npx hardhat compile
- Load the environment variables:
source .env
- Create a Hardhat keystore entry for your private key. Use the same name as
KEYSTORE_NAMEin your.envfile. Hardhat will prompt you to enter the private key and a password to encrypt it:
npx hardhat keystore set your_keystore_name
- Install/Update
ccip-cliand verify the installed version:
npm install -g @chainlink/ccip-cli
ccip-cli --version
2 Deploy your contracts
Deploy ProgrammableTokenTransfers on both chains:
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/deploy/deploy.ts
========================================
🚀 Deploy CCIP Contracts on Both Chains
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
========================================
[Step 1] Deploying ProgrammableTokenTransfers on Ethereum Sepolia
Contract deployed at: 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c
https://sepolia.etherscan.io/address/0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c
========================================
✅ Deployment Complete on Ethereum Sepolia!
========================================
Source Contract Address: 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c
[Step 2] Deploying ProgrammableTokenTransfers on Arbitrum Sepolia
Contract deployed at: 0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e
https://sepolia.arbiscan.io/address/0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e
========================================
✅ Deployment Complete on Arbitrum Sepolia!
========================================
Destination Contract Address: 0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e
Run this command to set both environment variables:
export ETHEREUM_SEPOLIA_CONTRACT=0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c && export ARBITRUM_SEPOLIA_CONTRACT=0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e
========================================
Export the deployed contract addresses so the next scripts can find them:
export ETHEREUM_SEPOLIA_CONTRACT=<Sender Contract Address> && export ARBITRUM_SEPOLIA_CONTRACT=<Receiver Contract Address>
Check out the complete script code on Github.
3 Configure allowlists and finality
Finality configuration
Run the configure script with faster than finality enabled and a minimum documented block depth of 32:
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \
ALLOWED_BLOCK_DEPTH=32 \
npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/configure/configure.ts
========================================
⚙️ Configure CCIP Contracts on Both Chains
========================================
Source Chain: Ethereum Sepolia
Source Contract: 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c
Destination Chain: Arbitrum Sepolia
Destination Contract: 0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e
========================================
[Step 1] Configuring sender on Ethereum Sepolia
Allowlisting Arbitrum Sepolia as destination chain...
✅ Destination chain allowlisted: Arbitrum Sepolia
[Step 2] Configuring receiver on Arbitrum Sepolia
Allowlisting sender 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c from Ethereum Sepolia...
✅ Chain-sender pair allowlisted: Ethereum Sepolia -> 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c
Setting allowed finality config to 0x00000020 (BLOCK_DEPTH=32)...
✅ Allowed finality config set to 0x00000020 (BLOCK_DEPTH=32) for Ethereum Sepolia
========================================
✅ All Configurations Complete!
========================================
Ethereum Sepolia can send messages to Arbitrum Sepolia
Arbitrum Sepolia can receive messages from Ethereum Sepolia
========================================
Check out the complete script code on Github.
4 Fund your wallet with test tokens and gas
Drip CCIP-BnM to your wallet on Ethereum Sepolia:
CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS=<your-wallet-address> \
npx hardhat run hardhat/scripts/faucet/drip-bnm-token.ts
========================================
💰 Drip CCIP-BnM Token
========================================
Chain: Ethereum Sepolia
CCIP-BnM Address: 0x9a97F119cFE1D5Ea77c264441C0A0aBC9B34E119
Recipient: 0x0a1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3
========================================
✅ CCIP-BnM tokens dripped successfully!
https://sepolia.etherscan.io/tx/0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b
Check out the faucet script on Github.
5 Trigger failure: send with low `GAS_LIMIT` and `BLOCK_DEPTH=32`
This step forces a destination execution failure. Hardhat estimates the receiver callback gas limit when GAS_LIMIT is
unset, so set GAS_LIMIT=20000 to trigger an out-of-gas failure.
Faster Than Finality (block depth)
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
BLOCK_DEPTH=32 \
GAS_LIMIT=20000 \
TOKEN_AMOUNT=1000000000000000 \
MESSAGE='Hello from manual execution tutorial!' \
npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/interact/send-message.ts
Record the printed messageId.
========================================
📡 CCIP Message Transfer - Pay with ETH
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c
Receiver: 0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e
Fee Token: Native (ETH)
========================================
[Pre-validation] Querying lane features and receiver contract...
Gas limit (override): 20000
Token pool ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s))
Receiver contract ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s))
✅ Using V3 extraArgs with FTF (gasLimit=20000, finalityConfig=32 block(s)).
[Pre-validation] CCIP fee: 140525448541409
[Step 1] Approving contract to spend CCIP-BnM...
✅ Contract approved to spend CCIP-BnM
[Step 2] Sending CCIP message with native token fee (ETH)...
Required CCIP fee (in WEI): 140525448541409
========================================
✅ Message sent successfully!
========================================
CCIP messageId: 0x6b8d0f2a4c6e8b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d
CCIP Explorer:
https://ccip.chain.link/#/side-drawer/msg/0x6b8d0f2a4c6e8b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d
Check out the complete script code on Github.
6 Option A : Manual execute in CCIP Explorer (confirm + trigger)
Use CCIP Explorer to confirm the message is Ready for manual execution and trigger manual execution.
- Open CCIP Explorer and locate your message by
messageId. You can paste themessageIdinto the Explorer search bar, or open the message page directly athttps://ccip.chain.link/#/side-drawer/msg/<messageId>(replace<messageId>). - Wait for the status to become Ready for manual execution.
- Connect your wallet, set the gas limit override to
200000, and trigger manual execution. Manual execution is a destination-chain transaction, so the executing wallet must have enough Arbitrum Sepolia ETH to pay gas. - Wait for the message status to become Success. Then skip Option B and continue to Verify receipt after manual execution.
7 Option B : Manual execute with CCIP CLI (show + manual-exec)
Use ccip-cli to confirm eligibility and retry execution from the terminal.
- Inspect the message status and confirm it is Ready for manual execution:
ccip-cli show <messageId> \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
- Manually execute with a higher receiver callback gas limit override:
ccip-cli manual-exec <messageId> \
--wallet hardhat:$KEYSTORE_NAME \
--gas-limit 200000 \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
Lane:
┌────────────────┬──────────────────────────────────────────────┬───────────────────────────────────────┐
│ (index) │ source │ dest │
├────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┤
│ name │ 'ethereum-testnet-sepolia' │ 'ethereum-testnet-sepolia-arbitrum-1' │
│ chainId │ 11155111 │ 421614 │
│ chainSelector │ 16015286601757825753n │ 3478487238524512106n │
└────────────────┴──────────────────────────────────────────────┴───────────────────────────────────────┘
Request (source):
┌─────────────────────────┬──────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├─────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ messageId │ '0x6b8d0f2a4c6e8b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d' │
│ sender │ '0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c' │
│ receiver │ '0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e' │
│ finality │ 32n │
│ finalityType │ 'BLOCK_DEPTH' │
│ status │ 'FAILED' │
│ readyForManualExecution │ true │
└─────────────────────────┴──────────────────────────────────────────────────────────────────────┘
[FAILED] Message execution failed on destination chain
┌──────────────────┬──────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├──────────────────┼──────────────────────────────────────────────────────────────────────┤
│ state │ '❌ failed' │
│ returnData.error │ 'ReceiverError(bytes err)' │
│ returnData.err │ '0x [likely out-of-gas]' │
└──────────────────┴──────────────────────────────────────────────────────────────────────┘
Receipt (dest):
┌─────────────────┬──────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ state │ '✅ success' │
│ gasUsed │ 185432 │
│ contract │ '0x5F2b8D4a6C1e9A3f7B0d5E8c2A4f6B9d1E3a7C5b' │
│ transactionHash │ '0x9c1e3b5d7f9a2c4e6b8d0f2a4c6e8b0d3f5a7c9e1b3d5f7a9c2e4b6d8f0a1c3e' │
│ blockNumber │ 39404025 │
└─────────────────┴──────────────────────────────────────────────────────────────────────┘
8 Verify receipt after manual execution
After manual execution (Option A or Option B), re-run the destination receipt query. This time, the receiver should report a received message:
CHAIN=ARBITRUM_SEPOLIA \
npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/interact/get-last-received-message-details.ts
========================================
🔍 Verify Received Message
========================================
Chain: Arbitrum Sepolia
Receiver Address: 0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e
========================================
Checking for received message...
========================================
✅ Message Received Successfully!
========================================
Message ID: 0x6b8d0f2a4c6e8b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d
Sender: 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c
Received Text: "Hello from manual execution tutorial!"
Received Token: 0x686325E21F55c64Bf724047E0fe7C454D6faD37D
Received Token Amount: 1000000000000000
========================================
Check out the complete script code on Github.