From 7c2feb66ace3dcc21fac669a42b7e0af88f654c1 Mon Sep 17 00:00:00 2001 From: Karim <98668332+khadni@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:11:15 -0500 Subject: [PATCH 1/6] feat: add full LLM documentation for Solutions --- src/components/RightSidebar/LlmsLink.tsx | 14 +- src/components/RightSidebar/MoreMenu.astro | 20 +- .../Solutions/SolutionDisclaimer.astro | 19 +- src/config/llms.ts | 3 + src/config/solutions/disclaimer.ts | 9 + src/content/ccip/llms-full.txt | 26522 ++++++++++++++-- src/content/data-feeds/llms-full.txt | 45 +- src/content/solutions/README.md | 18 + .../cross-chain-vault-adapter/llms-full.txt | 4490 +++ src/layouts/SolutionLayout.astro | 2 + .../__tests__/transformMarkdown.test.ts | 58 + src/lib/markdown/buildMarkdownArtifact.ts | 6 +- src/lib/markdown/componentHandlers.ts | 43 +- src/lib/markdown/llms.ts | 119 + src/lib/markdown/solutionContent.ts | 34 + src/lib/markdown/solutionLlms.ts | 60 + src/lib/markdown/transformMarkdown.ts | 24 +- src/pages/solutions/[slug]/llms-full.txt.ts | 33 + src/scripts/check-markdown-fidelity.test.ts | 104 + src/scripts/generate-llms.test.ts | 152 +- src/scripts/generate-llms.ts | 188 +- src/scripts/validate-llms.ts | 133 +- src/utils/solutionLlms.test.ts | 188 + src/utils/solutions.ts | 46 +- 24 files changed, 30128 insertions(+), 2202 deletions(-) create mode 100644 src/config/solutions/disclaimer.ts create mode 100644 src/content/solutions/cross-chain-vault-adapter/llms-full.txt create mode 100644 src/lib/markdown/llms.ts create mode 100644 src/lib/markdown/solutionContent.ts create mode 100644 src/lib/markdown/solutionLlms.ts create mode 100644 src/pages/solutions/[slug]/llms-full.txt.ts create mode 100644 src/utils/solutionLlms.test.ts diff --git a/src/components/RightSidebar/LlmsLink.tsx b/src/components/RightSidebar/LlmsLink.tsx index 5d3b358f677..07283733da5 100644 --- a/src/components/RightSidebar/LlmsLink.tsx +++ b/src/components/RightSidebar/LlmsLink.tsx @@ -6,6 +6,8 @@ interface LlmsLinkProps { section: string supportedLanguages: string[] currentPageLanguage?: string | null + displayName?: string + downloadName?: string } // Map section slugs to display names @@ -21,7 +23,13 @@ const SECTION_DISPLAY_NAMES: Record = { // Add more as needed } -export function LlmsLink({ section, supportedLanguages, currentPageLanguage }: LlmsLinkProps) { +export function LlmsLink({ + section, + supportedLanguages, + currentPageLanguage, + displayName, + downloadName, +}: LlmsLinkProps) { // Subscribe to the language store for reactive updates const storeLanguage = useStore(selectedLanguage) @@ -50,13 +58,13 @@ export function LlmsLink({ section, supportedLanguages, currentPageLanguage }: L } else { // Single file section - include product name llmsHref = `/${section}/llms-full.txt` - const productName = SECTION_DISPLAY_NAMES[section] || section.toUpperCase() + const productName = displayName || SECTION_DISPLAY_NAMES[section] || section.toUpperCase() llmsLabel = `Complete ${productName} docs (TXT)` } return (
  • - + More} @@ -26,7 +32,9 @@ const supportedLanguages = cfg?.languages || [] llmsExists && ( diff --git a/src/components/Solutions/SolutionDisclaimer.astro b/src/components/Solutions/SolutionDisclaimer.astro index 3541270bf4a..2cb52f61cf2 100644 --- a/src/components/Solutions/SolutionDisclaimer.astro +++ b/src/components/Solutions/SolutionDisclaimer.astro @@ -2,6 +2,7 @@ // Rendered by SolutionLayout at the end of the main column on EVERY solution page. // Never import this from an MDX file: legal compliance must not depend on an author // remembering to paste it. +import { SOLUTION_DISCLAIMER } from "~/config/solutions/disclaimer.ts" import alertIcon from "~/components/Alert/Assets/alert-icon.svg" --- @@ -11,21 +12,9 @@ import alertIcon from "~/components/Alert/Assets/alert-icon.svg"

    Disclaimer

    - Please note, this repo contains community examples only — these are not Chainlink products or services and are not - supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and - is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without - warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to - use this example code does so entirely at their own risk and must perform its own audits, security and code review, - key management, and testing before any production deployment and ensure the operation and performance of such code - matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or - endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are - no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not - rely on this code without first conducting your own technical, engineering, and security review. This code is also - outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor - Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or - operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink - Foundation Terms of Service, which provides important information and - disclosures. By using this code, you acknowledge and agree to these terms. + {SOLUTION_DISCLAIMER.beforeTerms}{" "}{SOLUTION_DISCLAIMER.termsLabel}{ + SOLUTION_DISCLAIMER.afterTerms + }

    diff --git a/src/config/llms.ts b/src/config/llms.ts index 0fd3d8978f1..b5d89eb1a1a 100644 --- a/src/config/llms.ts +++ b/src/config/llms.ts @@ -4,6 +4,7 @@ export type LlmsSectionConfig = { excludeGlobs?: string[] order?: string[] languages?: string[] // Supported languages for this section (if applicable) + maxBytes?: number // Optional size ceiling for larger complete corpora; other sections use the validator default. } // Central list of sections that expose llms-full.txt or llms-full-[lang].txt @@ -48,6 +49,8 @@ export const LLM_SECTIONS_CONFIG: Record = ccip: { root: "src/content/ccip", includeGlobs: ["**/*.mdx"], + // Preserving all ordered-list and accordion content in the v1/v2 corpus exceeds the default 5 MB limit. + maxBytes: 8_000_000, }, "data-feeds": { root: "src/content/data-feeds", diff --git a/src/config/solutions/disclaimer.ts b/src/config/solutions/disclaimer.ts new file mode 100644 index 00000000000..fcd5f280d9a --- /dev/null +++ b/src/config/solutions/disclaimer.ts @@ -0,0 +1,9 @@ +/** Single source for the disclaimer shown on solution pages and in their Markdown/TXT exports. */ +export const SOLUTION_DISCLAIMER = { + beforeTerms: + "Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation", + termsLabel: "Terms of Service", + termsUrl: "https://chain.link/terms", + afterTerms: + ", which provides important information and disclosures. By using this code, you acknowledge and agree to these terms.", +} as const diff --git a/src/content/ccip/llms-full.txt b/src/content/ccip/llms-full.txt index 62069a9a35c..53892adc004 100644 --- a/src/content/ccip/llms-full.txt +++ b/src/content/ccip/llms-full.txt @@ -8358,9 +8358,7 @@ You will transfer *0.001 CCIP-BnM*. The CCIP fees for using CCIP will be paid in > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -8368,12 +8366,16 @@ You will transfer *0.001 CCIP-BnM*. The CCIP fees for using CCIP will be paid in > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -9087,9 +9089,7 @@ You will transfer *0.001 CCIP-BnM* and a text. The CCIP fees for using CCIP will > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -9097,12 +9097,16 @@ You will transfer *0.001 CCIP-BnM* and a text. The CCIP fees for using CCIP will > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -9958,9 +9962,7 @@ You will transfer *0.001 CCIP-BnM* and a text. The CCIP fees for using CCIP will > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -9968,12 +9970,16 @@ You will transfer *0.001 CCIP-BnM* and a text. The CCIP fees for using CCIP will > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -13758,9 +13764,7 @@ You will transfer *1 USDC* and arbitrary data, which contains the encoded stake > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -13768,12 +13772,16 @@ You will transfer *1 USDC* and arbitrary data, which contains the encoded stake > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -14974,9 +14982,7 @@ You will use CCIP to send a text. The CCIP fees for using CCIP will be paid in n > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -14984,12 +14990,16 @@ You will use CCIP to send a text. The CCIP fees for using CCIP will be paid in n > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -15369,9 +15379,7 @@ When the *message tracker* receives the acknowledgment message, the `ccipReceive > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -15379,12 +15387,16 @@ When the *message tracker* receives the acknowledgment message, the `ccipReceive > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -16573,9 +16585,7 @@ You will increase the gas limit and trigger manual execution: > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -16583,12 +16593,16 @@ You will increase the gas limit and trigger manual execution: > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -33646,6 +33660,10 @@ fun onInternalMessage(in: InMessage) { } ``` +[minimal_receiver.tolk](https://github.com/smartcontractkit/ccip-starter-kit-ton/blob/main/contracts/minimal_receiver.tolk) + +Complete contract source including storage, constants, and imports. + Deploy: ```bash filename="Terminal" @@ -33685,6 +33703,10 @@ Receiver_CCIPReceive => { } ``` +[receiver_with_validateAndConfirm.tolk](https://github.com/smartcontractkit/ccip-starter-kit-ton/blob/main/contracts/receiver_with_validateAndConfirm.tolk) + +Complete contract source including storage, constants, and imports. + Deploy: ```bash filename="Terminal" @@ -33785,6 +33807,10 @@ The `TestReceiverBehavior.RejectAll` and `ConsumeAllGas` branches are testing ai | `facilityId()` | `uint16` | Facility identifier (`191`) | | `errorCode(local)` | `uint16` | Namespaced error code for a given local code | +[contracts/ccip/test/receiver/](https://github.com/smartcontractkit/chainlink-ton/tree/main/contracts/contracts/ccip/test/receiver) + +Complete source including message handlers, admin functions, and upgrade logic. + Deploy: ```bash filename="Terminal" @@ -34182,6 +34208,11 @@ Router_CCIPSendNACK => { The complete flow from wallet setup to sending is available in the TON Starter Kit: +[scripts/ton2evm/sendMessage.ts](https://github.com/smartcontractkit/ccip-starter-kit-ton/blob/main/scripts/ton2evm/sendMessage.ts) + +Full script including wallet setup, message construction, fee estimation, and send with CLI options for chain, +receiver, message content, and optional Sender contract routing. + ## Related Tutorials To see these concepts in action with a step-by-step implementation guide, check out the following tutorial: @@ -35919,9 +35950,7 @@ Last Updated: 2025-09-08 > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -35929,12 +35958,16 @@ Last Updated: 2025-09-08 > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -35988,9 +36021,7 @@ Last Updated: 2025-09-08 > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -35998,12 +36029,16 @@ Last Updated: 2025-09-08 > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -36339,9 +36374,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -36349,12 +36382,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -36538,9 +36575,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/i-type-and-vers > **NOTE: This contract is now available in Chainlink Contracts v1.4.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > @@ -36552,6 +36587,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/i-type-and-vers > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > > ```shell @@ -36562,6 +36599,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/i-type-and-vers > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -36602,9 +36641,7 @@ Last Updated: 2025-09-08 > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -36612,12 +36649,16 @@ Last Updated: 2025-09-08 > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -36751,9 +36792,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -36761,12 +36800,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -37270,9 +37313,7 @@ Last Updated: 2025-09-08 > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -37280,12 +37321,16 @@ Last Updated: 2025-09-08 > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -37466,9 +37511,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/pool > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -37476,12 +37519,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/pool > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -37640,9 +37687,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/token-pool > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -37650,12 +37695,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/token-pool > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -39136,9 +39185,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -39146,12 +39193,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/burn-mint-token > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -39262,9 +39313,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -39272,12 +39321,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/burn-mint-token > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -39325,9 +39378,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/burn-from-mint- > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -39335,12 +39386,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/burn-from-mint- > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -39445,9 +39500,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/lock-release-to > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -39455,12 +39508,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/lock-release-to > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -39742,9 +39799,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/token-admin-reg > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -39752,12 +39807,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/token-admin-reg > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -40399,9 +40458,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/registry-module > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -40409,12 +40466,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/registry-module > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -40651,9 +40712,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/burn-mint-erc20 > **NOTE: This contract is now available in Chainlink Contracts v1.4.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > @@ -40665,6 +40724,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/burn-mint-erc20 > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > > ```shell @@ -40675,6 +40736,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/burn-mint-erc20 > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -41078,9 +41141,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/rate-limiter > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -41088,12 +41149,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/rate-limiter > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -41440,9 +41505,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/errors > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -41450,12 +41513,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/errors > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -41485,9 +41552,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/events > **NOTE: Integrate Chainlink CCIP v1.6.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -41495,12 +41560,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/events > npm install @chainlink/contracts-ccip@1.6.1 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.1 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -41534,9 +41603,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/ownable-2-step > **NOTE: This contract is now available in Chainlink Contracts v1.4.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > @@ -41548,6 +41615,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/ownable-2-step > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > > ```shell @@ -41558,6 +41627,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/ownable-2-step > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -41778,9 +41849,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/ownable-2-step- > **NOTE: This contract is now available in Chainlink Contracts v1.4.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > @@ -41792,6 +41861,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/ownable-2-step- > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > > ```shell @@ -41802,6 +41873,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.1/ownable-2-step- > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -41857,9 +41930,7 @@ Last Updated: 2025-05-19 > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -41867,12 +41938,16 @@ Last Updated: 2025-05-19 > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -41926,9 +42001,7 @@ Last Updated: 2025-01-17 > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -41936,12 +42009,16 @@ Last Updated: 2025-01-17 > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -42277,9 +42354,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -42287,12 +42362,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -42468,9 +42547,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/i-type-and-vers > **NOTE: This contract is now available in Chainlink Contracts v1.4.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > @@ -42482,6 +42559,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/i-type-and-vers > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > > ```shell @@ -42492,6 +42571,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/i-type-and-vers > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -42532,9 +42613,7 @@ Last Updated: 2025-05-19 > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -42542,12 +42621,16 @@ Last Updated: 2025-05-19 > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -42681,9 +42764,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -42691,12 +42772,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -43215,9 +43300,7 @@ Last Updated: 2025-05-19 > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -43225,12 +43308,16 @@ Last Updated: 2025-05-19 > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -43411,9 +43498,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/pool > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -43421,12 +43506,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/pool > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -43585,9 +43674,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/token-pool > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -43595,12 +43682,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/token-pool > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -44972,9 +45063,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -44982,12 +45071,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/burn-mint-token > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -45097,9 +45190,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -45107,12 +45198,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/burn-mint-token > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -45253,9 +45348,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/burn-from-mint- > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -45263,12 +45356,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/burn-from-mint- > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -45371,9 +45468,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/lock-release-to > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -45381,12 +45476,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/lock-release-to > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -45728,9 +45827,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/token-admin-reg > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -45738,12 +45835,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/token-admin-reg > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -46305,9 +46406,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/registry-module > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -46315,12 +46414,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/registry-module > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -46557,9 +46660,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/burn-mint-erc20 > **NOTE: This contract is now available in Chainlink Contracts v1.4.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > @@ -46571,6 +46672,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/burn-mint-erc20 > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > > ```shell @@ -46581,6 +46684,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/burn-mint-erc20 > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -46984,9 +47089,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/rate-limiter > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -46994,12 +47097,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/rate-limiter > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -47346,9 +47453,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/errors > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -47356,12 +47461,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/errors > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -47391,9 +47500,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/events > **NOTE: Integrate Chainlink CCIP v1.6.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -47401,12 +47508,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/events > npm install @chainlink/contracts-ccip@1.6.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -47440,9 +47551,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/ownable-2-step > **NOTE: This contract is now available in Chainlink Contracts v1.4.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > @@ -47454,6 +47563,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/ownable-2-step > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > > ```shell @@ -47464,6 +47575,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/ownable-2-step > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -47684,9 +47797,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/ownable-2-step- > **NOTE: This contract is now available in Chainlink Contracts v1.4.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > @@ -47698,6 +47809,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/ownable-2-step- > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.4.0): > > ```shell @@ -47708,6 +47821,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.0/ownable-2-step- > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -47762,9 +47877,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1 > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -47772,12 +47885,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1 > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -47830,9 +47947,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/client > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -47840,12 +47955,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/client > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -48067,9 +48186,7 @@ Last Updated: 2024-08-15 > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -48077,12 +48194,16 @@ Last Updated: 2024-08-15 > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -48830,9 +48951,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/i-type-and-vers > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -48840,12 +48959,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/i-type-and-vers > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -48881,9 +49004,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/i-router-client > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -48891,12 +49012,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/i-router-client > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -49030,9 +49155,7 @@ Last Updated: 2024-08-15 > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -49040,12 +49163,16 @@ Last Updated: 2024-08-15 > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -49718,9 +49845,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/ccip-receiver > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -49728,12 +49853,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/ccip-receiver > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -49914,9 +50043,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/pool > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -49924,12 +50051,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/pool > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -50088,9 +50219,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/token-pool > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -50098,12 +50227,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/token-pool > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -51475,9 +51608,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -51485,12 +51616,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/burn-mint-token > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -51600,9 +51735,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -51610,12 +51743,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/burn-mint-token > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -51756,9 +51893,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/burn-from-mint- > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -51766,12 +51901,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/burn-from-mint- > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -51874,9 +52013,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/lock-release-to > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -51884,12 +52021,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/lock-release-to > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -52231,9 +52372,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/token-admin-reg > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -52241,12 +52380,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/token-admin-reg > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -52808,9 +52951,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/registry-module > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -52818,12 +52959,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/registry-module > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -53060,9 +53205,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/burn-mint-erc20 > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -53070,12 +53213,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/burn-mint-erc20 > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -53475,9 +53622,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/rate-limiter > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -53485,12 +53630,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/rate-limiter > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -53837,9 +53986,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/errors > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -53847,12 +53994,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/errors > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -53880,9 +54031,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/events > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -53890,12 +54039,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/events > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -53929,9 +54082,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/ownable-2-step > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -53939,12 +54090,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/ownable-2-step > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -54161,9 +54316,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/ownable-2-step- > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -54171,12 +54324,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.1/ownable-2-step- > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -54227,9 +54384,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0 > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -54237,12 +54392,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0 > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -54289,9 +54448,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/client > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -54299,12 +54456,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/client > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -54478,9 +54639,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/internal > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -54488,12 +54647,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/internal > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -55241,9 +55404,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/i-type-and-vers > **NOTE: Integrate Chainlink CCIP v1.5.1 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -55251,12 +55412,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/i-type-and-vers > npm install @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.1-beta.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -55292,9 +55457,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/i-router-client > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -55302,12 +55465,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/i-router-client > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -55447,9 +55614,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/ccip-receiver > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -55457,12 +55622,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/ccip-receiver > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -55572,9 +55741,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/pool > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -55582,12 +55749,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/pool > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -55723,9 +55894,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/token-pool > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -55733,12 +55902,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/token-pool > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -56342,9 +56515,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -56352,12 +56523,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/burn-mint-token > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -56414,9 +56589,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -56424,12 +56597,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/burn-mint-token > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -56503,9 +56680,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/burn-from-mint- > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -56513,12 +56688,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/burn-from-mint- > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -56575,9 +56754,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/lock-release-to > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -56585,12 +56762,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/lock-release-to > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -56780,9 +56961,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/token-admin-reg > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -56790,12 +56969,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/token-admin-reg > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -57164,9 +57347,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/registry-module > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -57174,12 +57355,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/registry-module > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -57288,9 +57473,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/errors > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -57298,12 +57481,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/errors > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -57329,9 +57516,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/events > **NOTE: Integrate Chainlink CCIP v1.5.0 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -57339,12 +57524,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.5.0/events > npm install @chainlink/contracts-ccip@1.5.0 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.5.0 > ``` > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -61803,9 +61992,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/burn-from-mint- > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -61813,12 +62000,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/burn-from-mint- > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -61958,9 +62149,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/burn-mint-erc20 > **NOTE: This contract is now available in Chainlink Contracts v1.5.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > @@ -61972,6 +62161,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/burn-mint-erc20 > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > > ```shell @@ -61982,6 +62173,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/burn-mint-erc20 > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -62385,9 +62578,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -62395,12 +62586,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/burn-mint-token > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -62483,9 +62678,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -62493,12 +62686,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/burn-mint-token > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -62645,9 +62842,7 @@ Last Updated: 2025-10-06 > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -62655,12 +62850,16 @@ Last Updated: 2025-10-06 > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -62877,9 +63076,7 @@ Last Updated: 2025-10-06 > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -62887,12 +63084,16 @@ Last Updated: 2025-10-06 > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -63262,9 +63463,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/errors > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -63272,12 +63471,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/errors > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -63342,9 +63545,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/events > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -63352,12 +63553,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/events > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -63427,9 +63632,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -63437,12 +63640,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -63984,9 +64191,7 @@ Last Updated: 2025-10-06 > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -63994,12 +64199,16 @@ Last Updated: 2025-10-06 > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -64167,9 +64376,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/i-type-and-vers > **NOTE: This contract is now available in Chainlink Contracts v1.5.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > @@ -64181,6 +64388,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/i-type-and-vers > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > > ```shell @@ -64191,6 +64400,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/i-type-and-vers > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -64231,9 +64442,7 @@ Last Updated: 2025-10-06 > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -64241,12 +64450,16 @@ Last Updated: 2025-10-06 > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -64335,9 +64548,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -64345,12 +64556,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -64569,9 +64784,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/lock-release-to > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -64579,12 +64792,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/lock-release-to > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -64915,9 +65132,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/ownable-2-step- > **NOTE: This contract is now available in Chainlink Contracts v1.5.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > @@ -64929,6 +65144,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/ownable-2-step- > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > > ```shell @@ -64939,6 +65156,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/ownable-2-step- > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -64993,9 +65212,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/ownable-2-step > **NOTE: This contract is now available in Chainlink Contracts v1.5.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > @@ -65007,6 +65224,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/ownable-2-step > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > > ```shell @@ -65017,6 +65236,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/ownable-2-step > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -65237,9 +65458,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/pool > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -65247,12 +65466,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/pool > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -65446,9 +65669,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/rate-limiter > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -65456,12 +65677,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/rate-limiter > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -65843,9 +66068,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/registry-module > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -65853,12 +66076,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/registry-module > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -66130,9 +66357,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/token-admin-reg > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -66140,12 +66365,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/token-admin-reg > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -66742,9 +66971,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/token-pool > **NOTE: Integrate Chainlink CCIP v1.6.2 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -66752,12 +66979,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.2/token-pool > npm install @chainlink/contracts-ccip@1.6.2 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.2 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -68269,9 +68500,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/burn-from-mint- > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -68279,12 +68508,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/burn-from-mint- > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -68424,9 +68657,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/burn-mint-erc20 > **NOTE: This contract is now available in Chainlink Contracts v1.5.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > @@ -68438,6 +68669,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/burn-mint-erc20 > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > > ```shell @@ -68448,6 +68681,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/burn-mint-erc20 > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -68851,9 +69086,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -68861,12 +69094,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/burn-mint-token > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -68949,9 +69186,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/burn-mint-token > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -68959,12 +69194,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/burn-mint-token > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -69111,9 +69350,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -69121,12 +69358,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -69343,9 +69584,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -69353,12 +69592,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -69817,9 +70060,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/errors > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -69827,12 +70068,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/errors > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -69897,9 +70142,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/events > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -69907,12 +70150,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/events > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -69982,9 +70229,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -69992,12 +70237,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -70539,9 +70788,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -70549,12 +70796,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -70722,9 +70973,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/i-type-and-vers > **NOTE: This contract is now available in Chainlink Contracts v1.5.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > @@ -70736,6 +70985,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/i-type-and-vers > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > > ```shell @@ -70746,6 +70997,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/i-type-and-vers > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -70786,9 +71039,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -70796,12 +71047,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -70890,9 +71145,7 @@ Last Updated: 2025-11-05 > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -70900,12 +71153,16 @@ Last Updated: 2025-11-05 > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -71124,9 +71381,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/lock-release-to > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -71134,12 +71389,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/lock-release-to > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -71470,9 +71729,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/ownable-2-step- > **NOTE: This contract is now available in Chainlink Contracts v1.5.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > @@ -71484,6 +71741,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/ownable-2-step- > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > > ```shell @@ -71494,6 +71753,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/ownable-2-step- > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -71548,9 +71809,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/ownable-2-step > **NOTE: This contract is now available in Chainlink Contracts v1.5.0** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > @@ -71562,6 +71821,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/ownable-2-step > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts NPM package](https://www.npmjs.com/package/@chainlink/contracts/v/1.5.0): > > ```shell @@ -71572,6 +71833,8 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/ownable-2-step > > > > Note: This contract is now maintained under the [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) package instead of [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip). > +> ### foundry +> > If you use [Foundry](https://book.getfoundry.sh/), install the package: > > ```shell @@ -71792,9 +72055,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/pool > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -71802,12 +72063,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/pool > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -72001,9 +72266,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/rate-limiter > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -72011,12 +72274,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/rate-limiter > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -72398,9 +72665,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/registry-module > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -72408,12 +72673,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/registry-module > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -72685,9 +72954,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/token-admin-reg > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -72695,12 +72962,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/token-admin-reg > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -73297,9 +73568,7 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/token-pool > **NOTE: Integrate Chainlink CCIP v1.6.3 into your project** > -> npm -> yarn -> foundry +> ### npm > > If you use [NPM](https://www.npmjs.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > @@ -73307,12 +73576,16 @@ Source: https://docs.chain.link/ccip/v1/evm/api-reference/v1.6.3/token-pool > npm install @chainlink/contracts-ccip@1.6.3 > ``` > +> ### yarn +> > If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip): > > ```shell > yarn add @chainlink/contracts-ccip@1.6.3 > ``` > +> ### foundry +> > In your [Foundry](https://book.getfoundry.sh/) project, for easier integration, if you want to avoid installing the dependencies that are only used by the `chainlink-ccip` package, you can use either [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install the `chainlink-ccip` package: > > - Using `npm`: @@ -81245,14 +81518,13 @@ grant uses ServiceAccount names that only exist once the chart is rendered: > **NOTE** > -> The release name changes the resource names. The Helm fullname collapses only when the release name already -> contains the chart name (`ccv-cell-1` renders `ccv-cell-1-aggregator`, but `demo-cell` renders -> `demo-cell-ccv-cell-aggregator`), and your secret grant depends on those StatefulSet and ServiceAccount names. -> Avoid `ccv-cell*` release names, or read the names from the rendered output. -> -> Startup errors are expected for the first 15 to 30 seconds. The verifier logs `connection refused` against its own -> aggregator until the aggregator is up. If they continue past a minute, see [Logging and -> monitoring](/ccip/ccv-starter-kit/logging-and-monitoring#read-the-startup-logs). +> - The release name changes the resource names. The Helm fullname collapses only when the release name already +> contains the chart name (`ccv-cell-1` renders `ccv-cell-1-aggregator`, but `demo-cell` renders +> `demo-cell-ccv-cell-aggregator`), and your secret grant depends on those StatefulSet and ServiceAccount names. +> Avoid `ccv-cell*` release names, or read the names from the rendered output. +> - Startup errors are expected for the first 15 to 30 seconds. The verifier logs `connection refused` against its own +> aggregator until the aggregator is up. If they continue past a minute, see [Logging and +> monitoring](/ccip/ccv-starter-kit/logging-and-monitoring#read-the-startup-logs). Checkpoint: both the verifier and aggregator pods report 1/1, and the verifier log is clean after the startup window. @@ -81357,12 +81629,11 @@ curl -s https://api.ccip.chain.link/v2/messages/ | jq -r '.status' > **NOTE: Two normal states while you wait** > -> The verifier attests only once the source message reaches the finality the message requests, and it logs `Healthy` -> while it waits. A default (finalized) message waits for the source chain's `finalized` tag, roughly 13 to 17 -> minutes on Ethereum Sepolia; a message that requests the `safe` tag or a block depth is faster. -> -> The public CCIP API does not know your verifier yet. Until your committee is onboarded it reports your verifier -> entry as `UNKNOWN`; rely on what your own aggregator returns. +> - The verifier attests only once the source message reaches the finality the message requests, and it logs `Healthy` +> while it waits. A default (finalized) message waits for the source chain's `finalized` tag, roughly 13 to 17 +> minutes on Ethereum Sepolia; a message that requests the `safe` tag or a block depth is faster. +> - The public CCIP API does not know your verifier yet. Until your committee is onboarded it reports your verifier +> entry as `UNKNOWN`; rely on what your own aggregator returns. Checkpoint: the message reached the destination. The cell is ready for onboarding. @@ -81546,12 +81817,11 @@ has every field. > **CAUTION: When you provide secrets.toml yourself** > -> RPC URLs that carry an API key go in the verifier EVM secret, not in values. Config rendered from values lands in -> a ConfigMap, which cannot hold a credential. -> -> `keystoreBackend` and `kms.*` in values are ignored on these backends. Put the `[keystore]` block inside the -> `secrets.toml` you mount. See [Signer key custody and -> KMS](/ccip/ccv-starter-kit/how-to/signer-key-custody-and-kms). +> - RPC URLs that carry an API key go in the verifier EVM secret, not in values. Config rendered from values lands in +> a ConfigMap, which cannot hold a credential. +> - `keystoreBackend` and `kms.*` in values are ignored on these backends. Put the `[keystore]` block inside the +> `secrets.toml` you mount. See [Signer key custody and +> KMS](/ccip/ccv-starter-kit/how-to/signer-key-custody-and-kms). --- @@ -103011,6 +103281,11 @@ Source: https://docs.chain.link/ccip/evm/concepts/cross-chain-token/rate-limits/ Last Updated: 2025-06-09 > **NOTE: CCIP 2.0** +> +> 1. This section is written for `TokenPool` v2.0 contracts and their variants (for example, `BurnMintTokenPool`, +> `LockReleaseTokenPool`). +> 2. The same operational concepts apply to **v1.x token pools**; differences are called out inline as **v1.x pools** +> notes. CCIP rate limits are an operator-level control on token pools. They limit how much of a token can move across a specific CCIP lane over time, reducing blast radius during incidents and helping manage operational risk. @@ -103062,6 +103337,11 @@ Because many chains finalize blocks in batches, several transfers can become exe Rate limits are scoped per token pool, remote chain, bucket type, and direction. They do not pause CCIP globally. > **NOTE: Config changes and bucket refill** +> +> 1. **v2.0 pools:** Config changes immediately set available tokens to the new capacity (a full refill). +> 2. **v1.x pools:** After a config change, the bucket continues refilling at the **normal rate** rather +> than jumping to full capacity. Account for this when tuning limits during live traffic or when restoring service +> after a lockdown. ## Why rate limits exist @@ -103567,6 +103847,44 @@ leveraging the power of CCIP 2.0's infrastructure. - [Transporter UI (Testnet)](https://test.transporter.io/) - [Transporter UI (Mainnet)](https://www.transporter.io/) +### 1. Connect Your Wallet + +1. The Transporter UI supports most popular wallets, please make sure you have some funds on your desired chain in an account that is imported into one of these wallets. +2. Click the `Connect +` button on the top right of the Transporter UI to connect your wallet. + +![Chainlink CCIP Transporter Connect Wallet](/images/ccip/tutorials/getting-started/Transporter-ui-1.png) + +### 2. Select Lane and Approve Allowance + +1. Select the source and destination chains from the network dropdowns. +2. This tutorial uses the testnet version of the Transporter UI, your view may differ depending on the version of the UI you are using, and the types of lanes that are + available in the moment. + +![Chainlink CCIP Transporter Select Source and Destination Chains](/images/ccip/tutorials/getting-started/Transporter-ui-2.png) + +3. The UI might ask you to `Approve` an allowance on the amount of tokens that you are trying to bridge. We highly recommend users to stick to a one-time + allowance and not approve an unlimited allowance, unless the user explicitly intends to do so. + +![Chainlink CCIP Transporter Approve Allowance](/images/ccip/tutorials/getting-started/Transporter-ui-3.png) + +### 3. (Optional): Pay Gas Fee using LINK + +1. The UI allows you to pay for the transfer using `LINK` tokens, as shown in the screenshot attached. +2. If you select this option, the UI might ask for an additional approval of the `LINK` token, to pay for the gas fees. + +![Chainlink CCIP Transporter Pay Gas Fee using LINK](/images/ccip/tutorials/getting-started/Transporter-ui-4.png) + +### 4. Initiate Transfer + +1. After all the settings are configured, and allowances approved, click on the `Send` button to initiate the transfer. +2. Approve the transfer in the wallet popup. + +![Chainlink CCIP Transporter Initiate Transfer](/images/ccip/tutorials/getting-started/Transporter-ui-5.png) + +3. You can check out the status of the transfer in the `Activity` tab of the Transporter UI, or on the [CCIP Explorer](https://ccip.chain.link/). + +![Chainlink CCIP Transporter Check Transfer Status](/images/ccip/tutorials/getting-started/Transporter-ui-6.png) + ## Send and Receive Cross-Chain Messages Using CCIP ### Before you begin @@ -103587,2361 +103905,24635 @@ You will need: This section goes through the code for the `Sender` and `Receiver` contracts needed to complete the tutorial. We will use the same contracts for all three development environments. -### Send a cross-chain message using CCIP +#### 1. Sender code -Send and verify a cross-chain message using CCIP in under 10 minutes, with your favorite development framework. +The sender contract interacts with CCIP to send data cross-chain. Key elements are explained below. -#### Foundry +```sol +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; -Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. +import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {ExtraArgsCodec} from "@chainlink/contracts-ccip/contracts/libraries/ExtraArgsCodec.sol"; +import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol"; +import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; -#### Hardhat 3 +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ -Best for a `TypeScript`-based scripting workflow where you deploy contracts, send a CCIP message, and verify delivery from the command line. +/// @title - A simple contract for sending string data across chains. +contract Sender is OwnerIsCreator { + error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); -#### Remix + event MessageSent( + bytes32 indexed messageId, + uint64 indexed destinationChainSelector, + address receiver, + string text, + address feeToken, + uint256 fees + ); -Best for **Web3-native** workflows that prefer a browser-based IDE. + IRouterClient private s_router; + LinkTokenInterface private s_linkToken; -Once you understand basic transfers, most applications move to [programmable token transfers (PTT)](/ccip/evm/tutorials/application-developers/programmable-token-transfers) to combine value and execution. + /// @notice Constructor initializes the contract with the router address. + /// @param _router The address of the router contract. + /// @param _link The address of the LINK token contract. + constructor( + address _router, + address _link + ) { + s_router = IRouterClient(_router); + s_linkToken = LinkTokenInterface(_link); + } ---- + /// @notice Sends data to receiver on the destination chain. + /// @dev Assumes your contract has sufficient LINK to cover fees. + /// @param destinationChainSelector The identifier (aka selector) for the destination blockchain. + /// @param receiver The address of the recipient on the destination blockchain. + /// @param text The string text to be sent. + /// @return messageId The ID of the message that was sent. + function sendMessage( + uint64 destinationChainSelector, + address receiver, + string calldata text + ) external onlyOwner returns (bytes32 messageId) { + // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message + Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), // ABI-encoded receiver address + data: abi.encode(text), // ABI-encoded string + tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array — no tokens are being sent + extraArgs: ExtraArgsCodec._getBasicEncodedExtraArgsV3( + 200_000, // Gas limit for the callback on the destination chain + bytes4(0) // Default finality (wait for full finalization) + ), + feeToken: address(s_linkToken) // Pay CCIP fees in LINK + }); -# Tools Cheatsheet -Source: https://docs.chain.link/ccip/evm/getting-started/tools-cheatsheet -Last Updated: 2026-07-20 + // Get the fee required to send the message + uint256 fees = s_router.getFee(destinationChainSelector, evm2AnyMessage); -This page is an overview of the developer tools available for testing and integrating Chainlink CCIP. + if (fees > s_linkToken.balanceOf(address(this))) { + revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees); + } -## Starter Kits + // Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK + s_linkToken.approve(address(s_router), fees); -Convenient Github templates to get your app from 0 to 1 in the shortest time possible. + // Send the message through the router and store the returned message ID + messageId = s_router.ccipSend(destinationChainSelector, evm2AnyMessage); -## Follow along Repos + // Emit an event with message details + emit MessageSent(messageId, destinationChainSelector, receiver, text, address(s_linkToken), fees); -The following repos contain all the code that is referenced inside CCIP 2.0 EVM tutorials. Feel free to clone them and follow along with the tutorials. -The tutorials themselves are divided into two main categories: + // Return the message ID + return messageId; + } +} +``` -### Application Developers +##### Initializing the contract -For developers who want to build cross-chain applications on top of Chainlink CCIP. +When deploying the contract, you define the router address and the LINK contract address of the blockchain where you deploy. The router provides: -### Asset Issuers +- The `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee) to estimate CCIP fees. +- The `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend) to send CCIP messages. -For token issuers who want to create/manage their own token on Chainlink CCIP. +##### Sending data -## Transfer Tokens using CCIP +The `sendMessage` function: -You can use our Transporter UI to conveniently transfer tokens across chains using CCIP. +1. Constructs a CCIP message using the `EVM2AnyMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage): + - `receiver`: ABI-encoded destination address. + - `data`: ABI-encoded string payload. + - `tokenAmounts`: Empty array (no tokens sent). + - `extraArgs`: Encoded via `ExtraArgsCodec._getBasicEncodedExtraArgsV3` with a `gasLimit` of `200000` and default finality (`bytes4(0)`). + - `feeToken`: The LINK token address, indicating fees are paid in LINK. -### Mainnet +2. Computes the fees via the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee). -- [Transporter](https://www.transporter.io/): Transfer tokens across chains using CCIP, conveniently from a UI. +3. Verifies the contract's LINK balance covers the fees. -### Testnet +4. Approves the router to spend the required LINK. -- [Transporter](https://test.transporter.io/): Transfer tokens across chains using CCIP, conveniently from a UI. +5. Dispatches the message via the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend). -## Solidity Reference +> **CAUTION: Best Practices** +> +> This example is simplified for educational purposes. For production code, follow these best practices: +> +> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). +> +> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). +> +> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) +> +> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> +> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). -Onchain Solidity interfaces and contracts for integrating CCIP 2.0 directly from your smart contracts. +#### 2. Receiver code -- [Solidity Reference (v2.0.0)](/ccip/evm/api-reference/v2.0.0/overview): Complete API reference for CCIP v2.0.0 smart contracts, libraries, and interfaces on EVM. -- [Solidity Reference (v1.6.1)](/ccip/v1/evm/api-reference/v1.6.1): Complete API reference for CCIP v1.6.1 smart contracts, libraries, and interfaces on EVM. +The receiver contract interacts with CCIP to receive data on the destination chain. -## REST API +```sol +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; -Offchain REST API for querying message status and transfer data. +import {CCIPReceiver} from "@chainlink/contracts-ccip/contracts/applications/CCIPReceiver.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {FinalityCodec} from "@chainlink/contracts-ccip/contracts/libraries/FinalityCodec.sol"; -- [REST API](https://docs.chain.link/ccip/tools/api): Query CCIP message status, transfers, and other offchain data via HTTP endpoints. +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ -## TypeScript SDK +/// @title - A simple contract for receiving string data across chains. +contract Receiver is CCIPReceiver { + event MessageReceived(bytes32 indexed messageId, uint64 indexed sourceChainSelector, address sender, string text); -Programmatic CCIP interactions from TypeScript/JavaScript applications. + bytes32 private s_lastReceivedMessageId; + string private s_lastReceivedText; -- [TypeScript SDK](https://docs.chain.link/ccip/tools/sdk): Type-safe SDK for building CCIP integrations in Node.js and browser environments. + /// @notice Constructor initializes the contract with the router address. + /// @param router The address of the router contract. + constructor( + address router + ) CCIPReceiver(router) {} -## CLI + /// @notice Handle a received message. + function _ccipReceive( + Client.Any2EVMMessage memory any2EvmMessage + ) internal override { + s_lastReceivedMessageId = any2EvmMessage.messageId; + s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); -Command-line workflows and automation for CCIP operations. + emit MessageReceived( + any2EvmMessage.messageId, + any2EvmMessage.sourceChainSelector, + abi.decode(any2EvmMessage.sender, (address)), + abi.decode(any2EvmMessage.data, (string)) + ); + } -- [CLI](https://docs.chain.link/ccip/tools/cli): Automate CCIP tasks and integrate CCIP into scripts and CI/CD pipelines from the terminal. + /// @notice Returns the CCVs and finality config for a given source chain. + /// @dev Override to advertise receiver finality policy to the OffRamp. + function getCCVsAndFinalityConfig( + uint64, + bytes calldata + ) + external + view + override + returns ( + address[] memory requiredCCVs, + address[] memory optionalCCVs, + uint8 optionalThreshold, + bytes4 allowedFinalityConfig + ) + { + return (new address[](0), new address[](0), 0, FinalityCodec.WAIT_FOR_FINALITY_FLAG); + } ---- + /// @notice Fetches the details of the last received message. + /// @return messageId The ID of the last received message. + /// @return text The last received text. + function getLastReceivedMessageDetails() external view returns (bytes32 messageId, string memory text) { + return (s_lastReceivedMessageId, s_lastReceivedText); + } +} +``` -# CCIP Service Limits (EVM) -Source: https://docs.chain.link/ccip/evm/service-limits -Last Updated: 2025-05-19 +###### Initializing the contract -CCIP enforces service limits to maintain reliable and predictable cross-chain execution. -Limits are enforced during message processing and execution. Values vary by chain and deployment. +When you deploy the contract, you define the router address. The receiver inherits from [CCIPReceiver](/ccip/evm/api-reference/v2.0.0/ccip-receiver), which uses the router address. -## EVM Chains +###### Receiving data -| Item | Description | Limit | -| :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- | -| Maximum message `data` length | `data` payload sent within the [CCIP message](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage). | 32 kilobytes | -| Maximum number of tokens | Maximum number of token transfers (entries in the message's `tokenAmounts` array) a user can include in a single message. | 1 | -| Token Pool Execution Gas Limit | Maximum gas for executing the combined steps in token pools during cross-chain transfers, including (1) `balanceOf` check before minting/releasing, (2) `releaseOrMint` function, and (3) `balanceOf` check after minting/releasing. For more details on building custom token pools and handling gas constraints, refer to the [Token Pools documentation](/ccip/concepts/cross-chain-token/overview#token-pool-gas-requirements). | 90,000 | -| Message Execution Gas Limit | User-specified [gas limit](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2). | Varies by destination chain (see table below) | +On the destination blockchain: -### Message Execution Gas Limit by destination chain +1. The CCIP Router invokes `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive). This function is protected by the `onlyRouter` [modifier](/ccip/evm/api-reference/v2.0.0/ccip-receiver#onlyrouter-modifier), ensuring only the router can call it. -> These limits are for CCIP 2.0 lanes. For CCIP V1 lanes, see the [V1 service limits](/ccip/v1/evm/service-limits). +2. `ccipReceive` calls the internal `_ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#_ccipreceive). -| Destination chain | Gas limit | -| :---------------- | :-------- | -| 0G | 15M | -| Arbitrum | 15M | -| Avalanche | 15M | -| Base | 15M | -| BNB Chain | 15M | -| Ethereum | 15M | -| HyperEVM | 15M | -| Ink | 15M | -| Mantle | 15M | -| Plasma | 15M | -| Plume | 15M | -| Polygon | 15M | -| Unichain | 6M | +> **CAUTION: _ccipReceive** +> +> - The receiver contract must implement this function. - The CCIP router uses this function to deliver messages. - +> Messages will not be delivered if this function is not implemented correctly. -## Network-Specific Limitations +3. `_ccipReceive` receives an `Any2EVMMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#any2evmmessage) containing: + - The CCIP `messageId`. + - The `sourceChainSelector`. + - The `sender` address in bytes format, decoded via [abi.decode](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html). + - The `data` in bytes format, decoded to a `string`. -Some EVM networks have additional constraints beyond the standard limits above: +> **CAUTION: Recommendations for receiver contracts** +> +> This example is simplified for learning purposes. For production code: +> +> - Validate the source chain. +> - Depending on your use case, validate the sender address. +> +> The receiver inherits from [CCIPReceiver](/ccip/evm/api-reference/v2.0.0/ccip-receiver), which uses the `onlyRouter` [modifier](/ccip/evm/api-reference/v2.0.0/ccip-receiver#onlyrouter-modifier) to ensure only the router can call `ccipReceive`. -| Network | Special Considerations | Documentation | -| -------- | ------------------------------------------------- | ------------------------------------------------------------ | -| HyperEVM | Data availability limitations during RPC downtime | [HyperEVM Service Limits](/ccip/evm/service-limits/hyperevm) | +### Send a cross-chain message using CCIP -## Private / Permissioned Network Limitations +Send and verify a cross-chain message using CCIP in under 10 minutes, with your favorite development framework. -Some CCIP integrations are with private / permissioned networks. CCIP's integration with these networks depends on a single centralized RPC provider rather than a distinct set of local nodes or RPCs used by node operators. As a result: +#### Foundry -- Transaction data sourced from these networks cannot be independently verified across multiple nodes in the same way as public chains. -- A compromised RPC provider could manipulate transaction data, up to and including theft of bridged funds. -- Anomalous or malicious activity on these networks may be harder to detect and alert on, as monitoring infrastructure relies on the same private RPC layer. +Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. -Developers and integrators building CCIP integrations on private / permissioned networks should review these constraints carefully and implement additional safeguards for their use case. Safeguards include, but are not limited to, more conservative token pool rate limits, monitoring of messages and token transfers, and emergency preparedness. +## 1. Bootstrap a new Foundry project ---- +1. Open a new terminal in a directory of your choice and run this command to initialize a new Foundry project: -# Chainlink CCIP Network Specific Limits -Source: https://docs.chain.link/ccip/evm/service-limits/hyperevm -Last Updated: 2025-06-27 +```bash filename="Terminal" +forge init +``` -This page describes known network-specific limitations that may result from the design of a particular network. End users, application developers, blockchain development teams, token developers and others should read and understand these risks when considering interacting with these networks on CCIP. +2. Install the required dependencies: -## HyperEVM +```bash filename="Terminal" +forge install smartcontractkit/chainlink-ccip smartcontractkit/chainlink-evm +``` -**Risk**: -[Hyperliquid Data availability](/ccip/v1/service-responsibility#blockchain-development-team-responsibilities): If a HyperEVM RPC is out of sync or offline for 10+ minutes, missing logs from during the downtime are only available through a centralized resource that is hosted and maintained by Hyperliquid. In the event that logs related to specific CCIP messages are irrecoverable, those CCIP messages can become stuck or fail. +> **NOTE: Note** +> +> `cast wallet import` will throw an error if an `env` variable with the same name already exists. Use `cast wallet +> list` to check previously set variables. -**CCIP Mitigation**: -Hyperliquid has provided a workaround to serve logs through a [dedicated resource](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/raw-hyperevm-block-data) in case their network is inaccessible from RPC providers. CCIP is utilizing Hyperliquid's data source to backfill missing logs. In addition, CCIP Node Operators have implemented additional defense, using an multi-RPC approach, allowing for detection of out-of-sync or offline RPCs and an automatic fallback. If all configured RPCs are unavailable for a period of 10+ minutes AND the Hyperliquid data source does not provide them, logs may not be recoverable. +3. Use Foundry's `cast` command to create a new keystore for your `PRIVATE_KEY`: ---- +```bash filename="Terminal" +cast wallet import --interactive PRIVATE_KEY +``` -# HyperEVM Testnet RPC Guide -Source: https://docs.chain.link/ccip/evm/tools-resources/network-specific/hyperevm-testnet-rpc -Last Updated: 2025-10-08 +And use the `cast wallet list` command to verify: -> **CAUTION: Disclaimer** +![Foundry keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-2.png) + +> **CAUTION: Note** > -> This RPC is provided “as is” without warranties of any kind. Use is at your own risk, Chainlink Labs and its -> affiliates are not liable for any losses, damages, or interruptions arising from its use. The RPC may be modified or -> unavailable at any time, and users are solely responsible for their interactions and compliance with applicable laws. +> While the Hardhat project pulls RPC URLs via encrypted keystore variables, Foundry supports keystore encryption only +> for private keys — not generic strings. -To enhance the HyperEVM developer experience, we provide free public access to our professionally hosted Testnet RPC endpoint. Developers can use this endpoint to build Hyperliquid applications and configure it as a testnet RPC in browser wallets. For mainnet deployments, see available mainnet RPC URLs on the [Hyperliquid docs](https://hyperliquid.gitbook.io/hyperliquid-docs/builder-tools/hyperevm-tools). +4. Configure the remappings so that your `foundry.toml` file looks like this: -## RPC Endpoint Details +```toml filename="foundry.toml" -- **Network Name**: HyperEVM Testnet -- **RPC URL**: [https://rpcs.chain.link/hyperevm/testnet](https://rpcs.chain.link/hyperevm/testnet) -- **Chain ID**: 998 -- **Currency Symbol**: HYPE -- **Block Explorer URL (Optional)**: [https://testnet.purrsec.com/](https://testnet.purrsec.com/) +[profile.default] +solc = "0.8.34" +src = "src" +out = "out" +libs = ["lib"] -## For Developers: Building with the RPC +remappings = [ + "forge-std/=lib/forge-std/src/", + "@chainlink/contracts-ccip/contracts/=lib/chainlink-ccip/chains/evm/contracts/", + "@chainlink/contracts/=lib/chainlink-evm/contracts/", + "@openzeppelin/contracts@5.3.0/utils/introspection/=lib/forge-std/src/interfaces/" +] -You can use this endpoint in your dApps, scripts, or backend services with any standard Web3 library. +[rpc_endpoints] +sepolia = "ENTER_YOUR_SEPOLIA_RPC_URL_HERE" +arbitrumSepolia = "ENTER_YOUR_ARBITRUM_SEPOLIA_RPC_URL_HERE" +``` -### Ethers.js (JavaScript/TypeScript) +## 2. Set up the contracts -[Ethers.js](https://docs.ethers.org/v6/) is a library for interacting with Ethereum and EVM-compatible chains. +1. Create a new directory named `src` at the root of the project if it doesn't already exist. +2. Create a new file named `Sender.sol` in this directory and paste the [sender contract code](#sender-code) inside it. +3. Create a new file named `Receiver.sol` in the same directory and paste the [receiver contract code](#receiver-code) inside it. +4. Run the following command to compile the contracts: -```javascript -import { ethers } from "ethers" +```bash filename="Terminal" +forge build +``` -const rpcUrl = "https://rpcs.chain.link/hyperevm/testnet" -const provider = new ethers.JsonRpcProvider(rpcUrl) +## 3. Send a cross-chain message -async function getBlock() { - const blockNumber = await provider.getBlockNumber() - console.log("Current Block Number:", blockNumber) -} +1. Create a new directory named `script` at the root of the project if it doesn't already exist. +2. Create a new file named `SendCrossChainMessage.s.sol` in this directory and paste the following code inside it: -getBlock() -``` +```solidity filename="script/SendCrossChainMessage.s.sol" +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; -### Viem (JavaScript/TypeScript) +import {Script, console} from "forge-std/Script.sol"; +import {Sender} from "../src/Sender.sol"; +import {Receiver} from "../src/Receiver.sol"; +import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; -[Viem](https://viem.sh/) is a TypeScript library for interacting with Ethereum and EVM-compatible chains. It provides modular, type-safe APIs as an alternative to ethers.js. +contract SendCrossChainMessage is Script { + // Ethereum Sepolia configuration + address constant SEPOLIA_ROUTER = 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59; + address constant SEPOLIA_LINK = 0x779877A7B0D9E8603169DdbD7836e478b4624789; -```javascript -import { createPublicClient, http } from "viem" + // Arbitrum Sepolia configuration + address constant ARBITRUM_SEPOLIA_ROUTER = 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165; + uint64 constant ARBITRUM_SEPOLIA_CHAIN_SELECTOR = 3478487238524512106; -const rpcUrl = "https://rpcs.chain.link/hyperevm/testnet" + uint256 ONE_LINK = 1e18; -const client = createPublicClient({ - transport: http(rpcUrl), -}) + function run() public { -async function getBlock() { - const blockNumber = await client.getBlockNumber() - console.log("Current Block Number:", blockNumber) -} + // Load RPC configs from foundry.toml + uint256 sepoliaFork = vm.createFork(vm.rpcUrl("sepolia")); + uint256 arbitrumSepoliaFork = vm.createFork(vm.rpcUrl("arbitrumSepolia")); -getBlock() -``` + // Step 1: Deploy Sender on Sepolia + console.log("Connecting to Ethereum Sepolia..."); + vm.selectFork(sepoliaFork); + vm.startBroadcast(); -### Web3.py (Python) + console.log("\n[Step 1] Deploying Sender contract on Ethereum Sepolia..."); + Sender sender = new Sender(SEPOLIA_ROUTER, SEPOLIA_LINK); + console.log("Sender deployed on Sepolia:", address(sender)); + console.log( + string.concat( + "View on Etherscan: https://sepolia.etherscan.io/address/", + vm.toString(address(sender)) + ) + ); -[Web3.py](https://web3py.readthedocs.io/) is a Python library for interacting with EVM-compatible chains. + // Step 2: Fund Sender with 1 LINK + console.log("\n[Step 2] Funding Sender with 1 LINK..."); + LinkTokenInterface(SEPOLIA_LINK).transfer(address(sender), ONE_LINK); + vm.stopBroadcast(); + console.log("Funded Sender with 1 LINK"); -```python -from web3 import Web3 + // Step 3: Deploy Receiver on Arbitrum Sepolia + console.log("\nConnecting to Arbitrum Sepolia..."); + vm.selectFork(arbitrumSepoliaFork); + vm.startBroadcast(); -rpc_url = "https://rpcs.chain.link/hyperevm/testnet" -w3 = Web3(Web3.HTTPProvider(rpc_url)) + console.log("\n[Step 3] Deploying Receiver on Arbitrum Sepolia..."); + Receiver receiver = new Receiver(ARBITRUM_SEPOLIA_ROUTER); + vm.stopBroadcast(); + console.log("Receiver deployed on Arbitrum Sepolia:", address(receiver)); + console.log( + string.concat( + "View on Arbiscan: https://sepolia.arbiscan.io/address/", + vm.toString(address(receiver)) + ) + ); + console.log("\n .....Copy the receiver address for the verification script.....\n"); + console.log(address(receiver)); -if w3.is_connected(): - print("Connected to RPC!") - block_number = w3.eth.block_number - print("Current Block Number:", block_number) -else: - print("Failed to connect.") -``` + // Step 4: Send cross-chain message (Sepolia -> Arbitrum Sepolia) + vm.selectFork(sepoliaFork); + vm.startBroadcast(); -### cURL (Testing) + console.log("\n[Step 4] Sending cross-chain message from Sepolia to Arbitrum Sepolia..."); + bytes32 messageId = sender.sendMessage( + ARBITRUM_SEPOLIA_CHAIN_SELECTOR, + address(receiver), + "Hello World from Foundry script!" + ); + vm.stopBroadcast(); -```shell -curl -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - https://rpcs.chain.link/hyperevm/testnet + console.log("Message sent! Check for delivery after a few minutes..."); + console.log("CCIP messageId:"); + console.logBytes32(messageId); + console.log("View transaction status on CCIP Explorer: https://ccip.chain.link"); + } +} ``` -**Expected Response**: +3. Run the following command to send the cross-chain message: -```json -{ "jsonrpc": "2.0", "id": 1, "result": "0x1f92031" } +```bash filename="Terminal" +forge script script/SendCrossChainMessage.s.sol:SendCrossChainMessage --broadcast --multi --account PRIVATE_KEY ``` -## For Users: Adding to Your Wallet +> **NOTE** +> +> - The `--broadcast` flag broadcasts transactions to the network. +> - The `-vvvv` verbosity flag gives detailed runtime logs. +> - The `--multi` flag sends transactions to multiple chains. -You can easily add this network to browser extension wallets like [MetaMask](https://metamask.io/) or [Rabby](https://rabby.io/). +## 4. Verify message delivery -### Using MetaMask +1. Create a new file named `VerifyCrossChainMessage.s.sol` in the `script` directory and paste the following code inside it: -[MetaMask](https://metamask.io/) is a popular browser extension wallet for Ethereum and EVM-compatible chains. +```solidity filename="script/VerifyCrossChainMessage.s.sol" +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; -1. Open your MetaMask wallet. -2. Click the network selection dropdown at the top left. -3. Click "Add network". -4. At the bottom, select "Add a network manually". -5. Fill in the form with the network details: - - Network Name: HyperEVM Testnet - - RPC URL: [https://rpcs.chain.link/hyperevm/testnet](https://rpcs.chain.link/hyperevm/testnet) - - Chain ID: 998 - - Currency Symbol: HYPE - - Block Explorer URL (Optional): [https://testnet.purrsec.com/](https://testnet.purrsec.com/) -6. Click "Save". +import {Script, console} from "forge-std/Script.sol"; +import {Receiver} from "../src/Receiver.sol"; -### Using Rabby +contract VerifyCrossChainMessage is Script { -[Rabby](https://rabby.io/) is a browser extension wallet designed for multi-chain DeFi users with enhanced security features. + bytes32 constant ZERO_BYTES32 = bytes32(0); -1. Click on the "More" button, find "Add Custom Network". -2. Press "Add Custom Network", or the edit button if editing an existing connection. -3. Fill in the network details: - - Network Name: HyperEVM Testnet - - RPC URL: [https://rpcs.chain.link/hyperevm/testnet](https://rpcs.chain.link/hyperevm/testnet) - - Chain ID: 998 - - Currency Symbol: HYPE - - Block Explorer URL (Optional): [https://testnet.purrsec.com/](https://testnet.purrsec.com/) -4. Click "Confirm." + function run() public { -## Supported JSON-RPC Methods + address receiverAddress = PASTE_RECEIVER_ADDRESS_HERE; + require(receiverAddress != address(0), "Set RECEIVER_ADDRESS"); -| Method | Description | -| ---------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| net_version | Returns the current network ID (as a decimal string). | -| web3_clientVersion | Returns the client software version string. | -| eth_blockNumber | Returns the number of the most recent block (hex quantity). | -| eth_call | Executes a read-only call (no state change) against a contract. Only the latest block is supported. | -| eth_chainId | Returns the chain ID used for EIP-155 transaction signing (hex quantity). | -| eth_estimateGas | Estimates the gas required to execute a transaction/call. Only the latest block is supported. | -| eth_feeHistory | Returns historical base fees, priority fee rewards, and gas usage ratios for a range of recent blocks. | -| eth_gasPrice | Returns the node’s suggested gas price. Returns the base fee for the next small block. | -| eth_getBalance | Returns the account balance for an address. Only the latest block is supported. | -| eth_getBlockByHash | Returns block details by block hash; can include full transactions when requested. | -| eth_getBlockByNumber | Returns block details by block number; can include full transactions when requested. | -| eth_getBlockReceipts | Returns all transaction receipts for the specified block. | -| eth_getBlockTransactionCountByHash | Returns the number of transactions in a block, by block hash. | -| eth_getBlockTransactionCountByNumber | Returns the number of transactions in a block, by block number. | -| eth_getCode | Returns the EVM bytecode at an address. Only the latest block is supported. | -| eth_getLogs | Returns log entries that match a filter object. Up to 4 topics and up to 50 blocks in query range. | -| eth_getStorageAt | Returns the value from a storage slot at an address. Only the latest block is supported. | -| eth_getTransactionByBlockHashAndIndex | Returns a transaction from a block, by block hash and transaction index. | -| eth_getTransactionByBlockNumberAndIndex | Returns a transaction from a block, by block number and transaction index. | -| eth_getTransactionByHash | Returns a transaction by its hash. | -| eth_getTransactionCount | Returns the number of transactions sent from an address (the nonce). Only the latest block is supported. | -| eth_getTransactionReceipt | Returns the receipt of a transaction by hash (includes status, gas used, logs). | -| eth_maxPriorityFeePerGas | Returns the current max priority fee per gas (tip). Always returns zero currently. | -| eth_syncing | Reports node sync status. Always returns false. | + console.log("Connecting to Arbitrum Sepolia..."); + uint256 arbitrumSepoliaFork = vm.createFork(vm.rpcUrl("arbitrumSepolia")); + vm.selectFork(arbitrumSepoliaFork); -The following HyperEVM-specific endpoints are available: + console.log("Checking for received message...\n"); + Receiver receiver = Receiver(receiverAddress); -| Method | Description | -| ------------------------------ | ---------------------------------------------------------------------------------------------------------- | -| eth_bigBlockGasPrice | Returns the base fee for the next big block. | -| eth_usingBigBlocks | Returns whether the address is using big blocks. | -| eth_getSystemTxsByBlockHash | Similar to the "getTransaction" analogs but returns the system transactions that originate from HyperCore. | -| eth_getSystemTxsByBlockNumber | Similar to the "getTransaction" analogs but returns the system transactions that originate from HyperCore. | + (bytes32 messageId, string memory text) = receiver + .getLastReceivedMessageDetails(); -## Rate Limits + if (messageId == ZERO_BYTES32) { + console.log("No message received yet."); + console.log("Please wait a bit longer and try again."); + revert("No message received yet"); + } -The RPC endpoint allows 1,000 requests per IP address within a 5-minute moving window. + console.log("Received Message ID:"); + console.logBytes32(messageId); + console.log(string.concat('Received Text: "', text, '"')); + } +} +``` + +2. Run the following command to verify the cross-chain message: + +```bash filename="Terminal" +forge script script/VerifyCrossChainMessage.s.sol:VerifyCrossChainMessage +``` + +#### Hardhat 3 + +Best for a `TypeScript`-based scripting workflow where you deploy contracts, send a CCIP message, and verify delivery from the command line. + +## 1. Bootstrap a new Hardhat project + +1. Open a new terminal in a directory of your choice and run this command: + +```bash filename="Terminal" +npx hardhat --init +``` + +Create a project with the following options: + +- Hardhat Version: hardhat-3 +- Initialize project: At root of the project +- Type of project: A minimal Hardhat project +- Install the necessary dependencies: Yes + +2. Install the additional dependencies required by this tutorial: + +```bash filename="Terminal" +npm install @chainlink/contracts-ccip @chainlink/contracts viem +npm install --save-dev @nomicfoundation/hardhat-viem @nomicfoundation/hardhat-keystore +``` + +> **CAUTION: WE HIGHLY RECOMMEND NOT USING PLAINTEXT PRIVATE KEYS** +> +> 1. It is not recommended to store your sensitive keys in plaintext in `.env` files. +> 2. In this tutorial we use in-built keystores for both Hardhat and Foundry for their respective projects. + +3. Update `hardhat.config.ts` to use the `hardhat-viem` and `hardhat-keystore` plugins: + +```typescript filename="hardhat.config.ts" +import { configVariable, defineConfig } from "hardhat/config" +import hardhatKeystore from "@nomicfoundation/hardhat-keystore" +import hardhatViem from "@nomicfoundation/hardhat-viem" + +export default defineConfig({ + plugins: [hardhatViem, hardhatKeystore], + solidity: { + version: "0.8.34", + }, + networks: { + sepolia: { + type: "http", + url: configVariable("SEPOLIA_RPC_URL"), + accounts: [configVariable("PRIVATE_KEY")], + }, + arbitrumSepolia: { + type: "http", + url: configVariable("ARBITRUM_SEPOLIA_RPC_URL"), + accounts: [configVariable("PRIVATE_KEY")], + }, + }, +}) +``` + +4. Set the environment variables using `hardhat-keystore`. Run the following commands in succession — Hardhat will ask you to enter a password for the keystore for each variable: + +```bash filename="Terminal" +npx hardhat keystore set SEPOLIA_RPC_URL +npx hardhat keystore set ARBITRUM_SEPOLIA_RPC_URL +npx hardhat keystore set PRIVATE_KEY +``` + +The output of `npx hardhat keystore list` should look like this: + +![Hardhat keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-1.png) + +## 2. Set up the contracts + +1. Create a new directory named `contracts` for your smart contracts if it doesn't already exist. +2. Create a new file named `Sender.sol` in this directory and paste the [sender contract code](#sender-code) inside it. +3. Create a new file named `Receiver.sol` in the same directory and paste the [receiver contract code](#receiver-code) inside it. +4. Run the following command to compile the contracts: + +```bash filename="Terminal" +npx hardhat build +``` + +## 3. Send a cross-chain message + +1. Create a new directory named `scripts` at the root of the project if it doesn't already exist. +2. Create a new file named `send-cross-chain-message.ts` in this directory and paste the following code inside it: + +```typescript filename="scripts/send-cross-chain-message.ts" +import { network } from "hardhat" +import { getContract, parseAbi, parseUnits } from "viem" + +// Ethereum Sepolia configuration +const SEPOLIA_ROUTER = "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59" +const SEPOLIA_LINK = "0x779877A7B0D9E8603169DdbD7836e478b4624789" + +// Arbitrum Sepolia configuration +const ARBITRUM_SEPOLIA_ROUTER = "0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165" +const ARBITRUM_SEPOLIA_CHAIN_SELECTOR = 3478487238524512106n + +// Connect to Ethereum Sepolia +console.log("Connecting to Ethereum Sepolia...") +const sepoliaNetwork = await network.connect("sepolia") + +// Connect to Arbitrum Sepolia +console.log("Connecting to Arbitrum Sepolia...") +const arbitrumSepoliaNetwork = await network.connect("arbitrumSepolia") + +// Step 1: Deploy Sender on Sepolia +console.log("\n[Step 1] Deploying Sender contract on Ethereum Sepolia...") + +const sender = await sepoliaNetwork.viem.deployContract("Sender", [SEPOLIA_ROUTER, SEPOLIA_LINK]) +const sepoliaPublicClient = await sepoliaNetwork.viem.getPublicClient() + +console.log(`Sender deployed on Sepolia: ${sender.address}`) +console.log(`View on Etherscan: https://sepolia.etherscan.io/address/${sender.address}`) + +// Step 2: Fund Sender with LINK +console.log("\n[Step 2] Funding Sender with 1 LINK...") + +const [sepoliaWalletClient] = await sepoliaNetwork.viem.getWalletClients() +if (!sepoliaWalletClient) { + throw new Error("No wallet client available. Check PRIVATE_KEY + network config in hardhat.config.ts.") +} + +const linkTokenInterfaceAbi = parseAbi(["function transfer(address to, uint256 value) returns (bool)"]) + +const link = getContract({ + address: SEPOLIA_LINK, + abi: linkTokenInterfaceAbi, + client: { public: sepoliaPublicClient, wallet: sepoliaWalletClient }, +}) + +const transferLinkTx = await link.write.transfer([sender.address, parseUnits("1", 18)]) + +console.log("LINK token transfer in progress, awaiting confirmation...") +await sepoliaPublicClient.waitForTransactionReceipt({ hash: transferLinkTx, confirmations: 1 }) +console.log("Funded Sender with 1 LINK") + +// Step 3: Deploy Receiver on Arbitrum Sepolia +console.log("\n[Step 3] Deploying Receiver on Arbitrum Sepolia...") + +const receiver = await arbitrumSepoliaNetwork.viem.deployContract("Receiver", [ARBITRUM_SEPOLIA_ROUTER]) +const arbitrumSepoliaPublicClient = await arbitrumSepoliaNetwork.viem.getPublicClient() + +console.log(`Receiver deployed on Arbitrum Sepolia: ${receiver.address}`) +console.log(`View on Arbiscan: https://sepolia.arbiscan.io/address/${receiver.address}`) +console.log(`\n📋 Copy the receiver address since it will be needed to run the verification script 📋\n`) + +// Step 4: Send cross-chain message +console.log("\n[Step 4] Sending cross-chain message...") + +const sendMessageTx = await sender.write.sendMessage([ + ARBITRUM_SEPOLIA_CHAIN_SELECTOR, + receiver.address, + "Hello World from Hardhat script!", +]) + +console.log("Cross-chain message sent, awaiting confirmation...") +console.log(`Message sent! ✅\nTx hash: ${sendMessageTx}`) +console.log(`View transaction status on CCIP Explorer: https://ccip.chain.link`) +console.log("Run the verification script after a few minutes to check if the message has been received.") +``` + +3. Run the following command to send the cross-chain message: + +```bash filename="Terminal" +npx hardhat run scripts/send-cross-chain-message.ts +``` + +## 4. Verify message delivery + +1. Wait for a few minutes for the message to be delivered to the receiver contract. + +2. Create a new file named `verify-cross-chain-message.ts` in the `scripts` directory and paste the following code inside it: + +> **NOTE: Paste the Receiver contract address here** +> +> The verification script calls `getLastReceivedMessageDetails` on the receiver contract. It needs the correct address. + +```typescript filename="scripts/verify-cross-chain-message.ts" +import { network } from "hardhat" + +// Paste the Receiver contract address +const RECEIVER_ADDRESS = "" + +console.log("Connecting to Arbitrum Sepolia...") +const arbitrumSepoliaNetwork = await network.connect("arbitrumSepolia") + +console.log("Checking for received message...\n") +const receiver = await arbitrumSepoliaNetwork.viem.getContractAt("Receiver", RECEIVER_ADDRESS) + +const [messageId, text] = await receiver.read.getLastReceivedMessageDetails() + +const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000" + +if (messageId === ZERO_BYTES32) { + console.log("No message received yet.") + console.log("Please wait a bit longer and try again.") + process.exit(1) +} else { + console.log(`✅ Message ID: ${messageId}`) + console.log(`Text: "${text}"`) +} +``` + +3. Run the following command to verify the cross-chain message: + +```bash filename="Terminal" +npx hardhat run scripts/verify-cross-chain-message.ts +``` + +4. You should see the message ID and text of the last received message printed in the terminal. + +#### Remix + +Best for **Web3-native** workflows that prefer a browser-based IDE. + +## 1. Deploy the sender contract + +Deploy the `Sender.sol` contract on *Ethereum Sepolia*. To see a detailed explanation of this contract, read the [Sender code](#sender-code) section. + +1. Open the Sender.sol contract in Remix. + +2. Compile the contract. + +3. Deploy the sender contract on *Ethereum Sepolia*: + 1. Open MetaMask and select the *Ethereum Sepolia* network. + + 2. In Remix under the **Deploy & Run Transactions** tab, select *Injected Provider - MetaMask* in the **Environment** list. Remix will use the MetaMask wallet to communicate with *Ethereum Sepolia*. + + 3. Under the **Deploy** section, fill in the router address and the LINK token contract addresses. You can find both on the [CCIP Directory](/ccip/directory). For *Ethereum Sepolia*, the router address is 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 and the LINK address is 0x779877A7B0D9E8603169DdbD7836e478b4624789. + + ![Chainlink CCIP deploy sender Ethereum Sepolia](/images/ccip/tutorials/deploy-sender-avalanche-fuji.webp) + + 4. Click the **transact** button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to *Ethereum Sepolia*. + + 5. After you confirm the transaction, the contract address appears in the **Deployed Contracts** list. Copy your contract address. + + ![Chainlink CCIP deployed sender Ethereum Sepolia](/images/ccip/tutorials/deployed-sender-avalanche-fuji.webp) + + 6. Open MetaMask and send 1 LINK to the contract address that you copied. Your contract will pay CCIP fees in LINK. + +## 2. Deploy the receiver contract + +Deploy the receiver contract on *Arbitrum Sepolia*. You will use this contract to receive data from the sender on *Ethereum Sepolia*. To see a detailed explanation of this contract, read the [Receiver code](#receiver-code) section. + +1. [Open the Receiver.sol](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/Receiver.sol) contract in Remix. + + [Open Receiver.sol in Remix](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/Receiver.sol) + +2. Compile the contract. + +3. Deploy the receiver contract on *Arbitrum Sepolia*: + 1. Open MetaMask and select the *Arbitrum Sepolia* network. + + 2. In Remix under the **Deploy & Run Transactions** tab, make sure the **Environment** is still set to *Injected Provider - MetaMask*. + + 3. Under the **Deploy** section, fill in the router address field. For *Arbitrum Sepolia*, the Router address is 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165. You can find the addresses for each network on the [CCIP Directory](/ccip/directory). + + ![Chainlink CCIP Deploy receiver Arbitrum Sepolia](/images/ccip/tutorials/deploy-receiver-sepolia.webp) + + 4. Click the **Deploy** button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to *Arbitrum Sepolia*. + + 5. After you confirm the transaction, the contract address appears as the second item in the **Deployed Contracts** list. Copy this contract address. + + ![Chainlink CCIP deployed receiver Arbitrum Sepolia](/images/ccip/tutorials/deployed-receiver-sepolia.webp) + +You now have one *sender* contract on *Ethereum Sepolia* and one *receiver* contract on *Arbitrum Sepolia*. You sent `1` LINK to the *sender* contract to pay the CCIP fees. Next, send data from the sender contract to the receiver contract. + +## 3. Send data + +Send a `Hello World!` string from your contract on *Ethereum Sepolia* to the contract you deployed on *Arbitrum Sepolia*: + +1. Open MetaMask and select the *Ethereum Sepolia* network. + +2. In Remix under the **Deploy & Run Transactions** tab, expand the first contract in the **Deployed Contracts** section. + +3. Expand the **sendMessage** function and fill in the following arguments: + + | Argument | Description | Value (*Arbitrum Sepolia*) | + | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | + | destinationChainSelector | CCIP Chain identifier of the target blockchain. You can find each network's chain selector on the [CCIP Directory](/ccip/directory) | 3478487238524512106 | + | receiver | The destination smart contract address | Your deployed contract address | + | text | Any `string` | Hello World! | + + ![Chainlink CCIP Sepolia send message](/images/ccip/tutorials/fuji-sendmessage.webp) + +4. Click the **transact** button to run the function. MetaMask prompts you to confirm the transaction. + +> **NOTE: Gas price spikes** +> +> Under normal circumstances, transactions on the Ethereum Sepolia network require significantly fewer tokens to pay for gas. However, during exceptional periods of high gas price spikes, your transactions may fail if not sufficiently funded. In such cases, you may need to fund your contract with additional tokens. We recommend paying for your CCIP transactions in **LINK** tokens (rather than native tokens) as you can obtain extra LINK testnet tokens from [faucets.chain.link](https://faucets.chain.link/). If you encounter a transaction failure due to these gas price spikes, please add additional LINK tokens to your contract and try again. +> Alternatively, you can use a supported testnet other than Sepolia. + +5. After the transaction is successful, note the transaction hash. Here is an [example](https://sepolia.etherscan.io/tx/0x113933ec9f1b2e795a1e2f564c9d452db92d3e9a150545712687eb546916e633) of a successful transaction on *Ethereum Sepolia*. + +After the transaction is finalized on the source chain, it will take a few minutes for CCIP to deliver the data to *Arbitrum Sepolia* and call the `ccipReceive` function on your receiver contract. You can use the [CCIP explorer](https://ccip.chain.link/) to see the status of your CCIP transaction and then read data stored by your receiver contract. + +6. Open the [CCIP explorer](https://ccip.chain.link/) and use the transaction hash that you copied to search for your cross-chain transaction. The explorer provides several details about your request. + + ![Chainlink CCIP Explorer transaction details](/images/ccip/tutorials/ccip-explorer-tx-details.webp) + +7. When the status of the transaction is marked with a "Success" status, the CCIP transaction and the destination transaction are complete. + + ![Chainlink CCIP Explorer transaction details success](/images/ccip/tutorials/ccip-explorer-tx-details-success.webp) + +## 4. Read data + +Read data stored by the receiver contract on *Arbitrum Sepolia*: + +1. Open MetaMask and select the *Arbitrum Sepolia* network. +2. In Remix under the **Deploy & Run Transactions** tab, expand the receiver contract deployed on *Arbitrum Sepolia*. +3. Click the **getLastReceivedMessageDetails** function button to read the stored data. In this example, it should be "Hello World!". + + ![Chainlink CCIP Arbitrum Sepolia message details](/images/ccip/tutorials/sepolia-getmessagedetails.webp) + +Congratulations! You just sent your first cross-chain data using CCIP 2.0. Next, examine the example code to learn how this contract works. + +Once you understand basic transfers, most applications move to [programmable token transfers (PTT)](/ccip/evm/tutorials/application-developers/programmable-token-transfers) to combine value and execution. + +--- + +# Tools Cheatsheet +Source: https://docs.chain.link/ccip/evm/getting-started/tools-cheatsheet +Last Updated: 2026-07-20 + +This page is an overview of the developer tools available for testing and integrating Chainlink CCIP. + +## Starter Kits + +Convenient Github templates to get your app from 0 to 1 in the shortest time possible. + +## Follow along Repos + +The following repos contain all the code that is referenced inside CCIP 2.0 EVM tutorials. Feel free to clone them and follow along with the tutorials. +The tutorials themselves are divided into two main categories: + +### Application Developers + +For developers who want to build cross-chain applications on top of Chainlink CCIP. + +[CCIP Tutorials](https://github.com/smartcontractkit/docs-ccip) + +CCIP tutorials for application developers using Hardhat and Foundry. + +### Asset Issuers + +For token issuers who want to create/manage their own token on Chainlink CCIP. + +## Transfer Tokens using CCIP + +You can use our Transporter UI to conveniently transfer tokens across chains using CCIP. + +### Mainnet + +- [Transporter](https://www.transporter.io/): Transfer tokens across chains using CCIP, conveniently from a UI. + +### Testnet + +- [Transporter](https://test.transporter.io/): Transfer tokens across chains using CCIP, conveniently from a UI. + +## Solidity Reference + +Onchain Solidity interfaces and contracts for integrating CCIP 2.0 directly from your smart contracts. + +- [Solidity Reference (v2.0.0)](/ccip/evm/api-reference/v2.0.0/overview): Complete API reference for CCIP v2.0.0 smart contracts, libraries, and interfaces on EVM. +- [Solidity Reference (v1.6.1)](/ccip/v1/evm/api-reference/v1.6.1): Complete API reference for CCIP v1.6.1 smart contracts, libraries, and interfaces on EVM. + +## REST API + +Offchain REST API for querying message status and transfer data. + +- [REST API](https://docs.chain.link/ccip/tools/api): Query CCIP message status, transfers, and other offchain data via HTTP endpoints. + +## TypeScript SDK + +Programmatic CCIP interactions from TypeScript/JavaScript applications. + +- [TypeScript SDK](https://docs.chain.link/ccip/tools/sdk): Type-safe SDK for building CCIP integrations in Node.js and browser environments. + +## CLI + +Command-line workflows and automation for CCIP operations. + +- [CLI](https://docs.chain.link/ccip/tools/cli): Automate CCIP tasks and integrate CCIP into scripts and CI/CD pipelines from the terminal. + +--- + +# CCIP Service Limits (EVM) +Source: https://docs.chain.link/ccip/evm/service-limits +Last Updated: 2025-05-19 + +CCIP enforces service limits to maintain reliable and predictable cross-chain execution. +Limits are enforced during message processing and execution. Values vary by chain and deployment. + +## EVM Chains + +| Item | Description | Limit | +| :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- | +| Maximum message `data` length | `data` payload sent within the [CCIP message](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage). | 32 kilobytes | +| Maximum number of tokens | Maximum number of token transfers (entries in the message's `tokenAmounts` array) a user can include in a single message. | 1 | +| Token Pool Execution Gas Limit | Maximum gas for executing the combined steps in token pools during cross-chain transfers, including (1) `balanceOf` check before minting/releasing, (2) `releaseOrMint` function, and (3) `balanceOf` check after minting/releasing. For more details on building custom token pools and handling gas constraints, refer to the [Token Pools documentation](/ccip/concepts/cross-chain-token/overview#token-pool-gas-requirements). | 90,000 | +| Message Execution Gas Limit | User-specified [gas limit](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2). | Varies by destination chain (see table below) | + +### Message Execution Gas Limit by destination chain + +> These limits are for CCIP 2.0 lanes. For CCIP V1 lanes, see the [V1 service limits](/ccip/v1/evm/service-limits). + +| Destination chain | Gas limit | +| :---------------- | :-------- | +| 0G | 15M | +| Arbitrum | 15M | +| Avalanche | 15M | +| Base | 15M | +| BNB Chain | 15M | +| Ethereum | 15M | +| HyperEVM | 15M | +| Ink | 15M | +| Mantle | 15M | +| Plasma | 15M | +| Plume | 15M | +| Polygon | 15M | +| Unichain | 6M | + +## Network-Specific Limitations + +Some EVM networks have additional constraints beyond the standard limits above: + +| Network | Special Considerations | Documentation | +| -------- | ------------------------------------------------- | ------------------------------------------------------------ | +| HyperEVM | Data availability limitations during RPC downtime | [HyperEVM Service Limits](/ccip/evm/service-limits/hyperevm) | + +## Private / Permissioned Network Limitations + +Some CCIP integrations are with private / permissioned networks. CCIP's integration with these networks depends on a single centralized RPC provider rather than a distinct set of local nodes or RPCs used by node operators. As a result: + +- Transaction data sourced from these networks cannot be independently verified across multiple nodes in the same way as public chains. +- A compromised RPC provider could manipulate transaction data, up to and including theft of bridged funds. +- Anomalous or malicious activity on these networks may be harder to detect and alert on, as monitoring infrastructure relies on the same private RPC layer. + +Developers and integrators building CCIP integrations on private / permissioned networks should review these constraints carefully and implement additional safeguards for their use case. Safeguards include, but are not limited to, more conservative token pool rate limits, monitoring of messages and token transfers, and emergency preparedness. + +--- + +# Chainlink CCIP Network Specific Limits +Source: https://docs.chain.link/ccip/evm/service-limits/hyperevm +Last Updated: 2025-06-27 + +This page describes known network-specific limitations that may result from the design of a particular network. End users, application developers, blockchain development teams, token developers and others should read and understand these risks when considering interacting with these networks on CCIP. + +## HyperEVM + +**Risk**: +[Hyperliquid Data availability](/ccip/v1/service-responsibility#blockchain-development-team-responsibilities): If a HyperEVM RPC is out of sync or offline for 10+ minutes, missing logs from during the downtime are only available through a centralized resource that is hosted and maintained by Hyperliquid. In the event that logs related to specific CCIP messages are irrecoverable, those CCIP messages can become stuck or fail. + +**CCIP Mitigation**: +Hyperliquid has provided a workaround to serve logs through a [dedicated resource](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/raw-hyperevm-block-data) in case their network is inaccessible from RPC providers. CCIP is utilizing Hyperliquid's data source to backfill missing logs. In addition, CCIP Node Operators have implemented additional defense, using an multi-RPC approach, allowing for detection of out-of-sync or offline RPCs and an automatic fallback. If all configured RPCs are unavailable for a period of 10+ minutes AND the Hyperliquid data source does not provide them, logs may not be recoverable. + +--- + +# HyperEVM Testnet RPC Guide +Source: https://docs.chain.link/ccip/evm/tools-resources/network-specific/hyperevm-testnet-rpc +Last Updated: 2025-10-08 + +> **CAUTION: Disclaimer** +> +> This RPC is provided “as is” without warranties of any kind. Use is at your own risk, Chainlink Labs and its +> affiliates are not liable for any losses, damages, or interruptions arising from its use. The RPC may be modified or +> unavailable at any time, and users are solely responsible for their interactions and compliance with applicable laws. + +To enhance the HyperEVM developer experience, we provide free public access to our professionally hosted Testnet RPC endpoint. Developers can use this endpoint to build Hyperliquid applications and configure it as a testnet RPC in browser wallets. For mainnet deployments, see available mainnet RPC URLs on the [Hyperliquid docs](https://hyperliquid.gitbook.io/hyperliquid-docs/builder-tools/hyperevm-tools). + +## RPC Endpoint Details + +- **Network Name**: HyperEVM Testnet +- **RPC URL**: [https://rpcs.chain.link/hyperevm/testnet](https://rpcs.chain.link/hyperevm/testnet) +- **Chain ID**: 998 +- **Currency Symbol**: HYPE +- **Block Explorer URL (Optional)**: [https://testnet.purrsec.com/](https://testnet.purrsec.com/) + +## For Developers: Building with the RPC + +You can use this endpoint in your dApps, scripts, or backend services with any standard Web3 library. + +### Ethers.js (JavaScript/TypeScript) + +[Ethers.js](https://docs.ethers.org/v6/) is a library for interacting with Ethereum and EVM-compatible chains. + +```javascript +import { ethers } from "ethers" + +const rpcUrl = "https://rpcs.chain.link/hyperevm/testnet" +const provider = new ethers.JsonRpcProvider(rpcUrl) + +async function getBlock() { + const blockNumber = await provider.getBlockNumber() + console.log("Current Block Number:", blockNumber) +} + +getBlock() +``` + +### Viem (JavaScript/TypeScript) + +[Viem](https://viem.sh/) is a TypeScript library for interacting with Ethereum and EVM-compatible chains. It provides modular, type-safe APIs as an alternative to ethers.js. + +```javascript +import { createPublicClient, http } from "viem" + +const rpcUrl = "https://rpcs.chain.link/hyperevm/testnet" + +const client = createPublicClient({ + transport: http(rpcUrl), +}) + +async function getBlock() { + const blockNumber = await client.getBlockNumber() + console.log("Current Block Number:", blockNumber) +} + +getBlock() +``` + +### Web3.py (Python) + +[Web3.py](https://web3py.readthedocs.io/) is a Python library for interacting with EVM-compatible chains. + +```python +from web3 import Web3 + +rpc_url = "https://rpcs.chain.link/hyperevm/testnet" +w3 = Web3(Web3.HTTPProvider(rpc_url)) + +if w3.is_connected(): + print("Connected to RPC!") + block_number = w3.eth.block_number + print("Current Block Number:", block_number) +else: + print("Failed to connect.") +``` + +### cURL (Testing) + +```shell +curl -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + https://rpcs.chain.link/hyperevm/testnet +``` + +**Expected Response**: + +```json +{ "jsonrpc": "2.0", "id": 1, "result": "0x1f92031" } +``` + +## For Users: Adding to Your Wallet + +You can easily add this network to browser extension wallets like [MetaMask](https://metamask.io/) or [Rabby](https://rabby.io/). + +### Using MetaMask + +[MetaMask](https://metamask.io/) is a popular browser extension wallet for Ethereum and EVM-compatible chains. + +1. Open your MetaMask wallet. +2. Click the network selection dropdown at the top left. +3. Click "Add network". +4. At the bottom, select "Add a network manually". +5. Fill in the form with the network details: + - Network Name: HyperEVM Testnet + - RPC URL: [https://rpcs.chain.link/hyperevm/testnet](https://rpcs.chain.link/hyperevm/testnet) + - Chain ID: 998 + - Currency Symbol: HYPE + - Block Explorer URL (Optional): [https://testnet.purrsec.com/](https://testnet.purrsec.com/) +6. Click "Save". + +### Using Rabby + +[Rabby](https://rabby.io/) is a browser extension wallet designed for multi-chain DeFi users with enhanced security features. + +1. Click on the "More" button, find "Add Custom Network". +2. Press "Add Custom Network", or the edit button if editing an existing connection. +3. Fill in the network details: + - Network Name: HyperEVM Testnet + - RPC URL: [https://rpcs.chain.link/hyperevm/testnet](https://rpcs.chain.link/hyperevm/testnet) + - Chain ID: 998 + - Currency Symbol: HYPE + - Block Explorer URL (Optional): [https://testnet.purrsec.com/](https://testnet.purrsec.com/) +4. Click "Confirm." + +## Supported JSON-RPC Methods + +| Method | Description | +| ---------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| net_version | Returns the current network ID (as a decimal string). | +| web3_clientVersion | Returns the client software version string. | +| eth_blockNumber | Returns the number of the most recent block (hex quantity). | +| eth_call | Executes a read-only call (no state change) against a contract. Only the latest block is supported. | +| eth_chainId | Returns the chain ID used for EIP-155 transaction signing (hex quantity). | +| eth_estimateGas | Estimates the gas required to execute a transaction/call. Only the latest block is supported. | +| eth_feeHistory | Returns historical base fees, priority fee rewards, and gas usage ratios for a range of recent blocks. | +| eth_gasPrice | Returns the node’s suggested gas price. Returns the base fee for the next small block. | +| eth_getBalance | Returns the account balance for an address. Only the latest block is supported. | +| eth_getBlockByHash | Returns block details by block hash; can include full transactions when requested. | +| eth_getBlockByNumber | Returns block details by block number; can include full transactions when requested. | +| eth_getBlockReceipts | Returns all transaction receipts for the specified block. | +| eth_getBlockTransactionCountByHash | Returns the number of transactions in a block, by block hash. | +| eth_getBlockTransactionCountByNumber | Returns the number of transactions in a block, by block number. | +| eth_getCode | Returns the EVM bytecode at an address. Only the latest block is supported. | +| eth_getLogs | Returns log entries that match a filter object. Up to 4 topics and up to 50 blocks in query range. | +| eth_getStorageAt | Returns the value from a storage slot at an address. Only the latest block is supported. | +| eth_getTransactionByBlockHashAndIndex | Returns a transaction from a block, by block hash and transaction index. | +| eth_getTransactionByBlockNumberAndIndex | Returns a transaction from a block, by block number and transaction index. | +| eth_getTransactionByHash | Returns a transaction by its hash. | +| eth_getTransactionCount | Returns the number of transactions sent from an address (the nonce). Only the latest block is supported. | +| eth_getTransactionReceipt | Returns the receipt of a transaction by hash (includes status, gas used, logs). | +| eth_maxPriorityFeePerGas | Returns the current max priority fee per gas (tip). Always returns zero currently. | +| eth_syncing | Reports node sync status. Always returns false. | + +The following HyperEVM-specific endpoints are available: + +| Method | Description | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------- | +| eth_bigBlockGasPrice | Returns the base fee for the next big block. | +| eth_usingBigBlocks | Returns whether the address is using big blocks. | +| eth_getSystemTxsByBlockHash | Similar to the "getTransaction" analogs but returns the system transactions that originate from HyperCore. | +| eth_getSystemTxsByBlockNumber | Similar to the "getTransaction" analogs but returns the system transactions that originate from HyperCore. | + +## Rate Limits + +The RPC endpoint allows 1,000 requests per IP address within a 5-minute moving window. + +--- + +# Hyperliquid Integration Guide +Source: https://docs.chain.link/ccip/evm/tools-resources/network-specific/hyperliquid-integration-guide + +CCIP is fully compatible with Hyperliquid, allowing multichain tokens to be eligible for trading on the Hyperliquid decentralized exchange. This guide will provide comprehensive information about Hyperliquid for CCIP users who want to get enabled on Hyperliquid. + +> **NOTE: Enable Your CCIP Token on Hyperliquid** +> +> We currently offer white glove support to validate your configuration and automatically execute the process for CCIP +> users who are ready to deploy to Hyperliquid. Please contact us +> [here](https://chain.link/ccip-contact?v=General%20Technical%20Support) to schedule a deployment and stay tuned for +> enhanced tooling to further simplify this process for CCIP users. + +## How Hyperliquid Works + +Hyperliquid is an emerging app-chain that powers a popular decentralized exchange with over $4B USDC bridged in. Hyperliquid operates on a dual network system made up of HyperCore and HyperEVM which are fully integrated together to compose the general Hyperliquid State. + +- HyperCore is the network where high-speed trading takes place at up to 200k orders per second, secured by HyperBFT Consensus. Trading occurs through a Central Limit Order Book (CLOB) which is different from most decentralized exchange liquidity pool structures. +- HyperEVM is where the supporting infrastructure and contracts are deployed, such as the MintAndBurnERC20 tokens deployed as a part of a CCIP implementation. There is a growing DeFi ecosystem including Money Markets, LSTs and DEXes on HyperEVM. + +| | HyperCore | HyperEVM | +| :------------------: | :-----------------------------------------------------------------: | :----------------------------------------------------: | +| **Primary Function** | High-speed decentralized exchange trading via an onchain order book | Supporting infrastructure and external smart contracts | +| **Token Type** | HIP-1 tokens | ERC20 tokens | +| **Deployment** | Requires winning a spot slot auction | Standard ERC20 deployment | +| **Interaction** | Posting “signed actions” through a Rest API Endpoint | Standard RPC Methods | + +### HyperCore + +HyperCore is a bespoke L1 blockchain designed to deliver low latency, high throughput, and instant finality for the use case of high speed trading of spot assets and perpetual futures. This enables a user experience and market dynamic that feels like a centralized exchange. Users and developers interact with HyperCore by posting signed actions to the [HyperCore API](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api). + +#### HIP-1 Token Standard + +The native token standard for spot trading on HyperCore is called [HIP-1](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/hip-1-native-token-standard). HIP-1 tokens are fungible and have a fixed supply, which must be declared at the time of initialization. + +To deploy a HIP-1 token, the issuer must first win a Dutch auction for a spot slot—a permissioned right to enable a token for trading on Hyperliquid. These auctions are held every 31 hours and typically require a winning bid of tens of thousands of dollars worth of HYPE. + +Once a spot slot is secured, the owner can proceed with a defined deployment flow, including initializing the token supply and registering the trading pair—as outlined in the steps below. + +### HyperEVM + +HyperEVM is largely similar to any other EVM network, allowing standard development tooling to be used and similar smart contract design patterns. The main differences lie in the Dual Block Size architecture and the access to the HyperCore state via read precompiles. + +#### Dual Block Architecture + +The HyperEVM blockchain operates using a [dual block architecture, consisting of small, fast blocks and large, slow blocks](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/dual-block-architecture): + +- **Small blocks (3M gas limit)** are produced every 1 second and handle typical user transactions. + +- **Large blocks (30M gas limit)** are produced every 1 minute and are used for more complex operations such as contract deployments. + +Users can control which block type their transaction is included in by toggling their block size setting. This is done by submitting a signed action—a type of authenticated message—to a specific endpoint on the HyperCore API. + +For example, when deploying a CCIP token on HyperEVM, a developer must first switch to large blocks by submitting the appropriate action to the API. This ensures the transaction has sufficient gas capacity to succeed within a large block. + +```json +{ "type": "evmUserModify", "usingBigBlocks": true } +``` + +#### Read Precompiles + +Smart contracts on HyperEVM can access data from HyperCore by calling special system-level addresses known as [precompiles](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/interacting-with-hypercore). These addresses don't correspond to regular smart contracts; instead, they trigger native code execution on the validator node. + +For example, when a contract on HyperEVM queries a token balance by calling a designated read precompile, it bypasses standard EVM execution. The call is handled directly by the validator, which fetches the requested data from HyperCore in real time. This mechanism provides secure, gas-efficient, read-only access to the core exchange state — including data such as order book balances, positions, and other protocol-level metrics. + +This design enables the development of onchain applications that can respond to the live state of the central limit order book on HyperCore. The same precompile infrastructure also powers cross-chain bridging, allowing seamless integration between ERC-20 tokens on HyperEVM and their linked HIP-1 counterparts on HyperCore. + +In the following example snippet, a smart contract on HyperEVM can trustlessly read the balance of an HIP1 token on HyperCore. + +```solidity +address constant SPOT_BALANCE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801; + struct SpotBalance { + uint64 total; + uint64 hold; + uint64 entryNtl; + } +function spotBalance(address user, uint64 token) external view returns (SpotBalance memory) { + bool success; + bytes memory result; + (success, result) = SPOT_BALANCE_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, token)); + require(success, "SpotBalance precompile call failed"); + return abi.decode(result, (SpotBalance)); + } +``` + +## Integration Steps + +ERC-20 tokens deployed on HyperEVM can be linked to HIP-1 tokens on HyperCore by the same EOA that deployed the ERC-20 contract. Once linked, users can seamlessly convert their ERC-20 tokens on HyperEVM into the corresponding HIP-1 tokens on HyperCore using a native Lock-and-Release mechanism enabled by the dual-network architecture. + +Each HIP-1 token is assigned a unique token index, which serves two purposes: + +- It is used in API calls to identify the token. + +- It is used to derive the system address that acts as a bridge for transfers between HyperEVM and HyperCore. + +For example, the system address for token index 1385 is: +`0x2000000000000000000000000000000000000569`. + +### Deploying CCIP Tokens on Hyperliquid + +To make a CCIP token tradable on Hyperliquid, follow these high-level steps: + +1. **Secure Spot Slot via Auction**\ + Use the Hyperliquid UI to win the spot slot auction and secure the right to deploy a HIP-1 token. + +2. **Deploy Token on HyperEVM** + - Deploy your CCIP token to HyperEVM. + - In order to link the HIP-1 token on core to the deployed token on HyperEVM, ensure the token is deployed by an EOA (Externally Owned Account) capable of signing messages. + +3. **Initialize the HIP-1 Token** + - Allocate the **maximum intended supply** to the **System Address** derived from the spot slot ID. + - This System Address is consistent across HyperEVM and HyperCore and serves as a bridge between the ERC-20 and HIP-1 tokens. + - Think of this minting step as "funding the bridge" for inbound transfers. + - If the source token has **unbounded supply**, mint a large number (e.g., `2^64 - 1`). + - If the source token has **fixed supply**, mint the full max supply. + - Use the **HyperCore API** for this initialization. + +4. **Register Trading Pair** + - Register the trading pair between the HIP-1 token and **USDC** using the **HyperCore API**. + +5. **Link HIP-1 Token to ERC-20 Token** + - Make a signed API request to the **HyperCore network** from the EOA that deployed the ERC-20 token. + - This confirms the intent to link the tokens. + +6. **Connect the CCIP Token Pool** + - Deploy and configure a BurnAndMint Token Pool on HyperEVM using **CCIP Token Manager**. + +### Bridging Between HyperCore and HyperEVM + +Once the above steps are completed, a user can bridge their CCIP token from any connected network to HyperEVM as normal. Then they can [convert that ERC20 token into the corresponding HIP-1 token](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/hypercore-less-than-greater-than-hyperevm-transfers#transferring-between-core-and-evm) by simply sending it to the associated System Address. The system address then sends them the same amount of HIP-1 tokens on HyperCore. This works in the other direction in a similar fashion. + +## Contact Us + +Before proceeding, it’s essential to carefully review your deployment configuration. Even minor misconfigurations can permanently impact your spot slot, potentially requiring you to reacquire one through another auction. + +To help ensure a smooth and secure deployment, we currently offer white-glove support for CCIP users. Our team will validate your configuration and, if desired, provide tools and assistance to complete this process. + +If you're ready to deploy your CCIP token to Hyperliquid, please [contact us here](https://chain.link/ccip-contact?v=General%20Technical%20Support) to schedule a deployment. Stay tuned—enhanced tooling is on the way to further streamline this process for CCIP users. + +--- + +# CCIP Network Specific Guides +Source: https://docs.chain.link/ccip/evm/tools-resources/network-specific + +This section provides guidance on how to integrate with networks that require specific setup in order to successfully utilize CCIP. + +## Hyperliquid + +- [Hyperliquid Integration Guide](/ccip/evm/tools-resources/network-specific/hyperliquid-integration-guide) +- [HyperEVM Testnet RPC Guide](/ccip/evm/tools-resources/network-specific/hyperevm-testnet-rpc) + +--- + +# CCIP Token Manager +Source: https://docs.chain.link/ccip/evm/tools-resources/token-manager +Last Updated: 2025-05-19 + +The CCIP Token Manager is a web interface designed to simplify the deployment, configuration, and management of Cross-Chain Tokens for use with Chainlink CCIP. + +It provides guided workflows for token developers, abstracting away much of the complexity involved in setting up tokens for cross-chain transfers. + +**Access the Token Manager:** + +- **Mainnet:** [https://tokenmanager.chain.link/](https://tokenmanager.chain.link/) +- **Testnet:** [https://test.tokenmanager.chain.link/](https://test.tokenmanager.chain.link/) + +## Key Features & Workflows + +- **Deploy New Tokens**: Guides users through deploying a new token contract from scratch and configuring it for CCIP, primarily using the Burn & Mint mechanism. +- **Enable Existing Tokens**: Assists in making already-deployed tokens cross-chain capable by deploying and configuring the necessary CCIP token pool contracts. +- **Configuration Management**: Allows token administrators (once connected via wallet) to manage certain token pool settings, such as rate limits. +- **Admin Role Management**: Facilitates the process of registering and accepting administrative roles for token pools. +- **Token Verification**: Provides a process to request token verification for listing in the CCIP Directory. +- **Dashboard View**: Offers a dashboard to view and manage tokens associated with the connected wallet. + +--- + +# Application Developers +Source: https://docs.chain.link/ccip/evm/tutorials/application-developers +Last Updated: 2025-07-21 + +These tutorials are for application developers building cross-chain functionality with CCIP. They cover transferring tokens, sending arbitrary messages, programmable token transfers (tokens + data), manual execution, and USDC transfers with data. + +## Pages + +- [Transfer Tokens](/ccip/evm/tutorials/application-developers/transfer-tokens-from-contract): Transfer ERC-20 tokens from a smart contract to another chain using CCIP. +- [Send a Message](/ccip/evm/tutorials/application-developers/send-arbitrary-data): Send arbitrary data cross-chain using CCIP arbitrary messaging. +- [A → B → A Confirmation Pattern](/ccip/evm/tutorials/application-developers/send-arbitrary-data-receipt-acknowledgment): Send a message cross-chain and receive a confirmation back on the source chain. +- [Programmable Token Transfers](/ccip/evm/tutorials/application-developers/programmable-token-transfers): Send tokens and instructions in a single CCIP message. +- [Programmable Token Transfers - Defensive](/ccip/evm/tutorials/application-developers/programmable-token-transfers-defensive): A defensive example of programmable token transfers with extra safety checks. +- [Manual Execution](/ccip/evm/tutorials/application-developers/manual-execution): Manually execute a CCIP message when the default executor fails or is unavailable. +- [Transfer USDC with Data](/ccip/evm/tutorials/application-developers/usdc): Transfer USDC cross-chain with programmable instructions via CCTP. + +--- + +# Manual Execution +Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/manual-execution +Last Updated: 2026-05-27 + +> **NOTE: Just want to execute your stuck message?** +> +> You can jump straight down to either of these sections to execute your transaction: +> +> - [Manual Execution with CCIP Explorer](#foundry-option-a-manual-execute-in-ccip-explorer-confirm-trigger) +> - [Manual Execution with CCIP CLI](#foundry-option-b-manual-execute-with-ccip-cli-show-manual-exec) +> - Your exact command might differ depending on which framework you're using: `Foundry`, or `Hardhat`. \\ +> +> If you're not sure why your message failed, read the [manual execution concept page](/ccip/concepts/manual-execution) to learn how to tell gas errors apart from receiver logic errors before you retry. + +This tutorial extends the [Transfer Tokens with Data](/ccip/evm/tutorials/application-developers/programmable-token-transfers) 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) + +> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +> +> 1. Chainlink CCIP 2.0 supports [Faster-Than-Finality (FTF)](/ccip/concepts/execution-latency/ftf), which lets +> supported lanes process messages with a requested finality mode instead of always waiting for full finality. +> 2. In these tutorials, the scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`. +> 3. Supplying extraArgs that were encoded off-chain keeps the smart contract compatible with future CCIP versions +> without requiring an upgrade. We recommend against hardcoding extraArgs inside a smart contract. +> 4. We demonstrate FTF with a numeric block depth of `32` (`BLOCK_DEPTH=32`) in this tutorial; omit `BLOCK_DEPTH` (or +> set it to `0`/`DEFAULT`) to request `finalized` finality (`WAIT_FOR_FINALITY_FLAG`), which is the default. + +## Before you begin + +1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this + [tutorial](/quickstarts/deploy-your-first-contract) to get started. +2. Your wallet must have: + - `ETH` on *Ethereum Sepolia* (deploy + send) + - `ETH` on *Arbitrum Sepolia* (deploy + configure + manual execution transaction gas) + - `CCIP-BnM` on *Ethereum Sepolia* (token transfer) +3. If you plan to pay CCIP fees in LINK, you also need `LINK` on *Ethereum Sepolia*. Learn how to + [Acquire testnet LINK](/resources/acquire-link). +4. Check the [CCIP Directory](/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 [`CCIPReceiver`](/ccip/evm/api-reference/v2.0.0/ccip-receiver) and 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`) +- The receiver advertises its allowed finality modes per source chain via `getCCVsAndFinalityConfig`. The configure + scripts set this policy, and the send scripts validate `BLOCK_DEPTH` requests against it. + +```solidity filename="ProgrammableTokenTransfers.sol" +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`](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage) payload and passes it +to the Router. The important part for this tutorial is `extraArgs`: + +- `extraArgs` is a `bytes` field that carries message execution parameters. +- On EVM chains, this includes a `gasLimit` field (for example, see + [`GenericExtraArgsV2.gasLimit`](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2)). +- In CCIP 2.0 tutorials, the scripts encode `extraArgs` off-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. + +```solidity filename="_buildCCIPMessage" +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`: + +```solidity filename="Foundry send script (snippet)" +params.gasLimit = uint32(vm.envOr("GAS_LIMIT", uint256(200_000))); +bytes memory extraArgs = buildExtraArgs(/* ... */, params.gasLimit, params.requestedFinalityConfig); +``` + +```ts +// 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. + +```solidity filename="_ccipReceive (snippet)" +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_DEPTH` and `ALLOWED_BLOCK_DEPTH=32` +- Send step: `BLOCK_DEPTH=32` +- Default finalized finality: omit `BLOCK_DEPTH` or use `BLOCK_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`: + - `show` to confirm the message is stuck / eligible for manual execution + - `manual-exec` to retry execution with a higher `--gas-limit` + +[ProgrammableTokenTransfers.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/programmable-token-transfers/ProgrammableTokenTransfers.sol) + +Check out the complete contract code on Github. + +> **CAUTION: Best Practices** +> +> This example is simplified for educational purposes. For production code, follow these best practices: +> +> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). +> +> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). +> +> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) +> +> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> +> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). + +## Tutorial + +Let's get started! Choose your preferred development environment below. + +### Foundry + +Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a +> Foundry dev environment from scratch. + +#### 1. Bootstrap a new project + +[Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the Foundry Starter Kit for a smoother setup. + +> **Working directory:** Run all commands from the `docs-ccip` repository root (the folder that contains `foundry.toml`). + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside + the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. Create an encrypted Foundry keystore: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +3. Install dependencies: + +```bash filename="Terminal" +npm install +``` + +4. Install/Update `ccip-cli` and verify the installed version: + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +ccip-cli --version +``` + +5. Create a `.env` file by copying the example file, and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# 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= +``` + +6. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +7. Compile the contracts: + +```bash filename="Terminal" +forge build +``` + +#### 2. Deploy your contracts + +1. Deploy `ProgrammableTokenTransfers` on both *Ethereum Sepolia* (source) and *Arbitrum Sepolia* (destination): + +```bash filename="Terminal" +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: + +```bash filename="Terminal" +======================================== +🚀 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 +======================================== +``` + +[Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-token-transfers/deploy/Deploy.s.sol) + +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 of `32` + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +1. Export the addresses of the deployed contracts so they are available in your terminal: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && export ARBITRUM_SEPOLIA_CONTRACT= +``` + +2. Run the configure script: + +```bash filename="Terminal" +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 +``` + +```bash filename="Terminal" +======================================== +⚙️ 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 +======================================== +``` + +[Configure.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-token-transfers/configure/Configure.s.sol) + +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: + +```bash filename="Terminal" +CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS= \ +forge script foundry/scripts/faucet/DripBnMToken.s.sol:DripBnMToken \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +```bash filename="Terminal" +======================================== +💰 Drip CCIP-BnM Token +======================================== +Chain: Ethereum Sepolia +CCIP-BnM Address: 0x9a97F119cFE1D5Ea77c264441C0A0aBC9B34E119 +Recipient: 0x1A2b3C4d5E6f708192A3b4C5d6E7f8091A2b3C4D +======================================== + +✅ CCIP-BnM tokens dripped successfully! +``` + +[DripBnMToken.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/faucet/DripBnMToken.s.sol) + +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) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +Run the send script: + +```bash filename="Terminal" +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. + +```bash filename="Terminal" +======================================== +📡 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 +``` + +[SendMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-token-transfers/interact/SendMessage.s.sol) + +Check out the complete script code on Github. + +#### 6. Option A : Manual execute in CCIP Explorer (confirm + trigger) + +Use [CCIP Explorer](https://ccip.chain.link/) to confirm the message is **Ready for manual execution** and trigger manual execution. + +1. Open CCIP Explorer and locate your message by `messageId`. + You can paste the `messageId` into the Explorer search bar, or open the message page directly at + `https://ccip.chain.link/#/side-drawer/msg/` (replace ``). +2. Wait for the status to become **Ready for manual execution**. +3. 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. +4. 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. + +1. Inspect the message status and confirm it is **Ready for manual execution**: + +```bash filename="Terminal" +ccip-cli show \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +> **NOTE: Troubleshooting: \`ccip-cli show\`** +> +> - Use a bare `0x...` `messageId` (or tx hash) as the argument, not the full CCIP Explorer URL. - If you hit a +> transient `400` / missing-data response, wait a short time and retry. - Make sure you pass both `--rpc` endpoints +> (source + destination). + +2. Manually execute with a higher receiver callback gas limit override: + +```bash filename="Terminal" +ccip-cli manual-exec \ + --wallet foundry:$KEYSTORE_NAME \ + --gas-limit 200000 \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +```bash filename="Terminal" +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 │ +└─────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` + +> **NOTE: Destination-chain gas** +> +> The manual execution transaction is sent on the destination chain, so the executing wallet must have enough *Arbitrum +> Sepolia ETH* to pay gas. The `--gas-limit` flag overrides the receiver callback gas limit for this single +> retry (not the transaction gas limit). Use `--gas-limit 0` to reuse the original message gas limit. A +> non-zero override must not be lower than the original message gas limit. + +#### 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: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/programmable-token-transfers/interact/GetLastReceivedMessageDetails.s.sol:GetLastReceivedMessageDetails +``` + +```bash filename="Terminal" +======================================== +🔍 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 +======================================== +``` + +[GetLastReceivedMessageDetails.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-token-transfers/interact/GetLastReceivedMessageDetails.s.sol) + +Check out the complete script code on Github. + +### Hardhat + +Best for developers who want a mature, TypeScript-based smart contract development framework. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a +> Hardhat dev environment from scratch. + +#### 1. Bootstrap a new project + +[CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the starter kit (contains both Hardhat and Foundry code) for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside + the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. Copy the example environment file and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# 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= +``` + +3. Install dependencies and compile: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +4. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +5. Create a Hardhat keystore entry for your private key. Use the same name as `KEYSTORE_NAME` in your `.env` file. Hardhat will prompt you to enter the private key and a password to encrypt it: + +```bash filename="Terminal" +npx hardhat keystore set your_keystore_name +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +6. Install/Update `ccip-cli` and verify the installed version: + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +ccip-cli --version +``` + +#### 2. Deploy your contracts + +Deploy `ProgrammableTokenTransfers` on both chains: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/deploy/deploy.ts +``` + +```bash filename="Terminal" +======================================== +🚀 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: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && export ARBITRUM_SEPOLIA_CONTRACT= +``` + +[deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/deploy/deploy.ts) + +Check out the complete script code on Github. + +#### 3. Configure allowlists and finality + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +Run the configure script with faster than finality enabled and a minimum documented block depth of `32`: + +```bash filename="Terminal" +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 +``` + +```bash filename="Terminal" +======================================== +⚙️ 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 +======================================== +``` + +[configure.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/configure/configure.ts) + +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*: + +```bash filename="Terminal" +CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS= \ +npx hardhat run hardhat/scripts/faucet/drip-bnm-token.ts +``` + +```bash filename="Terminal" +======================================== +💰 Drip CCIP-BnM Token +======================================== +Chain: Ethereum Sepolia +CCIP-BnM Address: 0x9a97F119cFE1D5Ea77c264441C0A0aBC9B34E119 +Recipient: 0x0a1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 +======================================== + +✅ CCIP-BnM tokens dripped successfully! +https://sepolia.etherscan.io/tx/0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b +``` + +[drip-bnm-token.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/faucet/drip-bnm-token.ts) + +Check out the faucet script on Github. + +> **NOTE: Destination gas token** +> +> The manual execution retry transaction is sent on *Arbitrum Sepolia*. Make sure the wallet you use has enough +> *Arbitrum Sepolia ETH* to pay gas. + +#### 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) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +```bash filename="Terminal" +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`. + +```bash filename="Terminal" +======================================== +📡 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 +``` + +[send-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/interact/send-message.ts) + +Check out the complete script code on Github. + +#### 6. Option A : Manual execute in CCIP Explorer (confirm + trigger) + +Use [CCIP Explorer](https://ccip.chain.link/) to confirm the message is **Ready for manual execution** and trigger manual execution. + +1. Open CCIP Explorer and locate your message by `messageId`. + You can paste the `messageId` into the Explorer search bar, or open the message page directly at + `https://ccip.chain.link/#/side-drawer/msg/` (replace ``). +2. Wait for the status to become **Ready for manual execution**. +3. 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. +4. 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. + +1. Inspect the message status and confirm it is **Ready for manual execution**: + +```bash filename="Terminal" +ccip-cli show \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +> **NOTE: Troubleshooting: \`ccip-cli show\`** +> +> - Use a bare `0x...` `messageId` (or tx hash) as the argument, not the full CCIP Explorer URL. - If you hit a +> transient `400` / missing-data response, wait a short time and retry. - Make sure you pass both `--rpc` endpoints +> (source + destination). + +2. Manually execute with a higher receiver callback gas limit override: + +```bash filename="Terminal" +ccip-cli manual-exec \ + --wallet hardhat:$KEYSTORE_NAME \ + --gas-limit 200000 \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +> **NOTE: Troubleshooting: Hardhat keystore wallet** +> +> The `--wallet hardhat:$KEYSTORE_NAME` mode shells out to Hardhat to read your keystore entry. Run `ccip-cli` from +> inside the `docs-ccip` project directory (so `npx hardhat ...` is available), and make sure the keystore entry value +> is a `0x`-prefixed private key. + +```bash filename="Terminal" +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 │ +└─────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` + +> **NOTE: Destination-chain gas** +> +> The manual execution transaction is sent on the destination chain, so the executing wallet must have enough *Arbitrum +> Sepolia ETH* to pay gas. The `--gas-limit` flag overrides the receiver callback gas limit for this single +> retry (not the transaction gas limit). Use `--gas-limit 0` to reuse the original message gas limit. A +> non-zero override must not be lower than the original message gas limit. + +#### 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: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/interact/get-last-received-message-details.ts +``` + +```bash filename="Terminal" +======================================== +🔍 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 +======================================== +``` + +[get-last-received-message-details.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/interact/get-last-received-message-details.ts) + +Check out the complete script code on Github. + +--- + +# Transfer Tokens with Data - Defensive Example +Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/programmable-token-transfers-defensive +Last Updated: 2026-05-01 + +This tutorial extends the [Transfer Tokens with Data](/ccip/evm/tutorials/application-developers/programmable-token-transfers) example. It uses CCIP to send `tokens + arbitrary data` from one chain to another in a single transaction. +In addition though, it incorporates defensive receiver logic that stores failed messages and locks received tokens instead of reverting the whole delivery. + +You will send `CCIP-BnM` tokens and a `string` payload from *Ethereum Sepolia* to *Arbitrum Sepolia*. The destination contract is configured to fail intentionally, then you will recover the locked tokens. + +> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +> +> 1. Chainlink CCIP 2.0 supports [Faster-Than-Finality (FTF)](/ccip/concepts/execution-latency/ftf), which lets +> supported lanes process messages with a requested finality mode instead of always waiting for full finality. +> 2. In these tutorials, the scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`. +> 3. Supplying extraArgs that were encoded off-chain keeps the smart contract compatible with future CCIP versions +> without requiring an upgrade. We recommend against hardcoding extraArgs inside a smart contract. +> 4. We demonstrate FTF with a numeric block depth of `32` (`BLOCK_DEPTH=32`) in this tutorial; omit `BLOCK_DEPTH` (or +> set it to `0`/`DEFAULT`) to request `finalized` finality (`WAIT_FOR_FINALITY_FLAG`), which is the default. + +> **NOTE: Gas limit for defensive contracts** +> +> 1. Defensive error handling (try/catch, failed-message storage) requires more gas than standard receivers. +> 2. Foundry scripts default to **400,000** gas. Hardhat scripts estimate gas unless you set +> `GAS_LIMIT` (and fall back to 400,000 if estimation fails). +> 3. Make sure to test all scenarios, not just success paths, to accurately estimate the required gas limit. + +## Before you begin + +1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. +2. Your account must have some `ETH` on *Ethereum Sepolia* and `ETH` on *Arbitrum Sepolia* for gas. If you plan to pay CCIP fees in `LINK`, you also need testnet `LINK` on *Ethereum Sepolia*. \ + Learn how to [Acquire testnet LINK](/resources/acquire-link). +3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination chains/tokens. +4. [Acquire CCIP test tokens](/ccip/getting-started/test-tokens#evm-chains). You should have `CCIP-BnM` tokens, and `CCIP-BnM` should appear in the list of your tokens in MetaMask. + +## Examine the code + +#### 1. Initializing the contract + +When deploying the contract, we define the router address of the blockchain we deploy the contract on. +Defining the router address is useful for the following: + +- Sender part: + - Calls the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee) to estimate the CCIP fees. + - Calls the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend) to send CCIP messages. + +- Receiver part: + - The contract inherits from [CCIPReceiver](/ccip/evm/api-reference/v2.0.0/ccip-receiver), which serves as a base contract for receiver contracts. This contract requires that child contracts implement the `_ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#_ccipreceive). + - This defensive contract overrides `ccipReceive` directly so it can wrap processing in `try/catch`. The inherited `onlyRouter` modifier still ensures that only the router can deliver CCIP messages to the receiver contract. + +Some key things to note: + +- `OwnerIsCreator` sets the deployer as the owner of the contract. +- The constructor passes the router address into `CCIPReceiver` at deployment time. +- `sendMessage` is `payable` and open to any caller (not restricted to the owner). It handles both LINK and native fee payments: + - pass the LINK token address as `_feeTokenAddress` to pay in LINK, or, + - `address(0)` to pay in native gas. +- The function accepts pre-encoded `_extraArgs` bytes built off-chain, making the contract forward-compatible with any extraArgs version. +- Access control is enforced through allowlisting: + - outbound messages are restricted by destination chain selector (`onlyAllowlistedDestinationChain`), not by destination receiver address + - inbound messages are restricted by source chain selector + source sender contract pair (`onlyAllowlisted`) +- The contract overrides `getCCVsAndFinalityConfig` from `CCIPReceiver` to advertise per-source-chain receiver finality policy to the OffRamp. See **Configure receiver finality policy** below. +- An `EnumerableMap` (`s_failedMessages`) tracks failed message IDs and their error codes (`FAILED` or `RESOLVED`). The `s_messageContents` mapping stores the full CCIP message payload for failed messages, enabling token recovery. +- The `s_simRevert` flag allows the owner to simulate processing failures for testing. + +```solidity filename="ProgrammableDefensiveTokenTransfers.sol" +// Imports +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; + +contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator { + using EnumerableMap for EnumerableMap.Bytes32ToUintMap; + using SafeERC20 for IERC20; + + enum ErrorCode { RESOLVED, FAILED } + + mapping(uint64 => bool) public allowlistedDestinationChains; + mapping(uint64 => mapping(address => bool)) public allowlistedChainSenders; + mapping(uint64 => bytes4) private s_allowedFinalityConfig; + mapping(bytes32 messageId => Client.Any2EVMMessage contents) public s_messageContents; + EnumerableMap.Bytes32ToUintMap internal s_failedMessages; + bool internal s_simRevert = false; + + constructor(address _router) CCIPReceiver(_router) {} + + function sendMessage( + uint64 _destinationChainSelector, + address _receiver, + string calldata _text, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) + external + payable + onlyAllowlistedDestinationChain(_destinationChainSelector) + validateReceiver(_receiver) + returns (bytes32 messageId) + { + messageId = _sendCCIPMessage(/* ... */); + } + + // ... defensive receive pipeline, recovery functions, and finality config ... +} +``` + +#### 2. Build transaction payload + +`_sendCCIPMessage` calls the `_buildCCIPMessage` helper to build a CCIP message payload using `EVM2AnyMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage). \ +This payload is then passed to the router's `getFee` and `ccipSend` functions. + +The payload includes: + +- `receiver`: ABI-encoded destination address (`abi.encode(_receiver)`). +- `data`: ABI-encoded text payload (`abi.encode(_text)`). +- `tokenAmounts`: A 1-element array containing the token address and amount to transfer. +- `extraArgs`: Pre-encoded message execution parameters built off-chain by a helper script. For default finality, the scripts encode V3 extraArgs with the finalized/default finality config. For non-default finality requests, the scripts detect lane support and use V3 extraArgs with `requestedFinalityConfig` on FTF-capable lanes, or V2 extraArgs on pre-v2.0 lanes. +- `feeToken`: The token used to pay CCIP fees (`_feeTokenAddress`). Pass the LINK token address to pay in LINK, or `address(0)` to pay in native gas. + +> **NOTE: ExtraArgs are encoded off-chain** +> +> 1. The contract accepts pre-encoded `_extraArgs` bytes, so it does not need to know about specific encoding versions. +> The scripts handle lane detection and encoding. +> 2. This makes the contract forward-compatible with future CCIP versions. + +```solidity filename="_buildCCIPMessage" + function _buildCCIPMessage( + address _receiver, + string calldata _text, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) private pure returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: _token, amount: _amount}); + + return Client.EVM2AnyMessage({ + receiver: abi.encode(_receiver), + data: abi.encode(_text), + tokenAmounts: tokenAmounts, + extraArgs: _extraArgs, + feeToken: _feeTokenAddress + }); + } +``` + +> **CAUTION: Best Practices** +> +> This example is simplified for educational purposes. For production code, follow these best practices: +> +> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). +> +> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). +> +> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) +> +> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> +> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). + +#### 3. Sending messages + +The public `sendMessage` function delegates to `_sendCCIPMessage`, which performs four operations: + +1. Builds the message payload by calling `_buildCCIPMessage`. See **Build transaction payload** for details. +2. Computes the fees by invoking the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee). +3. Pulls tokens from the caller and grants the router the required approvals by calling `_handleFeeAndTokenApprovals`. See **Handling fees and token approvals** for details. +4. Dispatches the CCIP message by executing the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend). If paying in native gas (`_feeTokenAddress == address(0)`), the fee is forwarded via `{value: ccipFee}`. + +**Note**: As a security measure, `sendMessage` is protected by the `onlyAllowlistedDestinationChain` and `validateReceiver` modifiers. Any caller can invoke it -- access is governed by the destination chain allowlist, not ownership. + +```solidity filename="_sendCCIPMessage" + function _sendCCIPMessage( + uint64 _destinationChainSelector, + address _receiver, + string calldata _text, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) private returns (bytes32 messageId) { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _receiver, _text, _token, _amount, _feeTokenAddress, _extraArgs + ); + + IRouterClient router = IRouterClient(this.getRouter()); + uint256 ccipFee = router.getFee(_destinationChainSelector, evm2AnyMessage); + + _handleFeeAndTokenApprovals(router, _token, _amount, _feeTokenAddress, ccipFee); + + if (_feeTokenAddress == address(0)) { + messageId = router.ccipSend{value: ccipFee}(_destinationChainSelector, evm2AnyMessage); + } else { + messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); + } + + emit MessageSent( + messageId, _destinationChainSelector, _receiver, _text, _token, _amount, _feeTokenAddress, ccipFee + ); + + return messageId; + } +``` + +#### 4. Handling fees and token approvals + +The contract uses a **pull-from-caller** model: when a user calls `sendMessage`, the contract pulls the required tokens from `msg.sender` via `safeTransferFrom`, then approves the Router to spend them via `forceApprove`. The caller (EOA or upstream contract) must `approve` this contract **before** calling `sendMessage`. + +`_handleFeeAndTokenApprovals` handles three scenarios: + +1. **Native fee** (`_feeTokenAddress == address(0)`): Validates that `msg.value` covers the CCIP fee. Pulls the transfer token from the caller via `safeTransferFrom` and approves the Router. +2. **Same token for fee and transfer** (`_token == _feeTokenAddress`): Pulls the combined total (`ccipFee + amount`) from the caller in one `safeTransferFrom`. Approves the Router for the combined total. +3. **Different ERC-20 tokens**: Pulls each token separately from the caller. Approves the Router for each. + +> **NOTE: Approve the contract, not the Router** +> +> Because the contract pulls tokens from the caller, your scripts must call `approve(contractAddress, amount)` -- +> **not** `approve(routerAddress, amount)`. This is a key difference from older CCIP patterns where the contract was +> pre-funded. + +```solidity filename="_handleFeeAndTokenApprovals" + function _handleFeeAndTokenApprovals( + IRouterClient _router, + address _token, + uint256 _amount, + address _feeTokenAddress, + uint256 _ccipFee + ) private { + if (_feeTokenAddress == address(0)) { + if (msg.value < _ccipFee) { + revert InsufficientNativeForFees(msg.value, _ccipFee); + } + + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + IERC20(_token).forceApprove(address(_router), _amount); + } else if (_token == _feeTokenAddress) { + uint256 totalAmount = _ccipFee + _amount; + IERC20(_token).safeTransferFrom(msg.sender, address(this), totalAmount); + IERC20(_token).forceApprove(address(_router), totalAmount); + } else { + IERC20(_feeTokenAddress).safeTransferFrom(msg.sender, address(this), _ccipFee); + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + IERC20(_feeTokenAddress).forceApprove(address(_router), _ccipFee); + IERC20(_token).forceApprove(address(_router), _amount); + } + } +``` + +#### 5. Defensive message reception + +This is the core defensive pattern. Unlike the standard `CCIPReceiver`, this contract **overrides `ccipReceive` directly** and wraps message processing in a `try/catch` so application-level failures are captured and stored instead of reverting the delivery. + +**Step-by-step flow:** + +1. **`ccipReceive`** is called by the CCIP router. The `onlyRouter` modifier ensures no other caller can invoke it. The `onlyAllowlisted` modifier checks the (sourceChainSelector, sender) pair. + +2. Inside `ccipReceive`, the contract calls `this.processMessage(any2EvmMessage)` as an **external** call to itself. This external call is wrapped in a `try/catch` block. + +3. **`processMessage`** is protected by the `onlySelf` modifier (only the contract itself can call it). It first checks `s_simRevert` -- if `true`, it reverts with `ErrorCase()` to simulate a failure. Otherwise it forwards to `_ccipReceive`. + +4. **On success**: processing completes normally and `_ccipReceive` stores the message details and emits `MessageReceived`. + +5. **On failure**: the `catch` block stores the message ID in `s_failedMessages` with `ErrorCode.FAILED`, saves the full message payload in `s_messageContents`, and emits `MessageFailed`. The tokens are locked in the contract. + +```solidity filename="ccipReceive" + function ccipReceive(Client.Any2EVMMessage calldata any2EvmMessage) + external + override + onlyRouter + onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address))) + { + try this.processMessage(any2EvmMessage) { + } catch (bytes memory err) { + s_failedMessages.set(any2EvmMessage.messageId, uint256(ErrorCode.FAILED)); + s_messageContents[any2EvmMessage.messageId] = any2EvmMessage; + emit MessageFailed(any2EvmMessage.messageId, err); + return; + } + } +``` + +```solidity filename="processMessage" + function processMessage(Client.Any2EVMMessage calldata any2EvmMessage) + external + onlySelf + onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address))) + { + if (s_simRevert) revert ErrorCase(); + + _ccipReceive(any2EvmMessage); + } +``` + +The internal `_ccipReceive` is tokenless-safe -- it checks `destTokenAmounts.length > 0` before accessing the array: + +```solidity filename="_ccipReceive" + function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override { + s_lastReceivedMessageId = any2EvmMessage.messageId; + s_lastReceivedSender = abi.decode(any2EvmMessage.sender, (address)); + s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); + + bool hasToken = any2EvmMessage.destTokenAmounts.length > 0; + s_lastReceivedTokenAddress = hasToken ? any2EvmMessage.destTokenAmounts[0].token : address(0); + s_lastReceivedTokenAmount = hasToken ? any2EvmMessage.destTokenAmounts[0].amount : 0; + + emit MessageReceived( + any2EvmMessage.messageId, + any2EvmMessage.sourceChainSelector, + s_lastReceivedSender, + s_lastReceivedText, + s_lastReceivedTokenAddress, + s_lastReceivedTokenAmount + ); + } +``` + +#### 6. Recovering locked tokens + +When a message fails, the tokens are locked in the contract. The owner can recover them using `retryFailedMessage`: + +> **NOTE: retryFailedMessage recovers tokens (it doesn't replay execution)** +> +> Despite the name, `retryFailedMessage` does **not** re-run the original message delivery or your application logic. It +> marks the message as `RESOLVED` and transfers the locked tokens to `tokenReceiver`. + +1. **Validation**: checks that the message has `ErrorCode.FAILED` in `s_failedMessages`. Reverts with `MessageNotFailed` otherwise. +2. **Status update**: sets the error code to `RESOLVED` to prevent re-entry and duplicate retries. +3. **Token recovery**: retrieves the stored message from `s_messageContents` and transfers the locked tokens to the specified `tokenReceiver` via `safeTransfer`. +4. **Event**: emits `MessageRecovered`. + +```solidity filename="retryFailedMessage" + function retryFailedMessage(bytes32 messageId, address tokenReceiver) external onlyOwner { + if (s_failedMessages.get(messageId) != uint256(ErrorCode.FAILED)) { + revert MessageNotFailed(messageId); + } + + s_failedMessages.set(messageId, uint256(ErrorCode.RESOLVED)); + + Client.Any2EVMMessage memory message = s_messageContents[messageId]; + + IERC20(message.destTokenAmounts[0].token).safeTransfer(tokenReceiver, message.destTokenAmounts[0].amount); + + emit MessageRecovered(messageId); + } +``` + +**Supporting functions:** + +- `getFailedMessages(uint256 offset, uint256 limit)`: returns a paginated list of `FailedMessage` structs (each containing `messageId` and `errorCode`). Use this to find failed message IDs before calling `retryFailedMessage`. +- `setSimRevert(bool simRevert)`: owner-only toggle that controls the `s_simRevert` flag. Set to `true` to simulate failures for testing, `false` to resume normal processing. + +#### 7. Configure receiver finality policy + +The receiver exposes an `allowedFinalityConfig` for each source chain. This value tells CCIP which finality modes the receiver accepts for messages from that chain. The sender scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`, then validate the request against the receiver policy (and the token pool policy, when applicable) before sending. + +CCIP 2.0 supports two finality request styles: + +- **Default finality (finalized)**: Omit `BLOCK_DEPTH` (or set `BLOCK_DEPTH=DEFAULT`). +- **Numeric block depth (faster than finality)**: Set `BLOCK_DEPTH=32` (or higher). This tutorial standardizes on `32`. + +This contract uses two functions to manage receiver-side finality policy: + +1. **`setAllowedFinalityConfig`**: An owner-only setter that stores the `FinalityCodec`-encoded policy for a source chain. +2. **`getCCVsAndFinalityConfig`**: The OffRamp and scripts can call this hook to read the receiver's CCV and finality policy. This tutorial does not configure custom CCVs, so it returns empty CCV arrays and `optionalThreshold = 0`. + +```solidity filename="Receiver finality policy" + function setAllowedFinalityConfig( + uint64 _sourceChainSelector, + bytes4 _allowedFinalityConfig + ) external onlyOwner { + s_allowedFinalityConfig[_sourceChainSelector] = _allowedFinalityConfig; + emit AllowedFinalityConfigSet(_sourceChainSelector, _allowedFinalityConfig); + } + + function getCCVsAndFinalityConfig( + uint64 sourceChainSelector, + bytes calldata sender + ) + external + view + override + returns ( + address[] memory requiredCCVs, + address[] memory optionalCCVs, + uint8 optionalThreshold, + bytes4 allowedFinalityConfig + ) + { + address decodedSender = abi.decode(sender, (address)); + if (!allowlistedChainSenders[sourceChainSelector][decodedSender]) { + revert SenderNotAllowedForChain(sourceChainSelector, decodedSender); + } + + requiredCCVs = new address[](0); + optionalCCVs = new address[](0); + optionalThreshold = 0; + allowedFinalityConfig = s_allowedFinalityConfig[sourceChainSelector]; + } +``` + +[ProgrammableDefensiveTokenTransfers.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/programmable-defensive-token-transfers/ProgrammableDefensiveTokenTransfers.sol) + +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. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a +> Foundry dev environment from scratch. + +#### 1. Bootstrap a new Foundry project + +[Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the Foundry Starter Kit for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. If you don't already have a Foundry keystore, use the `cast` command to create a new one. Here, `your_keystore_name` is the alias you assign to this keystore entry -- Foundry will prompt you to enter the actual private key and a password to encrypt it: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +> **CAUTION: Note** +> +> As of writing, Foundry supports keystore encryption only for private keys (not generic strings), so in this tutorial +> we only encrypt the private key and store the keystore alias in `KEYSTORE_NAME`. + +3. Install dependencies: + +```bash filename="Terminal" +npm install +``` + +4. Create a `.env` file by copying the example file, and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +5. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +> **CAUTION: Run this in every new terminal session** +> +> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and +> confirm it prints your URL. + +6. Run the following command to compile all the contracts: + +```bash filename="Terminal" +forge build +``` + +#### 2. Deploy your contracts + +In this section, you will deploy the `ProgrammableDefensiveTokenTransfers` contracts on the source and destination chains. + +1. The `Deploy.s.sol` script does the following: + +- Deploy `ProgrammableDefensiveTokenTransfers` on *Ethereum Sepolia* (source). +- Deploy `ProgrammableDefensiveTokenTransfers` on *Arbitrum Sepolia* (destination). +- Returns the contract addresses in the terminal + +To run the script, use the following command: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/programmable-defensive-token-transfers/deploy/Deploy.s.sol \ +--account $KEYSTORE_NAME \ +--broadcast -vv \ +--verify +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🚀 Deploy CCIP Defensive Contracts on Both Chains +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +======================================== + + +[Step 1] Deploying ProgrammableDefensiveTokenTransfers on Ethereum Sepolia +Contract deployed at: 0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 +https://sepolia.etherscan.io/address/0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Source Contract Address: 0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 + + +[Step 2] Deploying ProgrammableDefensiveTokenTransfers on Arbitrum Sepolia +Contract deployed at: 0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c +https://sepolia.arbiscan.io/address/0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Destination Contract Address: 0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c + +======================================== +✅ All Deployments Complete! +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 +https://sepolia.etherscan.io/address/0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 + +Destination Chain: Arbitrum Sepolia +Destination Contract: 0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c +https://sepolia.arbiscan.io/address/0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c + +Run this command to set both environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 && export ARBITRUM_SEPOLIA_CONTRACT=0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c +======================================== +``` + +2. Before moving on, export the addresses of the previously deployed contracts so that they're available in the terminal: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && export ARBITRUM_SEPOLIA_CONTRACT= +``` + +Check out the complete file on Github here: + +[Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-defensive-token-transfers/deploy/Deploy.s.sol) + +Check out the complete script code on Github. + +#### 3. Configure allowlists and finality + +As a best practice, allowlist specific contracts to prevent unauthorized message delivery between your sender and receiver. + +[Configure.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-defensive-token-transfers/configure/Configure.s.sol) + +Check out the complete script code on Github. + +1. The `Configure.s.sol` script handles all configuration in a single command: + - Allowlists the destination chain on the sender contract. + - Allowlists the chain-sender pair on the receiver contract via `allowlistChainSender`. + - Sets the receiver's finality policy via `setAllowedFinalityConfig`. + - Enables simulated revert on the receiver contract via `setSimRevert(true)`. + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \ +ALLOWED_BLOCK_DEPTH=32 \ +forge script foundry/scripts/tutorials/programmable-defensive-token-transfers/configure/Configure.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +To allow numeric faster than finality requests, set `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` and choose a minimum `ALLOWED_BLOCK_DEPTH`. In this tutorial, `ALLOWED_BLOCK_DEPTH=32` allows send-time requests of `BLOCK_DEPTH=32` (or higher). + +> **NOTE: SIM_REVERT defaults to true** +> +> The `SIM_REVERT` environment variable defaults to `true` in this tutorial's configure script. This means the receiver +> contract will deliberately fail when processing incoming messages, causing tokens to be locked. This is the expected +> behavior for demonstrating the defensive recovery pattern. Set `SIM_REVERT=false` if you want normal message +> processing. + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Configure CCIP Defensive Contracts on Both Chains +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 +Destination Chain: Arbitrum Sepolia +Destination Contract: 0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c +======================================== + + +[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 0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 from Ethereum Sepolia... +✅ Chain-sender pair allowlisted: Ethereum Sepolia -> 0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 +Setting allowed finality config to 0x00000020 (BLOCK_DEPTH=32)... +✅ Allowed finality config set to 0x00000020 (BLOCK_DEPTH=32) for Ethereum Sepolia +✅ Sim revert set to true - messages will fail for testing recovery + +======================================== +✅ Configuration Complete on Arbitrum Sepolia! +======================================== + +======================================== +✅ All Configurations Complete! +======================================== +Ethereum Sepolia can send messages to Arbitrum Sepolia +Arbitrum Sepolia can receive messages from Ethereum Sepolia +⚠️ Messages will fail on destination (simRevert=true) to demonstrate defensive handling +``` + +#### 4. Fund your wallet with test tokens + +[DripBnMToken.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/faucet/DripBnMToken.s.sol) + +Check out the faucet script on Github. + +> **NOTE: Convenient way to print your wallet address** +> +> If you don't have the public address of your wallet handy, run `cast wallet address` with `--account $KEYSTORE_NAME` +> to print it from the terminal. + +Before sending a CCIP message, you need `CCIP-BnM` test tokens in your wallet. The send scripts transfer `CCIP-BnM` from +your EOA to the contract, so your wallet must hold a balance. + +Use the faucet script included in the starter kit to drip `CCIP-BnM` tokens to your address: + +```bash filename="Terminal" +CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS= forge script foundry/scripts/faucet/DripBnMToken.s.sol --account $KEYSTORE_NAME --broadcast -vv +``` + +> **NOTE: Note** +> +> If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the +> [Chainlink faucet](https://faucets.chain.link/). + +#### 5. Send a message + +`SendMessage.s.sol` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: + +- Builds off-chain `extraArgs` for the lane. Foundry uses `ExtraArgsHelper.buildExtraArgs` to detect the lane version and encode V2 or V3. +- Approves the contract to spend the caller's tokens (the contract then pulls via `safeTransferFrom`). +- Sends the CCIP message. + +> **NOTE: Expected behavior with SIM_REVERT=true** +> +> Because the configure step enabled `setSimRevert(true)` on the receiver, the message **will fail on the destination +> chain by design**. The receiver contract will lock the tokens instead of reverting. You will recover them in the next +> step. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` environment variable controls send-side finality behavior: + +- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality. +- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth. + +##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +BLOCK_DEPTH=32 \ +TOKEN_AMOUNT=1000000000000000 GAS_LIMIT=400000 \ +MESSAGE='Hello from Foundry!' \ +forge script foundry/scripts/tutorials/programmable-defensive-token-transfers/interact/SendMessage.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Defensive Message Transfer - Pay with Native +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 +Receiver: 0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c +Fee Token: Native (ETH) +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Gas limit (override): 400000 +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=400000, 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): 330463107598943 + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708 +``` + +##### Example 2: Pay with LINK + finalized finality (default) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=LINK \ +TOKEN_AMOUNT=1000000000000000 GAS_LIMIT=400000 \ +MESSAGE='Hello from Foundry!' \ +forge script foundry/scripts/tutorials/programmable-defensive-token-transfers/interact/SendMessage.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Defensive Message Transfer - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x8A1b2C3d4E5f60718293A4b5c6D7e8F9a0B1c2D3 +Receiver: 0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c +Fee Token: LINK +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Gas limit (override): 400000 +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=400000, finalityConfig=0x00000000). + +[Step 1] Approving contract to spend fee token for CCIP fees... +Required CCIP fee (in token units): 38700123456789000 +✅ Contract approved to spend fee token + + +[Step 2] Approving contract to spend CCIP-BnM... +✅ Contract approved to spend CCIP-BnM + + +[Step 3] Sending CCIP message... + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a +``` + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| `KEYSTORE_NAME` | Foundry encrypted keystore name used by `--account $KEYSTORE_NAME` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address per chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`, `ARBITRUM_SEPOLIA_CONTRACT`) | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `TOKEN_AMOUNT` | Amount of CCIP-BnM to transfer (in wei) | `1000000000000000` (0.001) | +| `GAS_LIMIT` | Gas limit for the destination callback | `400000` | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | +| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality mode(s) used by the configure step. Set to `BLOCK_DEPTH` to allow numeric faster than finality requests. | Not set | +| `ALLOWED_BLOCK_DEPTH` | Receiver minimum block depth (used when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`). Use `32` in this tutorial. | Not set | +| `SIM_REVERT` | Enables simulated failures on the receiver during configuration | `true` | +| `MESSAGE` | Text payload to send | `Default String Text` | +| `CHAIN` | Chain name identifier used by the failed-message scripts (the chain where the receiver is deployed) | Not set | +| `MESSAGE_ID` | Failed message ID to retry (used by the retry script) | Not set | +| `TOKEN_RECEIVER` | Address to receive recovered tokens when retrying a failed message | Not set | + +[SendMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-defensive-token-transfers/interact/SendMessage.s.sol) + +Check out the complete script code on Github. + +#### 6. Check failed messages and recover tokens + +After sending the message, wait for it to arrive on the destination chain (typically 10-20 minutes). Because `s_simRevert` is `true`, the message will fail and the tokens will be locked in the receiver contract. + +##### Step 1: Check for failed messages + +Run the `GetFailedMessages.s.sol` script to query the receiver contract for failed messages: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA forge script foundry/scripts/tutorials/programmable-defensive-token-transfers/interact/GetFailedMessages.s.sol -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +🔍 Check Failed Messages +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c +======================================== + +Found 1 unresolved failed message(s): + +======================================== +Failed Message #1 +======================================== +Message ID: 0x1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708 +Error Code: 1 (FAILED) + +``` + +Note the `Message ID` from the output -- you will need it for the next step. + +[GetFailedMessages.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-defensive-token-transfers/interact/GetFailedMessages.s.sol) + +Check out the complete script code on Github. + +##### Step 2: Recover the locked tokens + +Use the `RetryFailedMessage.s.sol` script to recover the locked tokens. Pass the failed message ID and the address where you want the tokens sent: + +```bash filename="Terminal" +MESSAGE_ID= TOKEN_RECEIVER= CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/programmable-defensive-token-transfers/interact/RetryFailedMessage.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +🔄 Retry Failed Message +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c +Message ID: 0x1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708 +======================================== + +Retrying failed message... +Token receiver address: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 + + +======================================== +✅ Message Retry Complete! +======================================== +Tokens have been recovered and sent to: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 + +``` + +[RetryFailedMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-defensive-token-transfers/interact/RetryFailedMessage.s.sol) + +Check out the complete script code on Github. + +##### Step 3: Verify the recovery + +Re-run the `GetFailedMessages.s.sol` script. The error code for the recovered message should now be `0` (`RESOLVED`): + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA forge script foundry/scripts/tutorials/programmable-defensive-token-transfers/interact/GetFailedMessages.s.sol -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +🔍 Check Failed Messages +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0x6D2a8F4c1E9b3A7d5C0e6B8f2A4c9E1d3F7b5A0c +======================================== + +✅ No unresolved failed messages found. +Total messages: 1 (all resolved) + +``` + +### Hardhat + +Best for developers who want a mature, TypeScript-based smart contract development framework. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a +> Hardhat dev environment from scratch. + +#### 1. Bootstrap a new Hardhat project + +[CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the starter kit (contains both Hardhat and Foundry code) for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. Copy the example environment file and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +3. Install dependencies and compile: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +4. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +> **CAUTION: Run this in every new terminal session** +> +> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and +> confirm it prints your URL. + +5. Create a Hardhat keystore entry for your private key. Use the same name as `KEYSTORE_NAME` in your `.env` file. Hardhat will prompt you to enter the private key and a password to encrypt it: + +```bash filename="Terminal" +npx hardhat keystore set your_keystore_name +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +#### 2. Deploy your contracts + +[deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-defensive-token-transfers/deploy/deploy.ts) + +Check out the complete script code on Github. + +In this section, you will deploy the `ProgrammableDefensiveTokenTransfers` contracts on the source and destination chains. + +1. The `deploy.ts` script does the following: + +- Deploy `ProgrammableDefensiveTokenTransfers` on *Ethereum Sepolia* (source). +- Deploy `ProgrammableDefensiveTokenTransfers` on *Arbitrum Sepolia* (destination). +- Returns the contract addresses in the terminal + +2. To run the script, use the following command: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/programmable-defensive-token-transfers/deploy/deploy.ts +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🚀 Deploy CCIP Defensive Contracts on Both Chains +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +======================================== + +[Step 1] Deploying ProgrammableDefensiveTokenTransfers on Ethereum Sepolia +Contract deployed at: 0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 +https://sepolia.etherscan.io/address/0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Source Contract Address: 0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 + +[Step 2] Deploying ProgrammableDefensiveTokenTransfers on Arbitrum Sepolia +Contract deployed at: 0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa +https://sepolia.arbiscan.io/address/0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Destination Contract Address: 0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa + +======================================== +✅ All Deployments Complete! +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 +https://sepolia.etherscan.io/address/0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 + +Destination Chain: Arbitrum Sepolia +Destination Contract: 0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa +https://sepolia.arbiscan.io/address/0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa + +Run this command to set both environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 && export ARBITRUM_SEPOLIA_CONTRACT=0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa +======================================== +``` + +3. Before moving on, export the addresses of the previously deployed contracts so that they're available in the terminal: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && \ +export ARBITRUM_SEPOLIA_CONTRACT= +``` + +#### 3. Configure allowlists and finality + +As a best practice, allowlist specific contracts to prevent unauthorized message delivery between your sender and receiver. + +[configure.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-defensive-token-transfers/configure/configure.ts) + +Check out the complete script code on Github. + +1. The `configure.ts` script handles all configuration in a single command: + - Allowlists the destination chain on the sender contract. + - Allowlists the chain-sender pair on the receiver contract via `allowlistChainSender`. + - Sets the receiver's finality policy via `setAllowedFinalityConfig`. + - Enables simulated revert on the receiver contract via `setSimRevert(true)`. + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \ +ALLOWED_BLOCK_DEPTH=32 \ +npx hardhat run hardhat/scripts/tutorials/programmable-defensive-token-transfers/configure/configure.ts +``` + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +To allow numeric faster than finality requests, set `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` and choose a minimum `ALLOWED_BLOCK_DEPTH`. In this tutorial, `ALLOWED_BLOCK_DEPTH=32` allows send-time requests of `BLOCK_DEPTH=32` (or higher). + +> **NOTE: SIM_REVERT defaults to true** +> +> The `SIM_REVERT` environment variable defaults to `true` in this tutorial's configure script. This means the receiver +> contract will deliberately fail when processing incoming messages, causing tokens to be locked. This is the expected +> behavior for demonstrating the defensive recovery pattern. Set `SIM_REVERT=false` if you want normal message +> processing. + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Configure CCIP Defensive Contracts on Both Chains +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 +Destination Chain: Arbitrum Sepolia +Destination Contract: 0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa +======================================== + +[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 0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 from Ethereum Sepolia... +✅ Chain-sender pair allowlisted: Ethereum Sepolia -> 0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 +Setting allowed finality config to 0x00000020 (BLOCK_DEPTH=32)... +✅ Allowed finality config set to 0x00000020 (BLOCK_DEPTH=32) for Ethereum Sepolia +Setting simRevert to true... +✅ Sim revert set to true - messages will fail for testing recovery + +======================================== +✅ Configuration Complete on Arbitrum Sepolia! +======================================== + +======================================== +✅ All Configurations Complete! +======================================== +Ethereum Sepolia can send messages to Arbitrum Sepolia +Arbitrum Sepolia can receive messages from Ethereum Sepolia +⚠️ Messages will fail on destination (simRevert=true) to demonstrate defensive handling +``` + +#### 4. Fund your wallet with test tokens + +[drip-bnm-token.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/faucet/drip-bnm-token.ts) + +Check out the faucet script on Github. + +Before sending a CCIP message, you need `CCIP-BnM` test tokens in your wallet. The send scripts transfer `CCIP-BnM` from your EOA to the contract, so your wallet must hold a balance. + +Use the faucet script included in the starter kit to drip `CCIP-BnM` tokens to your address: + +```bash filename="Terminal" +CHAIN=ETHEREUM_SEPOLIA \ +RECIPIENT_ADDRESS= \ +npx hardhat run hardhat/scripts/faucet/drip-bnm-token.ts +``` + +If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the [Chainlink faucet](https://faucets.chain.link/). + +#### 5. Send a message + +`send-message.ts` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: + +- Builds off-chain `extraArgs` for the lane. Hardhat uses `@chainlink/ccip-sdk` and the receiver policy check in `buildExtraArgs` to encode V2 or V3. +- Approves the contract to spend the caller's tokens (the contract then pulls via `safeTransferFrom`). +- Sends the CCIP message. + +> **NOTE: Expected behavior with SIM_REVERT=true** +> +> Because the configure step enabled `setSimRevert(true)` on the receiver, the message **will fail on the destination +> chain by design**. The receiver contract will lock the tokens instead of reverting. You will recover them in the next +> step. + +##### Dynamic gas estimation + +When `GAS_LIMIT` is **not** set, the script uses `estimateReceiveExecution` from `@chainlink/ccip-sdk` to dynamically estimate the required gas limit for the destination callback. If estimation fails, it falls back to `400000`. + +> **CAUTION: Dynamic estimation with simRevert** +> +> When `s_simRevert` is `true` on the receiver, the SDK simulates the failure path, which may produce inaccurate gas +> estimates. For this tutorial, we recommend setting `GAS_LIMIT=400000` explicitly to ensure the message has enough gas +> for the defensive try/catch and error-storage logic. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` environment variable controls send-side finality behavior: + +- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality. +- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth. + +##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`) + explicit gas limit + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +BLOCK_DEPTH=32 \ +TOKEN_AMOUNT=1000000000000000 \ +MESSAGE="Hello from Hardhat" \ +GAS_LIMIT=400000 \ +FEE_TOKEN=NATIVE \ +npx hardhat run hardhat/scripts/tutorials/programmable-defensive-token-transfers/interact/send-message.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Defensive Message Transfer - Pay with ETH +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 +Receiver: 0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa +Fee Token: Native (ETH) +======================================== + +[Pre-validation] Querying lane features and receiver contract... +Gas limit (override): 400000 +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=400000, finalityConfig=32 block(s)). +[Pre-validation] CCIP fee: 330463107598943 + +[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): 330463107598943 + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7082 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7082 +``` + +##### Example 2: Pay with LINK + finalized finality (default) + explicit gas limit + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +TOKEN_AMOUNT=1000000000000000 \ +MESSAGE="Hello from Hardhat" \ +GAS_LIMIT=400000 \ +FEE_TOKEN=LINK \ +npx hardhat run hardhat/scripts/tutorials/programmable-defensive-token-transfers/interact/send-message.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Defensive Message Transfer - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0xB0c1D2e3F4a5B6c7D8e9F00112233445566778899 +Receiver: 0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa +Fee Token: LINK +======================================== + +[Pre-validation] Querying lane features and receiver contract... +Gas limit (override): 400000 +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=400000, finalityConfig=0x00000000). +[Pre-validation] CCIP fee: 38700123456789000 + +[Step 1] Approving contract to spend LINK for CCIP fees... +Required CCIP fee (in LINK units): 38700123456789000 +✅ Contract approved to spend LINK + + +[Step 2] Approving contract to spend CCIP-BnM... +✅ Contract approved to spend CCIP-BnM + + +[Step 3] Sending CCIP message... + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708193 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708193 +``` + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| `KEYSTORE_NAME` | Hardhat keystore entry name, set in `.env` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address per chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`, `ARBITRUM_SEPOLIA_CONTRACT`) | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `TOKEN_AMOUNT` | Amount of CCIP-BnM to transfer (in wei) | `1000000000000000` (0.001) | +| `GAS_LIMIT` | Gas limit for the destination callback (if unset, estimated dynamically via `estimateReceiveExecution`) | `400000` (fallback) | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | +| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality mode(s) used by the configure step. Set to `BLOCK_DEPTH` to allow numeric faster than finality requests. | Not set | +| `ALLOWED_BLOCK_DEPTH` | Receiver minimum block depth (used when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`). Use `32` in this tutorial. | Not set | +| `SIM_REVERT` | Enables simulated failures on the receiver during configuration | `true` | +| `MESSAGE` | Text payload to send | `Default String Text` | +| `CHAIN` | Chain name identifier used by the failed-message scripts (the chain where the receiver is deployed) | Not set | +| `MESSAGE_ID` | Failed message ID to retry (used by the retry script) | Not set | +| `TOKEN_RECEIVER` | Address to receive recovered tokens when retrying a failed message | Not set | +| `OFFSET` | Offset into the receiver's failed-message list (used by `get-failed-messages.ts`) | `0` | +| `LIMIT` | Maximum number of failed messages to return (used by `get-failed-messages.ts`) | `10` | + +[send-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-defensive-token-transfers/interact/send-message.ts) + +Check out the complete script code on Github. + +#### 6. Check failed messages and recover tokens + +After sending the message, wait for it to arrive on the destination chain (typically 10-20 minutes). Because `s_simRevert` is `true`, the message will fail and the tokens will be locked in the receiver contract. + +##### Step 1: Check for failed messages + +Run the `get-failed-messages.ts` script to query the receiver contract for failed messages: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/programmable-defensive-token-transfers/interact/get-failed-messages.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +🔍 Check Failed Messages +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa +======================================== + +Found 1 unresolved failed message(s): + +======================================== +Failed Message #1 +======================================== +Message ID: 0x3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7082 +Error Code: 1 (FAILED) + +``` + +Note the `Message ID` from the output -- you will need it for the next step. + +[get-failed-messages.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-defensive-token-transfers/interact/get-failed-messages.ts) + +Check out the complete script code on Github. + +##### Step 2: Recover the locked tokens + +Use the `retry-failed-message.ts` script to recover the locked tokens. Pass the failed message ID and the address where you want the tokens sent: + +```bash filename="Terminal" +MESSAGE_ID= TOKEN_RECEIVER= CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/programmable-defensive-token-transfers/interact/retry-failed-message.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +🔄 Retry Failed Message +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa +Message ID: 0x3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7082 +Token Receiver: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 +======================================== + +Retrying failed message... +Token receiver address: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 + +======================================== +✅ Message Retry Complete! +======================================== +Tokens have been recovered and sent to: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 +Transaction: 0x5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a4b5c +``` + +[retry-failed-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-defensive-token-transfers/interact/retry-failed-message.ts) + +Check out the complete script code on Github. + +##### Step 3: Verify the recovery + +Re-run the `get-failed-messages.ts` script. The error code for the recovered message should now be `0` (`RESOLVED`): + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/programmable-defensive-token-transfers/interact/get-failed-messages.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +🔍 Check Failed Messages +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0xC1d2E3f4A5b6C7d8E9F00112233445566778899Aa +======================================== + +✅ No unresolved failed messages found. +Total messages: 1 (all resolved) + +``` + +--- + +# Transfer Tokens with Data +Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/programmable-token-transfers +Last Updated: 2026-05-01 + +In this tutorial, we will use CCIP to send some `tokens + arbitrary data` from one chain to another in a single transaction. \ +We will send `CCIP-BnM` tokens and a `string` payload from *Ethereum Sepolia* to *Arbitrum Sepolia* twice: + +1. Once, paying CCIP fees in native gas tokens `ETH` and using faster than finality (`BLOCK_DEPTH=32`). +2. And then a second time, paying CCIP fees in `LINK` and using finalized finality (default). + +> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +> +> 1. Chainlink CCIP 2.0 supports [Faster-Than-Finality (FTF)](/ccip/concepts/execution-latency/ftf), which lets +> supported lanes process messages with a requested finality mode instead of always waiting for full finality. +> 2. In these tutorials, the scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`. +> 3. Supplying extraArgs that were encoded off-chain keeps the smart contract compatible with future CCIP versions +> without requiring an upgrade. We recommend against hardcoding extraArgs inside a smart contract. +> 4. We demonstrate FTF with a numeric block depth of `32` (`BLOCK_DEPTH=32`) in this tutorial; omit `BLOCK_DEPTH` (or +> set it to `0`/`DEFAULT`) to request `finalized` finality (`WAIT_FOR_FINALITY_FLAG`), which is the default. + +## Before you begin + +1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. +2. Your account must have some `ETH` and `LINK` tokens on *Ethereum Sepolia* and `ETH` tokens on *Arbitrum Sepolia*. \ + Learn how to [Acquire testnet LINK](/resources/acquire-link). +3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination chains/tokens. + +## Examine the code + +#### 1. Initializing the contract + +When deploying the contract, we define the router address of the blockchain we deploy the contract on. +Defining the router address is useful for the following: + +- Sender part: + - Calls the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee) to estimate the CCIP fees. + - Calls the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend) to send CCIP messages. + +- Receiver part: + - The contract inherits from [CCIPReceiver](/ccip/evm/api-reference/v2.0.0/ccip-receiver), which serves as a base contract for receiver contracts. This contract requires that child contracts implement the `_ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#_ccipreceive). + - `_ccipReceive` is called by the `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive), which ensures that only the router can deliver CCIP messages to the receiver contract. + +Some key things to note: + +- `OwnerIsCreator` sets the deployer as the owner of the contract. + +- The constructor passes the router address into `CCIPReceiver` at deployment time. + +- `sendMessage` is `payable` and open to any caller (not restricted to the owner). It handles both LINK and native fee payments: + - pass the LINK token address as `_feeTokenAddress` to pay in LINK, or, + - `address(0)` to pay in native gas. + +- The function accepts pre-encoded `_extraArgs` bytes built off-chain, making the contract forward-compatible with any extraArgs version. + +- Access control is enforced through allowlisting: + - outbound messages are restricted by destination chain selector (`onlyAllowlistedDestinationChain`), not by destination receiver address + - inbound messages are restricted by source chain selector + source sender contract pair (`onlyAllowlisted`) + +- The contract overrides `getCCVsAndFinalityConfig` from `CCIPReceiver` to advertise per-source-chain receiver finality policy to the OffRamp. See **Configure receiver finality policy** below. + +```solidity filename="ProgrammableTokenTransfers.sol" +// Imports + +contract ProgrammableTokenTransfers is CCIPReceiver, OwnerIsCreator { + + constructor(address _router) CCIPReceiver(_router) {} + + // ... state variables, modifiers, allowlist admin functions ... + + function sendMessage( + uint64 _destinationChainSelector, + address _receiver, + string calldata _text, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) + external + payable + onlyAllowlistedDestinationChain(_destinationChainSelector) + validateReceiver(_receiver) + returns (bytes32 messageId) + { + messageId = _sendCCIPMessage( + _destinationChainSelector, + _receiver, + _text, + _token, + _amount, + _feeTokenAddress, + _extraArgs + ); + } + + // ... internal helpers (fee handling, message building, approvals) ... + + 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)); + + bool hasToken = any2EvmMessage.destTokenAmounts.length > 0; + s_lastReceivedTokenAddress = hasToken ? any2EvmMessage.destTokenAmounts[0].token : address(0); + s_lastReceivedTokenAmount = hasToken ? any2EvmMessage.destTokenAmounts[0].amount : 0; + + emit MessageReceived( /* ... */ ); + } + + // ... getCCVsAndFinalityConfig override, fee helpers, and withdrawal utilities ... +} +``` + +#### 2. Build transaction payload + +`_sendCCIPMessage` calls the `_buildCCIPMessage` helper to build a CCIP message payload using `EVM2AnyMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage). \ +This payload is then passed to the router's `getFee` and `ccipSend` functions. + +The payload includes: + +- `receiver`: ABI-encoded destination address (`abi.encode(_receiver)`). +- `data`: ABI-encoded text payload (`abi.encode(_text)`). +- `tokenAmounts`: A 1-element array containing the token address and amount to transfer. +- `extraArgs`: Pre-encoded message execution parameters built off-chain by a helper script. For finalized (default) finality, the scripts encode V3 extraArgs with the finalized finality config. For faster than finality requests, the scripts detect lane support and use V3 extraArgs with `requestedFinalityConfig` on FTF-capable lanes, or V2 extraArgs on pre-v2.0 lanes. +- `feeToken`: The token used to pay CCIP fees (`_feeTokenAddress`). Pass the LINK token address to pay in LINK, or `address(0)` to pay in native gas. + +> **NOTE: ExtraArgs are encoded off-chain** +> +> 1. The contract accepts pre-encoded `_extraArgs` bytes, so it does not need to know about specific encoding versions. +> The scripts handle lane detection and encoding. +> 2. This makes the contract forward-compatible with future CCIP versions. + +```solidity filename="_buildCCIPMessage" + function _buildCCIPMessage( + address _receiver, + string calldata _text, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) private pure returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: _token, amount: _amount}); + + return Client.EVM2AnyMessage({ + receiver: abi.encode(_receiver), + data: abi.encode(_text), + tokenAmounts: tokenAmounts, + extraArgs: _extraArgs, + feeToken: _feeTokenAddress + }); + } +``` + +> **CAUTION: Best Practices** +> +> This example is simplified for educational purposes. For production code, follow these best practices: +> +> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). +> +> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). +> +> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) +> +> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> +> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). + +#### 3. Sending messages + +The public `sendMessage` function delegates to `_sendCCIPMessage`, which performs four operations: + +1. Builds the message payload by calling `_buildCCIPMessage`. See **Build transaction payload** for details. +2. Computes the fees by invoking the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee). +3. Pulls tokens from the caller and grants the router the required approvals by calling `_handleFeeAndTokenApprovals`. See **Handling fees and token approvals** for details. +4. Dispatches the CCIP message by executing the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend). If paying in native gas (`_feeTokenAddress == address(0)`), the fee is forwarded via `{value: ccipFee}`. + +**Note**: As a security measure, `sendMessage` is protected by the `onlyAllowlistedDestinationChain` and `validateReceiver` modifiers. Any caller can invoke it -- access is governed by the destination chain allowlist, not ownership. + +```solidity filename="_sendCCIPMessage" + function _sendCCIPMessage( + uint64 _destinationChainSelector, + address _receiver, + string calldata _text, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) private returns (bytes32 messageId) { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _receiver, _text, _token, _amount, _feeTokenAddress, _extraArgs + ); + + IRouterClient router = IRouterClient(this.getRouter()); + uint256 ccipFee = router.getFee(_destinationChainSelector, evm2AnyMessage); + + _handleFeeAndTokenApprovals(router, _token, _amount, _feeTokenAddress, ccipFee); + + if (_feeTokenAddress == address(0)) { + messageId = router.ccipSend{value: ccipFee}(_destinationChainSelector, evm2AnyMessage); + } else { + messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); + } + + emit MessageSent( + messageId, _destinationChainSelector, _receiver, _text, _token, _amount, _feeTokenAddress, ccipFee + ); + + return messageId; + } +``` + +#### 4. Handling fees and token approvals + +The contract uses a **pull-from-caller** model: when a user calls `sendMessage`, the contract pulls the required tokens from `msg.sender` via `safeTransferFrom`, then approves the Router to spend them via `forceApprove`. The caller (EOA or upstream contract) must `approve` this contract **before** calling `sendMessage`. + +`_handleFeeAndTokenApprovals` handles three scenarios: + +1. **Native fee** (`_feeTokenAddress == address(0)`): Validates that `msg.value` covers the CCIP fee. Pulls the transfer token from the caller via `safeTransferFrom` and approves the Router. +2. **Same token for fee and transfer** (`_token == _feeTokenAddress`): Pulls the combined total (`ccipFee + amount`) from the caller in one `safeTransferFrom`. Approves the Router for the combined total. +3. **Different ERC-20 tokens**: Pulls each token separately from the caller. Approves the Router for each. + +> **NOTE: Approve the contract, not the Router** +> +> Because the contract pulls tokens from the caller, your scripts must call `approve(contractAddress, amount)` -- +> **not** `approve(routerAddress, amount)`. This is a key difference from older CCIP patterns where the contract was +> pre-funded. + +```solidity filename="_handleFeeAndTokenApprovals" + function _handleFeeAndTokenApprovals( + IRouterClient _router, + address _token, + uint256 _amount, + address _feeTokenAddress, + uint256 _ccipFee + ) private { + if (_feeTokenAddress == address(0)) { + if (msg.value < _ccipFee) { + revert InsufficientNativeForFees(msg.value, _ccipFee); + } + + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + IERC20(_token).forceApprove(address(_router), _amount); + } else if (_token == _feeTokenAddress) { + uint256 totalAmount = _ccipFee + _amount; + IERC20(_token).safeTransferFrom(msg.sender, address(this), totalAmount); + IERC20(_token).forceApprove(address(_router), totalAmount); + } else { + IERC20(_feeTokenAddress).safeTransferFrom(msg.sender, address(this), _ccipFee); + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + IERC20(_feeTokenAddress).forceApprove(address(_router), _ccipFee); + IERC20(_token).forceApprove(address(_router), _amount); + } + } +``` + +#### 5. Receiving messages + +On the destination blockchain, the router calls the inherited `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive), which verifies the caller is the router and then invokes the contract's internal `_ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#_ccipreceive). The `_ccipReceive` function expects an `Any2EVMMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#any2evmmessage) that contains: + +- The CCIP `messageId`. +- The `sourceChainSelector`. +- The `sender` address in bytes format. The address is decoded from bytes to an Ethereum address using the [ABI specifications](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html) and stored in `s_lastReceivedSender`. +- The `destTokenAmounts` array contains received tokens and their respective amounts. The function checks whether tokens were included before accessing the array, so it gracefully handles messages with or without token transfers. +- The `data`, which is also in bytes format. Given a `string` is expected, the data is decoded from bytes to a string using the [ABI specifications](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html). + +**Note**: Two important security measures are applied: + +- `_ccipReceive` is called by the `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive), which ensures that only the router can deliver CCIP messages to the receiver contract. See the `onlyRouter` [modifier](/ccip/evm/api-reference/v2.0.0/ccip-receiver#onlyrouter-modifier) for more information. +- The modifier `onlyAllowlisted` ensures that only a call from an allowlisted source chain and sender pair is accepted. + +```solidity filename="_ccipReceive" + 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)); + + bool hasToken = any2EvmMessage.destTokenAmounts.length > 0; + s_lastReceivedTokenAddress = hasToken ? any2EvmMessage.destTokenAmounts[0].token : address(0); + s_lastReceivedTokenAmount = hasToken ? any2EvmMessage.destTokenAmounts[0].amount : 0; + + emit MessageReceived( + any2EvmMessage.messageId, + any2EvmMessage.sourceChainSelector, + s_lastReceivedSender, + s_lastReceivedText, + s_lastReceivedTokenAddress, + s_lastReceivedTokenAmount + ); + } +``` + +#### 6. Configure receiver finality policy + +The receiver exposes an `allowedFinalityConfig` for each source chain. This value tells CCIP which finality modes the receiver accepts for messages from that chain. The sender scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`, then validate the request against the receiver policy (and the token pool policy, when applicable) before sending. + +CCIP 2.0 supports two finality request styles: + +- **Default finality (finalized)**: Omit `BLOCK_DEPTH` (or set `BLOCK_DEPTH=DEFAULT`). +- **Numeric block depth (faster than finality)**: Set `BLOCK_DEPTH=32` (or higher). This tutorial standardizes on `32`. + +This contract uses two functions to manage receiver-side finality policy: + +1. **`setAllowedFinalityConfig`**: An owner-only setter that stores the `FinalityCodec`-encoded policy for a source chain. +2. **`getCCVsAndFinalityConfig`**: The OffRamp and scripts can call this hook to read the receiver's CCV and finality policy. This tutorial does not configure custom CCVs, so it returns empty CCV arrays and `optionalThreshold = 0`. + +```solidity filename="Receiver finality policy" + function setAllowedFinalityConfig( + uint64 _sourceChainSelector, + bytes4 _allowedFinalityConfig + ) external onlyOwner { + s_allowedFinalityConfig[_sourceChainSelector] = _allowedFinalityConfig; + emit AllowedFinalityConfigSet(_sourceChainSelector, _allowedFinalityConfig); + } + + function getCCVsAndFinalityConfig( + uint64 sourceChainSelector, + bytes calldata sender + ) + external + view + override + returns ( + address[] memory requiredCCVs, + address[] memory optionalCCVs, + uint8 optionalThreshold, + bytes4 allowedFinalityConfig + ) + { + address decodedSender = abi.decode(sender, (address)); + if (!allowlistedChainSenders[sourceChainSelector][decodedSender]) { + revert SenderNotAllowedForChain(sourceChainSelector, decodedSender); + } + + requiredCCVs = new address[](0); + optionalCCVs = new address[](0); + optionalThreshold = 0; + allowedFinalityConfig = s_allowedFinalityConfig[sourceChainSelector]; + } +``` + +[ProgrammableTokenTransfers.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/programmable-token-transfers/ProgrammableTokenTransfers.sol) + +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. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a +> Foundry dev environment from scratch. + +#### 1. Bootstrap a new Foundry project + +[Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the Foundry Starter Kit for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. If you don't already have a Foundry keystore, use the `cast` command to create a new one. Here, `your_keystore_name` is the alias you assign to this keystore entry -- Foundry will prompt you to enter the actual private key and a password to encrypt it: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +And use the `cast wallet list` command to verify: + +![Foundry keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-2.png) + +> **CAUTION: Note** +> +> As of writing, Foundry supports keystore encryption only for private keys (not generic strings), so in this tutorial +> we only encrypt the private key and store the keystore alias in `KEYSTORE_NAME`. + +3. Install dependencies: + +```bash filename="Terminal" +npm install +``` + +4. Create a `.env` file by copying the example file, and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +5. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +> **CAUTION: Run this in every new terminal session** +> +> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and +> confirm it prints your URL. + +6. Run the following command to compile all the contracts: + +```bash filename="Terminal" +forge build +``` + +#### 2. Deploy your contracts + +In this section, you will deploy the contracts on the source and destination chains. + +1. The `Deploy.s.sol` script does the following: + +- Deploy `ProgrammableTokenTransfers` on *Ethereum Sepolia* (source). +- Deploy `ProgrammableTokenTransfers` on *Arbitrum Sepolia* (destination). +- Returns the contract addresses in the terminal + +To run the script, use the following command: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/programmable-token-transfers/deploy/Deploy.s.sol \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +The deploy script prints the deployed contract addresses and follow-up commands. Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🚀 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 +https://sepolia.etherscan.io/address/0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B + +Destination Chain: Arbitrum Sepolia +Destination Contract: 0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c +https://sepolia.arbiscan.io/address/0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c + +Run this command to set both environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B && export ARBITRUM_SEPOLIA_CONTRACT=0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c +======================================== +``` + +Check out the complete file on Github here: + +[Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-token-transfers/deploy/Deploy.s.sol) + +Check out the complete script code on Github. + +#### 3. Configure allowlists and finality + +As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair. + +[Configure.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-token-transfers/configure/Configure.s.sol) + +Check out the complete script code on Github. + +1. Export the addresses of the previously deployed contracts so that they're available in the terminal: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && export ARBITRUM_SEPOLIA_CONTRACT= +``` + +2. The `Configure.s.sol` script handles both sides in a single command: + - allowlists the destination chain on the sender contract, + - and allowlists the chain-sender pair on the receiver contract. + +```bash filename="Terminal" +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 \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +- To allow numeric faster than finality requests, set `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` and choose a minimum `ALLOWED_BLOCK_DEPTH`. \\ +- In this tutorial, `ALLOWED_BLOCK_DEPTH=32` allows send-time requests of `BLOCK_DEPTH=32` (or higher). + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ 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 +``` + +3. **Optional -- enable bidirectional messaging:** If you want the destination contract to also send messages back to the source, run the reversed command: + +```bash filename="Terminal" +SOURCE_CHAIN=ARBITRUM_SEPOLIA DEST_CHAIN=ETHEREUM_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \ +ALLOWED_BLOCK_DEPTH=32 \ +forge script foundry/scripts/tutorials/programmable-token-transfers/configure/Configure.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +#### 4. Fund your wallet with test tokens + +[DripBnMToken.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/faucet/DripBnMToken.s.sol) + +Check out the faucet script on Github. + +> **NOTE: Convenient way to print your wallet address** +> +> If you don't have the public address of your wallet handy, run `cast wallet address --account $KEYSTORE_NAME` to print +> it from the terminal. + +Before sending a CCIP message, you need `CCIP-BnM` test tokens in your wallet. The send scripts transfer `CCIP-BnM` from +your EOA to the contract, so your wallet must hold a balance. + +Use the faucet script included in the starter kit to drip `CCIP-BnM` tokens to your address: + +```bash filename="Terminal" +CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS= \ +forge script foundry/scripts/faucet/DripBnMToken.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +> **NOTE: Note** +> +> If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the +> [Chainlink faucet](https://faucets.chain.link/). + +#### 5. Send a message + +`SendMessage.s.sol` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: + +- Builds off-chain `extraArgs` for the lane. Foundry uses `ExtraArgsHelper.buildExtraArgs` to detect the lane version and encode V2 or V3. +- Approves the contract to spend the caller's tokens (the contract then pulls via `safeTransferFrom`). +- Sends the CCIP message. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` environment variable controls send-side finality behavior: + +- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality. +- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth. + +##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +BLOCK_DEPTH=32 \ +TOKEN_AMOUNT=1000000000000000 GAS_LIMIT=200000 \ +MESSAGE='Hello from Foundry!' \ +forge script foundry/scripts/tutorials/programmable-token-transfers/interact/SendMessage.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 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): 200000 +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=200000, 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): 230463107598943 + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0xa1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f91 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0xa1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f91 +``` + +##### Example 2: Pay with LINK + finalized finality (default) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=LINK \ +TOKEN_AMOUNT=1000000000000000 GAS_LIMIT=200000 \ +MESSAGE='Hello from Foundry!' \ +forge script foundry/scripts/tutorials/programmable-token-transfers/interact/SendMessage.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Message Transfer - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B +Receiver: 0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c +Fee Token: LINK +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Gas limit (override): 200000 +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=200000, finalityConfig=0x00000000). + +[Step 1] Approving contract to spend fee token for CCIP fees... +Required CCIP fee (in token units): 28700123456789000 +✅ Contract approved to spend fee token + + +[Step 2] Approving contract to spend CCIP-BnM... +✅ Contract approved to spend CCIP-BnM + + +[Step 3] Sending CCIP message... + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0xb2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a2 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0xb2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a2 +``` + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| `KEYSTORE_NAME` | Foundry encrypted keystore name used by `--account $KEYSTORE_NAME` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address per chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`, `ARBITRUM_SEPOLIA_CONTRACT`) | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `TOKEN_AMOUNT` | Amount of CCIP-BnM to transfer (in wei) | `1000000000000000` (0.001) | +| `GAS_LIMIT` | Gas limit for the destination callback | `200000` | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | +| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality mode(s) used by the configure step. Set to `BLOCK_DEPTH` to allow numeric faster than finality requests. | Not set | +| `ALLOWED_BLOCK_DEPTH` | Receiver minimum block depth (used when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`). Use `32` in this tutorial. | Not set | +| `MESSAGE` | Text payload to send | `Default String Text` | +| `CHAIN` | Chain name identifier used by the receipt verification scripts (the chain where the receiver is deployed) | Not set | + +[SendMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-token-transfers/interact/SendMessage.s.sol) + +Check out the complete script code on Github. + +#### 6. Verify receipt on the destination chain + +The `GetLastReceivedMessageDetails.s.sol` script queries the receiver contract for the last received message. This is a read-only operation: no transaction is broadcast. + +1. Run the script, passing the `CHAIN` where you deployed the receiver: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA forge script foundry/scripts/tutorials/programmable-token-transfers/interact/GetLastReceivedMessageDetails.s.sol +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🔍 Verify Received Message +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0x8A4f2E6c1B9d3D5a7C8e0F2b4D6a8C1e3F5b7A9c +======================================== + +Checking for received message... + +======================================== +✅ Message Received Successfully! +======================================== +Message ID: 0xa1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f91 +Sender: 0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B +Received Text: "Hello from Foundry!" +Received Token: 0x686325E21F55c64Bf724047E0fe7C454D6faD37D +Received Token Amount: 1000000000000000 +======================================== +``` + +Check out the complete file on Github here: + +[GetLastReceivedMessageDetails.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/programmable-token-transfers/interact/GetLastReceivedMessageDetails.s.sol) + +Check out the complete script code on Github. + +### Hardhat + +Best for developers who want a mature, TypeScript-based smart contract development framework. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a +> Hardhat dev environment from scratch. + +#### 1. Bootstrap a new Hardhat project + +[CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the starter kit (contains both Hardhat and Foundry code) for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. Copy the example environment file and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +3. Install dependencies and compile: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +4. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +> **CAUTION: Run this in every new terminal session** +> +> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and +> confirm it prints your URL. + +5. Create a Hardhat keystore entry for your private key. Use the same name as `KEYSTORE_NAME` in your `.env` file. Hardhat will prompt you to enter the private key and a password to encrypt it: + +```bash filename="Terminal" +npx hardhat keystore set your_keystore_name +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +#### 2. Deploy your contracts + +[deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/deploy/deploy.ts) + +Check out the complete script code on Github. + +In this section, you will deploy the contracts on the source and destination chains. The next step configures the sender contract to allow the destination chain and the receiver contract to allow the source chain-sender pair. + +1. The `deploy.ts` script does the following: + +- Deploy `ProgrammableTokenTransfers` on *Ethereum Sepolia* (source). +- Deploy `ProgrammableTokenTransfers` on *Arbitrum Sepolia* (destination). +- Returns the contract addresses in the terminal + +2. To run the script, use the following command: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/deploy/deploy.ts +``` + +The deploy script prints the deployed contract addresses and follow-up commands. Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🚀 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 + +======================================== +✅ All Deployments Complete! +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c +https://sepolia.etherscan.io/address/0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c + +Destination Chain: Arbitrum Sepolia +Destination Contract: 0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e +https://sepolia.arbiscan.io/address/0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e + +Run this command to set both environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c && export ARBITRUM_SEPOLIA_CONTRACT=0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e +======================================== +``` + +3. Before moving on, export the addresses of the previously deployed contracts so that they're available in the terminal: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && \ +export ARBITRUM_SEPOLIA_CONTRACT= +``` + +#### 3. Configure allowlists and finality + +As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair. + +[configure.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/configure/configure.ts) + +Check out the complete script code on Github. + +1. The `configure.ts` script handles both sides in a single command -- it allowlists the destination chain on the sender contract and allowlists the chain-sender pair on the receiver contract: + +```bash filename="Terminal" +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 +``` + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +To allow numeric faster than finality requests, set `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` and choose a minimum `ALLOWED_BLOCK_DEPTH`. In this tutorial, `ALLOWED_BLOCK_DEPTH=32` allows send-time requests of `BLOCK_DEPTH=32` (or higher). + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ 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 + +======================================== +✅ Configuration Complete on Ethereum 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 + +======================================== +✅ Configuration Complete on Arbitrum Sepolia! +======================================== + +======================================== +✅ All Configurations Complete! +======================================== +Ethereum Sepolia can send messages to Arbitrum Sepolia +Arbitrum Sepolia can receive messages from Ethereum Sepolia +``` + +2. **Optional: enable bidirectional messaging:** If you want the destination contract to also send messages back to the source, run the reversed command: + +```bash filename="Terminal" +SOURCE_CHAIN=ARBITRUM_SEPOLIA DEST_CHAIN=ETHEREUM_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \ +ALLOWED_BLOCK_DEPTH=32 \ +npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/configure/configure.ts +``` + +#### 4. Fund your wallet with test tokens + +[drip-bnm-token.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/faucet/drip-bnm-token.ts) + +Check out the faucet script on Github. + +Before sending a CCIP message, you need `CCIP-BnM` test tokens in your wallet. The send scripts transfer `CCIP-BnM` from your EOA to the contract, so your wallet must hold a balance. + +Use the faucet script included in the starter kit to drip `CCIP-BnM` tokens to your address: + +```bash filename="Terminal" +CHAIN=ETHEREUM_SEPOLIA \ +RECIPIENT_ADDRESS= \ +npx hardhat run hardhat/scripts/faucet/drip-bnm-token.ts +``` + +If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the [Chainlink faucet](https://faucets.chain.link/). + +#### 5. Send a message + +`send-message.ts` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: + +- Builds off-chain `extraArgs` for the lane. Hardhat uses `@chainlink/ccip-sdk` and the receiver policy check in `buildExtraArgs` to encode V2 or V3. +- Approves the contract to spend the caller's tokens (the contract then pulls via `safeTransferFrom`). +- Sends the CCIP message. + +##### Dynamic gas estimation + +When `GAS_LIMIT` is **not** set, the script uses `estimateReceiveExecution` from `@chainlink/ccip-sdk` to dynamically estimate the required gas limit for the destination callback. This avoids wasting gas by over-estimating, or failing by under-estimating. If estimation fails, it falls back to `200000`. You can override by setting `GAS_LIMIT` explicitly. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` environment variable controls send-side finality behavior: + +- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality. +- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth. + +##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`) + explicit gas limit + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +BLOCK_DEPTH=32 \ +TOKEN_AMOUNT=1000000000000000 \ +MESSAGE="Hello from Hardhat" \ +GAS_LIMIT=200000 \ +FEE_TOKEN=NATIVE \ +npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/interact/send-message.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 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): 200000 +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=200000, finalityConfig=32 block(s)). +[Pre-validation] CCIP fee: 230463107598943 + +[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): 230463107598943 + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0xc3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b3 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0xc3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b3 +``` + +##### Example 2: Pay with LINK + finalized finality (default) + dynamic gas estimation + +This example deliberately omits `GAS_LIMIT` to demonstrate dynamic estimation via `estimateReceiveExecution`: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +TOKEN_AMOUNT=1000000000000000 \ +MESSAGE="Hello from Hardhat" \ +FEE_TOKEN=LINK \ +npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/interact/send-message.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Message Transfer - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c +Receiver: 0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e +Fee Token: LINK +======================================== + +[Pre-validation] Querying lane features and receiver contract... +Gas limit (estimated): 198765 +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=198765, finalityConfig=0x00000000). +[Pre-validation] CCIP fee: 28700123456789000 + +[Step 1] Approving contract to spend LINK for CCIP fees... +Required CCIP fee (in LINK units): 28700123456789000 +✅ Contract approved to spend LINK + + +[Step 2] Approving contract to spend CCIP-BnM... +✅ Contract approved to spend CCIP-BnM + + +[Step 3] Sending CCIP message... + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0xd4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c4 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0xd4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c4 +``` + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| `KEYSTORE_NAME` | Hardhat keystore entry name, set in `.env` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address per chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`, `ARBITRUM_SEPOLIA_CONTRACT`) | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `TOKEN_AMOUNT` | Amount of CCIP-BnM to transfer (in wei) | `1000000000000000` (0.001) | +| `GAS_LIMIT` | Gas limit for the destination callback (if unset, estimated dynamically via `estimateReceiveExecution`) | Estimated | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | +| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality mode(s) used by the configure step. Set to `BLOCK_DEPTH` to allow numeric faster than finality requests. | Not set | +| `ALLOWED_BLOCK_DEPTH` | Receiver minimum block depth (used when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`). Use `32` in this tutorial. | Not set | +| `MESSAGE` | Text payload to send | `Default String Text` | +| `CHAIN` | Chain name identifier used by the receipt verification scripts (the chain where the receiver is deployed) | Not set | + +[send-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/interact/send-message.ts) + +Check out the complete script code on Github. + +#### 6. Verify receipt on the destination chain + +[get-last-received-message-details.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/interact/get-last-received-message-details.ts) + +Check out the complete script code on Github. + +The `get-last-received-message-details.ts` script queries the receiver contract for the last received message. This is a read-only operation: no transaction is actually broadcast to the destination chain. + +1. Run the script, passing the `CHAIN` where you deployed the receiver: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/interact/get-last-received-message-details.ts +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🔍 Verify Received Message +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0x3E7a9C1f5B2d8A4e6F0c3D7b9E1a5C8f2B4d6A0e +======================================== + +Checking for received message... + +======================================== +✅ Message Received Successfully! +======================================== +Message ID: 0xc3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b3 +Sender: 0x4b7C2d9E3F1a6B8c0D2e4F6a8B0c2D4e6F8a0B2c +Received Text: "Hello from Hardhat" +Received Token: 0x686325E21F55c64Bf724047E0fe7C454D6faD37D +Received Token Amount: 1000000000000000 +======================================== +``` + +--- + +# Send Arbitrary Data and Receive Transfer Confirmation: A -> B -> A +Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/send-arbitrary-data-receipt-acknowledgment +Last Updated: 2026-05-26 + +In this tutorial, we will use CCIP to send arbitrary data from one chain to another and then have the receiver contract send back an acknowledgment message back to the sender: + +- A MessageTracker contract on chain A sends a text message to an Acknowledger contract on chain B. +- After processing the message, the Acknowledger sends an acknowledgment message back to the MessageTracker. +- The MessageTracker updates onchain state to reflect the successful execution of the back and forth pattern. + +By tracking acknowledgment onchain, your contracts can safely trigger follow-up actions only after the destination contract has confirmed the receipt. + +> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +> +> 1. Chainlink CCIP 2.0 supports [Faster-Than-Finality (FTF)](/ccip/concepts/execution-latency/ftf), which lets +> supported lanes process messages with a requested finality mode instead of always waiting for full finality. +> 2. In these tutorials, the scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`. +> 3. Supplying extraArgs that were encoded off-chain keeps the smart contract compatible with future CCIP versions +> without requiring an upgrade. We recommend against hardcoding extraArgs inside a smart contract. +> 4. We demonstrate FTF with a numeric block depth of `32` (`BLOCK_DEPTH=32`) in this tutorial; omit `BLOCK_DEPTH` (or +> set it to `0`/`DEFAULT`) to request `finalized` finality (`WAIT_FOR_FINALITY_FLAG`), which is the default. + +## Before you begin + +- This tutorial assumes you have completed the [Send Arbitrary Data](/ccip/evm/tutorials/application-developers/send-arbitrary-data) tutorial. +- You will use *Ethereum Sepolia* (A) and *Arbitrum Sepolia* (B). +- Your wallet should have: + - Sepolia ETH (for deploying and sending from Ethereum Sepolia) + - Arbitrum Sepolia ETH (for deploying on Arbitrum Sepolia and funding the Acknowledger) + - Optional: Sepolia LINK (if you want to pay the CCIP fee in LINK) +- Learn how to [Acquire testnet LINK](/resources/acquire-link). + +## Examine the code + +The contracts and scripts used in this tutorial live in the Chainlink CCIP 2.0 starter kit repository (`docs-ccip`). + +[MessageTracker.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/MessageTracker.sol) + +Sends the initial message A→B and tracks the acknowledgment B→A. + +[Acknowledger.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/Acknowledger.sol) + +Receives the message on B and sends an acknowledgment back to A. + +#### 1. Initializing both contracts + +When deploying each contract, you pass the Router address for that chain to `CCIPReceiver`: + +- Chain A (source): `MessageTracker` (sends A→B and receives the B→A acknowledgment) +- Chain B (destination): `Acknowledger` (receives A→B and sends the B→A acknowledgment) + +Some key things to note: + +- Both contracts inherit from `CCIPReceiver`, so `ccipReceive` ensures that only the Router can deliver CCIP messages. +- Both contracts inherit from `OwnerIsCreator`, so the deployer is the owner and can configure allowlists and receiver finality policy. +- In the canonical code, `MessageTracker.sendMessage` is `onlyOwner`, so the tutorial scripts broadcast from the deployer/owner account. + +```solidity filename="MessageTracker.sol" +contract MessageTracker is CCIPReceiver, OwnerIsCreator { + constructor(address _router) CCIPReceiver(_router) {} + + function sendMessage( + uint64 _destinationChainSelector, + address _receiver, + string calldata _text, + address _feeTokenAddress, + bytes calldata _extraArgs + ) + external + payable + onlyOwner + // ... allowlists + receiver validation ... + returns (bytes32 messageId) + { + // ... + } +} +``` + +```solidity filename="Acknowledger.sol" +contract Acknowledger is CCIPReceiver, OwnerIsCreator { + constructor(address _router) CCIPReceiver(_router) {} +} +``` + +#### 2. Architecture and message lifecycle (A→B→A) + +In this tutorial, we deploy: + +- Chain A (source): *Ethereum Sepolia* → `MessageTracker` +- Chain B (destination): *Arbitrum Sepolia* → `Acknowledger` + +The lifecycle of a tracked message is: + +1. **A→B**: `MessageTracker.sendMessage(...)` sends a text payload to the Acknowledger. +2. **B→A**: the Acknowledger receives the message and sends an acknowledgment back, encoding the **initial** message ID. +3. **State update**: when the MessageTracker receives the acknowledgment, it marks the initial message as processed. + +`MessageTracker` tracks messages by the **initial CCIP messageId** (A→B), and stores the acknowledgment messageId (B→A) alongside it: + +```solidity filename="MessageTracker.sol" +enum MessageStatus { + NotSent, // 0: default / unknown messageId + Sent, // 1: sent to Acknowledger, awaiting acknowledgment + ProcessedOnDestination // 2: acknowledgment received +} + +struct MessageInfo { + MessageStatus status; + bytes32 acknowledgerMessageId; // CCIP messageId of the acknowledgment (B→A) +} + +mapping(bytes32 => MessageInfo) public messagesInfo; +``` + +#### 3. MessageTracker: build payload and send (A→B) + +`MessageTracker` sends a data-only CCIP message using the `Client.EVM2AnyMessage` struct. + +The payload includes: + +- `receiver`: ABI-encoded destination address (`abi.encode(_receiver)`). +- `data`: ABI-encoded `(text, extraArgs)` tuple (`abi.encode(_text, _extraArgs)`). +- `tokenAmounts`: an empty array (data-only). +- `extraArgs`: pre-encoded execution parameters built off-chain by the scripts (gas limit + requested finality config). +- `feeToken`: the token used to pay CCIP fees (`_feeTokenAddress`). Pass the LINK token address to pay in LINK, or `address(0)` to pay in native gas. + +```solidity filename="MessageTracker.sol" +function _buildCCIPMessage( + address _receiver, + string calldata _text, + address _feeTokenAddress, + bytes calldata _extraArgs +) private pure returns (Client.EVM2AnyMessage memory) { + return Client.EVM2AnyMessage({ + receiver: abi.encode(_receiver), + // Pack (text, extraArgs) so the Acknowledger can reuse the same lane/finality config for the acknowledgment. + data: abi.encode(_text, _extraArgs), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: _extraArgs, + feeToken: _feeTokenAddress + }); +} +``` + +> **NOTE: Why pack extraArgs into the data field?** +> +> The receiver's `Any2EVMMessage` does not include the original `extraArgs` field. Packing `extraArgs` into `data` lets +> the Acknowledger decode and forward the same bytes into the B→A acknowledgment message. + +After `ccipSend`, the contract immediately marks the initial message as `Sent`: + +```solidity filename="MessageTracker.sol" +if (_feeTokenAddress == address(0)) { + messageId = router.ccipSend{value: ccipFee}(_destinationChainSelector, evm2AnyMessage); +} else { + messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); +} + +messagesInfo[messageId].status = MessageStatus.Sent; +``` + +#### 4. Acknowledger: receive message and send acknowledgment (B→A) + +On chain B, the router calls `Acknowledger.ccipReceive(...)`, which verifies the caller is the router and then invokes the contract's internal `_ccipReceive(...)`. + +Two important details: + +- The A→B payload packs `(text, extraArgs)` into `data`, so the Acknowledger decodes both values. +- The Acknowledger forwards the decoded `extraArgs` into the acknowledgment send so both legs use the same lane/finality config. + +```solidity filename="Acknowledger.sol" +bytes memory ackExtraArgs; +(s_lastReceivedText, ackExtraArgs) = abi.decode(any2EvmMessage.data, (string, bytes)); + +bytes32 messageIdToAcknowledge = any2EvmMessage.messageId; +address messageTrackerAddress = abi.decode(any2EvmMessage.sender, (address)); +uint64 messageTrackerChainSelector = any2EvmMessage.sourceChainSelector; + +_sendAcknowledgment(messageIdToAcknowledge, messageTrackerAddress, messageTrackerChainSelector, ackExtraArgs); +``` + +The acknowledgment encodes the initial message ID and always pays fees with native gas held by the Acknowledger contract: + +```solidity filename="Acknowledger.sol" +Client.EVM2AnyMessage memory acknowledgment = Client.EVM2AnyMessage({ + receiver: abi.encode(_messageTrackerAddress), + data: abi.encode(_messageIdToAcknowledge), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: _ackExtraArgs, + feeToken: address(0) // pay with native (held by contract) +}); + +uint256 fees = router.getFee(_messageTrackerChainSelector, acknowledgment); +bytes32 messageId = router.ccipSend{value: fees}(_messageTrackerChainSelector, acknowledgment); +``` + +> **NOTE: Funding requirement** +> +> The Acknowledger pays CCIP fees for acknowledgment messages using native gas held by the contract, so you must fund it +> with Arbitrum Sepolia ETH before sending messages. + +#### 5. MessageTracker: receive acknowledgment and update status + +When the acknowledgment returns B→A, the MessageTracker decodes the initial message ID from the incoming message data. + +It then: + +- stores the acknowledgment messageId for auditability (`acknowledgerMessageId`) +- advances the state machine (`Sent → ProcessedOnDestination`) +- reverts if the initial message ID is unknown or already processed + +```solidity filename="MessageTracker.sol" +bytes32 initialMsgId = abi.decode(any2EvmMessage.data, (bytes32)); +bytes32 acknowledgerMsgId = any2EvmMessage.messageId; + +messagesInfo[initialMsgId].acknowledgerMessageId = acknowledgerMsgId; + +MessageStatus currentStatus = messagesInfo[initialMsgId].status; + +if (currentStatus == MessageStatus.Sent) { + messagesInfo[initialMsgId].status = MessageStatus.ProcessedOnDestination; + emit MessageProcessedOnDestination( + acknowledgerMsgId, + initialMsgId, + any2EvmMessage.sourceChainSelector, + abi.decode(any2EvmMessage.sender, (address)) + ); +} else if (currentStatus == MessageStatus.ProcessedOnDestination) { + revert MessageHasAlreadyBeenProcessedOnDestination(initialMsgId); +} else { + revert MessageWasNotSentByMessageTracker(initialMsgId); +} +``` + +The tutorial scripts use `getMessageInfo(initialMessageId)` to read: + +- the status code (`0`, `1`, or `2`) +- the acknowledgment’s CCIP message ID (once it has arrived) + +#### 6. Security: allowlists + receiver finality policy (both directions) + +Because this tutorial is bidirectional, both contracts enforce allowlists in both directions: + +- **Outbound sends** are restricted by destination chain selector: + - `MessageTracker` requires the destination chain selector to be allowlisted. + - `Acknowledger` requires the source chain selector (used as the acknowledgment destination) to be allowlisted. +- **Inbound delivery** is restricted by `(source chain selector, sender address)` via the `onlyAllowlisted` checks in both `_ccipReceive` implementations. + +Both contracts also expose a receiver finality policy for each source chain via `getCCVsAndFinalityConfig(...)`. The configuration scripts set this policy on both contracts so numeric block-depth finality requests (minimum `32`) can be accepted when configured. + +> **NOTE: Default finality is the default** +> +> If you omit the finality-related environment variables, the scripts use default finality behavior. + +> **CAUTION: Best Practices** +> +> This example is simplified for educational purposes. For production code, follow these best practices: +> +> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). +> +> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). +> +> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) +> +> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> +> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). + +## Tutorial + +Let's get started. Choose your preferred development environment below. + +### Foundry + +Best for Solidity-native workflows that prefer a modular, powerful scripting framework. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a +> Foundry dev environment from scratch. + +#### 1. Bootstrap a new Foundry project + +[Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the starter kit (contains both Hardhat and Foundry code) for a smoother setup. + +1. Clone the CCIP starter kit and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. Create a Foundry keystore entry for your private key. Foundry will prompt you to enter the private key and a password to encrypt it: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +3. Install dependencies: + +```bash filename="Terminal" +npm install +``` + +4. Create a `.env` file by copying the example file, and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +5. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +> **CAUTION: Run this in every new terminal session** +> +> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and +> confirm it prints your URL. + +6. Compile all contracts: + +```bash filename="Terminal" +forge build +``` + +#### 2. Deploy your contracts (A and B) + +[Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/deploy/Deploy.s.sol) + +Deploys MessageTracker on Ethereum Sepolia and Acknowledger on Arbitrum Sepolia. + +Run the deploy script: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/deploy/Deploy.s.sol:Deploy \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🚀 Deploy MessageTracker + Acknowledger Contracts +======================================== +Source Chain (MessageTracker): Ethereum Sepolia +Destination Chain (Acknowledger): Arbitrum Sepolia +======================================== + + +[Step 1] Deploying MessageTracker on Ethereum Sepolia... +MessageTracker deployed at: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +https://sepolia.etherscan.io/address/0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 + +======================================== +✅ MessageTracker Deployed on Ethereum Sepolia! +======================================== +MessageTracker Address: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 + + +[Step 2] Deploying Acknowledger on Arbitrum Sepolia... +Acknowledger deployed at: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +https://sepolia.arbiscan.io/address/0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a + +======================================== +✅ Acknowledger Deployed on Arbitrum Sepolia! +======================================== +Acknowledger Address: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a + +======================================== +✅ All Deployments Complete! +======================================== +Source Chain: Ethereum Sepolia + MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +https://sepolia.etherscan.io/address/0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 + +Destination Chain: Arbitrum Sepolia + Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +https://sepolia.arbiscan.io/address/0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a + +Run this command to set both environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 && export ARBITRUM_SEPOLIA_CONTRACT=0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +======================================== + +IMPORTANT: Fund the Acknowledger contract with native gas tokens on Arbitrum Sepolia + The Acknowledger pays CCIP fees (native) for sending acknowledgments back to the MessageTracker. +``` + +Export the contract addresses printed by the script: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT=0x... # MessageTracker +export ARBITRUM_SEPOLIA_CONTRACT=0x... # Acknowledger +``` + +#### 3. Fund the Acknowledger (required) + +The Acknowledger pays CCIP fees for acknowledgment messages with native gas held by the contract. +Fund it with Arbitrum Sepolia ETH before sending messages. + +Example (using cast): + +```bash filename="Terminal" +cast send $ARBITRUM_SEPOLIA_CONTRACT \ +--value 0.01ether \ +--account $KEYSTORE_NAME \ +--rpc-url $ARBITRUM_SEPOLIA_RPC_URL \ +--chain arbitrum-sepolia +``` + +#### 4. Configure allowlists and finality + +[Configure.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/configure/Configure.s.sol) + +Allowlists both contracts and configures receiver finality policy. + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +Configure both contracts in a single command. This tutorial uses a block-depth policy with a minimum depth of 32. + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \ +forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/configure/Configure.s.sol:Configure \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Configure MessageTracker + Acknowledger Contracts +======================================== +Source Chain: Ethereum Sepolia + MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +Destination Chain: Arbitrum Sepolia + Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +======================================== + + +[Step 1] Configuring MessageTracker on Ethereum Sepolia +Allowlisting Arbitrum Sepolia as destination chain on MessageTracker... +✅ Destination chain allowlisted on MessageTracker: Arbitrum Sepolia +Allowlisting Acknowledger (0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a) from Arbitrum Sepolia on MessageTracker... +✅ Acknowledger allowlisted on MessageTracker: Arbitrum Sepolia -> 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +Setting allowed finality config on MessageTracker to 0x00000020 (BLOCK_DEPTH=32)... +✅ Allowed finality config set on MessageTracker for Arbitrum Sepolia + +======================================== +✅ MessageTracker Configuration Complete on Ethereum Sepolia! +======================================== + + +[Step 2] Configuring Acknowledger on Arbitrum Sepolia +Allowlisting MessageTracker (0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123) from Ethereum Sepolia on Acknowledger... +✅ MessageTracker allowlisted on Acknowledger: Ethereum Sepolia -> 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +Allowlisting Ethereum Sepolia as destination chain on Acknowledger... +✅ Source chain allowlisted as destination on Acknowledger: Ethereum Sepolia +Setting allowed finality config on Acknowledger to 0x00000020 (BLOCK_DEPTH=32)... +✅ Allowed finality config set on Acknowledger for Ethereum Sepolia + +======================================== +✅ Acknowledger Configuration Complete on Arbitrum Sepolia! +======================================== + +======================================== +✅ All Configurations Complete! +======================================== +Ethereum Sepolia (MessageTracker) can send messages to Arbitrum Sepolia (Acknowledger) +Arbitrum Sepolia (Acknowledger) can receive messages from Ethereum Sepolia and send acknowledgments back +``` + +#### 5. Send a message (A→B) + +[SendMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/SendMessage.s.sol) + +Sends a message from MessageTracker to Acknowledger. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` environment variable controls send-side finality behavior: + +- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality. +- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth. + +##### Example 1: Send a message paying with native: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT \ +MESSAGE="Hello World From Foundry" \ +forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/SendMessage.s.sol:SendMessage \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +To request numeric faster than finality (minimum 32), set `BLOCK_DEPTH=32`. + +Example output: + +```bash filename="Terminal" +======================================== +📡 CCIP Message (A→B→A) - Pay with Native +======================================== +Source Chain: Ethereum Sepolia + MessageTracker: 0x6C16098831403AaC46eb0Bbd989454D0D890ECAb +Destination Chain: Arbitrum Sepolia + Acknowledger: 0x4F8a2C6e0B4d8A1f6E3c9B5d7A2f4C8e0B6d3A5c +Fee Token: Native (ETH) +Message: "Hello World From Foundry" +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Gas limit: 500000 +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=500000, finalityConfig=0x00000000). + +[Step 1] Sending message via MessageTracker with native fee (ETH)... +Required CCIP fee (in WEI): 300138348995647 + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5 + +Track message status (status 1 = Sent, 2 = ProcessedOnDestination): +SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5 forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/GetMessageStatus.s.sol:GetMessageStatus -vv +======================================== +``` + +##### Example 2: Send a message paying with LINK (optional): + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=LINK GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT \ +MESSAGE="Hello World From Foundry" \ +forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/SendMessage.s.sol:SendMessage \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +To request numeric faster than finality (minimum 32), set `BLOCK_DEPTH=32`. + +Example output: + +```bash filename="Terminal" +======================================== +📡 CCIP Message (A→B→A) - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia + MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +Destination Chain: Arbitrum Sepolia + Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +Fee Token: LINK +Message: "Hello World From Foundry" +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Gas limit: 500000 +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=500000, finalityConfig=0x00000000). + +[Step 1] Approving MessageTracker to spend fee token for CCIP fees... +Required CCIP fee (in token units): 16700123456789000 +✅ MessageTracker approved to spend fee token + + +[Step 2] Sending message via MessageTracker... + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 + +Track message status (status 1 = Sent, 2 = ProcessedOnDestination): +SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/GetMessageStatus.s.sol:GetMessageStatus -vv +======================================== +``` + +Copy the `CCIP messageId` from the output. + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| `KEYSTORE_NAME` | Foundry encrypted keystore name used by `--account $KEYSTORE_NAME` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address per chain (MessageTracker on source, Acknowledger on destination) | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `GAS_LIMIT` | Gas limit for the destination callback | `500000` | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | +| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality config (used by the configure step). Set `BLOCK_DEPTH` to allow numeric `BLOCK_DEPTH` requests. Omit for default-finality-only. | Not set | +| `ALLOWED_BLOCK_DEPTH` | Receiver minimum block depth (used by the configure step). Required when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`. Use `32` in this tutorial. | Not set | +| `MESSAGE` | Text payload to send | `Hello World From Foundry Script for CCIP 2.0!` | +| `MESSAGE_ID` | Message ID to query status (used by the status script) | Not set | +| `CHAIN` | Chain name identifier used by the withdraw script (the chain where the Acknowledger is deployed) | Not set | + +#### 6. Track status (B→A acknowledgment) + +[GetMessageStatus.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/GetMessageStatus.s.sol) + +Reads message status from MessageTracker. + +Track the message status on the source chain: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x... \ +forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/GetMessageStatus.s.sol:GetMessageStatus -vv +``` + +Status values: + +- `0`: `NotSent` +- `1`: `Sent` +- `2`: `ProcessedOnDestination` + +Example output: + +```bash filename="Terminal" +======================================== +🔍 Check Message Status +======================================== +Chain: Ethereum Sepolia +MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +Message ID: 0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 +======================================== + +======================================== +Message Status: 2 (ProcessedOnDestination) +✅ Message acknowledged! The Acknowledger has processed the message and the MessageTracker has received the acknowledgment. +Acknowledger Message ID: 0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e8 +Check the CCIP Explorer for the acknowledgment message: +https://ccip.chain.link/#/side-drawer/msg/0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e8 +======================================== +``` + +#### 7. Withdraw from Acknowledger (optional cleanup) + +[WithdrawFromAcknowledger.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/WithdrawFromAcknowledger.s.sol) + +Withdraws native balance from the Acknowledger contract. + +After testing, reclaim any remaining native balance from the Acknowledger: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/WithdrawFromAcknowledger.s.sol:WithdrawFromAcknowledger \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Example output: + +```bash filename="Terminal" +======================================== +💸 Withdraw from Acknowledger +======================================== +Chain: Arbitrum Sepolia +Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +Native balance: 10000000000000000 wei +======================================== + +[Step 1] Withdrawing native balance to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994... +✅ Native withdrawal complete + +======================================== +✅ Withdraw Complete +======================================== +Beneficiary: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +``` + +By default, funds are sent to the broadcasting account. Set `BENEFICIARY=0x...` to override the recipient. + +### Hardhat + +Best for devs looking for a mature, TypeScript-based smart contract development framework. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a +> Hardhat dev environment from scratch. + +#### 1. Bootstrap a new Hardhat project + +[CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the starter kit (contains both Hardhat and Foundry code) for a smoother setup. + +1. Clone the CCIP starter kit and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. Copy the example environment file and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +3. Install dependencies and compile: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +4. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +5. Create a Hardhat keystore entry for your private key. Use the same name as `KEYSTORE_NAME` in your `.env` file. Hardhat will prompt you to enter the private key and a password to encrypt it: + +```bash filename="Terminal" +npx hardhat keystore set your_keystore_name +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +#### 2. Deploy your contracts (A and B) + +[deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/deploy/deploy.ts) + +Deploys MessageTracker on Ethereum Sepolia and Acknowledger on Arbitrum Sepolia. + +Run the deploy script: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/deploy/deploy.ts +``` + +Example output: + +```bash filename="Terminal" +======================================== +🚀 Deploy MessageTracker + Acknowledger Contracts +======================================== +Source Chain (MessageTracker): Ethereum Sepolia +Destination Chain (Acknowledger): Arbitrum Sepolia +======================================== + +[Step 1] Deploying MessageTracker on Ethereum Sepolia +MessageTracker deployed at: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +https://sepolia.etherscan.io/address/0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 + +======================================== +✅ MessageTracker Deployed on Ethereum Sepolia! +======================================== +MessageTracker Address: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 + +[Step 2] Deploying Acknowledger on Arbitrum Sepolia +Acknowledger deployed at: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +https://sepolia.arbiscan.io/address/0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a + +======================================== +✅ Acknowledger Deployed on Arbitrum Sepolia! +======================================== +Acknowledger Address: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a + +======================================== +✅ All Deployments Complete! +======================================== +Source Chain: Ethereum Sepolia + MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +https://sepolia.etherscan.io/address/0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 + +Destination Chain: Arbitrum Sepolia + Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +https://sepolia.arbiscan.io/address/0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a + +Run this command to set both environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 && export ARBITRUM_SEPOLIA_CONTRACT=0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +======================================== + +IMPORTANT: Fund the Acknowledger contract with native gas tokens on Arbitrum Sepolia + The Acknowledger pays CCIP fees (native) for sending acknowledgments back to the MessageTracker. +``` + +Export the contract addresses printed by the script: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT=0x... # MessageTracker +export ARBITRUM_SEPOLIA_CONTRACT=0x... # Acknowledger +``` + +#### 3. Fund the Acknowledger (required) + +The Acknowledger pays CCIP fees for acknowledgment messages with native gas held by the contract. Fund it with Arbitrum Sepolia ETH before sending messages. + +Example (using cast): + +```bash filename="Terminal" +cast send $ARBITRUM_SEPOLIA_CONTRACT \ +--value 0.01ether \ +--account $KEYSTORE_NAME \ +--rpc-url $ARBITRUM_SEPOLIA_RPC_URL \ +--chain arbitrum-sepolia +``` + +If you don't have `cast` installed, you can fund the contract from any wallet by sending Arbitrum Sepolia ETH to `$ARBITRUM_SEPOLIA_CONTRACT`. + +#### 4. Configure allowlists and finality + +[configure.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/configure/configure.ts) + +Allowlists both contracts and configures receiver finality policy. + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +Configure both contracts in a single command: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/configure/configure.ts +``` + +Example output: + +```bash filename="Terminal" +======================================== +⚙️ Configure MessageTracker + Acknowledger Contracts +======================================== +Source Chain: Ethereum Sepolia + MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +Destination Chain: Arbitrum Sepolia + Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +======================================== + +[Step 1] Configuring MessageTracker on Ethereum Sepolia +Allowlisting Arbitrum Sepolia as destination chain on MessageTracker... +✅ Destination chain allowlisted on MessageTracker: Arbitrum Sepolia +Allowlisting Acknowledger (0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a) from Arbitrum Sepolia on MessageTracker... +✅ Acknowledger allowlisted on MessageTracker: Arbitrum Sepolia -> 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +Setting allowed finality config on MessageTracker to 0x00000020 (BLOCK_DEPTH=32)... +✅ Allowed finality config set on MessageTracker to 0x00000020 (BLOCK_DEPTH=32) for Arbitrum Sepolia + +======================================== +✅ MessageTracker Configuration Complete on Ethereum Sepolia! +======================================== + +[Step 2] Configuring Acknowledger on Arbitrum Sepolia +Allowlisting MessageTracker (0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123) from Ethereum Sepolia on Acknowledger... +✅ MessageTracker allowlisted on Acknowledger: Ethereum Sepolia -> 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +Allowlisting Ethereum Sepolia as destination chain on Acknowledger (for ack messages)... +✅ Source chain allowlisted as destination on Acknowledger: Ethereum Sepolia +Setting allowed finality config on Acknowledger to 0x00000020 (BLOCK_DEPTH=32)... +✅ Allowed finality config set on Acknowledger to 0x00000020 (BLOCK_DEPTH=32) for Ethereum Sepolia + +======================================== +✅ Acknowledger Configuration Complete on Arbitrum Sepolia! +======================================== + +======================================== +✅ All Configurations Complete! +======================================== +Ethereum Sepolia (MessageTracker) can send messages to Arbitrum Sepolia (Acknowledger) +Arbitrum Sepolia (Acknowledger) can receive messages from Ethereum Sepolia and send acknowledgments back + +Next Step: Send a Message +Send a message paying with LINK: +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA FEE_TOKEN=LINK GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT MESSAGE='Hello World From Hardhat Script for CCIP 2.0!' npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/send-message.ts +======================================== +``` + +#### 5. Send a message (A→B) + +[send-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/send-message.ts) + +Sends a message from MessageTracker to Acknowledger. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` environment variable controls send-side finality behavior: + +- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality. +- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth. + +##### Example 1: Send a message paying with native: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT \ +MESSAGE="Hello World From Hardhat" \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/send-message.ts +``` + +To request numeric faster than finality (minimum 32), set `BLOCK_DEPTH=32`. + +Example output: + +```bash filename="Terminal" +======================================== +📡 CCIP Message (A→B→A) - Pay with ETH +======================================== +Source Chain: Ethereum Sepolia + MessageTracker: 0x6c16098831403aac46eb0bbd989454d0d890ecab +Destination Chain: Arbitrum Sepolia + Acknowledger: 0x4f8a2c6e0b4d8a1f6e3c9b5d7a2f4c8e0b6d3a5c +Fee Token: Native (ETH) +Message: "Hello World From Hardhat" +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=500000, finalityConfig=0x00000000). +[Pre-validation] CCIP fee: 300138348995647 + +[Step 1] Sending message via MessageTracker with native fee (ETH)... +Required CCIP fee (in WEI): 300138348995647 + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0xaf1e2d3c4b5a69788796a5b4c3d2e1f00918273a4b5c6d7e8f90a1b2c3d4e5f6 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0xaf1e2d3c4b5a69788796a5b4c3d2e1f00918273a4b5c6d7e8f90a1b2c3d4e5f6 + +Track message status (1 = Sent, 2 = ProcessedOnDestination): +SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0xaf1e2d3c4b5a69788796a5b4c3d2e1f00918273a4b5c6d7e8f90a1b2c3d4e5f6 npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/get-message-status.ts +======================================== +``` + +##### Example 2: Send a message paying with LINK (optional): + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=LINK GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT \ +MESSAGE="Hello World From Hardhat" \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/send-message.ts +``` + +To request numeric faster than finality (minimum 32), set `BLOCK_DEPTH=32`. + +Example output: + +```bash filename="Terminal" +======================================== +📡 CCIP Message (A→B→A) - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia + MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +Destination Chain: Arbitrum Sepolia + Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +Fee Token: LINK +Message: "Hello World From Hardhat" +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=500000, finalityConfig=0x00000000). +[Pre-validation] CCIP fee: 16700123456789000 + +[Step 1] Approving MessageTracker to spend LINK for CCIP fees... +Required CCIP fee (in LINK units): 16700123456789000 +✅ MessageTracker approved to spend LINK + + +[Step 2] Sending message via MessageTracker... + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 + +Track message status (1 = Sent, 2 = ProcessedOnDestination): +SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/get-message-status.ts +======================================== +``` + +Copy the `CCIP messageId` from the output. + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| `KEYSTORE_NAME` | Hardhat keystore entry name, set in `.env` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address per chain (MessageTracker on source, Acknowledger on destination) | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `GAS_LIMIT` | Gas limit for the destination callback | `500000` | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | +| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality config (used by the configure step). Set `BLOCK_DEPTH` to allow numeric `BLOCK_DEPTH` requests. Omit for default-finality-only. | Not set | +| `ALLOWED_BLOCK_DEPTH` | Receiver minimum block depth (used by the configure step). Required when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`. Use `32` in this tutorial. | Not set | +| `MESSAGE` | Text payload to send | `Hello World From Hardhat Script for CCIP 2.0!` | +| `MESSAGE_ID` | Message ID to query status (used by the status script) | Not set | +| `CHAIN` | Chain name identifier used by the withdraw script (the chain where the Acknowledger is deployed) | Not set | + +#### 6. Track status (B→A acknowledgment) + +[get-message-status.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/get-message-status.ts) + +Reads message status from MessageTracker. + +Track the message status on the source chain: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x... \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/get-message-status.ts +``` + +Example output: + +```bash filename="Terminal" +======================================== +🔍 Check Message Status +======================================== +Chain: Ethereum Sepolia +MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 +Message ID: 0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 +======================================== + +======================================== +Message Status: 2 (ProcessedOnDestination) +✅ Message acknowledged! The Acknowledger has processed the message and the MessageTracker has received the acknowledgment. +Acknowledger Message ID: 0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e8 +Check the CCIP Explorer for the acknowledgment message: +https://ccip.chain.link/#/side-drawer/msg/0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e8 +======================================== +``` + +#### 7. Withdraw from Acknowledger (optional cleanup) + +[withdraw-from-acknowledger.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/withdraw-from-acknowledger.ts) + +Withdraws native balance from the Acknowledger contract. + +After testing, reclaim any remaining native balance from the Acknowledger: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/withdraw-from-acknowledger.ts +``` + +Example output: + +```bash filename="Terminal" +======================================== +💸 Withdraw from Acknowledger +======================================== +Chain: Arbitrum Sepolia +Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a +Native balance: 10000000000000000 wei +Beneficiary: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== + +[Step 1] Withdrawing native balance to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994... +✅ Native withdrawal complete (tx: 0xb0e1d2c3a49586778879a6b5c4d3e2f11029384756a6b7c8d9e0f1a2b3c4d5e6) + +======================================== +✅ Withdraw Complete +======================================== +Beneficiary: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +``` + +By default, funds are sent to the deployer account. Set `BENEFICIARY=0x...` to override the recipient. + +## Final note + +This tutorial uses a simple acknowledgment message to confirm delivery end-to-end. You can adapt the same pattern to: + +- programmable token transfers +- multi-step workflows that depend on destination-side execution +- onchain receipts that unlock follow-up actions on the source chain + +--- + +# Send Arbitrary Data +Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/send-arbitrary-data +Last Updated: 2026-05-01 + +In this tutorial, we will use CCIP to send arbitrary string data from one chain to another. + +We will send a `string` payload from *Ethereum Sepolia* to *Arbitrum Sepolia* twice: + +1. Once, using native gas tokens `ETH` and numeric faster than finality. +2. And then a second time, using `LINK` tokens and finalized finality (default). + +> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +> +> 1. Chainlink CCIP 2.0 supports [Faster-Than-Finality (FTF)](/ccip/concepts/execution-latency/ftf), which lets +> supported lanes process messages with a requested finality mode instead of always waiting for full finality. +> 2. In these tutorials, the scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`. +> 3. Supplying extraArgs that were encoded off-chain keeps the smart contract compatible with future CCIP versions +> without requiring an upgrade. We recommend against hardcoding extraArgs inside a smart contract. +> 4. We demonstrate FTF with a numeric block depth of `32` (`BLOCK_DEPTH=32`) in this tutorial; omit `BLOCK_DEPTH` (or +> set it to `0`/`DEFAULT`) to request `finalized` finality (`WAIT_FOR_FINALITY_FLAG`), which is the default. + +## Before you begin + +1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. +2. Your account must have some `ETH` and `LINK` tokens on *Ethereum Sepolia* and `ETH` tokens on *Arbitrum Sepolia*. Learn how to [Acquire testnet LINK](/resources/acquire-link). +3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different source and destination chain pair. + +## Examine the code + +#### 1. Initializing the contract + +When deploying the contract, we define the router address of the blockchain we deploy the contract on. +Defining the router address is useful for the following: + +- Sender part: + - Calls the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee) to estimate the CCIP fees. + - Calls the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend) to send CCIP messages. + +- Receiver part: + - The contract inherits from [CCIPReceiver](/ccip/evm/api-reference/v2.0.0/ccip-receiver), which serves as a base contract for receiver contracts. This contract requires that child contracts implement the `_ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#_ccipreceive). + - `_ccipReceive` is called by the `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive), which ensures that only the router can deliver CCIP messages to the receiver contract. + +Some key things to note: + +- `OwnerIsCreator` sets the deployer as the owner of the contract. +- The constructor passes the router address into `CCIPReceiver` at deployment time. +- `sendMessage` is `payable` and open to any caller. It handles both LINK and native fee payments: + - pass the LINK token address as `_feeTokenAddress` to pay in LINK, or + - `address(0)` to pay in native gas. +- The function accepts pre-encoded `_extraArgs` bytes built off-chain, making the contract forward-compatible with any extraArgs version. +- Access control is enforced through allowlisting: + - outbound messages are restricted by destination chain selector (`onlyAllowlistedDestinationChain`) + - inbound messages are restricted by source chain selector + source sender contract pair (`onlyAllowlisted`) +- The contract overrides `getCCVsAndFinalityConfig` from `CCIPReceiver` to advertise per-source-chain receiver finality policy to the OffRamp. See **Configure receiver finality policy** below. + +```solidity filename="Messenger.sol" +// Imports + +contract Messenger is CCIPReceiver, OwnerIsCreator { + constructor(address _router) CCIPReceiver(_router) {} + + // ... state variables, modifiers, allowlist admin functions ... + + function sendMessage( + uint64 _destinationChainSelector, + address _receiver, + string calldata _text, + address _feeTokenAddress, + bytes calldata _extraArgs + ) + external + payable + onlyAllowlistedDestinationChain(_destinationChainSelector) + validateReceiver(_receiver) + returns (bytes32 messageId) + { + messageId = _sendCCIPMessage(_destinationChainSelector, _receiver, _text, _feeTokenAddress, _extraArgs); + } + + // ... internal helpers (fee handling and message building) ... + + 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( /* ... */ ); + } + + // ... getCCVsAndFinalityConfig override and withdrawal utilities ... +} +``` + +#### 2. Build transaction payload + +`_sendCCIPMessage` calls the `_buildCCIPMessage` helper to build a CCIP message payload using the `EVM2AnyMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage). This payload is then passed to the router's `getFee` and `ccipSend` functions. + +The payload includes: + +- `receiver`: ABI-encoded destination address (`abi.encode(_receiver)`). +- `data`: ABI-encoded text payload (`abi.encode(_text)`). +- `tokenAmounts`: An empty `EVMTokenAmount` array because this tutorial sends data only. +- `extraArgs`: Pre-encoded message execution parameters built off-chain by a helper script. For finalized finality, the scripts encode V3 extraArgs with the finalized finality config. For non-default finality requests, the scripts probe whether the lane accepts V3 extraArgs for message-only sends and use V3 `requestedFinalityConfig` when available, or V2 extraArgs on pre-v2.0 lanes. +- `feeToken`: The token used to pay CCIP fees (`_feeTokenAddress`). Pass the LINK token address to pay in LINK, or `address(0)` to pay in native gas. + +> **NOTE: ExtraArgs are encoded off-chain** +> +> 1. The contract accepts pre-encoded `_extraArgs` bytes, so it does not need to know about specific encoding versions. +> The scripts handle lane detection and encoding. +> 2. For message-only sends, there is no transfer token or token pool to query. The helper detects lane support by +> probing the router's `getFee` function with V3 extraArgs, then falls back to V2 when needed. + +```solidity filename="_buildCCIPMessage" +function _buildCCIPMessage( + address _receiver, + string calldata _text, + address _feeTokenAddress, + bytes calldata _extraArgs +) private pure returns (Client.EVM2AnyMessage memory) { + return Client.EVM2AnyMessage({ + receiver: abi.encode(_receiver), + data: abi.encode(_text), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: _extraArgs, + feeToken: _feeTokenAddress + }); +} +``` + +> **CAUTION: Best Practices** +> +> This example is simplified for educational purposes. For production code, follow these best practices: +> +> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). +> +> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). +> +> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) +> +> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> +> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). + +#### 3. Sending messages + +The public `sendMessage` function delegates to `_sendCCIPMessage`, which performs four operations: + +1. Builds the message payload by calling `_buildCCIPMessage`. See **Build transaction payload** for details. +2. Computes the fees by invoking the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee). +3. Pulls ERC-20 fee tokens from the caller and grants the router the required approval, or validates that `msg.value` covers the fee when paying in native gas. +4. Dispatches the CCIP message by executing the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend). If paying in native gas (`_feeTokenAddress == address(0)`), the fee is forwarded via `{value: ccipFee}`. + +**Note**: As a security measure, `sendMessage` is protected by the `onlyAllowlistedDestinationChain` and `validateReceiver` modifiers. Any caller can invoke it; access is governed by the destination chain allowlist, not ownership. + +```solidity filename="_sendCCIPMessage" +function _sendCCIPMessage( + uint64 _destinationChainSelector, + address _receiver, + string calldata _text, + address _feeTokenAddress, + bytes calldata _extraArgs +) private returns (bytes32 messageId) { + Client.EVM2AnyMessage memory evm2AnyMessage = + _buildCCIPMessage(_receiver, _text, _feeTokenAddress, _extraArgs); + + IRouterClient router = IRouterClient(this.getRouter()); + uint256 ccipFee = router.getFee(_destinationChainSelector, evm2AnyMessage); + + _handleFeeApprovals(router, _feeTokenAddress, ccipFee); + + if (_feeTokenAddress == address(0)) { + messageId = router.ccipSend{value: ccipFee}(_destinationChainSelector, evm2AnyMessage); + } else { + messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); + } + + emit MessageSent(messageId, _destinationChainSelector, _receiver, _text, _feeTokenAddress, ccipFee); + + return messageId; +} +``` + +#### 4. Handling fees and approvals + +The contract uses a **pull-from-caller** model for ERC-20 fees: when a user calls `sendMessage` with an ERC-20 fee token, the contract pulls the required fee from `msg.sender` via `safeTransferFrom`, then approves the Router to spend it via `forceApprove`. The caller must `approve` this contract **before** calling `sendMessage`. + +`_handleFeeApprovals` handles two scenarios: + +1. **Native fee** (`_feeTokenAddress == address(0)`): Validates that `msg.value` covers the CCIP fee. +2. **ERC-20 fee token**: Pulls `ccipFee` from the caller and approves the Router for the same amount. + +> **NOTE: Approve the contract, not the Router** +> +> Because the contract pulls ERC-20 fees from the caller, your scripts must call `approve(contractAddress, ccipFee)` -- +> **not** `approve(routerAddress, ccipFee)`. This is a key difference from older CCIP patterns where the contract was +> pre-funded. + +```solidity filename="_handleFeeApprovals" +function _handleFeeApprovals(IRouterClient _router, address _feeTokenAddress, uint256 _ccipFee) private { + if (_feeTokenAddress == address(0)) { + if (msg.value < _ccipFee) { + revert InsufficientNativeForFees(msg.value, _ccipFee); + } + } else { + IERC20(_feeTokenAddress).safeTransferFrom(msg.sender, address(this), _ccipFee); + IERC20(_feeTokenAddress).forceApprove(address(_router), _ccipFee); + } +} +``` + +#### 5. Receiving messages + +On the destination blockchain, the router calls the inherited `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive), which verifies the caller is the router and then invokes the contract's internal `_ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#_ccipreceive). The `_ccipReceive` function expects an `Any2EVMMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#any2evmmessage) that contains: + +- The CCIP `messageId`. +- The `sourceChainSelector`. +- The `sender` address in bytes format. The address is decoded from bytes to an Ethereum address using the [ABI specifications](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html) and stored in `s_lastReceivedSender`. +- The `data`, which is also in bytes format. Given a `string` is expected, the data is decoded from bytes to a string using the [ABI specifications](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html). + +**Note**: Two important security measures are applied: + +- `_ccipReceive` is called by the `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive), which ensures that only the router can deliver CCIP messages to the receiver contract. See the `onlyRouter` [modifier](/ccip/evm/api-reference/v2.0.0/ccip-receiver#onlyrouter-modifier) for more information. +- The modifier `onlyAllowlisted` ensures that only a call from an allowlisted source chain and sender pair is accepted. + +```solidity filename="_ccipReceive" +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( + any2EvmMessage.messageId, + any2EvmMessage.sourceChainSelector, + s_lastReceivedSender, + s_lastReceivedText + ); +} +``` + +#### 6. Configure receiver finality policy + +> **CAUTION: Keep the default verifier; add CCVs as defense in depth** +> +> CCIP verifies every message with its default **committee verifier**. Additional CCVs are defense in depth on top of +> it, not a replacement. The recommended security posture is to keep the committee verifier as your default CCV and +> require additional CCVs only where you need them. See [verification models](/ccip/concepts/ccvs/verification-models) +> and the [trust and responsibility model](/ccip/concepts/ccvs/trust-responsibility-model). + +The receiver exposes an `allowedFinalityConfig` for each source chain. This value tells CCIP which finality modes the receiver accepts for messages from that chain. The sender scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`, then validate the request against the receiver policy (and the token pool policy, when applicable) before sending. + +CCIP 2.0 supports two finality request styles: + +- **Default finality (finalized)**: Omit `BLOCK_DEPTH` (or set `BLOCK_DEPTH=DEFAULT`). +- **Numeric block depth (faster than finality)**: Set `BLOCK_DEPTH=32` (or higher). This tutorial standardizes on `32`. + +This contract uses two functions to manage receiver-side finality policy: + +1. **`setAllowedFinalityConfig`**: An owner-only setter that stores the `FinalityCodec`-encoded policy for a source chain. +2. **`getCCVsAndFinalityConfig`**: The OffRamp and scripts can call this hook to read the receiver's CCV and finality policy. This tutorial does not configure custom CCVs, so it returns empty CCV arrays and `optionalThreshold = 0`. + +```solidity filename="Receiver finality policy" +function setAllowedFinalityConfig(uint64 _sourceChainSelector, bytes4 _allowedFinalityConfig) external onlyOwner { + s_allowedFinalityConfig[_sourceChainSelector] = _allowedFinalityConfig; + emit AllowedFinalityConfigSet(_sourceChainSelector, _allowedFinalityConfig); +} + +function getCCVsAndFinalityConfig(uint64 sourceChainSelector, bytes calldata sender) + external + view + override + returns ( + address[] memory requiredCCVs, + address[] memory optionalCCVs, + uint8 optionalThreshold, + bytes4 allowedFinalityConfig + ) +{ + address decodedSender = abi.decode(sender, (address)); + if (!allowlistedChainSenders[sourceChainSelector][decodedSender]) { + revert SenderNotAllowedForChain(sourceChainSelector, decodedSender); + } + + requiredCCVs = new address[](0); + optionalCCVs = new address[](0); + optionalThreshold = 0; + allowedFinalityConfig = s_allowedFinalityConfig[sourceChainSelector]; +} +``` + +[Messenger.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/send-arbitrary-data/Messenger.sol) + +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. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a +> Foundry dev environment from scratch. + +#### 1. Bootstrap a new Foundry project + +[Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the Foundry Starter Kit for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. If you don't already have a Foundry keystore, use the `cast` command to create a new one. Here, `your_keystore_name` is the alias you assign to this keystore entry. Foundry will prompt you to enter the actual private key and a password to encrypt it: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +3. Install dependencies: + +```bash filename="Terminal" +npm install +``` + +4. Create a `.env` file by copying the example file, and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +5. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +> **CAUTION: Run this in every new terminal session** +> +> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and +> confirm it prints your URL. + +6. Run the following command to compile all the contracts: + +```bash filename="Terminal" +forge build +``` + +#### 2. Deploy your contracts + +[Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data/deploy/Deploy.s.sol) + +Check out the complete script code on Github. + +In this section, you will deploy the contracts on the source and destination chains. The next step configures the sender contract to allow the destination chain and the receiver contract to allow the source chain-sender pair. + +The `Deploy.s.sol` script does the following: + +- Deploys `Messenger` on *Ethereum Sepolia* (source). +- Deploys `Messenger` on *Arbitrum Sepolia* (destination). +- Returns the contract addresses in the terminal. + +To run the script, use the following command: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/send-arbitrary-data/deploy/Deploy.s.sol \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🚀 Deploy CCIP Messenger Contracts on Both Chains +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +======================================== + + +[Step 1] Deploying Messenger on Ethereum Sepolia +Contract deployed at: 0x385240F511f64c25739fD05fC28a523649086491 +https://sepolia.etherscan.io/address/0x385240F511f64c25739fD05fC28a523649086491 + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Source Contract Address: 0x385240F511f64c25739fD05fC28a523649086491 + + +[Step 2] Deploying Messenger on Arbitrum Sepolia +Contract deployed at: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +https://sepolia.arbiscan.io/address/0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Destination Contract Address: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F + +======================================== +✅ All Deployments Complete! +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0x385240F511f64c25739fD05fC28a523649086491 +https://sepolia.etherscan.io/address/0x385240F511f64c25739fD05fC28a523649086491 + +Destination Chain: Arbitrum Sepolia +Destination Contract: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +https://sepolia.arbiscan.io/address/0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F + +Run this command to set both environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0x385240F511f64c25739fD05fC28a523649086491 && export ARBITRUM_SEPOLIA_CONTRACT=0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +======================================== +``` + +Before moving on, export the addresses of the previously deployed contracts: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && \ +export ARBITRUM_SEPOLIA_CONTRACT= +``` + +#### 3. Configure allowlists and finality + +As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair. + +[Configure.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol) + +Check out the complete script code on Github. + +The `Configure.s.sol` script handles both sides in a single command: + +- allowlists the destination chain on the sender contract +- allowlists the chain-sender pair on the receiver contract +- sets the receiver contract's allowed finality config for the source chain + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \ +forge script foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +To allow numeric faster than finality requests, set `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` and choose a minimum `ALLOWED_BLOCK_DEPTH`. In this tutorial, `ALLOWED_BLOCK_DEPTH=32` allows send-time requests of `BLOCK_DEPTH=32` (or higher). + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Configure CCIP Messenger Contracts on Both Chains +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0x385240F511f64c25739fD05fC28a523649086491 +Destination Chain: Arbitrum Sepolia +Destination Contract: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +======================================== + + +[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 0x385240F511f64c25739fD05fC28a523649086491 from Ethereum Sepolia... +✅ Chain-sender pair allowlisted: Ethereum Sepolia -> 0x385240F511f64c25739fD05fC28a523649086491 +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 +``` + +**Optional: enable bidirectional messaging:** If you want the destination contract to also send messages back to the source, run the reversed command: + +```bash filename="Terminal" +SOURCE_CHAIN=ARBITRUM_SEPOLIA DEST_CHAIN=ETHEREUM_SEPOLIA ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \ +forge script foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +#### 4. Send a message + +`SendMessage.s.sol` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: + +- Builds off-chain `extraArgs` for the lane. Foundry uses `ExtraArgsHelper.buildExtraArgs` to detect the lane version and encode V2 or V3. +- Approves the contract to spend the caller's ERC-20 fee token when paying with LINK or another ERC-20. +- Sends the CCIP message. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` environment variable controls send-side finality behavior: + +- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality. +- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth. + +The scripts detect lane support and encode V2 or V3 `extraArgs` as needed. + +##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=NATIVE GAS_LIMIT=200000 BLOCK_DEPTH=32 \ +MESSAGE="Hello from Foundry" \ +forge script foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage.s.sol \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Data-Only Message - Pay with Native +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x385240F511f64c25739fD05fC28a523649086491 +Receiver: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +Fee Token: Native (ETH) +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Gas limit (override): 200000 +V3 extraArgs accepted by lane. +Receiver contract ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s)) +✅ Using V3 extraArgs with FTF (gasLimit=200000, finalityConfig=0x00000020 (BLOCK_DEPTH: 32 block(s))). + +[Step 1] Sending CCIP message with native fee ( ETH )... +Required CCIP fee (in WEI): 10463107598943 + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607 +``` + +##### Example 2: Pay with LINK + finalized finality (default) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=LINK GAS_LIMIT=200000 \ +MESSAGE="Hello from Foundry" \ +forge script foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage.s.sol \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Data-Only Message - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x385240F511f64c25739fD05fC28a523649086491 +Receiver: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +Fee Token: LINK +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Gas limit (override): 200000 +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=200000, finalityConfig=0x00000000). + +[Step 1] Approving contract to spend fee token for CCIP fees... +Required CCIP fee (in token units): 16700123456789000 +✅ Contract approved to spend fee token + + +[Step 2] Sending CCIP message... + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60719 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60719 +``` + +If you use LINK or another ERC-20 fee token, the script approves the `Messenger` contract before sending. + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| `KEYSTORE_NAME` | Foundry encrypted keystore name used by `--account $KEYSTORE_NAME` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address per chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`, `ARBITRUM_SEPOLIA_CONTRACT`) | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `GAS_LIMIT` | Gas limit for the destination callback | `200000` | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | +| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality config (used by the configure step). Set `BLOCK_DEPTH` to allow numeric `BLOCK_DEPTH` requests. Omit for default-finality-only. | Not set | +| `ALLOWED_BLOCK_DEPTH` | Receiver minimum block depth (used by the configure step). Required when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`. Use `32` in this tutorial. | Not set | +| `MESSAGE` | Text payload to send | `Hello World From Foundry Script for CCIP 2.0!` | +| `CHAIN` | Chain name identifier used by the receipt verification scripts (the chain where the receiver is deployed) | Not set | + +[SendMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage.s.sol) + +Check out the complete script code on Github. + +[ExtraArgsHelper.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/helper/ExtraArgsHelper.s.sol) + +Check out the lane-aware extraArgs helper on Github. + +#### 5. Verify receipt on the destination chain + +The `GetLastReceivedMessageDetails.s.sol` script queries the receiver contract for the last received message. This is a read-only operation: no transaction is broadcast. + +1. Run the script, passing the `CHAIN` where you deployed the receiver: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/send-arbitrary-data/interact/GetLastReceivedMessageDetails.s.sol +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🔍 Verify Received Message +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +======================================== + +Checking for received message... + +======================================== +✅ Message Received Successfully! +======================================== +Message ID: 0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607 +Sender: 0x385240F511f64c25739fD05fC28a523649086491 +Received Text: "Hello from Foundry" +======================================== +``` + +[GetLastReceivedMessageDetails.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data/interact/GetLastReceivedMessageDetails.s.sol) + +Check out the complete script code on Github. + +### Hardhat + +Best for developers who want a mature, TypeScript-based smart contract development framework. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a +> Hardhat dev environment from scratch. + +#### 1. Bootstrap a new Hardhat project + +[CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the starter kit (contains both Hardhat and Foundry code) for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. Copy the example environment file and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +3. Install dependencies and compile: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +4. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +> **CAUTION: Run this in every new terminal session** +> +> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and +> confirm it prints your URL. + +5. Create a Hardhat keystore entry for your private key. Use the same name as `KEYSTORE_NAME` in your `.env` file. Hardhat will prompt you to enter the private key and a password to encrypt it: + +```bash filename="Terminal" +npx hardhat keystore set your_keystore_name +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +#### 2. Deploy your contracts + +[deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data/deploy/deploy.ts) + +Check out the complete script code on Github. + +In this section, you will deploy the contracts on the source and destination chains. The next step configures the sender contract to allow the destination chain and the receiver contract to allow the source chain-sender pair. + +The `deploy.ts` script does the following: + +- Deploys `Messenger` on *Ethereum Sepolia* (source). +- Deploys `Messenger` on *Arbitrum Sepolia* (destination). +- Returns the contract addresses in the terminal. + +To run the script, use the following command: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/deploy/deploy.ts +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🚀 Deploy CCIP Messenger Contracts on Both Chains +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +======================================== + +[Step 1] Deploying Messenger on Ethereum Sepolia +Contract deployed at: 0x385240F511f64c25739fD05fC28a523649086491 +https://sepolia.etherscan.io/address/0x385240F511f64c25739fD05fC28a523649086491 + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Source Contract Address: 0x385240F511f64c25739fD05fC28a523649086491 + +[Step 2] Deploying Messenger on Arbitrum Sepolia +Contract deployed at: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +https://sepolia.arbiscan.io/address/0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Destination Contract Address: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F + +======================================== +✅ All Deployments Complete! +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0x385240F511f64c25739fD05fC28a523649086491 +https://sepolia.etherscan.io/address/0x385240F511f64c25739fD05fC28a523649086491 + +Destination Chain: Arbitrum Sepolia +Destination Contract: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +https://sepolia.arbiscan.io/address/0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F + +Run this command to set both environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0x385240F511f64c25739fD05fC28a523649086491 && export ARBITRUM_SEPOLIA_CONTRACT=0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +======================================== +``` + +Before moving on, export the addresses of the previously deployed contracts: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && \ +export ARBITRUM_SEPOLIA_CONTRACT= +``` + +#### 3. Configure allowlists and finality + +As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair. + +[configure.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data/configure/configure.ts) + +Check out the complete script code on Github. + +The `configure.ts` script handles both sides in a single command: + +- allowlists the destination chain on the sender contract +- allowlists the chain-sender pair on the receiver contract +- sets the receiver contract's allowed finality config for the source chain + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/configure/configure.ts +``` + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +To allow numeric faster than finality requests, set `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` and choose a minimum `ALLOWED_BLOCK_DEPTH`. In this tutorial, `ALLOWED_BLOCK_DEPTH=32` allows send-time requests of `BLOCK_DEPTH=32` (or higher). + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Configure CCIP Messenger Contracts on Both Chains +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0x385240F511f64c25739fD05fC28a523649086491 +Destination Chain: Arbitrum Sepolia +Destination Contract: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +======================================== + +[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 0x385240F511f64c25739fD05fC28a523649086491 from Ethereum Sepolia... +✅ Chain-sender pair allowlisted: Ethereum Sepolia -> 0x385240F511f64c25739fD05fC28a523649086491 +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 +``` + +**Optional: enable bidirectional messaging:** If you want the destination contract to also send messages back to the source, run the reversed command: + +```bash filename="Terminal" +SOURCE_CHAIN=ARBITRUM_SEPOLIA DEST_CHAIN=ETHEREUM_SEPOLIA ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/configure/configure.ts +``` + +#### 4. Send a message + +`send-message.ts` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: + +- Builds off-chain `extraArgs` for the lane. Hardhat uses `buildMessageOnlyExtraArgs` from `hardhat/scripts/extra-args.ts` to detect whether the message-only lane accepts V3 extraArgs and to encode V2 or V3. +- Approves the contract to spend the caller's ERC-20 fee token when paying with LINK or another ERC-20. +- Sends the CCIP message. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` environment variable controls send-side finality behavior: + +- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality. +- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth. + +The scripts detect lane support and encode V2 or V3 `extraArgs` as needed. + +##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=NATIVE GAS_LIMIT=200000 BLOCK_DEPTH=32 \ +MESSAGE="Hello from Hardhat" \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/interact/send-message.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Data-Only Message - Pay with ETH +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x385240F511f64c25739fD05fC28a523649086491 +Receiver: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +Fee Token: Native (ETH) +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +V3 extraArgs accepted by lane. +Receiver contract ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s)) +✅ Using V3 extraArgs with FTF (gasLimit=200000, finalityConfig=32 block(s)). +[Pre-validation] CCIP fee: 10463107598943 + +[Step 1] Sending CCIP message with native token fee (ETH)... +Required CCIP fee (in WEI): 10463107598943 + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607 +``` + +##### Example 2: Pay with LINK + finalized finality (default) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=LINK GAS_LIMIT=200000 \ +MESSAGE="Hello from Hardhat" \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/interact/send-message.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Data-Only Message - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x385240F511f64c25739fD05fC28a523649086491 +Receiver: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +Fee Token: LINK +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=200000, finalityConfig=0x00000000). +[Pre-validation] CCIP fee: 16700123456789000 + +[Step 1] Approving contract to spend LINK for CCIP fees... +Required CCIP fee (in LINK units): 16700123456789000 +✅ Contract approved to spend LINK + + +[Step 2] Sending CCIP message... + +======================================== +✅ Message sent successfully! +======================================== +CCIP messageId: 0x2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60719 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60719 +``` + +If you use LINK or another ERC-20 fee token, the script approves the `Messenger` contract before sending. + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| `KEYSTORE_NAME` | Hardhat keystore entry name, set in `.env` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address per chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`, `ARBITRUM_SEPOLIA_CONTRACT`) | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `GAS_LIMIT` | Gas limit for the destination callback | `200000` | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | +| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality config (used by the configure step). Set `BLOCK_DEPTH` to allow numeric `BLOCK_DEPTH` requests. Omit for default-finality-only. | Not set | +| `ALLOWED_BLOCK_DEPTH` | Receiver minimum block depth (used by the configure step). Required when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`. Use `32` in this tutorial. | Not set | +| `MESSAGE` | Text payload to send | `Hello World From Hardhat Script for CCIP 2.0!` | +| `CHAIN` | Chain name identifier used by the receipt verification scripts (the chain where the receiver is deployed) | Not set | + +[send-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data/interact/send-message.ts) + +Check out the complete script code on Github. + +[extra-args.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/extra-args.ts) + +Check out the lane-aware extraArgs helper used by Hardhat scripts. + +#### 5. Verify receipt on the destination chain + +[get-last-received-message-details.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data/interact/get-last-received-message-details.ts) + +Check out the complete script code on Github. + +The `get-last-received-message-details.ts` script queries the receiver contract for the last received message. This is a read-only operation: no transaction is broadcast. + +1. Run the script, passing the `CHAIN` where you deployed the receiver: + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/interact/get-last-received-message-details.ts +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🔍 Verify Received Message +======================================== +Chain: Arbitrum Sepolia +Receiver Address: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F +======================================== + +Checking for received message... + +======================================== +✅ Message Received Successfully! +======================================== +Message ID: 0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607 +Sender: 0x385240F511f64c25739fD05fC28a523649086491 +Received Text: "Hello from Hardhat" +======================================== +``` + +--- + +# Transfer Tokens Between Chains from Smart Contracts +Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/transfer-tokens-from-contract +Last Updated: 2026-05-01 + +In this tutorial, we will use CCIP to transfer `CCIP-BnM` tokens from a smart contract on *Ethereum Sepolia* to an externally owned account (EOA) on *Arbitrum Sepolia*. The example uses off-chain `extraArgs` encoding so the scripts can select the correct finality configuration for the lane. + +We will send the transfer twice: + +1. Once, paying CCIP fees in native gas tokens `ETH` and using faster than finality (`BLOCK_DEPTH=32`). +2. And again, paying CCIP fees in `LINK` and using finalized finality (default). + +> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +> +> 1. Chainlink CCIP 2.0 supports [Faster-Than-Finality (FTF)](/ccip/concepts/execution-latency/ftf), which lets +> supported lanes process messages with a requested finality mode instead of always waiting for full finality. +> 2. In these tutorials, the scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`. +> 3. Supplying extraArgs that were encoded off-chain keeps the smart contract compatible with future CCIP versions +> without requiring an upgrade. We recommend against hardcoding extraArgs inside a smart contract. +> 4. We demonstrate FTF with a numeric block depth of `32` (`BLOCK_DEPTH=32`) in this tutorial; omit `BLOCK_DEPTH` (or +> set it to `0`/`DEFAULT`) to request `finalized` finality (`WAIT_FOR_FINALITY_FLAG`), which is the default. + +## Before you begin + +1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. +2. Your account must have some `ETH` and `LINK` tokens on *Ethereum Sepolia*. Learn how to [Acquire testnet LINK](/resources/acquire-link). +3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination chains/tokens. +4. [Acquire CCIP test tokens](/ccip/getting-started/test-tokens#evm-chains). You should have `CCIP-BnM` tokens, and `CCIP-BnM` should appear in the list of your tokens in MetaMask. + +## Examine the code + +#### 1. Initializing the contract + +When deploying the contract, you provide the CCIP router address for the source chain. +The router address is useful for the following: + +- Calls the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee) to estimate CCIP fees. +- Calls the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend) to send CCIP messages. + +Some key things to note: + +- `OwnerIsCreator` sets the deployer as the owner of the contract. +- This is a sender-only contract: it does not inherit from `CCIPReceiver` because no receiver callback is executed on the destination chain. Tokens can be sent to any address (EOA or contract) on the destination chain. +- `sendMessage` is `payable` and open to any caller (not restricted to the owner). It handles both LINK and native fee payments: pass the LINK token address as `_feeTokenAddress` to pay in LINK, or `address(0)` to pay in native gas. The function accepts pre-encoded `_extraArgs` bytes built off-chain, making the contract forward-compatible with any extraArgs version. +- Access control is enforced through allowlisting: destination chains are gated by the `onlyAllowlistedDestinationChain` modifier, and `validateReceiver` prevents sending to the zero address. + +```solidity filename="TokenTransferor.sol" +contract TokenTransferor is OwnerIsCreator { + using SafeERC20 for IERC20; + + IRouterClient private s_router; + + constructor(address _router) { + s_router = IRouterClient(_router); + } + + // ... modifiers, allowlist admin functions ... + + function sendMessage( + uint64 _destinationChainSelector, + address _receiver, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) + external + payable + onlyAllowlistedDestinationChain(_destinationChainSelector) + validateReceiver(_receiver) + returns (bytes32 messageId) + { + messageId = _sendCCIPMessage( + _destinationChainSelector, _receiver, _token, _amount, _feeTokenAddress, _extraArgs + ); + } + + function getFee( + uint64 _destinationChainSelector, + address _receiver, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) external view returns (uint256 fees) { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _receiver, _token, _amount, _feeTokenAddress, _extraArgs + ); + fees = s_router.getFee(_destinationChainSelector, evm2AnyMessage); + } + + // ... internal helpers (message building, fee handling, approvals) ... +} +``` + +#### 2. Build transaction payload + +`_sendCCIPMessage` calls the `_buildCCIPMessage` helper to build a CCIP message payload using `EVM2AnyMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage). \ +This payload is then passed to the router's `getFee` and `ccipSend` functions. + +The payload includes: + +- `receiver`: ABI-encoded destination address (`abi.encode(_receiver)`). +- `data`: Empty (`""`) because this tutorial transfers tokens only (no data payload). +- `tokenAmounts`: A 1-element array containing the token address and amount to transfer. +- `extraArgs`: Pre-encoded message execution parameters built off-chain by a helper script. For finalized finality (default), the scripts encode V3 extraArgs with finalized finality. For faster than finality requests (`BLOCK_DEPTH=32`), the scripts detect lane support and use V3 extraArgs with `requestedFinalityConfig` on FTF-capable lanes, or V2 extraArgs on pre-v2.0 lanes. +- `feeToken`: The token used to pay CCIP fees (`_feeTokenAddress`). Pass the LINK token address to pay in LINK, or `address(0)` to pay in native gas. + +> **NOTE: ExtraArgs are encoded off-chain** +> +> 1. The contract accepts pre-encoded `_extraArgs` bytes, so it does not need to know about specific encoding versions. +> The scripts handle lane detection and encoding. +> 2. This makes the contract forward-compatible with future CCIP versions. + +```solidity filename="_buildCCIPMessage" + function _buildCCIPMessage( + address _receiver, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) private pure returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: _token, amount: _amount}); + + return Client.EVM2AnyMessage({ + receiver: abi.encode(_receiver), + data: "", + tokenAmounts: tokenAmounts, + extraArgs: _extraArgs, + feeToken: _feeTokenAddress + }); + } +``` + +> **CAUTION: Best Practices** +> +> This example is simplified for educational purposes. For production code, follow these best practices: +> +> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). +> +> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). +> +> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) +> +> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> +> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). + +#### 3. Sending tokens + +The public `sendMessage` function delegates to `_sendCCIPMessage`, which performs four operations: + +1. Builds the message payload by calling `_buildCCIPMessage`. See **Build transaction payload** for details. +2. Computes the fees by invoking the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee). +3. Pulls tokens from the caller and grants the router the required approvals by calling `_handleFeeAndTokenApprovals`. See **Handling fees and token approvals** for details. +4. Dispatches the CCIP message by executing the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend). If paying in native gas (`_feeTokenAddress == address(0)`), the fee is forwarded via `{value: ccipFee}`. + +**Note**: As a security measure, `sendMessage` is protected by the `onlyAllowlistedDestinationChain` and `validateReceiver` modifiers. Any caller can invoke it -- access is governed by the destination chain allowlist, not ownership. + +```solidity filename="_sendCCIPMessage" + function _sendCCIPMessage( + uint64 _destinationChainSelector, + address _receiver, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes calldata _extraArgs + ) private returns (bytes32 messageId) { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _receiver, _token, _amount, _feeTokenAddress, _extraArgs + ); + + uint256 ccipFee = s_router.getFee(_destinationChainSelector, evm2AnyMessage); + + _handleFeeAndTokenApprovals(s_router, _token, _amount, _feeTokenAddress, ccipFee); + + if (_feeTokenAddress == address(0)) { + messageId = s_router.ccipSend{value: ccipFee}(_destinationChainSelector, evm2AnyMessage); + } else { + messageId = s_router.ccipSend(_destinationChainSelector, evm2AnyMessage); + } + + emit TokensTransferred( + messageId, _destinationChainSelector, _receiver, _token, _amount, _feeTokenAddress, ccipFee + ); + + return messageId; + } +``` + +#### 4. Handling fees and token approvals + +The contract uses a **pull-from-caller** model: when a user calls `sendMessage`, the contract pulls the required tokens from `msg.sender` via `safeTransferFrom`, then approves the Router to spend them via `forceApprove`. The caller (EOA or upstream contract) must `approve` this contract **before** calling `sendMessage`. + +`_handleFeeAndTokenApprovals` handles three scenarios: + +1. **Native fee** (`_feeTokenAddress == address(0)`): Validates that `msg.value` covers the CCIP fee. Pulls the transfer token from the caller via `safeTransferFrom` and approves the Router. +2. **Same token for fee and transfer** (`_token == _feeTokenAddress`): Pulls the combined total (`ccipFee + amount`) from the caller in one `safeTransferFrom`. Approves the Router for the combined total. +3. **Different ERC-20 tokens**: Pulls each token separately from the caller. Approves the Router for each. + +> **NOTE: Approve the contract, not the Router** +> +> Because the contract pulls tokens from the caller, your scripts must call `approve(contractAddress, amount)` -- +> **not** `approve(routerAddress, amount)`. This is a key difference from older CCIP patterns where the contract was +> pre-funded. + +```solidity filename="_handleFeeAndTokenApprovals" + function _handleFeeAndTokenApprovals( + IRouterClient _router, + address _token, + uint256 _amount, + address _feeTokenAddress, + uint256 _ccipFee + ) private { + if (_feeTokenAddress == address(0)) { + if (msg.value < _ccipFee) { + revert InsufficientNativeForFees(msg.value, _ccipFee); + } + + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + IERC20(_token).forceApprove(address(_router), _amount); + } else if (_token == _feeTokenAddress) { + uint256 totalAmount = _ccipFee + _amount; + IERC20(_token).safeTransferFrom(msg.sender, address(this), totalAmount); + IERC20(_token).forceApprove(address(_router), totalAmount); + } else { + IERC20(_feeTokenAddress).safeTransferFrom(msg.sender, address(this), _ccipFee); + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + IERC20(_feeTokenAddress).forceApprove(address(_router), _ccipFee); + IERC20(_token).forceApprove(address(_router), _amount); + } + } +``` + +[TokenTransferor.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/token-transfers/TokenTransferor.sol) + +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. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a +> Foundry dev environment from scratch. + +#### 1. Bootstrap a new Foundry project + +[Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the Foundry Starter Kit for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. If you don't already have a Foundry keystore, use the `cast` command to create a new one. Here, `your_keystore_name` is the alias you assign to this keystore entry -- Foundry will prompt you to enter the actual private key and a password to encrypt it: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +And use the `cast wallet list` command to verify: + +![Foundry keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-2.png) + +> **CAUTION: Note** +> +> As of writing, Foundry supports keystore encryption only for private keys (not generic strings), so in this tutorial +> we only encrypt the private key and store the keystore alias in `KEYSTORE_NAME`. + +3. Install dependencies: + +```bash filename="Terminal" +npm install +``` + +4. Create a `.env` file by copying the example file, and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +5. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +> **CAUTION: Run this in every new terminal session** +> +> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and +> confirm it prints your URL. + +6. Run the following command to compile all the contracts: + +```bash filename="Terminal" +forge build +``` + +#### 2. Deploy your contract + +In this section, you will deploy `TokenTransferor` on the source chain (*Ethereum Sepolia*). \ +Unlike the programmable token transfers tutorial, no destination contract is needed because tokens are transferred to an address (EOA or contract) on the destination chain. + +Run the script: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA forge script foundry/scripts/tutorials/token-transfers/deploy/Deploy.s.sol \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Your terminal output will look like this: + +```bash filename="Terminal" +======================================== +🚀 Deploy TokenTransferor Contract +======================================== +Source Chain: Ethereum Sepolia +======================================== + + +[Step 1] Deploying TokenTransferor on Ethereum Sepolia +Contract deployed at: 0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901 +https://sepolia.etherscan.io/address/0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901 + +======================================== +✅ Deployment Complete! +======================================== +Chain: Ethereum Sepolia +Contract: 0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901 +https://sepolia.etherscan.io/address/0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901 + +Run this command to set the environment variable: +export ETHEREUM_SEPOLIA_CONTRACT=0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901 +======================================== + +** Next Step: Configuration ** + +Allowlist the destination chain: +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA forge script foundry/scripts/tutorials/token-transfers/configure/Configure.s.sol:Configure --account $KEYSTORE_NAME --broadcast -vv +======================================== +``` + +Save the exported contract address from the output: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= +``` + +[Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/token-transfers/deploy/Deploy.s.sol) + +Check out the complete script code on Github. + +#### 3. Configure allowlists + +As a best practice, allowlist the destination chain before sending tokens. + +[Configure.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/token-transfers/configure/Configure.s.sol) + +Check out the complete script code on Github. + +1. If you haven't already, export the address of the deployed contract so that it's available in the terminal: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= +``` + +2. Run the configure script to allowlist the destination chain: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/token-transfers/configure/Configure.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +Your terminal output will look like this: + +```bash filename="Terminal" +======================================== +⚙️ Configure TokenTransferor +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0xD18B78Ce9f4dc76b65ECD22F784352041B92Adc1 +Destination Chain: Arbitrum Sepolia +======================================== + +Configuring TokenTransferor on Ethereum Sepolia +Allowlisting Arbitrum Sepolia as destination chain... +✅ Destination chain allowlisted: Arbitrum Sepolia + +======================================== +✅ Configuration Complete! +======================================== +Ethereum Sepolia can send tokens to Arbitrum Sepolia + +** Next Step: Transfer Tokens ** +``` + +#### 4. Fund your wallet with test tokens + +[DripBnMToken.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/faucet/DripBnMToken.s.sol) + +Check out the faucet script on Github. + +> **NOTE: Convenient way to print your wallet address** +> +> If you don't have the public address of your wallet handy, run `cast wallet address --account $KEYSTORE_NAME` to print +> it from the terminal. + +Before sending a CCIP message, you need `CCIP-BnM` test tokens in your wallet. The send scripts transfer `CCIP-BnM` from +your EOA to the contract, so your wallet must hold a balance. + +Use the faucet script included in the starter kit to drip `CCIP-BnM` tokens to your address: + +```bash filename="Terminal" +CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS= forge script foundry/scripts/faucet/DripBnMToken.s.sol --account $KEYSTORE_NAME --broadcast -vv +``` + +> **NOTE: Note** +> +> If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the +> [Chainlink faucet](https://faucets.chain.link/). + +#### 5. Transfer Tokens + +`SendMessage.s.sol` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: + +- Detects the lane version and encodes the correct extraArgs (V2 for pre-v2.0 lanes, V3 for v2.0+ lanes) via `ExtraArgsHelper.buildExtraArgs`. +- Approves the contract to spend the caller's tokens (the contract then pulls via `safeTransferFrom`). +- Sends the CCIP token transfer. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +Set the receiver address. This is the EOA or contract on the destination chain that will receive the transferred `CCIP-BnM` tokens: + +```bash filename="Terminal" +export RECEIVER_ADDRESS= +``` + +##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +RECEIVER_ADDRESS=$RECEIVER_ADDRESS \ +BLOCK_DEPTH=32 \ +forge script foundry/scripts/tutorials/token-transfers/interact/SendMessage.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Token Transfer - Pay with Native +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901 +Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Fee Token: Native (ETH) +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Token pool ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s)) +Receiver is an EOA — no receiver constraint. +✅ Using V3 extraArgs with FTF (gasLimit=0, 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 token transfer with native token fee ( ETH )... +Required CCIP fee (in WEI): 130463107598943 + +======================================== +✅ Tokens transferred successfully! +======================================== +CCIP messageId: 0xc1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0xc1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6 +``` + +##### Example 2: Pay with LINK + finalized finality (default) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +RECEIVER_ADDRESS=$RECEIVER_ADDRESS \ +FEE_TOKEN=LINK \ +forge script foundry/scripts/tutorials/token-transfers/interact/SendMessage.s.sol \ +--account $KEYSTORE_NAME --broadcast -vv +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Token Transfer - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901 +Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Fee Token: LINK +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=0, finalityConfig=0x00000000). + +[Step 1] Approving contract to spend fee token for CCIP fees... +Required CCIP fee (in token units): 26110523180283030 +✅ Contract approved to spend fee token + + +[Step 2] Approving contract to spend CCIP-BnM... +✅ Contract approved to spend CCIP-BnM + + +[Step 3] Sending CCIP token transfer... + +======================================== +✅ Tokens transferred successfully! +======================================== +``` + +##### Environment variables + +| Variable | Description | Default | +| ------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| `KEYSTORE_NAME` | Foundry encrypted keystore name used by `--account $KEYSTORE_NAME` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address on the source chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`) | Not set | +| `RECEIVER_ADDRESS` | Destination receiver address for the token transfer and balance verification | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `TOKEN_AMOUNT` | Amount of CCIP-BnM to transfer (in wei) | `1000000000000000` (0.001) | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | + +[SendMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/token-transfers/interact/SendMessage.s.sol) + +Check out the complete script code on Github. + +#### 6. Verify transfer on the destination chain + +After sending the transfer, you can verify it was successful: + +1. Use the **CCIP Explorer** link printed in the send output to track the message status in real time. + +2. Once the message is marked as delivered, check the receiver's token balance on the destination chain. You can use a block explorer or the following command: + +```bash filename="Terminal" +cast call "balanceOf(address)(uint256)" $RECEIVER_ADDRESS --rpc-url $ARBITRUM_SEPOLIA_RPC_URL --chain arbitrum-sepolia +``` + +Find the CCIP-BnM token address for each chain in the [CCIP Directory](/ccip/directory). + +### Hardhat + +Best for developers who want a mature, TypeScript-based smart contract development framework. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a +> Hardhat dev environment from scratch. + +#### 1. Bootstrap a new Hardhat project + +[CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) + +Clone the starter kit (contains both Hardhat and Foundry code) for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. Copy the example environment file and fill in your values: + +```bash filename="Terminal" +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: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY= +``` + +3. Install dependencies and compile: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +4. Create a Hardhat keystore entry for your private key. Use the same name as `KEYSTORE_NAME` in your `.env` file. Hardhat will prompt you to enter the private key and a password to encrypt it: + +```bash filename="Terminal" +npx hardhat keystore set your_keystore_name +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +#### 2. Deploy your contract + +[deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/token-transfers/deploy/deploy.ts) + +Check out the complete script code on Github. + +In this section, you will deploy `TokenTransferor` on the source chain (*Ethereum Sepolia*). No destination deployment is required. + +Run the script: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA npx hardhat run hardhat/scripts/tutorials/token-transfers/deploy/deploy.ts +``` + +Your terminal output will look like this: + +```bash filename="Terminal" +======================================== +🚀 Deploy TokenTransferor Contract +======================================== +Source Chain: Ethereum Sepolia +======================================== + + +[Step 1] Deploying TokenTransferor on Ethereum Sepolia +Contract deployed at: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1 +https://sepolia.etherscan.io/address/0x67b6e59e7b3d36ee90890935a1d13e87734e94d1 + +======================================== +✅ Deployment Complete! +======================================== +Chain: Ethereum Sepolia +Contract: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1 +https://sepolia.etherscan.io/address/0x67b6e59e7b3d36ee90890935a1d13e87734e94d1 + +Run this command to set the environment variable: +export ETHEREUM_SEPOLIA_CONTRACT=0x67b6e59e7b3d36ee90890935a1d13e87734e94d1 +======================================== + +** Next Step: Configuration ** + +Allowlist the destination chain: +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA npx hardhat run hardhat/scripts/tutorials/token-transfers/configure/configure.ts +======================================== +``` + +Before moving on, export the address of the deployed contract so that it's available in the terminal: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= +``` + +#### 3. Configure allowlists + +As a best practice, allowlist the destination chain before sending tokens. + +[configure.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/token-transfers/configure/configure.ts) + +Check out the complete script code on Github. + +1. The `configure.ts` script allowlists the destination chain on the sender contract: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/token-transfers/configure/configure.ts +``` + +Your terminal output will look like this: + +```bash filename="Terminal" +======================================== +⚙️ Configure TokenTransferor +======================================== +Source Chain: Ethereum Sepolia +Source Contract: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1 +Destination Chain: Arbitrum Sepolia +======================================== + +Configuring TokenTransferor on Ethereum Sepolia +Allowlisting Arbitrum Sepolia as destination chain... +✅ Destination chain allowlisted: Arbitrum Sepolia + +======================================== +✅ Configuration Complete! +======================================== +Ethereum Sepolia can send tokens to Arbitrum Sepolia + +** Next Step: Transfer Tokens ** + +Transfer tokens (pay with native gas, default): +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA RECEIVER_ADDRESS=$RECEIVER_ADDRESS BLOCK_DEPTH=DEFAULT npx hardhat run hardhat/scripts/tutorials/token-transfers/interact/send-message.ts + +Or pay with LINK: +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA RECEIVER_ADDRESS=$RECEIVER_ADDRESS FEE_TOKEN=LINK BLOCK_DEPTH=DEFAULT npx hardhat run hardhat/scripts/tutorials/token-transfers/interact/send-message.ts +======================================== +``` + +#### 4. Fund your wallet with test tokens + +[drip-bnm-token.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/faucet/drip-bnm-token.ts) + +Check out the faucet script on Github. + +Before sending a CCIP message, you need `CCIP-BnM` test tokens in your wallet. The send scripts transfer `CCIP-BnM` from your EOA to the contract, so your wallet must hold a balance. + +Use the faucet script included in the starter kit to drip `CCIP-BnM` tokens to your address: + +```bash filename="Terminal" +CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS= \ +npx hardhat run hardhat/scripts/faucet/drip-bnm-token.ts +``` + +If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the [Chainlink faucet](https://faucets.chain.link/). + +#### 5. Transfer Tokens + +`send-message.ts` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: + +- Queries lane features via `@chainlink/ccip-sdk` to encode the correct extraArgs (V2 for pre-v2.0 lanes, V3 for v2.0+ lanes). +- Approves the contract to spend the caller's tokens (the contract then pulls via `safeTransferFrom`). +- Sends the CCIP token transfer. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +Set the receiver address. This is the EOA or contract on the destination chain that will receive the transferred `CCIP-BnM` tokens: + +```bash filename="Terminal" +export RECEIVER_ADDRESS= +``` + +##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +RECEIVER_ADDRESS=$RECEIVER_ADDRESS \ +BLOCK_DEPTH=32 \ +npx hardhat run hardhat/scripts/tutorials/token-transfers/interact/send-message.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Token Transfer - Pay with ETH +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1 +Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Fee Token: Native (ETH) +======================================== + + +[Pre-validation] Querying lane features and building extraArgs... +Token pool ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s)) +Receiver is an EOA — no receiver constraint. +✅ Using V3 extraArgs with FTF (gasLimit=0, finalityConfig=32 block(s)). +[Pre-validation] CCIP fee: 130463107598943 + +[Step 1] Approving contract to spend CCIP-BnM... +✅ Contract approved to spend CCIP-BnM + + +[Step 2] Sending CCIP token transfer with native token fee (ETH)... +Required CCIP fee (in WEI): 130463107598943 + +======================================== +✅ Tokens transferred successfully! +======================================== +CCIP messageId: 0xd2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0xd2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607 +``` + +##### Example 2: Pay with LINK + finalized finality (default) + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +RECEIVER_ADDRESS=$RECEIVER_ADDRESS \ +FEE_TOKEN=LINK \ +npx hardhat run hardhat/scripts/tutorials/token-transfers/interact/send-message.ts +``` + +Your terminal should look like this: + +```bash filename="Terminal" +======================================== +📡 CCIP Token Transfer - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +Sender: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1 +Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Fee Token: LINK +======================================== + + +[Pre-validation] Querying lane features and building extraArgs... +✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=0, finalityConfig=0x00000000). +[Pre-validation] CCIP fee: 26110523180283030 + +[Step 1] Approving contract to spend LINK for CCIP fees... +Required CCIP fee (in LINK units): 26110523180283030 +✅ Contract approved to spend LINK + + +[Step 2] Approving contract to spend CCIP-BnM... +✅ Contract approved to spend CCIP-BnM + + +[Step 3] Sending CCIP token transfer... + +======================================== +✅ Tokens transferred successfully! +======================================== +CCIP messageId: 0xe3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0xe3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718 +``` + +##### Environment variables + +| Variable | Description | Default | +| ------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| `KEYSTORE_NAME` | Hardhat keystore entry name, set in `.env` | `your_keystore_name` | +| `SOURCE_CHAIN` | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`) | Not set | +| `DEST_CHAIN` | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`) | Not set | +| `{CHAIN}_RPC_URL` | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set | +| `{CHAIN}_CONTRACT` | Deployed tutorial contract address on the source chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`) | Not set | +| `RECEIVER_ADDRESS` | Destination receiver address for the token transfer and balance verification | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `TOKEN_AMOUNT` | Amount of CCIP-BnM to transfer (in wei) | `1000000000000000` (0.001) | +| `BLOCK_DEPTH` | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality | `DEFAULT` | + +[send-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/token-transfers/interact/send-message.ts) + +Check out the complete script code on Github. + +#### 6. Verify transfer on the destination chain + +After sending the transfer, you can verify it was successful: + +1. Use the **CCIP Explorer** link printed in the send output to track the message status in real time. + +2. Once the message is marked as delivered, check the receiver's token balance on the destination chain using a block explorer or any on-chain query tool. + +Find the CCIP-BnM token address for each chain in the [CCIP Directory](/ccip/directory). + +--- + +# Transfer USDC with Data +Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/usdc +Last Updated: 2026-09-19 + +USDC is a digital dollar backed 100% and is always redeemable 1:1 for US dollars. The [stablecoin](https://chain.link/education-hub/stablecoins) is issued by [Circle](https://www.circle.com/en/usdc) on multiple blockchain platforms. + +This guide will first explain how CCIP enables native USDC transfers when both the source and destination blockchains support [Circle's Cross-Chain Transfer Protocol (CCTP)](https://www.circle.com/en/cross-chain-transfer-protocol). + +Additionally, it will outline how CCIP also supports transferring Bridged USDC on blockchains that **are not** CCTP-enabled, allowing projects to later migrate to CCTP-enabled transfers if approved by Circle. + +The hands-on tutorial at the end demonstrates how to use Chainlink CCIP to transfer USDC and arbitrary data from a smart contract on *Ethereum Sepolia* to *Arbitrum Sepolia*. + +> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +> +> 1. Chainlink CCIP 2.0 supports [Faster-Than-Finality (FTF)](/ccip/concepts/execution-latency/ftf), which lets +> supported lanes process messages with a requested finality mode instead of always waiting for full finality. +> 2. In these tutorials, the scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`. +> 3. Supplying extraArgs that were encoded off-chain keeps the smart contract compatible with future CCIP versions +> without requiring an upgrade. We recommend against hardcoding extraArgs inside a smart contract. +> 4. We demonstrate FTF with a numeric block depth of `32` (`BLOCK_DEPTH=32`) in this tutorial; omit `BLOCK_DEPTH` (or +> set it to `0`/`DEFAULT`) to request `finalized` finality (`WAIT_FOR_FINALITY_FLAG`), which is the default. + +## Architecture + +Native USDC transfers through CCIP use Circle's [Cross-Chain Transfer Protocol (CCTP)](https://www.circle.com/en/cross-chain-transfer-protocol) when both the source and destination chains support it. Both chains on this lane: + +- *Ethereum Sepolia*, +- and *Arbitrum Sepolia*, + +run CCTP-enabled USDC token pools, so this tutorial moves **native USDC**: burned on the source chain, minted on the destination. + +> For chains without native CCTP, Circle's [Bridged USDC Standard](https://www.circle.com/blog/bridged-usdc-standard) lets teams deploy USDC early, with a seamless upgrade path to Native USDC later. + +CCIP maintains a consistent [API](/ccip/evm/api-reference/v2.0.0/i-router-client) regardless of whether the transfer involves Native USDC or Bridged USDC: + +- The sender interacts with the CCIP router to initiate a cross-chain transaction, just like any other token transfer. See the [Transfer Tokens](/ccip/evm/tutorials/application-developers/transfer-tokens-from-contract) guide to learn more. +- The process uses the same onchain components including the Router, OnRamp, OffRamp, and Token Pool. +- Offchain, messages are verified by independent Cross-Chain Verifiers (CCVs). Every lane runs a default, decentralized Committee Verifier, and USDC transfers are additionally verified by a CCTP Verifier that checks Circle's own attestation for the burn. +- Once the required verifiers have attested, the message is delivered on the destination chain through the OffRamp, which verifies each attestation before the USDC token pool mints USDC using Circle's attestation. +- Execution on destination is permissionless, as in, anyone can submit an attested message for execution, and the Chainlink Executor does so by default. +- All lanes remain protected by the Risk Management Network, which can halt sends and executions on a lane through its cursing mechanism if a safety issue is detected. + +The diagram below shows the native USDC flow: the USDC token pool burns tokens on the source chain via CCTP, the CCTP Verifier checks Circle's attestation for the burn, and the destination token pool mints USDC using that attestation once all required verifiers have signed off. To learn more about these components, read the [CCIP architecture overview](/ccip/concepts/architecture/overview). + +![Chainlink CCIP Detailed Architecture for USDC](/images/ccip/usdc-diagram.png) + +## Before you begin + +1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. +2. Your account must have some `ETH` and `LINK` tokens on *Ethereum Sepolia*, and `ETH` on *Arbitrum Sepolia* for deploying the destination contracts and redeeming `STK`. Learn how to [Acquire testnet LINK](/resources/acquire-link). +3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination chains/tokens. +4. Acquire USDC tokens from the [Circle faucet](https://faucet.circle.com/) on *Ethereum Sepolia*. + +## Examine the code + +Three contracts make this work, spread across two chains. \ +`USDCSender` sits on *Ethereum Sepolia* and starts the transfer. On *Arbitrum Sepolia*, `USDCReceiver` catches the incoming message and hands the USDC to `USDCStaker`, which mints a receipt token to whoever the sender nominated. + +#### 1. USDCSender (source chain) + +1. **Constructor.** Three addresses are stored as immutables and never change afterwards: the CCIP router, LINK, and USDC. + +2. **Receiver allowlist.** `s_receivers` maps a destination chain selector to the single receiver permitted on that chain. The owner manages entries through `setReceiverForDestinationChain` and `deleteReceiverForDestinationChain`. \ + Sending to a chain with no entry reverts with `NoReceiverOnDestinationChain`, and a zero selector is rejected before anything else happens. + +3. **The payload is a function call.** Most CCIP examples put arbitrary bytes in `data`. \ + Here it carries an encoded call instead, and that is the idea the tutorial rests on: + + ```solidity filename="USDCSender.sol" + data: abi.encodeWithSelector(IStaker.stake.selector, _beneficiary, _amount) + ``` + +4. **Token amounts.** An array with one entry holding USDC and the amount, since this contract only ever moves a single token. + +5. **Fee quoting.** `getFee` is a helper on this contract, not the router's own function. \ + It assembles the identical `EVM2AnyMessage` that `sendMessage` will build, then forwards it to the router, so both calls price exactly the same message. The only gap is fee drift between blocks, since `sendMessage` re-quotes when it runs. + +6. **Sending, and where the tokens come from.** `sendMessage` is restricted to the owner, which is the account that deployed the contract. It pulls USDC from `msg.sender` using `transferFrom`, and pulls the fee token the same way when you pay in an ERC-20. \ + You approve from your own wallet, so there is no need to move USDC into the sender contract beforehand. A zero amount reverts with `AmountIsZero`. + +7. **`extraArgs` arrive pre built and encoded.** The contract accepts encoded bytes and passes them through untouched. Keeping that encoding offchain is what lets one contract serve both pre-2.0 and 2.0 lanes without a redeploy. + +8. **Withdrawals.** `withdrawNativeToken`, `withdrawLinkToken`, and `withdrawUsdcToken` let the owner sweep out anything left behind, each reverting with `NothingToWithdraw` on an empty balance. + +> **CAUTION: Best Practices** +> +> This example is simplified for educational purposes. For production code, follow these best practices: +> +> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). +> +> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). +> +> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) +> +> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> +> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). + +[USDCSender.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/transfer-usdc-with-data/USDCSender.sol) + +View the complete contract on GitHub. + +#### 2. USDCStaker (destination chain) + +1. **It is an ERC-20 in its own right.** Staking mints a receipt token named `Simple Staker`, with the symbol `STK`. + +2. **Decimals follow USDC.** The constructor reads `decimals()` off the USDC contract and mirrors it, so `STK` also uses 6 decimals and balances line up 1:1 with the USDC backing them. A zero reverts with `InvalidNumberOfDecimals`. + +3. **`stake(beneficiary, amount)`.** Pulls USDC from the caller, then mints an equal amount of `STK` to the beneficiary. Those are deliberately two different addresses. Guards cover a zero beneficiary and a zero amount. + +4. **`redeem()`.** Burns the caller's entire `STK` balance and sends back the same amount of USDC. Since it works off `msg.sender`, only the beneficiary can redeem their own position. + +[USDCStaker.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/transfer-usdc-with-data/USDCStaker.sol) + +View the complete contract on GitHub. + +#### 3. USDCReceiver (destination chain) + +1. **Constructor.** Extends `CCIPReceiver` with the router address and stores USDC and the staker as immutables. It also grants the staker an unlimited USDC allowance up front, which is why staking needs no per message approval later on. + +2. **Sender allowlist.** The mirror image of the sender's list. `s_senders` maps a source chain selector to the one sender allowed to reach this contract. A message from an unlisted chain, or from the wrong address on a listed one, reverts with `WrongSenderForSourceChain`. + +3. **Finality policy.** `setAllowedFinalityConfig` stores a `bytes4` per source chain, and `getCCVsAndFinalityConfig` is what the OffRamp reads back when a message lands. Left at `bytes4(0)`, the contract accepts fully finalized messages only. A non-zero value opts into faster delivery by declaring the shallowest block depth this contract is willing to trust. The configure step later sets it to 32. + +4. **Failures are caught, not thrown.** `ccipReceive` wraps the real work in a `try` block: + + ```solidity filename="USDCReceiver.sol" + try this.processMessage(any2EvmMessage) {} + catch (bytes memory err) { + s_failedMessages.set(any2EvmMessage.messageId, uint256(ErrorCode.FAILED)); + s_messageContents[any2EvmMessage.messageId] = any2EvmMessage; + emit MessageFailed(any2EvmMessage.messageId, err); + return; + } + ``` + + Swallowing the revert keeps the CCIP execution itself successful, so the tokens settle in this contract rather than being stranded mid transfer. `processMessage` is external purely so the `try` is possible at all, and `onlySelf` keeps anyone else from calling it. + +5. **Processing a message.** `_ccipReceive` first checks that the arriving token matches the USDC the contract was deployed against, rejecting anything else with `WrongReceivedToken`. It then decodes the beneficiary from the payload and calls `stake()` with `destTokenAmounts[0].amount`, the amount that actually landed, rather than the figure the sender encoded. That distinction matters, because CCTP can take a fee on the destination side, so only the arriving balance is reliable. + +6. **Inspecting and recovering failures.** `getFailedMessages` returns a paginated view backed by an `EnumerableMap`. `retryFailedMessage` lets the owner mark a message `RESOLVED`, which blocks replays, and move the tokens to an address of their choosing. It reads the token out of the stored message rather than the immutable, so recovery still works for something unexpected. Neither runs on the happy path, but they are what makes a failed delivery recoverable. + +[USDCReceiver.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/transfer-usdc-with-data/USDCReceiver.sol) + +View the complete contract 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. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a +> Foundry dev environment from scratch. + +#### 1. Bootstrap a new Foundry project + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) + +Clone the template for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. If you don't already have a Foundry keystore, create one with `cast`. Here, `your_keystore_name` is the alias you assign to this entry, and Foundry will prompt you for the private key itself along with a password to encrypt it: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +> **CAUTION: Note** +> +> As of writing, Foundry supports keystore encryption only for private keys (not generic strings), so in this tutorial +> we only encrypt the private key and store the keystore alias in `KEYSTORE_NAME`. + +3. Install dependencies: + +```bash filename="Terminal" +npm install +``` + +4. Create a `.env` file by copying the example file, and fill in your values: + +```bash filename="Terminal" +cp .env.example .env +``` + +Set `KEYSTORE_NAME` to the entry you just created, and provide RPC endpoints for both chains on this lane: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (not used in this tutorial) +ETHERSCAN_API_KEY= +``` + +5. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +6. Compile the contracts: + +```bash filename="Terminal" +forge build +``` + +#### 2. Deploy your contracts + +`Deploy.s.sol` handles all three deployments across both chains in a single run: + +- Deploy `USDCSender` on *Ethereum Sepolia* (source). +- Deploy `USDCStaker` on *Arbitrum Sepolia* (destination). +- Deploy `USDCReceiver` on *Arbitrum Sepolia* (destination), wired to the staker it just deployed. +- Print all three addresses in the terminal. + +To run the script, use the following command: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/transfer-usdc-with-data/deploy/Deploy.s.sol:Deploy \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🚀 Deploy USDC Transfer-with-Data Contracts +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +======================================== + + +[Step 1] Deploying USDCSender on Ethereum Sepolia + router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + link: 0x779877A7B0D9E8603169DdbD7836e478b4624789 + usdc: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 +Contract deployed at: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D +https://sepolia.etherscan.io/address/0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D + +======================================== +✅ USDCSender deployed on Ethereum Sepolia! +======================================== + + +[Step 2] Deploying USDCStaker on Arbitrum Sepolia + usdc: 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d +Contract deployed at: 0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +https://sepolia.arbiscan.io/address/0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 + + +[Step 3] Deploying USDCReceiver on Arbitrum Sepolia + router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + usdc: 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d + staker: 0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +Contract deployed at: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +https://sepolia.arbiscan.io/address/0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c + +======================================== +✅ USDCStaker + USDCReceiver deployed on Arbitrum Sepolia! +======================================== + +======================================== +✅ All Deployments Complete! +======================================== +Source Chain: Ethereum Sepolia +USDCSender: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D +https://sepolia.etherscan.io/address/0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D + +Destination Chain: Arbitrum Sepolia +USDCStaker: 0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +https://sepolia.arbiscan.io/address/0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +USDCReceiver: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +https://sepolia.arbiscan.io/address/0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c + +Run this command to set all environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D && export ARBITRUM_SEPOLIA_STAKER_CONTRACT=0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 && export ARBITRUM_SEPOLIA_CONTRACT=0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +======================================== +``` + +2. Export the three addresses the remaining steps rely on: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && export ARBITRUM_SEPOLIA_STAKER_CONTRACT= && export ARBITRUM_SEPOLIA_CONTRACT= +``` + +`ARBITRUM_SEPOLIA_STAKER_CONTRACT` is used later when you redeem `STK` for USDC. + +[Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/transfer-usdc-with-data/deploy/Deploy.s.sol) + +View the complete script on GitHub. + +#### 3. Configure allowlists and finality + +The sender and receiver start out refusing this lane. `Configure.s.sol` introduces them to each other and sets the finality policy in one run: + +- On *Ethereum Sepolia*, call `setReceiverForDestinationChain` on `USDCSender` to allowlist the *Arbitrum Sepolia* receiver. +- On *Arbitrum Sepolia*, call `setSenderForSourceChain` on `USDCReceiver` to allowlist the *Ethereum Sepolia* sender. +- On *Arbitrum Sepolia*, call `setAllowedFinalityConfig` so the receiver will accept custom finality. + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +`ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` paired with `ALLOWED_BLOCK_DEPTH=32` sets the receiver's minimum accepted block depth to 32. Omit `ALLOWED_FINALITY_CONFIG` to leave the receiver at the default: full finality only. + +When the pool exposes a minimum, the script checks your value against it before broadcasting, so a depth the lane will not honor fails immediately instead of much later, as a rejected message. + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \ +ALLOWED_BLOCK_DEPTH=32 \ +forge script foundry/scripts/tutorials/transfer-usdc-with-data/configure/Configure.s.sol:Configure \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Configure USDC Transfer-with-Data Contracts +======================================== +Source Chain: Ethereum Sepolia +USDCSender: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D +Destination Chain: Arbitrum Sepolia +USDCReceiver: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +======================================== + + +[Step 1] Setting receiver on USDCSender (Ethereum Sepolia) + Destination chain: Arbitrum Sepolia (selector: 3478487238524512106) + Receiver address: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c + ✅ Receiver set for destination chain + +======================================== +✅ Configuration Complete on Ethereum Sepolia! +======================================== + + +[Step 2] Configuring USDCReceiver (Arbitrum Sepolia) + Source chain: Ethereum Sepolia (selector: 16015286601757825753) + Sender address: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D + ✅ Sender set for source chain + 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! +======================================== +USDCSender on Ethereum Sepolia is configured to send USDC to Arbitrum Sepolia +USDCReceiver on Arbitrum Sepolia is configured to accept messages from Ethereum Sepolia + +** Next Step: Send USDC with Data ** +======================================== +``` + +The script closes by printing ready-to-run send commands for both LINK and native fees. The next step covers the LINK version. + +[Configure.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/transfer-usdc-with-data/configure/Configure.s.sol) + +View the complete script on GitHub. + +#### 4. Send USDC with data + +`SendMessage.s.sol` carries out the transfer end to end: + +- Builds `extraArgs` offchain through `ExtraArgsHelper.buildExtraArgs`, which reads the USDC token pool's allowed finality config, falling back to a `router.getFee` probe when the pool exposes none, then encodes V2 or V3 to match the lane. +- Quotes the CCIP fee through the sender's `getFee`. +- Approves `USDCSender` to pull USDC and LINK from your wallet. +- Sends the message, then prints the message ID and a CCIP Explorer link. + +##### Beneficiary + +`BENEFICIARY` is the address that ends up holding the `STK` tokens on *Arbitrum Sepolia*. For this tutorial, use the same wallet that broadcasts the send transaction so the same keystore can redeem later. + +Get that address from your Foundry keystore: + +```bash filename="Terminal" +cast wallet address --account $KEYSTORE_NAME +``` + +Use the printed address in the `BENEFICIARY=` placeholder in the send command below. + +> You can set `BENEFICIARY` to a different address, but only that address can later redeem the `STK` tokens for USDC. So only use an address whose private key you control, and make sure it has the native gas token on *Arbitrum Sepolia* to cover gas for the redeem step. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` variable controls the source-chain finality requested for CCIP execution: + +- Omit it, or set `BLOCK_DEPTH=DEFAULT` or `BLOCK_DEPTH=0`, to request full finality. +- Set `BLOCK_DEPTH=32` to request faster delivery. This matches the `ALLOWED_BLOCK_DEPTH` you configured on the receiver in the previous step, so the two ends agree. + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=LINK \ +BENEFICIARY= \ +USDC_AMOUNT=1230000 \ +BLOCK_DEPTH=32 \ +forge script foundry/scripts/tutorials/transfer-usdc-with-data/interact/SendMessage.s.sol:SendMessage \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +`USDC_AMOUNT` is denominated in raw units, so `1230000` is 1.23 USDC at 6 decimals. That is not the script default, which is `1000000`. + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +📡 Send USDC with Data via CCIP - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +USDCSender: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D +USDCReceiver: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +Beneficiary: 0x5C6d7E8f9A0b1C2d3E4f5A6b7C8d9E0f1A2b3C4d +USDC Amount: 1230000 (raw units) +Fee Token: LINK +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Gas limit: 1000000 +Token pool ALLOWED_FINALITY_CONFIG: undefined (pool has no constraint or pre-v2.0 pool). +V3 extraArgs accepted by lane (v2.0+ lane, no pool constraint). +Receiver contract ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s)) +✅ Using V3 extraArgs with FTF (gasLimit=1000000, finalityConfig=0x00000020 (BLOCK_DEPTH: 32 block(s))). + +Estimated CCIP fee: 21374829384756123 +Extra args: 0xa69dd4aa000f42400000002000000000000000 + +[Step 1] Approving USDCSender to spend USDC... + USDC amount: 1230000 +✅ Approved + +[Step 2] Approving USDCSender to spend fee token for CCIP fees... + Fee: 21374829384756123 +✅ Approved + +[Step 3] Sending USDC with data via CCIP (ERC-20 fee)... + +======================================== +✅ CCIP Message Sent Successfully! +======================================== +CCIP Message ID: 0x8f2a4b6c1d3e5f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x8f2a4b6c1d3e5f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8 + +Once confirmed (status: Success), beneficiary 0x5C6d7E8f9A0b1C2d3E4f5A6b7C8d9E0f1A2b3C4d +will have STK tokens on Arbitrum Sepolia redeemable for USDC via the USDCStaker contract. +======================================== +``` + +The next step redeems the `STK` tokens after the message settles and staking succeeds. + +> **NOTE: Wait for the message to settle** +> +> Delivery takes a few minutes. Open the CCIP Explorer link from your terminal and wait for the status to read +> **Success** before checking the beneficiary's `STK` balance on the `USDCStaker` contract. If the redeem step still +> reports no `STK`, inspect failed messages on the receiver. + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | ------------------------------------------------------------------------------------------------- | --------- | +| `KEYSTORE_NAME` | Foundry keystore alias created with `cast wallet import` | Not set | +| `{CHAIN}_RPC_URL` | RPC endpoint per chain, for example `ETHEREUM_SEPOLIA_RPC_URL` | Not set | +| `SOURCE_CHAIN` | Source chain identifier, `ETHEREUM_SEPOLIA` on this lane | Not set | +| `DEST_CHAIN` | Destination chain identifier, `ARBITRUM_SEPOLIA` on this lane | Not set | +| `CHAIN` | Chain identifier for destination-chain scripts, `ARBITRUM_SEPOLIA` on this lane | Not set | +| `{CHAIN}_CONTRACT` | Deployed contract on that chain: the sender on the source, the receiver on the destination | Not set | +| `{CHAIN}_STAKER_CONTRACT` | Deployed `USDCStaker` contract on the destination, for example `ARBITRUM_SEPOLIA_STAKER_CONTRACT` | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | Any CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `BENEFICIARY` | Address that receives the `STK` tokens on the destination chain | Not set | +| `USDC_AMOUNT` | USDC to send, in raw units at 6 decimals | `1000000` | +| `GAS_LIMIT` | Gas made available to the receiver on the destination chain | `1000000` | +| `BLOCK_DEPTH` | Block depth requested on send. `DEFAULT` or `0` waits for full finality | `DEFAULT` | +| `WAIT_FOR_FINALITY` | Requests full finality explicitly. Equivalent to leaving `BLOCK_DEPTH` unset | `false` | +| `ALLOWED_FINALITY_CONFIG` | Finality modes the receiver will accept, set during configuration | Not set | +| `ALLOWED_BLOCK_DEPTH` | Shallowest block depth the receiver accepts. Required when `ALLOWED_FINALITY_CONFIG` uses it | Not set | + +[SendMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/transfer-usdc-with-data/interact/SendMessage.s.sol) + +View the complete script on GitHub. + +#### 5. Redeem STK for USDC + +After the CCIP Explorer shows that the message has been successfully processed and the beneficiary has received `STK`, the beneficiary can redeem those tokens on *Arbitrum Sepolia*. + +`Redeem.s.sol` checks the caller's `STK` balance, calls `redeem()` on `USDCStaker`, burns the caller's full `STK` balance, and transfers the same amount of USDC back to the caller. + +> **CAUTION: Redeem from the beneficiary wallet** +> +> 1. `redeem()` uses `msg.sender`, so run this command with the same keystore that controls `BENEFICIARY`. +> 2. If you used a different beneficiary address, import that account into your Foundry keystore and run this step with +> that alias. + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +forge script foundry/scripts/tutorials/transfer-usdc-with-data/interact/Redeem.s.sol:Redeem \ +--account $KEYSTORE_NAME \ +--broadcast -vv +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +💰 Redeem STK Tokens for USDC +======================================== +Chain: Arbitrum Sepolia +USDCStaker: 0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +======================================== + +STK balance: 1230000 +USDC to receive: 1230000 (raw units) + +[Step 1] Calling redeem() on USDCStaker... + This burns your entire STK balance and returns the equivalent USDC. + +======================================== +✅ Redeem Complete! +======================================== +USDC received: 1230000 (raw units) +USDC balance: 1230000 (raw units) +USDCStaker: 0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +https://sepolia.arbiscan.io/address/0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +======================================== +``` + +Your final USDC balance may be higher if the wallet already held USDC before redeeming. + +> **NOTE: Optional: inspect failed messages** +> +> 1. If delivery fails or `STK` was not minted, inspect failed messages on the receiver before redeeming. 2. +> `SendMessage.s.sol` prints commands for `GetFailedMessages.s.sol` and `RetryFailedMessage.s.sol` after each send. 3. +> `RetryFailedMessage.s.sol` recovers the received USDC from `USDCReceiver`; it does not mint `STK`. + +[Redeem.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/transfer-usdc-with-data/interact/Redeem.s.sol) + +View the complete script on GitHub. + +### Hardhat + +Best for developers who want a mature, TypeScript-based smart contract development framework. + +> **NOTE: Want to set up a dev environment from scratch? ** +> +> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a +> Hardhat dev environment from scratch. + +#### 1. Bootstrap a new Hardhat project + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) + +Clone the template (contains both Hardhat and Foundry code) for a smoother setup. + +1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory: + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip +``` + +2. Copy the example environment file and fill in your values: + +```bash filename="Terminal" +cp .env.example .env +``` + +Set `KEYSTORE_NAME` to the keystore alias you will create later in this section, and provide RPC endpoints for both chains on this lane: + +```env filename=".env" +# Keystore name +KEYSTORE_NAME=your_keystore_name + +# RPC URLs +ETHEREUM_SEPOLIA_RPC_URL= +ARBITRUM_SEPOLIA_RPC_URL= + +# Etherscan API key (not used in this tutorial) +ETHERSCAN_API_KEY= +``` + +3. Install dependencies and compile: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +4. Load the environment variables: + +```bash filename="Terminal" +source .env +``` + +> **CAUTION: Run this in every new terminal session** +> +> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and +> confirm it prints your URL. + +5. Create a Hardhat keystore entry for your private key. Use the same name as `KEYSTORE_NAME` in your `.env` file. Hardhat will prompt you to enter the private key and a password to encrypt it: + +```bash filename="Terminal" +npx hardhat keystore set your_keystore_name +``` + +#### 2. Deploy your contracts + +`deploy.ts` handles all three deployments across both chains in a single run: + +- Deploy `USDCSender` on *Ethereum Sepolia* (source). +- Deploy `USDCStaker` on *Arbitrum Sepolia* (destination). +- Deploy `USDCReceiver` on *Arbitrum Sepolia* (destination), wired to the staker it just deployed. +- Print all three addresses in the terminal. + +To run the script, use the following command: + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/transfer-usdc-with-data/deploy/deploy.ts +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +🚀 Deploy USDC Transfer-with-Data Contracts +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +======================================== + + +[Step 1] Deploying USDCSender on Ethereum Sepolia + router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + link: 0x779877A7B0D9E8603169DdbD7836e478b4624789 + usdc: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 +Contract deployed at: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D +https://sepolia.etherscan.io/address/0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D + +======================================== +✅ USDCSender deployed on Ethereum Sepolia! +======================================== + + +[Step 2] Deploying USDCStaker on Arbitrum Sepolia + usdc: 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d +Contract deployed at: 0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +https://sepolia.arbiscan.io/address/0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 + +[Step 3] Deploying USDCReceiver on Arbitrum Sepolia + router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + usdc: 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d + staker: 0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +Contract deployed at: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +https://sepolia.arbiscan.io/address/0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c + +======================================== +✅ USDCStaker + USDCReceiver deployed on Arbitrum Sepolia! +======================================== + +======================================== +✅ All Deployments Complete! +======================================== +Source Chain: Ethereum Sepolia +USDCSender: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D +https://sepolia.etherscan.io/address/0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D + +Destination Chain: Arbitrum Sepolia +USDCStaker: 0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +https://sepolia.arbiscan.io/address/0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +USDCReceiver: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +https://sepolia.arbiscan.io/address/0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c + +Run this command to set all environment variables: +export ETHEREUM_SEPOLIA_CONTRACT=0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D && export ARBITRUM_SEPOLIA_STAKER_CONTRACT=0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 && export ARBITRUM_SEPOLIA_CONTRACT=0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +======================================== +``` + +2. Export the three addresses the remaining steps rely on: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_CONTRACT= && export ARBITRUM_SEPOLIA_STAKER_CONTRACT= && export ARBITRUM_SEPOLIA_CONTRACT= +``` + +`ARBITRUM_SEPOLIA_STAKER_CONTRACT` is used later when you redeem `STK` for USDC. + +[deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/transfer-usdc-with-data/deploy/deploy.ts) + +View the complete script on GitHub. + +#### 3. Configure allowlists and finality + +The sender and receiver start out refusing this lane. `configure.ts` introduces them to each other and sets the finality policy in one run: + +- On *Ethereum Sepolia*, call `setReceiverForDestinationChain` on `USDCSender` to allowlist the *Arbitrum Sepolia* receiver. +- On *Arbitrum Sepolia*, call `setSenderForSourceChain` on `USDCReceiver` to allowlist the *Ethereum Sepolia* sender. +- On *Arbitrum Sepolia*, call `setAllowedFinalityConfig` so the receiver will accept custom finality. + +##### Finality configuration + +> **NOTE: Faster Than Finality (FTF)** +> +> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain. +> +> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality. +> +> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message: +> +> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today. +> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**. +> +> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request: +> +> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent. +> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits. +> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution. +> +> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf). + +`ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` paired with `ALLOWED_BLOCK_DEPTH=32` sets the receiver's minimum accepted block depth to 32. Omit `ALLOWED_FINALITY_CONFIG` to leave the receiver at the default: full finality only. + +When the pool exposes a minimum, the script checks your value against it before broadcasting, so a depth the lane will not honor fails immediately instead of much later, as a rejected message. + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \ +ALLOWED_BLOCK_DEPTH=32 \ +npx hardhat run hardhat/scripts/tutorials/transfer-usdc-with-data/configure/configure.ts +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Configure USDC Transfer-with-Data Contracts +======================================== +Source Chain: Ethereum Sepolia +USDCSender: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D +Destination Chain: Arbitrum Sepolia +USDCReceiver: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +======================================== + + +[Step 1] Setting receiver on USDCSender (Ethereum Sepolia) + Destination: Arbitrum Sepolia (selector: 3478487238524512106) + Receiver: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c + ✅ Receiver set for destination chain + +======================================== +✅ Configuration Complete on Ethereum Sepolia! +======================================== + + +[Step 2] Configuring USDCReceiver (Arbitrum Sepolia) + Source: Ethereum Sepolia (selector: 16015286601757825753) + Sender: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D + ✅ Sender set for source chain + 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! +======================================== +USDCSender on Ethereum Sepolia is configured to send USDC to Arbitrum Sepolia +USDCReceiver on Arbitrum Sepolia is configured to accept messages from Ethereum Sepolia + +** Next Step: Send USDC with Data ** +======================================== +``` + +The script closes by printing ready-to-run send commands for both LINK and native fees. The next step covers the LINK version. + +[configure.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/transfer-usdc-with-data/configure/configure.ts) + +View the complete script on GitHub. + +#### 4. Send USDC with data + +`send-message.ts` carries out the transfer end to end: + +- Builds `extraArgs` offchain through the shared `buildExtraArgs` helper, which queries lane features through `@chainlink/ccip-sdk`, validates the requested finality against the USDC token pool's and the receiver's allowed finality configs, then encodes V2 or V3 to match the lane. +- Quotes the CCIP fee through the sender's `getFee`. +- Approves `USDCSender` to pull USDC and LINK from your wallet. +- Sends the message, then prints the message ID and a CCIP Explorer link. + +##### Beneficiary + +`BENEFICIARY` is the address that ends up holding the `STK` tokens on *Arbitrum Sepolia*. For this tutorial, use the same wallet that broadcasts the send transaction so the same keystore can redeem later. + +Get that address from your keystore: + +```bash filename="Terminal" +cast wallet address --account $KEYSTORE_NAME +``` + +Use the printed address in the `BENEFICIARY=` placeholder in the send command below. + +> You can set `BENEFICIARY` to a different address, but only that address can later redeem the `STK` tokens for USDC. So only use an address whose private key you control, and make sure it has the native gas token on *Arbitrum Sepolia* to cover gas for the redeem step. + +##### Faster Than Finality (block depth) + +> **NOTE: ExtraArgsV3** +> +> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields: +> +> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers. +> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery. +> These tutorials use a depth of 32, which encodes as `0x00000020`. +> +> For the complete list of supported parameters, see the +> +> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference, +> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository. +> +> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored. + +The `BLOCK_DEPTH` variable controls the source-chain finality requested for CCIP execution: + +- Omit it, or set `BLOCK_DEPTH=DEFAULT` or `BLOCK_DEPTH=0`, to request full finality. +- Set `BLOCK_DEPTH=32` to request faster delivery. This matches the `ALLOWED_BLOCK_DEPTH` you configured on the receiver in the previous step, so the two ends agree. + +```bash filename="Terminal" +SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ +FEE_TOKEN=LINK \ +BENEFICIARY= \ +USDC_AMOUNT=1230000 \ +BLOCK_DEPTH=32 \ +npx hardhat run hardhat/scripts/tutorials/transfer-usdc-with-data/interact/send-message.ts +``` + +`USDC_AMOUNT` is denominated in raw units, so `1230000` is 1.23 USDC at 6 decimals. That is not the script default, which is `1000000`. + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +📡 Send USDC with Data via CCIP - Pay with LINK +======================================== +Source Chain: Ethereum Sepolia +Destination Chain: Arbitrum Sepolia +USDCSender: 0x7A9c4E1b2D3f5061A2b3C4d5E6f708192a3B4c5D +USDCReceiver: 0x9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c +Beneficiary: 0x5C6d7E8f9A0b1C2d3E4f5A6b7C8d9E0f1A2b3C4d +USDC Amount: 1230000 (raw units) +Fee Token: LINK +======================================== + + +[Pre-validation] Detecting lane version and building extraArgs... +Gas limit: 1000000 +Token pool ALLOWED_FINALITY_CONFIG: 0x00000001 (BLOCK_DEPTH: 1 block(s)) +Receiver contract ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s)) +✅ Using V3 extraArgs with FTF (gasLimit=1000000, finalityConfig=32 block(s)). +[Pre-validation] CCIP fee: 21374829384756123 + +[Step 1] Approving USDCSender to spend USDC: 1230000... +✅ Approved + +[Step 2] Approving USDCSender to spend LINK for CCIP fee: 21374829384756123... +✅ Approved + +[Step 3] Sending USDC with data via CCIP (LINK fee)... +Required CCIP fee (in LINK units): 21374829384756123 + +======================================== +✅ USDC sent successfully via CCIP! +======================================== +CCIP messageId: 0x8f2a4b6c1d3e5f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8 +CCIP Explorer: +https://ccip.chain.link/#/side-drawer/msg/0x8f2a4b6c1d3e5f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8 + +Once confirmed (status: Success), beneficiary 0x5C6d7E8f9A0b1C2d3E4f5A6b7C8d9E0f1A2b3C4d +will have STK tokens on Arbitrum Sepolia redeemable for USDC via the USDCStaker contract. +======================================== +``` + +The next step redeems the `STK` tokens after the message settles and staking succeeds. + +> **NOTE: Wait for the message to settle** +> +> Delivery takes a few minutes. Open the CCIP Explorer link from your terminal and wait for the status to read +> **Success** before checking the beneficiary's `STK` balance on the `USDCStaker` contract. If the redeem step still +> reports no `STK`, inspect failed messages on the receiver. + +##### Environment variables + +| Variable | Description | Default | +| ------------------------- | ------------------------------------------------------------------------------------------------- | -------------------- | +| `KEYSTORE_NAME` | Hardhat keystore entry name, set in `.env` | `your_keystore_name` | +| `{CHAIN}_RPC_URL` | RPC endpoint per chain, for example `ETHEREUM_SEPOLIA_RPC_URL` | Not set | +| `SOURCE_CHAIN` | Source chain identifier, `ETHEREUM_SEPOLIA` on this lane | Not set | +| `DEST_CHAIN` | Destination chain identifier, `ARBITRUM_SEPOLIA` on this lane | Not set | +| `CHAIN` | Chain identifier for destination-chain scripts, `ARBITRUM_SEPOLIA` on this lane | Not set | +| `{CHAIN}_CONTRACT` | Deployed contract on that chain: the sender on the source, the receiver on the destination | Not set | +| `{CHAIN}_STAKER_CONTRACT` | Deployed `USDCStaker` contract on the destination, for example `ARBITRUM_SEPOLIA_STAKER_CONTRACT` | Not set | +| `FEE_TOKEN` | `LINK` or `NATIVE` | `NATIVE` | +| `FEE_TOKEN_ADDRESS` | Any CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN` | Not set | +| `BENEFICIARY` | Address that receives the `STK` tokens on the destination chain | Not set | +| `USDC_AMOUNT` | USDC to send, in raw units at 6 decimals | `1000000` | +| `GAS_LIMIT` | Gas made available to the receiver on the destination chain | `1000000` | +| `BLOCK_DEPTH` | Block depth requested on send. `DEFAULT` or `0` waits for full finality | `DEFAULT` | +| `WAIT_FOR_FINALITY` | Requests full finality explicitly. Equivalent to leaving `BLOCK_DEPTH` unset | `false` | +| `ALLOWED_FINALITY_CONFIG` | Finality modes the receiver will accept, set during configuration | Not set | +| `ALLOWED_BLOCK_DEPTH` | Shallowest block depth the receiver accepts. Required when `ALLOWED_FINALITY_CONFIG` uses it | Not set | + +[send-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/transfer-usdc-with-data/interact/send-message.ts) + +View the complete script on GitHub. + +#### 5. Redeem STK for USDC + +After the CCIP Explorer shows that the message has been successfully processed and the beneficiary has received `STK`, the beneficiary can redeem those tokens on *Arbitrum Sepolia*. + +`redeem.ts` checks the caller's `STK` balance, calls `redeem()` on `USDCStaker`, burns the caller's full `STK` balance, and transfers the same amount of USDC back to the caller. + +> **CAUTION: Redeem from the beneficiary wallet** +> +> 1. `redeem()` uses `msg.sender`, so run this command with the same keystore that controls `BENEFICIARY`. +> 2. If you used a different beneficiary address, import that account into your Foundry keystore and run this step with +> that alias. + +```bash filename="Terminal" +CHAIN=ARBITRUM_SEPOLIA \ +npx hardhat run hardhat/scripts/tutorials/transfer-usdc-with-data/interact/redeem.ts +``` + +Your terminal should look something like this: + +```bash filename="Terminal" +======================================== +💰 Redeem STK Tokens for USDC +======================================== +Chain: Arbitrum Sepolia +USDCStaker: 0x2F8b1D4e5A6c7089B1c2D3e4F5a6B7c8D9e0F1a2 +======================================== + +STK balance: 1230000 (raw units) +USDC to receive: 1230000 (raw units) + +[Step 1] Calling redeem() on USDCStaker... + This burns your entire STK balance and returns the equivalent USDC. + Transaction: 0x6f2c8e4a1b3d5f7a9c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6 + Waiting for confirmation... + +======================================== +✅ Redeem Complete! +======================================== +USDC received: 1230000 (raw units) +Explorer: https://sepolia.arbiscan.io/tx/0x6f2c8e4a1b3d5f7a9c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6 +======================================== +``` + +> **NOTE: Optional: inspect failed messages** +> +> 1. If delivery fails or `STK` was not minted, inspect failed messages on the receiver before redeeming. 2. +> `send-message.ts` prints commands for `get-failed-messages.ts` and `retry-failed-message.ts` after each send. 3. +> `retry-failed-message.ts` recovers the received USDC from `USDCReceiver`; it does not mint `STK`. + +[redeem.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/transfer-usdc-with-data/interact/redeem.ts) + +View the complete script on GitHub. + +--- + +# Optimizing Gas Limit Settings in CCIP Messages +Source: https://docs.chain.link/ccip/evm/tutorials/ccipreceive-gaslimit +Last Updated: 2025-10-21 + +When constructing a [CCIP message](/ccip/v1/evm/api-reference/v1.6.1/client#evm2anymessage), it's crucial to set the gas limit accurately. The gas limit represents the maximum amount of gas consumed to execute the [`ccipReceive`](/ccip/v1/evm/api-reference/v1.6.1/ccip-receiver#ccipreceive) function on the CCIP Receiver, which influences the transaction fees for sending a CCIP message. Notably, unused gas is not reimbursed, making it essential to estimate the gas limit carefully: + +- Setting the gas limit too low will cause the transaction to revert when CCIP calls `ccipReceive` on the CCIP Receiver, which requires a manual re-execution with an increased gas limit. For more details about this scenario, read the [Manual Execution](/ccip/evm/tutorials/application-developers/manual-execution) guide. +- Conversely, an excessively high gas limit leads to higher fees. + +This tutorial shows you how to estimate the gas limit for the `ccipReceive` function using various methods. You will learn how to use a CCIP Receiver where the gas consumption of the `ccipReceive` function varies based on the input data. This example emphasizes the need for testing under diverse conditions. This tutorial includes tasks for the following environments: + +1. **Local Environment**: Using [Hardhat](https://hardhat.org/) and [Foundry](https://book.getfoundry.sh/) on a local blockchain provides a swift initial gas estimate. However, different frameworks can yield different results and the local environment will not always be representative of your destination blockchain. Consider these figures to be preliminary estimates. Then, incorporate a buffer and conduct subsequent validations on a testnet. +2. **Testnet**: You can precisely determine the required gas limit by deploying your CCIP Sender and Receiver on a testnet and transmitting several CCIP messages with the previously estimated gas. Although this approach is more time-intensive, especially if testing across multiple blockchains, it offers enhanced accuracy. +3. **Offchain Methods**: Estimating gas with an offchain Web3 provider or tools like [Tenderly](https://docs.tenderly.co/) offers the most accurate and swift way to determine the needed gas limit. + +These approaches will give you insights into accurately estimating the gas limit for the `ccipReceive` function, ensuring cost-effective CCIP transactions. + +Before you begin, open a terminal, clone the [smart-contract-examples repository](https://github.com/smartcontractkit/smart-contract-examples), and navigate to the `smart-contract-examples/ccip/estimate-gas` directory. + +```bash +git clone https://github.com/smartcontractkit/smart-contract-examples.git && \ +cd smart-contract-examples/ccip/estimate-gas +``` + +## Examine the code + +The source code for the CCIP Sender and Receiver is located in the `contracts` directory for Hardhat projects and in the `src` directory for Foundry projects. The code includes detailed comments for clarity and is designed to ensure self-explanatory functionality. This section focuses on the `_ccipReceive` function: + +```solidity +function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override { + uint256 iterations = abi.decode(any2EvmMessage.data, (uint256)); + + uint256 result = iterations; + uint256 maxIterations = iterations % 100; + for (uint256 i = 0; i < maxIterations; i++) { + result += i; + } + + emit MessageReceived( + any2EvmMessage.messageId, + any2EvmMessage.sourceChainSelector, + abi.decode(any2EvmMessage.sender, (address)), + iterations, + maxIterations, + result + ); +} +``` + +The `_ccipReceive` function operates as follows: + +1. **Input Processing:** The function accepts a `Client.Any2EVMMessage`. The first step involves decoding the number of iterations from the message's data using ABI decoding. +2. **Logic Execution:** It initializes the `result` variable with the number of iterations. The function calculates `maxIterations` by taking the modulo of iterations with 100, which sets an upper limit for iteration. This step is a safeguard to ensure that the function does not run out of gas. +3. **Iteration:** The function executes a loop from 0 to `maxIterations`, simulating variable computational work based on the input data. This variability directly influences gas consumption. +4. **Event Emission:** Finally, an event `MessageReceived` is emitted. + +This code shows how gas consumption for the `_ccipReceive` function can fluctuate in response to the input data, highlighting the necessity for thorough testing under different scenarios to determine the correct `gasLimit`. + +## Gas estimation in a local environment + +To facilitate testing within a local environment, you will use the [MockCCIPRouter](https://github.com/smartcontractkit/chainlink-ccip/blob/release/contracts-ccip-1.6.2/chains/evm/contracts/test/mocks/MockRouter.sol) contract. This contract serves as a mock implementation of the CCIP Router contract, enabling the local testing of CCIP Sender and Receiver contracts. +A notable feature of the `MockCCIPRouter` contract is its ability to emit a `MsgExecuted` event: + +```solidity +event MsgExecuted(bool success, bytes retData, uint256 gasUsed) +``` + +This event reports the amount of gas consumed by the `ccipReceive` function. + +### Foundry + +#### Prerequisites + +1. In your terminal, change to the `foundry` directory: + + ```bash + cd foundry + ``` + +2. Ensure Foundry is [installed](https://book.getfoundry.sh/getting-started/installation). + +3. Check the Foundry version: + + ```bash + forge --version + ``` + + The output should be similar to the following: + + ```text + forge Version: 1.4.2-stable + Commit SHA: 828441d243f552f82c5a89bdc818e52bdd57b26b + Build Timestamp: 2025-10-18T08:19:39.947006000Z (1760775579) + Build Profile: maxperf + ``` + + You need version 1.4.2 or above. Run `foundryup` to update Foundry if necessary. + +4. Build your project: + + ```bash + forge build + ``` + + The output should be similar to: + + ```text + [⠊] Compiling... + [⠑] Compiling 59 files with Solc 0.8.24 + [⠘] Solc 0.8.24 finished in 1.67s + Compiler run successful! + ``` + +#### Estimate gas + +Located in the `test` directory, the `SendReceive.t.sol` test file assesses the gas consumption of the `ccipReceive` function. This file features a test case that sends a CCIP message to the `MockCCIPRouter` contract, which triggers the `MsgExecuted` event. This event provides insights into the gas requirements of the `ccipReceive` function by detailing the amount of gas consumed. The test case explores three scenarios to examine gas usage comprehensively across various operational conditions: + +- **Baseline gas consumption:** This scenario runs `0` iteration to determine the baseline gas consumption, representing the least amount of gas required. +- **Average gas consumption:** This scenario runs `50` iterations to estimate the gas consumption under average operational conditions. +- **Peak gas consumption:** This scenario runs `99` iterations to estimate the peak gas consumption, marking the upper limit of gas usage. + +To run the test, execute the following command: + +```bash +forge test -vv --isolate +``` + +Output example: + +```text +[⠊] Compiling... +No files changed, compilation skipped + +Ran 3 tests for test/SendReceive.t.sol:SenderReceiverTest +[PASS] test_SendReceiveAverage() (gas: 137800) +Logs: + Number of iterations 50 - Gas used: 11323 + +[PASS] test_SendReceiveMax() (gas: 143803) +Logs: + Number of iterations 99 - Gas used: 17350 + +[PASS] test_SendReceiveMin() (gas: 131614) +Logs: + Number of iterations 0 - Gas used: 5173 + +Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 8.26ms (9.35ms CPU time) + +Ran 1 test suite in 123.83ms (8.26ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests) +``` + +This table summarizes the gas usage for different iterations: + +| Scenario | Number of iterations | Gas used | +| ------------------------ | -------------------- | -------- | +| Baseline gas consumption | 0 | 5173 | +| Average gas consumption | 50 | 11323 | +| Peak gas consumption | 99 | 17350 | + +The output demonstrates that gas consumption increases with the number of iterations, peaking when the iteration count reaches `99`. +In the next section, you will compare these results with those obtained from a local Hardhat environment. + +### Hardhat + +#### Prerequisites + +1. In your terminal, navigate to the `hardhat` directory: + + ```bash + cd ../hardhat + ``` + +2. Install the dependencies and compile the contracts: + + ```bash + npm install && npx hardhat compile + ``` + + The output should be similar to: + + ```text + Compiled 4 Solidity files with solc 0.8.24 (evm target: shanghai) + Nothing to compile + ``` + +3. Set the password to encrypt your environment variables using the following command: + + ```bash + npx env-enc set-pw + ``` + +4. Set the following environment variables to deploy contracts on testnets: + + - `PRIVATE_KEY`: The private key for your testnet wallet. If you use MetaMask, follow the instructions to [Export a Private Key](https://support.metamask.io/hc/en-us/articles/360015289632-How-to-export-an-account-s-private-key). **Note:** Your private key is needed to sign any transactions you make such as making requests. + - `ETHEREUM_SEPOLIA_RPC_URL`: The RPC URL for Ethereum Sepolia testnet. You can sign up for a personal endpoint from [Alchemy](https://www.alchemy.com/), [Infura](https://www.infura.io/), or another node provider service. + - `AVALANCHE_FUJI_RPC_URL`: The RPC URL for Avalanche Fuji testnet. You can sign up for a personal endpoint from [Infura](https://www.infura.io/) or another node provider service. + - `ETHERSCAN_API_KEY`: An Ethereum explorer API key, used to verify your contract. Follow [this guide](https://docs.etherscan.io/getting-started/viewing-api-usage-statistics) to get one from Etherscan. + + Input these variables using the following command: + + ```bash + npx env-enc set + ``` + +#### Estimate gas + +Located in the `test` directory, the `Send-Receive.ts` test file is designed to evaluate the gas usage of the `ccipReceive` function. This file employs the same logic as the Foundry test file, featuring three scenarios varying by the number of iterations. The test case transmits a CCIP message to the `MockCCIPRouter` contract, triggering the `MsgExecuted` event. This event provides insights into the gas requirements of the `ccipReceive` function by detailing the amount of gas used. + +To run only the TypeScript tests, use the test nodejs task: + +```bash +npx hardhat test nodejs +``` + +Example of the output: + +```text +Running node:test tests + +Final Gas Usage Report: +Number of iterations 0 - Gas used: 5151 +Number of iterations 50 - Gas used: 11301 +Number of iterations 99 - Gas used: 17328 + Sender and Receiver + ✔ should CCIP message from sender to receiver (760ms) + + + 1 passing (1036ms) +``` + +This table summarizes the gas usage across different iterations: + +| Scenario | Number of iterations | Gas used | +| ------------------------ | -------------------- | -------- | +| Baseline gas consumption | 0 | 5151 | +| Average gas consumption | 50 | 11301 | +| Peak gas consumption | 99 | 17328 | + +The output demonstrates that gas consumption increases with the number of iterations, peaking when the iteration count reaches `99`. + +### Compare the results from Foundry and Hardhat + +This table summarizes the gas usage for different iterations from both Foundry and Hardhat: + +| Scenario | Number of iterations | Gas used (Foundry) | Gas used (Hardhat) | +| ------------------------ | -------------------- | ------------------ | ------------------ | +| Baseline gas consumption | 0 | 5173 | 5151 | +| Average gas consumption | 50 | 11323 | 11301 | +| Peak gas consumption | 99 | 17350 | 17328 | + +Gas usage trends across different iterations are consistent between Foundry and Hardhat and increase with the number of iterations, reaching a peak at 99. However, slight variations in gas usage between the two environments at each iteration level demonstrate the importance of extending gas usage estimation beyond local environment testing. To accurately determine the appropriate gas limit, it is recommended to conduct additional validations on the target blockchain. Setting the gas limit with a buffer is advisable to account for differences between local environment estimations and actual gas usage on the target blockchain. + +### Estimate gas usage on your local environment + +Now that you've locally estimated the gas usage of the `ccipReceive` function using the provided projects, you can apply the same approach to your own Foundry or Hardhat project. This section will guide you through estimating gas usage in your Foundry or Hardhat project. + +#### Estimate `ccipReceive` gas usage locally in your Foundry project + +To estimate the gas usage of the `ccipReceive` function within your own Foundry project, follow these steps: + +1. Create a testing file in the `test` directory of your project and import the [`MockCCIPRouter`](https://github.com/smartcontractkit/chainlink-ccip/blob/release/contracts-ccip-1.6.2/chains/evm/contracts/test/mocks/MockRouter.sol) contract: + + ```solidity + import {MockCCIPRouter} from "@chainlink/contracts-ccip/contracts/test/mocks/MockRouter.sol"; + ``` + + ***Note*:** The `MockCCIPRouter` receives the CCIP message from your CCIP Sender, calls the `ccipReceive` function on your CCIP Receiver, and emits the `MsgExecuted` event with the gas used. + +2. Inside the `setUp` function, deploy the `MockCCIPRouter` contract, and use its address to deploy your CCIP Sender and CCIP Receiver contracts. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/foundry/test/SendReceive.t.sol#L23). + +3. In your test cases: + 1. Before transmitting any CCIP messages, use `vm.recordLogs()` to start capturing events. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/foundry/test/SendReceive.t.sol#L39). + + 2. After sending the CCIP message, use `vm.getRecordedLogs()` to collect the recorded logs. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/foundry/test/SendReceive.t.sol#L47-L64). + + 3. Parse the logs to find the `MsgExecuted(bool,bytes,uint256)` event and extract the gas used. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/foundry/test/SendReceive.t.sol#L53). + +#### Estimate `ccipReceive` gas usage locally in your Hardhat project + +To estimate the gas usage of the `ccipReceive` function within your own Hardhat project, follow these steps: + +1. Create a Solidity file in the `contracts` directory of your project and import the [`MockCCIPRouter`](https://github.com/smartcontractkit/chainlink-ccip/blob/release/contracts-ccip-1.6.2/chains/evm/contracts/test/mocks/MockRouter.sol) contract: + + ```solidity + import {MockCCIPRouter} from "@chainlink/contracts-ccip/contracts/test/mocks/MockRouter.sol"; + ``` + + ***Note*:** The `MockCCIPRouter` receives the CCIP message from your CCIP Sender, calls the `ccipReceive` function on your CCIP Receiver, and emits the `MsgExecuted` event with the gas used. + +2. Create a testing file in your project's `test` directory. + +3. Inside the `deployFixture` function, deploy the `MockCCIPRouter` contract and use its address to deploy your CCIP Sender and CCIP Receiver contracts. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/hardhat/test/Send-Receive.ts#L11). + +4. In your test cases: + 1. Send the CCIP message to the `MockCCIPRouter` contract. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/hardhat/test/Send-Receive.ts#L77). + 2. Parse the logs to find the `MsgExecuted(bool,bytes,uint256)` event and extract the gas used. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/hardhat/test/Send-Receive.ts#L59). + +## Gas estimation on a testnet + +To accurately validate your local environment's gas usage estimations, follow these steps: + +1. Deploy and configure the CCIP Sender contract on the Avalanche Fuji testnet and the CCIP Receiver contract on the Ethereum Sepolia testnet. + +2. Send several CCIP messages with the same number of iterations used in your local testing. For this purpose, use the `sendCCIPMessage.ts` script in the `scripts/testing` directory. This script includes a 10% buffer over the estimated gas usage to ensure a sufficient gas limit. Refer to the table below for the buffered gas limits for each iteration: + + | Scenario | Number of iterations | Estimated gas usage (Hardhat) | Buffered gas limit (+10%) | + | ------------------------ | -------------------- | ----------------------------- | ------------------------- | + | Baseline gas consumption | 0 | 5151 | 5666 | + | Average gas consumption | 50 | 11301 | 12431 | + | Peak gas consumption | 99 | 17328 | 19061 | + +3. Use [Tenderly](https://dashboard.tenderly.co) to monitor and confirm that the transactions execute successfully within the buffered gas limits. Subsequently, compare the actual gas usage of the `ccipReceive` function on the Ethereum Sepolia testnet against the buffered limits to fine-tune the final gas limit. + +This approach ensures that your gas limit settings are validated against real-world conditions on testnets, providing a more accurate and reliable estimation for deploying on live blockchains. + +### Deploy and configure the contracts + +To deploy and configure the CCIP Sender contract on the Avalanche Fuji testnet and the CCIP Receiver contract on the Ethereum Sepolia testnet, follow the steps below. **Note**: Your account must have some ETH tokens on Ethereum Sepolia and AVAX tokens on Avalanche Fuji. + +1. Deploy the CCIP Sender on the Avalanche Fuji testnet: + + ```bash + npx hardhat run scripts/deployment/deploySender.ts --network avalancheFuji + ``` + +2. Deploy the CCIP Receiver on the Ethereum Sepolia testnet: + + ```bash + npx hardhat run scripts/deployment/deployReceiver.ts --network ethereumSepolia + ``` + +3. Authorize the Sender to send messages to Ethereum Sepolia: + + ```bash + npx hardhat run scripts/configuration/allowlistingForSender.ts --network avalancheFuji + ``` + +4. Authorize the Receiver to receive messages from the Sender: + + ```bash + npx hardhat run scripts/configuration/allowlistingForReceiver.ts --network ethereumSepolia + ``` + +Upon completion, you will find the CCIP Sender and Receiver contracts deployed and configured on their respective testnets. Contract addresses are available in the `scripts/generatedData.json` file. + +### Send CCIP Messages + +1. Send three CCIP messages with different numbers of iterations: + + ```bash + npx hardhat run scripts/testing/sendCCIPMessages.ts --network avalancheFuji + ``` + + Your output should look something like this: + + ```text + $ npx hardhat run scripts/testing/sendCCIPMessages.ts --network avalancheFuji + Approving 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 for 0x6e80C9d77B12470A6158459cED1616F705aA13Ea. Allowance is max uint256. Signer 0x8c244f0b2164e6a3bed74ab429b0ebd661bb14ca... + + Number of iterations 0 - Gas limit: 5685 - Message Id: 0x7a51c84fd0734afef641e3578b29e718c208d6ac2e50826f73a3fec24076a72b + Number of iterations 50 - Gas limit: 16190 - Message Id: 0x348ab504289f5a0a0ffed023d8315668dbdd8a8a2ae93349845e837e397aa1ac + Number of iterations 99 - Gas limit: 26485 - Message Id: 0xe4dfdac75ae90f355583b356033179a89a95e5174256a04c371ab10dfe433d96 + ``` + +2. Open the [CCIP explorer](https://ccip.chain.link), search each message by its ID, and wait for each message to be successfully transmitted (`Status` in the explorer: `Success`). + +For the example above, here are the destination transaction hashes: + +| Message id | Ethereum Sepolia transaction hash | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| 0x7a51c84fd0734afef641e3578b29e718c208d6ac2e50826f73a3fec24076a72b | 0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413 | +| 0x348ab504289f5a0a0ffed023d8315668dbdd8a8a2ae93349845e837e397aa1ac | 0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413 | +| 0xe4dfdac75ae90f355583b356033179a89a95e5174256a04c371ab10dfe433d96 | 0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413 | + +**Note** that the Ethereum Sepolia transaction hash is the same for all the messages. This is because CCIP batched the messages. + +### Check the actual gas usage + +1. Open [Tenderly](https://dashboard.tenderly.co) and Search for the [destination transaction hash](https://dashboard.tenderly.co/tx/sepolia/0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413). + +2. Search for `_callWithExactGasSafeReturnData` that has payload containing your messageId (without `0x`). This is an [example](https://dashboard.tenderly.co/tx/0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413?trace=0.0.0.12.0.2.2) for `0x7a51c84fd0734afef641e3578b29e718c208d6ac2e50826f73a3fec24076a72b`. + +3. Just below it, you will find the call trace from the Router to your Receiver contract. This is the [trace](https://dashboard.tenderly.co/tx/0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413?trace=0.0.0.12.0.2.2). + + > **CAUTION: Note** + > + > Tenderly's numeric trace indices aren't stable over time (reprocessing can shift them), so if the trace link above + > doesn't work, you can still find the correct trace by searching for `_callWithExactGasSafeReturnData`. + +4. Right below the `_callWithExactGasSafeReturnData`, you will see the `Router` => `Receiver`.`ccipReceive` call. If it shows "No source for this contract", click on "Fetch the contract from public explorer" to load the source code from Etherscan since it's already verified. When you click on the value of the `message` parameter, it will display the exact message ID (as `messageId`) (i.e., `0x7a51c84fd0734afef641e3578b29e718c208d6ac2e50826f73a3fec24076a72b` in this case) as one of its fields. + +5. Click on the debugger and you'll get the gas details: + + ```text + "gas":{ + "gas_left":5685 + "gas_used":5016 + "total_gas_used":7994315 + } + ``` + +6. Note the `gas_left` is equal to the limit that is set in the `sendCCIPMessages.ts` script: `5685`. The `gas_used` is the actual gas used by the Receiver contract to process the message. + +7. Repeating the same steps for the other two messages, we can summarize the output: + + | Scenario | Number of iterations | Estimated gas usage (Hardhat) | Buffered gas limit (+10%) | Gas used on testnet | + | ------------------------ | -------------------- | ----------------------------- | ------------------------- | ------------------- | + | Baseline gas consumption | 0 | 5151 | 5666 | 5016 | + | Average gas consumption | 50 | 11301 | 12431 | 11166 | + | Peak gas consumption | 99 | 17328 | 19061 | 17193 | + +Testing on testnets has confirmed that a gas limit of `19061` is adequate for the `ccipReceive` function to execute successfully under various conditions. However, it is important to note that gas usage may differ across testnets. Therefore, it is advisable to conduct similar validation efforts on the blockchain where you intend to deploy. Deploying and validating contracts across multiple testnets can be time-consuming. For efficiency, consider using [offchain methods](#offchain-methods) to estimate gas usage. + +## Offchain methods + +This section guides you through estimating gas usage using two different offchain methods: + +- A Web3 provider using the [ethers.js `estimateGas`](https://docs.ethers.org/v6/api/providers/#Provider-estimateGas) function. +- [Tenderly simulation API](https://docs.tenderly.co/reference/api#tag/Simulations/operation/simulateTransaction). The Tenderly simulation API provides a more accurate result (Read this [blog post](https://blog.tenderly.co/how-tenderly-enables-most-accurate-ethereum-gas-estimation/) to learn more) but you are limited to the blockchains supported by Tenderly. + +These methods provide the most accurate and rapid means to determine the necessary gas limit for the `ccipReceive` function. You will use the same CCIP Receiver contract deployed on the Ethereum Sepolia testnet in the previous section. + +### Prerequisites + +1. In your terminal, navigate to the `offchain` directory: + + ```bash + cd ../offchain + ``` + +2. Modify the `data.json` file to insert the deployed addresses of your Sender and Receiver contracts. + +3. Install the dependencies: + + ```bash + npm install + ``` + +4. Set the password to encrypt your environment variables: + + ```bash + npx env-enc set-pw + ``` + +5. Set up the following environment variables: + + - `ETHEREUM_SEPOLIA_RPC_URL`: The RPC URL for Ethereum Sepolia testnet. You can sign up for a personal endpoint from [Alchemy](https://www.alchemy.com/), [Infura](https://www.infura.io/), or another node provider service. + - `TENDERLY_ACCOUNT_SLUG`: This is one part of your Tenderly API URL. You can [find this value in your Tenderly account](https://docs.tenderly.co/account/projects/account-project-slug). + - `TENDERLY_PROJECT_SLUG`: This is one part of your Tenderly API URL. You can [find this value in your Tenderly account](https://docs.tenderly.co/account/projects/account-project-slug). + - `TENDERLY_ACCESS_KEY`: If you don't already have one, you can [generate a new access token](https://docs.tenderly.co/account/projects/how-to-generate-api-access-token). + + Input these variables using the following command: + + ```bash + npx env-enc set + ``` + +6. Generate [Typechain typings](https://www.npmjs.com/package/typechain) for the Receiver contract: + + ```bash + npm run generate-types + ``` + +### Introduction of the scripts + +The scripts are located in the `src` directory. Each script is self-explanatory and includes comprehensive comments to explain its functionality and usage. There are three scripts: + +- `estimateGasProvider.ts`: This script uses the [`eth_estimateGas`](https://docs.alchemy.com/reference/eth-estimategas) Ethereum API to estimate the gas usage of the `ccipReceive` function. It simulates sending three CCIP messages to the Receiver contract with a varying number of iterations and estimates the gas usage using the [ethers.js `estimateGas`](https://docs.ethers.org/v6/api/providers/#Provider-estimateGas) function. + +- `estimateGasTenderly.ts`: This script leverages the Tenderly `simulate` API to estimate the gas usage of the `ccipReceive` function. Similar to the previous script, it simulates sending three CCIP messages to the Receiver contract with different numbers of iterations and estimates the gas usage using the [Tenderly `simulate` API](https://docs.tenderly.co/reference/api#tag/Simulations/operation/simulateTransaction). + +- `helper.ts`: This script contains helper functions used by the other scripts. The two main functions are: + - `buildTransactionData`: This function constructs a CCIP message for a specified number of iterations and then returns the transaction data. + - `estimateIntrinsicGas`: Exclusively called by the `estimateGasProvider.ts` script, this function estimates the intrinsic gas of a transaction. The intrinsic gas represents the minimum amount of gas required before executing a transaction. It is determined by the transaction data and the type of transaction. Since this gas is paid by the initiator of the transaction, we use this function to estimate the intrinsic gas and then deduct it from the total gas used to isolate the gas consumed by the `ccipReceive` function. + +### Estimate gas using a Web3 provider + +Ethereum nodes implement the [`eth_estimateGas`](https://docs.alchemy.com/reference/eth-estimategas) Ethereum API to predict the gas required for a transaction's successful execution. To estimate the gas usage of the `ccipReceive` function, you can directly call the `eth_estimateGas` API via a Web3 provider or leverage a library like ethers.js, simplifying this interaction. This guide focuses on the [ethers.js `estimateGas`](https://docs.ethers.org/v6/api/providers/#Provider-estimateGas) function for gas estimation. To estimate the gas usage, execute the following command in your terminal: + +```bash +npm run estimate-gas-provider +``` + +Your output should look something like this: + +```text +$ npm run estimate-gas-provider + +> offchain-simulator@1.0.0 estimate-gas-provider +> ts-node src/estimateGasProvider.ts + +Final Gas Usage Report: +Number of iterations: 0 - Gas used: 5363 +Number of iterations: 50 - Gas used: 11525 +Number of iterations: 99 - Gas used: 17564 +``` + +The estimate may exceed the actual gas used by the transaction for various reasons, including differences in node performance and EVM mechanics. For a more precise estimation, consider using Tenderly (see the next section for details). + +### Estimate gas using Tenderly + +To estimate the gas usage of the `ccipReceive` function using Tenderly, execute the following command: + +```bash +npm run estimate-gas-tenderly +``` + +Your output should look something like this: + +```text +$ npm run estimate-gas-tenderly + +> offchain-simulator@1.0.0 estimate-gas-tenderly +> ts-node src/estimateGasTenderly.ts + +Final Gas Usage Report: +Number of iterations: 0 - Gas used: 5016 +Number of iterations: 50 - Gas used: 11166 +Number of iterations: 99 - Gas used: 17193 +``` + +### Comparison + +The table below summarizes the gas estimations for different iterations using both Web3 provider and Tenderly: + +| Scenario | Number of iterations | Gas estimated (Web3 provider) | Gas estimated (Tenderly) | +| ------------------------ | -------------------- | ----------------------------- | ------------------------ | +| Baseline gas consumption | 0 | 5363 | 5016 | +| Average gas consumption | 50 | 11525 | 11166 | +| Peak gas consumption | 99 | 17564 | 17193 | + +The gas estimations from both Web3 provider and Tenderly are consistent across different iterations and align with actual testnet results. This demonstrates the accuracy and reliability of these offchain methods in estimating gas usage. However, you can notice that Tenderly provides more accurate results. + +## Conclusion + +This tutorial has guided you through estimating the gas limit for the `ccipReceive` function using various methods. You have learned how to estimate gas usage in a local environment using Hardhat and Foundry, validate these estimations on testnets, and use offchain methods to estimate gas usage. + +As we have explored various methods for estimating gas for the `ccipReceive` function, it is crucial to apply this knowledge effectively. Here are some targeted recommendations to enhance your approach to gas estimation: + +1. **Comprehensive Testing:** Emphasize testing under diverse scenarios to ensure your gas estimations are robust. Different conditions can significantly affect gas usage, so covering as many cases as possible in your tests is crucial. +2. **Preliminary Local Estimates:** Local testing is a critical first step for estimating gas and ensuring your contracts function correctly under various scenarios. While Hardhat and Foundry facilitate development and testing, it's key to remember that these environments may not perfectly mirror your target blockchain's conditions. These initial estimates serve as valuable insights, guiding you toward more accurate gas limit settings when you proceed to testnet validations. Incorporating a buffer based on these preliminary results can help mitigate the risks of underestimating gas requirements due to environmental differences. +3. **Efficiency with Offchain Methods:** Since testing across different blockchains can be resource-intensive, leaning on offchain methods for gas estimation is invaluable. Tools such as Tenderly not only facilitate rapid and accurate gas usage insights on your target blockchains but also enable you to simulate the execution of the `ccipReceive` function on actual contracts deployed on mainnets. If Tenderly doesn't support a particular blockchain, a practical alternative is to use a Web3 provider that does support that chain, as illustrated in the [Estimate gas using a Web3 provider](#estimate-gas-using-a-web3-provider) section. This is particularly helpful when considering the diversity in gas metering rules across blockchains. This approach saves time and enhances the precision of your gas limit estimations, allowing for more cost-effective transactions from your dApp. + +--- + +# Configure a sender allowlist with AdvancedPoolHooks using Foundry +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry +Last Updated: 2026-05-05 + +## Guide Versions + +- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry) + +- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat) + +CCIP 2.0 token pools support an optional [`AdvancedPoolHooks`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) contract being attached to them. + +This contract can run certain checks: + +1. before tokens are locked/burned (source chain) +2. before tokens are released/minted (destination chain) + +If no hooks contract is attached, the token pool keeps its existing behavior. + +This tutorial covers the **sender allowlist** use case: only approved addresses may initiate outbound transfers on the source chain. A sender not on the list reverts the source transaction with `SenderNotAllowed`. For the other capabilities, see the [AdvancedPoolHooks concept page](/ccip/concepts/cross-chain-token/advanced-pool-hooks) or [Enforce ACE policies on CCIP token transfers](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry). + +Hooks are enforced only when **both** of these aspects are configured: + +- The token pool points to the `AdvancedPoolHooks` contract. +- The hooks contract authorizes that token pool as a caller. + +Authorize the pool before attaching hooks so the first hook call does not revert. Hooks are configured **per token pool, per chain**, which means that: +you need to repeat this setup for any other pool where you want hooks enforced. + +> **Note:** Remember. Your CCT token can be deployed on multiple chains, with each chain having its own token pool. + +In this tutorial you will: + +1. Review the current hooks state on your deployed token pool. +2. Send a baseline transfer (*with no hooks attached*). This should succeed. +3. Deploy an `AdvancedPoolHooks` contract with your deployer address on the allowlist. +4. Authorize the pool as a caller on the hooks contract and attach the hooks contract to the token pool. +5. Verify the hooks attachment, authorized callers, and allowlist state. +6. Remove your address from the allowlist. +7. Attempt a transfer (*expected to revert with `SenderNotAllowed(address)`*). +8. Detach the hooks contract. +9. Send a transfer again (*expected to succeed*). +10. Manage the allowlist over time (add, remove, and check addresses). + +## Before You Begin + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If `npm` is unavailable, reinstall or update Node.js. + +2. **Install Foundry**: + If you haven't already, follow the instructions in the [Foundry documentation](https://book.getfoundry.sh/getting-started/installation) to install Foundry. \ + Verify the installation by running the following command: + +```bash filename="Terminal" +forge --version +``` + +3. Install/Update Chainlink `ccip-cli`: + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +ccip-cli --version +``` + +4. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-foundry) + +Clone the CCIP 2.0 template for a smoother setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-foundry.git +cd docs-cct-foundry +``` + +5. Create an encrypted Foundry keystore, if you haven't already: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +> **NOTE: Note** +> +> 1. For more information on how to create an encrypted Foundry keystore, refer to the +> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 2. `cast wallet import` will throw an error if a keystore with the same name already exists. +> 3. Use `cast wallet list` to check previously created keystores. + +6. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `cast wallet import`) +KEYSTORE_NAME= + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +This tutorial uses a single wallet for every deployment, configuration update, and test transfer: the wallet stored in `KEYSTORE_NAME`. + +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/HelperConfig.s.sol) + +View the complete list of supported chains in the `HelperConfig.s.sol` file. + +7. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +8. Build the project: + +```bash filename="Terminal" +npm install && forge build +``` + +## Tutorial + +### 1. Confirm prerequisites, addresses, and permissions + +This tutorial assumes you have already deployed tokens and token pools and configured a working lane. If not, complete one of the registration tutorials first: + +- [Burn & Mint (Foundry)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) +- [Lock & Mint (Foundry)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) + +Export the addresses for the chain you are configuring: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN=0x... +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x... +``` + +Use these values throughout the tutorial: + +- `ETHEREUM_SEPOLIA_TOKEN_POOL` is the [`TokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) v2 contract being configured. +- `POOL_HOOKS` is the deployed `AdvancedPoolHooks` contract, exported after deployment. +- `ETHEREUM_SEPOLIA_TOKEN` is the source-chain token that we will transfer across to the destination chain (Arbitrum Sepolia) + +> **CAUTION: Funding requirements** +> +> - **Sepolia ETH**: the wallet `KEYSTORE_NAME` needs ETH on Sepolia to pay gas for +> transactions. +> - **Your token on Sepolia**: the same wallet needs some `ETHEREUM_SEPOLIA_TOKEN` to transfer +> the tokens across to the destination chain (Arbitrum Sepolia). + +> **NOTE: One-wallet assumption** +> +> 1. This tutorial assumes the wallet stored in `KEYSTORE_NAME` is the owner of the token pool and will be +> the owner of the deployed `AdvancedPoolHooks` contract. 2. These roles can be distributed across multiple +> wallets if needed. + +> **NOTE: Target the right contract** +> +> - `AdvancedPoolHooks` attach to a **TokenPool v2** contract. In Lock & Mint, do not +> confuse the token pool with the `ERC20LockBox` (this tutorial configures token pool hooks only). +> - Hooks are configured per token pool, per chain. Repeat this setup for any other pool where you want hooks +> enforced. + +> **CAUTION: Common failure cases** +> +> - **Missing function selector:** the target pool is likely not a TokenPool v2 contract and does not +> support advanced hooks. +> - **Wrong signer / wrong owner:** if a write script reverts with `OnlyCallableByOwner()`, +> the wallet stored in `KEYSTORE_NAME` is not the current owner of that contract. +> - **AllowListNotEnabled():** the hooks contract was deployed with an empty allowlist, so allowlist mode +> is disabled for that deployment. +> - **Hook call reverts:** confirm the token pool is listed as an authorized caller on the hooks contract +> before attaching hooks. +> - **SenderNotAllowed(address):** the transfer sender is not allowlisted as the original outbound +> sender. + +### 2. Review current pool hooks state + +Use the `GetAdvancedPoolHooks.s.sol` script to check whether an `AdvancedPoolHooks` contract is currently attached to your token pool. + +[GetAdvancedPoolHooks.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/GetAdvancedPoolHooks.s.sol) + +View the hooks query script on GitHub. + +```bash filename="Terminal" +forge script \ + script/configure/allowlist/GetAdvancedPoolHooks.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Example output (no hooks attached): + +```bash filename="Terminal" +======================================== +🪝 Get Advanced Pool Hooks +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View pool hooks +======================================== + +No AdvancedPoolHooks contract is attached to this pool. +Deploy one with DeployAdvancedPoolHooks.s.sol and attach it via UpdateAdvancedPoolHooks.s.sol. + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +> **NOTE** +> +> 1. `getAdvancedPoolHooks()` is only available on TokenPool v2.0 and later. If the script reverts with a +> missing function selector, the pool is a v1 contract and does not support hooks. 2. If your token pool already has +> hooks attached, you can detach them by following the 'Detach hooks' section, to follow the tutorial. + +### 3. Send a baseline transfer (no hooks attached) + +This step confirms your lane works before enabling hooks. Make sure the previous step shows no hooks attached. + +> **NOTE: Router address** +> +> You can find the router address for your chain in the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). + +Export the Sepolia router address for `ccip-cli`: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x... +``` + +> **NOTE: Token amounts in this tutorial** +> +> 1. Earlier Foundry scripts use smallest-unit amounts (wei-like). In the `ccip-cli` examples below we use a +> compact, human-readable token amount (for example, `1.23`). +> 2. The wallet stored in `KEYSTORE_NAME` should have some of your tokens on Sepolia so it can send a +> cross-chain transfer. + +Send a baseline transfer to confirm if the cross chain transfer succeeds without any hooks attached. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xreceiveraddress \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Expected output (example): + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +### 4. Deploy AdvancedPoolHooks + +Use the `DeployAdvancedPoolHooks.s.sol` script to deploy a new hooks contract. + +> **NOTE: Inspect script/input/advanced-pool-hooks.json before deployment** +> +> The `DeployAdvancedPoolHooks.s.sol` script reads `script/input/advanced-pool-hooks.json` when +> environment variables are omitted. Inspect the file and clear any stale addresses you do not intend to use. Because +> allowlist enablement is permanent, prefer setting `ALLOWLIST` and `AUTHORIZED_CALLERS` +> explicitly. + +[DeployAdvancedPoolHooks.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/DeployAdvancedPoolHooks.s.sol) + +View the hooks deployment script on GitHub. + +Deployment inputs: + +- `ALLOWLIST`: CSV or JSON array of original outbound sender addresses allowed to initiate outbound transfers. +- `AUTHORIZED_CALLERS`: CSV or JSON array of token pool addresses permitted to invoke the hooks. + +> **CAUTION: Allowlist enablement is permanent** +> +> 1. Allowlist mode is set at deployment time and **cannot be changed later**. +> 2. If you deploy with a non-empty `ALLOWLIST`, the allowlist is permanently enabled. +> 3. If you deploy with an empty allowlist, calling `applyAllowListUpdates()` later will revert with +> `AllowListNotEnabled()`. + +In this example, you will deploy hooks on Ethereum Sepolia with your deployer address on the allowlist, and authorize your token pool as a caller: + +> **NOTE: Why we start by allowlisting the deployer** +> +> 1. You will first allowlist your deployer address so you can attach hooks without immediately blocking your own +> transfers. 2. Later in the tutorial, you will remove your address from the allowlist to demonstrate an enforced +> failure. + +```bash filename="Terminal" +ALLOWLIST=0xYourDeployerAddress \ +AUTHORIZED_CALLERS=$ETHEREUM_SEPOLIA_TOKEN_POOL \ +forge script \ +script/configure/allowlist/DeployAdvancedPoolHooks.s.sol \ +--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ +--account $KEYSTORE_NAME \ +--broadcast \ +--verify +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔒 Deploy Advanced Pool Hooks +======================================== +Chain: Ethereum Sepolia +Action: Deploy pool hooks +======================================== + +Advanced Pool Hooks Parameters: +Allowlist Enabled: Yes +Allowlist Size: 1 +[0] 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Threshold Amount: Disabled (0) +Policy Engine: Disabled (0x0) +Authorized Callers Enabled: Yes +Authorized Callers Size: 1 +[0] 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + +[Step 1] Deploying AdvancedPoolHooks on Ethereum Sepolia +AdvancedPoolHooks deployed at: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +✅ AdvancedPoolHooks deployed successfully! + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +AdvancedPoolHooks Address: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE + +Deployment saved: history/advanced-pool-hooks/ethereum-testnet-sepolia/1746652800-BnM-T-BurnMint-AdvancedPoolHooks.json + +Configuration Summary: +Allowlist: Enabled +Threshold: Disabled +Policy Engine: Disabled +Authorized Callers: Enabled + +Next Steps: +1. When deploying a TokenPool, pass this hooks address as the 'poolHooks' parameter +2. Attach to an existing pool: TOKEN_POOL=
    NEW_HOOK=
    forge script script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol --rpc-url $RPC_URL --account $KEYSTORE_NAME --broadcast +3. Manage allowlist: POOL_HOOKS=
    ADD_ADDRESSES="0xAddr" forge script script/configure/allowlist/UpdateAllowList.s.sol --rpc-url $RPC_URL --account $KEYSTORE_NAME --broadcast +======================================== +``` + +Export the hooks address for subsequent commands: + +```bash filename="Terminal" +export POOL_HOOKS=0xHooksAddress +``` + +> **NOTE: Deployment address recovery** +> +> - A broadcast deploy records the hooks address in the project store (`project/.json`, +> under `addresses.active.poolHooks` and the `deployments` entry), so later scripts resolve it +> automatically with no manual export. +> - If you lose the session export, read the address from the project store, or from the append-only history ledger at +> `history/advanced-pool-hooks//` (the `POOL_HOOKS` key in the JSON file). +> - For a pool or token deployed outside this repository, run `make adopt-token` to record it in the same +> registry. Environment variable exports still work as temporary overrides, but they are not saved. + +### 5. Authorize the pool and attach hooks + +Authorize the token pool as a caller on the hooks contract before attaching hooks so the first hook call does not revert. + +> **NOTE: Already authorized at deploy time?** +> +> If you passed `AUTHORIZED_CALLERS` when deploying the hooks contract (as shown in the previous step), your +> pool is already authorized. You can skip the authorization command and go directly to attaching the hooks. + +**If you need to authorize the pool separately**, use `UpdateAuthorizedCallers.s.sol`: + +[UpdateAuthorizedCallers.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol) + +View the authorized callers script on GitHub. + +```bash filename="Terminal" +POOL_HOOKS=$POOL_HOOKS \ + ADD_ADDRESSES=$ETHEREUM_SEPOLIA_TOKEN_POOL \ + forge script \ + script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +📝 Update Authorized Callers +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: Update authorized callers +======================================== + +Adding 1 caller(s): +[0] 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + +======================================== +✅ Authorized callers updated on Ethereum Sepolia! +======================================== +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +**Now attach the hooks** to your token pool using `UpdateAdvancedPoolHooks.s.sol`: + +[UpdateAdvancedPoolHooks.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol) + +View the hooks update script on GitHub. + +```bash filename="Terminal" +NEW_HOOK=$POOL_HOOKS \ + forge script \ + script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔄 Update Advanced Pool Hooks +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Update pool hooks +======================================== + +New Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE + +✅ AdvancedPoolHooks updated successfully! + +======================================== +✅ Pool hooks updated on Ethereum Sepolia! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +New Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +New Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +> **NOTE: Access control** +> +> - Updating authorized callers requires the hooks contract **owner**. +> - Attaching or detaching hooks requires the token pool **owner**. +> - If either command reverts with `OnlyCallableByOwner()`, the wallet stored in `KEYSTORE_NAME` +> is not the current owner of the target contract. + +### 6. Verify the setup + +Run the following read-only scripts to verify wiring. These checks confirm configuration state only;the next step attempts a transfer that is expected to revert. + +**1. Confirm hooks are attached to the pool:** + +```bash filename="Terminal" +forge script \ + script/configure/allowlist/GetAdvancedPoolHooks.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Expected output should now show your hooks address: + +```bash filename="Terminal" +======================================== +🪝 Get Advanced Pool Hooks +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View pool hooks +======================================== + +✅ AdvancedPoolHooks: + 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +**2. Confirm the pool is an authorized caller on the hooks:** + +[GetAuthorizedCallers.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/authorized-callers/GetAuthorizedCallers.s.sol) + +View the authorized callers query script on GitHub. + +```bash filename="Terminal" +POOL_HOOKS=$POOL_HOOKS \ + forge script \ + script/configure/authorized-callers/GetAuthorizedCallers.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔎 Get Authorized Callers +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: View authorized callers +======================================== + +Authorized Callers count: 1 +[0] 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +**3. View the current allowlist:** + +[GetAllowList.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/GetAllowList.s.sol) + +View the allowlist query script on GitHub. + +```bash filename="Terminal" +POOL_HOOKS=$POOL_HOOKS \ + forge script \ + script/configure/allowlist/GetAllowList.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔎 Get AllowList +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: View allowlist +======================================== + +AllowList count: 1 +0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +**4. (Optional): Check if a particular sender is allowlisted:** + +[IsAllowListed.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/IsAllowListed.s.sol) + +View the allowlist check script on GitHub. + +```bash filename="Terminal" +POOL_HOOKS=$POOL_HOOKS \ +CHECK_ADDRESS=0xsenderaddress \ +forge script \ +script/configure/allowlist/IsAllowListed.s.sol \ +--rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔎 Is AllowListed? +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Check Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Action: Check allowlist +======================================== + +✅ Address IS allowlisted. +======================================== +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +### 7. Remove your address from the allowlist + +In this step, you remove your own address from the hooks allowlist. This sets up the next transfer to fail. + +> **NOTE: Admin access vs transfer permissions** +> +> Updating the allowlist is admin-only (`onlyOwner`) and is not gated by allowlist membership. Even if you +> are the hooks admin, your transfers will still revert unless your sender address is on the allowlist. + +Remove your deployer address from the allowlist: + +```bash filename="Terminal" +POOL_HOOKS=$POOL_HOOKS \ + REMOVE_ADDRESSES="0xYourDeployerAddress" \ + forge script \ + script/configure/allowlist/UpdateAllowList.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Verify that your address is no longer allowlisted: + +```bash filename="Terminal" +POOL_HOOKS=$POOL_HOOKS \ +CHECK_ADDRESS=0xYourDeployerAddress \ +forge script \ +script/configure/allowlist/IsAllowListed.s.sol \ +--rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +### 8. Attempt a transfer (expected failure) + +This step attempts a token transfer and is expected to fail because you removed your sender address from the allowlist. Hooks are configured per token pool, per chain, although this does not attach hooks on any destination-chain pool. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourDeployerAddress \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Expected result: the send transaction reverts. You should see an error that includes `SenderNotAllowed(address)`. + +If this step does **not** fail as expected, make sure: + +- Hooks are attached (`GetAdvancedPoolHooks.s.sol` shows `POOL_HOOKS`). +- Your sender address is **NOT** on the allowlist (previous step returns NOT allowlisted). +- The token pool is an authorized caller on the hooks (`GetAuthorizedCallers.s.sol` includes `ETHEREUM_SEPOLIA_TOKEN_POOL`). + +### 9. Detach hooks + +Detach hooks to disable enforcement. This sets the pool hooks address back to the zero address. + +```bash filename="Terminal" +NEW_HOOK=0x0000000000000000000000000000000000000000 \ + forge script \ + script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Verify hooks are detached: + +```bash filename="Terminal" +forge script \ + script/configure/allowlist/GetAdvancedPoolHooks.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +### 10. Send a transfer again (expected success) + +With hooks detached, the token pool returns to its default behavior and your transfer should succeed again. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourDeployerAddress \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Expected output (example): + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +🚀 Sending message to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +### 11. Manage the allowlist + +Use the `UpdateAllowList.s.sol` script to add or remove addresses from the allowlist. Only the hooks contract **owner** can modify the allowlist. + +[UpdateAllowList.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/UpdateAllowList.s.sol) + +View the allowlist update script on GitHub. + +| Env var | Required | Description | +| ------------------ | -------- | ------------------------------------------- | +| `POOL_HOOKS` | Yes (v2) | Address of the `AdvancedPoolHooks` contract | +| `ADD_ADDRESSES` | No | CSV or JSON array of addresses to add | +| `REMOVE_ADDRESSES` | No | CSV or JSON array of addresses to remove | + +> **NOTE: v2 pools require POOL_HOOKS** +> +> Always pass `POOL_HOOKS`. The allowlist lives on the `AdvancedPoolHooks` contract, not on the +> pool itself. + +**Add an address:** + +```bash filename="Terminal" +POOL_HOOKS=$POOL_HOOKS \ + ADD_ADDRESSES="0xYourDeployerAddress" \ + forge script \ + script/configure/allowlist/UpdateAllowList.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +📝 Update AllowList +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: Update allowlist +======================================== + +✅ AllowList updated successfully! + +======================================== +✅ Allowlist updated on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +**Remove an address:** + +```bash filename="Terminal" +POOL_HOOKS=$POOL_HOOKS \ + REMOVE_ADDRESSES="0xYourDeployerAddress" \ + forge script \ + script/configure/allowlist/UpdateAllowList.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +**Verify the updated allowlist** by re-running the `GetAllowList.s.sol` or `IsAllowListed.s.sol` scripts from the previous step. + +--- + +# Configure a sender allowlist with AdvancedPoolHooks using Hardhat +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat +Last Updated: 2026-05-05 + +## Guide Versions + +- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry) + +- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat) + +CCIP 2.0 token pools support an optional [`AdvancedPoolHooks`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) contract being attached to them. + +This contract can run certain checks: + +1. before tokens are locked/burned (source chain) +2. before tokens are released/minted (destination chain) + +If no hooks contract is attached, the token pool keeps its existing behavior. + +This tutorial covers the **sender allowlist** use case: only approved addresses may initiate outbound transfers on the source chain. A sender not on the list reverts the source transaction with `SenderNotAllowed`. For the other capabilities, see the [AdvancedPoolHooks concept page](/ccip/concepts/cross-chain-token/advanced-pool-hooks) or [Enforce ACE policies on CCIP token transfers](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-hardhat). + +Hooks are enforced only when **both** of these aspects are configured: + +- The token pool points to the `AdvancedPoolHooks` contract. +- The hooks contract authorizes that token pool as a caller. + +Authorize the pool before attaching hooks so the first hook call does not revert. Hooks are configured **per token pool, per chain**, which means that: +you need to repeat this setup for any other pool where you want hooks enforced. + +> **Note:** Remember. Your CCT token can be deployed on multiple chains, with each chain having its own token pool. + +In this tutorial you will: + +1. Review the current hooks state on your deployed token pool. +2. Send a baseline transfer (*with no hooks attached*). This should succeed. +3. Deploy an `AdvancedPoolHooks` contract with your deployer address on the allowlist. +4. Authorize the pool as a caller on the hooks contract and attach the hooks contract to the token pool. +5. Verify the hooks attachment, authorized callers, and allowlist state. +6. Remove your address from the allowlist. +7. Attempt a transfer (*expected to revert with `SenderNotAllowed(address)`*). +8. Detach the hooks contract. +9. Send a transfer again (*expected to succeed*). +10. Manage the allowlist over time (add, remove, and check addresses). + +## Before You Begin + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If `npm` is unavailable, reinstall or update Node.js. + +2. Install/Update Chainlink `ccip-cli`: + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +ccip-cli --version +``` + +3. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-hardhat) + +Clone the CCIP 2.0 Hardhat template for a smoother setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-hardhat.git +cd docs-cct-hardhat +``` + +4. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `npx hardhat keystore set`) +KEYSTORE_NAME= + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment tasks) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +This tutorial uses a single wallet for every deployment, configuration update, and test transfer: the wallet stored in `KEYSTORE_NAME`. + +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/helper-config.ts) + +View the complete list of supported chains in the `helper-config.ts` file. + +5. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +6. Build the project: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +7. Create an encrypted Hardhat keystore, if you haven't already: + +```bash filename="Terminal" +npx hardhat keystore set PRIVATE_KEY +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +> **NOTE: Note** +> +> 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it +> asks you to enter your private key as hidden text. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in +> `.env`. +> 3. Use `npx hardhat keystore list` to check previously created keystores. + +## Tutorial + +### 1. Confirm prerequisites, addresses, and permissions + +This tutorial assumes you have already deployed tokens and token pools and configured a working lane. If not, complete one of the registration tutorials first: + +- [Burn & Mint (Hardhat)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) +- [Lock & Mint (Hardhat)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) + +Export the addresses for the chain you are configuring: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN=0x... +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +export ARBITRUM_SEPOLIA_TOKEN=0x... +export ARBITRUM_SEPOLIA_TOKEN_POOL=0x... +``` + +Use these values throughout the tutorial: + +- `ETHEREUM_SEPOLIA_TOKEN_POOL` is the [`TokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) v2 contract being configured. +- `POOL_HOOKS` is the deployed `AdvancedPoolHooks` contract, exported after deployment. +- `ETHEREUM_SEPOLIA_TOKEN` is the source-chain token that we will transfer across to the destination chain (Arbitrum Sepolia) + +> **CAUTION: Funding requirements** +> +> - **Sepolia ETH**: the wallet `KEYSTORE_NAME` needs ETH on Sepolia to pay gas for +> transactions. +> - **Your token on Sepolia**: the same wallet needs some `ETHEREUM_SEPOLIA_TOKEN` to transfer +> the tokens across to the destination chain (Arbitrum Sepolia). + +> **NOTE: One-wallet assumption** +> +> 1. This tutorial assumes the wallet stored in `KEYSTORE_NAME` is the owner of the token pool and will be +> the owner of the deployed `AdvancedPoolHooks` contract. 2. These roles can be distributed across multiple +> wallets if needed. + +> **NOTE: Target the right contract** +> +> - `AdvancedPoolHooks` attach to a **TokenPool v2** contract. In Lock & Mint, do not +> confuse the token pool with the `ERC20LockBox` (this tutorial configures token pool hooks only). +> - Hooks are configured per token pool, per chain. Repeat this setup for any other pool where you want hooks +> enforced. + +> **CAUTION: Common failure cases** +> +> - **Missing function selector:** the target pool is likely not a TokenPool v2 contract and does not +> support advanced hooks. +> - **Wrong signer / wrong owner:** if a write task reverts with `OnlyCallableByOwner()`, the +> wallet stored in `KEYSTORE_NAME` is not the current owner of that contract. +> - **AllowListNotEnabled():** the hooks contract was deployed with an empty allowlist, so allowlist mode +> is disabled for that deployment. +> - **Hook call reverts:** confirm the token pool is listed as an authorized caller on the hooks contract +> before attaching hooks. +> - **SenderNotAllowed(address):** the transfer sender is not allowlisted as the original outbound +> sender. + +### 2. Review current pool hooks state + +Use the `getAdvancedPoolHooks` task to check whether an `AdvancedPoolHooks` contract is currently attached to your token pool. + +[getAdvancedPoolHooks.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/allowlist/getAdvancedPoolHooks.ts) + +View the hooks query task on GitHub. + +```bash filename="Terminal" +npx hardhat getAdvancedPoolHooks --network sepolia +``` + +Example output (no hooks attached): + +```bash filename="Terminal" +======================================== +🪝 Get Advanced Pool Hooks +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View pool hooks +======================================== + +No AdvancedPoolHooks contract is attached to this pool. + Deploy one with: npx hardhat deployAdvancedPoolHooks + Attach it with: npx hardhat updateAdvancedPoolHooks --newhook
    + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +> **NOTE** +> +> 1. `getAdvancedPoolHooks()` is only available on TokenPool v2.0 and later. 2. If your token pool already +> has hooks attached, you can detach them by following the 'Detach hooks' section, to follow the tutorial. + +### 3. Send a baseline transfer (no hooks attached) + +This step confirms your lane works before enabling hooks. Make sure the previous step shows no hooks attached. + +> **NOTE: Router address** +> +> You can find the router address for your chain in the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). + +Export the Sepolia router address for `ccip-cli`: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x... +``` + +> **NOTE: Token amounts in this tutorial** +> +> 1. Earlier Hardhat tasks use smallest-unit amounts (wei-like). In the `ccip-cli` examples below we use a +> compact, human-readable token amount (for example, `1.23`). +> 2. The wallet stored in `KEYSTORE_NAME` should have some of your tokens on Sepolia so it can send a +> cross-chain transfer. + +Send a baseline transfer to confirm if the cross chain transfer succeeds without any hooks attached. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xreceiveraddress \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Expected output (example): + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +### 4. Deploy AdvancedPoolHooks + +Use the `deployAdvancedPoolHooks` task to deploy a new hooks contract. + +[deployAdvancedPoolHooks.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/allowlist/deployAdvancedPoolHooks.ts) + +View the hooks deployment task on GitHub. + +Deployment flags: + +- `--allowlist`: CSV list of original outbound sender addresses allowed to initiate outbound transfers. +- `--authorizedcallers`: CSV list of token pool addresses permitted to invoke the hooks. + +> **CAUTION: Allowlist enablement is permanent** +> +> 1. Allowlist mode is set at deployment time and **cannot be changed later**. +> 2. If you deploy with a non-empty `--allowlist`, the allowlist is permanently enabled. +> 3. If you deploy with an empty allowlist, calling `applyAllowListUpdates()` later will revert with +> `AllowListNotEnabled()`. + +In this example, you will deploy hooks on Ethereum Sepolia with your deployer address on the allowlist, and authorize your token pool as a caller: + +> **NOTE: Why we start by allowlisting the deployer** +> +> 1. You will first allowlist your deployer address so you can attach hooks without immediately blocking your own +> transfers. 2. Later in the tutorial, you will remove your address from the allowlist to demonstrate an enforced +> failure. + +```bash filename="Terminal" +npx hardhat deployAdvancedPoolHooks \ + --allowlist 0xYourDeployerAddress \ + --authorizedcallers $ETHEREUM_SEPOLIA_TOKEN_POOL \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔒 Deploy Advanced Pool Hooks +======================================== +Chain: Ethereum Sepolia +Action: Deploy pool hooks +======================================== + +Advanced Pool Hooks Parameters: + Allowlist Enabled: Yes + Allowlist Size: 1 + [0] 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Threshold Amount: Disabled (0) + Policy Engine: Disabled (0x0) + Authorized Callers Enabled: Yes + Authorized Callers Size: 1 + [0] 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + +[Step 1] Deploying AdvancedPoolHooks on Ethereum Sepolia +⏳ Deployment tx: 0x9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d +✅ AdvancedPoolHooks deployed successfully! + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +AdvancedPoolHooks Address: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE + +Deployment saved: deployments/advanced-pool-hooks/ETHEREUM_SEPOLIA/1746652800-AdvancedPoolHooks.json + +Configuration Summary: + Allowlist: Enabled + Threshold: Disabled + Policy Engine: Disabled + Authorized Callers: Enabled + +Next Steps: + 1. When deploying a TokenPool, pass this hooks address as the 'poolHooks' parameter + 2. Attach to an existing pool: npx hardhat updateAdvancedPoolHooks --newhook
    --network + 3. Manage allowlist: npx hardhat updateAllowList --poolhooks
    --add "0xAddr" --network +======================================== +``` + +Export the hooks address for subsequent commands: + +```bash filename="Terminal" +export POOL_HOOKS=0xHooksAddress +``` + +> **NOTE: Deployment address recovery** +> +> - Deployment addresses are saved to `deployments/advanced-pool-hooks/`. +> - If you lose the session export, retrieve the address from the JSON file under the `POOL_HOOKS` key. + +### 5. Authorize the pool and attach hooks + +Authorize the token pool as a caller on the hooks contract before attaching hooks so the first hook call does not revert. + +> **NOTE: Already authorized at deploy time?** +> +> If you passed `--authorizedcallers` when deploying the hooks contract (as shown in the previous step), your +> pool is already authorized. You can skip the authorization command and go directly to attaching the hooks. + +**If you need to authorize the pool separately**, use the `updateAuthorizedCallers` task: + +[updateAuthorizedCallers.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/authorized-callers/updateAuthorizedCallers.ts) + +View the authorized callers task on GitHub. + +```bash filename="Terminal" +npx hardhat updateAuthorizedCallers \ + --poolhooks $POOL_HOOKS \ + --add $ETHEREUM_SEPOLIA_TOKEN_POOL \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +📝 Update Authorized Callers +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: Update authorized callers +======================================== + +Adding 1 caller(s): + [0] 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + +[Step 1] Applying authorized caller updates on Ethereum Sepolia +⏳ Tx: 0xaaabbbcccdddeeefff000111222333444555666777888999aaabbbcccdddeeff + +======================================== +✅ Authorized callers updated on Ethereum Sepolia! +======================================== +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Transaction: 0xaaabbbcccdddeeefff000111222333444555666777888999aaabbbcccdddeeff +======================================== +``` + +**Now attach the hooks** to your token pool using the `updateAdvancedPoolHooks` task: + +[updateAdvancedPoolHooks.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/allowlist/updateAdvancedPoolHooks.ts) + +View the hooks update task on GitHub. + +| Flag | Required | Description | +| ------------- | -------- | ------------------------------------------------------------- | +| `--tokenpool` | No | Token pool address (defaults to `{CHAIN}_TOKEN_POOL` env var) | +| `--newhook` | Yes | Address of the deployed `AdvancedPoolHooks` contract | + +```bash filename="Terminal" +npx hardhat updateAdvancedPoolHooks \ + --newhook $POOL_HOOKS \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔄 Update Advanced Pool Hooks +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Update pool hooks +======================================== + +New Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE + +[Step 1] Updating AdvancedPoolHooks on Ethereum Sepolia +⏳ Tx: 0xbbccddeeff00112233445566778899aabbccddeeff00112233445566778899aa +✅ AdvancedPoolHooks updated successfully! + +======================================== +✅ Pool hooks updated on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +New Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +> **NOTE: Access control** +> +> - Updating authorized callers requires the hooks contract **owner**. +> - Attaching or detaching hooks requires the token pool **owner**. +> - If either command reverts with `OnlyCallableByOwner()`, the wallet stored in `KEYSTORE_NAME` +> is not the current owner of the target contract. + +### 6. Verify the setup + +Run the following read-only tasks to verify wiring. These checks confirm configuration state only; the next step attempts a transfer that is expected to revert. + +**1. Confirm hooks are attached to the pool:** + +```bash filename="Terminal" +npx hardhat getAdvancedPoolHooks --network sepolia +``` + +Expected output should now show your hooks address: + +```bash filename="Terminal" +======================================== +🪝 Get Advanced Pool Hooks +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View pool hooks +======================================== + +✅ AdvancedPoolHooks: + 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +**2. Confirm the pool is an authorized caller on the hooks:** + +[getAuthorizedCallers.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/authorized-callers/getAuthorizedCallers.ts) + +View the authorized callers query task on GitHub. + +```bash filename="Terminal" +npx hardhat getAuthorizedCallers \ + --poolhooks $POOL_HOOKS \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔎 Get Authorized Callers +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: View authorized callers +======================================== + +Authorized Callers count: 1 + [0] 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + +======================================== +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +**3. View the current allowlist:** + +[getAllowList.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/allowlist/getAllowList.ts) + +View the allowlist query task on GitHub. + +```bash filename="Terminal" +npx hardhat getAllowList \ + --poolhooks $POOL_HOOKS \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔎 Get AllowList +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: View allowlist +======================================== + +AllowList count: 1 + 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +**4. (Optional): Check if a particular sender is allowlisted:** + +[isAllowListed.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/allowlist/isAllowListed.ts) + +View the allowlist check task on GitHub. + +```bash filename="Terminal" +npx hardhat isAllowListed \ + --poolhooks $POOL_HOOKS \ + --checkaddress 0xsenderaddress \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔎 Is AllowListed? +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Check Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Action: Check allowlist +======================================== + +✅ Address IS allowlisted. +======================================== +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +### 7. Remove your address from the allowlist + +In this step, you remove your own address from the hooks allowlist. This sets up the next transfer to fail. + +> **NOTE: Admin access vs transfer permissions** +> +> Updating the allowlist is admin-only (`onlyOwner`) and is not gated by allowlist membership. Even if you +> are the hooks admin, your transfers will still revert unless your sender address is on the allowlist. + +Remove your deployer address from the allowlist: + +```bash filename="Terminal" +npx hardhat updateAllowList \ + --poolhooks $POOL_HOOKS \ + --remove "0xYourDeployerAddress" \ + --network sepolia +``` + +Verify that your address is no longer allowlisted: + +```bash filename="Terminal" +npx hardhat isAllowListed \ + --poolhooks $POOL_HOOKS \ + --checkaddress 0xYourDeployerAddress \ + --network sepolia +``` + +### 8. Attempt a transfer (expected failure) + +This step attempts a token transfer and is expected to fail because you removed your sender address from the allowlist. Hooks are configured per token pool, per chain, although this does not attach hooks on any destination-chain pool. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourDeployerAddress \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Expected result: the send transaction reverts. You should see an error that includes `SenderNotAllowed(address)`. + +If this step does **not** fail as expected, make sure: + +- Hooks are attached (`getAdvancedPoolHooks` shows `POOL_HOOKS`). +- Your sender address is **NOT** on the allowlist (previous step returns NOT allowlisted). +- The token pool is an authorized caller on the hooks (`getAuthorizedCallers` includes `ETHEREUM_SEPOLIA_TOKEN_POOL`). + +### 9. Detach hooks + +Detach hooks to disable enforcement. This sets the pool hooks address back to the zero address. + +```bash filename="Terminal" +npx hardhat updateAdvancedPoolHooks \ + --newhook 0x0000000000000000000000000000000000000000 \ + --network sepolia +``` + +Verify hooks are detached: + +```bash filename="Terminal" +npx hardhat getAdvancedPoolHooks --network sepolia +``` + +### 10. Send a transfer again (expected success) + +With hooks detached, the token pool returns to its default behavior and your transfer should succeed again. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourDeployerAddress \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Expected output (example): + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +🚀 Sending message to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +### 11. Manage the allowlist + +Use the `updateAllowList` task to add or remove addresses from the allowlist. Only the hooks contract **owner** can modify the allowlist. + +[updateAllowList.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/allowlist/updateAllowList.ts) + +View the allowlist update task on GitHub. + +| Flag | Required | Description | +| ------------- | -------- | ------------------------------------------------------ | +| `--poolhooks` | Yes (v2) | Address of the `AdvancedPoolHooks` contract | +| `--add` | No | Comma-separated addresses to add to the allowlist | +| `--remove` | No | Comma-separated addresses to remove from the allowlist | + +**Add an address:** + +```bash filename="Terminal" +npx hardhat updateAllowList \ + --poolhooks $POOL_HOOKS \ + --add "0xYourDeployerAddress" \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +📝 Update AllowList +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: Update allowlist +======================================== + +[Step 1] Updating allowlist on Ethereum Sepolia +⏳ Tx: 0xccddeeff00112233445566778899aabbccddeeff00112233445566778899aabb +✅ AllowList updated successfully! + +======================================== +✅ Allowlist updated on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +======================================== +``` + +**Remove an address:** + +```bash filename="Terminal" +npx hardhat updateAllowList \ + --poolhooks $POOL_HOOKS \ + --remove "0xYourDeployerAddress" \ + --network sepolia +``` + +**Verify the updated allowlist** by re-running the `getAllowList` or `isAllowListed` tasks from the previous step. + +--- + +# Enforce ACE policies on CCIP token transfers using Foundry +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry +Last Updated: 2026-09-26 + +## Guide Versions + +- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry) + +- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-hardhat) + +[`AdvancedPoolHooks`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) can forward each transfer to a [Chainlink ACE](/ace) Policy Engine for evaluation before the source token pool locks or burns tokens (`preflightCheck`) and before the destination token pool releases or mints tokens (`postflightCheck`). If a policy rejects the call, the hook reverts and the transfer does not proceed. + +This tutorial covers the **ACE policy enforcement** use case on a working CCT lane. For the sender allowlist use case instead, see [Configure a sender allowlist with AdvancedPoolHooks](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry). For how the hook, engine, extractor, and policies fit together, read the [AdvancedPoolHooks concept page](/ccip/concepts/cross-chain-token/advanced-pool-hooks). + +In this tutorial you will: + +1. Reuse an existing working CCT lane between Ethereum Sepolia and Arbitrum Sepolia. +2. Deploy `AdvancedPoolHooks` on both chains with their Policy Engine addresses set, and attach them to both token pools. +3. Point the hooks at their Policy Engines with `setPolicyEngine` when the engine was not set at deployment or needs to change. +4. Complete the ACE Platform setup: target detection, contract-type assignment, policy creation, and extractor mappings. +5. Demonstrate a source `preflightCheck` rejection using the `from` parameter. +6. Demonstrate a successful unrestricted transfer. +7. Demonstrate a destination `postflightCheck` rejection using the `to` parameter. +8. Update the destination policy and manually execute the failed message. + +> **NOTE: What this tutorial does not repeat** +> +> - Token and pool deployment, admin registration, and lane configuration: complete a registration tutorial first (see +> [Confirm prerequisites](#confirm-prerequisites-addresses-and-permissions)). +> - The ACE Platform workflow (engine creation, target detection, contract-type assignment, policy instances, +> protections, extractor mappings): follow +> [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools) for each step. + +## Before You Begin + +> **CAUTION: ACE Beta access required** +> +> ACE is currently available through the Beta program. [Contact us](https://chain.link/contact) to request +> access or schedule a demo. After ACE is enabled for your organization, complete the [ACE account +> setup](/ace/getting-started/account-setup) before continuing. + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If `npm` is unavailable, reinstall or update Node.js. + +2. **Install Foundry**: + If you haven't already, follow the instructions in the [Foundry documentation](https://book.getfoundry.sh/getting-started/installation) to install Foundry. \ + Verify the installation by running the following command: + +```bash filename="Terminal" +forge --version +``` + +3. Install/Update Chainlink `ccip-cli`: + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +ccip-cli --version +``` + +4. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-foundry) + +Clone the CCIP 2.0 template for a smoother setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-foundry.git +cd docs-cct-foundry +``` + +5. Create an encrypted Foundry keystore, if you haven't already: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +> **NOTE: Note** +> +> 1. For more information on how to create an encrypted Foundry keystore, refer to the +> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 2. `cast wallet import` will throw an error if a keystore with the same name already exists. +> 3. Use `cast wallet list` to check previously created keystores. + +6. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `cast wallet import`) +KEYSTORE_NAME= + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +This tutorial uses a single wallet for every deployment, configuration update, and test transfer: the wallet stored in `KEYSTORE_NAME`. + +7. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +8. Build the project: + +```bash filename="Terminal" +npm install && forge build +``` + +## Tutorial + +### 1. Confirm prerequisites, addresses, and permissions + +This tutorial assumes you have already deployed tokens and token pools and configured a working lane between Ethereum Sepolia and Arbitrum Sepolia. If not, complete one of the registration tutorials first: + +- [Burn & Mint (Foundry)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) +- [Lock & Mint (Foundry)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) + +Export the addresses for both chains: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN=0x... +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x... +export ETHEREUM_SEPOLIA_ROUTER=0x... +export UNRESTRICTED_RECEIVER=0x... +export DENIED_RECEIVER=0x... +``` + +Use two different recipient addresses. The destination policy will deny only `DENIED_RECEIVER`; you will use +`UNRESTRICTED_RECEIVER` to confirm that transfers to other recipients still succeed. + +> **NOTE: ACE access required** +> +> If ACE is not yet enabled for your Chainlink Platform organization, +> [request access](https://chain.link/contact) and complete the [ACE account +> setup](/ace/getting-started/account-setup) before creating a Policy Engine. + +You also need a Policy Engine on each chain. [Create a policy engine](/ace/guides/policy-manager/manage-engines#create-a-policy-engine), include Ethereum Sepolia and Arbitrum Sepolia, wait until the engine is **Active**, and copy its onchain address for each chain. + +> **CAUTION: Funding requirements** +> +> - **Sepolia ETH**: the wallet `KEYSTORE_NAME` needs ETH on Sepolia to pay gas for hook +> deployment and configuration. +> - **Arbitrum Sepolia ETH**: the same wallet needs ETH on Arbitrum Sepolia for the destination hook +> deployment, configuration, and the manual execution transaction. +> - **Your token on Sepolia**: the same wallet needs some `ETHEREUM_SEPOLIA_TOKEN` to send the +> test transfers. + +> **NOTE: One-wallet assumption** +> +> This tutorial assumes the wallet stored in `KEYSTORE_NAME` is the owner of both token pools and will be the +> owner of both deployed `AdvancedPoolHooks` contracts. These roles can be distributed across multiple +> wallets if needed. + +> **NOTE: The engine must be on the same chain as the hook** +> +> ACE evaluates policies onchain. The source hook must point at the engine deployed on Ethereum Sepolia, and the +> destination hook at the engine deployed on Arbitrum Sepolia. A single Policy Engine can cover both chains if you +> included both when creating it; in that case use the engine's per-chain onchain addresses. + +### 2. Deploy hooks on both chains and attach them + +Deploy an `AdvancedPoolHooks` contract on each chain, then attach it to that chain's token pool. The allowlist stays empty in this tutorial: policy enforcement comes from ACE, not the allowlist. Pass the engine address at deploy time so the hooks connect to it in the same transaction: the constructor calls `attach()` on the engine, which starts ACE target detection. + +[DeployAdvancedPoolHooks.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/DeployAdvancedPoolHooks.s.sol) + +View the hooks deployment script on GitHub. + +**1. Deploy hooks on Ethereum Sepolia (source chain):** + +```bash filename="Terminal" +AUTHORIZED_CALLERS=$ETHEREUM_SEPOLIA_TOKEN_POOL \ +POLICY_ENGINE=0xYourSepoliaEngineAddress \ +forge script \ +script/configure/allowlist/DeployAdvancedPoolHooks.s.sol \ +--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ +--account $KEYSTORE_NAME \ +--broadcast \ +--verify +``` + +Export the source hooks address: + +```bash filename="Terminal" +export SOURCE_POOL_HOOKS=0xSourceHooksAddress +``` + +**2. Deploy hooks on Arbitrum Sepolia (destination chain):** + +```bash filename="Terminal" +AUTHORIZED_CALLERS=$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL \ +POLICY_ENGINE=0xYourArbitrumSepoliaEngineAddress \ +forge script \ +script/configure/allowlist/DeployAdvancedPoolHooks.s.sol \ +--rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ +--account $KEYSTORE_NAME \ +--broadcast \ +--verify +``` + +Export the destination hooks address: + +```bash filename="Terminal" +export DEST_POOL_HOOKS=0xDestinationHooksAddress +``` + +> **NOTE: Deployment address recovery** +> +> - A broadcast deploy records the hooks address in the project store (`project/.json`, +> under `addresses.active.poolHooks` and the `deployments` entry), so later scripts resolve it +> automatically with no manual export. +> - If you lose the session export, read the address from the project store, or from the append-only history ledger at +> `history/advanced-pool-hooks//` (the `POOL_HOOKS` key in the JSON file). +> - For a pool or token deployed outside this repository, run `make adopt-token` to record it in the same +> registry. Environment variable exports still work as temporary overrides, but they are not saved. + +**3. Attach the source hooks to the source pool:** + +```bash filename="Terminal" +NEW_HOOK=$SOURCE_POOL_HOOKS \ +forge script \ +script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol \ +--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ +--account $KEYSTORE_NAME \ +--broadcast +``` + +**4. Attach the destination hooks to the destination pool:** + +```bash filename="Terminal" +NEW_HOOK=$DEST_POOL_HOOKS \ +forge script \ +script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol \ +--rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ +--account $KEYSTORE_NAME \ +--broadcast +``` + +> **NOTE: Why AUTHORIZED_CALLERS at deploy time** +> +> Passing the pool as an authorized caller at deploy time authorizes it in the same transaction the hooks are created +> in, so the first hook call cannot revert for a missing caller. If you deployed without it, add the pool with +> `UpdateAuthorizedCallers.s.sol` (`POOL_HOOKS= ADD_ADDRESSES=`) before +> attaching. + +### 3. Set or update the policy engine + +If you passed `POLICY_ENGINE` when deploying the hooks, they are already connected to the engine and ACE target detection has started. Skip the commands below and verify the connections with the read script. To connect hooks deployed without an engine, or to point them at a different engine, use the `SetPolicyEngine.s.sol` script. + +[SetPolicyEngine.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/policy-engine/SetPolicyEngine.s.sol) + +View the policy engine script on GitHub. + +**1. Set the engine on the source hooks (Ethereum Sepolia):** + +```bash filename="Terminal" +POOL_HOOKS=$SOURCE_POOL_HOOKS \ +POLICY_ENGINE=0xYourSepoliaEngineAddress \ +forge script \ +script/configure/policy-engine/SetPolicyEngine.s.sol \ +--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ +--account $KEYSTORE_NAME \ +--broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔗 Set Policy Engine +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: Set policy engine +======================================== + +Current Policy Engine: 0x0000000000000000000000000000000000000000 +New Policy Engine: 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9 + +⏳ SENDING (unconfirmed): point the pool hooks at policy engine 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9 +======================================== +``` + +**2. Set the engine on the destination hooks (Arbitrum Sepolia):** + +```bash filename="Terminal" +POOL_HOOKS=$DEST_POOL_HOOKS \ +POLICY_ENGINE=0xYourArbitrumSepoliaEngineAddress \ +forge script \ +script/configure/policy-engine/SetPolicyEngine.s.sol \ +--rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ +--account $KEYSTORE_NAME \ +--broadcast +``` + +**3. Verify both connections with the read script:** + +```bash filename="Terminal" +POOL_HOOKS=$SOURCE_POOL_HOOKS \ +forge script \ +script/configure/policy-engine/GetPolicyEngine.s.sol \ +--rpc-url $ETHEREUM_SEPOLIA_RPC_URL + +POOL_HOOKS=$DEST_POOL_HOOKS \ +forge script \ +script/configure/policy-engine/GetPolicyEngine.s.sol \ +--rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Expected output for each chain: + +```bash filename="Terminal" +======================================== +🔎 Get Policy Engine +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: View policy engine +======================================== + +✅ Policy Engine: + 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9 +======================================== +``` + +> **NOTE: Replacing or disconnecting an engine** +> +> - `setPolicyEngine` detaches the old engine first and reverts `PolicyEngineDetachReverted` if +> the old engine's `detach()` reverts. The on-chain recovery path is +> [`setPolicyEngineAllowFailedDetach`](/ccip/evm/api-reference/v2.0.0/advanced-pool-hooks#policy-engine) on the hook +> itself. +> - `POLICY_ENGINE=0x0000000000000000000000000000000000000000` disconnects the engine and stops policy +> checks. + +### 4. Complete the ACE Platform setup + +The hook is now attached to the engine onchain. Complete the Platform-side configuration once per engine by following [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools). This tutorial does not repeat those steps; it links the exact section for each task: + +1. **Verify the detected targets**: each hooks address appears as a detected target under its engine. Assign the built-in `CCIP-AdvancedPoolHooks` contract type to each target. See [Verify the detected target](/ace/guides/policy-manager/ccip-token-pools#verify-the-detected-target). +2. **Create a policy instance on each chain**: use the `reject` policy type. On the source engine, configure the instance's denylist with the address you will send from. On the destination engine, configure the instance's denylist with `$DENIED_RECEIVER`. See [Create a policy instance](/ace/guides/policy-manager/ccip-token-pools#create-a-policy-instance). +3. **Protect the hook functions**: on the source target, protect `preflightCheck` and map the policy parameter `Account (address)` to the extractor output `from`. On the destination target, protect `postflightCheck` and map `Account (address)` to `to`. Read [Restrict source senders](/ace/guides/policy-manager/ccip-token-pools#restrict-source-senders) first: the payload sender is the token pool, not the user, so the `from` output is what evaluates the transfer sender. See [Protect a hook function](/ace/guides/policy-manager/ccip-token-pools#protect-a-hook-function). + +> **NOTE: Confirm the chain on every protection** +> +> The Platform creates a protection only on chains where the target and policy instance are both deployed. Confirm the +> selected chain matches the hook deployment: Ethereum Sepolia for the source protection, Arbitrum Sepolia for the +> destination protection. + +### 5. Demonstrate a preflight rejection (from) + +Send a transfer from the address on the source denylist. The source `preflightCheck` runs before the token pool locks or burns tokens, so the source transaction reverts and the transfer never starts. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver $UNRESTRICTED_RECEIVER \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Expected result: the send transaction reverts. The Policy Engine returns `PolicyRunRejected`; its reject reason identifies the underlying policy rejection, and the hook call propagates that revert. + +If this step does **not** fail as expected, check: + +- The source hooks point at the engine (`GetPolicyEngine.s.sol` shows the engine address). +- The source target uses the `CCIP-AdvancedPoolHooks` contract type. +- The protection maps `Account (address)` to the `from` output on `preflightCheck`. +- The denylisted address is the one sending the transfer. + +### 6. Send an unrestricted transfer (expected success) + +Remove the sending address from the source policy's denylist (see [Update policy configuration](/ace/guides/policy-manager/manage-policies#update-policy-configuration)). Wait for the configuration transaction to confirm on Ethereum Sepolia, then send the same transfer again. With no policy rejecting it, the transfer executes end to end. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver $UNRESTRICTED_RECEIVER \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Expected output (example): + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +This confirms the lane and both hooks work when no policy rejects the transfer. + +### 7. Demonstrate a postflight rejection (to) + +Send a transfer whose **recipient** is on the destination denylist. The source `preflightCheck` passes (the sender is not denied), the message travels to Arbitrum Sepolia, and the destination `postflightCheck` rejects before the token pool releases or mints tokens. The tokens remain unreleased, and the message stays unexecuted. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver $DENIED_RECEIVER \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +The send transaction succeeds on Sepolia. On Arbitrum Sepolia, execution fails: the CCIP Explorer shows the message in a failure state with the rejecting policy in the error data. + +> **NOTE: Why the source transaction succeeds** +> +> `postflightCheck` runs on the destination chain, after the source transaction has already committed. A +> destination rejection cannot roll the source back; it leaves the message unexecuted until the condition is resolved. +> This is the difference between the two hooks: preflight stops the transfer at its source, postflight enforces the +> destination's own policy. + +### 8. Update the destination policy and manually execute + +Resolve the rejection condition, then complete the message through manual execution. + +**1. Remove `$DENIED_RECEIVER` from the destination policy's denylist** (see [Update policy configuration](/ace/guides/policy-manager/manage-policies#update-policy-configuration)). Wait for the configuration transaction to confirm on Arbitrum Sepolia before continuing. The next evaluation of `postflightCheck` passes. + +**2. Confirm the message is ready for manual execution:** + +```bash filename="Terminal" +ccip-cli show \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Use the bare `0x...` message ID (or the source transaction hash) as the argument, not the full CCIP Explorer URL. The status shows the failure state and `readyForManualExecution`. + +**3. Manually execute the message on Arbitrum Sepolia:** + +```bash filename="Terminal" +ccip-cli manual-exec \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Manual execution is a destination-chain transaction: the wallet needs Arbitrum Sepolia ETH for gas. After the transaction confirms, the recipient receives the tokens and the message status becomes **Success**. + +For the full recovery flow, including gas overrides and diagnosing other failure causes, read [CCIP Manual Execution](/ccip/concepts/manual-execution). + +--- + +# Enforce ACE policies on CCIP token transfers using Hardhat +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-hardhat +Last Updated: 2026-09-26 + +## Guide Versions + +- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry) + +- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-hardhat) + +[`AdvancedPoolHooks`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) can forward each transfer to a [Chainlink ACE](/ace) Policy Engine for evaluation before the source token pool locks or burns tokens (`preflightCheck`) and before the destination token pool releases or mints tokens (`postflightCheck`). If a policy rejects the call, the hook reverts and the transfer does not proceed. + +This tutorial covers the **ACE policy enforcement** use case on a working CCT lane. For the sender allowlist use case instead, see [Configure a sender allowlist with AdvancedPoolHooks](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat). For how the hook, engine, extractor, and policies fit together, read the [AdvancedPoolHooks concept page](/ccip/concepts/cross-chain-token/advanced-pool-hooks). + +In this tutorial you will: + +1. Reuse an existing working CCT lane between Ethereum Sepolia and Arbitrum Sepolia. +2. Deploy `AdvancedPoolHooks` on both chains with their Policy Engine addresses set, and attach them to both token pools. +3. Point the hooks at their Policy Engines with `setPolicyEngine` when the engine was not set at deployment or needs to change. +4. Complete the ACE Platform setup: target detection, contract-type assignment, policy creation, and extractor mappings. +5. Demonstrate a source `preflightCheck` rejection using the `from` parameter. +6. Demonstrate a successful unrestricted transfer. +7. Demonstrate a destination `postflightCheck` rejection using the `to` parameter. +8. Update the destination policy and manually execute the failed message. + +> **NOTE: What this tutorial does not repeat** +> +> - Token and pool deployment, admin registration, and lane configuration: complete a registration tutorial first (see +> [Confirm prerequisites](#confirm-prerequisites-addresses-and-permissions)). +> - The ACE Platform workflow (engine creation, target detection, contract-type assignment, policy instances, +> protections, extractor mappings): follow +> [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools) for each step. + +## Before You Begin + +> **CAUTION: ACE Beta access required** +> +> ACE is currently available through the Beta program. [Contact us](https://chain.link/contact) to request +> access or schedule a demo. After ACE is enabled for your organization, complete the [ACE account +> setup](/ace/getting-started/account-setup) before continuing. + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If `npm` is unavailable, reinstall or update Node.js. + +2. Install/Update Chainlink `ccip-cli`: + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +ccip-cli --version +``` + +3. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-hardhat) + +Clone the CCIP 2.0 Hardhat template for a smoother setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-hardhat.git +cd docs-cct-hardhat +``` + +4. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `npx hardhat keystore set`) +KEYSTORE_NAME= + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment tasks) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +This tutorial uses a single wallet for every deployment, configuration update, and test transfer: the wallet stored in `KEYSTORE_NAME`. + +5. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +6. Build the project: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +7. Create an encrypted Hardhat keystore, if you haven't already: + +```bash filename="Terminal" +npx hardhat keystore set PRIVATE_KEY +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +> **NOTE: Note** +> +> 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it +> asks you to enter your private key as hidden text. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in +> `.env`. +> 3. Use `npx hardhat keystore list` to check previously created keystores. + +## Tutorial + +### 1. Confirm prerequisites, addresses, and permissions + +This tutorial assumes you have already deployed tokens and token pools and configured a working lane between Ethereum Sepolia and Arbitrum Sepolia. If not, complete one of the registration tutorials first: + +- [Burn & Mint (Hardhat)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) +- [Lock & Mint (Hardhat)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) + +Export the addresses for both chains: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN=0x... +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +export ARBITRUM_SEPOLIA_TOKEN=0x... +export ARBITRUM_SEPOLIA_TOKEN_POOL=0x... +export ETHEREUM_SEPOLIA_ROUTER=0x... +export UNRESTRICTED_RECEIVER=0x... +export DENIED_RECEIVER=0x... +``` + +Use two different recipient addresses. The destination policy will deny only `DENIED_RECEIVER`; you will use +`UNRESTRICTED_RECEIVER` to confirm that transfers to other recipients still succeed. + +> **NOTE: ACE access required** +> +> If ACE is not yet enabled for your Chainlink Platform organization, +> [request access](https://chain.link/contact) and complete the [ACE account +> setup](/ace/getting-started/account-setup) before creating a Policy Engine. + +You also need a Policy Engine on each chain. [Create a policy engine](/ace/guides/policy-manager/manage-engines#create-a-policy-engine), include Ethereum Sepolia and Arbitrum Sepolia, wait until the engine is **Active**, and copy its onchain address for each chain. + +> **CAUTION: Funding requirements** +> +> - **Sepolia ETH**: the wallet `KEYSTORE_NAME` needs ETH on Sepolia to pay gas for hook +> deployment and configuration. +> - **Arbitrum Sepolia ETH**: the same wallet needs ETH on Arbitrum Sepolia for the destination hook +> deployment, configuration, and the manual execution transaction. +> - **Your token on Sepolia**: the same wallet needs some `ETHEREUM_SEPOLIA_TOKEN` to send the +> test transfers. + +> **NOTE: One-wallet assumption** +> +> This tutorial assumes the wallet stored in `KEYSTORE_NAME` is the owner of both token pools and will be the +> owner of both deployed `AdvancedPoolHooks` contracts. These roles can be distributed across multiple +> wallets if needed. + +> **NOTE: The engine must be on the same chain as the hook** +> +> ACE evaluates policies onchain. The source hook must point at the engine deployed on Ethereum Sepolia, and the +> destination hook at the engine deployed on Arbitrum Sepolia. A single Policy Engine can cover both chains if you +> included both when creating it; in that case use the engine's per-chain onchain addresses. + +### 2. Deploy hooks on both chains and attach them + +Deploy an `AdvancedPoolHooks` contract on each chain, then attach it to that chain's token pool. The allowlist stays empty in this tutorial: policy enforcement comes from ACE, not the allowlist. Pass the engine address at deploy time so the hooks connect to it in the same transaction: the constructor calls `attach()` on the engine, which starts ACE target detection. + +[deployAdvancedPoolHooks.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/allowlist/deployAdvancedPoolHooks.ts) + +View the hooks deployment task on GitHub. + +**1. Deploy hooks on Ethereum Sepolia (source chain):** + +```bash filename="Terminal" +npx hardhat deployAdvancedPoolHooks \ + --authorizedcallers $ETHEREUM_SEPOLIA_TOKEN_POOL \ + --policyengine 0xYourSepoliaEngineAddress \ + --network sepolia +``` + +Export the source hooks address: + +```bash filename="Terminal" +export SOURCE_POOL_HOOKS=0xSourceHooksAddress +``` + +**2. Deploy hooks on Arbitrum Sepolia (destination chain):** + +```bash filename="Terminal" +npx hardhat deployAdvancedPoolHooks \ + --authorizedcallers $ARBITRUM_SEPOLIA_TOKEN_POOL \ + --policyengine 0xYourArbitrumSepoliaEngineAddress \ + --network arbitrumSepolia +``` + +Export the destination hooks address: + +```bash filename="Terminal" +export DEST_POOL_HOOKS=0xDestinationHooksAddress +``` + +> **NOTE: Deployment address recovery** +> +> Deployment addresses are saved to `deployments/advanced-pool-hooks/` per chain. If you lose the session +> export, retrieve the address from the JSON file under the `POOL_HOOKS` key. + +**3. Attach the source hooks to the source pool:** + +```bash filename="Terminal" +npx hardhat updateAdvancedPoolHooks \ + --newhook $SOURCE_POOL_HOOKS \ + --network sepolia +``` + +**4. Attach the destination hooks to the destination pool:** + +```bash filename="Terminal" +npx hardhat updateAdvancedPoolHooks \ + --newhook $DEST_POOL_HOOKS \ + --network arbitrumSepolia +``` + +> **NOTE: Why --authorizedcallers at deploy time** +> +> Passing the pool as an authorized caller at deploy time authorizes it in the same transaction the hooks are created +> in, so the first hook call cannot revert for a missing caller. If you deployed without it, add the pool with +> `updateAuthorizedCallers` (`--poolhooks --add `) before attaching. + +### 3. Set or update the policy engine + +If you passed `--policyengine` when deploying the hooks, they are already connected to the engine and ACE target detection has started. Skip the commands below and verify the connections with the read task. To connect hooks deployed without an engine, or to point them at a different engine, use the `setPolicyEngine` task. + +[setPolicyEngine.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/policy-engine/setPolicyEngine.ts) + +View the policy engine task on GitHub. + +**1. Set the engine on the source hooks (Ethereum Sepolia):** + +```bash filename="Terminal" +npx hardhat setPolicyEngine \ + --poolhooks $SOURCE_POOL_HOOKS \ + --policyengine 0xYourSepoliaEngineAddress \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔗 Set Policy Engine +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: Set policy engine +======================================== + +Current Policy Engine: 0x0000000000000000000000000000000000000000 +New Policy Engine: 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9 + +[Step 1] Setting policy engine on Ethereum Sepolia +⏳ Tx: 0xaaabbbcccdddeeefff000111222333444555666777888999aaabbbcccdddeeff +✅ Policy engine set successfully! +======================================== +``` + +**2. Set the engine on the destination hooks (Arbitrum Sepolia):** + +```bash filename="Terminal" +npx hardhat setPolicyEngine \ + --poolhooks $DEST_POOL_HOOKS \ + --policyengine 0xYourArbitrumSepoliaEngineAddress \ + --network arbitrumSepolia +``` + +**3. Verify both connections with the read task:** + +```bash filename="Terminal" +npx hardhat getPolicyEngine \ + --poolhooks $SOURCE_POOL_HOOKS \ + --network sepolia + +npx hardhat getPolicyEngine \ + --poolhooks $DEST_POOL_HOOKS \ + --network arbitrumSepolia +``` + +Expected output for each chain: + +```bash filename="Terminal" +======================================== +🔎 Get Policy Engine +======================================== +Chain: Ethereum Sepolia +Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE +Action: View policy engine +======================================== + +✅ Policy Engine: + 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9 +======================================== +``` + +> **NOTE: Replacing or disconnecting an engine** +> +> - `setPolicyEngine` detaches the old engine first and reverts `PolicyEngineDetachReverted` if +> the old engine's `detach()` reverts. The on-chain recovery path is +> [`setPolicyEngineAllowFailedDetach`](/ccip/evm/api-reference/v2.0.0/advanced-pool-hooks#policy-engine) on the hook +> itself. +> - `--policyengine 0x0000000000000000000000000000000000000000` disconnects the engine and stops policy +> checks. + +### 4. Complete the ACE Platform setup + +The hook is now attached to the engine onchain. Complete the Platform-side configuration once per engine by following [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools). This tutorial does not repeat those steps; it links the exact section for each task: + +1. **Verify the detected targets**: each hooks address appears as a detected target under its engine. Assign the built-in `CCIP-AdvancedPoolHooks` contract type to each target. See [Verify the detected target](/ace/guides/policy-manager/ccip-token-pools#verify-the-detected-target). +2. **Create a policy instance on each chain**: use the `reject` policy type. On the source engine, configure the instance's denylist with the address you will send from. On the destination engine, configure the instance's denylist with `$DENIED_RECEIVER`. See [Create a policy instance](/ace/guides/policy-manager/ccip-token-pools#create-a-policy-instance). +3. **Protect the hook functions**: on the source target, protect `preflightCheck` and map the policy parameter `Account (address)` to the extractor output `from`. On the destination target, protect `postflightCheck` and map `Account (address)` to `to`. Read [Restrict source senders](/ace/guides/policy-manager/ccip-token-pools#restrict-source-senders) first: the payload sender is the token pool, not the user, so the `from` output is what evaluates the transfer sender. See [Protect a hook function](/ace/guides/policy-manager/ccip-token-pools#protect-a-hook-function). + +> **NOTE: Confirm the chain on every protection** +> +> The Platform creates a protection only on chains where the target and policy instance are both deployed. Confirm the +> selected chain matches the hook deployment: Ethereum Sepolia for the source protection, Arbitrum Sepolia for the +> destination protection. + +### 5. Demonstrate a preflight rejection (from) + +Send a transfer from the address on the source denylist. The source `preflightCheck` runs before the token pool locks or burns tokens, so the source transaction reverts and the transfer never starts. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver $UNRESTRICTED_RECEIVER \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Expected result: the send transaction reverts. The Policy Engine returns `PolicyRunRejected`; its reject reason identifies the underlying policy rejection, and the hook call propagates that revert. + +If this step does **not** fail as expected, check: + +- The source hooks point at the engine (`getPolicyEngine` shows the engine address). +- The source target uses the `CCIP-AdvancedPoolHooks` contract type. +- The protection maps `Account (address)` to the `from` output on `preflightCheck`. +- The denylisted address is the one sending the transfer. + +### 6. Send an unrestricted transfer (expected success) + +Remove the sending address from the source policy's denylist (see [Update policy configuration](/ace/guides/policy-manager/manage-policies#update-policy-configuration)). Wait for the configuration transaction to confirm on Ethereum Sepolia, then send the same transfer again. With no policy rejecting it, the transfer executes end to end. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver $UNRESTRICTED_RECEIVER \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Expected output (example): + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +This confirms the lane and both hooks work when no policy rejects the transfer. + +### 7. Demonstrate a postflight rejection (to) + +Send a transfer whose **recipient** is on the destination denylist. The source `preflightCheck` passes (the sender is not denied), the message travels to Arbitrum Sepolia, and the destination `postflightCheck` rejects before the token pool releases or mints tokens. The tokens remain unreleased, and the message stays unexecuted. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver $DENIED_RECEIVER \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +The send transaction succeeds on Sepolia. On Arbitrum Sepolia, execution fails: the CCIP Explorer shows the message in a failure state with the rejecting policy in the error data. + +> **NOTE: Why the source transaction succeeds** +> +> `postflightCheck` runs on the destination chain, after the source transaction has already committed. A +> destination rejection cannot roll the source back; it leaves the message unexecuted until the condition is resolved. +> This is the difference between the two hooks: preflight stops the transfer at its source, postflight enforces the +> destination's own policy. + +### 8. Update the destination policy and manually execute + +Resolve the rejection condition, then complete the message through manual execution. + +**1. Remove `$DENIED_RECEIVER` from the destination policy's denylist** (see [Update policy configuration](/ace/guides/policy-manager/manage-policies#update-policy-configuration)). Wait for the configuration transaction to confirm on Arbitrum Sepolia before continuing. The next evaluation of `postflightCheck` passes. + +**2. Confirm the message is ready for manual execution:** + +```bash filename="Terminal" +ccip-cli show \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Use the bare `0x...` message ID (or the source transaction hash) as the argument, not the full CCIP Explorer URL. The status shows the failure state and `readyForManualExecution`. + +**3. Manually execute the message on Arbitrum Sepolia:** + +```bash filename="Terminal" +ccip-cli manual-exec \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Manual execution is a destination-chain transaction: the wallet needs Arbitrum Sepolia ETH for gas. After the transaction confirms, the recipient receives the tokens and the message status becomes **Success**. + +For the full recovery flow, including gas overrides and diagnosing other failure causes, read [CCIP Manual Execution](/ccip/concepts/manual-execution). + +--- + +# Cross-Chain Token (CCT) Tutorials +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens +Last Updated: 2025-05-19 + +> **NOTE: Prerequisites** +> +> Familiarize yourself with the [CCT standard](/ccip/concepts/cross-chain-token/overview) and [CCIP +> architecture](/ccip/concepts/architecture/overview) before proceeding with these tutorials. + +Before diving into the [tutorials](#tutorials), it's important first to understand the overall procedure for enabling your tokens in CCIP. This procedure involves deploying tokens and token pools, registering administrative roles, and configuring token pools to enable secure token transfers using CCIP. The diagram below outlines the entire process: + +![Process for enabling a token in CCIP.](/images/ccip/CCIP_enabled_tokens_flowchart.jpg) + +### Understanding the Procedure + +The steps in the diagram highlight the flow of actions needed to enable a token for cross-chain transfers. These steps will be the foundation of the tutorials. Whether you're working with an Externally Owned Account (EOA) or a **Smart Account** (such as one using a multisig scheme), the overall logic remains the same. You'll follow the same process to enable cross-chain token transfers, configure pools, and register administrative roles. + +In the following tutorials, we will walk through each step of the process to give you hands-on experience, from deploying your token to registering and configuring token pools. The process will apply equally whether you use an EOA or a Smart Account (such as with multisig transactions), ensuring flexibility across different account types. + +### Key Steps to Keep in Mind: + +1. **Token Deployment**: If the token is not yet deployed, you'll deploy an ERC20-compatible token. +2. **Admin Registration**: The token administrator must be registered in the [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol) via self-service. +3. **Pool Deployment and Configuration**: Token pools are deployed, linked to tokens, and configured to manage cross-chain token transfers. + +The tutorials will implement the logic of this process, which involves deploying and configuring token pools and registering administrative roles, step-by-step. + +## Tutorials + +- [Register from an EOA (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry): Register a cross-chain token with the Burn & Mint mechanism using an EOA. Hardhat and Foundry versions available. +- [Register from an EOA (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry): Register a cross-chain token with the Lock & Mint mechanism using an EOA. Hardhat and Foundry versions available. +- [Set Token Pool Rate Limits](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry): Set rate limits for token pools to control cross-chain token transfer flow. Hardhat and Foundry versions available. +- [Set Token Transfer Fee Config](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry): Configure token transfer fee parameters directly on your v2 token pool, overriding FeeQuoter defaults. Hardhat and Foundry versions available. +- [Configure a Sender Allowlist with AdvancedPoolHooks](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry): Deploy and attach AdvancedPoolHooks to your v2 token pool to restrict outbound transfers to approved sender addresses. Hardhat and Foundry versions available. +- [Enforce ACE Policies on CCIP Token Transfers](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry): Connect source and destination AdvancedPoolHooks to Chainlink ACE Policy Engines and exercise preflight and postflight rejections with live transfers. Hardhat and Foundry versions available. + +--- + +# Migrate your CCT token pools from CCIP v1 to v2 (Burn & Mint) using Foundry +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry +Last Updated: 2026-06-11 + +## Guide Versions + +- [Foundry (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry) + +- [Hardhat (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat) + +This migration guide helps you upgrade a deployed **Burn & Mint** Cross-Chain Token (CCT) setup from **CCIP v1 (1.5.x / 1.6.x)** to **CCIP 2.0**, while keeping your existing token addresses and minimizing disruption. + +This guide uses Foundry scripts inside our [docs-cct-foundry](https://github.com/smartcontractkit/docs-cct-foundry) repository. + +> **NOTE: Finality and v2 features are optional** +> +> 1. **Default finality:** Transfers use the chain’s native finality unless you explicitly opt into +> block-depth faster than finality. +> 2. **Opt-in changes:** Pool-level fees, faster than finality, and hooks only apply if you configure +> them. This guide focuses on a safe pool migration first. + +In this guide you will: + +1. Confirm prerequisites and record your existing CCIP v1 deployment details. +2. Deploy new CCIP 2.0 `Burn Mint` token pools on each chain. +3. Configure the v2 pools to accept messages from **both** the old v1 pools and the new v2 pools. +4. Cut over CCIP routing by calling [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol).setPool on each chain. +5. Validate transfers end-to-end, then optionally clean up legacy configuration. + +## Before You Begin + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their + [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can’t run `npm`, reinstall/update Node.js from the official installer. + +2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/). You can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli). + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +``` + +Verify the installation by running the following command: + +```bash filename="Terminal" +ccip-cli --version +``` + +3. **Install Foundry**: + If you haven't already, follow the instructions in the [Foundry documentation](https://book.getfoundry.sh/getting-started/installation) to install Foundry. \ + Verify the installation by running the following command: + +```bash filename="Terminal" +forge --version +``` + +4. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 Foundry template](https://github.com/smartcontractkit/docs-cct-foundry) + +Clone the CCIP 2.0 Foundry template for a smoother CCT setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-foundry.git +cd docs-cct-foundry +``` + +5. Create an encrypted Foundry keystore, if you haven't already: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +6. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `cast wallet import`) +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (lane used in this guide) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/HelperConfig.s.sol) + +View the complete list of supported chains in the `HelperConfig.s.sol` file. + +7. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +8. Install dependencies and build the project: + +```bash filename="Terminal" +npm install && forge build +``` + +### 2. What this guide assumes + +- **EVM-to-EVM only**: this page is strictly for **Ethereum Sepolia ↔ Arbitrum Sepolia**. - **Burn & Mint pools + only**: standard + [`BurnMintTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol) + migrations (no custom pools). - You control the `TokenAdminRegistry` **administrator** role for the token **on both + chains**. - You can grant the required **mint/burn** permissions to the new v2 pools on each chain. - You can execute + **pool-owner** actions on both chains (for example, `applyChainUpdates`, `removeRemotePool`). - You are migrating an + already-live CCIP v1 setup (1.5.x or 1.6.x), not doing a new registration. + +## Migration Guide + +> **TIP: Set these variables once for cleaner copy/paste** +> +> This guide uses explicit addresses so you don’t need local `script/deployments/` files. Start by exporting +> your token addresses, then we’ll export the v1/v2 pool addresses as we discover them. +> +> ```bash filename="Terminal" +> export ETHEREUM_SEPOLIA_TOKEN=0x... +> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... +> ``` + +### 1. Step 1: Confirm the live CCIP v1 pool (and record v1 addresses) + +This step answers two questions, on **both chains**: + +1. **Which pool is currently live for your token?** (read from `TokenAdminRegistry.getTokenConfig`) +2. **Is that pool a v1.5/v1.6 pool?** (read `typeAndVersion()` from the pool) + +[GetTokenConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/GetTokenConfig.s.sol) + +View the token config query script on GitHub. + +1. Read the token config on **Ethereum Sepolia**: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_SEPOLIA_TOKEN forge script \ + script/setup/GetTokenConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + administrator: 0xYourAdminAddress + pendingAdministrator: 0x0000000000000000000000000000000000000000 + tokenPool: 0xOldSepoliaV1PoolAddress +``` + +Export the v1 pool address: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_V1_POOL=0xOldSepoliaV1PoolAddress +``` + +2. Repeat on **Arbitrum Sepolia**: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN forge script \ + script/setup/GetTokenConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + administrator: 0xYourAdminAddress + pendingAdministrator: 0x0000000000000000000000000000000000000000 + tokenPool: 0xOldArbitrumV1PoolAddress +``` + +Export the v1 pool address: + +```bash filename="Terminal" +export ARBITRUM_SEPOLIA_V1_POOL=0xOldArbitrumV1PoolAddress +``` + +[GetTypeAndVersion.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/GetTypeAndVersion.s.sol) + +View the type/version query script on GitHub. + +3. Confirm the v1 pool version on **Ethereum Sepolia**: + +```bash filename="Terminal" +ADDRESS=$ETHEREUM_SEPOLIA_V1_POOL forge script \ + script/setup/GetTypeAndVersion.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +typeAndVersion: BurnMintTokenPool 1.6.1 +``` + +4. Confirm the v1 pool version on **Arbitrum Sepolia**: + +```bash filename="Terminal" +ADDRESS=$ARBITRUM_SEPOLIA_V1_POOL forge script \ + script/setup/GetTypeAndVersion.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +typeAndVersion: BurnMintTokenPool 1.6.1 +``` + +> **NOTE: What to check before continuing** +> +> - **administrator** should match the account/multisig you control (this account must execute +> `setPool` during cutover). +> - **pendingAdministrator** should typically be `0x0`. If it’s set, resolve it before +> migrating to avoid confusion. +> - **typeAndVersion** should indicate a v1.5.x or v1.6.x `BurnMintTokenPool`. + +### 2. Step 2: Record your v1 lane configuration (remote pools + rate limits) + +Before deploying anything, snapshot your current v1 configuration so you can keep behavior consistent after migration. + +[GetSupportedChains.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/GetSupportedChains.s.sol) + +View the supported-chains query script on GitHub. + +1. List configured remote chains on the **Sepolia v1 pool**: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V1_POOL forge script \ + script/setup/GetSupportedChains.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Supported Remote Chains: 1 + + [0] Arbitrum Sepolia (3478487238524512106) + Remote Pools: 1 + [0] 0xOldArbitrumV1PoolAddress +``` + +2. List configured remote chains on the **Arbitrum v1 pool**: + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V1_POOL forge script \ + script/setup/GetSupportedChains.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Supported Remote Chains: 1 + + [0] Ethereum Sepolia (16015286601757825753) + Remote Pools: 1 + [0] 0xOldSepoliaV1PoolAddress +``` + +[GetCurrentRateLimits.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/GetCurrentRateLimits.s.sol) + +View the rate limiter query script on GitHub. + +3. Snapshot current v1 rate limits on the **Sepolia → Arbitrum** lane: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V1_POOL DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Pool Version: v1 + + Outbound Enabled: true + Outbound Capacity: 1000000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 1000000000000000000000 + Inbound Rate: 100000000000000000 +``` + +4. Snapshot current v1 rate limits on the **Arbitrum → Sepolia** lane: + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V1_POOL DEST_CHAIN=ETHEREUM_SEPOLIA forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Pool Version: v1 + + Outbound Enabled: true + Outbound Capacity: 1000000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 1000000000000000000000 + Inbound Rate: 100000000000000000 +``` + +Record these values since you'll reuse them when configuring the v2 pools in Step 5. + +### 3. Step 3: Deploy new v2 pools (per chain) + +Deploy a **new v2 `BurnMintTokenPool`** on each chain, pointing to your existing token address on that chain. + +[DeployBurnMintTokenPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployBurnMintTokenPool.s.sol) + +View the Burn & Mint pool deployment script on GitHub. + +1. Deploy the v2 pool on **Ethereum Sepolia**: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_SEPOLIA_TOKEN forge script \ + script/deploy/DeployBurnMintTokenPool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Pool deployed at: 0xNewSepoliaV2PoolAddress +✅ Roles granted successfully! +``` + +Export the new v2 pool address: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_V2_POOL=0xNewSepoliaV2PoolAddress +``` + +2. Deploy the v2 pool on **Arbitrum Sepolia**: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN forge script \ + script/deploy/DeployBurnMintTokenPool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Pool deployed at: 0xNewArbitrumV2PoolAddress +✅ Roles granted successfully! +``` + +Export the new v2 pool address: + +```bash filename="Terminal" +export ARBITRUM_SEPOLIA_V2_POOL=0xNewArbitrumV2PoolAddress +``` + +> **NOTE: Dual mint/burn window** +> +> Until post-migration cleanup, **both** v1 and v2 pools may hold mint/burn roles on your tokens. Your CCIP +> routing still points to the v1 pool until Step 7. + +### 4. Step 4 (Recommended): Pause outbound transfers on the v1 pools + +This recommended step throttles new outbound traffic on v1 while you cut over. Use **capacity = 2** and **rate = 1** +as the most restrictive (near-zero) configuration we document for v1 pools while keeping the lane configured. + +> **CAUTION: Run Step 2 first if you want the original rate limits** +> +> Step 4 overwrites the v1 pool’s **outbound** rate limits. If you want to carry your original v1 rate +> limits over to v2, make sure you record them in Step 2 before pausing outbound. + +[UpdateRateLimiters.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/UpdateRateLimiters.s.sol) + +View the rate limiter update script on GitHub. + +1. Pause outbound on the **Sepolia v1 pool** (lane to Arbitrum Sepolia): + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V1_POOL \ + DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + OUTBOUND_RATE_LIMIT_CAPACITY=2 \ + OUTBOUND_RATE_LIMIT_RATE=1 \ + forge script \ + script/configure/rate-limiter/UpdateRateLimiters.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +Pool Version: v1 (setChainRateLimiterConfig) + +New Configuration: + Outbound Enabled: true + Outbound Capacity: 2 + Outbound Rate: 1 +``` + +2. Pause outbound on the **Arbitrum v1 pool** (lane to Ethereum Sepolia): + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V1_POOL \ + DEST_CHAIN=ETHEREUM_SEPOLIA \ + OUTBOUND_RATE_LIMIT_CAPACITY=2 \ + OUTBOUND_RATE_LIMIT_RATE=1 \ + forge script \ + script/configure/rate-limiter/UpdateRateLimiters.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +### 5. Step 5: Configure the v2 pools (applyChainUpdates) + +On **each chain**, configure the v2 pool to recognize the lane to the remote chain. + +**Critical migration rule:** the `destPools` list must include **both** the old v1 remote pool address **and** the new +v2 remote pool address. This allows in-flight messages sent via v1 (before cutover) to still validate after you cut +over to v2. + +[ApplyChainUpdates.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/ApplyChainUpdates.s.sol) + +View the applyChainUpdates script (JSON mode recommended) on GitHub. + +[apply-chain-updates.json](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/input/apply-chain-updates.json) + +View the JSON input file used for applyChainUpdates on GitHub. + +1. Configure the **Sepolia v2 pool** (remote chain: Arbitrum Sepolia) + +Update `script/input/apply-chain-updates.json`: + +```json filename="script/input/apply-chain-updates.json" +{ + "_comment": "Configure Sepolia v2 pool → Arbitrum Sepolia. Replace 0x... placeholders with your real addresses.", + "sourcePool": "0xNewSepoliaV2PoolAddress", + "remoteChains": [ + { + "destChain": "ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1", + "destPools": ["0xOldArbitrumV1PoolAddress", "0xNewArbitrumV2PoolAddress"], + "destToken": "0x...", + "outboundRateLimit": { + "enabled": true, + "capacity": 1000000000000000000000, + "rate": 100000000000000000 + }, + "inboundRateLimit": { + "enabled": true, + "capacity": 1000000000000000000000, + "rate": 100000000000000000 + } + } + ] +} +``` + +Copy your v1 lane rate limits from Step 2 into `outboundRateLimit` and `inboundRateLimit`. If you don’t have existing +values, you can use the baseline values shown above. Setting `"enabled": false` disables rate limiting (unlimited +transfers). + +Run the script on **Ethereum Sepolia**: + +```bash filename="Terminal" +VIA_JSON_FILE=true forge script \ + script/setup/ApplyChainUpdates.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +Remote Chains: 1 + + [0] Arbitrum Sepolia + Pools: 2 + [0] 0xOldArbitrumV1PoolAddress + [1] 0xNewArbitrumV2PoolAddress + +✅ Chain updates applied successfully! +``` + +2. Configure the **Arbitrum v2 pool** (remote chain: Ethereum Sepolia) + +Update `script/input/apply-chain-updates.json`: + +```json filename="script/input/apply-chain-updates.json" +{ + "_comment": "Configure Arbitrum v2 pool → Ethereum Sepolia. Replace 0x... placeholders with your real addresses.", + "sourcePool": "0xNewArbitrumV2PoolAddress", + "remoteChains": [ + { + "destChain": "ETHEREUM_SEPOLIA", + "destPools": ["0xOldSepoliaV1PoolAddress", "0xNewSepoliaV2PoolAddress"], + "destToken": "0x...", + "outboundRateLimit": { + "enabled": true, + "capacity": 1000000000000000000000, + "rate": 100000000000000000 + }, + "inboundRateLimit": { + "enabled": true, + "capacity": 1000000000000000000000, + "rate": 100000000000000000 + } + } + ] +} +``` + +Run the script on **Arbitrum Sepolia**: + +```bash filename="Terminal" +VIA_JSON_FILE=true forge script \ + script/setup/ApplyChainUpdates.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +Remote Chains: 1 + + [0] Ethereum Sepolia + Pools: 2 + [0] 0xOldSepoliaV1PoolAddress + [1] 0xNewSepoliaV2PoolAddress + +✅ Chain updates applied successfully! +``` + +### 6. Step 6: Verify v2 configuration (before cutover) + +Verify that each v2 pool has **two** remote pools configured for the lane: `[remoteV1Pool, remoteV2Pool]`. + +[GetRemotePools.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/remote-pools/GetRemotePools.s.sol) + +View the remote pool query script on GitHub. + +1. Check remote pools on the **Sepolia v2 pool**: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 forge script \ + script/configure/remote-pools/GetRemotePools.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Chain Supported: Yes +Remote Pools: 2 + [0] 0xOldArbitrumV1PoolAddress + [1] 0xNewArbitrumV2PoolAddress +``` + +2. Check remote pools on the **Arbitrum v2 pool**: + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL DEST_CHAIN=ETHEREUM_SEPOLIA forge script \ + script/configure/remote-pools/GetRemotePools.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Chain Supported: Yes +Remote Pools: 2 + [0] 0xOldSepoliaV1PoolAddress + [1] 0xNewSepoliaV2PoolAddress +``` + +[GetCurrentRateLimits.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/GetCurrentRateLimits.s.sol) + +View the rate limiter query script on GitHub. + +3. Re-check rate limits on the v2 pools (optional but recommended) + +Sepolia v2 pool (lane to Arbitrum): + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Arbitrum v2 pool (lane to Sepolia): + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL DEST_CHAIN=ETHEREUM_SEPOLIA forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +### 7. Step 7: Cut over routing (TokenAdminRegistry.setPool) + +This is the migration cutover step. On each chain, update `TokenAdminRegistry` so your token routes through the new v2 +pool. + +> **NOTE: Who must run this step** +> +> The signer running `setPool` must be the token’s `TokenAdminRegistry` +> **administrator** on that chain (as shown by `getTokenConfig` in Step 1). + +[SetPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/SetPool.s.sol) + +View the setPool script on GitHub. + +1. Cut over on **Ethereum Sepolia**: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_SEPOLIA_TOKEN TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL forge script \ + script/setup/SetPool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +✅ Pool set successfully! +``` + +2. Cut over on **Arbitrum Sepolia**: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL forge script \ + script/setup/SetPool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +> **NOTE: Rollback** +> +> If you discover an issue immediately after cutover, you can roll back by calling `setPool` again and +> pointing back to your v1 pool on that chain. Keep the dual-remote-pool configuration in place until you’re stable. + +### 8. Step 8: Validate end-to-end (Sepolia → Arbitrum) with ccip-cli + +Send a small test transfer from **Ethereum Sepolia → Arbitrum Sepolia** and confirm it succeeds. + +> **NOTE: Prerequisites for the send** +> +> 1. You need some of your token on Ethereum Sepolia to send. +> 2. You need Sepolia ETH for gas and fees. +> 3. You need Arbitrum Sepolia ETH for gas. + +Set the router address for Ethereum Sepolia (from the [CCIP Directory](https://docs.chain.link/ccip/directory/testnet) or `HelperConfig.s.sol` in the template): + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 +``` + +1. Send a transfer on **default finality**: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Your output should look something like this: + +```bash filename="Terminal" +🚀 Sending message ... messageId => 0x... +CCIP Explorer: https://ccip.chain.link/msg/0x... +``` + +2. (Optional) Enable and request **block-depth faster than finality** with depth 32 + +First, allow block-depth finality on your v2 pools: + +[SetFinalityConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/finality-config/SetFinalityConfig.s.sol) + +View the finality configuration script on GitHub. + +```bash filename="Terminal" +BLOCK_DEPTH=32 TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL forge script \ + script/configure/finality-config/SetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast + +BLOCK_DEPTH=32 TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL forge script \ + script/configure/finality-config/SetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Then send using `--extra finality=32`: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --extra finality=32 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +### 9. Step 9: Confirm your token now routes through v2 pools + +Re-run the Step 1 checks. You should now see the **v2 pool addresses** as the live `tokenPool` on both chains, and +`typeAndVersion` should report v2. + +1. Ethereum Sepolia: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_SEPOLIA_TOKEN forge script script/setup/GetTokenConfig.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +ADDRESS=$ETHEREUM_SEPOLIA_V2_POOL forge script script/setup/GetTypeAndVersion.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + tokenPool: 0xNewSepoliaV2PoolAddress + +typeAndVersion: BurnMintTokenPool 2.0.0 +``` + +2. Arbitrum Sepolia: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN forge script script/setup/GetTokenConfig.s.sol --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +ADDRESS=$ARBITRUM_SEPOLIA_V2_POOL forge script script/setup/GetTypeAndVersion.s.sol --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + tokenPool: 0xNewArbitrumV2PoolAddress + +typeAndVersion: BurnMintTokenPool 2.0.0 +``` + +### 10. Step 10: Safety checks and cleanup (after v1 inflight settles) + +After cutover, keep the v1 remote pool addresses configured on the v2 pools until you are confident there are no more +in-flight v1 messages. There is no cost to waiting longer. + +Once you’re ready, remove the **old v1 remote pool addresses** from the v2 pools. + +[RemoveRemotePool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/remote-pools/RemoveRemotePool.s.sol) + +View the remote pool removal script on GitHub. + +1. On the **Sepolia v2 pool**, remove the old Arbitrum v1 pool address: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL \ + DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + REMOTE_POOL_ADDRESS=$ARBITRUM_SEPOLIA_V1_POOL \ + forge script \ + script/configure/remote-pools/RemoveRemotePool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +⚠️ WARNING: All inflight transactions from this pool will be rejected after removal. + +Current Remote Pools: 2 + [0] 0xOldArbitrumV1PoolAddress + [1] 0xNewArbitrumV2PoolAddress + +✅ Remote pool removed successfully! +``` + +2. On the **Arbitrum v2 pool**, remove the old Sepolia v1 pool address: + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL \ + DEST_CHAIN=ETHEREUM_SEPOLIA \ + REMOTE_POOL_ADDRESS=$ETHEREUM_SEPOLIA_V1_POOL \ + forge script \ + script/configure/remote-pools/RemoveRemotePool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +> **CAUTION: If you removed too early** +> +> If a v1-sent message is still in-flight and you removed its source pool address, it can fail validation on the +> destination. You can recover by adding the pool address back with `AddRemotePool`, then retry cleanup +> later. + +[AddRemotePool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/remote-pools/AddRemotePool.s.sol) + +View the remote pool add script on GitHub (recovery). + +Recovery example (re-add the old pool address, then retry cleanup later): + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL \ + DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + REMOTE_POOL_ADDRESS=$ARBITRUM_SEPOLIA_V1_POOL \ + forge script \ + script/configure/remote-pools/AddRemotePool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +> **NOTE: Optional hardening** +> +> After migration and cleanup, you can also revoke mint/burn roles from the old v1 pools (if your token’s access control +> allows it). This is token-specific and may require calling role revocation functions directly on your token contract. + +--- + +# Migrate your CCT token pools from CCIP v1 to v2 (Burn & Mint) using Hardhat +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat +Last Updated: 2026-06-11 + +## Guide Versions + +- [Foundry (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry) + +- [Hardhat (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat) + +This migration guide helps you upgrade a deployed Burn & Mint Cross-Chain Token (CCT) setup from CCIP v1 (1.5.x / 1.6.x) to CCIP 2.0, while keeping your existing token addresses and minimizing disruption. + +> **NOTE: Finality and v2 features are optional** +> +> 1. **Default finality:** Transfers use the chain's native finality unless you explicitly opt into +> block-depth faster than finality. +> 2. **Opt-in changes:** Pool-level fees, faster than finality, and hooks only apply if you configure +> them. This guide only focuses on a safe pool migration. + +In this guide you will: + +1. Confirm prerequisites and record your existing CCIP v1 deployment details. +2. Deploy new CCIP 2.0 `Burn Mint` token pools on each chain. +3. Configure the v2 pools to accept messages from both the old v1 pools and the new v2 pools. +4. Cut over CCIP routing by calling [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol).setPool on each chain. +5. Validate transfers end-to-end, then optionally clean up legacy configuration. + +## Before You Begin + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install `Node.js v22.10.0` using their + [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can't run `npm`, reinstall/update Node.js from the official installer. + +2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/). You can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli). + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +``` + +Verify the installation by running the following command: + +```bash filename="Terminal" +ccip-cli --version +``` + +3. Clone the repository and navigate to the project directory: + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-hardhat) + +Clone the CCIP 2.0 Hardhat template for a smoother CCT setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-hardhat.git +cd docs-cct-hardhat +``` + +4. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `npx hardhat keystore set`) +KEYSTORE_NAME= + +# RPC URLs +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment tasks) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/helper-config.ts) + +View the complete list of supported chains in the `helper-config.ts` file. + +5. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +6. Build the project: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +7. Create an encrypted Hardhat keystore, if you haven't already: + +```bash filename="Terminal" +npx hardhat keystore set +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +> **NOTE: Note** +> +> 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it +> asks you to enter your private key as hidden text. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in +> `.env`. +> 3. Use `npx hardhat keystore list` to check previously created keystores. + +### 2. What this guide assumes + +- **EVM-to-EVM only**: this page is strictly for Ethereum Sepolia ↔ Arbitrum Sepolia. - **Burn & Mint pools only**: + standard + [`BurnMintTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol) + migrations (no custom pools). - **Admin control**: you control the `TokenAdminRegistry` administrator role for the + token on both chains. - **Token permissions**: you can grant the required mint/burn permissions to the new v2 pools on + each chain. - **Pool-owner actions**: you can execute pool-owner actions on both chains (for example, + `applyChainUpdates`, `removeRemotePool`). - **Existing v1 deployment**: you are migrating an already-live CCIP v1 + setup (1.5.x or 1.6.x), not doing a new registration. + +## Migration Guide + +> **TIP: Set these variables once for cleaner copy/paste** +> +> This guide uses explicit addresses so you don't need local `deployments/` files. Start by exporting your +> token addresses, then we'll export the v1/v2 pool addresses as we discover them. +> +> ```bash filename="Terminal" +> # Token addresses (existing deployments) +> export ETHEREUM_SEPOLIA_TOKEN=0x... +> export ARBITRUM_SEPOLIA_TOKEN=0x... +> ``` + +### 1. Step 1: Confirm the live CCIP v1 pool (and record v1 addresses) + +This step answers two questions, on both chains: + +1. Which pool is currently live for your token? (read from `TokenAdminRegistry.getTokenConfig`) +2. Is that pool a v1.5/v1.6 pool? (read `typeAndVersion()` from the pool) + +[getTokenConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/getTokenConfig.ts) + +View the token config query task on GitHub. + +1. Read the token config on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat getTokenConfig \ + --tokenaddress $ETHEREUM_SEPOLIA_TOKEN \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +📋 Get Token Config +======================================== +Chain: Ethereum Sepolia +Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Action: Read getTokenConfig +======================================== + +Token Config: + administrator: 0xYourAdminAddress + pendingAdministrator: 0x0000000000000000000000000000000000000000 + tokenPool: 0xOldSepoliaV1PoolAddress +``` + +Export the v1 pool address: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_V1_POOL=0xOldSepoliaV1PoolAddress +``` + +2. Repeat on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat getTokenConfig \ + --tokenaddress $ARBITRUM_SEPOLIA_TOKEN \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + administrator: 0xYourAdminAddress + pendingAdministrator: 0x0000000000000000000000000000000000000000 + tokenPool: 0xOldArbitrumV1PoolAddress +``` + +Export the v1 pool address: + +```bash filename="Terminal" +export ARBITRUM_SEPOLIA_V1_POOL=0xOldArbitrumV1PoolAddress +``` + +[getTypeAndVersion.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/getTypeAndVersion.ts) + +View the type/version query task on GitHub. + +3. Confirm the v1 pool version on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat getTypeAndVersion \ + --address $ETHEREUM_SEPOLIA_V1_POOL \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +typeAndVersion: BurnMintTokenPool 1.6.1 +``` + +4. Confirm the v1 pool version on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat getTypeAndVersion \ + --address $ARBITRUM_SEPOLIA_V1_POOL \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +typeAndVersion: BurnMintTokenPool 1.6.1 +``` + +> **NOTE: What to check before continuing** +> +> - **administrator** should match the account/multisig you control (this account must execute +> `setPool` during cutover). +> - **pendingAdministrator** should typically be `0x0`. If it's set, resolve it before +> migrating to avoid confusion. +> - **typeAndVersion** should indicate a v1.5.x or v1.6.x `BurnMintTokenPool`. + +### 2. Step 2: Record your v1 lane configuration (remote pools + rate limits) + +Before deploying anything, snapshot your current v1 configuration so you can keep behavior consistent after migration. + +[getSupportedChains.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/getSupportedChains.ts) + +View the supported-chains query task on GitHub. + +1. List configured remote chains on the Sepolia v1 pool: + +```bash filename="Terminal" +npx hardhat getSupportedChains \ + --tokenpool $ETHEREUM_SEPOLIA_V1_POOL \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Supported Remote Chains: 1 + + [0] Arbitrum Sepolia (3478487238524512106) + Remote Pools: 1 + [0] 0xOldArbitrumV1PoolAddress +``` + +2. List configured remote chains on the Arbitrum v1 pool: + +```bash filename="Terminal" +npx hardhat getSupportedChains \ + --tokenpool $ARBITRUM_SEPOLIA_V1_POOL \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Supported Remote Chains: 1 + + [0] Ethereum Sepolia (16015286601757825753) + Remote Pools: 1 + [0] 0xOldSepoliaV1PoolAddress +``` + +[getCurrentRateLimits.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/rate-limiter/getCurrentRateLimits.ts) + +View the rate limiter query task on GitHub. + +3. Snapshot current v1 rate limits on the Sepolia → Arbitrum lane: + +```bash filename="Terminal" +npx hardhat getCurrentRateLimits \ + --tokenpool $ETHEREUM_SEPOLIA_V1_POOL \ + --destchain arbitrumSepolia \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Pool Version: v1 + + Outbound Enabled: true + Outbound Capacity: 1000000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 1000000000000000000000 + Inbound Rate: 100000000000000000 +``` + +4. Snapshot current v1 rate limits on the Arbitrum → Sepolia lane: + +```bash filename="Terminal" +npx hardhat getCurrentRateLimits \ + --tokenpool $ARBITRUM_SEPOLIA_V1_POOL \ + --destchain sepolia \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Pool Version: v1 + + Outbound Enabled: true + Outbound Capacity: 1000000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 1000000000000000000000 + Inbound Rate: 100000000000000000 +``` + +Record these values, you'll reuse them when configuring the v2 pools in Step 5. + +### 3. Step 3: Deploy new v2 pools (per chain) + +Deploy a new v2 `BurnMintTokenPool` on each chain, pointing to your existing token address on that chain. + +[deployBurnMintTokenPool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/deploy/deployBurnMintTokenPool.ts) + +View the Burn & Mint pool deployment task on GitHub. + +1. Deploy the v2 pool on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat deployTokenPool \ + --tokenaddress $ETHEREUM_SEPOLIA_TOKEN \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +🔥⚒️ Deploy Burn & Mint Token Pool + +[Step 1] Deploying BurnMintTokenPool on Ethereum Sepolia +Token Pool deployed at: 0xNewSepoliaV2PoolAddress + +[Step 2] Granting mint and burn roles to token pool: 0xNewSepoliaV2PoolAddress +✅ Roles granted successfully! +``` + +Export the new v2 pool address: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_V2_POOL=0xNewSepoliaV2PoolAddress +``` + +2. Deploy the v2 pool on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat deployTokenPool \ + --tokenaddress $ARBITRUM_SEPOLIA_TOKEN \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +🔥⚒️ Deploy Burn & Mint Token Pool + +[Step 1] Deploying BurnMintTokenPool on Arbitrum Sepolia +Token Pool deployed at: 0xNewArbitrumV2PoolAddress + +[Step 2] Granting mint and burn roles to token pool: 0xNewArbitrumV2PoolAddress +✅ Roles granted successfully! +``` + +Export the new v2 pool address: + +```bash filename="Terminal" +export ARBITRUM_SEPOLIA_V2_POOL=0xNewArbitrumV2PoolAddress +``` + +> **NOTE: Dual mint/burn window** +> +> Until post-migration cleanup, **both** v1 and v2 pools may hold mint/burn roles on your tokens. Your CCIP +> routing still points to the v1 pool until Step 7. + +### 4. Step 4 (Recommended): Pause outbound transfers on the v1 pools + +This recommended step throttles new outbound traffic on v1 while you cut over. Use capacity = 2 and rate = 1 +as the most restrictive (near-zero) configuration we document for v1 pools while keeping the lane configured. + +[updateRateLimiters.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/rate-limiter/updateRateLimiters.ts) + +View the rate limiter update task on GitHub. + +1. Pause outbound on the Sepolia v1 pool (lane to Arbitrum Sepolia): + +```bash filename="Terminal" +npx hardhat updateRateLimiters \ + --tokenpool $ETHEREUM_SEPOLIA_V1_POOL \ + --destchain arbitrumSepolia \ + --outboundcapacity 2 \ + --outboundrate 1 \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +⚡️ Update Rate Limiters +======================================== +Pool Version: v1 (setChainRateLimiterConfig) + +New Configuration: + Outbound Enabled: true + Outbound Capacity: 2 + Outbound Rate: 1 +``` + +2. Pause outbound on the Arbitrum v1 pool (lane to Ethereum Sepolia): + +```bash filename="Terminal" +npx hardhat updateRateLimiters \ + --tokenpool $ARBITRUM_SEPOLIA_V1_POOL \ + --destchain sepolia \ + --outboundcapacity 2 \ + --outboundrate 1 \ + --network arbitrumSepolia +``` + +### 5. Step 5: Configure the v2 pools (applyChainUpdates) + +On each chain, configure the v2 pool to recognize the lane to the remote chain. + +Critical migration rule: the `destPools` list must include both the old v1 remote pool address and the new +v2 remote pool address. This allows in-flight messages sent via v1 (before cutover) to still validate after you cut +over to v2. + +[applyChainUpdates.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/applyChainUpdates.ts) + +View the applyChainUpdates task (JSON mode recommended) on GitHub. + +1. Configure the Sepolia v2 pool (remote chain: Arbitrum Sepolia) + +Update `input/apply-chain-updates.json`: + +```json filename="input/apply-chain-updates.json" +{ + "_comment": "Configure Sepolia v2 pool → Arbitrum Sepolia. Replace 0x... placeholders with your real addresses.", + "sourcePool": "0xNewSepoliaV2PoolAddress", + "remoteChains": [ + { + "destChain": "ARBITRUM_SEPOLIA", + "destPools": ["0xOldArbitrumV1PoolAddress", "0xNewArbitrumV2PoolAddress"], + "destToken": "0xYourArbitrumTokenAddress", + "outboundRateLimit": { "enabled": false, "capacity": "0", "rate": "0" }, + "inboundRateLimit": { "enabled": false, "capacity": "0", "rate": "0" } + } + ] +} +``` + +If you had v1 rate limits enabled, copy the values you recorded in Step 2 into `outboundRateLimit` and +`inboundRateLimit` so the v2 pool behavior stays consistent. + +Run the task on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat applyChainUpdates --viajsonfile --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔗 Apply Chain Updates (JSON mode) +======================================== +Source Chain: Ethereum Sepolia +Token Pool: 0xNewSepoliaV2PoolAddress +Remote Chains: 1 + + [0] Arbitrum Sepolia + Pools: 2 + [0] 0xOldArbitrumV1PoolAddress + [1] 0xNewArbitrumV2PoolAddress + +✅ Chain updates applied successfully! +``` + +2. Configure the Arbitrum v2 pool (remote chain: Ethereum Sepolia) + +Update `input/apply-chain-updates.json`: + +```json filename="input/apply-chain-updates.json" +{ + "_comment": "Configure Arbitrum v2 pool → Ethereum Sepolia. Replace 0x... placeholders with your real addresses.", + "sourcePool": "0xNewArbitrumV2PoolAddress", + "remoteChains": [ + { + "destChain": "ETHEREUM_SEPOLIA", + "destPools": ["0xOldSepoliaV1PoolAddress", "0xNewSepoliaV2PoolAddress"], + "destToken": "0xYourSepoliaTokenAddress", + "outboundRateLimit": { "enabled": false, "capacity": "0", "rate": "0" }, + "inboundRateLimit": { "enabled": false, "capacity": "0", "rate": "0" } + } + ] +} +``` + +Run the task on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat applyChainUpdates --viajsonfile --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Source Chain: Arbitrum Sepolia +Token Pool: 0xNewArbitrumV2PoolAddress +Remote Chains: 1 + + [0] Ethereum Sepolia + Pools: 2 + [0] 0xOldSepoliaV1PoolAddress + [1] 0xNewSepoliaV2PoolAddress + +✅ Chain updates applied successfully! +``` + +### 6. Step 6: Verify v2 configuration (before cutover) + +Verify that each v2 pool has two remote pools configured for the lane: `[remoteV1Pool, remoteV2Pool]`. + +[getRemotePools.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/remote-pools/getRemotePools.ts) + +View the remote pool query task on GitHub. + +1. Check remote pools on the Sepolia v2 pool: + +```bash filename="Terminal" +npx hardhat getRemotePools \ + --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \ + --destchain arbitrumSepolia \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Chain Supported: Yes +Remote Pools: 2 + [0] 0xOldArbitrumV1PoolAddress + [1] 0xNewArbitrumV2PoolAddress +``` + +2. Check remote pools on the Arbitrum v2 pool: + +```bash filename="Terminal" +npx hardhat getRemotePools \ + --tokenpool $ARBITRUM_SEPOLIA_V2_POOL \ + --destchain sepolia \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Chain Supported: Yes +Remote Pools: 2 + [0] 0xOldSepoliaV1PoolAddress + [1] 0xNewSepoliaV2PoolAddress +``` + +[getCurrentRateLimits.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/rate-limiter/getCurrentRateLimits.ts) + +View the rate limiter query task on GitHub. + +3. Re-check rate limits on the v2 pools (optional but recommended) + +Sepolia v2 pool (lane to Arbitrum): + +```bash filename="Terminal" +npx hardhat getCurrentRateLimits \ + --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \ + --destchain arbitrumSepolia \ + --network sepolia +``` + +Arbitrum v2 pool (lane to Sepolia): + +```bash filename="Terminal" +npx hardhat getCurrentRateLimits \ + --tokenpool $ARBITRUM_SEPOLIA_V2_POOL \ + --destchain sepolia \ + --network arbitrumSepolia +``` + +### 7. Step 7: Cut over routing (TokenAdminRegistry.setPool) + +This is the migration cutover step. On each chain, update `TokenAdminRegistry` so your token routes through the new v2 +pool. + +> **NOTE: Who must run this step** +> +> The signer running `setPool` must be the token's `TokenAdminRegistry` +> **administrator** on that chain (as shown by `getTokenConfig` in Step 1). + +[setPool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/setPool.ts) + +View the setPool task on GitHub. + +1. Cut over on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat setPool \ + --tokenaddress $ETHEREUM_SEPOLIA_TOKEN \ + --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🏊 Set Token Pool +======================================== +Chain: Ethereum Sepolia +Token Pool: 0xNewSepoliaV2PoolAddress +Action: Set token pool +======================================== + +✅ Pool Set Complete on Ethereum Sepolia! +``` + +2. Cut over on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat setPool \ + --tokenaddress $ARBITRUM_SEPOLIA_TOKEN \ + --tokenpool $ARBITRUM_SEPOLIA_V2_POOL \ + --network arbitrumSepolia +``` + +> **NOTE: Rollback** +> +> If you discover an issue immediately after cutover, you can roll back by calling `setPool` again and +> pointing back to your v1 pool on that chain. Keep the dual-remote-pool configuration in place until you're stable. + +### 8. Step 8: Validate end-to-end (Sepolia → Arbitrum) with ccip-cli + +Send a small test transfer from Ethereum Sepolia → Arbitrum Sepolia and confirm it succeeds. + +> **NOTE: Prerequisites for the send** +> +> 1. You need some of your token on Ethereum Sepolia to send. +> 2. You need Sepolia ETH for gas (and optionally LINK if paying fees in LINK). +> 3. You need Arbitrum Sepolia ETH for gas to run any Arbitrum-side verification commands. + +Set the router address for Ethereum Sepolia (from the [CCIP Directory](https://docs.chain.link/ccip/directory/testnet) or `helper-config.ts` in the template): + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 +``` + +1. Send a transfer on default finality: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Your output should look something like this: + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +🚀 Sending message ... messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +2. (Optional) Request block-depth faster than finality with depth 32 + +Only use this if your v2 pools explicitly allow block-depth faster than finality. If this fails, omit `--extra` and retry with +default finality. + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --extra finality=32 \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +### 9. Step 9: Confirm your token now routes through v2 pools + +Re-run the Step 1 checks. You should now see the v2 pool addresses as the live `tokenPool` on both chains, and +`typeAndVersion` should report v2. + +1. Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat getTokenConfig --tokenaddress $ETHEREUM_SEPOLIA_TOKEN --network sepolia +npx hardhat getTypeAndVersion --address $ETHEREUM_SEPOLIA_V2_POOL --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + tokenPool: 0xNewSepoliaV2PoolAddress + +typeAndVersion: BurnMintTokenPool 2.0.0 +``` + +2. Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat getTokenConfig --tokenaddress $ARBITRUM_SEPOLIA_TOKEN --network arbitrumSepolia +npx hardhat getTypeAndVersion --address $ARBITRUM_SEPOLIA_V2_POOL --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + tokenPool: 0xNewArbitrumV2PoolAddress + +typeAndVersion: BurnMintTokenPool 2.0.0 +``` + +### 10. Step 10: Safety checks and cleanup (after v1 inflight settles) + +After cutover, keep the v1 remote pool addresses configured on the v2 pools until you are confident there are no more +in-flight v1 messages. There is no cost to waiting longer. + +Once you're ready, remove the old v1 remote pool +addresses from the v2 pools. + +[removeRemotePool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/remote-pools/removeRemotePool.ts) + +View the remote pool removal task on GitHub. + +1. On the Sepolia v2 pool, remove the old Arbitrum v1 pool address: + +```bash filename="Terminal" +npx hardhat removeRemotePool \ + --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \ + --destchain arbitrumSepolia \ + --remotepooladdress $ARBITRUM_SEPOLIA_V1_POOL \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +➖ Remove Remote Pool + +⚠️ WARNING: All inflight transactions from this pool will be rejected after removal. + +Current Remote Pools: 2 + [0] 0xOldArbitrumV1PoolAddress + [1] 0xNewArbitrumV2PoolAddress + +✅ Remote pool removed successfully! +``` + +2. On the Arbitrum v2 pool, remove the old Sepolia v1 pool address: + +```bash filename="Terminal" +npx hardhat removeRemotePool \ + --tokenpool $ARBITRUM_SEPOLIA_V2_POOL \ + --destchain sepolia \ + --remotepooladdress $ETHEREUM_SEPOLIA_V1_POOL \ + --network arbitrumSepolia +``` + +> **CAUTION: If you removed too early** +> +> If a v1-sent message is still in-flight and you removed its source pool address, it can fail validation on the +> destination. You can recover by adding the pool address back with `addRemotePool`, then retry cleanup +> later. + +[addRemotePool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/remote-pools/addRemotePool.ts) + +View the remote pool add task on GitHub (recovery). + +Recovery example (re-add the old pool address, then retry cleanup later): + +```bash filename="Terminal" +npx hardhat addRemotePool \ + --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \ + --destchain arbitrumSepolia \ + --remotepooladdress $ARBITRUM_SEPOLIA_V1_POOL \ + --network sepolia +``` + +> **NOTE: Optional hardening** +> +> After migration and cleanup, you can also revoke mint/burn roles from the old v1 pools (if your token's access control +> allows it). This is token-specific and may require calling role revocation functions directly on your token contract. + +--- + +# Migrate your CCT token pools from CCIP v1 to v2 (Lock & Mint) using Foundry +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-lock-mint-foundry +Last Updated: 2026-06-11 + +This migration guide helps you upgrade a deployed **Lock & Mint** Cross-Chain Token (CCT) setup from **CCIP v1 (1.5.x / 1.6.x)** to **CCIP 2.0**, while keeping your existing token addresses and minimizing disruption. + +This guide uses Foundry scripts inside our [docs-cct-foundry](https://github.com/smartcontractkit/docs-cct-foundry) repository. + +> **NOTE: Lock & Mint is asymmetric** +> +> In a Lock & Mint deployment the two chains are **not** symmetric. The canonical (source) chain runs a **Lock & +> Release** pool backed by an **ERC20 lockbox** that holds the locked liquidity; the destination chain runs a **Burn & +> Mint** pool. Because of this, the migration has an extra step that Burn & Mint does not: you must **move the locked +> liquidity** out of the old v1 pool and into the new v2 lockbox. + +> **NOTE: Finality and v2 features are optional** +> +> 1. **Default finality:** Transfers use the chain’s native finality unless you explicitly opt into +> block-depth faster than finality. +> 2. **Opt-in changes:** Pool-level fees, faster than finality, and hooks only apply if you configure +> them. This guide focuses on a safe pool migration first. + +In this guide you will: + +1. Confirm prerequisites and record your existing CCIP v1 deployment details, including the locked liquidity on the canonical chain. +2. Deploy the new CCIP 2.0 pools: an [`ERC20LockBox`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/ERC20LockBox.sol) + [`LockReleaseTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/LockReleaseTokenPool.sol) on the canonical chain, and a [`BurnMintTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol) on the destination chain. +3. Pause the old v1 pools and migrate the locked liquidity from the v1 Lock & Release pool into the new lockbox. +4. Configure the v2 pools to accept messages from **both** the old v1 pools and the new v2 pools. +5. Cut over CCIP routing by calling [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol).setPool on each chain, validate transfers end-to-end, then optionally clean up legacy configuration. + +## Before You Begin + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their + [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can’t run `npm`, reinstall/update Node.js from the official installer. + +2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/). You can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli). + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +``` + +Verify the installation by running the following command: + +```bash filename="Terminal" +ccip-cli --version +``` + +3. **Install Foundry**: + If you haven't already, follow the instructions in the [Foundry documentation](https://book.getfoundry.sh/getting-started/installation) to install Foundry. \ + Verify the installation by running the following command: + +```bash filename="Terminal" +forge --version +``` + +4. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 Foundry template](https://github.com/smartcontractkit/docs-cct-foundry) + +Clone the CCIP 2.0 Foundry template for a smoother CCT setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-foundry.git +cd docs-cct-foundry +``` + +5. Create an encrypted Foundry keystore, if you haven't already: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +6. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `cast wallet import`) +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (lane used in this guide) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/HelperConfig.s.sol) + +View the complete list of supported chains in the `HelperConfig.s.sol` file. + +7. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +8. Install dependencies and build the project: + +```bash filename="Terminal" +npm install && forge build +``` + +### 2. What this guide assumes + +- **EVM-to-EVM only**: this page is strictly for **Ethereum Sepolia ↔ Arbitrum Sepolia**. +- **Asymmetric Lock & Mint topology**: **Ethereum Sepolia** is the **canonical / Lock & Release** chain (it holds the `ERC20LockBox` and the `LockReleaseTokenPool`); **Arbitrum Sepolia** is the **destination / Burn & Mint** chain (it holds the `BurnMintTokenPool`). +- **Standard pools only**: a single shared lockbox with `LockReleaseTokenPool` (not [`SiloedLockReleaseTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/SiloedLockReleaseTokenPool.sol)) and a standard `BurnMintTokenPool` (no custom pools). +- You control the `TokenAdminRegistry` **administrator** role for the token **on both chains**. +- You own the **v1 pools on both chains** (needed for `setRebalancer`, `withdrawLiquidity`, `applyChainUpdates`, and `removeRemotePool`). +- You can grant the required **mint/burn** permissions to the new v2 `BurnMintTokenPool` on **Arbitrum Sepolia**. +- You are migrating an already-live CCIP v1 setup (1.5.x or 1.6.x), not doing a new registration. + +## Migration Guide + +> **TIP: Set these variables once for cleaner copy/paste** +> +> This guide uses explicit addresses so you don’t need local `script/deployments/` files. Start by exporting +> your token addresses; we’ll export the v1/v2 pool addresses and the lockbox address as we discover or deploy them. +> +> ```bash filename="Terminal" +> export ETHEREUM_SEPOLIA_TOKEN=0x... +> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... +> ``` + +### 1. Step 1: Confirm the live CCIP v1 pools (and record v1 addresses) + +This step answers two questions, on **both chains**: + +1. **Which pool is currently live for your token?** (read from `TokenAdminRegistry.getTokenConfig`) +2. **Is that pool a v1.5/v1.6 pool?** (read `typeAndVersion()` from the pool) + +Because Lock & Mint is asymmetric, expect a **`LockReleaseTokenPool`** on Ethereum Sepolia and a **`BurnMintTokenPool`** on Arbitrum Sepolia. + +[GetTokenConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/GetTokenConfig.s.sol) + +View the token config query script on GitHub. + +1. Read the token config on **Ethereum Sepolia** (canonical / Lock & Release): + +```bash filename="Terminal" +TOKEN=$ETHEREUM_SEPOLIA_TOKEN forge script \ + script/setup/GetTokenConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + administrator: 0xYourAdminAddress + pendingAdministrator: 0x0000000000000000000000000000000000000000 + tokenPool: 0xOldSepoliaV1LockReleasePool +``` + +Export the v1 pool address: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_V1_POOL=0xOldSepoliaV1LockReleasePool +``` + +2. Repeat on **Arbitrum Sepolia** (destination / Burn & Mint): + +```bash filename="Terminal" +TOKEN=$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN forge script \ + script/setup/GetTokenConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + administrator: 0xYourAdminAddress + pendingAdministrator: 0x0000000000000000000000000000000000000000 + tokenPool: 0xOldArbitrumV1BurnMintPool +``` + +Export the v1 pool address: + +```bash filename="Terminal" +export ARBITRUM_SEPOLIA_V1_POOL=0xOldArbitrumV1BurnMintPool +``` + +[GetTypeAndVersion.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/GetTypeAndVersion.s.sol) + +View the type/version query script on GitHub. + +3. Confirm the v1 pool version on **Ethereum Sepolia** (expect a Lock & Release pool): + +```bash filename="Terminal" +ADDRESS=$ETHEREUM_SEPOLIA_V1_POOL forge script \ + script/setup/GetTypeAndVersion.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +typeAndVersion: LockReleaseTokenPool 1.6.1 +``` + +4. Confirm the v1 pool version on **Arbitrum Sepolia** (expect a Burn & Mint pool): + +```bash filename="Terminal" +ADDRESS=$ARBITRUM_SEPOLIA_V1_POOL forge script \ + script/setup/GetTypeAndVersion.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +typeAndVersion: BurnMintTokenPool 1.6.1 +``` + +> **NOTE: What to check before continuing** +> +> - **administrator** should match the account/multisig you control (this account must execute +> `setPool` during cutover). +> - **pendingAdministrator** should typically be `0x0`. If it’s set, resolve it before +> migrating to avoid confusion. +> - **typeAndVersion** should indicate a v1.5.x or v1.6.x `LockReleaseTokenPool` on Ethereum +> Sepolia and a v1.5.x or v1.6.x `BurnMintTokenPool` on Arbitrum Sepolia. + +### 2. Step 2: Record your v1 lane configuration (remote pools, rate limits, and canonical liquidity) + +Before deploying anything, snapshot your current v1 configuration so you can keep behavior consistent after migration; and record the **locked liquidity** you will move into the new lockbox. + +[GetSupportedChains.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/GetSupportedChains.s.sol) + +View the supported-chains query script on GitHub. + +1. List configured remote chains on the **Sepolia v1 pool**: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V1_POOL forge script \ + script/setup/GetSupportedChains.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +2. List configured remote chains on the **Arbitrum v1 pool**: + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V1_POOL forge script \ + script/setup/GetSupportedChains.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +[GetCurrentRateLimits.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/GetCurrentRateLimits.s.sol) + +View the rate limiter query script on GitHub. + +3. Snapshot current v1 rate limits on the **Sepolia → Arbitrum** lane: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V1_POOL DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Outbound Enabled: true +Outbound Capacity: 1000000000000000000000 +Outbound Rate: 100000000000000000 +Inbound Enabled: true +Inbound Capacity: 1000000000000000000000 +Inbound Rate: 100000000000000000 +``` + +4. Snapshot current v1 rate limits on the **Arbitrum → Sepolia** lane: + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V1_POOL DEST_CHAIN=ETHEREUM_SEPOLIA forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Record these values since you'll reuse them when configuring the v2 pools in Step 6. + +5. Record the **locked liquidity** held by the Sepolia v1 Lock & Release pool. This is the exact amount (in the token's smallest unit) you will migrate into the new lockbox in Step 5. + There is no “drain all” shortcut, the withdrawal script requires an exact amount; so read the balance directly: + +```bash filename="Terminal" +cast call $ETHEREUM_SEPOLIA_TOKEN \ + "balanceOf(address)(uint256)" \ + $ETHEREUM_SEPOLIA_V1_POOL \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Export the value so you can reuse it later: + +```bash filename="Terminal" +export DRAIN_AMOUNT= +``` + +> **CAUTION: Record rate limits before you pause** +> +> Step 4 overwrites the v1 pool’s **outbound** rate limits. If you want to carry your original v1 rate +> limits over to v2, make sure you record them here first. + +### 3. Step 3: Deploy the new v2 pools (LockBox + Lock & Release on Ethereum Sepolia, Burn & Mint on Arbitrum Sepolia) + +This step is **asymmetric**: Ethereum Sepolia needs a lockbox **and** a `LockReleaseTokenPool`, while Arbitrum Sepolia needs only a `BurnMintTokenPool`. + +### Ethereum Sepolia (canonical): LockBox → Lock & Release pool → authorize + +[DeployERC20LockBox.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployERC20LockBox.s.sol) + +View the lockbox deployment script on GitHub. + +1. Deploy the `ERC20LockBox` on **Ethereum Sepolia**. It holds the locked liquidity on behalf of the Lock & Release pool and **must be deployed before** the pool: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_SEPOLIA_TOKEN forge script \ + script/deploy/DeployERC20LockBox.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +ERC20LockBox deployed at: 0xNewSepoliaLockBox +✅ ERC20LockBox deployed successfully! +``` + +Export the lockbox address: + +```bash filename="Terminal" +export LOCK_BOX=0xNewSepoliaLockBox +``` + +[DeployLockReleaseTokenPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployLockReleaseTokenPool.s.sol) + +View the Lock & Release pool deployment script on GitHub. + +2. Deploy the v2 `LockReleaseTokenPool` on **Ethereum Sepolia**, passing the lockbox address: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_SEPOLIA_TOKEN LOCK_BOX=$LOCK_BOX forge script \ + script/deploy/DeployLockReleaseTokenPool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Pool deployed at: 0xNewSepoliaV2LockReleasePool +✅ LockReleaseTokenPool deployed successfully! +``` + +Export the new v2 pool address: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_V2_POOL=0xNewSepoliaV2LockReleasePool +``` + +[UpdateAuthorizedCallers.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol) + +View the authorized callers script on GitHub. + +3. Authorize the new v2 pool as a **caller on the lockbox** so it can deposit and withdraw tokens during cross-chain transfers: + +```bash filename="Terminal" +LOCK_BOX=$LOCK_BOX \ + ADD_ADDRESSES=$ETHEREUM_SEPOLIA_V2_POOL \ + forge script \ + script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +### Arbitrum Sepolia (destination): Burn & Mint pool + +[DeployBurnMintTokenPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployBurnMintTokenPool.s.sol) + +View the Burn & Mint pool deployment script on GitHub. + +4. Deploy the v2 `BurnMintTokenPool` on **Arbitrum Sepolia**: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN forge script \ + script/deploy/DeployBurnMintTokenPool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Pool deployed at: 0xNewArbitrumV2BurnMintPool +✅ Roles granted successfully! +``` + +Export the new v2 pool address: + +```bash filename="Terminal" +export ARBITRUM_SEPOLIA_V2_POOL=0xNewArbitrumV2BurnMintPool +``` + +> **CAUTION: If the role grant is skipped** +> +> The deploy script tries to call `grantMintAndBurnRoles` on your Arbitrum token. If your token predates that +> interface, the script prints a warning instead of `✅ Roles granted successfully!` and you must grant +> **mint** and **burn** roles to `$ARBITRUM_SEPOLIA_V2_POOL` yourself, using +> whatever access-control mechanism your token exposes. The new pool cannot mint or burn until it holds these roles. + +> **NOTE: Dual-role window** +> +> Until post-migration cleanup, **both** v1 and v2 Burn & Mint pools may hold mint/burn roles on your +> Arbitrum token. The new Ethereum Sepolia pool is deployed and authorized on the lockbox but is still +> **empty** (no liquidity yet). Your CCIP routing still points to the v1 pools until Step 8. + +### 4. Step 4 (Required): Pause outbound transfers on the v1 pools + +For Lock & Mint this step is **required**, not optional: Step 5 drains the old v1 Lock & Release pool, and pausing prevents new transfers from locking additional tokens (which would be stranded outside the lockbox). Use **capacity = 2** and **rate = 1**: the most restrictive (near-zero) configuration we document that works across all v1 pool versions while keeping the lane configured. Only the **outbound** direction is changed; inbound is left as-is so in-flight messages can still arrive. + +[UpdateRateLimiters.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/UpdateRateLimiters.s.sol) + +View the rate limiter update script on GitHub. + +1. Pause outbound on the **Sepolia v1 pool** (lane to Arbitrum Sepolia): + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V1_POOL \ + DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + OUTBOUND_RATE_LIMIT_CAPACITY=2 \ + OUTBOUND_RATE_LIMIT_RATE=1 \ + forge script \ + script/configure/rate-limiter/UpdateRateLimiters.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +2. Pause outbound on the **Arbitrum v1 pool** (lane to Ethereum Sepolia): + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V1_POOL \ + DEST_CHAIN=ETHEREUM_SEPOLIA \ + OUTBOUND_RATE_LIMIT_CAPACITY=2 \ + OUTBOUND_RATE_LIMIT_RATE=1 \ + forge script \ + script/configure/rate-limiter/UpdateRateLimiters.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +> **CAUTION: Wait for in-flight transfers to settle before draining** +> +> - After pausing, wait until all in-flight transfers in **both** directions show a final status in the [CCIP +> Explorer](https://ccip.chain.link/) before you drain liquidity in Step 5. - If you drain the Sepolia v1 pool while a +> Arbitrum → Sepolia (mint → release) message is still in flight, that message would try to release from the now-empty +> v1 pool: which is still the registered pool until cutover, and fail. + +### 5. Step 5: Migrate locked liquidity into the LockBox (Ethereum Sepolia only) + +This step is unique to Lock & Mint. In v1 the Lock & Release pool held liquidity directly in its own balance; in v2 that liquidity lives in the `ERC20LockBox`. You must move it. + +> **CAUTION: Non-atomic custody** +> +> Executed as separate EOA transactions, the drained tokens sit in **your wallet** between the withdrawal and the +> deposit. Use a secure signer (for example, a hardware wallet). Teams that require atomicity can batch these calls +> through a Safe multisig meta-transaction, which is outside the scope of this guide. + +First, export the address of your keystore account (it must be the v1 pool owner and will be the temporary rebalancer and lockbox depositor): + +```bash filename="Terminal" +export MY_ADDRESS=0xYourKeystoreAccountAddress +``` + +[GetRebalancer.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/liquidity/GetRebalancer.s.sol) + +View the rebalancer query script on GitHub. + +1. (Optional) Read the current rebalancer on the v1 pool: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V1_POOL forge script \ + script/configure/liquidity/GetRebalancer.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +[SetRebalancer.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/liquidity/SetRebalancer.s.sol) + +View the set-rebalancer script on GitHub. + +2. Set yourself as the rebalancer on the v1 pool (only the rebalancer can withdraw liquidity; this call must be sent by the **pool owner**): + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V1_POOL \ + REBALANCER=$MY_ADDRESS \ + forge script \ + script/configure/liquidity/SetRebalancer.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +[WithdrawLiquidity.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/liquidity/WithdrawLiquidity.s.sol) + +View the withdraw-liquidity script on GitHub. + +3. Withdraw the full balance from the v1 pool into your wallet (must be sent by the **rebalancer** you just set). Use the exact `DRAIN_AMOUNT` you recorded in Step 2: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V1_POOL \ + AMOUNT=$DRAIN_AMOUNT \ + forge script \ + script/configure/liquidity/WithdrawLiquidity.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +4. Temporarily authorize your address as a caller on the lockbox. The lockbox `deposit` function is restricted to authorized callers; even the lockbox owner must be authorized to deposit: + +```bash filename="Terminal" +LOCK_BOX=$LOCK_BOX \ + ADD_ADDRESSES=$MY_ADDRESS \ + forge script \ + script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +[DepositToLockBox.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/operations/DepositToLockBox.s.sol) + +View the lockbox deposit script on GitHub. + +5. Deposit the tokens into the lockbox. The script approves the lockbox and deposits in a single run: + +```bash filename="Terminal" +LOCK_BOX=$LOCK_BOX \ + TOKEN=$ETHEREUM_SEPOLIA_TOKEN \ + AMOUNT=$DRAIN_AMOUNT \ + forge script \ + script/operations/DepositToLockBox.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +6. Remove your address from the lockbox authorized callers, leaving only the pool authorized: + +```bash filename="Terminal" +LOCK_BOX=$LOCK_BOX \ + REMOVE_ADDRESSES=$MY_ADDRESS \ + forge script \ + script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +7. Verify the v1 pool is fully drained (this should return `0`): + +```bash filename="Terminal" +cast call $ETHEREUM_SEPOLIA_TOKEN \ + "balanceOf(address)(uint256)" \ + $ETHEREUM_SEPOLIA_V1_POOL \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +The old v1 pool is being retired, so there is no need to restore its original rebalancer. + +### 6. Step 6: Configure the v2 pools (applyChainUpdates) + +On **each chain**, configure the v2 pool to recognize the lane to the remote chain. + +**Critical migration rule:** the `destPools` list must include **both** the old v1 remote pool address **and** the new +v2 remote pool address. This allows in-flight messages sent via v1 (before cutover) to still validate after you cut over +to v2. You must use **JSON mode** here, because it is the only mode that accepts more than one remote pool per chain. + +[ApplyChainUpdates.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/ApplyChainUpdates.s.sol) + +View the applyChainUpdates script (JSON mode) on GitHub. + +[apply-chain-updates.json](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/input/apply-chain-updates.json) + +View the JSON input file used for applyChainUpdates on GitHub. + +1. Configure the **Sepolia v2 Lock & Release pool** (remote chain: Arbitrum Sepolia). + +Update `script/input/apply-chain-updates.json`: + +```json filename="script/input/apply-chain-updates.json" +{ + "_comment": "Configure Sepolia v2 pool → Arbitrum Sepolia. Replace 0x... placeholders with your real addresses.", + "sourcePool": "0xNewSepoliaV2LockReleasePool", + "remoteChains": [ + { + "destChain": "ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1", + "destPools": ["0xOldArbitrumV1BurnMintPool", "0xNewArbitrumV2BurnMintPool"], + "destToken": "0xYourArbitrumToken", + "outboundRateLimit": { + "enabled": true, + "capacity": 1000000000000000000000, + "rate": 100000000000000000 + }, + "inboundRateLimit": { + "enabled": true, + "capacity": 1000000000000000000000, + "rate": 100000000000000000 + } + } + ] +} +``` + +Copy your v1 lane rate limits from Step 2 into `outboundRateLimit` and `inboundRateLimit`. If you don’t have existing +values, you can use the baseline values shown above. Setting `"enabled": false` disables rate limiting (unlimited +transfers). + +Run the script on **Ethereum Sepolia**: + +```bash filename="Terminal" +VIA_JSON_FILE=true forge script \ + script/setup/ApplyChainUpdates.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +Remote Chains: 1 + + [0] Arbitrum Sepolia + Pools: 2 + [0] 0xOldArbitrumV1BurnMintPool + [1] 0xNewArbitrumV2BurnMintPool + +✅ Chain updates applied successfully! +``` + +2. Configure the **Arbitrum v2 Burn & Mint pool** (remote chain: Ethereum Sepolia). + +Update `script/input/apply-chain-updates.json`: + +```json filename="script/input/apply-chain-updates.json" +{ + "_comment": "Configure Arbitrum v2 pool → Ethereum Sepolia. Replace 0x... placeholders with your real addresses.", + "sourcePool": "0xNewArbitrumV2BurnMintPool", + "remoteChains": [ + { + "destChain": "ETHEREUM_SEPOLIA", + "destPools": ["0xOldSepoliaV1LockReleasePool", "0xNewSepoliaV2LockReleasePool"], + "destToken": "0xYourSepoliaToken", + "outboundRateLimit": { + "enabled": true, + "capacity": 1000000000000000000000, + "rate": 100000000000000000 + }, + "inboundRateLimit": { + "enabled": true, + "capacity": 1000000000000000000000, + "rate": 100000000000000000 + } + } + ] +} +``` + +Run the script on **Arbitrum Sepolia**: + +```bash filename="Terminal" +VIA_JSON_FILE=true forge script \ + script/setup/ApplyChainUpdates.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +> **NOTE: Re-running is safe** +> +> The script is idempotent: if a remote chain is already configured, it removes and re-adds it in the same transaction. +> You do not need to worry about a “chain already exists” error. + +### 7. Step 7: Verify the v2 configuration (before cutover) + +Verify that each v2 pool has **two** remote pools configured for the lane (`[remoteV1Pool, remoteV2Pool]`), and that the lockbox is attached and funded. + +[GetRemotePools.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/remote-pools/GetRemotePools.s.sol) + +View the remote pool query script on GitHub. + +1. Check remote pools on the **Sepolia v2 pool**: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 forge script \ + script/configure/remote-pools/GetRemotePools.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Chain Supported: Yes +Remote Pools: 2 + [0] 0xOldArbitrumV1BurnMintPool + [1] 0xNewArbitrumV2BurnMintPool +``` + +2. Check remote pools on the **Arbitrum v2 pool**: + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL DEST_CHAIN=ETHEREUM_SEPOLIA forge script \ + script/configure/remote-pools/GetRemotePools.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +[GetCurrentRateLimits.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/GetCurrentRateLimits.s.sol) + +View the rate limiter query script on GitHub. + +3. (Optional but recommended) Re-check rate limits on the v2 pools: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL + +TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL DEST_CHAIN=ETHEREUM_SEPOLIA forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +[GetLockBox.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/GetLockBox.s.sol) + +View the lockbox query script on GitHub. + +4. Confirm the lockbox is attached to the Sepolia v2 pool and holds the migrated liquidity (`Balance` should equal your `DRAIN_AMOUNT`): + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL forge script \ + script/configure/GetLockBox.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +✅ LockBox: + 0xNewSepoliaLockBox + Token: 0xYourSepoliaToken (CCT) + Balance: 1000000000000000000000 +``` + +### 8. Step 8: Cut over routing (TokenAdminRegistry.setPool) + +This is the migration cutover step. On each chain, update `TokenAdminRegistry` so your token routes through the new v2 +pool. + +> **NOTE: Who must run this step** +> +> The signer running `setPool` must be the token’s `TokenAdminRegistry` +> **administrator** on that chain (as shown by `getTokenConfig` in Step 1). + +[SetPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/SetPool.s.sol) + +View the setPool script on GitHub. + +1. Cut over on **Ethereum Sepolia**: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_SEPOLIA_TOKEN TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL forge script \ + script/setup/SetPool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +✅ Pool set successfully! +``` + +2. Cut over on **Arbitrum Sepolia**: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL forge script \ + script/setup/SetPool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +The order does not matter, because Step 6 configured both pools to recognize the old and new remote pools. + +> **CAUTION: Rollback is harder for Lock & Mint** +> +> You can roll back by calling `setPool` again and pointing back to your v1 pool on a chain. But for Lock & +> Mint this is painful once you have cut over: the Sepolia v1 pool has been drained, so you would first have to re-fund +> it (via `provideLiquidity` as its rebalancer) before it can release again, and liquidity is now split +> between the old pool and the lockbox. Prefer resolving issues **before** the liquidity migration in Step +> 5\. + +### 9. Step 9: Validate end-to-end with ccip-cli + +Send small test transfers in **both** directions and confirm they succeed. + +> **NOTE: Prerequisites for the sends** +> +> 1. You need some of your token on the source chain to send. +> 2. You need Sepolia ETH and Arbitrum Sepolia ETH for gas and fees. + +Set the router addresses (from the [CCIP Directory](https://docs.chain.link/ccip/directory/testnet) or `HelperConfig.s.sol` in the template): + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 +export ARBITRUM_SEPOLIA_ROUTER=0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 +``` + +1. **Ethereum Sepolia → Arbitrum Sepolia** (tokens are **locked** into the lockbox on Sepolia and **minted** on Arbitrum): + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Your output should look something like this: + +```bash filename="Terminal" +🚀 Sending message ... messageId => 0x... +CCIP Explorer: https://ccip.chain.link/msg/0x... +``` + +2. **Arbitrum Sepolia → Ethereum Sepolia** (tokens are **burned** on Arbitrum and **released** from the lockbox on Sepolia): + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia-arbitrum-1 \ + --router $ARBITRUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia \ + --transfer-tokens $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" +``` + +3. (Optional) Enable and request **faster than finality** with block depth 32. + +First, allow block-depth faster than finality on your v2 pools: + +[SetFinalityConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/finality-config/SetFinalityConfig.s.sol) + +View the finality configuration script on GitHub. + +```bash filename="Terminal" +BLOCK_DEPTH=32 TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL forge script \ + script/configure/finality-config/SetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast + +BLOCK_DEPTH=32 TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL forge script \ + script/configure/finality-config/SetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Then send using `--extra finality=32`: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --extra finality=32 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +### 10. Step 10: Confirm your token now routes through the v2 pools + +Re-run the Step 1 checks. You should now see the **v2 pool addresses** as the live `tokenPool` on both chains, and +`typeAndVersion` should report v2. + +1. Ethereum Sepolia: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_SEPOLIA_TOKEN forge script script/setup/GetTokenConfig.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +ADDRESS=$ETHEREUM_SEPOLIA_V2_POOL forge script script/setup/GetTypeAndVersion.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + tokenPool: 0xNewSepoliaV2LockReleasePool + +typeAndVersion: LockReleaseTokenPool 2.0.0 +``` + +2. Arbitrum Sepolia: + +```bash filename="Terminal" +TOKEN=$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN forge script script/setup/GetTokenConfig.s.sol --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +ADDRESS=$ARBITRUM_SEPOLIA_V2_POOL forge script script/setup/GetTypeAndVersion.s.sol --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +Token Config: + tokenPool: 0xNewArbitrumV2BurnMintPool + +typeAndVersion: BurnMintTokenPool 2.0.0 +``` + +### 11. Step 11: Safety checks and cleanup (after v1 in-flight settles) + +After cutover, keep the v1 remote pool addresses configured on the v2 pools until you are confident there are no more +in-flight v1 messages. There is no cost to waiting longer. + +Once you’re ready, remove the **old v1 remote pool addresses** from the v2 pools. + +[RemoveRemotePool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/remote-pools/RemoveRemotePool.s.sol) + +View the remote pool removal script on GitHub. + +1. On the **Sepolia v2 pool**, remove the old Arbitrum v1 pool address: + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL \ + DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + REMOTE_POOL_ADDRESS=$ARBITRUM_SEPOLIA_V1_POOL \ + forge script \ + script/configure/remote-pools/RemoveRemotePool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +2. On the **Arbitrum v2 pool**, remove the old Sepolia v1 pool address: + +```bash filename="Terminal" +TOKEN_POOL=$ARBITRUM_SEPOLIA_V2_POOL \ + DEST_CHAIN=ETHEREUM_SEPOLIA \ + REMOTE_POOL_ADDRESS=$ETHEREUM_SEPOLIA_V1_POOL \ + forge script \ + script/configure/remote-pools/RemoveRemotePool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +> **CAUTION: If you removed too early** +> +> If a v1-sent message is still in-flight and you removed its source pool address, it can fail validation on the +> destination. You can recover by adding the pool address back with `AddRemotePool`, then retry cleanup +> later. + +[AddRemotePool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/remote-pools/AddRemotePool.s.sol) + +View the remote pool add script on GitHub (recovery). + +Recovery example (re-add the old pool address, then retry cleanup later): + +```bash filename="Terminal" +TOKEN_POOL=$ETHEREUM_SEPOLIA_V2_POOL \ + DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + REMOTE_POOL_ADDRESS=$ARBITRUM_SEPOLIA_V1_POOL \ + forge script \ + script/configure/remote-pools/AddRemotePool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +> **NOTE: Optional hardening** +> +> On **Arbitrum Sepolia**, you can revoke mint/burn roles from the old v1 Burn & Mint pool (this is token-specific +> and may require calling role-revocation functions directly on your token contract). On **Ethereum Sepolia**, the old +> v1 Lock & Release pool never held mint/burn roles and is already drained and removed from the +> `TokenAdminRegistry`, so it is inert; no revocation is needed. + +--- + +# Enable your tokens in CCIP (Burn & Mint): Register from an EOA using Foundry +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry +Last Updated: 2025-10-30 + +## Guide Versions + +- [Foundry (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) + +- [Hardhat (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) + +In this tutorial you will: + +1. Deploy a token on two chains using [Foundry](https://book.getfoundry.sh/). +2. Set up a `Burn Mint` token pool on each chain. +3. Configure and activate the pools in CCIP so your token becomes a Cross-Chain Token (CCT). +4. (Optional) Test the setup by minting and transferring tokens across networks. + +> **NOTE: If you do not explicitly configure these features, default behavior applies** +> +> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting +> `finality` or using `finality=finalized`) unless you opt into faster than finality via +> `finality` (for example, `finality=` for block-depth faster than finality). +> 2. **Opt-in changes.** New v2 behavior only applies when you explicitly configure it (for example, +> pool-level fees or faster than finality). + +## Before You Begin + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can’t run `npm`, reinstall/update Node.js from the official installer. + +2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/). You can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli). + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +``` + +Verify the installation by running the following command: + +```bash filename="Terminal" +ccip-cli --version +``` + +3. **Install Foundry**: + If you haven't already, follow the instructions in the [Foundry documentation](https://book.getfoundry.sh/getting-started/installation) to install Foundry. \ + Verify the installation by running the following command: + +```bash filename="Terminal" +forge --version +``` + +4. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-foundry) + +Clone the CCIP 2.0 template for a smoother setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-foundry.git +cd docs-cct-foundry +``` + +5. Create an encrypted Foundry keystore, if you haven't already: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +> **NOTE: Note** +> +> 1. For more information on how to create an encrypted Foundry keystore, refer to the +> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 2. `cast wallet import` will throw an error if a keystore with the same name already exists. +> 3. Use `cast wallet list` to check previously created keystores. + +![Foundry keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-2.png) + +6. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `cast wallet import`) +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required for --verify on Ethereum Sepolia) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/HelperConfig.s.sol) + +View the complete list of supported chains in the `HelperConfig.s.sol` file. + +7. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +8. Build the project: + +```bash filename="Terminal" +npm install && forge build +``` + +### 2. How to configure deployment parameters? + +Deployment parameters can be configured in two ways: + +1. Edit the `.json` files in `script/input/`. +2. Pass environment variables inline at runtime. + +> **Note:** Environment variables take precedence over `.json` file values. + +#### Token Deployment Configuration + +Default values live in `script/input/token.json`: + +```json filename="script/input/token.json" +{ + "name": "BnM Test", + "symbol": "BnM-T", + "decimals": 18, + "maxSupply": 0, + "preMint": 0, + "tokenAmountToMint": 1000000000000000000000, + "tokenAmountToTransfer": 1000000000000000000 +} +``` + +Deployment fields can be overridden with inline env vars: + +| Env var | Overrides (`token.json`) | Optional | Description | +| -------------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------ | +| `TOKEN_NAME` | `.name` | Yes | Token name | +| `TOKEN_SYMBOL` | `.symbol` | Yes | Token symbol | +| `TOKEN_DECIMALS` | `.decimals` | Yes | Decimal places (usually `18`) | +| `TOKEN_MAX_SUPPLY` | `.maxSupply` | Yes | Max supply in smallest unit (`0` = unlimited) | +| `TOKEN_PRE_MINT` | `.preMint` | Yes | Amount pre-minted to the configured recipient (`0` = none) | +| `TOKEN_PRE_MINT_RECIPIENT` | N/A (env-only) | Yes | Address receiving pre-minted tokens. If omitted and `TOKEN_PRE_MINT > 0`, the script defaults to broadcaster | +| `CCIP_ADMIN_ADDRESS` | N/A (env-only) | Yes | Address assigned as CCIP admin on the token. If omitted, broadcaster is used | + +#### (Optional) Advanced Pool Hooks + +> **NOTE: What are advanced pool hooks?** +> +> [Advanced Pool +> Hooks](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) +> are an optional security layer you can attach to a token pool. They run checks: +> +> 1. **before** tokens are locked/burned (source chain) +> 2. **before** tokens are released/minted (destination chain) +> +> Most deployments do **not** need a hook. You would attach one only if you want to: +> +> - Make it so that only certain addresses can transfer your token cross-chain. +> - Put in place additional checks for large transfers above a threshold amount. +> - Plug in an external policy engine for custom compliance checks. + +Default values live in `script/input/advanced-pool-hooks.json`: + +```json filename="script/input/advanced-pool-hooks.json" +{ + "allowlist": [], + "thresholdAmount": 0, + "policyEngine": "0x0000000000000000000000000000000000000000", + "authorizedCallers": [] +} +``` + +All fields can be overridden with inline env vars: + +| Env var | Overrides (`advanced-pool-hooks.json`) | Optional | Description | +| -------------------- | -------------------------------------- | -------- | ---------------------------------------------------------------------------------------- | +| `ALLOWLIST` | `.allowlist` | Yes | Addresses allowed to transfer (CSV or JSON array) | +| `THRESHOLD_AMOUNT` | `.thresholdAmount` | Yes | Threshold amount after which additional hook checks/policy checks may apply (`0` = none) | +| `POLICY_ENGINE` | `.policyEngine` | Yes | Policy engine contract address | +| `AUTHORIZED_CALLERS` | `.authorizedCallers` | Yes | Addresses authorized to call the hooks (CSV or JSON array) | + +> **Note:** To configure an `AdvancedPoolHooks` sender allowlist, follow [Configure a sender allowlist with AdvancedPoolHooks using Foundry](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry). For an overview of the other hook capabilities, read [Advanced Pool Hooks](/ccip/concepts/cross-chain-token/advanced-pool-hooks). + +## Tutorial + +### Prerequisites + +> **CAUTION: Before you continue** +> +> - **Sepolia ETH**: required for Sepolia transactions (deploy/configure/send). +> - **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions (deploy/configure). +> - **LINK (optional)**: if you plan to pay for the cross-chain transfer with `LINK`, fund the sender +> wallet with testnet LINK on the **source chain**. +> - You can use our [faucet](https://faucets.chain.link/) to get testnet LINK. + +**Already have deployments? Skip only what you already have:** + +- If you already have tokens deployed on **both** chains, skip **Deploy Tokens**. +- If you have deployed tokens but not the corresponding pools, you must still run **Deploy Token Pools** before continuing. +- If you already have tokens and **V2-compatible** token pools deployed on **both** chains, skip both sections and move straight on to [Enablement](#enablement). + +**If you already have a V1 token pool deployed and want to use newer features or prepare for lane migration:** + +- You will need to deploy a **V2-compatible token pool** +- Your existing V1 pool can continue to support current transfers +- Follow the migration guide to upgrade your setup without disrupting your existing deployment + +> **Read**: [Migrate from V1 to V2 token pools](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry) + +Before continuing, export the relevant addresses so subsequent scripts can find them: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN= +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN= + +export ETHEREUM_SEPOLIA_TOKEN_POOL= +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL= +``` + +#### 1. Deploy Tokens + +> **TIP: Already have tokens deployed?** +> +> - Your tokens must support burn and mint operations (this tutorial deploys Burn & Mint pools). +> - They must also implement either `getCCIPAdmin()` or OpenZeppelin's `owner()` so the `Claim Admin Role` step can +> identify the token administrator. + +Use the `DeployToken.s.sol` script to deploy [burn-mint ERC-20 tokens](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/interfaces/IBurnMintERC20.sol) on two testnets, Ethereum Sepolia and Arbitrum Sepolia. The deployed token implements the `IBurnMintERC20` interface, which exposes the `mint` and `burn` functions that the token pool later calls during cross-chain transfers. + +> **Note:** The script reads the `script/input/token.json` file to get the token name, symbol, decimals, maximum supply, and pre-mint amount. \ +> You can also override these values by providing inline environment variables at runtime. \ +> See [how to configure deployment parameters](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry#token-deployment-configuration) for more details. + +[DeployToken.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployToken.s.sol) + +View the deployment script on GitHub. + +Optional: If you want to grant mint and burn roles to an address other than the deployer, export `ROLES_RECIPIENT` once: + +```bash filename="Terminal" +export ROLES_RECIPIENT= +``` + +1. Deploy token on Ethereum Sepolia: + +```bash filename="Terminal" +forge script \ + script/deploy/DeployToken.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +> **NOTE: Please note** +> +> 1. `--verify` requires `ETHERSCAN_API_KEY` to be set. +> 2. `ROLES_RECIPIENT` is optional, defaults to deployer's address. +> 3. `CCIP_ADMIN_ADDRESS` is optional, defaults to broadcaster, reused in +> [Claim Admin Role](#claim-admin-role) step. +> 4. `TOKEN_PRE_MINT_RECIPIENT` is optional, applies only when `TOKEN_PRE_MINT > 0`, defaults +> to broadcaster. +> 5. `TOKEN_PRE_MINT` is required to be set to a value greater than 0, only if +> `TOKEN_PRE_MINT_RECIPIENT` is set. + +Your output should look something like this: + +``` + ======================================== + 🪙 Deploy Token + ======================================== + Chain: Ethereum Sepolia + Action: Deploy token + ======================================== + + Token Parameters: + Name: BnM Test + Symbol: BnM-T + Decimals: 18 + Max Supply: 0 + Pre-mint: 0 + + Pre-mint Recipient: 0xYourDesignatedPreMintRecipient + CCIP Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Deploying BnM Test (BnM-T) on Ethereum Sepolia + Token deployed at: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + +[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + ✅ Roles granted successfully! + + ======================================== + ✅ Deployment Complete on Ethereum Sepolia! + ======================================== + Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + + Run this command to set the environment variable: + export ETHEREUM_SEPOLIA_TOKEN=0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + ======================================== +``` + +After deployment, the token address is automatically saved to: + +```bash +script/deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json +``` + +1. The file uses the env var name as the key (for example, `ETHEREUM_SEPOLIA_TOKEN`). + +2. You can copy the key and value directly into an `export` command. + +3. The `script/deployments/` directory is ignored by `.gitignore`, so these files stay local to each user. + +4. Deploy token on Arbitrum Sepolia: + +```bash filename="Terminal" +forge script \ + script/deploy/DeployToken.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +Your output should look something like this: + +``` + ======================================== + 🪙 Deploy Token + ======================================== + Chain: Arbitrum Sepolia + Action: Deploy token + ======================================== + + Token Parameters: + Name: BnM Test + Symbol: BnM-T + Decimals: 18 + Max Supply: 0 + Pre-mint: 0 + + Pre-mint Recipient: 0xYourDesignatedPreMintRecipient + CCIP Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Deploying BnM Test (BnM-T) on Arbitrum Sepolia + Token deployed at: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + +[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + ✅ Roles granted successfully! + + ======================================== + ✅ Deployment Complete on Arbitrum Sepolia! + ======================================== + Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + + Run this command to set the environment variable: + export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + ======================================== +``` + +After deployment, the token address is automatically saved to: + +```bash +script/deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json +``` + +3. Set the environment variables for the deployed token addresses: + +> **NOTE: Why do I need to export these?** +> +> 1. Deployment addresses are persisted locally under `script/deployments/`, but each script still reads +> values from your current shell environment. +> 2. Run the `export` commands so subsequent scripts in this terminal session can discover the correct +> addresses. +> 3. You can also pass the addresses directly as inline environment variables `TOKEN=0x...` / +> `TOKEN_POOL=0x...` on any script that requires them. + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN=0x... +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... +``` + +#### 2. Deploy Token Pools + +Use the `DeployBurnMintTokenPool.s.sol` script to [deploy Burn & Mint token pools](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol) for the tokens on both testnets, Ethereum Sepolia and Arbitrum Sepolia, and grant mint and burn roles to the token pools. + +A `BurnMintTokenPool` burns tokens on the source chain and mints the equivalent amount on the destination chain. For this to work, the pool must hold the token's mint and burn roles, which the script grants automatically after deployment. + +[DeployBurnMintTokenPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployBurnMintTokenPool.s.sol) + +View the token pool deployment script on GitHub. + +> **NOTE: Note** +> +> These scripts deploy `V2` token pools that unlock new features as part of CCIP 2.0. Check out how to use these new +> features in the following sections. + +1. Deploy a Burn & Mint token pool for the deployed token on Ethereum Sepolia: + +```bash filename="Terminal" +forge script \ + script/deploy/DeployBurnMintTokenPool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +Your output should look something like this: + +``` + ======================================== + 🔥⚒️ Deploy Burn & Mint Token Pool + ======================================== + Chain: Ethereum Sepolia + Action: Deploy burn & mint token pool + ======================================== + + Token Pool Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Decimals: 18 + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + RMN Proxy: 0xba3f6251de62dED61Ff98590cB2fDf6871FbB991 + AdvancedPoolHooks: None (0x0) + +[Step 1] Deploying BurnMintTokenPool on Ethereum Sepolia + Token Pool deployed at: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + +[Step 2] Granting mint and burn roles to token pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + ✅ Roles granted successfully! + + ======================================== + ✅ Deployment Complete on Ethereum Sepolia! + ======================================== + Token Pool Address: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + + Run this command to set the environment variable: + export ETHEREUM_SEPOLIA_TOKEN_POOL=0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + ======================================== +``` + +After deployment, the pool address is automatically saved to: + +```bash +script/deployments/token-pools/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-BurnMintTokenPool.json +``` + +2. Deploy a Burn & Mint token pool for the deployed token on Arbitrum Sepolia: + +```bash filename="Terminal" +forge script \ + script/deploy/DeployBurnMintTokenPool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +Your output should look something like this: + +``` + ======================================== + 🔥⚒️ Deploy Burn & Mint Token Pool + ======================================== + Chain: Arbitrum Sepolia + Action: Deploy burn & mint token pool + ======================================== + + Token Pool Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Decimals: 18 + Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + RMN Proxy: 0x9527E2d01A3064ef6b50c1Da1C0cC523803BCFF2 + AdvancedPoolHooks: None (0x0) + +[Step 1] Deploying BurnMintTokenPool on Arbitrum Sepolia + Token Pool deployed at: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + +[Step 2] Granting mint and burn roles to token pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + ✅ Roles granted successfully! + + ======================================== + ✅ Deployment Complete on Arbitrum Sepolia! + ======================================== + Token Pool Address: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + + Run this command to set the environment variable: + export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + ======================================== +``` + +After deployment, the pool address is automatically saved to: + +```bash +script/deployments/token-pools/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-BurnMintTokenPool.json +``` + +> **NOTE: Token role-grant fallback** +> +> 1. The script attempts to call `grantMintAndBurnRoles` on the token to grant the pool mint and burn +> rights. +> 2. If the token does not implement this function, the script prints instructions to grant the roles manually. + +3. Set the environment variables for the deployed token pool addresses: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x... +``` + +### Enablement + +These steps configure your token pools and activate them under the CCIP protocol so your token becomes a fully registered Cross-Chain Token (CCT). + +#### 1. Claim Admin Role + +Use the `ClaimAdmin.s.sol` script to register your EOA as the administrator for the deployed tokens on both chains. \ +This process involves interacting with the [`RegistryModuleOwnerCustom`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol) contract to set up your EOA as the admin. + +> **NOTE: Supported admin patterns** +> +> The script auto-detects how your token exposes its admin and uses the appropriate registration method: +> +> 1. If your token implements `getCCIPAdmin()`, the script calls `registerAdminViaGetCCIPAdmin()` +> . +> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the script calls +> `registerAdminViaOwner()` instead. +> +> No extra configuration is needed; both patterns are fully supported. + +[ClaimAdmin.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/ClaimAdmin.s.sol) + +View the admin claim script on GitHub. + +1. Claim the Admin role for the token on Ethereum Sepolia: + +> **NOTE: Note** +> +> 1. If you do not set `CCIP_ADMIN_ADDRESS`, the script defaults it to the broadcaster (the signer used in +> `--account`). +> 2. If you set `CCIP_ADMIN_ADDRESS` to a different address, the `ClaimAdmin.s.sol` transaction +> must be signed by that same address (use that address's keystore in `--account`), or the claim step +> will revert. + +```bash filename="Terminal" +CCIP_ADMIN_ADDRESS= \ + forge script \ + script/setup/ClaimAdmin.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 👑 Claim Token Admin + ======================================== + Chain: Ethereum Sepolia + Action: Claim token admin + ======================================== + + Claim Admin Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Current Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Expected Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Registry Module: 0xa3c796d480638d7476792230da1E2ADa86e031b0 + Admin Method: getCCIPAdmin() + +[Step 1] Claiming admin for token via getCCIPAdmin() on Ethereum Sepolia + ✅ Admin claimed successfully! + + ======================================== + ✅ Admin Claim Complete on Ethereum Sepolia! + ======================================== + Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + ======================================== +``` + +2. Claim the Admin role for the token on Arbitrum Sepolia: + +```bash filename="Terminal" +CCIP_ADMIN_ADDRESS= \ + forge script \ + script/setup/ClaimAdmin.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 👑 Claim Token Admin + ======================================== + Chain: Arbitrum Sepolia + Action: Claim token admin + ======================================== + + Claim Admin Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Current Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Expected Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Registry Module: 0xaD417c0611dBD225471D31F056b8B6beC1CBC153 + Admin Method: getCCIPAdmin() + +[Step 1] Claiming admin for token via getCCIPAdmin() on Arbitrum Sepolia + ✅ Admin claimed successfully! + + ======================================== + ✅ Admin Claim Complete on Arbitrum Sepolia! + ======================================== + Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + ======================================== +``` + +#### 2. Accept Admin Role + +Use the `AcceptAdminRole.s.sol` script to accept the admin role for the deployed tokens on both chains. \ +Once you have claimed the role, accepting it finalizes your control over the token administration via the [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol). + +[AcceptAdminRole.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/AcceptAdminRole.s.sol) + +View the admin acceptance script on GitHub. + +1. Accept the admin role for the token on Ethereum Sepolia: + +```bash filename="Terminal" +forge script \ + script/setup/AcceptAdminRole.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 👑 Accept Admin Role + ======================================== + Chain: Ethereum Sepolia + Action: Accept admin role + ======================================== + + Accept Admin Role Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 + Pending Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Accepting admin role for token on Ethereum Sepolia + ✅ Admin role accepted successfully! + + ======================================== + ✅ Admin Role Accepted on Ethereum Sepolia! + ======================================== + Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + ======================================== +``` + +2. Accept the admin role for the token on Arbitrum Sepolia: + +```bash filename="Terminal" +forge script \ + script/setup/AcceptAdminRole.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 👑 Accept Admin Role + ======================================== + Chain: Arbitrum Sepolia + Action: Accept admin role + ======================================== + + Accept Admin Role Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Token Admin Registry: 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f + Pending Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Accepting admin role for token on Arbitrum Sepolia + ✅ Admin role accepted successfully! + + ======================================== + ✅ Admin Role Accepted on Arbitrum Sepolia! + ======================================== + Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + ======================================== +``` + +#### 3. Configure Token Pools + +Use the `ApplyChainUpdates.s.sol` script to initialize the token pool configuration on each blockchain to enable cross-chain transfers between the two chains. \ +This involves linking the token pool on one blockchain to its corresponding pool on the other blockchain, allowing cross-chain token transfers.\ +You will interact with the [`TokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) contract by calling the `applyChainUpdates()` function to set up cross-chain parameters and rate limits. + +`applyChainUpdates()` registers the destination chain and the destination pool on the local pool by recording: + +- The remote chain selector (to identify the exact chain), +- The remote pool address (to identify the exact pool on that exact chain), +- The remote token address (to identify the exact token on the remote pool), +- And per-direction rate limit buckets (outbound and inbound). + +You must run it on **both** pools so each side knows its counterpart. + +[ApplyChainUpdates.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/ApplyChainUpdates.s.sol) + +View the pool configuration script on GitHub. + +> **CAUTION: Rate Limiting** +> +> 1. Rate limiting is disabled by default (`isEnabled: false`). With rate limiting disabled, there are **no +> restrictions** on the volume of tokens that can be transferred through the pool in any direction. +> 2. To protect against large, unexpected outflows, set `isEnabled` to `true` and configure `capacity` (maximum bucket +> size) and `rate` (tokens refilled per second). +> 3. See the [Update Rate Limiters](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry) tutorial for +> more details. + +| Env var | Required | Description | +| ------------------------------ | -------- | -------------------------------------------------------------------------------------------------- | +| `OUTBOUND_RATE_LIMIT_CAPACITY` | No | Token bucket capacity for outbound transfers | +| `OUTBOUND_RATE_LIMIT_RATE` | No | Token bucket refill rate (tokens/second) for outbound transfers | +| `OUTBOUND_RATE_LIMIT_ENABLED` | No | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when CAPACITY or RATE are set) | +| `INBOUND_RATE_LIMIT_CAPACITY` | No | Token bucket capacity for inbound transfers | +| `INBOUND_RATE_LIMIT_RATE` | No | Token bucket refill rate (tokens/second) for inbound transfers | +| `INBOUND_RATE_LIMIT_ENABLED` | No | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when CAPACITY or RATE are set) | + +1. Configure the token pool on Ethereum Sepolia: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=100000000000000000 \ + forge script \ + script/setup/ApplyChainUpdates.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 🔗 Apply Chain Updates + ======================================== + Chain: Ethereum Sepolia + Remote Chain: Arbitrum Sepolia + Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Action: Configure cross-chain lane + ======================================== + + Chain Update Parameters: + Source Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Destination Chain Selector: 3478487238524512106 + Destination Chain Family: evm + Destination Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Destination Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Outbound Rate Limit Enabled: true + Outbound Rate Limit Rate: 100000000000000000 + Inbound Rate Limit Enabled: true + Inbound Rate Limit Capacity: 1000000000000000000000 + Inbound Rate Limit Rate: 100000000000000000 + + +[Step 1] Applying chain updates to pool on Ethereum Sepolia + No existing config for destination chain selector; adding new one. + ✅ Chain updates applied successfully! + + ======================================== + ✅ Chain Updates Complete on Ethereum Sepolia! + ======================================== + Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Remote Chain: Arbitrum Sepolia (Selector: 3478487238524512106) + Remote Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Explorer: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + ======================================== +``` + +2. Configure the token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=100000000000000000 \ + forge script \ + script/setup/ApplyChainUpdates.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 🔗 Apply Chain Updates + ======================================== + Chain: Arbitrum Sepolia + Remote Chain: Ethereum Sepolia + Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Action: Configure cross-chain lane + ======================================== + + Chain Update Parameters: + Source Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Destination Chain Selector: 16015286601757825753 + Destination Chain Family: evm + Destination Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Destination Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Outbound Rate Limit Enabled: true + Outbound Rate Limit Rate: 100000000000000000 + Inbound Rate Limit Enabled: true + Inbound Rate Limit Capacity: 1000000000000000000000 + Inbound Rate Limit Rate: 100000000000000000 + + +[Step 1] Applying chain updates to pool on Arbitrum Sepolia + No existing config for destination chain selector; adding new one. + ✅ Chain updates applied successfully! + + ======================================== + ✅ Chain Updates Complete on Arbitrum Sepolia! + ======================================== + Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Remote Chain: Ethereum Sepolia (Selector: 16015286601757825753) + Remote Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Explorer: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + ======================================== +``` + +3. (Optional) Read the list of supported chains and currently configured remote pools: + +```bash filename="Terminal" +forge script \ + script/setup/GetSupportedChains.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +#### 4. (Optional) Configure Faster Than Finality for the Token Pool + +1. CCIP 2.0 supports **faster than finality** as an opt-in feature, letting token issuers control the speed vs. security + tradeoff for cross-chain transfers of their token. + +2. By default, a newly deployed token pool only allows transfers at default finality (equivalent to omitting `finality` + or using `finality=finalized`). + +3. To enable faster than finality, the pool owner must configure which faster than finality modes are allowed. Senders then request + one mode per transfer using `finality` (either `finality=finalized` or `finality=` for block-depth faster than finality). + +> This section applies only to [**token pools with versions ≥ 2.0**](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol). Existing v1 pools do not support fast +> finality. + +> **NOTE: Pool-level finality vs. transaction-level finality** +> +> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via +> `setAllowedFinalityConfig(bytes4)`. +> +> - Default = faster than finality is **disabled**. The pool only allows transfers that use default +> finality (omit `finality` or use `finality=finalized`). +> - The pool can opt in to faster than finality by allowing a minimum block depth (`finality=` +> ). +> 2. **Transaction-level finality** (`finality`): A per-transfer setting the sender specifies +> via extra args when sending a CCIP message. +> +> - `finality=finalized` (or omit `finality`) = default finality. This is always allowed. +> - `finality=` = faster than finality using block depth (block confirmations). Only succeeds +> if the pool allows block-depth mode and the requested `N` is ≥ the pool's configured minimum. +> +> Each transfer requests exactly **one** mode (either default finality or block depth). +> +> Faster Than Finality transfers use the **faster than finality** rate limit bucket when configured. If the +> faster than finality bucket isn’t configured for a lane, the pool falls back to the standard rate limit bucket. + +[SetFinalityConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/finality-config/SetFinalityConfig.s.sol) + +View the pool configuration script on GitHub. + +1. Set the allowed finality config for the token pool on Ethereum Sepolia: + +```bash filename="Terminal" +BLOCK_DEPTH=32 \ + DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=100000000000000000 \ + forge script \ + script/configure/finality-config/SetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + ⏱️ Set Finality Config + ======================================== + Chain: Ethereum Sepolia + Remote Chain: Arbitrum Sepolia + Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Action: Set finality config + ======================================== + + Current Finality Config: 0x00000000 + New Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + + ---------------------------------------- + 📊 Current Rate Limits (faster than finality where enabled, standard otherwise): + ---------------------------------------- + Outbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + + [Step 1] Setting finality config on Ethereum Sepolia + ✅ Finality config set successfully! + + [Step 2] Updating rate limits (faster than finality bucket) on Ethereum Sepolia -> Arbitrum Sepolia + New Configuration: + Outbound Enabled: true + Outbound Capacity: 1000000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 1000000000000000000000 + Inbound Rate: 100000000000000000 + + ✅ Rate limits updated successfully! + + ---------------------------------------- + 📊 Updated Rate Limits (faster than finality where enabled, standard otherwise): + ---------------------------------------- + Outbound [custom finality]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [custom finality]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + + + ======================================== + ✅ Configuration Complete on Ethereum Sepolia! + ======================================== + Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + ======================================== +``` + +2. Set the allowed finality config for the token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +BLOCK_DEPTH=32 \ + DEST_CHAIN=ETHEREUM_SEPOLIA \ + OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=100000000000000000 \ + forge script \ + script/configure/finality-config/SetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + ⏱️ Set Finality Config + ======================================== + Chain: Arbitrum Sepolia + Remote Chain: Ethereum Sepolia + Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Action: Set finality config + ======================================== + + Current Finality Config: 0x00000000 + New Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + + ---------------------------------------- + 📊 Current Rate Limits (faster than finality where enabled, standard otherwise): + ---------------------------------------- + Outbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + + [Step 1] Setting finality config on Arbitrum Sepolia + ✅ Finality config set successfully! + + [Step 2] Updating rate limits (faster than finality bucket) on Arbitrum Sepolia -> Ethereum Sepolia + New Configuration: + Outbound Enabled: true + Outbound Capacity: 1000000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 1000000000000000000000 + Inbound Rate: 100000000000000000 + + ✅ Rate limits updated successfully! + + ---------------------------------------- + 📊 Updated Rate Limits (faster than finality where enabled, standard otherwise): + ---------------------------------------- + Outbound [custom finality]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [custom finality]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + + + ======================================== + ✅ Configuration Complete on Arbitrum Sepolia! + ======================================== + Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + ======================================== +``` + +#### 5. Activate the Token Pool + +Use the `SetPool.s.sol` script to register each token pool in the CCIP [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol), making it the official pool for your token. This is the step that activates the pool in the CCIP protocol. + +[SetPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/SetPool.s.sol) + +View the pool linking script on GitHub. + +1. Link the token to its respective token pool on Ethereum Sepolia: + +```bash filename="Terminal" +forge script \ + script/setup/SetPool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 🏊‍♂️ Set Token Pool + ======================================== + Chain: Ethereum Sepolia + Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Action: Set token pool + ======================================== + + Set Pool Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 + Token Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + + +[Step 1] Setting pool for token on Ethereum Sepolia + ✅ Pool set successfully! + + ======================================== + ✅ Pool Set Complete on Ethereum Sepolia! + ======================================== + Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Pool Address: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Pool Address: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + ======================================== +``` + +2. Link the token to its respective token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +forge script \ + script/setup/SetPool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 🏊‍♂️ Set Token Pool + ======================================== + Chain: Arbitrum Sepolia + Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Action: Set token pool + ======================================== + + Set Pool Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Token Admin Registry: 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f + Token Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + + +[Step 1] Setting pool for token on Arbitrum Sepolia + ✅ Pool set successfully! + + ======================================== + ✅ Pool Set Complete on Arbitrum Sepolia! + ======================================== + Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Pool Address: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Pool Address: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + ======================================== +``` + +### Cross-chain Token Operations + +If all the previous steps were successful, your CCT token is now fully registered and activated under the CCIP protocol. +Use these next steps to mint and transfer your tokens across networks. + +#### 1. Mint Tokens + +Use the `MintTokens.s.sol` script to mint tokens to your Externally Owned Account (EOA) on Ethereum Sepolia. \ +Since you granted mint and burn privileges to your EOA during the token deployment in the first step, you are authorized to mint tokens for testing purposes. \ +This ensures that your EOA has sufficient tokens to perform cross-chain transfers in the next step. + +[MintTokens.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/operations/MintTokens.s.sol) + +View the minting script on GitHub. + +1. Mint tokens for your EOA on Ethereum Sepolia: + +> **NOTE: Note** +> +> 1. If you don't set `AMOUNT` as an environment variable, the script will use the amount from the `tokenAmountToMint` +> field in your `script/input/token.json` file. +> 2. Set `MINT_RECEIVER` to mint to a different address (defaults to the EOA broadcasting the transaction). + +```bash filename="Terminal" +AMOUNT=1000000000000000000000 \ + forge script \ + script/operations/MintTokens.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 💰 Mint Tokens + ======================================== + Chain: Ethereum Sepolia + Action: Mint tokens + ======================================== + + Mint Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Symbol: BnM-T + Amount: 1000000000000000000000 + Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + + +[Step 1] Minting 1000000000000000000000 BnM-T to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + ✅ Tokens minted successfully! + + ======================================== + ✅ Minting Complete on Ethereum Sepolia! + ======================================== + Receiver Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Receiver Address: https://sepolia.etherscan.io/address/0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + New Balance: 1000000000000000000000 BnM-T + ======================================== +``` + +#### 2. Transfer Tokens Across Networks + +Use the `ccip-cli` command to securely transfer tokens across networks from your terminal. +Tokens will be: + +1. **burned** on the source chain (Ethereum Sepolia), and, +2. **minted** on the destination chain (Arbitrum Sepolia). \\ + +You can choose to pay CCIP fees using LINK tokens or the native gas token. + +> **Note:** +> +> 1. Add `--fee-token LINK` to pay CCIP fees in LINK. If you omit this flag, fees are paid in the native gas token. +> 2. Use `--receiver
    ` for a custom recipient. If omitted on EVM-to-EVM, the receiver defaults to the sender. + +The CLI command expects the router address to be set in the environment variables. You can set it using the following command: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x... +``` + +> **NOTE: Router address** +> +> You can find the router address for your chain in the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). + +> **NOTE: Token amounts in this tutorial** +> +> Earlier Foundry scripts use smallest-unit amounts (wei-like). In the `ccip-cli` examples below we use a +> compact, human-readable token amount (for example, `1.23`). + +1. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia on **default finality**: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Your output should look something like this: + +``` +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +2. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia using **faster than finality**: + +This assumes you enabled faster than finality on the pool in the optional finality configuration accordion above. +When you pass `--extra finality=...`, `ccip-cli` encodes it into the CCIP message extra args. + +Transfer with explicit finalized finality (`finality=finalized`): + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --extra finality=finalized \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Your output should look something like this: + +``` +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0xeee045c912703291e6d6166e4ef8bfb0a7fb9035af1a41ac00b65c58ad868a94 , messageId => 0x5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4 +CCIP Explorer: https://ccip.chain.link/msg/0x5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4 +``` + +Faster Than Finality using numeric block depth (`finality=`): + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --extra finality=7 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +> **NOTE: Understand Minimum Block Depth Requirements 🧑‍🏫** +> +> 1. The last command failed because the pool's **allowed finality config** requires a larger block depth +> for numeric faster than finality (for example, if you configured `BLOCK_DEPTH=32`) but we requested +> `finality=7`. +> 2. Try running the command again with an appropriate value for `finality` (for example, +> `--extra finality=20`), or omit `finality` to use default finality ( +> `finality=finalized`). +> 3. If you see a revert like `InvalidRequestedFinality(requestedFinality, allowedFinality)`, it means the +> requested mode (or depth) is not permitted by the pool. + +#### 3. (Optional) Hand Off Token Administration After Enablement + +1. The process to set up your tokens for cross-chain transfers is complete. At this stage, you might want to hand off token administration to a different address, for example, transferring control to a multisig or a dedicated operations wallet + This new admin will be able to mint and transfer tokens as needed. +2. Use the `TransferTokenAdminRole.s.sol` script to achieve this. + This is a **two-step process**: the current admin initiates the transfer, and the new admin must run `AcceptAdminRole.s.sol` to complete it. Until the new admin accepts, the current admin retains full control. + +[TransferTokenAdminRole.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/TransferTokenAdminRole.s.sol) + +View the transfer admin script on GitHub. + +1. Initiate the transfer on Ethereum Sepolia: + +```bash filename="Terminal" +NEW_ADMIN=0x... \ + forge script \ + script/setup/TransferTokenAdminRole.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +``` + ======================================== + 🔄 Transfer Token Admin Role + ======================================== + Chain: Ethereum Sepolia + Action: Transfer admin role + ======================================== + + Transfer Admin Role Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 + Current Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Pending Administrator: 0x0000000000000000000000000000000000000000 + New Admin: 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 + Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Transferring admin role for token on Ethereum Sepolia + ✅ Admin role transfer initiated successfully! + + ======================================== + ✅ Admin Role Transfer Initiated on Ethereum Sepolia! + ======================================== + Token: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + New Admin: 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 + ======================================== + + ℹ️ The new admin (0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3) must run AcceptAdminRole to complete the transfer. +``` + +> **NOTE: Note** +> +> 1. Set `TOKEN=0x...` to specify the token explicitly. If omitted, the script reads the deployed token +> address from the environment variable for the current chain. +> 2. Only the current admin can initiate this transfer. If the signer is not the admin, the script will revert with an +> error. + +2. Repeat the same command on Arbitrum Sepolia if you also want to transfer admin rights on the destination chain: + +```bash filename="Terminal" +NEW_ADMIN=0x... \ + forge script \ + script/setup/TransferTokenAdminRole.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +3. The new admin completes the transfer by running the `AcceptAdminRole` script for both chains. + Set `NEW_ADMIN_KEYSTORE_NAME` to the Foundry keystore name that the new admin created via `cast wallet import`: + +```bash filename="Terminal" +export NEW_ADMIN_KEYSTORE_NAME=new_admin_keystore_name + +forge script \ + script/setup/AcceptAdminRole.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $NEW_ADMIN_KEYSTORE_NAME \ + --broadcast + +forge script \ + script/setup/AcceptAdminRole.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $NEW_ADMIN_KEYSTORE_NAME \ + --broadcast +``` + +--- + +# Enable your tokens in CCIP (Burn & Mint): Register from an EOA using Hardhat +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat +Last Updated: 2026-04-24 + +## Guide Versions + +- [Foundry (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) + +- [Hardhat (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) + +In this tutorial you will: + +1. Deploy a token on two chains using [Hardhat](https://hardhat.org/). +2. Set up a `Burn Mint` token pool on each chain. +3. Configure and activate the pools in CCIP so your token becomes a Cross-Chain Token (CCT). +4. (Optional) Test the setup by minting and transferring tokens across networks. + +> **NOTE: If you do not explicitly configure these features, default behavior applies** +> +> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting +> `finality` or using `finality=finalized`) unless you opt into faster than finality via +> `finality` (for example, `finality=` for block-depth faster than finality). +> 2. **Opt-in changes.** New v2 behavior only applies when you explicitly configure it (for example, +> pool-level fees or faster than finality). + +## Before You Begin + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can’t run `npm`, reinstall/update Node.js from the official installer. + +2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/), you can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli). + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +``` + +Verify the installation by running the following command: + +```bash filename="Terminal" +ccip-cli --version +``` + +3. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-hardhat) + +Clone the CCIP 2.0 Hardhat template for a smoother setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-hardhat.git +cd docs-cct-hardhat +``` + +4. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `npx hardhat keystore set`) +KEYSTORE_NAME= + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment tasks on Ethereum Sepolia) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/helper-config.ts) + +View the complete list of supported chains in the `helper-config.ts` file. + +5. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +6. Build the project: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +7. Create an encrypted Hardhat keystore, if you haven't already: + +```bash filename="Terminal" +npx hardhat keystore set +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +> **NOTE: Note** +> +> 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it +> asks you to enter your private key as hidden text. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in +> `.env`. +> 3. Use `npx hardhat keystore list` to check previously created keystores. + +### 2. How to configure deployment parameters? + +Deployment parameters can be configured in two ways: + +1. Edit the `.json` files in `input/`. +2. Pass CLI flags inline at runtime. + +> **Note:** CLI flags take precedence over `.json` file values. + +#### Token Deployment Configuration + +Default values live in `input/token.json`: + +```json filename="input/token.json" +{ + "name": "BnM Test", + "symbol": "BnM-T", + "decimals": 18, + "maxSupply": 0, + "preMint": 0, + "tokenAmountToMint": 1000000000000000000000, + "tokenAmountToTransfer": 1000000000000000000 +} +``` + +Deployment fields can be overridden with CLI flags: + +| Flag | Overrides (`token.json`) | Description | +| ------------- | ------------------------ | --------------------------------------------- | +| `--name` | `.name` | Token name | +| `--symbol` | `.symbol` | Token symbol | +| `--decimals` | `.decimals` | Decimal places (usually `18`) | +| `--maxsupply` | `.maxSupply` | Max supply in smallest unit (`0` = unlimited) | +| `--premint` | `.preMint` | Amount pre-minted to deployer (`0` = none) | + +#### (Optional) Advanced Pool Hooks + +> **NOTE: What are advanced pool hooks?** +> +> [Advanced Pool +> Hooks](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) +> are an optional security layer you can attach to a token pool. They run checks: +> +> 1. **before** tokens are locked/burned (source chain) +> 2. **before** tokens are released/minted (destination chain) +> +> Most deployments do **not** need a hook. You would attach one only if you want to: +> +> - Make it so that only certain addresses can transfer your token cross-chain. +> - Put in place additional checks for large transfers above a threshold amount. +> - Plug in an external policy engine for custom compliance checks. + +Default values live in `input/advanced-pool-hooks.json`: + +```json filename="input/advanced-pool-hooks.json" +{ + "allowlist": [], + "thresholdAmount": 0, + "policyEngine": "0x0000000000000000000000000000000000000000", + "authorizedCallers": [] +} +``` + +All fields can be overridden with CLI flags: + +| Flag | Overrides (`advanced-pool-hooks.json`) | Description | +| --------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------- | +| `--allowlist` | `.allowlist` | Addresses allowed to transfer (comma-separated) | +| `--thresholdamount` | `.thresholdAmount` | Threshold amount after which additional hook checks/policy checks may apply (`0` = none) | +| `--policyengine` | `.policyEngine` | Policy engine contract address | +| `--authorizedcallers` | `.authorizedCallers` | Addresses authorized to call the hooks (comma-separated) | + +> **Note:** To configure an `AdvancedPoolHooks` sender allowlist, follow [Configure a sender allowlist with AdvancedPoolHooks using Hardhat](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat). For an overview of the other hook capabilities, read [Advanced Pool Hooks](/ccip/concepts/cross-chain-token/advanced-pool-hooks). + +## Tutorial + +### Prerequisites + +> **CAUTION: Before you continue** +> +> - **Sepolia ETH**: required for Sepolia transactions (deploy/configure/send). +> - **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions (deploy/configure). +> - **LINK (optional)**: if you plan to pay for the cross-chain transfer with `LINK`, fund the sender +> wallet with testnet LINK on the **source chain**. +> - You can use our [faucet](https://faucets.chain.link/) to get testnet LINK. + +**Already have deployments? Skip only what you already have:** + +- If you already have tokens deployed on **both** chains, skip **Deploy Tokens**. +- If you have deployed tokens but not the corresponding pools, you must still run **Deploy Token Pools** before continuing. +- If you already have tokens and **V2-compatible** token pools deployed on **both** chains, skip both sections and move straight on to [Enablement](#enablement). + +**If you already have a V1 token pool deployed and want to use newer features or prepare for lane migration:** + +- You will need to deploy a **V2-compatible token pool** +- Your existing V1 pool can continue to support current transfers +- Follow the migration guide to upgrade your setup without disrupting your existing deployment + +> **Read**: [Migrate from V1 to V2 token pools](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat) + +Before continuing, export the relevant addresses so subsequent tasks can find them: + +```bash filename="Terminal" +# For tokens +export ETHEREUM_SEPOLIA_TOKEN= +export ARBITRUM_SEPOLIA_TOKEN= + +# For token pools +export ETHEREUM_SEPOLIA_TOKEN_POOL= +export ARBITRUM_SEPOLIA_TOKEN_POOL= +``` + +#### 1. Deploy Tokens + +> **TIP: Already have tokens deployed?** +> +> - Your tokens must support burn and mint operations (this tutorial deploys Burn & Mint pools). +> - They must also implement either `getCCIPAdmin()` or OpenZeppelin's `owner()` so the `Claim Admin Role` step can +> identify the token administrator. + +Use the `deployToken` task to deploy [burn-mint ERC-20 tokens](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/interfaces/IBurnMintERC20.sol) on two testnets, Ethereum Sepolia and Arbitrum Sepolia. The deployed token implements the `IBurnMintERC20` interface, which exposes the `mint` and `burn` functions that the token pool later calls during cross-chain transfers. + +> **Note:** The task reads the `input/token.json` file to get the token name, symbol, decimals, maximum supply, and pre-mint amount. \ +> You can also override these values by providing CLI flags at runtime. \ +> See [how to configure deployment parameters](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat#token-deployment-configuration) for more details. + +[deployToken.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/deploy/deployToken.ts) + +View the deployment task on GitHub. + +1. Deploy token on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat deployToken --network sepolia --rolesrecipient --verify +``` + +> **NOTE: Please note** +> +> 1. `--verify` requires `ETHERSCAN_API_KEY` to be set. +> 2. `--rolesrecipient` is an optional flag. If not provided, the task will use the deployer's address as +> the roles recipient. + +Your output should look something like this: + +``` +======================================== +🪙 Deploy Token +======================================== +Chain: Ethereum Sepolia +Action: Deploy CrossChainToken +======================================== + +Token Parameters: + Name: BnM Test + Symbol: BnM-T + Decimals: 18 + Max Supply: 0 + Pre-Mint: 0 + + Pre-mint Recipient: 0xYourdesignatedAddressForPreMint + CCIP Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Deploying BnM Test (BnM-T) on Ethereum Sepolia +⏳ Deployment tx: 0x7d2f5d7f4b7c9f3b5b8f6a4b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d + Waiting for 2 confirmation(s)... +Token deployed at: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + +[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Waiting for 2 confirmation(s)... +✅ Roles granted successfully! + +Verifying contract on block explorer... +✅ Contract verified successfully + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + +Run this command to set the environment variable: +export ETHEREUM_SEPOLIA_TOKEN=0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +======================================== +``` + +After deployment, the token address is automatically saved to: + +```bash +deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json +``` + +1. The file uses the env var name as the key (for example, `ETHEREUM_SEPOLIA_TOKEN`). + +2. You can copy the key and value directly into an `export` command. + +3. The `deployments/` directory is ignored by `.gitignore`, so these files stay local to each user. + +4. Deploy token on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat deployToken --network arbitrumSepolia --rolesrecipient --verify +``` + +Your output should look something like this: + +``` +======================================== +🪙 Deploy Token +======================================== +Chain: Arbitrum Sepolia +Action: Deploy CrossChainToken +======================================== + +Token Parameters: + Name: BnM Test + Symbol: BnM-T + Decimals: 18 + Max Supply: 0 + Pre-Mint: 0 + + Pre-mint Recipient: 0xYourdesignatedAddressForPreMint + CCIP Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Deploying BnM Test (BnM-T) on Arbitrum Sepolia +⏳ Deployment tx: 0x2b6d9a1c4f3e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c + Waiting for 2 confirmation(s)... +Token deployed at: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + +[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Waiting for 2 confirmation(s)... +✅ Roles granted successfully! + +Verifying contract on block explorer... +✅ Contract verified successfully + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + +Run this command to set the environment variable: +export ARBITRUM_SEPOLIA_TOKEN=0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +======================================== +``` + +After deployment, the token address is automatically saved to: + +```bash +deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json +``` + +3. Set the environment variables for the deployed token addresses: + +> **NOTE: Why do I need to export these?** +> +> 1. Deployment addresses are persisted locally under `deployments/`, but each task still reads values from +> your current shell environment. +> 2. Run the `export` commands so subsequent tasks in this terminal session can discover the correct +> addresses. +> 3. You can also pass the addresses directly via `--tokenaddress` / `--tokenpool` flags on any task that requires +> them. + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN=0x... +export ARBITRUM_SEPOLIA_TOKEN=0x... +``` + +#### 2. Deploy Token Pools + +Use the `deployTokenPool` task to [deploy Burn & Mint token pools](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol) for the tokens on both testnets, Ethereum Sepolia and Arbitrum Sepolia, and grant mint and burn roles to the token pools. + +A `BurnMintTokenPool` burns tokens on the source chain and mints the equivalent amount on the destination chain. For this to work, the pool must hold the token's mint and burn roles, which the task grants automatically after deployment. + +[deployBurnMintTokenPool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/deploy/deployBurnMintTokenPool.ts) + +View the deployment task on GitHub. + +1. Deploy a Burn & Mint token pool on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat deployTokenPool --network sepolia --verify +``` + +Your output should look something like this: + +``` +======================================== +🔥⚒️ Deploy Burn & Mint Token Pool +======================================== +Chain: Ethereum Sepolia +Action: Deploy BurnMintTokenPool +======================================== + +Token Pool Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Decimals: 18 + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + RMN Proxy: 0xba3f6251de62dED61Ff98590cB2fDf6871FbB991 + Pool Hooks: None (0x0) + +[Step 1] Deploying BurnMintTokenPool on Ethereum Sepolia +⏳ Deployment tx: 0x8a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0fedcba987654321 + Waiting for 2 confirmation(s)... +Token Pool deployed at: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + +[Step 2] Granting mint and burn roles to token pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Waiting for 2 confirmation(s)... +✅ Roles granted successfully! + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Token Pool Address: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + +Run this command to set the environment variable: +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== + +Verifying contract on block explorer... +✅ Contract verified successfully +``` + +2. Deploy a Burn & Mint token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat deployTokenPool --network arbitrumSepolia --verify +``` + +Your output should look something like this: + +``` +======================================== +🔥⚒️ Deploy Burn & Mint Token Pool +======================================== +Chain: Arbitrum Sepolia +Action: Deploy BurnMintTokenPool +======================================== + +Token Pool Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Decimals: 18 + Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + RMN Proxy: 0x9527E2d01A3064ef6b50c1Da1C0cC523803BCFF2 + Pool Hooks: None (0x0) + +[Step 1] Deploying BurnMintTokenPool on Arbitrum Sepolia +⏳ Deployment tx: 0x6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d + Waiting for 2 confirmation(s)... +Token Pool deployed at: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + +[Step 2] Granting mint and burn roles to token pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Waiting for 2 confirmation(s)... +✅ Roles granted successfully! + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Token Pool Address: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + +Run this command to set the environment variable: +export ARBITRUM_SEPOLIA_TOKEN_POOL=0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== + +Verifying contract on block explorer... +✅ Contract verified successfully +``` + +> **NOTE: Token role-grant fallback** +> +> 1. The task attempts to call `grantMintAndBurnRoles` on the token to grant the pool mint and burn rights. +> 2. If the token does not implement this function, the task prints instructions to grant the roles manually. + +3. Set the environment variables for the deployed token pool addresses: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +export ARBITRUM_SEPOLIA_TOKEN_POOL=0x... +``` + +### Enablement + +These steps configure your token pools and activate them under the CCIP protocol so your token becomes a truly Cross-Chain Token (CCT). + +#### 1. Claim Admin Role + +Use the `claimAdmin` task to register your EOA as the administrator for the deployed tokens on both chains. \ +This process involves interacting with the [`RegistryModuleOwnerCustom`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol) contract to set up your EOA as the admin. + +> **NOTE: Supported admin patterns** +> +> The task auto-detects how your token exposes its admin and uses the appropriate registration method: +> +> 1. If your token implements `getCCIPAdmin()`, the task calls `registerAdminViaGetCCIPAdmin()`. +> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the task calls +> `registerAdminViaOwner()` instead. +> +> No extra configuration is needed: both patterns are fully supported. + +[claimAdmin.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/claimAdmin.ts) + +View the admin claim task on GitHub. + +1. Claim the Admin role for the token on Ethereum Sepolia: + +> **NOTE: Note** +> +> If you don't pass `--ccipadmin`, the task defaults to the address of the account broadcasting the +> transaction. + +```bash filename="Terminal" +npx hardhat claimAdmin --network sepolia --ccipadmin +``` + +Your output should look something like this: + +``` +======================================== +👑 Claim Token Admin +======================================== +Chain: Ethereum Sepolia +Action: Claim token admin +======================================== + +Claim Admin Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Current Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Expected Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Registry Module: 0xa3c796d480638d7476792230da1E2ADa86e031b0 + Admin Method: getCCIPAdmin() + +[Step 1] Claiming admin via getCCIPAdmin() on Ethereum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809 + +======================================== +✅ Admin Claim Complete on Ethereum Sepolia! +======================================== +Token: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Transaction: https://sepolia.etherscan.io/tx/0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809 +Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +2. Claim the Admin role for the token on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat claimAdmin --network arbitrumSepolia --ccipadmin +``` + +Your output should look something like this: + +``` +======================================== +👑 Claim Token Admin +======================================== +Chain: Arbitrum Sepolia +Action: Claim token admin +======================================== + +Claim Admin Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Current Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Expected Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Registry Module: 0xaD417c0611dBD225471D31F056b8B6beC1CBC153 + Admin Method: getCCIPAdmin() + +[Step 1] Claiming admin via getCCIPAdmin() on Arbitrum Sepolia +⏳ Tx: https://sepolia.arbiscan.io/tx/0x9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8 + +======================================== +✅ Admin Claim Complete on Arbitrum Sepolia! +======================================== +Token: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Transaction: https://sepolia.arbiscan.io/tx/0x9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8 +Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +#### 2. Accept Admin Role + +Use the `acceptAdminRole` task to accept the admin role for the deployed tokens on both chains. +Once you have claimed the role, accepting the role finalizes your control over the token administration via the [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol). + +[acceptAdminRole.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/acceptAdminRole.ts) + +View the admin acceptance task on GitHub. + +1. Accept the admin role for the token on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat acceptAdminRole --network sepolia +``` + +Your output should look something like this: + +``` +======================================== +👑 Accept Admin Role +======================================== +Chain: Ethereum Sepolia +Action: Accept admin role +======================================== + +Accept Admin Role Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 + Pending Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Accepting admin role for token on Ethereum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0 + +======================================== +✅ Admin Role Accepted on Ethereum Sepolia! +======================================== +Token: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Transaction: https://sepolia.etherscan.io/tx/0x0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0 +New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +2. Accept the admin role for the token on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat acceptAdminRole --network arbitrumSepolia +``` + +Your output should look something like this: + +``` +======================================== +👑 Accept Admin Role +======================================== +Chain: Arbitrum Sepolia +Action: Accept admin role +======================================== + +Accept Admin Role Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Token Admin Registry: 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f + Pending Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Accepting admin role for token on Arbitrum Sepolia +⏳ Tx: https://sepolia.arbiscan.io/tx/0xa0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5 + +======================================== +✅ Admin Role Accepted on Arbitrum Sepolia! +======================================== +Token: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Transaction: https://sepolia.arbiscan.io/tx/0xa0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5 +New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +#### 3. Configure Token Pools + +Use the `applyChainUpdates` task to initialize the token pool configuration on each blockchain to enable cross-chain transfers between the two chains. \ +This involves linking the token pool on one blockchain to its corresponding pool on the other blockchain, allowing cross-chain token transfers.\ +You will interact with the [`TokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) contract by calling the `applyChainUpdates()` function to set up cross-chain parameters and rate limits. + +`applyChainUpdates()` registers the destination chain and the destination pool on the local pool by recording: + +- The remote chain selector (to identify the exact chain), +- The remote pool address (to identify the exact pool on that exact chain), +- The remote token address (to identify the exact token on the remote pool), +- And per-direction rate limit buckets (outbound and inbound). + +You must run it on **both** pools so each side knows its counterpart. + +[applyChainUpdates.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/applyChainUpdates.ts) + +View the pool configuration task on GitHub. + +> **CAUTION: Rate Limiting** +> +> 1. Rate limiting is disabled by default (`isEnabled: false`). With rate limiting disabled, there are **no +> restrictions** on the volume of tokens that can be transferred through the pool in any direction. +> 2. To protect against large, unexpected outflows, set `isEnabled` to `true` and configure `capacity` (maximum bucket +> size) and `rate` (tokens refilled per second). +> 3. See the [Update Rate Limiters](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-hardhat) tutorial for +> more details. + +| Flag | Required | Description | +| -------------------- | -------- | ------------------------------------------------------------------- | +| `--destchain` | Yes | Destination chain name (e.g. `arbitrumSepolia`, `ARBITRUM_SEPOLIA`) | +| `--outboundcapacity` | No | Token bucket capacity for outbound transfers | +| `--outboundrate` | No | Token bucket refill rate (tokens/second) for outbound transfers | +| `--inboundcapacity` | No | Token bucket capacity for inbound transfers | +| `--inboundrate` | No | Token bucket refill rate (tokens/second) for inbound transfers | + +1. Configure the token pool on Ethereum Sepolia: + +```bash filename="Terminal" +# Configure Ethereum Sepolia → Arbitrum Sepolia with rate limits +npx hardhat applyChainUpdates \ + --destchain arbitrumSepolia \ + --outboundcapacity 1000000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 1000000000000000000000 \ + --inboundrate 100000000000000000 \ + --network sepolia +``` + +Your output should look something like this: + +``` +======================================== +🔗 Apply Chain Updates +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Configure cross-chain lane +======================================== + +Chain Update Parameters: + Source Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Destination Chain Selector: 3478487238524512106 + Destination Chain Family: evm + Destination Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Destination Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Outbound Rate Limit Enabled: true + Outbound Rate Limit Capacity: 1000000000000000000000 + Outbound Rate Limit Rate: 100000000000000000 + Inbound Rate Limit Enabled: true + Inbound Rate Limit Capacity: 1000000000000000000000 + Inbound Rate Limit Rate: 100000000000000000 + +⚠️ Existing config detected for destination chain selector; replacing it. +[Step 1] Applying chain updates to pool on Ethereum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x6cf4ac6987c82c018b3ea8e39f133cdc736e8e85a14669c765fe99e8ebdfbaf9 + +======================================== +✅ Chain Updates Complete on Ethereum Sepolia! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Explorer: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Transaction: https://sepolia.etherscan.io/tx/0x6cf4ac6987c82c018b3ea8e39f133cdc736e8e85a14669c765fe99e8ebdfbaf9 +Remote Chain: Arbitrum Sepolia (Selector: 3478487238524512106) +Remote Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +2. Configure the token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +# Configure Arbitrum Sepolia → Ethereum Sepolia with rate limits +npx hardhat applyChainUpdates \ + --destchain sepolia \ + --outboundcapacity 1000000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 1000000000000000000000 \ + --inboundrate 100000000000000000 \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +``` +======================================== +🔗 Apply Chain Updates +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Configure cross-chain lane +======================================== + +Chain Update Parameters: + Source Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Destination Chain Selector: 16015286601757825753 + Destination Chain Family: evm + Destination Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Destination Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Outbound Rate Limit Enabled: true + Outbound Rate Limit Capacity: 1000000000000000000000 + Outbound Rate Limit Rate: 100000000000000000 + Inbound Rate Limit Enabled: true + Inbound Rate Limit Capacity: 1000000000000000000000 + Inbound Rate Limit Rate: 100000000000000000 + +⚠️ Existing config detected for destination chain selector; replacing it. +[Step 1] Applying chain updates to pool on Arbitrum Sepolia +⏳ Tx: https://sepolia.arbiscan.io/tx/0x7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6 + +======================================== +✅ Chain Updates Complete on Arbitrum Sepolia! +======================================== +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Explorer: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Transaction: https://sepolia.arbiscan.io/tx/0x7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6 +Remote Chain: Ethereum Sepolia (Selector: 16015286601757825753) +Remote Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +3. (Optional) Read the list of supported chains and currently configured remote pools: + +```bash filename="Terminal" +npx hardhat getSupportedChains --network sepolia +npx hardhat getSupportedChains --network arbitrumSepolia +``` + +#### 4. (Optional) Configure Faster Than Finality for the Token Pool + +1. CCIP 2.0 supports **faster than finality** as an opt-in feature, letting token issuers control the speed vs. security + tradeoff for cross-chain transfers of their token. + +2. By default, a newly deployed token pool only allows transfers at default finality (equivalent to omitting `finality` + or using `finality=finalized`). + +3. To enable faster than finality, the pool owner must configure which faster than finality modes are allowed. Senders then request + one mode per transfer using `finality` (either `finality=finalized` or `finality=` for block-depth faster than finality). + +> This section applies only to [**token pools with versions ≥ 2.0**](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol). Existing v1 pools do not support fast +> finality. + +> **NOTE: Pool-level finality vs. transaction-level finality** +> +> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via +> `setAllowedFinalityConfig(bytes4)`. +> +> - Default = faster than finality is **disabled**. The pool only allows transfers that use default +> finality (omit `finality` or use `finality=finalized`). +> - The pool can opt in to faster than finality by allowing a minimum block depth (`finality=` +> ). +> 2. **Transaction-level finality** (`finality`): A per-transfer setting the sender specifies +> via extra args when sending a CCIP message. +> +> - `finality=finalized` (or omit `finality`) = default finality. This is always allowed. +> - `finality=` = faster than finality using block depth (block confirmations). Only succeeds +> if the pool allows block-depth mode and the requested `N` is ≥ the pool's configured minimum. +> +> Each transfer requests exactly **one** finality mode. +> +> Faster Than Finality transfers use the **faster than finality** rate limit bucket when configured. If the +> faster than finality bucket isn’t configured for a lane, the pool falls back to the standard rate limit bucket. + +[setFinalityConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/finality-config/setFinalityConfig.ts) + +View the pool configuration task on GitHub. + +1. Set the allowed finality config for the token pool on Ethereum Sepolia: + +```bash filename="Terminal" +# Allow block-depth faster than finality and optionally configure the faster than finality rate limit bucket +npx hardhat setFinalityConfig \ + --blockdepth 32 \ + --destchain arbitrumSepolia \ + --outboundcapacity 1000000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 1000000000000000000000 \ + --inboundrate 100000000000000000 \ + --network sepolia +``` + +Your output should look something like this: + +``` +======================================== +⏱️ Set Finality Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set finality config +======================================== + + Current Finality Config: 0x00000014 + New Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + +---------------------------------------- +📊 Current Rate Limits (faster than finality where enabled, standard otherwise): +---------------------------------------- + Outbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + +[Step 1] Setting finality config on Ethereum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x280399cd3dce1815a30ca3763736c7cbc4a8018bdb3ba1b650e728479640f5b6 +✅ Finality config set successfully! + +[Step 2] Updating rate limits (faster than finality bucket) on Ethereum Sepolia → Arbitrum Sepolia +New Configuration: + Outbound Enabled: true + Outbound Capacity: 1000000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 1000000000000000000000 + Inbound Rate: 100000000000000000 + +⏳ Tx: https://sepolia.etherscan.io/tx/0xa69245f229f41313d597ea452fa7c9d8ead9e05bfe36aad5a997a6850d198ac5 +✅ Rate limits updated successfully! + +---------------------------------------- +📊 Updated Rate Limits (faster than finality where enabled, standard otherwise): +---------------------------------------- + Outbound [faster than finality]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [faster than finality]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + +======================================== +✅ Configuration Complete on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) +======================================== +``` + +2. Set the allowed finality config for the token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +# Allow block-depth faster than finality and optionally configure the faster than finality rate limit bucket +npx hardhat setFinalityConfig \ + --blockdepth 32 \ + --destchain sepolia \ + --outboundcapacity 1000000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 1000000000000000000000 \ + --inboundrate 100000000000000000 \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +``` +======================================== +⏱️ Set Finality Config +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Set finality config +======================================== + + Current Finality Config: 0x00000014 + New Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + +---------------------------------------- +📊 Current Rate Limits (faster than finality where enabled, standard otherwise): +---------------------------------------- + Outbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + +[Step 1] Setting finality config on Arbitrum Sepolia +⏳ Tx: https://sepolia.arbiscan.io/tx/0xb1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6 +✅ Finality config set successfully! + +[Step 2] Updating rate limits (faster than finality bucket) on Arbitrum Sepolia → Ethereum Sepolia +New Configuration: + Outbound Enabled: true + Outbound Capacity: 1000000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 1000000000000000000000 + Inbound Rate: 100000000000000000 + +⏳ Tx: https://sepolia.arbiscan.io/tx/0x8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7 +✅ Rate limits updated successfully! + +---------------------------------------- +📊 Updated Rate Limits (faster than finality where enabled, standard otherwise): +---------------------------------------- + Outbound [faster than finality]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [faster than finality]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + +======================================== +✅ Configuration Complete on Arbitrum Sepolia! +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) +======================================== +``` + +#### 5. Activate the Token Pool + +Use the `setPool` task to register each token pool in the CCIP [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol), making it the official pool for your token. This is the step that activates the pool in the CCIP protocol. + +[setPool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/setPool.ts) + +View the pool linking task on GitHub. + +1. Link the token to its respective token pool on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat setPool --network sepolia +``` + +Your output should look something like this: + +``` +======================================== +🏊 Set Token Pool +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set token pool +======================================== + +Set Pool Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 + Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 + Token Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Setting pool for token on Ethereum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x4abdea890bd094239b3931134e8001511f82fcadfc7554b7facd98987c0e55c5 + +======================================== +✅ Pool Set Complete on Ethereum Sepolia! +======================================== +Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Pool Address: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Transaction: https://sepolia.etherscan.io/tx/0x4abdea890bd094239b3931134e8001511f82fcadfc7554b7facd98987c0e55c5 +======================================== +``` + +2. Link the token to its respective token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat setPool --network arbitrumSepolia +``` + +Your output should look something like this: + +``` +======================================== +🏊 Set Token Pool +======================================== +Chain: Arbitrum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Set token pool +======================================== + +Set Pool Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Token Admin Registry: 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f + Token Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Setting pool for token on Arbitrum Sepolia +⏳ Tx: https://sepolia.arbiscan.io/tx/0x6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5 + +======================================== +✅ Pool Set Complete on Arbitrum Sepolia! +======================================== +Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Pool Address: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Transaction: https://sepolia.arbiscan.io/tx/0x6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5 +======================================== +``` + +### Cross-chain Token Operations + +If all the previous steps were successful, your CCT token is now fully registered and activated under the CCIP protocol. +Use these next steps to mint and transfer your tokens across networks. + +#### 1. Mint Tokens + +Use the `mintTokens` task to mint tokens to your Externally Owned Account (EOA) on Ethereum Sepolia. \ +Since you granted mint and burn privileges to your EOA during the token deployment in the first step, you are authorized to mint tokens for testing purposes. \ +This ensures that your EOA has sufficient tokens to perform cross-chain transfers in the next step. + +[mintTokens.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/operations/mintTokens.ts) + +View the minting task on GitHub. + +1. Mint tokens for your EOA on Ethereum Sepolia: + +> **NOTE: Note** +> +> 1. If you don't pass `--amount`, the task will use the amount from the `tokenAmountToMint` +> field in your `input/token.json` file. +> 2. Pass `--receiver 0x...` to mint to a different address (defaults to the deployer). + +```bash filename="Terminal" +npx hardhat mintTokens --amount 1000000000000000000000 --network sepolia +``` + +Your output should look something like this: + +``` +======================================== +💰 Mint Tokens +======================================== +Chain: Ethereum Sepolia +Action: Mint tokens +======================================== + +Mint Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Symbol: BnM-T + Amount: 1000000000000000000000 + Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Minting 1000000000000000000000 BnM-T to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +⏳ Tx: https://sepolia.etherscan.io/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef +✅ Tokens minted successfully! + +======================================== +✅ Minting Complete on Ethereum Sepolia! +======================================== +Receiver Address: https://sepolia.etherscan.io/address/0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +New Balance: 1000000000000000000000 BnM-T +======================================== +``` + +#### 2. Transfer Tokens Across Networks + +Use the `ccip-cli` command to securely transfer tokens across networks from your terminal. +Tokens will be: + +1. **burned** on the source chain (Ethereum Sepolia), and, +2. **minted** on the destination chain (Arbitrum Sepolia). \\ + +You can choose to pay CCIP fees using LINK tokens or the native gas token. + +> **Note:** +> +> 1. Add `--fee-token LINK` to pay CCIP fees in LINK. If you omit this flag, fees are paid in the native gas token. +> 2. Use `--receiver
    ` for a custom recipient. If omitted on EVM-to-EVM, the receiver defaults to the sender. + +The CLI command expects the router address to be set in the environment variables. You can set it using the following command: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x... +``` + +> **NOTE: Router address** +> +> You can find the router address for your chain in the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). + +1. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia on **default finality**: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Your output should look something like this: + +``` +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +> **NOTE: chainId vs. chainSelector** +> +> The `chainId` column shows the EVM chain ID (for example, `11155111` for Sepolia). The +> `chainSelector` is CCIP's own chain identifier used internally by the protocol. You do not need to set +> chain selectors yourself; the CLI and tasks resolve them automatically from the chain name. + +2. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia using **faster than finality**: + +This assumes you enabled faster than finality on the pool in the optional finality configuration accordion above. +When you pass `--extra finality=...`, `ccip-cli` encodes it into the CCIP message extra args. + +Transfer with explicit finalized finality (`finality=finalized`): + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --extra finality=finalized \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Your output should look something like this: + +``` +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0xeee045c912703291e6d6166e4ef8bfb0a7fb9035af1a41ac00b65c58ad868a94 , messageId => 0x1dd7414db95d6c0f81e15780b93e3cda6df0c58fb32ff81a2c0468e952929264 +CCIP Explorer: https://ccip.chain.link/msg/0x1dd7414db95d6c0f81e15780b93e3cda6df0c58fb32ff81a2c0468e952929264 +``` + +Faster Than Finality using numeric block depth (`finality=`): + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --extra finality=7 \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` + +Your output should look something like this: + +``` +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +error: Failed to call "ccipSend" +Error = execution reverted: InvalidRequestedFinality(requestedFinality, allowedFinality) +Revert.error = InvalidRequestedFinality(uint32 requestedFinality, uint32 allowedFinality) +Revert.requestedFinality = 7n +Revert.allowedFinality = 32n +``` + +> **NOTE: Understand Minimum Block Depth Requirements 🧑‍🏫** +> +> 1. The last command failed because the pool's **allowed finality config** requires a larger block depth +> for numeric faster than finality (for example, if you configured `--blockdepth 32`) but we requested +> `finality=7`. +> 2. Try running the command again with an appropriate value for `finality` (for example, +> `--extra finality=20`), or omit `finality` to use default finality ( +> `finality=finalized`). +> 3. If you see a revert like `InvalidRequestedFinality(requestedFinality, allowedFinality)`, it means the +> requested mode (or depth) is not permitted by the pool. + +#### 3. (Optional) Hand Off Token Administration After Enablement + +1. The process to set up your tokens for cross-chain transfers is complete. At this stage, you might want to hand off token administration to a different address, for example, transferring control to a multisig or a dedicated operations wallet + This new admin will be able to mint and transfer tokens as needed. +2. Use the `transferTokenAdminRole` task to achieve this. + This is a **two-step process**: the current admin initiates the transfer, and the new admin must run `AcceptAdminRole` task to complete it. Until the new admin accepts, the current admin retains full control. + +[transferTokenAdminRole.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/transferTokenAdminRole.ts) + +View the transfer admin role task on GitHub. + +1. Initiate the transfer on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat transferTokenAdminRole --newadmin new_admin_address --network sepolia +``` + +Your output should look something like this: + +``` +======================================== +🔄 Transfer Token Admin Role +======================================== +Chain: Ethereum Sepolia +Action: Transfer admin role +======================================== + +Transfer Admin Role Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 + Current Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Pending Administrator: 0x0000000000000000000000000000000000000000 + New Admin: 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 + Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Transferring admin role for token on Ethereum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 + +======================================== +✅ Admin Role Transfer Initiated on Ethereum Sepolia! +======================================== +Token: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Transaction: https://sepolia.etherscan.io/tx/0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 +New Admin: 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 +======================================== + +ℹ️ The new admin (0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3) must call acceptAdminRole to complete the transfer. +``` + +> **NOTE: Note** +> +> 1. Pass `--tokenaddress 0x...` to specify the token explicitly. If omitted, the task reads the deployed +> token address from the environment variable for the current chain. +> 2. Only the current admin can initiate this transfer. If the signer is not the admin, the task will fail with an +> error. + +2. Repeat the same command on Arbitrum Sepolia if you also want to transfer admin rights on the destination chain: + +```bash filename="Terminal" +npx hardhat transferTokenAdminRole --newadmin new_admin_address --network arbitrumSepolia +``` + +3. The new admin completes the transfer by calling `acceptAdminRole`: + +```bash filename="Terminal" +npx hardhat acceptAdminRole --network sepolia +npx hardhat acceptAdminRole --network arbitrumSepolia +``` + +--- + +# Enable your tokens in CCIP (Lock & Mint): Register from an EOA using Foundry +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry +Last Updated: 2025-10-30 + +## Guide Versions + +- [Foundry (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) + +- [Hardhat (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) + +In this tutorial you will: + +1. Deploy a token on two chains using [Foundry](https://book.getfoundry.sh/). +2. Set up a `Lock & Release` token pool on the source chain and a `Burn Mint` token pool on the destination chain. +3. Configure and activate the pools in CCIP so your token becomes a Cross-Chain Token (CCT). +4. (Optional) Test the setup by minting and transferring tokens across networks. + +> **NOTE: If you do not explicitly configure these features, default behavior applies** +> +> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting +> `finality` or using `finality=finalized`) unless you opt into faster than finality via +> `finality` (for example, `finality=32`). +> 2. **Opt-in changes.** New v2 behavior only applies when you explicitly configure it (for example, +> pool-level fees or faster than finality). + +## Before You Begin + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + +2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/). You can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli). + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +``` + +Verify the installation by running the following command: + +```bash filename="Terminal" +ccip-cli --version +``` + +3. **Install Foundry**: + If you haven't already, follow the instructions in the [Foundry documentation](https://book.getfoundry.sh/getting-started/installation) to install Foundry. \ + Verify the installation by running the following command: + +```bash filename="Terminal" +forge --version +``` + +4. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-foundry) + +Clone the CCIP 2.0 template for a smoother setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-foundry.git +cd docs-cct-foundry +``` + +5. Create an encrypted Foundry keystore, if you haven't already: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +> **NOTE: Note** +> +> 1. For more information on how to create an encrypted Foundry keystore, refer to the +> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 2. `cast wallet import` will throw an error if a keystore with the same name already exists. +> 3. Use `cast wallet list` to check previously created keystores. + +![Foundry keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-2.png) + +6. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `cast wallet import`) +KEYSTORE_NAME=your_keystore_name + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/HelperConfig.s.sol) + +View the complete list of supported chains in the `HelperConfig.s.sol` file. + +7. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +8. Build the project: + +```bash filename="Terminal" +npm install && forge build +``` + +### 2. How to configure deployment parameters? + +Deployment parameters can be configured in two ways: + +1. Edit the `.json` files in `script/input/`. +2. Pass environment variables inline at runtime. + +> **Note:** Environment variables take precedence over `.json` file values. + +#### Token Deployment Configuration + +Default values live in `script/input/token.json`: + +```json filename="script/input/token.json" +{ + "name": "BnM Test", + "symbol": "BnM-T", + "decimals": 18, + "maxSupply": 0, + "preMint": 0, + "tokenAmountToMint": 1000000000000000000000, + "tokenAmountToTransfer": 1000000000000000000 +} +``` + +Deployment fields can be overridden with inline env vars: + +| Env var | Overrides (`token.json`) | Optional | Description | +| -------------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------ | +| `TOKEN_NAME` | `.name` | Yes | Token name | +| `TOKEN_SYMBOL` | `.symbol` | Yes | Token symbol | +| `TOKEN_DECIMALS` | `.decimals` | Yes | Decimal places (usually `18`) | +| `TOKEN_MAX_SUPPLY` | `.maxSupply` | Yes | Max supply in smallest unit (`0` = unlimited) | +| `TOKEN_PRE_MINT` | `.preMint` | Yes | Amount pre-minted to the configured recipient (`0` = none) | +| `TOKEN_PRE_MINT_RECIPIENT` | N/A (env-only) | Yes | Address receiving pre-minted tokens. If omitted and `TOKEN_PRE_MINT > 0`, the script defaults to broadcaster | +| `CCIP_ADMIN_ADDRESS` | N/A (env-only) | Yes | Address assigned as CCIP admin on the token. If omitted, broadcaster is used | + +#### (Optional) Advanced Pool Hooks + +> **NOTE: What are advanced pool hooks?** +> +> [Advanced Pool +> Hooks](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) +> are an optional security layer you can attach to a token pool. They run checks: +> +> 1. **before** tokens are locked/burned (source chain) +> 2. **before** tokens are released/minted (destination chain) +> +> Most deployments do **not** need a hook. You would attach one only if you want to: +> +> - Make it so that only certain addresses can transfer your token cross-chain. +> - Put in place additional checks for large transfers above a threshold amount. +> - Plug in an external policy engine for custom compliance checks. + +Default values live in `script/input/advanced-pool-hooks.json`: + +```json filename="script/input/advanced-pool-hooks.json" +{ + "allowlist": [], + "thresholdAmount": 0, + "policyEngine": "0x0000000000000000000000000000000000000000", + "authorizedCallers": [] +} +``` + +All fields can be overridden with inline env vars: + +| Env var | Overrides (`advanced-pool-hooks.json`) | Optional | Description | +| -------------------- | -------------------------------------- | -------- | ---------------------------------------------------------------------------------------- | +| `ALLOWLIST` | `.allowlist` | Yes | Addresses allowed to transfer (CSV or JSON array) | +| `THRESHOLD_AMOUNT` | `.thresholdAmount` | Yes | Threshold amount after which additional hook checks/policy checks may apply (`0` = none) | +| `POLICY_ENGINE` | `.policyEngine` | Yes | Policy engine contract address | +| `AUTHORIZED_CALLERS` | `.authorizedCallers` | Yes | Addresses authorized to call the hooks (CSV or JSON array) | + +> **Note:** To configure an `AdvancedPoolHooks` sender allowlist, follow [Configure a sender allowlist with AdvancedPoolHooks using Foundry](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry). For an overview of the other hook capabilities, read [Advanced Pool Hooks](/ccip/concepts/cross-chain-token/advanced-pool-hooks). + +## Tutorial + +### Prerequisites + +> **CAUTION: Before you continue** +> +> - **Sepolia ETH**: required for Sepolia transactions (deploy/configure/send). +> - **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions (deploy/configure). +> - **LINK (optional)**: if you plan to pay for the cross-chain transfer with `LINK`, fund the sender +> wallet with testnet LINK on the **source chain**. +> - You can use our [faucet](https://faucets.chain.link/) to get testnet LINK. + +**Already have deployments? Skip only what you already have:** + +- If you already have tokens deployed on **both** chains, skip **Deploy Tokens**. +- If you have deployed tokens but not the corresponding pools and lockbox, you must still run **Deploy Token Pools** before continuing. +- If you already have tokens deployed on **both** chains, **V2-compatible** token pools deployed on **both** chains, and the lockbox deployed on the **Lock & Release** side (Ethereum Sepolia), skip both sections and move straight on to [Enablement](#enablement). + +**If you already have a V1 token pool deployed and want to use newer features or prepare for lane migration:** + +- You will need to deploy a **V2-compatible token pool** +- Your existing V1 pool can continue to support current transfers +- Follow the migration guide to upgrade your setup without disrupting your existing deployment + +> **Read**: [Migrate from V1 to V2 token pools](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-lock-mint-foundry) + +Before continuing, export the relevant addresses so subsequent scripts can find them: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN= +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN= + +export ETHEREUM_SEPOLIA_TOKEN_POOL= +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL= +``` + +#### 1. Deploy Tokens + +> **TIP: Already have tokens deployed?** +> +> - Your source chain token (canonical token) does **NOT** need to support burn and mint (this tutorial deploys a Lock +> & Release pool on the source chain). Your destination chain token **MUST** support burn and mint operations (this +> tutorial deploys a Burn & Mint pool on the destination chain). +> - Both tokens must implement either `getCCIPAdmin()` or OpenZeppelin's `owner()` so the `Claim Admin Role` step can +> identify the token administrator. + +Use the `DeployToken.s.sol` script to deploy your tokens on two testnets, Ethereum Sepolia and Arbitrum Sepolia. The destination chain token (Arbitrum Sepolia) implements the [`IBurnMintERC20`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/interfaces/IBurnMintERC20.sol) interface, which exposes the `mint` and `burn` functions that the Burn & Mint token pool later calls during cross-chain transfers. The source chain token (Ethereum Sepolia) does not need burn/mint support; it is locked in the lockbox by the Lock & Release pool. + +> **Note:** The script reads the `script/input/token.json` file to get the token name, symbol, decimals, maximum supply, and pre-mint amount. \ +> You can also override these values by providing inline environment variables at runtime. \ +> See [how to configure deployment parameters](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#token-deployment-configuration) for more details. + +[DeployToken.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployToken.s.sol) + +View the deployment script on GitHub. + +Optional: If you want to grant mint and burn roles to an address other than the deployer, export `ROLES_RECIPIENT` once: + +```bash filename="Terminal" +export ROLES_RECIPIENT= +``` + +1. Deploy token on Ethereum Sepolia: + +```bash filename="Terminal" +forge script \ + script/deploy/DeployToken.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +> **NOTE: Please note** +> +> 1. `--verify` requires `ETHERSCAN_API_KEY` to be set. +> 2. `ROLES_RECIPIENT` is an optional environment variable. If not provided, the script will use the +> deployer's address as the roles recipient. +> 3. `CCIP_ADMIN_ADDRESS` is optional and sets who becomes CCIP admin at deploy time. If not set, the +> broadcaster is used. This same variable is reused in the [Claim Admin Role](#claim-admin-role) step. +> 4. `TOKEN_PRE_MINT_RECIPIENT` is optional and applies only when `TOKEN_PRE_MINT > 0`. If not +> set, pre-minted tokens default to the broadcaster. +> 5. `TOKEN_PRE_MINT` is required to be set to a value greater than 0, only if +> `TOKEN_PRE_MINT_RECIPIENT` is set. + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🪙 Deploy Token +======================================== +Chain: Ethereum Sepolia +Action: Deploy token +======================================== + +Token Parameters: +Name: BnM Test +Symbol: BnM-T +Decimals: 18 +Max Supply: 0 +Pre-mint: 0 + +Pre-mint Recipient: 0xYourDesignatedPreMintRecipient +CCIP Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Deploying BnM Test (BnM-T) on Ethereum Sepolia +Token deployed at: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + +[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +✅ Roles granted successfully! + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +======================================== +``` + +After deployment, the token address is automatically saved to: + +```bash +script/deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json +``` + +1. The file uses the env var name as the key (for example, `ETHEREUM_SEPOLIA_TOKEN`). + +2. You can copy the key and value directly into an `export` command. + +3. The `script/deployments/` directory is ignored by `.gitignore`, so these files stay local to each user. + +4. Deploy token on Arbitrum Sepolia: + +```bash filename="Terminal" +forge script \ + script/deploy/DeployToken.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🪙 Deploy Token +======================================== +Chain: Arbitrum Sepolia +Action: Deploy token +======================================== + +Token Parameters: +Name: BnM Test +Symbol: BnM-T +Decimals: 18 +Max Supply: 0 +Pre-mint: 0 + +Pre-mint Recipient: 0xYourDesignatedPreMintRecipient +CCIP Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Deploying BnM Test (BnM-T) on Arbitrum Sepolia +Token deployed at: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + +[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +✅ Roles granted successfully! + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +======================================== +``` + +After deployment, the token address is automatically saved to: + +```bash +script/deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json +``` + +3. Set the environment variables for the deployed token addresses: + +> **NOTE: Why do I need to export these?** +> +> 1. Deployment addresses are persisted locally under `script/deployments/`, but each script still reads +> values from your current shell environment. +> 2. Run the `export` commands so subsequent scripts in this terminal session can discover the correct +> addresses. +> 3. You can also pass the addresses directly as inline environment variables `TOKEN=0x...` / +> `TOKEN_POOL=0x...` on any script that requires them. + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN=0x... +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... +``` + +#### 2. Deploy Token Pools + +This tutorial deploys: + +- A Lock & Release token pool on the source chain (Ethereum Sepolia) +- A Burn & Mint token pool on the destination chain (Arbitrum Sepolia) +- An `ERC20LockBox` to hold token liquidity on behalf of the Lock & Release token pool on the source chain. +- On the destination chain, the tokens are simply burned or minted as required by the Burn & Mint token pool. + +> **NOTE: Note** +> +> These scripts deploy `V2` token pools that unlock new features as part of CCIP 2.0. Check out how to use these new +> features in the following sections. + +##### Deploy ERC20LockBox (Ethereum Sepolia) + +The [`ERC20LockBox`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/ERC20LockBox.sol) holds token liquidity on behalf of the [`LockReleaseTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/LockReleaseTokenPool.sol). It must be deployed before the pool. + +[DeployERC20LockBox.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployERC20LockBox.s.sol) + +View the lockbox deployment script on GitHub. + +1. Deploy the ERC20LockBox on Ethereum Sepolia: + +```bash filename="Terminal" +forge script \ + script/deploy/DeployERC20LockBox.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +📦 Deploy ERC20 LockBox +======================================== +Chain: Ethereum Sepolia +Action: Deploy ERC20 lockbox +======================================== + +ERC20LockBox Parameters: +Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Authorized Callers: None (add after deploying the token pool) + +[Step 1] Deploying ERC20LockBox on Ethereum Sepolia +ERC20LockBox deployed at: 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +https://sepolia.etherscan.io/address/0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +✅ ERC20LockBox deployed successfully! + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +ERC20LockBox Address: 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +https://sepolia.etherscan.io/address/0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b + +Copy this address to use in the next commands: +LOCK_BOX=0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +======================================== +``` + +After deployment, the lockbox address is automatically saved to: + +```bash +script/deployments/lock-boxes/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-LockBox.json +``` + +2. Set the environment variable for the deployed lockbox address: + +```bash filename="Terminal" +export LOCK_BOX=0x... +``` + +##### Deploy Lock & Release Token Pool (Ethereum Sepolia) + +Use this script to [deploy a Lock & Release token pool](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/LockReleaseTokenPool.sol) on the source chain. A `LockReleaseTokenPool` locks tokens in the `ERC20LockBox` on the source chain during outbound transfers and releases escrowed tokens during inbound transfers. + +[DeployLockReleaseTokenPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployLockReleaseTokenPool.s.sol) + +View the Lock & Release token pool deployment script on GitHub. + +1. Deploy a Lock & Release token pool for the deployed token on Ethereum Sepolia: + +```bash filename="Terminal" +LOCK_BOX=$LOCK_BOX \ + forge script \ + script/deploy/DeployLockReleaseTokenPool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔐 Deploy Lock & Release Token Pool +======================================== +Chain: Ethereum Sepolia +Action: Deploy lock & release token pool +======================================== + +Token Pool Parameters: +Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Decimals: 18 +Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 +RMN Proxy: 0xba3f6251de62dED61Ff98590cB2fDf6871FbB991 +LockBox: 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +AdvancedPoolHooks: None (0x0) + +[Step 1] Deploying LockReleaseTokenPool on Ethereum Sepolia +Token Pool deployed at: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +✅ LockReleaseTokenPool deployed successfully! + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Token Pool Address: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +======================================== +``` + +After deployment, the pool address is automatically saved to: + +```bash +script/deployments/token-pools/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-LockReleaseTokenPool.json +``` + +2. Set the environment variable for the deployed pool address: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +``` + +##### Authorize Pool on LockBox (Ethereum Sepolia) + +The pool must be authorized as a caller on the lockbox before it can deposit and withdraw tokens during cross-chain transfers. + +[UpdateAuthorizedCallers.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol) + +View the authorized callers script on GitHub. + +1. Authorize the token pool on the lockbox: + +```bash filename="Terminal" +LOCK_BOX=$LOCK_BOX \ + ADD_ADDRESSES=$ETHEREUM_SEPOLIA_TOKEN_POOL \ + forge script \ + script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +📝 Update Authorized Callers +======================================== +Chain: Ethereum Sepolia +LockBox: 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +Action: Update authorized callers +======================================== + +Adding 1 caller(s): +[0] 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 + +======================================== +✅ Authorized callers updated on Ethereum Sepolia! +======================================== +LockBox: 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +LockBox: https://sepolia.etherscan.io/address/0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +======================================== +``` + +##### Deploy Burn & Mint Token Pool (Arbitrum Sepolia) + +Use this script to [deploy a Burn & Mint token pool](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol) on the destination chain. A `BurnMintTokenPool` burns tokens on the source chain and mints the equivalent amount on the destination chain, so the pool must hold the destination token's mint and burn roles, which the script grants automatically after deployment. + +[DeployBurnMintTokenPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployBurnMintTokenPool.s.sol) + +View the Burn & Mint token pool deployment script on GitHub. + +1. Deploy a Burn & Mint token pool for the deployed token on Arbitrum Sepolia: + +```bash filename="Terminal" +forge script \ + script/deploy/DeployBurnMintTokenPool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔥⚒️ Deploy Burn & Mint Token Pool +======================================== +Chain: Arbitrum Sepolia +Action: Deploy burn & mint token pool +======================================== + +Token Pool Parameters: +Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Decimals: 18 +Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 +RMN Proxy: 0x9527E2d01A3064ef6b50c1Da1C0cC523803BCFF2 +AdvancedPoolHooks: None (0x0) + +[Step 1] Deploying BurnMintTokenPool on Arbitrum Sepolia +Token Pool deployed at: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + +[Step 2] Granting mint and burn roles to token pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +✅ Roles granted successfully! + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Token Pool Address: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +After deployment, the pool address is automatically saved to: + +```bash +script/deployments/token-pools/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-BurnMintTokenPool.json +``` + +> **NOTE: Token role-grant fallback** +> +> 1. The script attempts to call `grantMintAndBurnRoles` on the token to grant the pool mint and burn +> rights. +> 2. If the token does not implement this function, the script prints instructions to grant the roles manually. + +##### Set environment variables + +Set the environment variables for the deployed token pool addresses: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x... +``` + +### Enablement + +These steps configure your token pools and activate them under the CCIP protocol so your token becomes a fully registered Cross-Chain Token (CCT). + +#### 1. Claim Admin Role + +Use the `ClaimAdmin.s.sol` script to register your EOA as the administrator for the deployed tokens on both chains. \ +This process involves interacting with the [`RegistryModuleOwnerCustom`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol) contract to set up your EOA as the admin. + +> **NOTE: Supported admin patterns** +> +> The script auto-detects how your token exposes its admin and uses the appropriate registration method: +> +> 1. If your token implements `getCCIPAdmin()`, the script calls `registerAdminViaGetCCIPAdmin()` +> . +> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the script calls +> `registerAdminViaOwner()` instead. +> +> No extra configuration is needed;both patterns are fully supported. + +[ClaimAdmin.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/ClaimAdmin.s.sol) + +View the admin claim script on GitHub. + +1. Claim the Admin role for the token on Ethereum Sepolia: + +> **NOTE: Note** +> +> 1. If you do not set `CCIP_ADMIN_ADDRESS`, the script defaults it to the broadcaster (the signer used in +> `--account`). +> 2. If you set `CCIP_ADMIN_ADDRESS` to a different address, the `ClaimAdmin.s.sol` transaction +> must be signed by that same address (use that address's keystore in `--account`), or the claim step +> will revert. + +```bash filename="Terminal" +CCIP_ADMIN_ADDRESS= \ + forge script \ + script/setup/ClaimAdmin.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +👑 Claim Token Admin +======================================== +Chain: Ethereum Sepolia +Action: Claim token admin +======================================== + +Claim Admin Parameters: +Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Current Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Expected Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Registry Module: 0xa3c796d480638d7476792230da1E2ADa86e031b0 +Admin Method: getCCIPAdmin() + +[Step 1] Claiming admin for token via getCCIPAdmin() on Ethereum Sepolia +✅ Admin claimed successfully! + +======================================== +✅ Admin Claim Complete on Ethereum Sepolia! +======================================== +Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +2. Claim the Admin role for the token on Arbitrum Sepolia: + +```bash filename="Terminal" +CCIP_ADMIN_ADDRESS= \ + forge script \ + script/setup/ClaimAdmin.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +👑 Claim Token Admin +======================================== +Chain: Arbitrum Sepolia +Action: Claim token admin +======================================== + +Claim Admin Parameters: +Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Current Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Expected Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Registry Module: 0xaD417c0611dBD225471D31F056b8B6beC1CBC153 +Admin Method: getCCIPAdmin() + +[Step 1] Claiming admin for token via getCCIPAdmin() on Arbitrum Sepolia +✅ Admin claimed successfully! + +======================================== +✅ Admin Claim Complete on Arbitrum Sepolia! +======================================== +Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +#### 2. Accept Admin Role + +Use the `AcceptAdminRole.s.sol` script to accept the admin role for the deployed tokens on both chains. \ +Once you have claimed the role, accepting it finalizes your control over the token administration via the [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol). + +[AcceptAdminRole.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/AcceptAdminRole.s.sol) + +View the admin acceptance script on GitHub. + +1. Accept the admin role for the token on Ethereum Sepolia: + +```bash filename="Terminal" +forge script \ + script/setup/AcceptAdminRole.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +👑 Accept Admin Role +======================================== +Chain: Ethereum Sepolia +Action: Accept admin role +======================================== + +Accept Admin Role Parameters: +Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 +Pending Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Accepting admin role for token on Ethereum Sepolia +✅ Admin role accepted successfully! + +======================================== +✅ Admin Role Accepted on Ethereum Sepolia! +======================================== +Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +2. Accept the admin role for the token on Arbitrum Sepolia: + +```bash filename="Terminal" +forge script \ + script/setup/AcceptAdminRole.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +👑 Accept Admin Role +======================================== +Chain: Arbitrum Sepolia +Action: Accept admin role +======================================== + +Accept Admin Role Parameters: +Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Token Admin Registry: 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f +Pending Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Accepting admin role for token on Arbitrum Sepolia +✅ Admin role accepted successfully! + +======================================== +✅ Admin Role Accepted on Arbitrum Sepolia! +======================================== +Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +#### 3. Configure Token Pools + +Use the `ApplyChainUpdates.s.sol` script to link each token pool to its counterpart on the other chain and configure rate limits. \ +You will interact with the [`TokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) contract by calling the `applyChainUpdates()` function to enable cross-chain transfers. + +`applyChainUpdates()` registers the destination chain and the destination pool on the local pool by recording: + +- The remote chain selector (to identify the exact chain), +- The remote pool address (to identify the exact pool on that exact chain), +- The remote token address (to identify the exact token on the remote pool), +- And per-direction rate limit buckets (outbound and inbound). + +You must run it on **both** pools so each side knows its counterpart. + +[ApplyChainUpdates.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/ApplyChainUpdates.s.sol) + +View the pool configuration script on GitHub. + +> **CAUTION: Rate Limiting** +> +> 1. Rate limiting is disabled by default (`isEnabled: false`). With rate limiting disabled, there are **no +> restrictions** on the volume of tokens that can be transferred through the pool in any direction. +> 2. To protect against large, unexpected outflows, set `isEnabled` to `true` and configure `capacity` (maximum bucket +> size) and `rate` (tokens refilled per second). +> 3. See the [Update Rate Limiters](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry) tutorial for +> more details. + +| Env var | Required | Description | +| ------------------------------ | -------- | -------------------------------------------------------------------------------------------------- | +| `OUTBOUND_RATE_LIMIT_CAPACITY` | No | Token bucket capacity for outbound transfers | +| `OUTBOUND_RATE_LIMIT_RATE` | No | Token bucket refill rate (tokens/second) for outbound transfers | +| `OUTBOUND_RATE_LIMIT_ENABLED` | No | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when CAPACITY or RATE are set) | +| `INBOUND_RATE_LIMIT_CAPACITY` | No | Token bucket capacity for inbound transfers | +| `INBOUND_RATE_LIMIT_RATE` | No | Token bucket refill rate (tokens/second) for inbound transfers | +| `INBOUND_RATE_LIMIT_ENABLED` | No | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when CAPACITY or RATE are set) | + +1. Configure the token pool on Ethereum Sepolia: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=100000000000000000 \ + forge script \ + script/setup/ApplyChainUpdates.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔗 Apply Chain Updates +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Action: Configure cross-chain lane +======================================== + +Chain Update Parameters: +Source Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Destination Chain Selector: 3478487238524512106 +Destination Chain Family: evm +Destination Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Destination Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Outbound Rate Limit Enabled: true +Outbound Rate Limit Capacity: 1000000000000000000000 +Outbound Rate Limit Rate: 100000000000000000 +Inbound Rate Limit Enabled: true +Inbound Rate Limit Capacity: 1000000000000000000000 +Inbound Rate Limit Rate: 100000000000000000 + +[Step 1] Applying chain updates on Ethereum Sepolia +✅ Chain updates applied successfully! + +======================================== +✅ Chain Updates Complete on Ethereum Sepolia! +======================================== +Token Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Token Pool: https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Remote Chain: Arbitrum Sepolia (Selector: 3478487238524512106) +Remote Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +2. Configure the token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=100000000000000000 \ + forge script \ + script/setup/ApplyChainUpdates.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔗 Apply Chain Updates +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Configure cross-chain lane +======================================== + +Chain Update Parameters: +Source Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Destination Chain Selector: 16015286601757825753 +Destination Chain Family: evm +Destination Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Destination Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Outbound Rate Limit Enabled: true +Outbound Rate Limit Capacity: 1000000000000000000000 +Outbound Rate Limit Rate: 100000000000000000 +Inbound Rate Limit Enabled: true +Inbound Rate Limit Capacity: 1000000000000000000000 +Inbound Rate Limit Rate: 100000000000000000 + +[Step 1] Applying chain updates on Arbitrum Sepolia +✅ Chain updates applied successfully! + +======================================== +✅ Chain Updates Complete on Arbitrum Sepolia! +======================================== +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Remote Chain: Ethereum Sepolia (Selector: 16015286601757825753) +Remote Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +======================================== +``` + +3. (Optional) Read the list of supported chains and currently configured remote pools: + +```bash filename="Terminal" +forge script \ + script/setup/GetSupportedChains.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +#### 4. (Optional) Configure Faster Than Finality for the Token Pool + +1. CCIP 2.0 supports **faster than finality** as an opt-in feature, letting token issuers control the speed vs. security + tradeoff for cross-chain transfers of their token. + +2. By default, a newly deployed token pool only allows transfers at default finality (equivalent to omitting `finality` + or using `finality=finalized`). + +3. To enable faster than finality, the pool owner must configure which faster than finality modes are allowed. Senders then request + faster than finality per transfer using a numeric block depth via `finality=` (minimum recommended depth: `32`). + +> This section applies only to [**token pools with versions ≥ 2.0**](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol). Existing v1 pools do not support fast +> finality. +> +> In this tutorial, configure faster than finality on both token pools: the `Lock & Release` pool on Ethereum Sepolia and the +> `Burn Mint` pool on Arbitrum Sepolia. + +> **NOTE: Pool-level finality vs. transaction-level finality** +> +> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via +> `setAllowedFinalityConfig(bytes4)`. +> +> - Default = faster than finality is **disabled**. The pool only allows transfers that use default +> finality (omit `finality` or use `finality=finalized`). +> - The pool can opt in to faster than finality by allowing a minimum block depth (`finality=` +> ). +> 2. **Transaction-level finality** (`finality`): A per-transfer setting the sender specifies +> via extra args when sending a CCIP message. +> +> - `finality=finalized` (or omit `finality`) = default finality. This is always allowed. +> - `finality=` = faster than finality using block depth (block confirmations). Only succeeds +> if the pool allows block-depth mode and the requested `N` is ≥ the pool's configured minimum. +> +> Each transfer uses either default finality or block depth. +> +> Faster Than Finality transfers use the **faster than finality** rate limit bucket when configured. If the +> faster than finality bucket isn’t configured for a lane, the pool falls back to the standard rate limit bucket. + +[SetFinalityConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/finality-config/SetFinalityConfig.s.sol) + +View the pool configuration script on GitHub. + +1. Set the allowed finality config for the token pool on Ethereum Sepolia: + +```bash filename="Terminal" +BLOCK_DEPTH=32 \ + DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=100000000000000000 \ + forge script \ + script/configure/finality-config/SetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +⏱️ Set Finality Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Action: Set finality config +======================================== + +Current Finality Config: 0x00000000 +New Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) + +---------------------------------------- +📊 Current Rate Limits (faster than finality where enabled, standard otherwise): +---------------------------------------- +Outbound [standard fallback]: +Enabled: true +Capacity: 1000000000000000000000 +Rate: 100000000000000000 +Tokens: 1000000000000000000000 +Inbound [standard fallback]: +Enabled: true +Capacity: 1000000000000000000000 +Rate: 100000000000000000 +Tokens: 1000000000000000000000 + +[Step 1] Setting finality config on Ethereum Sepolia +✅ Finality config set successfully! + +[Step 2] Updating rate limits (faster than finality bucket) on Ethereum Sepolia -> Arbitrum Sepolia +✅ Rate limits updated successfully! + +======================================== +✅ Configuration Complete on Ethereum Sepolia! +======================================== +Token Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) +Token Pool: https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +======================================== +``` + +2. Set the allowed finality config for the token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +BLOCK_DEPTH=32 \ + DEST_CHAIN=ETHEREUM_SEPOLIA \ + OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=100000000000000000 \ + forge script \ + script/configure/finality-config/SetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +⏱️ Set Finality Config +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Set finality config +======================================== + +Current Finality Config: 0x00000000 +New Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) + +---------------------------------------- +📊 Current Rate Limits (faster than finality where enabled, standard otherwise): +---------------------------------------- +Outbound [standard fallback]: +Enabled: true +Capacity: 1000000000000000000000 +Rate: 100000000000000000 +Tokens: 1000000000000000000000 +Inbound [standard fallback]: +Enabled: true +Capacity: 1000000000000000000000 +Rate: 100000000000000000 +Tokens: 1000000000000000000000 + +[Step 1] Setting finality config on Arbitrum Sepolia +✅ Finality config set successfully! + +[Step 2] Updating rate limits (faster than finality bucket) on Arbitrum Sepolia -> Ethereum Sepolia +✅ Rate limits updated successfully! + +======================================== +✅ Configuration Complete on Arbitrum Sepolia! +======================================== +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +#### 5. Activate the Token Pool + +Use the `SetPool.s.sol` script to register each token pool in the CCIP [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol), making it the official pool for your token. This is the step that activates the pool in the CCIP protocol. + +[SetPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/SetPool.s.sol) + +View the pool linking script on GitHub. + +1. Link the token to its respective token pool on Ethereum Sepolia: + +```bash filename="Terminal" +forge script \ + script/setup/SetPool.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🏊‍♂️ Set Token Pool +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Action: Set token pool +======================================== + +Set Pool Parameters: +Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 +Token Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Setting pool for token on Ethereum Sepolia +✅ Pool set successfully! + +======================================== +✅ Pool Set Complete on Ethereum Sepolia! +======================================== +Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Pool Address: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Pool Address: https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +======================================== +``` + +2. Link the token to its respective token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +forge script \ + script/setup/SetPool.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🏊‍♂️ Set Token Pool +======================================== +Chain: Arbitrum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Set token pool +======================================== + +Set Pool Parameters: +Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Token Admin Registry: 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f +Token Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Setting pool for token on Arbitrum Sepolia +✅ Pool set successfully! + +======================================== +✅ Pool Set Complete on Arbitrum Sepolia! +======================================== +Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Pool Address: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Pool Address: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +### Cross-chain Token Operations + +If all the previous steps were successful, your CCT token is now fully registered and activated under the CCIP protocol. +Use these next steps to mint and transfer your tokens across networks. + +#### 1. Mint Tokens + +Use the `MintTokens.s.sol` script to mint tokens to your Externally Owned Account (EOA) on Ethereum Sepolia. \ +Since you granted mint and burn privileges to your EOA during the token deployment in the first step, you are authorized to mint tokens for testing purposes. \ +This ensures that your EOA has sufficient tokens to perform cross-chain transfers in the next step. + +[MintTokens.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/operations/MintTokens.s.sol) + +View the minting script on GitHub. + +1. Mint tokens for your EOA on Ethereum Sepolia: + +> **NOTE: Note** +> +> 1. If you don't set `AMOUNT` as an environment variable, the script will use the amount from the `tokenAmountToMint` +> field in your `script/input/token.json` file. +> 2. Set `MINT_RECEIVER` to mint to a different address (defaults to the EOA broadcasting the transaction). +> 3. If your source-chain token does not support minting, skip this step and fund your sender address through your +> token's normal distribution flow before running the transfer step. + +```bash filename="Terminal" +AMOUNT=1000000000000000000000 \ + forge script \ + script/operations/MintTokens.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +💰 Mint Tokens +======================================== +Chain: Ethereum Sepolia +Action: Mint tokens +======================================== + +Mint Parameters: +Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Token Symbol: BnM-T +Amount: 1000000000000000000000 +Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Minting 1000000000000000000000 BnM-T to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +✅ Tokens minted successfully! + +======================================== +✅ Minting Complete on Ethereum Sepolia! +======================================== +Receiver Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Receiver Address: https://sepolia.etherscan.io/address/0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +New Balance: 1000000000000000000000 BnM-T +======================================== +``` + +#### 2. Transfer Tokens Across Networks + +Use the `ccip-cli` command to securely transfer tokens across networks from your terminal. +Tokens will be: + +1. **locked** on the source chain (Ethereum Sepolia), and, +2. **minted** on the destination chain (Arbitrum Sepolia). \\ + +You can choose to pay CCIP fees using LINK tokens or the native gas token. + +> **Note:** +> +> 1. Add `--fee-token LINK` to pay CCIP fees in LINK. If you omit this flag, fees are paid in the native gas token. +> 2. Use `--receiver
    ` for a custom recipient. If omitted on EVM-to-EVM, the receiver defaults to the sender. + +The CLI command expects the router address to be set in the environment variables. You can set it using the following command: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x... +``` + +> **NOTE: Router address** +> +> You can find the router address for your chain in the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). + +> **NOTE: Token amounts in this tutorial** +> +> Earlier Foundry scripts use smallest-unit amounts (wei-like). In the `ccip-cli` examples below we use a +> compact, human-readable token amount (for example, `1.23`). + +1. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia on **default finality**: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Your output should look something like this: + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` + +2. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia using **faster than finality (block depth)**: + +This assumes you enabled faster than finality on the pool in the optional finality configuration accordion above. +When you pass `--extra finality=...`, `ccip-cli` encodes it into the CCIP message extra args. + +Faster Than Finality using numeric block depth (`finality=`). Use a value of at least `32`: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --extra finality=32 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Your output should look something like this: + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0xeee045c912703291e6d6166e4ef8bfb0a7fb9035af1a41ac00b65c58ad868a94 , messageId => 0x5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4 +CCIP Explorer: https://ccip.chain.link/msg/0x5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4 +``` + +> **NOTE: Understand Minimum Block Depth Requirements 🧑‍🏫** +> +> 1. If a transfer reverts with `InvalidRequestedFinality(requestedFinality, allowedFinality)`, it usually +> means the pool requires a larger block depth than you requested (for example, the pool minimum is `32` +> but you requested a smaller value). +> 2. Use a value of at least `32` (for example, `--extra finality=32`), or omit +> `finality` for default finality. +> 3. Other reverts may indicate the requested depth (or mode) is not permitted by the pool. + +#### 3. (Optional) Hand Off Token Administration After Enablement + +1. The process to set up your tokens for cross-chain transfers is complete. At this stage, you might want to hand off token administration to a different address, for example, transferring control to a multisig or a dedicated operations wallet. + This new admin will be able to mint and transfer tokens as needed. +2. Use the `TransferTokenAdminRole.s.sol` script to achieve this. + This is a **two-step process**: the current admin initiates the transfer, and the new admin must run `AcceptAdminRole.s.sol` to complete it. Until the new admin accepts, the current admin retains full control. + +[TransferTokenAdminRole.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/TransferTokenAdminRole.s.sol) + +View the transfer admin script on GitHub. + +1. Initiate the transfer on Ethereum Sepolia: + +```bash filename="Terminal" +NEW_ADMIN=0x... \ + forge script \ + script/setup/TransferTokenAdminRole.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔄 Transfer Token Admin Role +======================================== +Chain: Ethereum Sepolia +Action: Transfer admin role +======================================== + +Transfer Admin Role Parameters: +Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 +Current Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Pending Administrator: 0x0000000000000000000000000000000000000000 +New Admin: 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 +Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Transferring admin role for token on Ethereum Sepolia +✅ Admin role transfer initiated successfully! + +======================================== +✅ Admin Role Transfer Initiated on Ethereum Sepolia! +======================================== +Token: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +New Admin: 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 +======================================== + +ℹ️ The new admin (0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3) must run AcceptAdminRole to complete the transfer. +``` + +> **NOTE: Note** +> +> 1. Set `TOKEN=0x...` to specify the token explicitly. If omitted, the script reads the deployed token +> address from the environment variable for the current chain. +> 2. Only the current admin can initiate this transfer. If the signer is not the admin, the script will revert with an +> error. + +2. Repeat the same command on Arbitrum Sepolia if you also want to transfer admin rights on the destination chain: + +```bash filename="Terminal" +NEW_ADMIN=0x... \ + forge script \ + script/setup/TransferTokenAdminRole.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +3. The new admin completes the transfer by running the `AcceptAdminRole` script for both chains. + Set `NEW_ADMIN_KEYSTORE_NAME` to the Foundry keystore name that the new admin created via `cast wallet import`: + +```bash filename="Terminal" +export NEW_ADMIN_KEYSTORE_NAME=new_admin_keystore_name + +forge script \ + script/setup/AcceptAdminRole.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $NEW_ADMIN_KEYSTORE_NAME \ + --broadcast + +forge script \ + script/setup/AcceptAdminRole.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $NEW_ADMIN_KEYSTORE_NAME \ + --broadcast +``` --- -# Hyperliquid Integration Guide -Source: https://docs.chain.link/ccip/evm/tools-resources/network-specific/hyperliquid-integration-guide +# Enable your tokens in CCIP (Lock & Mint): Register from an EOA using Hardhat +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat +Last Updated: 2026-04-24 + +## Guide Versions + +- [Foundry (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) + +- [Hardhat (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) + +In this tutorial you will: + +1. Deploy a token on two chains using [Hardhat](https://hardhat.org/). +2. Set up a `Lock & Release` token pool on the source chain and a `Burn Mint` token pool on the destination chain. +3. Configure and activate the pools in CCIP so your token becomes a Cross-Chain Token (CCT). +4. (Optional) Test the setup by minting and transferring tokens across networks. + +> **NOTE: If you do not explicitly configure these features, default behavior applies** +> +> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting +> `finality` or using `finality=finalized`) unless you opt into faster than finality via +> `finality` (for example, `finality=32`). +> 2. **Opt-in changes.** New v2 behavior only applies when you explicitly configure it (for example, +> pool-level fees or faster than finality). + +## Before You Begin + +### 1. Set up your development environment + +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can’t run `npm`, reinstall/update Node.js from the official installer. + +2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/), you can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli). + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +``` + +Verify the installation by running the following command: + +```bash filename="Terminal" +ccip-cli --version +``` + +3. **Clone the repository and navigate to the project directory**: + +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-hardhat) + +Clone the CCIP 2.0 Hardhat template for a smoother setup. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-hardhat.git +cd docs-cct-hardhat +``` + +4. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env +``` + +```bash filename=".env" +# Keystore name (created via `npx hardhat keystore set`) +KEYSTORE_NAME= + +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment tasks) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/helper-config.ts) + +View the complete list of supported chains in the `helper-config.ts` file. + +5. To make sure your terminal has access to these variables, run the following command: + +```bash filename="Terminal" +source .env +``` + +6. Build the project: + +```bash filename="Terminal" +npm install && npx hardhat compile +``` + +7. Create an encrypted Hardhat keystore, if you haven't already: + +```bash filename="Terminal" +npx hardhat keystore set +``` + +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. + +> **NOTE: Note** +> +> 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it +> asks you to enter your private key as hidden text. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in +> `.env`. +> 3. Use `npx hardhat keystore list` to check previously created keystores. + +### 2. How to configure deployment parameters? + +Deployment parameters can be configured in two ways: + +1. Edit the `.json` files in `input/`. +2. Pass CLI flags inline at runtime. + +> **Note:** CLI flags take precedence over `.json` file values. + +#### Token Deployment Configuration + +Default values live in `input/token.json`: + +```json filename="input/token.json" +{ + "name": "BnM Test", + "symbol": "BnM-T", + "decimals": 18, + "maxSupply": 0, + "preMint": 0, + "tokenAmountToMint": 1000000000000000000000, + "tokenAmountToTransfer": 1000000000000000000 +} +``` + +Deployment fields can be overridden with CLI flags: + +| Flag | Overrides (`token.json`) | Description | +| ------------- | ------------------------ | --------------------------------------------- | +| `--name` | `.name` | Token name | +| `--symbol` | `.symbol` | Token symbol | +| `--decimals` | `.decimals` | Decimal places (usually `18`) | +| `--maxsupply` | `.maxSupply` | Max supply in smallest unit (`0` = unlimited) | +| `--premint` | `.preMint` | Amount pre-minted to deployer (`0` = none) | + +#### (Optional) Advanced Pool Hooks + +> **NOTE: What are advanced pool hooks?** +> +> [Advanced Pool +> Hooks](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) +> are an optional security layer you can attach to a token pool. They run checks: +> +> 1. **before** tokens are locked/burned (source chain) +> 2. **before** tokens are released/minted (destination chain) +> +> Most deployments do **not** need a hook. You would attach one only if you want to: +> +> - Make it so that only certain addresses can transfer your token cross-chain. +> - Put in place additional checks for large transfers above a threshold amount. +> - Plug in an external policy engine for custom compliance checks. + +Default values live in `input/advanced-pool-hooks.json`: + +```json filename="input/advanced-pool-hooks.json" +{ + "allowlist": [], + "thresholdAmount": 0, + "policyEngine": "0x0000000000000000000000000000000000000000", + "authorizedCallers": [] +} +``` + +All fields can be overridden with CLI flags: + +| Flag | Overrides (`advanced-pool-hooks.json`) | Description | +| --------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------- | +| `--allowlist` | `.allowlist` | Addresses allowed to transfer (comma-separated) | +| `--thresholdamount` | `.thresholdAmount` | Threshold amount after which additional hook checks/policy checks may apply (`0` = none) | +| `--policyengine` | `.policyEngine` | Policy engine contract address | +| `--authorizedcallers` | `.authorizedCallers` | Addresses authorized to call the hooks (comma-separated) | + +> **Note:** To configure an `AdvancedPoolHooks` sender allowlist, follow [Configure a sender allowlist with AdvancedPoolHooks using Hardhat](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat). For an overview of the other hook capabilities, read [Advanced Pool Hooks](/ccip/concepts/cross-chain-token/advanced-pool-hooks). + +## Tutorial + +### Prerequisites + +> **CAUTION: Before you continue** +> +> - **Sepolia ETH**: required for Sepolia transactions (deploy/configure/send). +> - **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions (deploy/configure). +> - **LINK (optional)**: if you plan to pay for the cross-chain transfer with `LINK`, fund the sender +> wallet with testnet LINK on the **source chain**. +> - You can use our [faucet](https://faucets.chain.link/) to get testnet LINK. + +**Already have deployments? Skip only what you already have:** + +- If you already have tokens deployed on **both** chains, skip **Deploy Tokens**. +- If you have deployed tokens but not the corresponding pools and lockbox, you must still run **Deploy Token Pools** before continuing. +- If you already have tokens deployed on **both** chains, **V2-compatible** token pools deployed on **both** chains, and the lockbox deployed on the **Lock & Release** side (Ethereum Sepolia), skip both sections and move straight on to [Enablement](#enablement). + +**If you already have a V1 token pool deployed and want to use newer features or prepare for lane migration:** + +- You will need to deploy a **V2-compatible token pool** +- Your existing V1 pool can continue to support current transfers +- Follow the migration guide to upgrade your setup without disrupting your existing deployment + +> **Read**: [Migrate from V1 to V2 token pools](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-lock-mint-foundry) + +Before continuing, export the relevant addresses so subsequent tasks can find them: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN= +export ARBITRUM_SEPOLIA_TOKEN= + +export ETHEREUM_SEPOLIA_TOKEN_POOL= +export ARBITRUM_SEPOLIA_TOKEN_POOL= +``` + +#### 1. Deploy Tokens + +> **TIP: Already have tokens deployed?** +> +> 1. Your source chain token (canonical token) does **NOT** need to support the burn and mint functions (this tutorial +> deploys a Lock & Release pool on the source chain), but, +> 2. Both tokens must implement either `getCCIPAdmin()` or OpenZeppelin's `owner()` so the `Claim Admin Role` step can +> identify the token administrator. + +Use the `deployToken` task to deploy your tokens on two testnets, Ethereum Sepolia and Arbitrum Sepolia. The destination chain token (Arbitrum Sepolia) implements the [`IBurnMintERC20`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/interfaces/IBurnMintERC20.sol) interface, which exposes the `mint` and `burn` functions that the Burn & Mint token pool later calls during cross-chain transfers. The source chain token (Ethereum Sepolia) does not need burn/mint support; it is locked in the lockbox by the Lock & Release pool. + +> **Note:** The task reads the `input/token.json` file to get the token name, symbol, decimals, maximum supply, and pre-mint amount. \ +> You can also override these values by providing CLI flags at runtime. \ +> See [how to configure deployment parameters](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat#token-deployment-configuration) for more details. + +[deployToken.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/deploy/deployToken.ts) + +View the deployment task on GitHub. + +1. Deploy token on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat deployToken --network sepolia --rolesrecipient --verify +``` + +> **NOTE: Please note** +> +> 1. `--verify` requires `ETHERSCAN_API_KEY` to be set. +> 2. `--rolesrecipient` is an optional flag. If not provided, the task will use the deployer's address as +> the roles recipient. + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🪙 Deploy Token +======================================== +Chain: Ethereum Sepolia +Action: Deploy CrossChainToken +======================================== + +Token Parameters: + Name: BnM Test + Symbol: BnM-T + Decimals: 18 + Max Supply: 0 + Pre-Mint: 0 + + Pre-mint Recipient: 0xYourdesignatedAddressForPreMint + CCIP Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Deploying BnM Test (BnM-T) on Ethereum Sepolia +⏳ Deployment tx: 0x7d2f5d7f4b7c9f3b5b8f6a4b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d + Waiting for 2 confirmation(s)... +Token deployed at: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + +[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Waiting for 2 confirmation(s)... +✅ Roles granted successfully! + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +======================================== +``` + +After deployment, the token address is automatically saved to: + +```bash +deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json +``` + +1. The file uses the env var name as the key (for example, `ETHEREUM_SEPOLIA_TOKEN`). + +2. You can copy the key and value directly into an `export` command. + +3. The `deployments/` directory is ignored by `.gitignore`, so these files stay local to each user. + +4. Deploy token on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat deployToken --network arbitrumSepolia --rolesrecipient --verify +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🪙 Deploy Token +======================================== +Chain: Arbitrum Sepolia +Action: Deploy CrossChainToken +======================================== + +Token Parameters: + Name: BnM Test + Symbol: BnM-T + Decimals: 18 + Max Supply: 0 + Pre-Mint: 0 + + Pre-mint Recipient: 0xYourdesignatedAddressForPreMint + CCIP Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Deploying BnM Test (BnM-T) on Arbitrum Sepolia +⏳ Deployment tx: 0xc2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7 + Waiting for 2 confirmation(s)... +Token deployed at: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + +[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Waiting for 2 confirmation(s)... +✅ Roles granted successfully! + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +======================================== +``` + +After deployment, the token address is automatically saved to: + +```bash +deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json +``` + +3. Set the environment variables for the deployed token addresses: + +> **NOTE: Why do I need to export these?** +> +> 1. Deployment addresses are persisted locally under `deployments/`, but each task still reads values from +> your current shell environment. +> 2. Run the `export` commands so subsequent tasks in this terminal session can discover the correct +> addresses. +> 3. You can also pass the addresses directly via `--tokenaddress` / `--tokenpool` flags on any task that requires +> them. + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN=0x... +export ARBITRUM_SEPOLIA_TOKEN=0x... +``` + +#### 2. Deploy Token Pools + +This tutorial deploys: + +- A Lock & Release token pool on the source chain (Ethereum Sepolia) +- A Burn & Mint token pool on the destination chain (Arbitrum Sepolia) +- An `ERC20LockBox` to hold token liquidity on behalf of the Lock & Release token pool on the source chain. +- On the destination chain, the tokens are simply burnt or minted as required by the Burn & Mint token pool. + +> **NOTE: Note** +> +> These tasks deploy `V2` token pools that unlock new features as part of CCIP 2.0. Check out how to use these new +> features in the following sections. + +##### Deploy ERC20LockBox (Ethereum Sepolia) + +The [`ERC20LockBox`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/ERC20LockBox.sol) holds token liquidity on behalf of the [`LockReleaseTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/LockReleaseTokenPool.sol). It must be deployed before the pool. + +[deployERC20LockBox.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/deploy/deployERC20LockBox.ts) + +View the lockbox deployment task on GitHub. + +1. Deploy the ERC20LockBox on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat deployERC20LockBox --network sepolia --verify +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +📦 Deploy ERC20 LockBox +======================================== +Chain: Ethereum Sepolia +Action: Deploy ERC20 lockbox +======================================== + +ERC20LockBox Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Authorized Callers: None (add after deploying the token pool) + +[Step 1] Deploying ERC20LockBox on Ethereum Sepolia +⏳ Deployment tx: 0x5f0b4a3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a + Waiting for 2 confirmation(s)... +ERC20LockBox deployed at: 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +https://sepolia.etherscan.io/address/0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +✅ ERC20LockBox deployed successfully! + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +ERC20LockBox Address: 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +https://sepolia.etherscan.io/address/0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b + +Copy this address to use in the next command: + --lockbox 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +======================================== +``` + +After deployment, the lockbox address is automatically saved to: + +```bash +deployments/lock-boxes/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-LockBox.json +``` + +2. Set the environment variable for the deployed lockbox address: + +```bash filename="Terminal" +export LOCK_BOX=0x... +``` + +##### Deploy Lock & Release Token Pool (Ethereum Sepolia) + +Use the `deployLockReleaseTokenPool` task to [deploy a Lock & Release token pool](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/LockReleaseTokenPool.sol) on the source chain. A `LockReleaseTokenPool` locks tokens in the `ERC20LockBox` on the source chain during outbound transfers and releases escrowed tokens during inbound transfers. + +[deployLockReleaseTokenPool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/deploy/deployLockReleaseTokenPool.ts) + +View the Lock & Release token pool deployment task on GitHub. + +1. Deploy a Lock & Release token pool for the deployed token on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat deployLockReleaseTokenPool --lockbox $LOCK_BOX --network sepolia --verify +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔐 Deploy Lock & Release Token Pool +======================================== +Chain: Ethereum Sepolia +Action: Deploy LockReleaseTokenPool +======================================== + +Token Pool Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Decimals: 18 + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + RMN Proxy: 0xba3f6251de62dED61Ff98590cB2fDf6871FbB991 + LockBox: 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b + Pool Hooks: None (0x0) + +[Step 1] Deploying LockReleaseTokenPool on Ethereum Sepolia +⏳ Deployment tx: 0x0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b + Waiting for 2 confirmation(s)... +Token Pool deployed at: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +✅ LockReleaseTokenPool deployed successfully! + +======================================== +✅ Deployment Complete on Ethereum Sepolia! +======================================== +Token Pool Address: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +======================================== +``` + +After deployment, the pool address is automatically saved to: + +```bash +deployments/token-pools/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-LockReleaseTokenPool.json +``` + +2. Set the environment variable for the deployed pool address: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +``` + +##### Authorize Pool on LockBox (Ethereum Sepolia) + +The pool must be authorized as a caller on the lockbox before it can deposit and withdraw tokens during cross-chain transfers. + +[updateAuthorizedCallers.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/authorized-callers/updateAuthorizedCallers.ts) + +View the authorized callers task on GitHub. + +1. Authorize the token pool on the lockbox: + +```bash filename="Terminal" +npx hardhat updateAuthorizedCallers --lockbox $LOCK_BOX --add $ETHEREUM_SEPOLIA_TOKEN_POOL --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +📝 Update Authorized Callers +======================================== +Chain: Ethereum Sepolia +LockBox: 0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +Action: Update authorized callers +======================================== + +Adding 1 caller(s): + [0] 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 + +[Step 1] Applying authorized caller updates on Ethereum Sepolia +⏳ Tx: 0xb1c2d3e4f5a678901234567890abcdef012345678901234567890abcdef012345 + +======================================== +✅ Authorized callers updated on Ethereum Sepolia! +======================================== +LockBox: https://sepolia.etherscan.io/address/0x1c0b9A8f7E6d5C4b3A2f1E0d9C8b7A6f5E4d3C2b +Transaction: 0xb1c2d3e4f5a678901234567890abcdef012345678901234567890abcdef012345 +======================================== +``` + +##### Deploy Burn & Mint Token Pool (Arbitrum Sepolia) + +Use the `deployTokenPool` task to [deploy a Burn & Mint token pool](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol) on the destination chain. A `BurnMintTokenPool` burns tokens on the source chain and mints the equivalent amount on the destination chain, so the pool must hold the destination token's mint and burn roles, which the task grants automatically after deployment. + +[deployBurnMintTokenPool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/deploy/deployBurnMintTokenPool.ts) + +View the deployment task on GitHub. + +1. Deploy a Burn & Mint token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat deployTokenPool --network arbitrumSepolia --verify +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔥⚒️ Deploy Burn & Mint Token Pool +======================================== +Chain: Arbitrum Sepolia +Action: Deploy BurnMintTokenPool +======================================== + +Token Pool Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Decimals: 18 + Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + RMN Proxy: 0x9527E2d01A3064ef6b50c1Da1C0cC523803BCFF2 + Pool Hooks: None (0x0) + +[Step 1] Deploying BurnMintTokenPool on Arbitrum Sepolia +⏳ Deployment tx: 0xd3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708 + Waiting for 2 confirmation(s)... +Token Pool deployed at: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + +[Step 2] Granting mint and burn roles to token pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Waiting for 2 confirmation(s)... +✅ Roles granted successfully! + +======================================== +✅ Deployment Complete on Arbitrum Sepolia! +======================================== +Token Pool Address: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +> **NOTE: Token role-grant fallback** +> +> 1. The task attempts to call `grantMintAndBurnRoles` on the token to grant the pool mint and burn rights. +> 2. If the token does not implement this function, the task prints instructions to grant the roles manually. + +##### Set environment variables + +Set the environment variables for the deployed token pool addresses: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +export ARBITRUM_SEPOLIA_TOKEN_POOL=0x... +``` + +### Enablement + +These steps configure your token pools and activate them under the CCIP protocol so your token becomes a truly Cross-Chain Token (CCT). + +#### 1. Claim Admin Role + +Use the `claimAdmin` task to register your EOA as the administrator for the deployed tokens on both chains. \ +This process involves interacting with the [`RegistryModuleOwnerCustom`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol) contract to set up your EOA as the admin. + +> **NOTE: Supported admin patterns** +> +> The task auto-detects how your token exposes its admin and uses the appropriate registration method: +> +> 1. If your token implements `getCCIPAdmin()`, the task calls `registerAdminViaGetCCIPAdmin()`. +> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the task calls +> `registerAdminViaOwner()` instead. +> +> No extra configuration is needed: both patterns are fully supported. + +[claimAdmin.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/claimAdmin.ts) + +View the admin claim task on GitHub. + +1. Claim the Admin role for the token on Ethereum Sepolia: + +> **NOTE: Note** +> +> If you don't pass `--ccipadmin`, the task defaults to the address of the account broadcasting the +> transaction. + +```bash filename="Terminal" +npx hardhat claimAdmin --network sepolia --ccipadmin +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +👑 Claim Token Admin +======================================== +Chain: Ethereum Sepolia +Action: Claim token admin +======================================== + +Claim Admin Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Current Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Expected Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Registry Module: 0xa3c796d480638d7476792230da1E2ADa86e031b0 + Admin Method: getCCIPAdmin() + +[Step 1] Claiming admin via getCCIPAdmin() on Ethereum Sepolia +⏳ Tx: 0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809 + +======================================== +✅ Admin Claim Complete on Ethereum Sepolia! +======================================== +Token: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Transaction: 0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809 +Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +2. Claim the Admin role for the token on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat claimAdmin --network arbitrumSepolia --ccipadmin +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +👑 Claim Token Admin +======================================== +Chain: Arbitrum Sepolia +Action: Claim token admin +======================================== + +Claim Admin Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Current Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Expected Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Registry Module: 0xaD417c0611dBD225471D31F056b8B6beC1CBC153 + Admin Method: getCCIPAdmin() + +[Step 1] Claiming admin via getCCIPAdmin() on Arbitrum Sepolia +⏳ Tx: 0x9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8 + +======================================== +✅ Admin Claim Complete on Arbitrum Sepolia! +======================================== +Token: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Transaction: 0x9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8 +Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +#### 2. Accept Admin Role + +Use the `acceptAdminRole` task to accept the admin role for the deployed tokens on both chains. +Once you have claimed the role, accepting the role finalizes your control over the token administration via the [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol). + +[acceptAdminRole.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/acceptAdminRole.ts) + +View the admin acceptance task on GitHub. + +1. Accept the admin role for the token on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat acceptAdminRole --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +👑 Accept Admin Role +======================================== +Chain: Ethereum Sepolia +Action: Accept admin role +======================================== + +Accept Admin Role Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 + Pending Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Accepting admin role for token on Ethereum Sepolia +⏳ Tx: 0x0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0 + +======================================== +✅ Admin Role Accepted on Ethereum Sepolia! +======================================== +Token: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Transaction: 0x0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0 +New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +2. Accept the admin role for the token on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat acceptAdminRole --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +👑 Accept Admin Role +======================================== +Chain: Arbitrum Sepolia +Action: Accept admin role +======================================== + +Accept Admin Role Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Token Admin Registry: 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f + Pending Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Accepting admin role for token on Arbitrum Sepolia +⏳ Tx: 0xa0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5 + +======================================== +✅ Admin Role Accepted on Arbitrum Sepolia! +======================================== +Token: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Transaction: 0xa0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5 +New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +======================================== +``` + +#### 3. Configure Token Pools + +Use the `applyChainUpdates` task to link each token pool to its counterpart on the other chain and configure rate limits. \ +You will interact with the [`TokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) contract by calling the `applyChainUpdates()` function to enable cross-chain transfers. + +`applyChainUpdates()` registers the destination chain and the destination pool on the local pool by recording: + +- The remote chain selector (to identify the exact chain), +- The remote pool address (to identify the exact pool on that exact chain), +- The remote token address (to identify the exact token on the remote pool), +- And per-direction rate limit buckets (outbound and inbound). + +You must run it on **both** pools so each side knows its counterpart. + +[applyChainUpdates.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/applyChainUpdates.ts) + +View the pool configuration task on GitHub. + +> **CAUTION: Rate Limiting** +> +> 1. Rate limiting is disabled by default (`isEnabled: false`). With rate limiting disabled, there are **no +> restrictions** on the volume of tokens that can be transferred through the pool in any direction. +> 2. To protect against large, unexpected outflows, set `isEnabled` to `true` and configure `capacity` (maximum bucket +> size) and `rate` (tokens refilled per second). +> 3. See the [Update Rate Limiters](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-hardhat) tutorial for +> more details. + +| Flag | Required | Description | +| -------------------- | -------- | ------------------------------------------------------------------------------ | +| `--destchain` | Yes | Destination chain name (e.g. `sepolia`, `ETHEREUM_SEPOLIA`, `ethereumSepolia`) | +| `--outboundcapacity` | No | Token bucket capacity for outbound transfers | +| `--outboundrate` | No | Token bucket refill rate (tokens/second) for outbound transfers | +| `--inboundcapacity` | No | Token bucket capacity for inbound transfers | +| `--inboundrate` | No | Token bucket refill rate (tokens/second) for inbound transfers | + +1. Configure the token pool on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat applyChainUpdates \ + --destchain arbitrumSepolia \ + --outboundcapacity 1000000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 1000000000000000000000 \ + --inboundrate 100000000000000000 \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔗 Apply Chain Updates +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Action: Configure cross-chain lane +======================================== + +Chain Update Parameters: + Source Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 + Destination Chain Selector: 3478487238524512106 + Destination Chain Family: evm + Destination Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Destination Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Outbound Rate Limit Enabled: true + Outbound Rate Limit Capacity: 1000000000000000000000 + Outbound Rate Limit Rate: 100000000000000000 + Inbound Rate Limit Enabled: true + Inbound Rate Limit Capacity: 1000000000000000000000 + Inbound Rate Limit Rate: 100000000000000000 + +[Step 1] Applying chain updates to pool on Ethereum Sepolia +⏳ Tx: 0x6cf4ac6987c82c018b3ea8e39f133cdc736e8e85a14669c765fe99e8ebdfbaf9 + +======================================== +✅ Chain Updates Complete on Ethereum Sepolia! +======================================== +Token Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Explorer: https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Transaction: 0x6cf4ac6987c82c018b3ea8e39f133cdc736e8e85a14669c765fe99e8ebdfbaf9 +Remote Chain: Arbitrum Sepolia (Selector: 3478487238524512106) +Remote Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +2. Configure the token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat applyChainUpdates \ + --destchain sepolia \ + --outboundcapacity 1000000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 1000000000000000000000 \ + --inboundrate 100000000000000000 \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔗 Apply Chain Updates +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Configure cross-chain lane +======================================== + +Chain Update Parameters: + Source Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Destination Chain Selector: 16015286601757825753 + Destination Chain Family: evm + Destination Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 + Destination Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Outbound Rate Limit Enabled: true + Outbound Rate Limit Capacity: 1000000000000000000000 + Outbound Rate Limit Rate: 100000000000000000 + Inbound Rate Limit Enabled: true + Inbound Rate Limit Capacity: 1000000000000000000000 + Inbound Rate Limit Rate: 100000000000000000 + +[Step 1] Applying chain updates to pool on Arbitrum Sepolia +⏳ Tx: 0x7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6 + +======================================== +✅ Chain Updates Complete on Arbitrum Sepolia! +======================================== +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Explorer: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Transaction: 0x7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6 +Remote Chain: Ethereum Sepolia (Selector: 16015286601757825753) +Remote Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +======================================== +``` + +3. (Optional) Read the list of supported chains and currently configured remote pools: + +```bash filename="Terminal" +npx hardhat getSupportedChains --network sepolia +npx hardhat getSupportedChains --network arbitrumSepolia +``` + +#### 4. (Optional) Configure Faster Than Finality for the Token Pool + +1. CCIP 2.0 supports **faster than finality** as an opt-in feature, letting token issuers control the speed vs. security + tradeoff for cross-chain transfers of their token. + +2. By default, a newly deployed token pool only allows transfers at default finality (equivalent to omitting `finality` + or using `finality=finalized`). + +3. To enable faster than finality, the pool owner must configure which faster than finality modes are allowed. Senders then request + faster than finality per transfer using a numeric block depth via `finality=` (minimum recommended depth: `32`). + +> This section applies only to [**token pools with versions ≥ 2.0**](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol). Existing v1 pools do not support fast +> finality. +> +> In this tutorial, configure faster than finality on both token pools: the `Lock & Release` pool on Ethereum Sepolia and the +> `Burn Mint` pool on Arbitrum Sepolia. + +> **NOTE: Pool-level finality vs. transaction-level finality** +> +> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via +> `setAllowedFinalityConfig(bytes4)`. +> +> - Default = faster than finality is **disabled**. The pool only allows transfers that use default +> finality (omit `finality` or use `finality=finalized`). +> - The pool can opt in to faster than finality by allowing a minimum block depth (`finality=` +> ). +> 2. **Transaction-level finality** (`finality`): A per-transfer setting the sender specifies +> via extra args when sending a CCIP message. +> +> - `finality=finalized` (or omit `finality`) = default finality. This is always allowed. +> - `finality=` = faster than finality using block depth (block confirmations). Only succeeds +> if the pool allows block-depth mode and the requested `N` is ≥ the pool's configured minimum. +> +> Each transfer uses either default finality or block depth. +> +> Faster Than Finality transfers use the **faster than finality** rate limit bucket when configured. If the +> faster than finality bucket isn’t configured for a lane, the pool falls back to the standard rate limit bucket. + +[setFinalityConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/finality-config/setFinalityConfig.ts) + +View the pool configuration task on GitHub. + +1. Set the allowed finality config for the token pool on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat setFinalityConfig \ + --blockdepth 32 \ + --destchain arbitrumSepolia \ + --outboundcapacity 1000000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 1000000000000000000000 \ + --inboundrate 100000000000000000 \ + --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +⏱️ Set Finality Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Action: Set finality config +======================================== + + Current Finality Config: 0x00000000 + New Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + +---------------------------------------- +📊 Current Rate Limits (faster than finality where enabled, standard otherwise): +---------------------------------------- + Outbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + +[Step 1] Setting finality config on Ethereum Sepolia +⏳ Tx: 0x280399cd3dce1815a30ca3763736c7cbc4a8018bdb3ba1b650e728479640f5b6 +✅ Finality config set successfully! + +[Step 2] Updating rate limits (faster than finality bucket) on Ethereum Sepolia → Arbitrum Sepolia +⏳ Tx: 0xa69245f229f41313d597ea452fa7c9d8ead9e05bfe36aad5a997a6850d198ac5 +✅ Rate limits updated successfully! + +======================================== +✅ Configuration Complete on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) +======================================== +``` + +2. Set the allowed finality config for the token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat setFinalityConfig \ + --blockdepth 32 \ + --destchain sepolia \ + --outboundcapacity 1000000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 1000000000000000000000 \ + --inboundrate 100000000000000000 \ + --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +⏱️ Set Finality Config +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Set finality config +======================================== + + Current Finality Config: 0x00000000 + New Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + +---------------------------------------- +📊 Current Rate Limits (faster than finality where enabled, standard otherwise): +---------------------------------------- + Outbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + Inbound [standard fallback]: + Enabled: true + Capacity: 1000000000000000000000 + Rate: 100000000000000000 + Tokens: 1000000000000000000000 + +[Step 1] Setting finality config on Arbitrum Sepolia +⏳ Tx: 0xb1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6 +✅ Finality config set successfully! + +[Step 2] Updating rate limits (faster than finality bucket) on Arbitrum Sepolia → Ethereum Sepolia +⏳ Tx: 0x8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7 +✅ Rate limits updated successfully! + +======================================== +✅ Configuration Complete on Arbitrum Sepolia! +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) +======================================== +``` + +#### 5. Activate the Token Pool + +Use the `setPool` task to register each token pool in the CCIP [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol), making it the official pool for your token. This is the step that activates the pool in the CCIP protocol. + +[setPool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/setPool.ts) + +View the pool linking task on GitHub. + +1. Link the token to its respective token pool on Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat setPool --network sepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🏊 Set Token Pool +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Action: Set token pool +======================================== + +Set Pool Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Pool: 0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 + Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 + Token Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Setting pool for token on Ethereum Sepolia +⏳ Tx: 0x4abdea890bd094239b3931134e8001511f82fcadfc7554b7facd98987c0e55c5 + +======================================== +✅ Pool Set Complete on Ethereum Sepolia! +======================================== +Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Pool Address: https://sepolia.etherscan.io/address/0x8a1b2C3d4E5F60718293a4B5c6D7E8F901234567 +Transaction: 0x4abdea890bd094239b3931134e8001511f82fcadfc7554b7facd98987c0e55c5 +======================================== +``` + +2. Link the token to its respective token pool on Arbitrum Sepolia: + +```bash filename="Terminal" +npx hardhat setPool --network arbitrumSepolia +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🏊 Set Token Pool +======================================== +Chain: Arbitrum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Set token pool +======================================== + +Set Pool Parameters: + Token: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c + Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d + Token Admin Registry: 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f + Token Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Setting pool for token on Arbitrum Sepolia +⏳ Tx: 0x6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5 + +======================================== +✅ Pool Set Complete on Arbitrum Sepolia! +======================================== +Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c +Pool Address: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Transaction: 0x6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5 +======================================== +``` + +### Cross-chain Token Operations + +If all the previous steps were successful, your CCT token is now fully registered and activated under the CCIP protocol. +Use these next steps to mint and transfer your tokens across networks. + +#### 1. Mint Tokens + +Use the `mintTokens` task to mint tokens to your Externally Owned Account (EOA) on Ethereum Sepolia. \ +Since you granted mint and burn privileges to your EOA during the token deployment in the first step, you are authorized to mint tokens for testing purposes. \ +This ensures that your EOA has sufficient tokens to perform cross-chain transfers in the next step. + +[mintTokens.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/operations/mintTokens.ts) -CCIP is fully compatible with Hyperliquid, allowing multichain tokens to be eligible for trading on the Hyperliquid decentralized exchange. This guide will provide comprehensive information about Hyperliquid for CCIP users who want to get enabled on Hyperliquid. +View the minting task on GitHub. -> **NOTE: Enable Your CCIP Token on Hyperliquid** +1. Mint tokens for your EOA on Ethereum Sepolia: + +> **NOTE: Note** > -> We currently offer white glove support to validate your configuration and automatically execute the process for CCIP -> users who are ready to deploy to Hyperliquid. Please contact us -> [here](https://chain.link/ccip-contact?v=General%20Technical%20Support) to schedule a deployment and stay tuned for -> enhanced tooling to further simplify this process for CCIP users. +> 1. If you don't pass `--amount`, the task will use the amount from the `tokenAmountToMint` +> field in your `input/token.json` file. +> 2. Pass `--receiver 0x...` to mint to a different address (defaults to the deployer). +> 3. If your source-chain token does not support minting, skip this step and fund your sender address through your +> token's normal distribution flow before running the transfer step. -## How Hyperliquid Works +```bash filename="Terminal" +npx hardhat mintTokens --amount 1000000000000000000000 --network sepolia +``` -Hyperliquid is an emerging app-chain that powers a popular decentralized exchange with over $4B USDC bridged in. Hyperliquid operates on a dual network system made up of HyperCore and HyperEVM which are fully integrated together to compose the general Hyperliquid State. +Your output should look something like this: -- HyperCore is the network where high-speed trading takes place at up to 200k orders per second, secured by HyperBFT Consensus. Trading occurs through a Central Limit Order Book (CLOB) which is different from most decentralized exchange liquidity pool structures. -- HyperEVM is where the supporting infrastructure and contracts are deployed, such as the MintAndBurnERC20 tokens deployed as a part of a CCIP implementation. There is a growing DeFi ecosystem including Money Markets, LSTs and DEXes on HyperEVM. +```bash filename="Terminal" +======================================== +💰 Mint Tokens +======================================== +Chain: Ethereum Sepolia +Action: Mint tokens +======================================== -| | HyperCore | HyperEVM | -| :------------------: | :-----------------------------------------------------------------: | :----------------------------------------------------: | -| **Primary Function** | High-speed decentralized exchange trading via an onchain order book | Supporting infrastructure and external smart contracts | -| **Token Type** | HIP-1 tokens | ERC20 tokens | -| **Deployment** | Requires winning a spot slot auction | Standard ERC20 deployment | -| **Interaction** | Posting “signed actions” through a Rest API Endpoint | Standard RPC Methods | +Mint Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Symbol: BnM-T + Amount: 1000000000000000000000 + Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 -### HyperCore +[Step 1] Minting 1000000000000000000000 BnM-T to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +⏳ Tx: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef +✅ Tokens minted successfully! -HyperCore is a bespoke L1 blockchain designed to deliver low latency, high throughput, and instant finality for the use case of high speed trading of spot assets and perpetual futures. This enables a user experience and market dynamic that feels like a centralized exchange. Users and developers interact with HyperCore by posting signed actions to the [HyperCore API](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api). +======================================== +✅ Minting Complete on Ethereum Sepolia! +======================================== +Receiver Address: https://sepolia.etherscan.io/address/0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +New Balance: 1000000000000000000000 BnM-T +======================================== +``` -#### HIP-1 Token Standard +#### 2. Transfer Tokens Across Networks -The native token standard for spot trading on HyperCore is called [HIP-1](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/hip-1-native-token-standard). HIP-1 tokens are fungible and have a fixed supply, which must be declared at the time of initialization. +Use the `ccip-cli` command to securely transfer tokens across networks from your terminal. +Tokens will be: -To deploy a HIP-1 token, the issuer must first win a Dutch auction for a spot slot—a permissioned right to enable a token for trading on Hyperliquid. These auctions are held every 31 hours and typically require a winning bid of tens of thousands of dollars worth of HYPE. +1. **locked** on the source chain (Ethereum Sepolia), and, +2. **minted** on the destination chain (Arbitrum Sepolia). \\ -Once a spot slot is secured, the owner can proceed with a defined deployment flow, including initializing the token supply and registering the trading pair—as outlined in the steps below. +You can choose to pay CCIP fees using LINK tokens or the native gas token. -### HyperEVM +> **Note:** +> +> 1. Add `--fee-token LINK` to pay CCIP fees in LINK. If you omit this flag, fees are paid in the native gas token. +> 2. Use `--receiver
    ` for a custom recipient. If omitted on EVM-to-EVM, the receiver defaults to the sender. -HyperEVM is largely similar to any other EVM network, allowing standard development tooling to be used and similar smart contract design patterns. The main differences lie in the Dual Block Size architecture and the access to the HyperCore state via read precompiles. +The CLI command expects the router address to be set in the environment variables. You can set it using the following command: -#### Dual Block Architecture +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x... +``` -The HyperEVM blockchain operates using a [dual block architecture, consisting of small, fast blocks and large, slow blocks](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/dual-block-architecture): +> **NOTE: Router address** +> +> You can find the router address for your chain in the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). -- **Small blocks (3M gas limit)** are produced every 1 second and handle typical user transactions. +> **NOTE: Token amounts in this tutorial** +> +> Earlier Hardhat tasks use smallest-unit amounts (wei-like). In the `ccip-cli` examples below we use a +> compact, human-readable token amount (for example, `1.23`). -- **Large blocks (30M gas limit)** are produced every 1 minute and are used for more complex operations such as contract deployments. +1. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia on **default finality**: -Users can control which block type their transaction is included in by toggling their block size setting. This is done by submitting a signed action—a type of authenticated message—to a specific endpoint on the HyperCore API. +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` -For example, when deploying a CCIP token on HyperEVM, a developer must first switch to large blocks by submitting the appropriate action to the API. This ensures the transaction has sufficient gas capacity to succeed within a large block. +Your output should look something like this: -```json -{ "type": "evmUserModify", "usingBigBlocks": true } +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 ``` -#### Read Precompiles +2. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia using **faster than finality (block depth)**: -Smart contracts on HyperEVM can access data from HyperCore by calling special system-level addresses known as [precompiles](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/interacting-with-hypercore). These addresses don't correspond to regular smart contracts; instead, they trigger native code execution on the validator node. +This assumes you enabled faster than finality on the pool in the optional finality configuration accordion above. +When you pass `--extra finality=...`, `ccip-cli` encodes it into the CCIP message extra args. -For example, when a contract on HyperEVM queries a token balance by calling a designated read precompile, it bypasses standard EVM execution. The call is handled directly by the validator, which fetches the requested data from HyperCore in real time. This mechanism provides secure, gas-efficient, read-only access to the core exchange state — including data such as order book balances, positions, and other protocol-level metrics. +Faster Than Finality using numeric block depth (`finality=`). Use a value of at least `32`: -This design enables the development of onchain applications that can respond to the live state of the central limit order book on HyperCore. The same precompile infrastructure also powers cross-chain bridging, allowing seamless integration between ERC-20 tokens on HyperEVM and their linked HIP-1 counterparts on HyperCore. +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --extra finality=32 \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` -In the following example snippet, a smart contract on HyperEVM can trustlessly read the balance of an HIP1 token on HyperCore. +Your output should look something like this: -```solidity -address constant SPOT_BALANCE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801; - struct SpotBalance { - uint64 total; - uint64 hold; - uint64 entryNtl; - } -function spotBalance(address user, uint64 token) external view returns (SpotBalance memory) { - bool success; - bytes memory result; - (success, result) = SPOT_BALANCE_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, token)); - require(success, "SpotBalance precompile call failed"); - return abi.decode(result, (SpotBalance)); - } +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0xeee045c912703291e6d6166e4ef8bfb0a7fb9035af1a41ac00b65c58ad868a94 , messageId => 0x5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4 +CCIP Explorer: https://ccip.chain.link/msg/0x5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4 ``` -## Integration Steps +> **NOTE: Understand Minimum Block Depth Requirements 🧑‍🏫** +> +> 1. If a transfer reverts with `InvalidRequestedFinality(requestedFinality, allowedFinality)`, it usually +> means the pool requires a larger block depth than you requested (for example, the pool minimum is `32` +> but you requested a smaller value). +> 2. Use a value of at least `32` (for example, `--extra finality=32`), or omit +> `finality` for default finality. +> 3. Other reverts may indicate the requested depth (or mode) is not permitted by the pool. -ERC-20 tokens deployed on HyperEVM can be linked to HIP-1 tokens on HyperCore by the same EOA that deployed the ERC-20 contract. Once linked, users can seamlessly convert their ERC-20 tokens on HyperEVM into the corresponding HIP-1 tokens on HyperCore using a native Lock-and-Release mechanism enabled by the dual-network architecture. +#### 3. (Optional) Hand Off Token Administration After Enablement -Each HIP-1 token is assigned a unique token index, which serves two purposes: +1. The process to set up your tokens for cross-chain transfers is complete. At this stage, you might want to hand off token administration to a different address, for example, transferring control to a multisig or a dedicated operations wallet + This new admin will be able to mint and transfer tokens as needed. +2. Use the `transferTokenAdminRole` task to achieve this. + This is a **two-step process**: the current admin initiates the transfer, and the new admin must call `acceptAdminRole` to complete it. Until the new admin accepts, the current admin retains full control. -- It is used in API calls to identify the token. +[transferTokenAdminRole.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/transferTokenAdminRole.ts) -- It is used to derive the system address that acts as a bridge for transfers between HyperEVM and HyperCore. +View the transfer admin task on GitHub. -For example, the system address for token index 1385 is: -`0x2000000000000000000000000000000000000569`. +1. Initiate the transfer on Ethereum Sepolia: -### Deploying CCIP Tokens on Hyperliquid +```bash filename="Terminal" +npx hardhat transferTokenAdminRole --newadmin new_admin_address --network sepolia +``` -To make a CCIP token tradable on Hyperliquid, follow these high-level steps: +Your output should look something like this: -1. **Secure Spot Slot via Auction**\ - Use the Hyperliquid UI to win the spot slot auction and secure the right to deploy a HIP-1 token. +```bash filename="Terminal" +======================================== +🔄 Transfer Token Admin Role +======================================== +Chain: Ethereum Sepolia +Action: Transfer admin role +======================================== + +Transfer Admin Role Parameters: + Token: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D + Token Admin Registry: 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 + Current Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Pending Administrator: 0x0000000000000000000000000000000000000000 + New Admin: 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 + Signer: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Transferring admin role for token on Ethereum Sepolia +⏳ Tx: 0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 + +======================================== +✅ Admin Role Transfer Initiated on Ethereum Sepolia! +======================================== +Token: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +Transaction: 0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 +New Admin: 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 +======================================== + +ℹ️ The new admin (0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3) must run acceptAdminRole to complete the transfer. +``` -2. **Deploy Token on HyperEVM** - - Deploy your CCIP token to HyperEVM. - - In order to link the HIP-1 token on core to the deployed token on HyperEVM, ensure the token is deployed by an EOA (Externally Owned Account) capable of signing messages. +> **NOTE: Note** +> +> 1. Pass `--tokenaddress 0x...` to specify the token explicitly. If omitted, the task reads the deployed +> token address from the environment variable for the current chain. +> 2. Only the current admin can initiate this transfer. If the signer is not the admin, the task will fail with an +> error. -3. **Initialize the HIP-1 Token** - - Allocate the **maximum intended supply** to the **System Address** derived from the spot slot ID. - - This System Address is consistent across HyperEVM and HyperCore and serves as a bridge between the ERC-20 and HIP-1 tokens. - - Think of this minting step as "funding the bridge" for inbound transfers. - - If the source token has **unbounded supply**, mint a large number (e.g., `2^64 - 1`). - - If the source token has **fixed supply**, mint the full max supply. - - Use the **HyperCore API** for this initialization. +2. Repeat the same command on Arbitrum Sepolia if you also want to transfer admin rights on the destination chain: -4. **Register Trading Pair** - - Register the trading pair between the HIP-1 token and **USDC** using the **HyperCore API**. +```bash filename="Terminal" +npx hardhat transferTokenAdminRole --newadmin new_admin_address --network arbitrumSepolia +``` -5. **Link HIP-1 Token to ERC-20 Token** - - Make a signed API request to the **HyperCore network** from the EOA that deployed the ERC-20 token. - - This confirms the intent to link the tokens. +3. The new admin completes the transfer by calling `acceptAdminRole`: -6. **Connect the CCIP Token Pool** - - Deploy and configure a BurnAndMint Token Pool on HyperEVM using **CCIP Token Manager**. +```bash filename="Terminal" +npx hardhat acceptAdminRole --network sepolia +npx hardhat acceptAdminRole --network arbitrumSepolia +``` -### Bridging Between HyperCore and HyperEVM +--- -Once the above steps are completed, a user can bridge their CCIP token from any connected network to HyperEVM as normal. Then they can [convert that ERC20 token into the corresponding HIP-1 token](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/hypercore-less-than-greater-than-hyperevm-transfers#transferring-between-core-and-evm) by simply sending it to the associated System Address. The system address then sends them the same amount of HIP-1 tokens on HyperCore. This works in the other direction in a similar fashion. +# Manage Token Transfer Fees Using Foundry +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry +Last Updated: 2026-05-05 -## Contact Us +## Guide Versions -Before proceeding, it’s essential to carefully review your deployment configuration. Even minor misconfigurations can permanently impact your spot slot, potentially requiring you to reacquire one through another auction. +- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry) -To help ensure a smooth and secure deployment, we currently offer white-glove support for CCIP users. Our team will validate your configuration and, if desired, provide tools and assistance to complete this process. +- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat) -If you're ready to deploy your CCIP token to Hyperliquid, please [contact us here](https://chain.link/ccip-contact?v=General%20Technical%20Support) to schedule a deployment. Stay tuned—enhanced tooling is on the way to further streamline this process for CCIP users. +CCIP 2.0 token pools let pool owners opt into **pool-level token transfer fee overrides** per destination lane. When an override is disabled, the [OnRamp](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/onRamp/OnRamp.sol) falls back to [`FeeQuoter`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/FeeQuoter.sol) defaults for that lane. In this tutorial you will: ---- +1. Review the current fee configuration for both configured lanes. +2. Optionally set a fee admin who can withdraw accrued fees. +3. Configure lane-specific flat fees, transfer fees, destination gas overhead, and destination bytes overhead. +4. Send a LINK-paid transfer and inspect accrued fees in the source token pool. +5. Withdraw accrued fee tokens. +6. Disable the fee configuration on both lanes to return to `FeeQuoter` defaults. -# CCIP Network Specific Guides -Source: https://docs.chain.link/ccip/evm/tools-resources/network-specific +> **NOTE: Defaults apply unless you opt in** +> +> 1. **Fee overrides are per lane:** each direction is configured independently (Ethereum Sepolia → +> Arbitrum Sepolia vs. Arbitrum Sepolia → Ethereum Sepolia). +> 2. **FeeQuoter is the default:** when `isEnabled: false` for a lane, the OnRamp uses +> FeeQuoter defaults for that lane. +> 3. **Faster Than Finality fees are opt-in:** the `fastFinality*` fields only apply when you +> request block-depth finality (for example, `--extra finality=`). -This section provides guidance on how to integrate with networks that require specific setup in order to successfully utilize CCIP. +## Before You Begin -## Hyperliquid +### 1. Set Up Your Development Environment -- [Hyperliquid Integration Guide](/ccip/evm/tools-resources/network-specific/hyperliquid-integration-guide) -- [HyperEVM Testnet RPC Guide](/ccip/evm/tools-resources/network-specific/hyperevm-testnet-rpc) +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using the [Node.js documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can't run `npm`, reinstall or update Node.js from the official installer. ---- +2. Install Foundry. If you haven't already, follow the [Foundry installation instructions](https://book.getfoundry.sh/getting-started/installation), then verify the installation: -# CCIP Token Manager -Source: https://docs.chain.link/ccip/evm/tools-resources/token-manager -Last Updated: 2025-05-19 +```bash filename="Terminal" +forge --version +``` -The CCIP Token Manager is a web interface designed to simplify the deployment, configuration, and management of Cross-Chain Tokens for use with Chainlink CCIP. +3. Install/Update `ccip-cli` and verify the installed version: -It provides guided workflows for token developers, abstracting away much of the complexity involved in setting up tokens for cross-chain transfers. +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +ccip-cli --version +``` -**Access the Token Manager:** +4. Clone the repository and navigate to the project directory: -- **Mainnet:** [https://tokenmanager.chain.link/](https://tokenmanager.chain.link/) -- **Testnet:** [https://test.tokenmanager.chain.link/](https://test.tokenmanager.chain.link/) +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-foundry) -## Key Features & Workflows +Clone docs-cct-foundry for the CCT Foundry scripts used in this tutorial. -- **Deploy New Tokens**: Guides users through deploying a new token contract from scratch and configuring it for CCIP, primarily using the Burn & Mint mechanism. -- **Enable Existing Tokens**: Assists in making already-deployed tokens cross-chain capable by deploying and configuring the necessary CCIP token pool contracts. -- **Configuration Management**: Allows token administrators (once connected via wallet) to manage certain token pool settings, such as rate limits. -- **Admin Role Management**: Facilitates the process of registering and accepting administrative roles for token pools. -- **Token Verification**: Provides a process to request token verification for listing in the CCIP Directory. -- **Dashboard View**: Offers a dashboard to view and manage tokens associated with the connected wallet. +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-foundry.git +cd docs-cct-foundry +``` ---- +5. Create an encrypted Foundry keystore, if you haven't already: -# Application Developers -Source: https://docs.chain.link/ccip/evm/tutorials/application-developers -Last Updated: 2025-07-21 +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` -These tutorials are for application developers building cross-chain functionality with CCIP. They cover transferring tokens, sending arbitrary messages, programmable token transfers (tokens + data), manual execution, and USDC transfers with data. +Optionally, create an encrypted Foundry keystore for the fee admin, if you haven't already: -## Pages +```bash filename="Terminal" +cast wallet import your_fee_admin_keystore_name --interactive +``` -- [Transfer Tokens](/ccip/evm/tutorials/application-developers/transfer-tokens-from-contract): Transfer ERC-20 tokens from a smart contract to another chain using CCIP. -- [Send a Message](/ccip/evm/tutorials/application-developers/send-arbitrary-data): Send arbitrary data cross-chain using CCIP arbitrary messaging. -- [A → B → A Confirmation Pattern](/ccip/evm/tutorials/application-developers/send-arbitrary-data-receipt-acknowledgment): Send a message cross-chain and receive a confirmation back on the source chain. -- [Programmable Token Transfers](/ccip/evm/tutorials/application-developers/programmable-token-transfers): Send tokens and instructions in a single CCIP message. -- [Programmable Token Transfers - Defensive](/ccip/evm/tutorials/application-developers/programmable-token-transfers-defensive): A defensive example of programmable token transfers with extra safety checks. -- [Manual Execution](/ccip/evm/tutorials/application-developers/manual-execution): Manually execute a CCIP message when the default executor fails or is unavailable. -- [Transfer USDC with Data](/ccip/evm/tutorials/application-developers/usdc): Transfer USDC cross-chain with programmable instructions via CCTP. +> **NOTE: Keystore** +> +> 1. For more information on encrypted Foundry keystores, refer to the +> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 2. `cast wallet import` will throw an error if a keystore with the same name already exists. +> 3. Use `cast wallet list` to check previously created keystores. ---- +6. Create a `.env` file by copying `.env.example`, then fill in the required values: -# Manual Execution -Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/manual-execution -Last Updated: 2026-05-27 +```bash filename="Terminal" +cp .env.example .env +``` -> **NOTE: Just want to execute your stuck message?** -> -> You can jump straight down to either of these sections to execute your transaction: -> -> - [Manual Execution with CCIP Explorer](#foundry-option-a-manual-execute-in-ccip-explorer-confirm-trigger) -> - [Manual Execution with CCIP CLI](#foundry-option-b-manual-execute-with-ccip-cli-show-manual-exec) -> - Your exact command might differ depending on which framework you're using: `Foundry`, or `Hardhat`. \\ +```bash filename=".env" +# Keystore name (created via `cast wallet import`) +KEYSTORE_NAME=your_keystore_name + +# Optional: keystore name for a configured fee admin +FEE_ADMIN_KEYSTORE_NAME=your_fee_admin_keystore_name + +# RPC URLs +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` + +Use the [CCIP Directory](/ccip/directory/testnet) or `script/HelperConfig.s.sol` to confirm the router and LINK addresses for your network. + +> **TIP: Funding** > -> If you're not sure why your message failed, read the [manual execution concept page](/ccip/concepts/manual-execution) to learn how to tell gas errors apart from receiver logic errors before you retry. +> 1. Configuration and withdrawal transactions require native gas on the source chain where you broadcast. +> 2. The `ccip-cli --fee-token LINK` examples require LINK on the source chain. You can get testnet LINK +> from the [Chainlink faucet](https://faucets.chain.link/). +> 3. Token transfer examples require the sender to hold the source-chain token being transferred. -This tutorial extends the [Transfer Tokens with Data](/ccip/evm/tutorials/application-developers/programmable-token-transfers) 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) +7. To make sure your terminal has access to these variables, run: -> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +```bash filename="Terminal" +source .env +``` -## Before you begin +8. Build the project: -1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this - [tutorial](/quickstarts/deploy-your-first-contract) to get started. -2. Your wallet must have: - - `ETH` on *Ethereum Sepolia* (deploy + send) - - `ETH` on *Arbitrum Sepolia* (deploy + configure + manual execution transaction gas) - - `CCIP-BnM` on *Ethereum Sepolia* (token transfer) -3. If you plan to pay CCIP fees in LINK, you also need `LINK` on *Ethereum Sepolia*. Learn how to - [Acquire testnet LINK](/resources/acquire-link). -4. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination - chains/tokens. +```bash filename="Terminal" +npm install && forge build +``` -## Examine the code +## Tutorial -> **CAUTION: Best Practices** +> **TIP: Prerequisites** > -> This example is simplified for educational purposes. For production code, follow these best practices: +> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the two +> tutorials first: > -> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). +> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) +> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) > -> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). +> Use the token pool address for the chain you are configuring. In a Burn & Mint setup, both pools are +> `BurnMintTokenPool` contracts. In a Lock & Mint setup, the source chain can use a +> `LockReleaseTokenPool` backed by an `ERC20LockBox`, while the destination chain uses a +> `BurnMintTokenPool`. This tutorial configures the token pool, not the lockbox. > -> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) +> ```bash filename="Terminal" +> export ETHEREUM_SEPOLIA_TOKEN=0x... +> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... > -> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x... +> ``` > -> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). +> 1. `KEYSTORE_NAME` must resolve to the owner of the source-chain token pool when you set dynamic config or +> fee config. +> 2. A configured fee admin can withdraw accrued fee tokens, but cannot set fee config or set another fee admin. -## Tutorial +### 1. Verify and Set Fee Admin -Let's get started! Choose your preferred development environment below. +The fee admin is part of the token pool dynamic configuration. Use `GetDynamicConfig.s.sol` to inspect the current router, rate limit admin, and fee admin before making any changes. -### Foundry +[GetDynamicConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/dynamic-config/GetDynamicConfig.s.sol) -Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. +View the dynamic config query script on GitHub. -> **NOTE: Want to set up a dev environment from scratch? ** +```bash filename="Terminal" +forge script \ + script/configure/dynamic-config/GetDynamicConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Get Dynamic Config +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View dynamic config +======================================== + +Dynamic Configuration: + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0xYourAddressForFeeAdmin + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +```bash filename="Terminal" +forge script \ + script/configure/dynamic-config/GetDynamicConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Get Dynamic Config +======================================== +Chain: Arbitrum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View dynamic config +======================================== + +Dynamic Configuration: + Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0xYourAddressForFeeAdmin + +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +Use `SetDynamicConfig.s.sol` to set a fee admin. This script sets the router, rate limit admin, and fee admin together, so pass the current router and rate limit admin from the previous output unless you intend to change them. + +[SetDynamicConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/dynamic-config/SetDynamicConfig.s.sol) + +View the dynamic config update script on GitHub. + +| Env var | Required | Description | +| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------- | +| `ROUTER` | No | Router to keep or set. Use the current router from `GetDynamicConfig.s.sol` unless intentionally changing it. | +| `RATE_LIMIT_ADMIN` | No | Rate limit admin to keep or set. Use the current value unless intentionally changing it. | +| `FEE_ADMIN` | No | Address allowed to withdraw accrued fee tokens. Set to `address(0)` to restrict withdrawal to the pool owner. | +| `KEYSTORE_NAME` | Yes | Foundry keystore for the pool owner. | + +1. Set the fee admin on the Ethereum Sepolia pool: + +```bash filename="Terminal" +ROUTER=0xCurrentRouterFromOutput \ + RATE_LIMIT_ADMIN=0xYourAddressForRateLimitAdmin \ + FEE_ADMIN=0xYourFeeAdminAddress \ + forge script \ + script/configure/dynamic-config/SetDynamicConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Set Dynamic Config +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set dynamic config +======================================== + +Current Configuration: + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0xYourAddressForFeeAdmin + +New Configuration: + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + Rate Limit Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Fee Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Setting dynamic config on Ethereum Sepolia +✅ Dynamic config updated successfully! + +======================================== +✅ Configuration Complete on Ethereum Sepolia! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 +Rate Limit Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Fee Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +2. Set the fee admin on the Arbitrum Sepolia pool: + +```bash filename="Terminal" +ROUTER=0xCurrentRouterFromOutput \ + RATE_LIMIT_ADMIN=0xYourAddressForRateLimitAdmin \ + FEE_ADMIN=0xYourFeeAdminAddress \ + forge script \ + script/configure/dynamic-config/SetDynamicConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +⚙️ Set Dynamic Config +======================================== +Chain: Arbitrum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Set dynamic config +======================================== + +Current Configuration: + Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0xYourAddressForFeeAdmin + +New Configuration: + Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + Rate Limit Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Fee Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Setting dynamic config on Arbitrum Sepolia +✅ Dynamic config updated successfully! + +======================================== +✅ Configuration Complete on Arbitrum Sepolia! +======================================== +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 +Rate Limit Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Fee Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +> **NOTE: Fee admin permissions** > -> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a -> Foundry dev environment from scratch. +> 1. The pool owner can set dynamic config and token transfer fee config. +> 2. The pool owner (or a configured fee admin) can withdraw accrued fee tokens. Anyone can inspect fee token balances. -### Hardhat +### 2. Review Current Fee Configuration -Best for developers who want a mature, TypeScript-based smart contract development framework. +Use `GetTokenTransferFeeConfig.s.sol` to read the current fee configuration for a destination lane from the source-chain token pool. -> **NOTE: Want to set up a dev environment from scratch? ** +[GetTokenTransferFeeConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/fee-config/GetTokenTransferFeeConfig.s.sol) + +View the fee config query script on GitHub. + +| Env var | Required | Description | +| ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `DEST_CHAIN` | Yes | Remote chain whose lane is being queried. | +| Source pool env var | Yes | `ETHEREUM_SEPOLIA_TOKEN_POOL`, `ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL`, or the inline `TOKEN_POOL` alias for the source chain selected by `--rpc-url`. | + +1. Check the Ethereum Sepolia pool for the lane to Arbitrum Sepolia: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + forge script \ + script/configure/fee-config/GetTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View fee config +======================================== + +Dest Chain Selector: 3478487238524512106 + +Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 + +⚠️ Fee config is disabled for this lane. + The OnRamp will fall back to FeeQuoter defaults for this destination. + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +2. Check the Arbitrum Sepolia pool for the lane to Ethereum Sepolia: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + forge script \ + script/configure/fee-config/GetTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View fee config +======================================== + +Dest Chain Selector: 16015286601757825753 + +Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 + +⚠️ Fee config is disabled for this lane. + The OnRamp will fall back to FeeQuoter defaults for this destination. + +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +> **NOTE: Fee config disabled by default** > -> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a -> Hardhat dev environment from scratch. +> 1. When fee config is disabled, the OnRamp uses FeeQuoter defaults for token transfer fee calculation on that source -> destination lane. +> 2. If you have already configured fees, the script will show non-zero values and `isEnabled: true`. ---- +### 3. Set Token Transfer Fee Configuration -# Transfer Tokens with Data - Defensive Example -Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/programmable-token-transfers-defensive -Last Updated: 2026-05-01 +Use `UpdateTokenTransferFeeConfig.s.sol` to configure fee parameters for a destination lane on the source-chain token pool. -This tutorial extends the [Transfer Tokens with Data](/ccip/evm/tutorials/application-developers/programmable-token-transfers) example. It uses CCIP to send `tokens + arbitrary data` from one chain to another in a single transaction. -In addition though, it incorporates defensive receiver logic that stores failed messages and locks received tokens instead of reverting the whole delivery. +[UpdateTokenTransferFeeConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/fee-config/UpdateTokenTransferFeeConfig.s.sol) -You will send `CCIP-BnM` tokens and a `string` payload from *Ethereum Sepolia* to *Arbitrum Sepolia*. The destination contract is configured to fail intentionally, then you will recover the locked tokens. +View the fee config update script on GitHub. -> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +| Env var | Required | Description | +| -------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `DEST_CHAIN` | Yes | Remote chain to configure fees for. | +| Source pool env var | Yes | `ETHEREUM_SEPOLIA_TOKEN_POOL`, `ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL`, or the inline `TOKEN_POOL` alias for the source chain selected by `--rpc-url`. | +| `KEYSTORE_NAME` | Yes | Foundry keystore for the pool owner. | +| `DEST_GAS_OVERHEAD` | No | Gas overhead charged in the fee model. Must be greater than `0` when enabled. | +| `DEST_BYTES_OVERHEAD` | No | Data availability bytes overhead. Use at least `32` for Burn & Mint and Lock & Release pools. | +| `FINALITY_FEE_USD_CENTS` | No | Flat fee in 0.01 USD units for default-finality transfers. | +| `FAST_FINALITY_FEE_USD_CENTS` | No | Flat fee in 0.01 USD units for fast-finality transfers. | +| `FINALITY_TRANSFER_FEE_BPS` | No | Basis-point fee deducted from the transferred amount for default-finality transfers. Must be below `10000`. | +| `FAST_FINALITY_TRANSFER_FEE_BPS` | No | Basis-point fee deducted from the transferred amount for fast-finality transfers. Must be below `10000`. | +| `DISABLE` | No | Set to `true` to disable the fee config for this lane. | -> **NOTE: Gas limit for defensive contracts** +> **NOTE: Fee units: basis points vs flat fee** +> +> - **Flat fee:** values are in `0.01 USD (1 cent)` units. For example, `1` means +> $0.01 and `100` means $1.00. +> - **Basis points (bps):** `1 basis points = 0.01%`, `100 basis points = 1%`, and +> `10000 basis points = 100%`. Values must be below `10000`. -## Before you begin +> **CAUTION: Validation rules** +> +> 1. `DEST_GAS_OVERHEAD` must be greater than `0` when the config is enabled. +> 2. `DEST_BYTES_OVERHEAD` must be at least `32` for Burn & Mint and Lock & Release pools. +> 3. Basis-point fee values must be below `10000`. +> 4. First-time setup should pass every fee field shown in this tutorial. Later updates can omit fields you do not want +> to change because the script defaults omitted fields to the current on-chain values. -1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. -2. Your account must have some `ETH` on *Ethereum Sepolia* and `ETH` on *Arbitrum Sepolia* for gas. If you plan to pay CCIP fees in `LINK`, you also need testnet `LINK` on *Ethereum Sepolia*. \ - Learn how to [Acquire testnet LINK](/resources/acquire-link). -3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination chains/tokens. -4. [Acquire CCIP test tokens](/ccip/getting-started/test-tokens#evm-chains). You should have `CCIP-BnM` tokens, and `CCIP-BnM` should appear in the list of your tokens in MetaMask. +This section uses a **two-step update demo** for the Ethereum Sepolia → Arbitrum Sepolia lane so you can see that **omitting fields does not reset them**. -## Examine the code +- **Step 1** sets a basis-point fee for default-finality transfers. +- **Step 2** adds a flat fee for fast-finality transfers, while intentionally omitting the basis points env var so it carries forward. -## Tutorial +Fee behavior in this tutorial: -Let's get started! Choose your preferred development environment below. +- **bps fields** are deducted from the transferred token amount and retained by the source pool as the pool token. +- **Flat fee fields** are charged in the selected fee token (LINK in this tutorial) and credited to the source pool. -### Foundry +#### Step 1: Configure a basis points fee for default-finality transfers (ETH Sepolia → Arbitrum Sepolia) -Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + DEST_GAS_OVERHEAD=50000 \ + DEST_BYTES_OVERHEAD=32 \ + FINALITY_FEE_USD_CENTS=0 \ + FINALITY_TRANSFER_FEE_BPS=25 \ + FAST_FINALITY_FEE_USD_CENTS=0 \ + FAST_FINALITY_TRANSFER_FEE_BPS=0 \ + forge script \ + script/configure/fee-config/UpdateTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` -> **NOTE: Want to set up a dev environment from scratch? ** +Your output should look something like this: + +```bash filename="Terminal" +======================================== +💰 Update Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set fee config +======================================== + +Dest Chain Selector: 3478487238524512106 + +Current On-Chain Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 + +Fee Configuration to Apply: + destGasOverhead: 50000 + destBytesOverhead: 32 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 25 + fastFinalityTransferFeeBps: 0 + +[Step 1] Applying fee config for lane to Arbitrum Sepolia +✅ Fee config applied successfully! + +======================================== +✅ Operation Complete! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +#### Step 2: Add a flat fee for fast-finality transfers (ETH Sepolia → Arbitrum Sepolia) + +This command intentionally omits `FINALITY_TRANSFER_FEE_BPS`. The script defaults omitted fields to the current on-chain values, so the 25 basis points default-finality fee carries forward. + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + FAST_FINALITY_FEE_USD_CENTS=150 \ + forge script \ + script/configure/fee-config/UpdateTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +💰 Update Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set fee config +======================================== + +Dest Chain Selector: 3478487238524512106 + +Current On-Chain Fee Configuration: + isEnabled: true + destGasOverhead: 50000 + destBytesOverhead: 32 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 25 + fastFinalityTransferFeeBps: 0 + +Fee Configuration to Apply: + destGasOverhead: 50000 + destBytesOverhead: 32 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 150 + finalityTransferFeeBps: 25 + fastFinalityTransferFeeBps: 0 + +[Step 1] Applying fee config for lane to Arbitrum Sepolia +✅ Fee config applied successfully! + +======================================== +✅ Operation Complete! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +#### Repeat for the reverse lane (Arbitrum Sepolia → Ethereum Sepolia) + +Repeat the same two steps on the Arbitrum Sepolia token pool by switching the RPC URL and destination chain. + +Step 1: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + DEST_GAS_OVERHEAD=50000 \ + DEST_BYTES_OVERHEAD=32 \ + FINALITY_FEE_USD_CENTS=0 \ + FINALITY_TRANSFER_FEE_BPS=25 \ + FAST_FINALITY_FEE_USD_CENTS=0 \ + FAST_FINALITY_TRANSFER_FEE_BPS=0 \ + forge script \ + script/configure/fee-config/UpdateTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Step 2: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + FAST_FINALITY_FEE_USD_CENTS=150 \ + forge script \ + script/configure/fee-config/UpdateTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Both steps should complete successfully and print the token pool address and transaction hash for the update. + +### 4. Verify the Updated Fee Configuration + +Re-run `GetTokenTransferFeeConfig.s.sol` to confirm the final (Step 2) configuration for the Ethereum Sepolia → Arbitrum Sepolia lane. + +1. Verify the Ethereum Sepolia pool for the lane to Arbitrum Sepolia: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + forge script \ + script/configure/fee-config/GetTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View fee config +======================================== + +Dest Chain Selector: 3478487238524512106 + +Fee Configuration: + isEnabled: true + destGasOverhead: 50000 + destBytesOverhead: 32 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 150 + finalityTransferFeeBps: 25 + fastFinalityTransferFeeBps: 0 + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +2. Repeat for the reverse lane (Arbitrum Sepolia → Ethereum Sepolia): + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + forge script \ + script/configure/fee-config/GetTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Success criteria: + +- Ethereum Sepolia → Arbitrum Sepolia shows `isEnabled: true`. +- Ethereum Sepolia → Arbitrum Sepolia shows `destGasOverhead: 50000`. +- Ethereum Sepolia → Arbitrum Sepolia shows `destBytesOverhead: 32`. +- Ethereum Sepolia → Arbitrum Sepolia shows `finalityFeeUSDCents: 0` and `finalityTransferFeeBps: 25`. +- Ethereum Sepolia → Arbitrum Sepolia shows `fastFinalityFeeUSDCents: 150` and `fastFinalityTransferFeeBps: 0`. +- The reverse lane should show the same values after you repeat the two-step update. + +### 5. Send Transfers That Use the Fee Config + +This section demonstrates Ethereum Sepolia → Arbitrum Sepolia cross-chain transfers. Repeat the same pattern from Arbitrum Sepolia if you want the Arbitrum Sepolia source pool to accrue fees. + +Before sending, inspect the source pool balances for LINK and the source token: + +> **NOTE: Note** > -> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a -> Foundry dev environment from scratch. +> 1. Export the `LINK` token address for the source chain. For example, `export ETHEREUM_SEPOLIA_LINK=0x...`. +> 2. Export the Router address for the source chain. For example, `export ETHEREUM_SEPOLIA_ROUTER=0x...`. +> 3. You can find a complete list of all addresses inside the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). -### Hardhat +```bash filename="Terminal" +FEE_TOKENS="$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + forge script \ + script/operations/GetFeeTokenBalances.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` -Best for developers who want a mature, TypeScript-based smart contract development framework. +Your output should look something like this: -> **NOTE: Want to set up a dev environment from scratch? ** +```bash filename="Terminal" +======================================== +🔍 Get Fee Token Balances +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Inspect fee token balances +======================================== + +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) + +Fee Token Balances: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: 0 ⚠️ (skipping) + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 0 ⚠️ (skipping) + +ℹ️ No fee tokens have a non-zero balance in the pool. Nothing to withdraw. +``` + +1. Send a default-finality transfer and pay CCIP fees in LINK: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --fee-token LINK \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Your output should look something like this: + +```bash filename="Terminal" +Fee: 119317588427515265n = 0.119317588427515265 LINK +Request (source): +┌────────────────────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├────────────────────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ fee │ '0.119317588427515265 LINK' │ +│ tokens │ '1.226925 BnM-T' │ +│ tokenAmountBeforeTokenPoolFees │ 1230000000000000000n │ +│ receipts[1].issuer │ '0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9' │ +│ receipts[1].feeTokenAmount │ 93786750729051300n │ +│ finality │ 'finalized' │ +└────────────────────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` + +This send uses `finalityTransferFeeBps` (25 bps). In this demo, `finalityFeeUSDCents` is `0`. + +2. Send a fast-finality transfer with block-depth finality and pay CCIP fees in LINK: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --fee-token LINK \ + --extra finality=32 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +Your output should look something like this: + +```bash filename="Terminal" +Request (source): +┌────────────────────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├────────────────────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ tokenAmountBeforeTokenPoolFees │ 1230000000000000000n │ +│ tokens │ '1.23 BnM-T' │ +└────────────────────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` + +This send uses `fastFinalityFeeUSDCents` (150). In this demo, `fastFinalityTransferFeeBps` is `0`. + +> **CAUTION: Faster Than Finality must be allowed** > -> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a -> Hardhat dev environment from scratch. +> 1. This send uses `fastFinalityFeeUSDCents` and `fastFinalityTransferFeeBps`. Use `--extra finality=` only when the pool allows block-depth finality. +> 2. If the pool does not allow block-depth finality, or if the requested `finality=` is below the pool minimum, the send reverts. ---- +#### How to read fees from the `ccip-cli` output -# Transfer Tokens with Data -Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/programmable-token-transfers -Last Updated: 2026-05-01 +- The top-level `fee: LINK` is the total fee paid by the sender. It is not necessarily the amount credited to the token pool. +- For the fee-token amount credited to the pool, find the receipt where `receipts[i].issuer` equals the source token pool address. The corresponding `receipts[i].feeTokenAmount` is denominated in the fee token (LINK) in wei. +- For basis points deductions, compare `tokenAmountBeforeTokenPoolFees` with the `tokens` field. Their difference is the token amount retained by the source pool. -In this tutorial, we will use CCIP to send some `tokens + arbitrary data` from one chain to another in a single transaction. \ -We will send `CCIP-BnM` tokens and a `string` payload from *Ethereum Sepolia* to *Arbitrum Sepolia* twice: +### 6. Inspect and Withdraw Accrued Fees -1. Once, paying CCIP fees in native gas tokens `ETH` and using faster than finality (`BLOCK_DEPTH=32`). -2. And then a second time, paying CCIP fees in `LINK` and using finalized finality (default). +After the transfers complete, inspect the source pool balances again: -> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +```bash filename="Terminal" +FEE_TOKENS="$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + forge script \ + script/operations/GetFeeTokenBalances.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔍 Get Fee Token Balances +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Inspect fee token balances +======================================== + +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) + +Fee Token Balances: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 3075000000000000 + +✅ 2 token(s) with non-zero balances are ready for withdrawal. +``` + +Expected balances: + +- **Source token (bps fee)**: The default-finality send uses `finalityTransferFeeBps = 25`. With a `1.23` token transfer: + - Retained by the pool: (1.23 \times 25 / 10000 = 0.003075) tokens + - Verify in `ccip-cli` output: `tokenAmountBeforeTokenPoolFees (1.23)` - `tokens received (1.226925)` = `0.003075` +- **LINK (fee token credited to the pool)**: The fee-token amount credited to the token pool is visible in the send output receipt where `receipts[i].issuer` equals the source token pool address. Sum `receipts[i].feeTokenAmount` across the default-finality and fast-finality sends, then convert from wei to LINK by dividing by (10^). Confirm it matches `getFeeTokenBalances` for LINK. + +Set a value for the `FEE_ADMIN_KEYSTORE_NAME` variable inside your `.env`, using the instructions covered inside [Set Up Your Development Environment](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry#set-up-your-development-environment). + +Use `WithdrawFeeTokens.s.sol` to withdraw accrued fee balances to a recipient. Withdraw as the configured fee admin: + +```bash filename="Terminal" +RECIPIENT=0xYourRecipientAddress \ + FEE_TOKENS="$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + forge script \ + script/operations/WithdrawFeeTokens.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $FEE_ADMIN_KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +💸 Withdraw Fee Tokens +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Withdraw fee tokens +======================================== + +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) + +Tokens to Withdraw: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 3075000000000000 + +[Step 1] Withdrawing fee tokens on Ethereum Sepolia +Recipient: 0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 +✅ Fee tokens withdrawn successfully! + +======================================== +✅ Withdrawal complete on Ethereum Sepolia! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Recipient: 0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 +Recipient: https://sepolia.etherscan.io/address/0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 +======================================== +``` + +Or withdraw as the pool owner: + +```bash filename="Terminal" +RECIPIENT=0xYourRecipientAddress \ + FEE_TOKENS="$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + forge script \ + script/operations/WithdrawFeeTokens.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +💸 Withdraw Fee Tokens +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Withdraw fee tokens +======================================== + +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) + +Tokens to Withdraw: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 3075000000000000 + +[Step 1] Withdrawing fee tokens on Ethereum Sepolia +Recipient: 0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 +✅ Fee tokens withdrawn successfully! + +======================================== +✅ Withdrawal complete on Ethereum Sepolia! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Recipient: 0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 +Recipient: https://sepolia.etherscan.io/address/0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 +======================================== +``` + +Verify the balances after withdrawal: + +```bash filename="Terminal" +FEE_TOKENS="$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + forge script \ + script/operations/GetFeeTokenBalances.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +🔍 Get Fee Token Balances +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Inspect fee token balances +======================================== + +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) + +Fee Token Balances: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: 0 ⚠️ (skipping) + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 0 ⚠️ (skipping) + +ℹ️ No fee tokens have a non-zero balance in the pool. Nothing to withdraw. +======================================== +``` + +> **NOTE: ERC20 fee tokens only** +> +> - `WithdrawFeeTokens.s.sol` accepts ERC20 token addresses. It does not withdraw native gas tokens. +> - This tutorial uses LINK for the fee-token portion so the accrued flat fee component can be inspected and withdrawn +> with the existing script. + +### 7. Disable the Fee Configuration + +Disabling a lane deletes the pool-level override and makes the OnRamp use FeeQuoter defaults for that lane. Disabling does not withdraw already accrued fee balances. + +1. Disable the Ethereum Sepolia → Arbitrum Sepolia fee config: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + DISABLE=true \ + forge script \ + script/configure/fee-config/UpdateTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` + +Your output should look something like this: + +```bash filename="Terminal" +======================================== +💰 Update Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Disable fee config +======================================== + +Dest Chain Selector: 3478487238524512106 -## Before you begin +[Step 1] Disabling fee config for lane to Arbitrum Sepolia +✅ Fee config disabled for this lane. + The OnRamp will now use FeeQuoter defaults for this destination. -1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. -2. Your account must have some `ETH` and `LINK` tokens on *Ethereum Sepolia* and `ETH` tokens on *Arbitrum Sepolia*. \ - Learn how to [Acquire testnet LINK](/resources/acquire-link). -3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination chains/tokens. +======================================== +✅ Operation Complete! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` -## Examine the code +2. Verify the Ethereum Sepolia → Arbitrum Sepolia fee config is disabled: -## Tutorial +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + forge script \ + script/configure/fee-config/GetTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` -Let's get started! Choose your preferred development environment below. +Your output should look something like this: -### Foundry +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View fee config +======================================== -Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. +Dest Chain Selector: 3478487238524512106 -> **NOTE: Want to set up a dev environment from scratch? ** -> -> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a -> Foundry dev environment from scratch. +Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 -### Hardhat +⚠️ Fee config is disabled for this lane. + The OnRamp will fall back to FeeQuoter defaults for this destination. -Best for developers who want a mature, TypeScript-based smart contract development framework. +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` -> **NOTE: Want to set up a dev environment from scratch? ** -> -> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a -> Hardhat dev environment from scratch. +3. Disable the Arbitrum Sepolia → Ethereum Sepolia fee config: ---- +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + DISABLE=true \ + forge script \ + script/configure/fee-config/UpdateTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` -# Send Arbitrary Data and Receive Transfer Confirmation: A -> B -> A -Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/send-arbitrary-data-receipt-acknowledgment -Last Updated: 2026-05-26 +Your output should look something like this: -In this tutorial, we will use CCIP to send arbitrary data from one chain to another and then have the receiver contract send back an acknowledgment message back to the sender: +```bash filename="Terminal" +======================================== +💰 Update Token Transfer Fee Config +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Disable fee config +======================================== -- A MessageTracker contract on chain A sends a text message to an Acknowledger contract on chain B. -- After processing the message, the Acknowledger sends an acknowledgment message back to the MessageTracker. -- The MessageTracker updates onchain state to reflect the successful execution of the back and forth pattern. +Dest Chain Selector: 16015286601757825753 -By tracking acknowledgment onchain, your contracts can safely trigger follow-up actions only after the destination contract has confirmed the receipt. +[Step 1] Disabling fee config for lane to Ethereum Sepolia +✅ Fee config disabled for this lane. + The OnRamp will now use FeeQuoter defaults for this destination. -> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +======================================== +✅ Operation Complete! +======================================== +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` -## Before you begin +4. Verify the Arbitrum Sepolia → Ethereum Sepolia fee config is disabled: -- This tutorial assumes you have completed the [Send Arbitrary Data](/ccip/evm/tutorials/application-developers/send-arbitrary-data) tutorial. -- You will use *Ethereum Sepolia* (A) and *Arbitrum Sepolia* (B). -- Your wallet should have: - - Sepolia ETH (for deploying and sending from Ethereum Sepolia) - - Arbitrum Sepolia ETH (for deploying on Arbitrum Sepolia and funding the Acknowledger) - - Optional: Sepolia LINK (if you want to pay the CCIP fee in LINK) -- Learn how to [Acquire testnet LINK](/resources/acquire-link). +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + forge script \ + script/configure/fee-config/GetTokenTransferFeeConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` -## Examine the code +Your output should look something like this: -The contracts and scripts used in this tutorial live in the Chainlink CCIP 2.0 starter kit repository (`docs-ccip`). +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View fee config +======================================== + +Dest Chain Selector: 16015286601757825753 + +Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 + +⚠️ Fee config is disabled for this lane. + The OnRamp will fall back to FeeQuoter defaults for this destination. + +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` -> **CAUTION: Best Practices** -> -> This example is simplified for educational purposes. For production code, follow these best practices: -> -> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs). -> -> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain). -> -> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*) -> -> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0. +> **CAUTION: Troubleshooting** > -> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). +> - **Owner revert:** confirm `KEYSTORE_NAME` is the owner of the source-chain token pool. +> - **Fee admin withdrawal revert:** confirm the fee admin was set, the correct fee admin keystore is +> used, and the action is withdrawal rather than owner-only configuration. +> - **Missing gas:** confirm the broadcaster has native gas on the source chain. +> - **Missing LINK:** confirm the sender has source-chain LINK when using `--fee-token LINK`. +> - **Missing lane:** confirm the pool lane was configured with `ApplyChainUpdates.s.sol`. +> - **Wrong lane or pool:** confirm `--rpc-url`, `DEST_CHAIN`, and the source pool +> env var match the intended source -> destination lane. +> - **Invalid config:** confirm `DEST_GAS_OVERHEAD` is not `0`, +> `DEST_BYTES_OVERHEAD` is at least `32`, and basis points values are below `10000`. +> - **Fast-finality revert:** confirm the pool allows block-depth finality and the requested +> `finality=` is not below the pool minimum. +> - **No accrued fees:** confirm `FEE_TOKENS` includes LINK and the source token, the send +> completed, native fee token was not used, and flat or basis points fee values are non-zero. -## Tutorial +--- -Let's get started. Choose your preferred development environment below. +# Manage Token Transfer Fees Using Hardhat +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat +Last Updated: 2026-05-05 -### Foundry +## Guide Versions -Best for Solidity-native workflows that prefer a modular, powerful scripting framework. +- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry) -> **NOTE: Want to set up a dev environment from scratch? ** -> -> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a -> Foundry dev environment from scratch. +- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat) -### Hardhat +CCIP 2.0 token pools let pool owners opt into **pool-level token transfer fee overrides** per destination lane. When an override is disabled, the [OnRamp](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/onRamp/OnRamp.sol) falls back to [`FeeQuoter`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/FeeQuoter.sol) defaults for that lane. In this tutorial you will: -Best for devs looking for a mature, TypeScript-based smart contract development framework. +1. Review the current fee configuration for both configured lanes. +2. Optionally set a fee admin who can withdraw accrued fees. +3. Configure lane-specific flat fees, transfer fees, destination gas overhead, and destination bytes overhead. +4. Send a LINK-paid transfer and inspect accrued fees in the source token pool. +5. Withdraw accrued fee tokens. +6. Disable the fee configuration on both lanes to return to `FeeQuoter` defaults. -> **NOTE: Want to set up a dev environment from scratch? ** +> **NOTE: Defaults apply unless you opt in** > -> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a -> Hardhat dev environment from scratch. +> 1. **Fee overrides are per lane:** each direction is configured independently (Ethereum Sepolia → +> Arbitrum Sepolia vs. Arbitrum Sepolia → Ethereum Sepolia). +> 2. **FeeQuoter is the default:** when `isEnabled: false` for a lane, the OnRamp uses +> FeeQuoter defaults for that lane. +> 3. **Faster Than Finality fees are opt-in:** the `fastFinality*` fields only apply when you +> request block-depth finality (for example, `--extra finality=32`). -## Final note +## Before You Begin -This tutorial uses a simple acknowledgment message to confirm delivery end-to-end. You can adapt the same pattern to: +### 1. Set Up Your Development Environment -- programmable token transfers -- multi-step workflows that depend on destination-side execution -- onchain receipts that unlock follow-up actions on the source chain +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using the [Node.js documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can't run `npm`, reinstall or update Node.js from the official installer. ---- +2. Install/Update Chainlink `ccip-cli`: -# Send Arbitrary Data -Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/send-arbitrary-data -Last Updated: 2026-05-01 +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +ccip-cli --version +``` -In this tutorial, we will use CCIP to send arbitrary string data from one chain to another. +3. Clone the repository and navigate to the project directory: -We will send a `string` payload from *Ethereum Sepolia* to *Arbitrum Sepolia* twice: +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-hardhat) -1. Once, using native gas tokens `ETH` and numeric faster than finality. -2. And then a second time, using `LINK` tokens and finalized finality (default). +Clone docs-cct-hardhat for the CCT Hardhat scripts used in this tutorial. -> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-hardhat.git +cd docs-cct-hardhat +``` -## Before you begin +4. Create a `.env` file by copying the `.env.example` file, and fill in the required values: -1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. -2. Your account must have some `ETH` and `LINK` tokens on *Ethereum Sepolia* and `ETH` tokens on *Arbitrum Sepolia*. Learn how to [Acquire testnet LINK](/resources/acquire-link). -3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different source and destination chain pair. +```bash filename="Terminal" +cp .env.example .env +``` -## Examine the code +```bash filename=".env" +# Keystore name (created via `npx hardhat keystore set`) +KEYSTORE_NAME= -## Tutorial +# Optional: keystore name for a configured fee admin +FEE_ADMIN_KEYSTORE_NAME=your_fee_admin_keystore_name -Let's get started! Choose your preferred development environment below. +# RPC URLs +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc -### Foundry +# Etherscan API key (required only if you pass --verify to deployment tasks) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` -Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. +Use the [CCIP Directory](/ccip/directory/testnet) or `helper-config.ts` to confirm the router and LINK addresses for your network. -> **NOTE: Want to set up a dev environment from scratch? ** +> **TIP: Funding** > -> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a -> Foundry dev environment from scratch. +> 1. Configuration and withdrawal transactions require native gas on the source chain where you broadcast. +> 2. The `ccip-cli --fee-token LINK` examples require LINK on the source chain. You can get testnet LINK +> from the [Chainlink faucet](https://faucets.chain.link/). +> 3. Token transfer examples require the sender to hold the source-chain token being transferred. -### Hardhat +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/helper-config.ts) -Best for developers who want a mature, TypeScript-based smart contract development framework. +View the complete list of supported chains in the `helper-config.ts` file. -> **NOTE: Want to set up a dev environment from scratch? ** -> -> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a -> Hardhat dev environment from scratch. +5. Load your environment variables: ---- +```bash filename="Terminal" +source .env +``` -# Transfer Tokens Between Chains from Smart Contracts -Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/transfer-tokens-from-contract -Last Updated: 2026-05-01 +6. Build the project: -In this tutorial, we will use CCIP to transfer `CCIP-BnM` tokens from a smart contract on *Ethereum Sepolia* to an externally owned account (EOA) on *Arbitrum Sepolia*. The example uses off-chain `extraArgs` encoding so the scripts can select the correct finality configuration for the lane. +```bash filename="Terminal" +npm install && npx hardhat compile +``` -We will send the transfer twice: +7. Create an encrypted Hardhat keystore, if you haven't already: -1. Once, paying CCIP fees in native gas tokens `ETH` and using faster than finality (`BLOCK_DEPTH=32`). -2. And again, paying CCIP fees in `LINK` and using finalized finality (default). +```bash filename="Terminal" +npx hardhat keystore set PRIVATE_KEY +``` -> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +Optionally, create an encrypted Hardhat keystore for the fee admin, if you haven't already: -## Before you begin +```bash filename="Terminal" +npx hardhat keystore set FEE_ADMIN_PRIVATE_KEY +``` -1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. -2. Your account must have some `ETH` and `LINK` tokens on *Ethereum Sepolia*. Learn how to [Acquire testnet LINK](/resources/acquire-link). -3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination chains/tokens. -4. [Acquire CCIP test tokens](/ccip/getting-started/test-tokens#evm-chains). You should have `CCIP-BnM` tokens, and `CCIP-BnM` should appear in the list of your tokens in MetaMask. +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. -## Examine the code +> **NOTE: Hardhat keystore** +> +> 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it +> asks you to enter your private key as hidden text. +> 2. The name you choose, such as `PRIVATE_KEY`, is what you set as `KEYSTORE_NAME` in +> `.env`. +> 3. Use `npx hardhat keystore list` to check previously created keystores. ## Tutorial -Let's get started! Choose your preferred development environment below. - -### Foundry - -Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. - -> **NOTE: Want to set up a dev environment from scratch? ** +> **TIP: Prerequisites** > -> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a -> Foundry dev environment from scratch. - -### Hardhat +> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the two +> tutorials first: +> +> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) +> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) +> +> Use the token pool address for the chain you are configuring. In a Burn & Mint setup, both pools are +> `BurnMintTokenPool` contracts. In a Lock & Mint setup, the source chain can use a +> `LockReleaseTokenPool` backed by an `ERC20LockBox`, while the destination chain uses a +> `BurnMintTokenPool`. This tutorial configures the token pool, not the lockbox. +> +> ```bash filename="Terminal" +> export ETHEREUM_SEPOLIA_TOKEN=0x... +> export ARBITRUM_SEPOLIA_TOKEN=0x... +> +> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +> export ARBITRUM_SEPOLIA_TOKEN_POOL=0x... +> ``` +> +> 1. `KEYSTORE_NAME` must resolve to the owner of the source-chain token pool when you set dynamic config or +> fee config. +> 2. A configured fee admin can withdraw accrued fee tokens, but cannot set fee config or set another fee admin. -Best for developers who want a mature, TypeScript-based smart contract development framework. +### 1. Verify and Set Fee Admin -> **NOTE: Want to set up a dev environment from scratch? ** -> -> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a -> Hardhat dev environment from scratch. +The fee admin is part of the token pool dynamic configuration. Use `getDynamicConfig` to inspect the current router, rate limit admin, and fee admin before making any changes. ---- +[getDynamicConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/dynamic-config/getDynamicConfig.ts) -# Transfer USDC with Data -Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/usdc -Last Updated: 2026-09-19 +View the dynamic config query task on GitHub. -USDC is a digital dollar backed 100% and is always redeemable 1:1 for US dollars. The [stablecoin](https://chain.link/education-hub/stablecoins) is issued by [Circle](https://www.circle.com/en/usdc) on multiple blockchain platforms. +```bash filename="Terminal" +npx hardhat getDynamicConfig \ + --network sepolia +``` -This guide will first explain how CCIP enables native USDC transfers when both the source and destination blockchains support [Circle's Cross-Chain Transfer Protocol (CCTP)](https://www.circle.com/en/cross-chain-transfer-protocol). +Your output should look something like this: -Additionally, it will outline how CCIP also supports transferring Bridged USDC on blockchains that **are not** CCTP-enabled, allowing projects to later migrate to CCTP-enabled transfers if approved by Circle. +```bash filename="Terminal" +======================================== +⚙️ Get Dynamic Config +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View dynamic config +======================================== -The hands-on tutorial at the end demonstrates how to use Chainlink CCIP to transfer USDC and arbitrary data from a smart contract on *Ethereum Sepolia* to *Arbitrum Sepolia*. +Dynamic Configuration: + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0xYourAddressForFeeAdmin -> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)** +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` -## Architecture +```bash filename="Terminal" +npx hardhat getDynamicConfig \ + --network arbitrumSepolia +``` -Native USDC transfers through CCIP use Circle's [Cross-Chain Transfer Protocol (CCTP)](https://www.circle.com/en/cross-chain-transfer-protocol) when both the source and destination chains support it. Both chains on this lane: +Your output should look something like this: -- *Ethereum Sepolia*, -- and *Arbitrum Sepolia*, +```bash filename="Terminal" +======================================== +⚙️ Get Dynamic Config +======================================== +Chain: Arbitrum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View dynamic config +======================================== -run CCTP-enabled USDC token pools, so this tutorial moves **native USDC**: burned on the source chain, minted on the destination. +Dynamic Configuration: + Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0xYourAddressForFeeAdmin -> For chains without native CCTP, Circle's [Bridged USDC Standard](https://www.circle.com/blog/bridged-usdc-standard) lets teams deploy USDC early, with a seamless upgrade path to Native USDC later. +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` -CCIP maintains a consistent [API](/ccip/evm/api-reference/v2.0.0/i-router-client) regardless of whether the transfer involves Native USDC or Bridged USDC: +Use `setDynamicConfig` to set a fee admin if you want to. This task sets the router, rate limit admin, and fee admin together, so pass the current router and rate limit admin from the previous output unless you intend to change them. -- The sender interacts with the CCIP router to initiate a cross-chain transaction, just like any other token transfer. See the [Transfer Tokens](/ccip/evm/tutorials/application-developers/transfer-tokens-from-contract) guide to learn more. -- The process uses the same onchain components including the Router, OnRamp, OffRamp, and Token Pool. -- Offchain, messages are verified by independent Cross-Chain Verifiers (CCVs). Every lane runs a default, decentralized Committee Verifier, and USDC transfers are additionally verified by a CCTP Verifier that checks Circle's own attestation for the burn. -- Once the required verifiers have attested, the message is delivered on the destination chain through the OffRamp, which verifies each attestation before the USDC token pool mints USDC using Circle's attestation. -- Execution on destination is permissionless, as in, anyone can submit an attested message for execution, and the Chainlink Executor does so by default. -- All lanes remain protected by the Risk Management Network, which can halt sends and executions on a lane through its cursing mechanism if a safety issue is detected. +[setDynamicConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/dynamic-config/setDynamicConfig.ts) -The diagram below shows the native USDC flow: the USDC token pool burns tokens on the source chain via CCTP, the CCTP Verifier checks Circle's attestation for the burn, and the destination token pool mints USDC using that attestation once all required verifiers have signed off. To learn more about these components, read the [CCIP architecture overview](/ccip/concepts/architecture/overview). +View the dynamic config update task on GitHub. -![Chainlink CCIP Detailed Architecture for USDC](/images/ccip/usdc-diagram.png) +| Input | Required | Description | +| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------- | +| `--router` | No | Router to keep or set. Use the current router from `getDynamicConfig` unless intentionally changing it. | +| `--ratelimitadmin` | No | Rate limit admin to keep or set. Use the current value unless intentionally changing it. | +| `--feeadmin` | No | Address allowed to withdraw accrued fee tokens. Set to `address(0)` to restrict withdrawal to the pool owner. | +| `KEYSTORE_NAME` | Yes | Hardhat keystore name for the pool owner (set in `.env`). | -## Before you begin +1. Set the fee admin on the Ethereum Sepolia pool: -1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started. -2. Your account must have some `ETH` and `LINK` tokens on *Ethereum Sepolia*, and `ETH` on *Arbitrum Sepolia* for deploying the destination contracts and redeeming `STK`. Learn how to [Acquire testnet LINK](/resources/acquire-link). -3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination chains/tokens. -4. Acquire USDC tokens from the [Circle faucet](https://faucet.circle.com/) on *Ethereum Sepolia*. +```bash filename="Terminal" +npx hardhat setDynamicConfig \ + --router 0xCurrentRouterFromOutput \ + --ratelimitadmin 0xYourAddressForRateLimitAdmin \ + --feeadmin 0xYourFeeAdminAddress \ + --network sepolia +``` -## Examine the code +Your output should look something like this: -Three contracts make this work, spread across two chains. \ -`USDCSender` sits on *Ethereum Sepolia* and starts the transfer. On *Arbitrum Sepolia*, `USDCReceiver` catches the incoming message and hands the USDC to `USDCStaker`, which mints a receipt token to whoever the sender nominated. +```bash filename="Terminal" +======================================== +⚙️ Set Dynamic Config +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set dynamic config +======================================== + +Current Configuration: + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0xYourAddressForFeeAdmin + +New Configuration: + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + +[Step 1] Setting dynamic config on Ethereum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x0f4fda7e9e39f35d2bb6d6b7967db7f1b8d2d99fc1f7a64dfe8a3f0d8c7a2b31 +✅ Dynamic config updated successfully! + +======================================== +✅ Configuration Complete on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 +Rate Limit Admin: 0xYourAddressForRateLimitAdmin +Fee Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Transaction: https://sepolia.etherscan.io/tx/0x0f4fda7e9e39f35d2bb6d6b7967db7f1b8d2d99fc1f7a64dfe8a3f0d8c7a2b31 +======================================== +``` + +2. Set the fee admin on the Arbitrum Sepolia pool: -## Tutorial +```bash filename="Terminal" +npx hardhat setDynamicConfig \ + --router 0xCurrentRouterFromOutput \ + --ratelimitadmin 0xYourAddressForRateLimitAdmin \ + --feeadmin 0xYourFeeAdminAddress \ + --network arbitrumSepolia +``` -Let's get started! Choose your preferred development environment below. +Your output should look something like this: -### Foundry +```bash filename="Terminal" +======================================== +⚙️ Set Dynamic Config +======================================== +Chain: Arbitrum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Set dynamic config +======================================== -Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. +Current Configuration: + Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0xYourAddressForFeeAdmin -> **NOTE: Want to set up a dev environment from scratch? ** -> -> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a -> Foundry dev environment from scratch. +New Configuration: + Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 + Rate Limit Admin: 0xYourAddressForRateLimitAdmin + Fee Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 -### Hardhat +[Step 1] Setting dynamic config on Arbitrum Sepolia +⏳ Tx: https://sepolia.arbiscan.io/tx/0xe5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4 +✅ Dynamic config updated successfully! -Best for developers who want a mature, TypeScript-based smart contract development framework. +======================================== +✅ Configuration Complete on Arbitrum Sepolia! +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 +Rate Limit Admin: 0xYourAddressForRateLimitAdmin +Fee Admin: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +Transaction: https://sepolia.arbiscan.io/tx/0xe5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4 +======================================== +``` -> **NOTE: Want to set up a dev environment from scratch? ** +> **NOTE: Fee admin permissions** > -> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a -> Hardhat dev environment from scratch. +> 1. The pool owner can set dynamic config and token transfer fee config. +> 2. The pool owner (or a configured fee admin) can withdraw accrued fee tokens. Anyone can inspect fee token balances. ---- +### 2. Review Current Fee Configuration -# Optimizing Gas Limit Settings in CCIP Messages -Source: https://docs.chain.link/ccip/evm/tutorials/ccipreceive-gaslimit -Last Updated: 2025-10-21 +Use `getTokenTransferFeeConfig` to read the current fee configuration for a destination lane from the source-chain token pool. -When constructing a [CCIP message](/ccip/v1/evm/api-reference/v1.6.1/client#evm2anymessage), it's crucial to set the gas limit accurately. The gas limit represents the maximum amount of gas consumed to execute the [`ccipReceive`](/ccip/v1/evm/api-reference/v1.6.1/ccip-receiver#ccipreceive) function on the CCIP Receiver, which influences the transaction fees for sending a CCIP message. Notably, unused gas is not reimbursed, making it essential to estimate the gas limit carefully: +[getTokenTransferFeeConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/fee-config/getTokenTransferFeeConfig.ts) -- Setting the gas limit too low will cause the transaction to revert when CCIP calls `ccipReceive` on the CCIP Receiver, which requires a manual re-execution with an increased gas limit. For more details about this scenario, read the [Manual Execution](/ccip/evm/tutorials/application-developers/manual-execution) guide. -- Conversely, an excessively high gas limit leads to higher fees. +View the fee config query task on GitHub. -This tutorial shows you how to estimate the gas limit for the `ccipReceive` function using various methods. You will learn how to use a CCIP Receiver where the gas consumption of the `ccipReceive` function varies based on the input data. This example emphasizes the need for testing under diverse conditions. This tutorial includes tasks for the following environments: +| Input | Required | Description | +| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `--destchain` | Yes | Remote chain whose lane is being queried. | +| Source pool env var | Yes | `ETHEREUM_SEPOLIA_TOKEN_POOL`, `ARBITRUM_SEPOLIA_TOKEN_POOL`, or the `--tokenpool` override for the source chain selected by `--network`. | -1. **Local Environment**: Using [Hardhat](https://hardhat.org/) and [Foundry](https://book.getfoundry.sh/) on a local blockchain provides a swift initial gas estimate. However, different frameworks can yield different results and the local environment will not always be representative of your destination blockchain. Consider these figures to be preliminary estimates. Then, incorporate a buffer and conduct subsequent validations on a testnet. -2. **Testnet**: You can precisely determine the required gas limit by deploying your CCIP Sender and Receiver on a testnet and transmitting several CCIP messages with the previously estimated gas. Although this approach is more time-intensive, especially if testing across multiple blockchains, it offers enhanced accuracy. -3. **Offchain Methods**: Estimating gas with an offchain Web3 provider or tools like [Tenderly](https://docs.tenderly.co/) offers the most accurate and swift way to determine the needed gas limit. +1. Check the Ethereum Sepolia pool for the lane to Arbitrum Sepolia: -These approaches will give you insights into accurately estimating the gas limit for the `ccipReceive` function, ensuring cost-effective CCIP transactions. +```bash filename="Terminal" +npx hardhat getTokenTransferFeeConfig \ + --destchain arbitrumSepolia \ + --network sepolia +``` -Before you begin, open a terminal, clone the [smart-contract-examples repository](https://github.com/smartcontractkit/smart-contract-examples), and navigate to the `smart-contract-examples/ccip/estimate-gas` directory. +Your output should look something like this: -```bash -git clone https://github.com/smartcontractkit/smart-contract-examples.git && \ -cd smart-contract-examples/ccip/estimate-gas -``` +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View fee config +======================================== -## Examine the code +Dest Chain Selector: 3478487238524512106 -The source code for the CCIP Sender and Receiver is located in the `contracts` directory for Hardhat projects and in the `src` directory for Foundry projects. The code includes detailed comments for clarity and is designed to ensure self-explanatory functionality. This section focuses on the `_ccipReceive` function: +Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 -```solidity -function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override { - uint256 iterations = abi.decode(any2EvmMessage.data, (uint256)); +⚠️ Fee config is disabled for this lane. + The OnRamp will fall back to FeeQuoter defaults for this destination. - uint256 result = iterations; - uint256 maxIterations = iterations % 100; - for (uint256 i = 0; i < maxIterations; i++) { - result += i; - } +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` - emit MessageReceived( - any2EvmMessage.messageId, - any2EvmMessage.sourceChainSelector, - abi.decode(any2EvmMessage.sender, (address)), - iterations, - maxIterations, - result - ); -} +2. Check the Arbitrum Sepolia pool for the lane to Ethereum Sepolia: + +```bash filename="Terminal" +npx hardhat getTokenTransferFeeConfig \ + --destchain sepolia \ + --network arbitrumSepolia ``` -The `_ccipReceive` function operates as follows: +Your output should look something like this: -1. **Input Processing:** The function accepts a `Client.Any2EVMMessage`. The first step involves decoding the number of iterations from the message's data using ABI decoding. -2. **Logic Execution:** It initializes the `result` variable with the number of iterations. The function calculates `maxIterations` by taking the modulo of iterations with 100, which sets an upper limit for iteration. This step is a safeguard to ensure that the function does not run out of gas. -3. **Iteration:** The function executes a loop from 0 to `maxIterations`, simulating variable computational work based on the input data. This variability directly influences gas consumption. -4. **Event Emission:** Finally, an event `MessageReceived` is emitted. +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View fee config +======================================== -This code shows how gas consumption for the `_ccipReceive` function can fluctuate in response to the input data, highlighting the necessity for thorough testing under different scenarios to determine the correct `gasLimit`. +Dest Chain Selector: 16015286601757825753 -## Gas estimation in a local environment +Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 -To facilitate testing within a local environment, you will use the [MockCCIPRouter](https://github.com/smartcontractkit/chainlink-ccip/blob/release/contracts-ccip-1.6.2/chains/evm/contracts/test/mocks/MockRouter.sol) contract. This contract serves as a mock implementation of the CCIP Router contract, enabling the local testing of CCIP Sender and Receiver contracts. -A notable feature of the `MockCCIPRouter` contract is its ability to emit a `MsgExecuted` event: +⚠️ Fee config is disabled for this lane. + The OnRamp will fall back to FeeQuoter defaults for this destination. -```solidity -event MsgExecuted(bool success, bytes retData, uint256 gasUsed) +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== ``` -This event reports the amount of gas consumed by the `ccipReceive` function. +> **NOTE: Fee config disabled by default** +> +> 1. When fee config is disabled, the OnRamp uses FeeQuoter defaults for token transfer fee calculation on that source -> destination lane. +> 2. If you have already configured fees, the task will show non-zero values and `isEnabled: true`. -### Foundry +### 3. Set Token Transfer Fee Configuration -#### Prerequisites +Use `updateTokenTransferFeeConfig` to configure fee parameters for a destination lane on the source-chain token pool. -1. In your terminal, change to the `foundry` directory: +[updateTokenTransferFeeConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/fee-config/updateTokenTransferFeeConfig.ts) - ```bash - cd foundry - ``` +View the fee config update task on GitHub. -2. Ensure Foundry is [installed](https://book.getfoundry.sh/getting-started/installation). +| Input | Required | Description | +| ------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `--destchain` | Yes | Remote chain to configure fees for. | +| Source pool env var | Yes | `ETHEREUM_SEPOLIA_TOKEN_POOL`, `ARBITRUM_SEPOLIA_TOKEN_POOL`, or the `--tokenpool` override for the source chain selected by `--network`. | +| `KEYSTORE_NAME` | Yes | Hardhat keystore name for the pool owner (set in `.env`). | +| `--destgasoverhead` | No | Gas overhead charged in the fee model. Must be greater than `0` when enabled. | +| `--destbytesoverhead` | No | Data availability bytes overhead. Use at least `32` for Burn & Mint and Lock & Release pools. | +| `--finalityfeeusdcents` | No | Flat fee in 0.01 USD units for default-finality transfers. | +| `--fastfinalityfeeusdcents` | No | Flat fee in 0.01 USD units for fast-finality transfers. | +| `--finalitytransferfeebps` | No | Basis-point fee deducted from the transferred amount for default-finality transfers. Must be below `10000`. | +| `--fastfinalitytransferfeebps` | No | Basis-point fee deducted from the transferred amount for fast-finality transfers. Must be below `10000`. | +| `--disable` | No | Set to disable the fee config for this lane. | +| `--tokenpool` | No | Token pool address. Overrides the `{CHAIN}_TOKEN_POOL` env var. | -3. Check the Foundry version: +> **NOTE: Fee units: basis points vs flat fee** +> +> - **Flat fee:** values are in `0.01 USD (1 cent)` units. For example, `1` means +> $0.01 and `100` means $1.00. +> - **Basis points (bps):** `1 basis points = 0.01%`, `100 basis points = 1%`, and +> `10000 basis points = 100%`. Values must be below `10000`. - ```bash - forge --version - ``` +> **CAUTION: Validation rules** +> +> 1. `--destgasoverhead` must be greater than `0` when the config is enabled. +> 2. `--destbytesoverhead` must be at least `32` for Burn & Mint and Lock & Release pools. +> 3. Basis-point fee values must be below `10000`. +> 4. First-time setup should pass every fee field shown in this tutorial. Later updates can omit fields you do not want +> to change because the task defaults omitted fields to the current on-chain values. - The output should be similar to the following: +This section uses a **two-step update demo** for the Ethereum Sepolia → Arbitrum Sepolia lane so you can see that **omitting fields does not reset them**. - ```text - forge Version: 1.4.2-stable - Commit SHA: 828441d243f552f82c5a89bdc818e52bdd57b26b - Build Timestamp: 2025-10-18T08:19:39.947006000Z (1760775579) - Build Profile: maxperf - ``` +- **Step 1** sets a basis-point fee for default-finality transfers. +- **Step 2** adds a flat fee for fast-finality transfers, while intentionally omitting the basis points flag so it carries forward. - You need version 1.4.2 or above. Run `foundryup` to update Foundry if necessary. +Fee behavior in this tutorial: -4. Build your project: +- **bps fields** are deducted from the transferred token amount and retained by the source pool as the pool token. +- **Flat fee fields** are charged in the selected fee token (LINK in this tutorial) and credited to the source pool. - ```bash - forge build - ``` +#### Step 1: Configure a basis points fee for default-finality transfers (ETH Sepolia → Arbitrum Sepolia) - The output should be similar to: +```bash filename="Terminal" +npx hardhat updateTokenTransferFeeConfig \ + --destchain arbitrumSepolia \ + --destgasoverhead 50000 \ + --destbytesoverhead 32 \ + --finalityfeeusdcents 0 \ + --finalitytransferfeebps 25 \ + --fastfinalityfeeusdcents 0 \ + --fastfinalitytransferfeebps 0 \ + --network sepolia +``` - ```text - [⠊] Compiling... - [⠑] Compiling 59 files with Solc 0.8.24 - [⠘] Solc 0.8.24 finished in 1.67s - Compiler run successful! - ``` +Your output should look something like this: -#### Estimate gas +```bash filename="Terminal" +======================================== +💰 Update Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set fee config +======================================== + +Dest Chain Selector: 3478487238524512106 + +Current On-Chain Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 + +Fee Configuration to Apply: + destGasOverhead: 50000 + destBytesOverhead: 32 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 25 + fastFinalityTransferFeeBps: 0 + +[Step 1] Applying fee config for lane to Arbitrum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x... +✅ Fee config applied successfully! + +======================================== +✅ Operation Complete! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +#### Step 2: Add a flat fee for fast-finality transfers (ETH Sepolia → Arbitrum Sepolia) + +This command intentionally omits `--finalitytransferfeebps`. The task defaults omitted fields to the current on-chain values, so the 25 basis points default-finality fee carries forward. -Located in the `test` directory, the `SendReceive.t.sol` test file assesses the gas consumption of the `ccipReceive` function. This file features a test case that sends a CCIP message to the `MockCCIPRouter` contract, which triggers the `MsgExecuted` event. This event provides insights into the gas requirements of the `ccipReceive` function by detailing the amount of gas consumed. The test case explores three scenarios to examine gas usage comprehensively across various operational conditions: +```bash filename="Terminal" +npx hardhat updateTokenTransferFeeConfig \ + --destchain arbitrumSepolia \ + --fastfinalityfeeusdcents 150 \ + --network sepolia +``` -- **Baseline gas consumption:** This scenario runs `0` iteration to determine the baseline gas consumption, representing the least amount of gas required. -- **Average gas consumption:** This scenario runs `50` iterations to estimate the gas consumption under average operational conditions. -- **Peak gas consumption:** This scenario runs `99` iterations to estimate the peak gas consumption, marking the upper limit of gas usage. +Your output should look something like this: -To run the test, execute the following command: +```bash filename="Terminal" +======================================== +💰 Update Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set fee config +======================================== + +Dest Chain Selector: 3478487238524512106 + +Current On-Chain Fee Configuration: + isEnabled: true + destGasOverhead: 50000 + destBytesOverhead: 32 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 25 + fastFinalityTransferFeeBps: 0 + +Fee Configuration to Apply: + destGasOverhead: 50000 + destBytesOverhead: 32 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 150 + finalityTransferFeeBps: 25 + fastFinalityTransferFeeBps: 0 + +[Step 1] Applying fee config for lane to Arbitrum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x... +✅ Fee config applied successfully! + +======================================== +✅ Operation Complete! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +#### Repeat for the reverse lane (Arbitrum Sepolia → Ethereum Sepolia) + +Repeat the same two steps on the Arbitrum Sepolia token pool by switching the network and destination chain. + +Step 1: -```bash -forge test -vv --isolate +```bash filename="Terminal" +npx hardhat updateTokenTransferFeeConfig \ + --destchain sepolia \ + --destgasoverhead 50000 \ + --destbytesoverhead 32 \ + --finalityfeeusdcents 0 \ + --finalitytransferfeebps 25 \ + --fastfinalityfeeusdcents 0 \ + --fastfinalitytransferfeebps 0 \ + --network arbitrumSepolia ``` -Output example: +Step 2: -```text -[⠊] Compiling... -No files changed, compilation skipped +```bash filename="Terminal" +npx hardhat updateTokenTransferFeeConfig \ + --destchain sepolia \ + --fastfinalityfeeusdcents 150 \ + --network arbitrumSepolia +``` -Ran 3 tests for test/SendReceive.t.sol:SenderReceiverTest -[PASS] test_SendReceiveAverage() (gas: 137800) -Logs: - Number of iterations 50 - Gas used: 11323 +Both steps should complete successfully and print the token pool address and transaction hash for the update. -[PASS] test_SendReceiveMax() (gas: 143803) -Logs: - Number of iterations 99 - Gas used: 17350 +### 4. Verify the Updated Fee Configuration -[PASS] test_SendReceiveMin() (gas: 131614) -Logs: - Number of iterations 0 - Gas used: 5173 +Re-run `getTokenTransferFeeConfig` to confirm the final (Step 2) configuration for the Ethereum Sepolia → Arbitrum Sepolia lane. -Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 8.26ms (9.35ms CPU time) +1. Verify the Ethereum Sepolia pool for the lane to Arbitrum Sepolia: -Ran 1 test suite in 123.83ms (8.26ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests) +```bash filename="Terminal" +npx hardhat getTokenTransferFeeConfig \ + --destchain arbitrumSepolia \ + --network sepolia ``` -This table summarizes the gas usage for different iterations: +Your output should look something like this: -| Scenario | Number of iterations | Gas used | -| ------------------------ | -------------------- | -------- | -| Baseline gas consumption | 0 | 5173 | -| Average gas consumption | 50 | 11323 | -| Peak gas consumption | 99 | 17350 | +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View fee config +======================================== -The output demonstrates that gas consumption increases with the number of iterations, peaking when the iteration count reaches `99`. -In the next section, you will compare these results with those obtained from a local Hardhat environment. +Dest Chain Selector: 3478487238524512106 -### Hardhat +Fee Configuration: + isEnabled: true + destGasOverhead: 50000 + destBytesOverhead: 32 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 150 + finalityTransferFeeBps: 25 + fastFinalityTransferFeeBps: 0 -#### Prerequisites +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` -1. In your terminal, navigate to the `hardhat` directory: +2. Repeat for the reverse lane (Arbitrum Sepolia → Ethereum Sepolia): - ```bash - cd ../hardhat - ``` +```bash filename="Terminal" +npx hardhat getTokenTransferFeeConfig \ + --destchain sepolia \ + --network arbitrumSepolia +``` -2. Install the dependencies and compile the contracts: +Success criteria: - ```bash - npm install && npx hardhat compile - ``` +- Ethereum Sepolia → Arbitrum Sepolia shows `isEnabled: true`. +- Ethereum Sepolia → Arbitrum Sepolia shows `destGasOverhead: 50000`. +- Ethereum Sepolia → Arbitrum Sepolia shows `destBytesOverhead: 32`. +- Ethereum Sepolia → Arbitrum Sepolia shows `finalityFeeUSDCents: 0` and `finalityTransferFeeBps: 25`. +- Ethereum Sepolia → Arbitrum Sepolia shows `fastFinalityFeeUSDCents: 150` and `fastFinalityTransferFeeBps: 0`. +- The reverse lane should show the same values after you repeat the two-step update. - The output should be similar to: +### 5. Send Transfers That Use the Fee Config - ```text - Compiled 4 Solidity files with solc 0.8.24 (evm target: shanghai) - Nothing to compile - ``` +This section demonstrates Ethereum Sepolia → Arbitrum Sepolia cross-chain transfers. Repeat the same pattern from Arbitrum Sepolia if you want the Arbitrum Sepolia source pool to accrue fees. -3. Set the password to encrypt your environment variables using the following command: +Before sending, inspect the source pool balances for LINK and the source token: - ```bash - npx env-enc set-pw - ``` +[getFeeTokenBalances.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/operations/getFeeTokenBalances.ts) -4. Set the following environment variables to deploy contracts on testnets: +View the fee-token balance inspection task on GitHub. - - `PRIVATE_KEY`: The private key for your testnet wallet. If you use MetaMask, follow the instructions to [Export a Private Key](https://support.metamask.io/hc/en-us/articles/360015289632-How-to-export-an-account-s-private-key). **Note:** Your private key is needed to sign any transactions you make such as making requests. - - `ETHEREUM_SEPOLIA_RPC_URL`: The RPC URL for Ethereum Sepolia testnet. You can sign up for a personal endpoint from [Alchemy](https://www.alchemy.com/), [Infura](https://www.infura.io/), or another node provider service. - - `AVALANCHE_FUJI_RPC_URL`: The RPC URL for Avalanche Fuji testnet. You can sign up for a personal endpoint from [Infura](https://www.infura.io/) or another node provider service. - - `ETHERSCAN_API_KEY`: An Ethereum explorer API key, used to verify your contract. Follow [this guide](https://docs.etherscan.io/getting-started/viewing-api-usage-statistics) to get one from Etherscan. +> **NOTE: Note** +> +> 1. Export the `LINK` token address for the source chain. For example, `export ETHEREUM_SEPOLIA_LINK=0x...`. +> 2. Export the Router address for the source chain. For example, `export ETHEREUM_SEPOLIA_ROUTER=0x...`. +> 3. You can find a complete list of all addresses inside the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). - Input these variables using the following command: +```bash filename="Terminal" +npx hardhat getFeeTokenBalances \ + --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + --network sepolia +``` - ```bash - npx env-enc set - ``` +Your output should look something like this: -#### Estimate gas +```bash filename="Terminal" +======================================== +🔍 Get Fee Token Balances +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Inspect fee token balances +======================================== -Located in the `test` directory, the `Send-Receive.ts` test file is designed to evaluate the gas usage of the `ccipReceive` function. This file employs the same logic as the Foundry test file, featuring three scenarios varying by the number of iterations. The test case transmits a CCIP message to the `MockCCIPRouter` contract, triggering the `MsgExecuted` event. This event provides insights into the gas requirements of the `ccipReceive` function by detailing the amount of gas used. +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) -To run only the TypeScript tests, use the test nodejs task: +Fee Token Balances: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: 0 ⚠️ (skipping) + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 0 ⚠️ (skipping) -```bash -npx hardhat test nodejs +ℹ️ No fee tokens have a non-zero balance in the pool. Nothing to withdraw. +======================================== ``` -Example of the output: +1. Send a default-finality transfer and pay CCIP fees in LINK: -```text -Running node:test tests +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --fee-token LINK \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` -Final Gas Usage Report: -Number of iterations 0 - Gas used: 5151 -Number of iterations 50 - Gas used: 11301 -Number of iterations 99 - Gas used: 17328 - Sender and Receiver - ✔ should CCIP message from sender to receiver (760ms) +Your output should look something like this: +```bash filename="Terminal" +Fee: 119317588427515265n = 0.119317588427515265 LINK +Request (source): +┌────────────────────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├────────────────────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ fee │ '0.119317588427515265 LINK' │ +│ tokens │ '1.226925 BnM-T' │ +│ tokenAmountBeforeTokenPoolFees │ 1230000000000000000n │ +│ receipts[1].issuer │ '0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9' │ +│ receipts[1].feeTokenAmount │ 93786750729051300n │ +│ finality │ 'finalized' │ +└────────────────────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` - 1 passing (1036ms) +This send uses `finalityTransferFeeBps` (25 bps). In this demo, `finalityFeeUSDCents` is `0`. + +2. Send a fast-finality transfer with block-depth finality and pay CCIP fees in LINK: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \ + --receiver 0xYourReceiverAddress \ + --fee-token LINK \ + --extra finality=32 \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" ``` -This table summarizes the gas usage across different iterations: +Your output should look something like this: -| Scenario | Number of iterations | Gas used | -| ------------------------ | -------------------- | -------- | -| Baseline gas consumption | 0 | 5151 | -| Average gas consumption | 50 | 11301 | -| Peak gas consumption | 99 | 17328 | +```bash filename="Terminal" +Request (source): +┌────────────────────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├────────────────────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ tokenAmountBeforeTokenPoolFees │ 1230000000000000000n │ +│ tokens │ '1.23 BnM-T' │ +└────────────────────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` -The output demonstrates that gas consumption increases with the number of iterations, peaking when the iteration count reaches `99`. +This send uses `fastFinalityFeeUSDCents` (150). In this demo, `fastFinalityTransferFeeBps` is `0`. -### Compare the results from Foundry and Hardhat +> **CAUTION: Faster Than Finality must be allowed** +> +> 1. This send uses `fastFinalityFeeUSDCents` and `fastFinalityTransferFeeBps`. Use `--extra finality=` only when the pool allows block-depth finality. +> 2. If the pool does not allow block-depth finality, or if the requested `finality=` is below the pool minimum, the send reverts. -This table summarizes the gas usage for different iterations from both Foundry and Hardhat: +#### How to read fees from the `ccip-cli` output -| Scenario | Number of iterations | Gas used (Foundry) | Gas used (Hardhat) | -| ------------------------ | -------------------- | ------------------ | ------------------ | -| Baseline gas consumption | 0 | 5173 | 5151 | -| Average gas consumption | 50 | 11323 | 11301 | -| Peak gas consumption | 99 | 17350 | 17328 | +- The top-level `fee: LINK` is the total fee paid by the sender. It is not necessarily the amount credited to the token pool. +- For the fee-token amount credited to the pool, find the receipt where `receipts[i].issuer` equals the source token pool address. The corresponding `receipts[i].feeTokenAmount` is denominated in the fee token (LINK) in wei. +- For basis points deductions, compare `tokenAmountBeforeTokenPoolFees` with the `tokens` field. Their difference is the token amount retained by the source pool. -Gas usage trends across different iterations are consistent between Foundry and Hardhat and increase with the number of iterations, reaching a peak at 99. However, slight variations in gas usage between the two environments at each iteration level demonstrate the importance of extending gas usage estimation beyond local environment testing. To accurately determine the appropriate gas limit, it is recommended to conduct additional validations on the target blockchain. Setting the gas limit with a buffer is advisable to account for differences between local environment estimations and actual gas usage on the target blockchain. +### 6. Inspect and Withdraw Accrued Fees -### Estimate gas usage on your local environment +After the transfers complete, inspect the source pool balances again: -Now that you've locally estimated the gas usage of the `ccipReceive` function using the provided projects, you can apply the same approach to your own Foundry or Hardhat project. This section will guide you through estimating gas usage in your Foundry or Hardhat project. +```bash filename="Terminal" +npx hardhat getFeeTokenBalances \ + --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + --network sepolia +``` -#### Estimate `ccipReceive` gas usage locally in your Foundry project +Your output should look something like this: -To estimate the gas usage of the `ccipReceive` function within your own Foundry project, follow these steps: +```bash filename="Terminal" +======================================== +🔍 Get Fee Token Balances +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Inspect fee token balances +======================================== -1. Create a testing file in the `test` directory of your project and import the [`MockCCIPRouter`](https://github.com/smartcontractkit/chainlink-ccip/blob/release/contracts-ccip-1.6.2/chains/evm/contracts/test/mocks/MockRouter.sol) contract: +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) - ```solidity - import {MockCCIPRouter} from "@chainlink/contracts-ccip/contracts/test/mocks/MockRouter.sol"; - ``` +Fee Token Balances: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 3075000000000000 - ***Note*:** The `MockCCIPRouter` receives the CCIP message from your CCIP Sender, calls the `ccipReceive` function on your CCIP Receiver, and emits the `MsgExecuted` event with the gas used. +✅ 2 token(s) with non-zero balances are ready for withdrawal. -2. Inside the `setUp` function, deploy the `MockCCIPRouter` contract, and use its address to deploy your CCIP Sender and CCIP Receiver contracts. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/foundry/test/SendReceive.t.sol#L23). +To withdraw, run: + npx hardhat withdrawFeeTokens --feetokens "0x779877A7B0D9E8603169DdbD7836e478b4624789,0xD73D06d36cEa36817186779fF877e418Ab759F09" --network sepolia + (Add --recipient 0x... to send to a different address; defaults to the deployer wallet.) +======================================== +``` -3. In your test cases: - 1. Before transmitting any CCIP messages, use `vm.recordLogs()` to start capturing events. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/foundry/test/SendReceive.t.sol#L39). +Expected balances: - 2. After sending the CCIP message, use `vm.getRecordedLogs()` to collect the recorded logs. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/foundry/test/SendReceive.t.sol#L47-L64). +- **Source token (bps fee)**: The default-finality send uses `finalityTransferFeeBps = 25`. With a `1.23` token transfer: + - Retained by the pool: (1.23 \times 25 / 10000 = 0.003075) tokens + - Verify in `ccip-cli` output: `tokenAmountBeforeTokenPoolFees (1.23)` - `tokens received (1.226925)` = `0.003075` +- **LINK (fee token credited to the pool)**: The fee-token amount credited to the token pool is visible in the send output receipt where `receipts[i].issuer` equals the source token pool address. Sum `receipts[i].feeTokenAmount` across the default-finality and fast-finality sends, then convert from wei to LINK by dividing by (10^). Confirm it matches `getFeeTokenBalances` for LINK. - 3. Parse the logs to find the `MsgExecuted(bool,bytes,uint256)` event and extract the gas used. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/foundry/test/SendReceive.t.sol#L53). +Set a value for the `FEE_ADMIN_KEYSTORE_NAME` variable inside your `.env`, using the instructions covered inside [Set Up Your Development Environment](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat#set-up-your-development-environment). -#### Estimate `ccipReceive` gas usage locally in your Hardhat project +Use `withdrawFeeTokens` to withdraw accrued fee balances to a recipient. Withdraw as the configured fee admin: -To estimate the gas usage of the `ccipReceive` function within your own Hardhat project, follow these steps: +[withdrawFeeTokens.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/operations/withdrawFeeTokens.ts) -1. Create a Solidity file in the `contracts` directory of your project and import the [`MockCCIPRouter`](https://github.com/smartcontractkit/chainlink-ccip/blob/release/contracts-ccip-1.6.2/chains/evm/contracts/test/mocks/MockRouter.sol) contract: +View the fee-token withdrawal task on GitHub. - ```solidity - import {MockCCIPRouter} from "@chainlink/contracts-ccip/contracts/test/mocks/MockRouter.sol"; - ``` +```bash filename="Terminal" +KEYSTORE_NAME=$FEE_ADMIN_KEYSTORE_NAME \ + npx hardhat withdrawFeeTokens \ + --recipient 0xYourRecipientAddress \ + --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + --network sepolia +``` + +Your output should look something like this: - ***Note*:** The `MockCCIPRouter` receives the CCIP message from your CCIP Sender, calls the `ccipReceive` function on your CCIP Receiver, and emits the `MsgExecuted` event with the gas used. +```bash filename="Terminal" +======================================== +💸 Withdraw Fee Tokens +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Withdraw fee tokens +======================================== -2. Create a testing file in your project's `test` directory. +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) +Recipient: 0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 -3. Inside the `deployFixture` function, deploy the `MockCCIPRouter` contract and use its address to deploy your CCIP Sender and CCIP Receiver contracts. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/hardhat/test/Send-Receive.ts#L11). +Tokens to Withdraw: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 3075000000000000 -4. In your test cases: - 1. Send the CCIP message to the `MockCCIPRouter` contract. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/hardhat/test/Send-Receive.ts#L77). - 2. Parse the logs to find the `MsgExecuted(bool,bytes,uint256)` event and extract the gas used. For more details, check this [example](https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip/estimate-gas/hardhat/test/Send-Receive.ts#L59). +[Step 1] Withdrawing fee tokens on Ethereum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x5e1c0d9a8b7f6e5d4c3b2a190f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6 +✅ Fee tokens withdrawn successfully! -## Gas estimation on a testnet +======================================== +✅ Withdrawal complete on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Recipient: https://sepolia.etherscan.io/address/0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 +======================================== +``` -To accurately validate your local environment's gas usage estimations, follow these steps: +Or withdraw as the pool owner: -1. Deploy and configure the CCIP Sender contract on the Avalanche Fuji testnet and the CCIP Receiver contract on the Ethereum Sepolia testnet. +```bash filename="Terminal" +npx hardhat withdrawFeeTokens \ + --recipient 0xYourRecipientAddress \ + --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + --network sepolia +``` -2. Send several CCIP messages with the same number of iterations used in your local testing. For this purpose, use the `sendCCIPMessage.ts` script in the `scripts/testing` directory. This script includes a 10% buffer over the estimated gas usage to ensure a sufficient gas limit. Refer to the table below for the buffered gas limits for each iteration: +Your output should look something like this: - | Scenario | Number of iterations | Estimated gas usage (Hardhat) | Buffered gas limit (+10%) | - | ------------------------ | -------------------- | ----------------------------- | ------------------------- | - | Baseline gas consumption | 0 | 5151 | 5666 | - | Average gas consumption | 50 | 11301 | 12431 | - | Peak gas consumption | 99 | 17328 | 19061 | +```bash filename="Terminal" +======================================== +💸 Withdraw Fee Tokens +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Withdraw fee tokens +======================================== -3. Use [Tenderly](https://dashboard.tenderly.co) to monitor and confirm that the transactions execute successfully within the buffered gas limits. Subsequently, compare the actual gas usage of the `ccipReceive` function on the Ethereum Sepolia testnet against the buffered limits to fine-tune the final gas limit. +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) +Recipient: 0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 -This approach ensures that your gas limit settings are validated against real-world conditions on testnets, providing a more accurate and reliable estimation for deploying on live blockchains. +Tokens to Withdraw: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 3075000000000000 -### Deploy and configure the contracts +[Step 1] Withdrawing fee tokens on Ethereum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d +✅ Fee tokens withdrawn successfully! -To deploy and configure the CCIP Sender contract on the Avalanche Fuji testnet and the CCIP Receiver contract on the Ethereum Sepolia testnet, follow the steps below. **Note**: Your account must have some ETH tokens on Ethereum Sepolia and AVAX tokens on Avalanche Fuji. +======================================== +✅ Withdrawal complete on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Recipient: https://sepolia.etherscan.io/address/0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411 +======================================== +``` -1. Deploy the CCIP Sender on the Avalanche Fuji testnet: +Verify the balances after withdrawal: - ```bash - npx hardhat run scripts/deployment/deploySender.ts --network avalancheFuji - ``` +```bash filename="Terminal" +npx hardhat getFeeTokenBalances \ + --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \ + --network sepolia +``` -2. Deploy the CCIP Receiver on the Ethereum Sepolia testnet: +Your output should look something like this: - ```bash - npx hardhat run scripts/deployment/deployReceiver.ts --network ethereumSepolia - ``` +```bash filename="Terminal" +======================================== +🔍 Get Fee Token Balances +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Inspect fee token balances +======================================== -3. Authorize the Sender to send messages to Ethereum Sepolia: +Pool Token: 0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference) - ```bash - npx hardhat run scripts/configuration/allowlistingForSender.ts --network avalancheFuji - ``` +Fee Token Balances: + [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789 → balance: 0 ⚠️ (skipping) + [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09 → balance: 0 ⚠️ (skipping) -4. Authorize the Receiver to receive messages from the Sender: +ℹ️ No fee tokens have a non-zero balance in the pool. Nothing to withdraw. +======================================== +``` - ```bash - npx hardhat run scripts/configuration/allowlistingForReceiver.ts --network ethereumSepolia - ``` +> **NOTE: ERC20 fee tokens only** +> +> - `withdrawFeeTokens` accepts ERC20 token addresses. It does not withdraw native gas tokens. +> - This tutorial uses LINK for the fee-token portion so the accrued flat fee component can be inspected and withdrawn +> with the existing task. -Upon completion, you will find the CCIP Sender and Receiver contracts deployed and configured on their respective testnets. Contract addresses are available in the `scripts/generatedData.json` file. +### 7. Disable the Fee Configuration -### Send CCIP Messages +Disabling fee configuration for a lane removes the lane-specific override and returns that lane to `FeeQuoter` defaults. Disable both lanes that you configured earlier. -1. Send three CCIP messages with different numbers of iterations: +1. Disable the fee config on the Ethereum Sepolia pool for the lane to Arbitrum Sepolia: - ```bash - npx hardhat run scripts/testing/sendCCIPMessages.ts --network avalancheFuji - ``` +```bash filename="Terminal" +npx hardhat updateTokenTransferFeeConfig \ + --destchain arbitrumSepolia \ + --disable \ + --network sepolia +``` - Your output should look something like this: +Your output should look something like this: - ```text - $ npx hardhat run scripts/testing/sendCCIPMessages.ts --network avalancheFuji - Approving 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 for 0x6e80C9d77B12470A6158459cED1616F705aA13Ea. Allowance is max uint256. Signer 0x8c244f0b2164e6a3bed74ab429b0ebd661bb14ca... +```bash filename="Terminal" +======================================== +💰 Update Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Disable fee config +======================================== - Number of iterations 0 - Gas limit: 5685 - Message Id: 0x7a51c84fd0734afef641e3578b29e718c208d6ac2e50826f73a3fec24076a72b - Number of iterations 50 - Gas limit: 16190 - Message Id: 0x348ab504289f5a0a0ffed023d8315668dbdd8a8a2ae93349845e837e397aa1ac - Number of iterations 99 - Gas limit: 26485 - Message Id: 0xe4dfdac75ae90f355583b356033179a89a95e5174256a04c371ab10dfe433d96 - ``` +Dest Chain Selector: 3478487238524512106 -2. Open the [CCIP explorer](https://ccip.chain.link), search each message by its ID, and wait for each message to be successfully transmitted (`Status` in the explorer: `Success`). +[Step 1] Disabling fee config for lane to Arbitrum Sepolia +⏳ Tx: https://sepolia.etherscan.io/tx/0x6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b +✅ Fee config disabled for this lane. + The OnRamp will now use FeeQuoter defaults for this destination. -For the example above, here are the destination transaction hashes: +======================================== +✅ Operation Complete! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` -| Message id | Ethereum Sepolia transaction hash | -| ------------------------------------------------------------------ | ------------------------------------------------------------------ | -| 0x7a51c84fd0734afef641e3578b29e718c208d6ac2e50826f73a3fec24076a72b | 0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413 | -| 0x348ab504289f5a0a0ffed023d8315668dbdd8a8a2ae93349845e837e397aa1ac | 0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413 | -| 0xe4dfdac75ae90f355583b356033179a89a95e5174256a04c371ab10dfe433d96 | 0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413 | +2. Verify the Ethereum Sepolia pool for the lane to Arbitrum Sepolia: -**Note** that the Ethereum Sepolia transaction hash is the same for all the messages. This is because CCIP batched the messages. +```bash filename="Terminal" +npx hardhat getTokenTransferFeeConfig \ + --destchain arbitrumSepolia \ + --network sepolia +``` -### Check the actual gas usage +Your output should look something like this: -1. Open [Tenderly](https://dashboard.tenderly.co) and Search for the [destination transaction hash](https://dashboard.tenderly.co/tx/sepolia/0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413). +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View fee config +======================================== -2. Search for `_callWithExactGasSafeReturnData` that has payload containing your messageId (without `0x`). This is an [example](https://dashboard.tenderly.co/tx/0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413?trace=0.0.0.12.0.2.2) for `0x7a51c84fd0734afef641e3578b29e718c208d6ac2e50826f73a3fec24076a72b`. +Dest Chain Selector: 3478487238524512106 -3. Just below it, you will find the call trace from the Router to your Receiver contract. This is the [trace](https://dashboard.tenderly.co/tx/0x25c26be00c5e72127588a29ef485b817dae0d1bf4cd59fd2446da9af03441413?trace=0.0.0.12.0.2.2). +Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 - > **CAUTION: Note** - > - > Tenderly's numeric trace indices aren't stable over time (reprocessing can shift them), so if the trace link above - > doesn't work, you can still find the correct trace by searching for `_callWithExactGasSafeReturnData`. +⚠️ Fee config is disabled for this lane. + The OnRamp will fall back to FeeQuoter defaults for this destination. -4. Right below the `_callWithExactGasSafeReturnData`, you will see the `Router` => `Receiver`.`ccipReceive` call. If it shows "No source for this contract", click on "Fetch the contract from public explorer" to load the source code from Etherscan since it's already verified. When you click on the value of the `message` parameter, it will display the exact message ID (as `messageId`) (i.e., `0x7a51c84fd0734afef641e3578b29e718c208d6ac2e50826f73a3fec24076a72b` in this case) as one of its fields. +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` -5. Click on the debugger and you'll get the gas details: +3. Disable the fee config on the Arbitrum Sepolia pool for the lane to Ethereum Sepolia: - ```text - "gas":{ - "gas_left":5685 - "gas_used":5016 - "total_gas_used":7994315 - } - ``` +```bash filename="Terminal" +npx hardhat updateTokenTransferFeeConfig \ + --destchain sepolia \ + --disable \ + --network arbitrumSepolia +``` -6. Note the `gas_left` is equal to the limit that is set in the `sendCCIPMessages.ts` script: `5685`. The `gas_used` is the actual gas used by the Receiver contract to process the message. +Your output should look something like this: -7. Repeating the same steps for the other two messages, we can summarize the output: +```bash filename="Terminal" +======================================== +💰 Update Token Transfer Fee Config +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Disable fee config +======================================== - | Scenario | Number of iterations | Estimated gas usage (Hardhat) | Buffered gas limit (+10%) | Gas used on testnet | - | ------------------------ | -------------------- | ----------------------------- | ------------------------- | ------------------- | - | Baseline gas consumption | 0 | 5151 | 5666 | 5016 | - | Average gas consumption | 50 | 11301 | 12431 | 11166 | - | Peak gas consumption | 99 | 17328 | 19061 | 17193 | +Dest Chain Selector: 16015286601757825753 -Testing on testnets has confirmed that a gas limit of `19061` is adequate for the `ccipReceive` function to execute successfully under various conditions. However, it is important to note that gas usage may differ across testnets. Therefore, it is advisable to conduct similar validation efforts on the blockchain where you intend to deploy. Deploying and validating contracts across multiple testnets can be time-consuming. For efficiency, consider using [offchain methods](#offchain-methods) to estimate gas usage. +[Step 1] Disabling fee config for lane to Ethereum Sepolia +⏳ Tx: https://sepolia.arbiscan.io/tx/0xf60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5 +✅ Fee config disabled for this lane. + The OnRamp will now use FeeQuoter defaults for this destination. -## Offchain methods +======================================== +✅ Operation Complete! +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` -This section guides you through estimating gas usage using two different offchain methods: +4. Verify the Arbitrum Sepolia pool for the lane to Ethereum Sepolia: -- A Web3 provider using the [ethers.js `estimateGas`](https://docs.ethers.org/v6/api/providers/#Provider-estimateGas) function. -- [Tenderly simulation API](https://docs.tenderly.co/reference/api#tag/Simulations/operation/simulateTransaction). The Tenderly simulation API provides a more accurate result (Read this [blog post](https://blog.tenderly.co/how-tenderly-enables-most-accurate-ethereum-gas-estimation/) to learn more) but you are limited to the blockchains supported by Tenderly. +```bash filename="Terminal" +npx hardhat getTokenTransferFeeConfig \ + --destchain sepolia \ + --network arbitrumSepolia +``` -These methods provide the most accurate and rapid means to determine the necessary gas limit for the `ccipReceive` function. You will use the same CCIP Receiver contract deployed on the Ethereum Sepolia testnet in the previous section. +Your output should look something like this: -### Prerequisites +```bash filename="Terminal" +======================================== +💰 Get Token Transfer Fee Config +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View fee config +======================================== -1. In your terminal, navigate to the `offchain` directory: +Dest Chain Selector: 16015286601757825753 - ```bash - cd ../offchain - ``` +Fee Configuration: + isEnabled: false + destGasOverhead: 0 + destBytesOverhead: 0 + finalityFeeUSDCents: 0 + fastFinalityFeeUSDCents: 0 + finalityTransferFeeBps: 0 + fastFinalityTransferFeeBps: 0 -2. Modify the `data.json` file to insert the deployed addresses of your Sender and Receiver contracts. +⚠️ Fee config is disabled for this lane. + The OnRamp will fall back to FeeQuoter defaults for this destination. -3. Install the dependencies: +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` - ```bash - npm install - ``` +Both lanes should show `isEnabled: false` and zeroed fee fields. The OnRamp will use `FeeQuoter` defaults for both directions again. -4. Set the password to encrypt your environment variables: +> **CAUTION: Troubleshooting** +> +> - **Owner revert:** confirm `KEYSTORE_NAME` is the owner of the source-chain token pool. +> - **Fee admin withdrawal revert:** confirm the fee admin was set, the correct fee admin keystore is +> used, and the action is withdrawal rather than owner-only configuration. +> - **Missing gas:** confirm the broadcaster has native gas on the source chain. +> - **Missing LINK:** confirm the sender has source-chain LINK when using `--fee-token LINK`. +> - **Missing lane:** confirm the pool lane was configured with `applyChainUpdates`. +> - **Wrong lane or pool:** confirm `--network`, `--destchain`, and the source pool +> env var (or `--tokenpool`) match the intended source -> destination lane. +> - **Invalid config:** confirm `--destgasoverhead` is not `0`, +> `--destbytesoverhead` is at least `32`, and basis points values are below `10000`. +> - **Fast-finality revert:** confirm the pool allows block-depth finality and the requested +> `finality=` is not below the pool minimum. +> - **No accrued fees:** confirm `--feetokens` includes LINK and the source token, the send +> completed, native fee token was not used, and flat or basis points fee values are non-zero. - ```bash - npx env-enc set-pw - ``` +--- -5. Set up the following environment variables: +# Set Token Pool Rate Limits Using Foundry +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry +Last Updated: 2026-04-27 - - `ETHEREUM_SEPOLIA_RPC_URL`: The RPC URL for Ethereum Sepolia testnet. You can sign up for a personal endpoint from [Alchemy](https://www.alchemy.com/), [Infura](https://www.infura.io/), or another node provider service. - - `TENDERLY_ACCOUNT_SLUG`: This is one part of your Tenderly API URL. You can [find this value in your Tenderly account](https://docs.tenderly.co/account/projects/account-project-slug). - - `TENDERLY_PROJECT_SLUG`: This is one part of your Tenderly API URL. You can [find this value in your Tenderly account](https://docs.tenderly.co/account/projects/account-project-slug). - - `TENDERLY_ACCESS_KEY`: If you don't already have one, you can [generate a new access token](https://docs.tenderly.co/account/projects/how-to-generate-api-access-token). +## Guide Versions - Input these variables using the following command: +- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry) - ```bash - npx env-enc set - ``` +- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-hardhat) -6. Generate [Typechain typings](https://www.npmjs.com/package/typechain) for the Receiver contract: +In this tutorial you will: - ```bash - npm run generate-types - ``` +1. Review current standard rate limiter configurations for your deployed token pool. +2. Update outbound and inbound standard rate limiter configurations. +3. Verify the updated configurations. +4. Test standard rate limiter enforcement with a cross-chain token transfer. +5. Optionally configure and test rate limits for faster than finality transfers. -### Introduction of the scripts +## Before You Begin -The scripts are located in the `src` directory. Each script is self-explanatory and includes comprehensive comments to explain its functionality and usage. There are three scripts: +### 1. Set up your development environment -- `estimateGasProvider.ts`: This script uses the [`eth_estimateGas`](https://docs.alchemy.com/reference/eth-estimategas) Ethereum API to estimate the gas usage of the `ccipReceive` function. It simulates sending three CCIP messages to the Receiver contract with a varying number of iterations and estimates the gas usage using the [ethers.js `estimateGas`](https://docs.ethers.org/v6/api/providers/#Provider-estimateGas) function. +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can't run `npm`, reinstall/update Node.js from the official installer. -- `estimateGasTenderly.ts`: This script leverages the Tenderly `simulate` API to estimate the gas usage of the `ccipReceive` function. Similar to the previous script, it simulates sending three CCIP messages to the Receiver contract with different numbers of iterations and estimates the gas usage using the [Tenderly `simulate` API](https://docs.tenderly.co/reference/api#tag/Simulations/operation/simulateTransaction). +2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/). You can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli). -- `helper.ts`: This script contains helper functions used by the other scripts. The two main functions are: - - `buildTransactionData`: This function constructs a CCIP message for a specified number of iterations and then returns the transaction data. - - `estimateIntrinsicGas`: Exclusively called by the `estimateGasProvider.ts` script, this function estimates the intrinsic gas of a transaction. The intrinsic gas represents the minimum amount of gas required before executing a transaction. It is determined by the transaction data and the type of transaction. Since this gas is paid by the initiator of the transaction, we use this function to estimate the intrinsic gas and then deduct it from the total gas used to isolate the gas consumed by the `ccipReceive` function. +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +``` -### Estimate gas using a Web3 provider +Verify the installation by running the following command: -Ethereum nodes implement the [`eth_estimateGas`](https://docs.alchemy.com/reference/eth-estimategas) Ethereum API to predict the gas required for a transaction's successful execution. To estimate the gas usage of the `ccipReceive` function, you can directly call the `eth_estimateGas` API via a Web3 provider or leverage a library like ethers.js, simplifying this interaction. This guide focuses on the [ethers.js `estimateGas`](https://docs.ethers.org/v6/api/providers/#Provider-estimateGas) function for gas estimation. To estimate the gas usage, execute the following command in your terminal: +```bash filename="Terminal" +ccip-cli --version +``` -```bash -npm run estimate-gas-provider +3. **Install Foundry**: + If you haven't already, follow the instructions in the [Foundry documentation](https://book.getfoundry.sh/getting-started/installation) to install Foundry. \ + Verify the installation by running the following command: + +```bash filename="Terminal" +forge --version ``` -Your output should look something like this: +4. **Clone the repository and navigate to the project directory**: -```text -$ npm run estimate-gas-provider +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-foundry) -> offchain-simulator@1.0.0 estimate-gas-provider -> ts-node src/estimateGasProvider.ts +Clone the CCIP 2.0 template for a smoother CCT setup. -Final Gas Usage Report: -Number of iterations: 0 - Gas used: 5363 -Number of iterations: 50 - Gas used: 11525 -Number of iterations: 99 - Gas used: 17564 +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/docs-cct-foundry.git +cd docs-cct-foundry ``` -The estimate may exceed the actual gas used by the transaction for various reasons, including differences in node performance and EVM mechanics. For a more precise estimation, consider using Tenderly (see the next section for details). +5. Create an encrypted Foundry keystore, if you haven't already: -### Estimate gas using Tenderly +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` -To estimate the gas usage of the `ccipReceive` function using Tenderly, execute the following command: +> **NOTE: Note** +> +> 1. For more information on how to create an encrypted Foundry keystore, refer to the +> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 2. `cast wallet import` will throw an error if a keystore with the same name already exists. +> 3. Use `cast wallet list` to check previously created keystores. -```bash -npm run estimate-gas-tenderly -``` +![Foundry keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-2.png) -Your output should look something like this: +6. Create a `.env` file by copying the `.env.example` file, and fill in the required values: -```text -$ npm run estimate-gas-tenderly +```bash filename="Terminal" +cp .env.example .env +``` -> offchain-simulator@1.0.0 estimate-gas-tenderly -> ts-node src/estimateGasTenderly.ts +```bash filename=".env" +# Keystore name (created via `cast wallet import`) +KEYSTORE_NAME=your_keystore_name -Final Gas Usage Report: -Number of iterations: 0 - Gas used: 5016 -Number of iterations: 50 - Gas used: 11166 -Number of iterations: 99 - Gas used: 17193 +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc + +# Etherscan API key (required only if you pass --verify to deployment scripts) +ETHERSCAN_API_KEY=your_etherscan_api_key ``` -### Comparison +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/HelperConfig.s.sol) -The table below summarizes the gas estimations for different iterations using both Web3 provider and Tenderly: +View the complete list of supported chains in the `HelperConfig.s.sol` file. -| Scenario | Number of iterations | Gas estimated (Web3 provider) | Gas estimated (Tenderly) | -| ------------------------ | -------------------- | ----------------------------- | ------------------------ | -| Baseline gas consumption | 0 | 5363 | 5016 | -| Average gas consumption | 50 | 11525 | 11166 | -| Peak gas consumption | 99 | 17564 | 17193 | +7. To make sure your terminal has access to these variables, run the following command: -The gas estimations from both Web3 provider and Tenderly are consistent across different iterations and align with actual testnet results. This demonstrates the accuracy and reliability of these offchain methods in estimating gas usage. However, you can notice that Tenderly provides more accurate results. +```bash filename="Terminal" +source .env +``` -## Conclusion +8. Build the project: -This tutorial has guided you through estimating the gas limit for the `ccipReceive` function using various methods. You have learned how to estimate gas usage in a local environment using Hardhat and Foundry, validate these estimations on testnets, and use offchain methods to estimate gas usage. +```bash filename="Terminal" +npm install && forge build +``` -As we have explored various methods for estimating gas for the `ccipReceive` function, it is crucial to apply this knowledge effectively. Here are some targeted recommendations to enhance your approach to gas estimation: +## Tutorial -1. **Comprehensive Testing:** Emphasize testing under diverse scenarios to ensure your gas estimations are robust. Different conditions can significantly affect gas usage, so covering as many cases as possible in your tests is crucial. -2. **Preliminary Local Estimates:** Local testing is a critical first step for estimating gas and ensuring your contracts function correctly under various scenarios. While Hardhat and Foundry facilitate development and testing, it's key to remember that these environments may not perfectly mirror your target blockchain's conditions. These initial estimates serve as valuable insights, guiding you toward more accurate gas limit settings when you proceed to testnet validations. Incorporating a buffer based on these preliminary results can help mitigate the risks of underestimating gas requirements due to environmental differences. -3. **Efficiency with Offchain Methods:** Since testing across different blockchains can be resource-intensive, leaning on offchain methods for gas estimation is invaluable. Tools such as Tenderly not only facilitate rapid and accurate gas usage insights on your target blockchains but also enable you to simulate the execution of the `ccipReceive` function on actual contracts deployed on mainnets. If Tenderly doesn't support a particular blockchain, a practical alternative is to use a Web3 provider that does support that chain, as illustrated in the [Estimate gas using a Web3 provider](#estimate-gas-using-a-web3-provider) section. This is particularly helpful when considering the diversity in gas metering rules across blockchains. This approach saves time and enhances the precision of your gas limit estimations, allowing for more cost-effective transactions from your dApp. +> **TIP: Prerequisites** +> +> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the +> registration tutorials first: +> +> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) +> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) +> +> Make sure your token and token pool addresses are exported as environment variables: +> +> ```bash +> export ETHEREUM_SEPOLIA_TOKEN=0x... +> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... +> +> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x... +> ``` +> +> This tutorial supports both CCT registration models: +> +> - In the Burn & Mint model, rate limiters are configured on the `BurnMintTokenPool` on each chain. +> - In the Lock & Mint model, rate limiters are configured on the token pools: the source-chain +> `LockReleaseTokenPool` and the destination-chain `BurnMintTokenPool`. The +> `ERC20LockBox` is not rate-limited and is not updated by the scripts in this tutorial. ---- +### 1. Review Current Rate Limiter Configurations -# Configure a sender allowlist with AdvancedPoolHooks using Foundry -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry -Last Updated: 2026-05-05 +Use the `GetCurrentRateLimits.s.sol` script to read the current rate limiter configuration state for a specific lane from your token pool. The script reports whether rate limiting is enabled, the maximum capacity (bucket size), the refill rate (tokens per second), and the current token count in each bucket. -## Guide Versions +[GetCurrentRateLimits.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/GetCurrentRateLimits.s.sol) -- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry) +View the rate limiter query script on GitHub. -- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat) +| Env var | Required | Description | +| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `DEST_CHAIN` | Yes | Remote chain whose lane is being queried (e.g. `ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1`) | +| `FAST_FINALITY` | No | Set to `true` to read the faster than finality bucket instead of the standard finality bucket (v2 pools only, default: `false`) | -CCIP 2.0 token pools support an optional [`AdvancedPoolHooks`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) contract being attached to them. +1. Check the Ethereum Sepolia pool (lane to Arbitrum Sepolia): -This contract can run certain checks: +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` -1. before tokens are locked/burned (source chain) -2. before tokens are released/minted (destination chain) +Your output should look something like this: -If no hooks contract is attached, the token pool keeps its existing behavior. +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View rate limits +Bucket: Standard finality +======================================== + +Pool Version: v2 + +Current Rate Limiter State: +Outbound Enabled: false +Outbound Capacity: 0 +Outbound Rate: 0 +Outbound Tokens: 0 +Inbound Enabled: false +Inbound Capacity: 0 +Inbound Rate: 0 +Inbound Tokens: 0 + +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +2. Check the Arbitrum Sepolia pool (lane to Ethereum Sepolia): -This tutorial covers the **sender allowlist** use case: only approved addresses may initiate outbound transfers on the source chain. A sender not on the list reverts the source transaction with `SenderNotAllowed`. For the other capabilities, see the [AdvancedPoolHooks concept page](/ccip/concepts/cross-chain-token/advanced-pool-hooks) or [Enforce ACE policies on CCIP token transfers](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry). +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` -Hooks are enforced only when **both** of these aspects are configured: +Your output should look something like this: -- The token pool points to the `AdvancedPoolHooks` contract. -- The hooks contract authorizes that token pool as a caller. +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View rate limits +Bucket: Standard finality +======================================== + +Pool Version: v2 + +Current Rate Limiter State: +Outbound Enabled: false +Outbound Capacity: 0 +Outbound Rate: 0 +Outbound Tokens: 0 +Inbound Enabled: false +Inbound Capacity: 0 +Inbound Rate: 0 +Inbound Tokens: 0 + +======================================== +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` -Authorize the pool before attaching hooks so the first hook call does not revert. Hooks are configured **per token pool, per chain**, which means that: -you need to repeat this setup for any other pool where you want hooks enforced. +> **NOTE: Rate Limiters Disabled** +> +> 1. Rate limiting is disabled by default when no rate-limiting parameters are provided during +> `ApplyChainUpdates`. In that case, both outbound and inbound capacities are `0`, meaning +> there are no restrictions on token flow. +> 2. In the next step you will enable and configure these rate limiters. -> **Note:** Remember. Your CCT token can be deployed on multiple chains, with each chain having its own token pool. +### 2. Update Rate Limiter Configurations -In this tutorial you will: +Use the `UpdateRateLimiters.s.sol` script to update rate limiter configurations for an existing lane in your token pool. The direction to update is inferred automatically from whichever `OUTBOUND_*` / `INBOUND_*` environment variables you set. -1. Review the current hooks state on your deployed token pool. -2. Send a baseline transfer (*with no hooks attached*). This should succeed. -3. Deploy an `AdvancedPoolHooks` contract with your deployer address on the allowlist. -4. Authorize the pool as a caller on the hooks contract and attach the hooks contract to the token pool. -5. Verify the hooks attachment, authorized callers, and allowlist state. -6. Remove your address from the allowlist. -7. Attempt a transfer (*expected to revert with `SenderNotAllowed(address)`*). -8. Detach the hooks contract. -9. Send a transfer again (*expected to succeed*). -10. Manage the allowlist over time (add, remove, and check addresses). +Only the directions you specify are updated; any direction you don't specify is left unchanged. Outbound and inbound are relative to the pool you're configuring: -## Before You Begin +1. `outbound` is tokens leaving this pool toward `DEST_CHAIN`; +2. `inbound` is tokens arriving from `DEST_CHAIN` into this pool. -## Tutorial +> **NOTE: Lane must exist** +> +> 1. Rate limiters can only be updated for lanes that already exist. +> 2. If you haven't configured the lane yet, complete one of the two tutorials first: +> +> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) +> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) +> 3. Run `ApplyChainUpdates.s.sol` to configure the lane. ---- +[UpdateRateLimiters.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/UpdateRateLimiters.s.sol) -# Configure a sender allowlist with AdvancedPoolHooks using Hardhat -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat -Last Updated: 2026-05-05 +View the rate limiter update script on GitHub. -## Guide Versions +| Env var | Required | Description | +| ------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `DEST_CHAIN` | Yes | Remote chain whose lane is being updated | +| `OUTBOUND_RATE_LIMIT_CAPACITY` | To update outbound | Token bucket capacity for outbound transfers | +| `OUTBOUND_RATE_LIMIT_RATE` | To update outbound | Token bucket refill rate (tokens/second) for outbound transfers | +| `OUTBOUND_RATE_LIMIT_ENABLED` | No | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when CAPACITY or RATE are set) | +| `INBOUND_RATE_LIMIT_CAPACITY` | To update inbound | Token bucket capacity for inbound transfers | +| `INBOUND_RATE_LIMIT_RATE` | To update inbound | Token bucket refill rate (tokens/second) for inbound transfers | +| `INBOUND_RATE_LIMIT_ENABLED` | No | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when CAPACITY or RATE are set) | +| `FAST_FINALITY` | No | `true` to update the faster than finality bucket instead of the standard finality bucket (v2 only, default: `false`) | -- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry) +> **NOTE: Access control and compatibility** +> +> 1. The pool owner and the designated `rateLimitAdmin` can update rate limiters. +> 2. The `rateLimitAdmin` is configured via `SetDynamicConfig.s.sol`. +> 3. This script supports both v1 and v2 token pools: on v2 pools it calls `setRateLimitConfig()`; on v1 +> pools it falls back to `setChainRateLimiterConfig()`. +> 4. The `FAST_FINALITY` flag is v2-only and is ignored on v1 pools. -- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat) +> **NOTE: Configuration best practices** +> +> 1. Set inbound rate limits a bit higher than outbound limits on the same lane. Many chains finalize blocks in +> batches, so multiple outbound messages may become executable on the destination simultaneously, requiring slightly +> more inbound capacity. +> 2. The exact additional capacity required depends on the refill rate and the size of the source chain epochs and CCIP +> round time. In most cases, a 5-10% buffer should be sufficient. +> 3. `rate` must be less than or equal to `capacity`, and when disabling rate limiting ( +> `isEnabled: false`), both `capacity` and `rate` must be `0`. -CCIP 2.0 token pools support an optional [`AdvancedPoolHooks`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) contract being attached to them. +In this example, we configure an 18-decimal token with outbound capacity of 10 tokens (refilling at 0.1 tokens/sec) and inbound capacity of 11 tokens (refilling at 0.11 tokens/sec, a 10% buffer): -This contract can run certain checks: +1. Update the Ethereum Sepolia pool (lane to Arbitrum Sepolia): -1. before tokens are locked/burned (source chain) -2. before tokens are released/minted (destination chain) +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + OUTBOUND_RATE_LIMIT_CAPACITY=10000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=11000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=110000000000000000 \ + forge script \ + script/configure/rate-limiter/UpdateRateLimiters.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` -If no hooks contract is attached, the token pool keeps its existing behavior. +Your output should look something like this: -This tutorial covers the **sender allowlist** use case: only approved addresses may initiate outbound transfers on the source chain. A sender not on the list reverts the source transaction with `SenderNotAllowed`. For the other capabilities, see the [AdvancedPoolHooks concept page](/ccip/concepts/cross-chain-token/advanced-pool-hooks) or [Enforce ACE policies on CCIP token transfers](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-hardhat). +```bash filename="Terminal" +======================================== +⚡️ Update Rate Limiters +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Update rate limits +Direction: Outbound + Inbound +Bucket: Standard finality +======================================== + +Pool Version: v2 (setRateLimitConfig) + +Current Rate Limiter State: +Outbound Enabled: false +Outbound Capacity: 0 +Outbound Rate: 0 +Outbound Tokens: 0 +Inbound Enabled: false +Inbound Capacity: 0 +Inbound Rate: 0 +Inbound Tokens: 0 + +New Configuration: +Outbound Enabled: true +Outbound Capacity: 10000000000000000000 +Outbound Rate: 100000000000000000 +Inbound Enabled: true +Inbound Capacity: 11000000000000000000 +Inbound Rate: 110000000000000000 + +======================================== +✅ Rate limiter update complete on Ethereum Sepolia! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +2. Update the Arbitrum Sepolia pool (lane to Ethereum Sepolia): -Hooks are enforced only when **both** of these aspects are configured: +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + OUTBOUND_RATE_LIMIT_CAPACITY=10000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=11000000000000000000 \ + INBOUND_RATE_LIMIT_RATE=110000000000000000 \ + forge script \ + script/configure/rate-limiter/UpdateRateLimiters.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` -- The token pool points to the `AdvancedPoolHooks` contract. -- The hooks contract authorizes that token pool as a caller. +Your output should look something like this: -Authorize the pool before attaching hooks so the first hook call does not revert. Hooks are configured **per token pool, per chain**, which means that: -you need to repeat this setup for any other pool where you want hooks enforced. +```bash filename="Terminal" +======================================== +⚡️ Update Rate Limiters +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Update rate limits +Direction: Outbound + Inbound +Bucket: Standard finality +======================================== + +Pool Version: v2 (setRateLimitConfig) + +Current Rate Limiter State: +Outbound Enabled: false +Outbound Capacity: 0 +Outbound Rate: 0 +Outbound Tokens: 0 +Inbound Enabled: false +Inbound Capacity: 0 +Inbound Rate: 0 +Inbound Tokens: 0 + +New Configuration: +Outbound Enabled: true +Outbound Capacity: 10000000000000000000 +Outbound Rate: 100000000000000000 +Inbound Enabled: true +Inbound Capacity: 11000000000000000000 +Inbound Rate: 110000000000000000 + +======================================== +✅ Rate limiter update complete on Arbitrum Sepolia! +======================================== +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` + +3. To **Disable** rate limiters for a lane, set `OUTBOUND_RATE_LIMIT_ENABLED=false` and `INBOUND_RATE_LIMIT_ENABLED=false`: + +> **CAUTION: Disabling rate limiters** +> +> 1. We highly recommend that devs have at least some limits set up. We advise against completely disabling rate +> limiters unless absolutely necessary. +> 2. **Informational-only for this tutorial:** If you disable rate limiters here, the later +> **Test the Rate Limiter Configurations** section will not behave as described (it will not revert +> with `TokenMaxCapacityExceeded` / `TokenRateLimitReached`). -> **Note:** Remember. Your CCT token can be deployed on multiple chains, with each chain having its own token pool. +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + OUTBOUND_RATE_LIMIT_ENABLED=false \ + INBOUND_RATE_LIMIT_ENABLED=false \ + forge script \ + script/configure/rate-limiter/UpdateRateLimiters.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` -In this tutorial you will: +### 3. Verify the Updated Rate Limiter Configurations -1. Review the current hooks state on your deployed token pool. -2. Send a baseline transfer (*with no hooks attached*). This should succeed. -3. Deploy an `AdvancedPoolHooks` contract with your deployer address on the allowlist. -4. Authorize the pool as a caller on the hooks contract and attach the hooks contract to the token pool. -5. Verify the hooks attachment, authorized callers, and allowlist state. -6. Remove your address from the allowlist. -7. Attempt a transfer (*expected to revert with `SenderNotAllowed(address)`*). -8. Detach the hooks contract. -9. Send a transfer again (*expected to succeed*). -10. Manage the allowlist over time (add, remove, and check addresses). +Re-run the `GetCurrentRateLimits.s.sol` script on both chains to confirm the update was applied correctly. -## Before You Begin +1. Verify the Ethereum Sepolia pool: -## Tutorial +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` ---- +> **NOTE: Note** +> +> Your output might differ based on whether or not you disabled rate limiters in the previous step. -# Enforce ACE policies on CCIP token transfers using Foundry -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry -Last Updated: 2026-09-26 +Your output should look something like this: -## Guide Versions +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View rate limits +Bucket: Standard finality +======================================== + +Pool Version: v2 + +Current Rate Limiter State: +Outbound Enabled: true +Outbound Capacity: 10000000000000000000 +Outbound Rate: 100000000000000000 +Outbound Tokens: 10000000000000000000 +Inbound Enabled: true +Inbound Capacity: 11000000000000000000 +Inbound Rate: 110000000000000000 +Inbound Tokens: 11000000000000000000 + +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +2. Verify the Arbitrum Sepolia pool: -- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry) +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_SEPOLIA \ + forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` -- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-hardhat) +Your output should look something like this: -[`AdvancedPoolHooks`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) can forward each transfer to a [Chainlink ACE](/ace) Policy Engine for evaluation before the source token pool locks or burns tokens (`preflightCheck`) and before the destination token pool releases or mints tokens (`postflightCheck`). If a policy rejects the call, the hook reverts and the transfer does not proceed. +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View rate limits +Bucket: Standard finality +======================================== -This tutorial covers the **ACE policy enforcement** use case on a working CCT lane. For the sender allowlist use case instead, see [Configure a sender allowlist with AdvancedPoolHooks](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry). For how the hook, engine, extractor, and policies fit together, read the [AdvancedPoolHooks concept page](/ccip/concepts/cross-chain-token/advanced-pool-hooks). +Pool Version: v2 -In this tutorial you will: +Current Rate Limiter State: +Outbound Enabled: true +Outbound Capacity: 10000000000000000000 +Outbound Rate: 100000000000000000 +Outbound Tokens: 10000000000000000000 +Inbound Enabled: true +Inbound Capacity: 11000000000000000000 +Inbound Rate: 110000000000000000 +Inbound Tokens: 11000000000000000000 -1. Reuse an existing working CCT lane between Ethereum Sepolia and Arbitrum Sepolia. -2. Deploy `AdvancedPoolHooks` on both chains with their Policy Engine addresses set, and attach them to both token pools. -3. Point the hooks at their Policy Engines with `setPolicyEngine` when the engine was not set at deployment or needs to change. -4. Complete the ACE Platform setup: target detection, contract-type assignment, policy creation, and extractor mappings. -5. Demonstrate a source `preflightCheck` rejection using the `from` parameter. -6. Demonstrate a successful unrestricted transfer. -7. Demonstrate a destination `postflightCheck` rejection using the `to` parameter. -8. Update the destination policy and manually execute the failed message. +======================================== +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` -> **NOTE: What this tutorial does not repeat** +> **NOTE: Token bucket values** > -> Token and pool deployment, admin registration, and lane configuration: complete a registration tutorial first (see -> [Confirm prerequisites](#confirm-prerequisites-addresses-and-permissions)). -> -> The ACE Platform workflow (engine creation, target detection, contract-type assignment, policy instances, -> protections, extractor mappings): follow -> [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools) for each step. +> The `Tokens` field shows the current fill level of the bucket. Immediately after an update the bucket +> resets to full capacity. Over time, as transfers consume tokens, this value decreases and refills at the configured +> rate. -## Before You Begin +### 4. Test the Rate Limiter Configurations -> **CAUTION: ACE Beta access required** +Initiate a cross-chain transfer to verify that the rate limiter enforces the configured limits. + +> **NOTE: Token balance** > -> ACE is currently available through the Beta program. [Contact us](https://chain.link/contact) to request -> access or schedule a demo. After ACE is enabled for your organization, complete the [ACE account -> setup](/ace/getting-started/account-setup) before continuing. +> Ensure your EOA has a sufficient balance of the cross-chain token on the source chain. If you need more tokens, use +> the `MintTokens.s.sol` script or transfer tokens from another account. -## Tutorial +Before testing, export the router and token addresses for the source chain: ---- +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x... +export ETHEREUM_SEPOLIA_TOKEN=0x... +``` -# Enforce ACE policies on CCIP token transfers using Hardhat -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-hardhat -Last Updated: 2026-09-26 +> **NOTE: Router address** +> +> You can find the router address for your chain in the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). -## Guide Versions +> **Note:** +> +> 1. Add `--fee-token LINK` to pay CCIP fees in LINK. If you omit this flag, fees are paid in the native gas token. +> 2. Use `--receiver
    ` for a custom recipient. If omitted on EVM-to-EVM, the receiver defaults to the sender. -- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry) +#### Test 1: Exceed capacity -- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-hardhat) +Transfer one wei more than the outbound capacity (10 tokens + 1 wei). This should fail because the requested amount exceeds the bucket size. -[`AdvancedPoolHooks`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) can forward each transfer to a [Chainlink ACE](/ace) Policy Engine for evaluation before the source token pool locks or burns tokens (`preflightCheck`) and before the destination token pool releases or mints tokens (`postflightCheck`). If a policy rejects the call, the hook reverts and the transfer does not proceed. +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10.000000000000000001 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` -This tutorial covers the **ACE policy enforcement** use case on a working CCT lane. For the sender allowlist use case instead, see [Configure a sender allowlist with AdvancedPoolHooks](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat). For how the hook, engine, extractor, and policies fit together, read the [AdvancedPoolHooks concept page](/ccip/concepts/cross-chain-token/advanced-pool-hooks). +Your output should look something like this: -In this tutorial you will: +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +error: Failed to call "ccipSend" +Error = execution reverted: TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress) +Revert.error = TokenMaxCapacityExceeded(uint256,uint256,address) +Revert.capacity = 10000000000000000000n +Revert.requested = 10000000000000000001n +Revert.tokenAddress = 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +``` -1. Reuse an existing working CCT lane between Ethereum Sepolia and Arbitrum Sepolia. -2. Deploy `AdvancedPoolHooks` on both chains with their Policy Engine addresses set, and attach them to both token pools. -3. Point the hooks at their Policy Engines with `setPolicyEngine` when the engine was not set at deployment or needs to change. -4. Complete the ACE Platform setup: target detection, contract-type assignment, policy creation, and extractor mappings. -5. Demonstrate a source `preflightCheck` rejection using the `from` parameter. -6. Demonstrate a successful unrestricted transfer. -7. Demonstrate a destination `postflightCheck` rejection using the `to` parameter. -8. Update the destination policy and manually execute the failed message. +The transfer should fail with `TokenMaxCapacityExceeded` because the requested amount is larger than the bucket capacity. -> **NOTE: What this tutorial does not repeat** +> **NOTE: How does the token bucket work?** > -> Token and pool deployment, admin registration, and lane configuration: complete a registration tutorial first (see -> [Confirm prerequisites](#confirm-prerequisites-addresses-and-permissions)). +> CCIP rate limiters use a model. Think of the bucket as a virtual allowance that controls how +> many tokens can flow through the pool in a given window: > -> The ACE Platform workflow (engine creation, target detection, contract-type assignment, policy instances, -> protections, extractor mappings): follow -> [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools) for each step. - -## Before You Begin - -> **CAUTION: ACE Beta access required** +> 1. The bucket starts full at its configured **capacity**. +> 2. Each transfer **consumes** its amount from the bucket. +> 3. The bucket **refills** continuously at the configured **rate** (tokens per second). > -> ACE is currently available through the Beta program. [Contact us](https://chain.link/contact) to request -> access or schedule a demo. After ACE is enabled for your organization, complete the [ACE account -> setup](/ace/getting-started/account-setup) before continuing. - -## Tutorial +> In this tutorial, the outbound bucket has a capacity of 10 tokens and refills at 0.1 tokens/sec. Transferring all 10 +> tokens in one go "drains" the bucket to zero. Any immediate follow-up transfer will fail because the bucket needs 100 +> seconds (10 ÷ 0.1) to fully replenish. ---- +#### Test 2: Drain the bucket, then hit the rate -# Cross-Chain Token (CCT) Tutorials -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens -Last Updated: 2025-05-19 +1. **First transfer** (succeeds: drains the outbound bucket): -> **NOTE: Prerequisites** -> -> Familiarize yourself with the [CCT standard](/ccip/concepts/cross-chain-token/overview) and [CCIP -> architecture](/ccip/concepts/architecture/overview) before proceeding with these tutorials. +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` -Before diving into the [tutorials](#tutorials), it's important first to understand the overall procedure for enabling your tokens in CCIP. This procedure involves deploying tokens and token pools, registering administrative roles, and configuring token pools to enable secure token transfers using CCIP. The diagram below outlines the entire process: +Your output should look something like this: -![Process for enabling a token in CCIP.](/images/ccip/CCIP_enabled_tokens_flowchart.jpg) +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` -### Understanding the Procedure +2. **Second transfer** (fails: bucket has not refilled): -The steps in the diagram highlight the flow of actions needed to enable a token for cross-chain transfers. These steps will be the foundation of the tutorials. Whether you're working with an Externally Owned Account (EOA) or a **Smart Account** (such as one using a multisig scheme), the overall logic remains the same. You'll follow the same process to enable cross-chain token transfers, configure pools, and register administrative roles. +Run the same command immediately: -In the following tutorials, we will walk through each step of the process to give you hands-on experience, from deploying your token to registering and configuring token pools. The process will apply equally whether you use an EOA or a Smart Account (such as with multisig transactions), ensuring flexibility across different account types. +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` -### Key Steps to Keep in Mind: +Your output should look something like this: -1. **Token Deployment**: If the token is not yet deployed, you'll deploy an ERC20-compatible token. -2. **Admin Registration**: The token administrator must be registered in the [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol) via self-service. -3. **Pool Deployment and Configuration**: Token pools are deployed, linked to tokens, and configured to manage cross-chain token transfers. +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +error: Failed to call "ccipSend" +Error = execution reverted: TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress) +Revert.error = TokenRateLimitReached(uint256,uint256,address) +Revert.minWaitInSeconds = 100n +Revert.available = 0n +Revert.tokenAddress = 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +``` -The tutorials will implement the logic of this process, which involves deploying and configuring token pools and registering administrative roles, step-by-step. +The transfer should fail with `TokenRateLimitReached` because the bucket has not yet refilled. With a capacity of 10 tokens and a rate of 0.1 tokens per second, it takes 100 seconds to fully replenish. -## Tutorials +### 5. (Optional) Configure Rate Limits for Faster Than Finality Transfers -- [Register from an EOA (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry): Register a cross-chain token with the Burn & Mint mechanism using an EOA. Hardhat and Foundry versions available. -- [Register from an EOA (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry): Register a cross-chain token with the Lock & Mint mechanism using an EOA. Hardhat and Foundry versions available. -- [Set Token Pool Rate Limits](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry): Set rate limits for token pools to control cross-chain token transfer flow. Hardhat and Foundry versions available. -- [Set Token Transfer Fee Config](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry): Configure token transfer fee parameters directly on your v2 token pool, overriding FeeQuoter defaults. Hardhat and Foundry versions available. -- [Configure a Sender Allowlist with AdvancedPoolHooks](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry): Deploy and attach AdvancedPoolHooks to your v2 token pool to restrict outbound transfers to approved sender addresses. Hardhat and Foundry versions available. -- [Enforce ACE Policies on CCIP Token Transfers](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry): Connect source and destination AdvancedPoolHooks to Chainlink ACE Policy Engines and exercise preflight and postflight rejections with live transfers. Hardhat and Foundry versions available. +This section applies to [`TokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) v2.0 or later only. CCIP 2.0 token pools maintain two independent rate limiter buckets per lane: ---- +- **Standard finality bucket**: Used for standard finality transfers (`finality=finalized`, which is the default when you omit `--extra finality=...`). This is the bucket you configured in the previous steps. +- **Faster Than Finality bucket**: Used for transfers that request faster than finality with `finality=` (numeric block depth). If this bucket is disabled for a direction, faster than finality transfers for that direction fall back to the standard bucket (enforced on-chain by the token pool). -# Migrate your CCT token pools from CCIP v1 to v2 (Burn & Mint) using Foundry -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry -Last Updated: 2026-06-11 +> **NOTE: Pool type and lockbox scope** +> +> Rate limiters are configured on token pools, regardless of whether you registered your token with Burn & Mint or +> Lock & Mint. In Lock & Mint, the `ERC20LockBox` is not rate-limited and is not updated by these +> scripts. -## Guide Versions +> **CAUTION: Faster Than Finality must be allowed first** +> +> 1. Updating the faster than finality bucket alone does **not** enable faster than finality transfers. +> The pool owner must also configure the pool's **allowed finality config** with +> `SetFinalityConfig.s.sol`. +> 2. Setting the allowed finality config is owner-only. Updating rate limiters can be done by the pool owner or the +> configured `rateLimitAdmin`. +> 3. For the full faster than finality setup flow, see the Configure Faster Than Finality section in either +> registration tutorial: +> +> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry#optional-configure-faster-than-finality-for-the-token-pool) +> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#optional-configure-fast-finality-for-the-token-pool) -- [Foundry (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry) +#### Check the pool's allowed finality config -- [Hardhat (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat) +Use `GetFinalityConfig.s.sol` to confirm which faster than finality modes the pool currently allows. -This migration guide helps you upgrade a deployed **Burn & Mint** Cross-Chain Token (CCT) setup from **CCIP v1 (1.5.x / 1.6.x)** to **CCIP 2.0**, while keeping your existing token addresses and minimizing disruption. +[GetFinalityConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/finality-config/GetFinalityConfig.s.sol) -This guide uses Foundry scripts inside our [docs-cct-foundry](https://github.com/smartcontractkit/docs-cct-foundry) repository. +View the finality config query script on GitHub. -> **NOTE: Finality and v2 features are optional** +1. Check the Ethereum Sepolia pool: -In this guide you will: +```bash filename="Terminal" +forge script \ + script/configure/finality-config/GetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` -1. Confirm prerequisites and record your existing CCIP v1 deployment details. -2. Deploy new CCIP 2.0 `Burn Mint` token pools on each chain. -3. Configure the v2 pools to accept messages from **both** the old v1 pools and the new v2 pools. -4. Cut over CCIP routing by calling [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol).setPool on each chain. -5. Validate transfers end-to-end, then optionally clean up legacy configuration. +Your output should look something like this: -## Before You Begin +```bash filename="Terminal" +======================================== +⏱️ Get Finality Config +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View finality config +======================================== -## Migration Guide +Allowed Finality Config (raw): 0x00000000 -> **TIP: Set these variables once for cleaner copy/paste** -> -> This guide uses explicit addresses so you don’t need local `script/deployments/` files. Start by exporting -> your token addresses, then we’ll export the v1/v2 pool addresses as we discover them. -> -> ```bash filename="Terminal" -> export ETHEREUM_SEPOLIA_TOKEN=0x... -> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... -> ``` +Mode: WAIT_FOR_FINALITY (default) +Full finality is required. Faster Than Finality transfers are disabled. ---- +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` -# Migrate your CCT token pools from CCIP v1 to v2 (Burn & Mint) using Hardhat -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat -Last Updated: 2026-06-11 +`BLOCK_DEPTH (32)` sets a minimum for numeric requests: `--extra finality=32` (or higher) is allowed; `--extra finality=10` reverts with `InvalidRequestedFinality(requestedFinality, allowedFinality)`. -## Guide Versions +2. Check the Arbitrum Sepolia pool: -- [Foundry (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry) +```bash filename="Terminal" +forge script \ + script/configure/finality-config/GetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` -- [Hardhat (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat) +#### Approach 1: Allow Faster Than Finality and Configure the Faster Than Finality Bucket in One Command -This migration guide helps you upgrade a deployed Burn & Mint Cross-Chain Token (CCT) setup from CCIP v1 (1.5.x / 1.6.x) to CCIP 2.0, while keeping your existing token addresses and minimizing disruption. +Use `SetFinalityConfig.s.sol` with the rate limit environment variables to allow faster than finality and configure the faster than finality bucket in a single broadcast. -> **NOTE: Finality and v2 features are optional** +[SetFinalityConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/finality-config/SetFinalityConfig.s.sol) -In this guide you will: +View the finality config update script on GitHub. -1. Confirm prerequisites and record your existing CCIP v1 deployment details. -2. Deploy new CCIP 2.0 `Burn Mint` token pools on each chain. -3. Configure the v2 pools to accept messages from both the old v1 pools and the new v2 pools. -4. Cut over CCIP routing by calling [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol).setPool on each chain. -5. Validate transfers end-to-end, then optionally clean up legacy configuration. +This example configures the Ethereum Sepolia pool for transfers to Arbitrum Sepolia. To configure the reverse direction, run the same script on Arbitrum Sepolia with `DEST_CHAIN=ETHEREUM_SEPOLIA`. -## Before You Begin +```bash filename="Terminal" +BLOCK_DEPTH=32 \ + DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + OUTBOUND_RATE_LIMIT_CAPACITY=5000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=50000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=5500000000000000000 \ + INBOUND_RATE_LIMIT_RATE=55000000000000000 \ + forge script \ + script/configure/finality-config/SetFinalityConfig.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` -## Migration Guide +Your output should look something like this: -> **TIP: Set these variables once for cleaner copy/paste** -> -> This guide uses explicit addresses so you don't need local `deployments/` files. Start by exporting your -> token addresses, then we'll export the v1/v2 pool addresses as we discover them. +> **NOTE: Output abbreviated** > -> ```bash filename="Terminal" -> # Token addresses (existing deployments) -> export ETHEREUM_SEPOLIA_TOKEN=0x... -> export ARBITRUM_SEPOLIA_TOKEN=0x... -> ``` +> `SetFinalityConfig.s.sol` also prints the lane's current and updated rate limits (faster than finality +> where enabled, standard otherwise). Those sections are omitted here for brevity. ---- +```bash filename="Terminal" +======================================== +⏱️ Set Finality Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set finality config +======================================== + + Current Finality Config: 0x00000000 + New Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + +[Step 1] Setting finality config on Ethereum Sepolia +✅ Finality config set successfully! + +[Step 2] Updating rate limits (faster than finality bucket) on Ethereum Sepolia -> Arbitrum Sepolia +New Configuration: + Outbound Enabled: true + Outbound Capacity: 5000000000000000000 + Outbound Rate: 50000000000000000 + Inbound Enabled: true + Inbound Capacity: 5500000000000000000 + Inbound Rate: 55000000000000000 + +✅ Rate limits updated successfully! + +======================================== +✅ Configuration Complete on Ethereum Sepolia! +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +#### Approach 2: Update the Faster Than Finality Bucket Directly + +If the pool already allows faster than finality, you can update the faster than finality bucket directly using `UpdateRateLimiters.s.sol` with `FAST_FINALITY=true`: -# Migrate your CCT token pools from CCIP v1 to v2 (Lock & Mint) using Foundry -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-lock-mint-foundry -Last Updated: 2026-06-11 +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + FAST_FINALITY=true \ + OUTBOUND_RATE_LIMIT_CAPACITY=5000000000000000000 \ + OUTBOUND_RATE_LIMIT_RATE=50000000000000000 \ + INBOUND_RATE_LIMIT_CAPACITY=5500000000000000000 \ + INBOUND_RATE_LIMIT_RATE=55000000000000000 \ + forge script \ + script/configure/rate-limiter/UpdateRateLimiters.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast +``` -This migration guide helps you upgrade a deployed **Lock & Mint** Cross-Chain Token (CCT) setup from **CCIP v1 (1.5.x / 1.6.x)** to **CCIP 2.0**, while keeping your existing token addresses and minimizing disruption. +#### Verify the faster than finality bucket -This guide uses Foundry scripts inside our [docs-cct-foundry](https://github.com/smartcontractkit/docs-cct-foundry) repository. +Pass `FAST_FINALITY=true` to `GetCurrentRateLimits.s.sol`. Each direction is shown independently: the faster than finality bucket for directions where it is enabled, and the standard bucket fallback for directions where it is not. -> **NOTE: Lock & Mint is asymmetric** +> **NOTE: Foundry output label** > -> In a Lock & Mint deployment the two chains are **not** symmetric. The canonical (source) chain runs a **Lock & -> Release** pool backed by an **ERC20 lockbox** that holds the locked liquidity; the destination chain runs a **Burn & -> Mint** pool. Because of this, the migration has an extra step that Burn & Mint does not: you must **move the locked -> liquidity** out of the old v1 pool and into the new v2 lockbox. +> Some current Foundry output labels this bucket as `Custom finality`. In this tutorial, that label refers to +> the faster than finality bucket selected by `FAST_FINALITY=true`. -> **NOTE: Finality and v2 features are optional** +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + FAST_FINALITY=true \ + forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` -In this guide you will: +Your output should look something like this: -1. Confirm prerequisites and record your existing CCIP v1 deployment details, including the locked liquidity on the canonical chain. -2. Deploy the new CCIP 2.0 pools: an [`ERC20LockBox`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/ERC20LockBox.sol) + [`LockReleaseTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/LockReleaseTokenPool.sol) on the canonical chain, and a [`BurnMintTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol) on the destination chain. -3. Pause the old v1 pools and migrate the locked liquidity from the v1 Lock & Release pool into the new lockbox. -4. Configure the v2 pools to accept messages from **both** the old v1 pools and the new v2 pools. -5. Cut over CCIP routing by calling [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol).setPool on each chain, validate transfers end-to-end, then optionally clean up legacy configuration. +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View rate limits +Bucket: Custom finality (standard finality fallback per direction if not enabled) +======================================== + +Pool Version: v2 + +Outbound [custom finality]: +Enabled: true +Capacity: 5000000000000000000 +Rate: 50000000000000000 +Tokens: 5000000000000000000 +Inbound [custom finality]: +Enabled: true +Capacity: 5500000000000000000 +Rate: 55000000000000000 +Tokens: 5500000000000000000 + +======================================== +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +#### Test a faster than finality transfer + +After the pool allows faster than finality and the faster than finality bucket is configured, send a small transfer with numeric block depth (for example `--extra finality=32`): -## Before You Begin +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1 \ + --receiver 0xYourReceiverAddress \ + --extra finality=32 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` -## Migration Guide +Your output should look something like this: -> **TIP: Set these variables once for cleaner copy/paste** -> -> This guide uses explicit addresses so you don’t need local `script/deployments/` files. Start by exporting -> your token addresses; we’ll export the v1/v2 pool addresses and the lockbox address as we discover or deploy them. +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x5f1c2d3e4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e , messageId => 0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2c4d +CCIP Explorer: https://ccip.chain.link/msg/0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2c4d +``` + +Re-run `GetCurrentRateLimits.s.sol` with `FAST_FINALITY=true` to confirm the outbound faster than finality bucket changed after the transfer: + +```bash filename="Terminal" +DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ + FAST_FINALITY=true \ + forge script \ + script/configure/rate-limiter/GetCurrentRateLimits.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +> **NOTE: Faster Than Finality troubleshooting** > -> ```bash filename="Terminal" -> export ETHEREUM_SEPOLIA_TOKEN=0x... -> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... -> ``` +> 1. This test assumes the pool's allowed finality config allows numeric block depth. If the transfer reverts with +> `InvalidRequestedFinality(requestedFinality, allowedFinality)`, run +> `GetFinalityConfig.s.sol` and confirm the configured `BLOCK_DEPTH`, then retry with +> `--extra finality=32` (or higher than the configured minimum). +> 2. If you request a numeric value lower than the configured minimum block depth, the token pool will revert. --- -# Enable your tokens in CCIP (Burn & Mint): Register from an EOA using Foundry -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry -Last Updated: 2025-10-30 +# Set Token Pool Rate Limits Using Hardhat +Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-hardhat +Last Updated: 2026-04-27 ## Guide Versions -- [Foundry (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) +- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry) -- [Hardhat (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) +- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-hardhat) In this tutorial you will: -1. Deploy a token on two chains using [Foundry](https://book.getfoundry.sh/). -2. Set up a `Burn Mint` token pool on each chain. -3. Configure and activate the pools in CCIP so your token becomes a Cross-Chain Token (CCT). -4. (Optional) Test the setup by minting and transferring tokens across networks. - -> **NOTE: If you do not explicitly configure these features, default behavior applies** +1. Review current standard rate limiter configurations for your deployed token pool. +2. Update outbound and inbound standard rate limiter configurations. +3. Verify the updated configurations. +4. Test standard rate limiter enforcement with a cross-chain token transfer. +5. Optionally configure and test rate limits for faster than finality transfers. ## Before You Begin -## Tutorial +### 1. Set up your development environment -### Prerequisites +1. Install Node.js and npm: + - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/). + - `npm` is bundled with Node.js. If you can’t run `npm`, reinstall/update Node.js from the official installer. -> **CAUTION: Before you continue** -> -> **Sepolia ETH**: required for Sepolia transactions (deploy/configure/send). -> -> **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions (deploy/configure). -> -> **LINK (optional)**: if you plan to pay for the cross-chain transfer with `LINK`, fund the sender -> wallet with testnet LINK on the **source chain**. -> -> You can use our [faucet](https://faucets.chain.link/) to get testnet LINK. +2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/). You can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli). -**Already have deployments? Skip only what you already have:** +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +``` -- If you already have tokens deployed on **both** chains, skip **Deploy Tokens**. -- If you have deployed tokens but not the corresponding pools, you must still run **Deploy Token Pools** before continuing. -- If you already have tokens and **V2-compatible** token pools deployed on **both** chains, skip both sections and move straight on to [Enablement](#enablement). +Verify the installation by running the following command: -**If you already have a V1 token pool deployed and want to use newer features or prepare for lane migration:** +```bash filename="Terminal" +ccip-cli --version +``` -- You will need to deploy a **V2-compatible token pool** -- Your existing V1 pool can continue to support current transfers -- Follow the migration guide to upgrade your setup without disrupting your existing deployment +3. **Clone the repository and navigate to the project directory**: -> **Read**: [Migrate from V1 to V2 token pools](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry) +[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-hardhat) -Before continuing, export the relevant addresses so subsequent scripts can find them: +Clone the CCIP 2.0 Hardhat template for a smoother CCT setup. ```bash filename="Terminal" -export ETHEREUM_SEPOLIA_TOKEN= -export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN= +git clone https://github.com/smartcontractkit/docs-cct-hardhat.git +cd docs-cct-hardhat +``` -export ETHEREUM_SEPOLIA_TOKEN_POOL= -export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL= +4. Create a `.env` file by copying the `.env.example` file, and fill in the required values: + +```bash filename="Terminal" +cp .env.example .env ``` -### Enablement +```bash filename=".env" +# Keystore name (created via `npx hardhat keystore set`) +KEYSTORE_NAME= -These steps configure your token pools and activate them under the CCIP protocol so your token becomes a fully registered Cross-Chain Token (CCT). +# RPC URLs (add the ones you need) +ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc +ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc -### Cross-chain Token Operations +# Etherscan API key (required only if you pass --verify to deployment tasks) +ETHERSCAN_API_KEY=your_etherscan_api_key +``` -If all the previous steps were successful, your CCT token is now fully registered and activated under the CCIP protocol. -Use these next steps to mint and transfer your tokens across networks. +[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/helper-config.ts) ---- +View the complete list of supported chains in the `helper-config.ts` file. -# Enable your tokens in CCIP (Burn & Mint): Register from an EOA using Hardhat -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat -Last Updated: 2026-04-24 +5. To make sure your terminal has access to these variables, run the following command: -## Guide Versions +```bash filename="Terminal" +source .env +``` -- [Foundry (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) +6. Build the project: -- [Hardhat (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) +```bash filename="Terminal" +npm install && npx hardhat compile +``` -In this tutorial you will: +7. Create an encrypted Hardhat keystore, if you haven't already: -1. Deploy a token on two chains using [Hardhat](https://hardhat.org/). -2. Set up a `Burn Mint` token pool on each chain. -3. Configure and activate the pools in CCIP so your token becomes a Cross-Chain Token (CCT). -4. (Optional) Test the setup by minting and transferring tokens across networks. +```bash filename="Terminal" +npx hardhat keystore set +``` -> **NOTE: If you do not explicitly configure these features, default behavior applies** +> **CAUTION: Private key format** +> +> When prompted for your private key, include the `0x` prefix. -## Before You Begin +> **NOTE: Note** +> +> 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it +> asks you to enter your private key as hidden text. +> 2. The name you choose (e.g. ``) is what you set as +> `KEYSTORE_NAME` in `.env`. +> 3. Use `npx hardhat keystore list` to check previously created keystores. ## Tutorial -### Prerequisites - -> **CAUTION: Before you continue** +> **TIP: Prerequisites** > -> **Sepolia ETH**: required for Sepolia transactions (deploy/configure/send). +> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the +> registration tutorials first: +> +> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) +> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) +> +> Make sure your token and token pool addresses are exported as environment variables: +> +> ```bash +> # For tokens +> export ETHEREUM_SEPOLIA_TOKEN=0x... +> export ARBITRUM_SEPOLIA_TOKEN=0x... > -> **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions (deploy/configure). +> # For token pools +> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... +> export ARBITRUM_SEPOLIA_TOKEN_POOL=0x... +> ``` > -> **LINK (optional)**: if you plan to pay for the cross-chain transfer with `LINK`, fund the sender -> wallet with testnet LINK on the **source chain**. +> This tutorial supports both CCT registration models: > -> You can use our [faucet](https://faucets.chain.link/) to get testnet LINK. +> - In the Burn & Mint model, rate limiters are configured on the `BurnMintTokenPool` on each chain. +> - In the Lock & Mint model, rate limiters are configured on the token pools: the source-chain +> `LockReleaseTokenPool` and the destination-chain `BurnMintTokenPool`. The +> `ERC20LockBox` is not rate-limited and is not updated by the tasks in this tutorial. -**Already have deployments? Skip only what you already have:** +### 1. Review Current Rate Limiter Configurations -- If you already have tokens deployed on **both** chains, skip **Deploy Tokens**. -- If you have deployed tokens but not the corresponding pools, you must still run **Deploy Token Pools** before continuing. -- If you already have tokens and **V2-compatible** token pools deployed on **both** chains, skip both sections and move straight on to [Enablement](#enablement). +Use the `getCurrentRateLimits` task to read the current rate limiter configuration state for a specific lane from your token pool. The task reports whether rate limiting is enabled, the maximum capacity (bucket size), the refill rate (tokens per second), and the current token count in each bucket. -**If you already have a V1 token pool deployed and want to use newer features or prepare for lane migration:** +[getCurrentRateLimits.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/rate-limiter/getCurrentRateLimits.ts) -- You will need to deploy a **V2-compatible token pool** -- Your existing V1 pool can continue to support current transfers -- Follow the migration guide to upgrade your setup without disrupting your existing deployment +View the rate limiter query task on GitHub. -> **Read**: [Migrate from V1 to V2 token pools](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat) +| Flag | Required | Description | +| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `--destchain` | Yes | Remote chain whose lane is being queried (e.g. `arbitrumSepolia`) | +| `--fastfinality` | No | Read the faster than finality bucket instead of the standard finality bucket (v2 pools only; falls back to standard per direction) | -Before continuing, export the relevant addresses so subsequent tasks can find them: +1. Check the Ethereum Sepolia pool (lane to Arbitrum Sepolia): ```bash filename="Terminal" -# For tokens -export ETHEREUM_SEPOLIA_TOKEN= -export ARBITRUM_SEPOLIA_TOKEN= - -# For token pools -export ETHEREUM_SEPOLIA_TOKEN_POOL= -export ARBITRUM_SEPOLIA_TOKEN_POOL= +npx hardhat getCurrentRateLimits \ + --destchain arbitrumSepolia \ + --network sepolia ``` -### Enablement - -These steps configure your token pools and activate them under the CCIP protocol so your token becomes a truly Cross-Chain Token (CCT). +Your output should look something like this: -### Cross-chain Token Operations +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View rate limits +Bucket: Standard finality +======================================== + +Pool Version: v2 + + Outbound Enabled: false + Outbound Capacity: 0 + Outbound Rate: 0 + Outbound Tokens: 0 + Inbound Enabled: false + Inbound Capacity: 0 + Inbound Rate: 0 + Inbound Tokens: 0 + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +2. Check the Arbitrum Sepolia pool (lane to Ethereum Sepolia): -If all the previous steps were successful, your CCT token is now fully registered and activated under the CCIP protocol. -Use these next steps to mint and transfer your tokens across networks. +```bash filename="Terminal" +npx hardhat getCurrentRateLimits \ + --destchain sepolia \ + --network arbitrumSepolia +``` ---- +Your output should look something like this: -# Enable your tokens in CCIP (Lock & Mint): Register from an EOA using Foundry -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry -Last Updated: 2025-10-30 +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View rate limits +Bucket: Standard finality +======================================== + +Pool Version: v2 + + Outbound Enabled: false + Outbound Capacity: 0 + Outbound Rate: 0 + Outbound Tokens: 0 + Inbound Enabled: false + Inbound Capacity: 0 + Inbound Rate: 0 + Inbound Tokens: 0 + +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` -## Guide Versions +> **NOTE: Rate Limiters Disabled** +> +> 1. Rate limiting is disabled by default when no rate-limiting parameters are provided during +> `applyChainUpdates`. In that case, both outbound and inbound capacities are `0`, meaning +> there are no restrictions on token flow. +> 2. In the next step you will enable and configure these rate limiters. -- [Foundry (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) +### 2. Update Rate Limiter Configurations -- [Hardhat (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) +Use the `updateRateLimiters` task to update rate limiter configurations for an existing lane in your token pool. The direction to update is inferred automatically from whichever `--outbound*` / `--inbound*` flags you set. -In this tutorial you will: +Only the directions you specify are updated; any direction you don't specify is left unchanged. Outbound and inbound are relative to the pool you're configuring: -1. Deploy a token on two chains using [Foundry](https://book.getfoundry.sh/). -2. Set up a `Lock & Release` token pool on the source chain and a `Burn Mint` token pool on the destination chain. -3. Configure and activate the pools in CCIP so your token becomes a Cross-Chain Token (CCT). -4. (Optional) Test the setup by minting and transferring tokens across networks. +1. `outbound` is tokens leaving this pool toward the `--destchain` remote chain; +2. `inbound` is tokens arriving from the `--destchain` remote chain into this pool. -> **NOTE: If you do not explicitly configure these features, default behavior applies** +> **NOTE: Lane must exist** +> +> 1. Rate limiters can only be updated for lanes that already exist. +> 2. If you haven't configured the lane yet, complete one of the two tutorials first: +> +> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) +> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) +> 3. Run `applyChainUpdates` to configure the lane. -## Before You Begin +[updateRateLimiters.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/rate-limiter/updateRateLimiters.ts) -## Tutorial +View the rate limiter update task on GitHub. -### Prerequisites +| Flag | Required | Description | +| -------------------- | ------------------ | -------------------------------------------------------------------------------------------------- | +| `--destchain` | Yes | Remote chain whose lane is being updated | +| `--outboundcapacity` | To update outbound | Token bucket capacity for outbound transfers | +| `--outboundrate` | To update outbound | Token bucket refill rate (tokens/second) for outbound transfers | +| `--outboundenabled` | No | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when capacity or rate are set) | +| `--inboundcapacity` | To update inbound | Token bucket capacity for inbound transfers | +| `--inboundrate` | To update inbound | Token bucket refill rate (tokens/second) for inbound transfers | +| `--inboundenabled` | No | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when capacity or rate are set) | +| `--fastfinality` | No | Update the faster than finality bucket instead of the standard finality bucket (v2 only) | -> **CAUTION: Before you continue** -> -> **Sepolia ETH**: required for Sepolia transactions (deploy/configure/send). -> -> **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions (deploy/configure). +> **NOTE: Access control and compatibility** > -> **LINK (optional)**: if you plan to pay for the cross-chain transfer with `LINK`, fund the sender -> wallet with testnet LINK on the **source chain**. +> 1. The pool owner and the designated `rateLimitAdmin` can update rate limiters. +> 2. The `rateLimitAdmin` is configured via `setDynamicConfig`. +> 3. This task supports both v1 and v2 token pools: on v2 pools it calls `setRateLimitConfig()`; on v1 pools +> it falls back to `setChainRateLimiterConfig()`. +> 4. The `--fastfinality` flag is v2-only and is ignored on v1 pools. + +> **NOTE: Configuration best practices** > -> You can use our [faucet](https://faucets.chain.link/) to get testnet LINK. +> 1. Set inbound rate limits a bit higher than outbound limits on the same lane. Many chains finalize blocks in +> batches, so multiple outbound messages may become executable on the destination simultaneously, requiring slightly +> more inbound capacity. +> 2. The exact additional capacity required depends on the refill rate and the size of the source chain epochs and CCIP +> round time. In most cases, a 5-10% buffer should be sufficient. +> 3. `rate` must be less than or equal to `capacity`, and when disabling rate limiting ( +> `isEnabled: false`), both `capacity` and `rate` must be `0`. -**Already have deployments? Skip only what you already have:** +In this example, we configure an 18-decimal token with outbound capacity of 10 tokens (refilling at 0.1 tokens/sec) and inbound capacity of 11 tokens (refilling at 0.11 tokens/sec, a 10% buffer): -- If you already have tokens deployed on **both** chains, skip **Deploy Tokens**. -- If you have deployed tokens but not the corresponding pools and lockbox, you must still run **Deploy Token Pools** before continuing. -- If you already have tokens deployed on **both** chains, **V2-compatible** token pools deployed on **both** chains, and the lockbox deployed on the **Lock & Release** side (Ethereum Sepolia), skip both sections and move straight on to [Enablement](#enablement). +1. Update the Ethereum Sepolia pool (lane to Arbitrum Sepolia): -**If you already have a V1 token pool deployed and want to use newer features or prepare for lane migration:** +```bash filename="Terminal" +npx hardhat updateRateLimiters \ + --destchain arbitrumSepolia \ + --outboundcapacity 10000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 11000000000000000000 \ + --inboundrate 110000000000000000 \ + --network sepolia +``` -- You will need to deploy a **V2-compatible token pool** -- Your existing V1 pool can continue to support current transfers -- Follow the migration guide to upgrade your setup without disrupting your existing deployment +Your output should look something like this: -> **Read**: [Migrate from V1 to V2 token pools](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-lock-mint-foundry) +```bash filename="Terminal" +======================================== +⚡️ Update Rate Limiters +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Update rate limits +Direction: Both +Bucket: Standard finality +======================================== + +Pool Version: v2 (setRateLimitConfig) + +Current Rate Limiter State: + Outbound Enabled: false + Outbound Capacity: 0 + Outbound Rate: 0 + Outbound Tokens: 0 + Inbound Enabled: false + Inbound Capacity: 0 + Inbound Rate: 0 + Inbound Tokens: 0 + +New Configuration: + Outbound Enabled: true + Outbound Capacity: 10000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 11000000000000000000 + Inbound Rate: 110000000000000000 + +[Step 1] Applying rate limit update on Ethereum Sepolia +⏳ Tx: 0xddeeff00112233445566778899aabbccddeeff00112233445566778899aabbcc + +======================================== +✅ Rate limiter update complete on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Transaction: 0xddeeff00112233445566778899aabbccddeeff00112233445566778899aabbcc +======================================== +``` + +2. Update the Arbitrum Sepolia pool (lane to Ethereum Sepolia): -Before continuing, export the relevant addresses so subsequent scripts can find them: +```bash filename="Terminal" +npx hardhat updateRateLimiters \ + --destchain sepolia \ + --outboundcapacity 10000000000000000000 \ + --outboundrate 100000000000000000 \ + --inboundcapacity 11000000000000000000 \ + --inboundrate 110000000000000000 \ + --network arbitrumSepolia +``` + +Your output should look something like this: ```bash filename="Terminal" -export ETHEREUM_SEPOLIA_TOKEN= -export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN= +======================================== +⚡️ Update Rate Limiters +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: Update rate limits +Direction: Both +Bucket: Standard finality +======================================== + +Pool Version: v2 (setRateLimitConfig) + +Current Rate Limiter State: + Outbound Enabled: false + Outbound Capacity: 0 + Outbound Rate: 0 + Outbound Tokens: 0 + Inbound Enabled: false + Inbound Capacity: 0 + Inbound Rate: 0 + Inbound Tokens: 0 + +New Configuration: + Outbound Enabled: true + Outbound Capacity: 10000000000000000000 + Outbound Rate: 100000000000000000 + Inbound Enabled: true + Inbound Capacity: 11000000000000000000 + Inbound Rate: 110000000000000000 + +[Step 1] Applying rate limit update on Arbitrum Sepolia +⏳ Tx: 0x2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70812 + +======================================== +✅ Rate limiter update complete on Arbitrum Sepolia! +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Transaction: 0x2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70812 +======================================== +``` + +3. To **disable** rate limiters for a lane, pass `--outboundenabled false` and `--inboundenabled false`: + +> **CAUTION: Disabling rate limiters** +> +> 1. We highly recommend that devs have at least some limits set up. We advise against completely disabling rate +> limiters unless absolutely necessary. +> 2. **Informational-only for this tutorial:** If you disable rate limiters here, the later +> **Test the Rate Limiter Configurations** section will not behave as described (it will not revert +> with `TokenMaxCapacityExceeded` / `TokenRateLimitReached`). -export ETHEREUM_SEPOLIA_TOKEN_POOL= -export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL= +```bash filename="Terminal" +npx hardhat updateRateLimiters \ + --destchain arbitrumSepolia \ + --outboundenabled false \ + --inboundenabled false \ + --network sepolia ``` -### Enablement +### 3. Verify the Updated Rate Limiter Configurations -These steps configure your token pools and activate them under the CCIP protocol so your token becomes a fully registered Cross-Chain Token (CCT). +Re-run the `getCurrentRateLimits` task on both chains to confirm the update was applied correctly. -### Cross-chain Token Operations +1. Verify the Ethereum Sepolia pool: -If all the previous steps were successful, your CCT token is now fully registered and activated under the CCIP protocol. -Use these next steps to mint and transfer your tokens across networks. +```bash filename="Terminal" +npx hardhat getCurrentRateLimits \ + --destchain arbitrumSepolia \ + --network sepolia +``` ---- +> **NOTE: Note** +> +> Your output might differ based on whether or not you disabled rate limiters in the previous step. -# Enable your tokens in CCIP (Lock & Mint): Register from an EOA using Hardhat -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat -Last Updated: 2026-04-24 +Your output should look something like this: -## Guide Versions +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View rate limits +Bucket: Standard finality +======================================== + +Pool Version: v2 + + Outbound Enabled: true + Outbound Capacity: 10000000000000000000 + Outbound Rate: 100000000000000000 + Outbound Tokens: 10000000000000000000 + Inbound Enabled: true + Inbound Capacity: 11000000000000000000 + Inbound Rate: 110000000000000000 + Inbound Tokens: 11000000000000000000 + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +2. Verify the Arbitrum Sepolia pool: -- [Foundry (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) +```bash filename="Terminal" +npx hardhat getCurrentRateLimits \ + --destchain sepolia \ + --network arbitrumSepolia +``` -- [Hardhat (Lock & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) +Your output should look something like this: -In this tutorial you will: +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Arbitrum Sepolia +Remote Chain: Ethereum Sepolia +Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +Action: View rate limits +Bucket: Standard finality +======================================== -1. Deploy a token on two chains using [Hardhat](https://hardhat.org/). -2. Set up a `Lock & Release` token pool on the source chain and a `Burn Mint` token pool on the destination chain. -3. Configure and activate the pools in CCIP so your token becomes a Cross-Chain Token (CCT). -4. (Optional) Test the setup by minting and transferring tokens across networks. +Pool Version: v2 -> **NOTE: If you do not explicitly configure these features, default behavior applies** + Outbound Enabled: true + Outbound Capacity: 10000000000000000000 + Outbound Rate: 100000000000000000 + Outbound Tokens: 10000000000000000000 + Inbound Enabled: true + Inbound Capacity: 11000000000000000000 + Inbound Rate: 110000000000000000 + Inbound Tokens: 11000000000000000000 -## Before You Begin +======================================== +Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d +======================================== +``` -## Tutorial +> **NOTE: Token bucket values** +> +> The `Tokens` field shows the current fill level of the bucket. Immediately after an update the bucket +> resets to full capacity. Over time, as transfers consume tokens, this value decreases and refills at the configured +> rate. -### Prerequisites +### 4. Test the Rate Limiter Configurations -> **CAUTION: Before you continue** -> -> **Sepolia ETH**: required for Sepolia transactions (deploy/configure/send). -> -> **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions (deploy/configure). -> -> **LINK (optional)**: if you plan to pay for the cross-chain transfer with `LINK`, fund the sender -> wallet with testnet LINK on the **source chain**. +Initiate a cross-chain transfer to verify that the rate limiter enforces the configured limits. + +> **NOTE: Token balance** > -> You can use our [faucet](https://faucets.chain.link/) to get testnet LINK. +> Ensure your EOA has a sufficient balance of the cross-chain token on the source chain. If you need more tokens, use +> the `mintTokens` task or transfer tokens from another account. -**Already have deployments? Skip only what you already have:** +Before testing, export the router and token addresses for the source chain: -- If you already have tokens deployed on **both** chains, skip **Deploy Tokens**. -- If you have deployed tokens but not the corresponding pools and lockbox, you must still run **Deploy Token Pools** before continuing. -- If you already have tokens deployed on **both** chains, **V2-compatible** token pools deployed on **both** chains, and the lockbox deployed on the **Lock & Release** side (Ethereum Sepolia), skip both sections and move straight on to [Enablement](#enablement). +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_ROUTER=0x... +export ETHEREUM_SEPOLIA_TOKEN=0x... +``` -**If you already have a V1 token pool deployed and want to use newer features or prepare for lane migration:** +> **NOTE: Router address** +> +> You can find the router address for your chain in the [CCIP +> Directory](https://docs.chain.link/ccip/directory/testnet). -- You will need to deploy a **V2-compatible token pool** -- Your existing V1 pool can continue to support current transfers -- Follow the migration guide to upgrade your setup without disrupting your existing deployment +> **Note:** +> +> 1. Add `--fee-token LINK` to pay CCIP fees in LINK. If you omit this flag, fees are paid in the native gas token. +> 2. Use `--receiver
    ` for a custom recipient. If omitted on EVM-to-EVM, the receiver defaults to the sender. -> **Read**: [Migrate from V1 to V2 token pools](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-lock-mint-foundry) +#### Test 1: Exceed capacity -Before continuing, export the relevant addresses so subsequent tasks can find them: +Transfer one wei more than the outbound capacity (10 tokens + 1 wei). This should fail because the requested amount exceeds the bucket size. ```bash filename="Terminal" -export ETHEREUM_SEPOLIA_TOKEN= -export ARBITRUM_SEPOLIA_TOKEN= +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10.000000000000000001 \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` -export ETHEREUM_SEPOLIA_TOKEN_POOL= -export ARBITRUM_SEPOLIA_TOKEN_POOL= +Your output should look something like this: + +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +error: Failed to call "ccipSend" +Error = execution reverted: TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress) +Revert.error = TokenMaxCapacityExceeded(uint256,uint256,address) +Revert.capacity = 10000000000000000000n +Revert.requested = 10000000000000000001n +Revert.tokenAddress = 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D ``` -### Enablement +The transfer should fail with `TokenMaxCapacityExceeded` because the requested amount is larger than the bucket capacity. -These steps configure your token pools and activate them under the CCIP protocol so your token becomes a truly Cross-Chain Token (CCT). +> **NOTE: How does the token bucket work?** +> +> CCIP rate limiters use a model. Think of the bucket as a virtual allowance that controls how +> many tokens can flow through the pool in a given window: +> +> 1. The bucket starts full at its configured **capacity**. +> 2. Each transfer **consumes** its amount from the bucket. +> 3. The bucket **refills** continuously at the configured **rate** (tokens per second). +> +> In this tutorial, the outbound bucket has a capacity of 10 tokens and refills at 0.1 tokens/sec. Transferring all 10 +> tokens in one go "drains" the bucket to zero. Any immediate follow-up transfer will fail because the bucket needs 100 +> seconds (10 ÷ 0.1) to fully replenish. -### Cross-chain Token Operations +#### Test 2: Drain the bucket, then hit the rate -If all the previous steps were successful, your CCT token is now fully registered and activated under the CCIP protocol. -Use these next steps to mint and transfer your tokens across networks. +1. **First transfer** (succeeds: drains the outbound bucket): ---- +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10 \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` -# Manage Token Transfer Fees Using Foundry -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry -Last Updated: 2026-05-05 +Your output should look something like this: -## Guide Versions +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293 +``` -- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry) +2. **Second transfer** (fails: bucket has not refilled): -- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat) +Run the same command immediately: -CCIP 2.0 token pools let pool owners opt into **pool-level token transfer fee overrides** per destination lane. When an override is disabled, the [OnRamp](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/onRamp/OnRamp.sol) falls back to [`FeeQuoter`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/FeeQuoter.sol) defaults for that lane. In this tutorial you will: +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10 \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` -1. Review the current fee configuration for both configured lanes. -2. Optionally set a fee admin who can withdraw accrued fees. -3. Configure lane-specific flat fees, transfer fees, destination gas overhead, and destination bytes overhead. -4. Send a LINK-paid transfer and inspect accrued fees in the source token pool. -5. Withdraw accrued fee tokens. -6. Disable the fee configuration on both lanes to return to `FeeQuoter` defaults. +Your output should look something like this: -> **NOTE: Defaults apply unless you opt in** +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +error: Failed to call "ccipSend" +Error = execution reverted: TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress) +Revert.error = TokenRateLimitReached(uint256,uint256,address) +Revert.minWaitInSeconds = 100n +Revert.available = 0n +Revert.tokenAddress = 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D +``` -## Before You Begin +The transfer should fail with `TokenRateLimitReached` because the bucket has not yet refilled. With a capacity of 10 tokens and a rate of 0.1 tokens per second, it takes 100 seconds to fully replenish. -## Tutorial +### 5. (Optional) Configure Rate Limits for Faster Than Finality Transfers -> **TIP: Prerequisites** -> -> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the two -> tutorials first: -> -> [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) -> -> [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) -> -> Use the token pool address for the chain you are configuring. In a Burn & Mint setup, both pools are -> `BurnMintTokenPool` contracts. In a Lock & Mint setup, the source chain can use a -> `LockReleaseTokenPool` backed by an `ERC20LockBox`, while the destination chain uses a -> `BurnMintTokenPool`. This tutorial configures the token pool, not the lockbox. -> -> ```bash filename="Terminal" -> export ETHEREUM_SEPOLIA_TOKEN=0x... -> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... -> -> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... -> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x... -> ``` +This section applies to [`TokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) v2.0 or later only. CCIP 2.0 token pools maintain two independent rate limiter buckets per lane: -> **CAUTION: Troubleshooting** -> -> **Owner revert:** confirm `KEYSTORE_NAME` is the owner of the source-chain token pool. -> -> **Fee admin withdrawal revert:** confirm the fee admin was set, the correct fee admin keystore is -> used, and the action is withdrawal rather than owner-only configuration. -> -> **Missing gas:** confirm the broadcaster has native gas on the source chain. -> -> **Missing LINK:** confirm the sender has source-chain LINK when using `--fee-token LINK`. -> -> **Missing lane:** confirm the pool lane was configured with `ApplyChainUpdates.s.sol`. -> -> **Wrong lane or pool:** confirm `--rpc-url`, `DEST_CHAIN`, and the source pool -> env var match the intended source -> destination lane. +- **Standard finality bucket**: Used for standard finality transfers (`finality=finalized`, which is the default when you omit `--extra finality=...`). This is the bucket you configured in the previous steps. +- **Faster Than Finality bucket**: Used for transfers that request faster than finality with `finality=` (numeric block depth). If this bucket is disabled for a direction, faster than finality transfers for that direction fall back to the standard bucket (enforced on-chain by the token pool). + +> **NOTE: Pool type and lockbox scope** > -> **Invalid config:** confirm `DEST_GAS_OVERHEAD` is not `0`, -> `DEST_BYTES_OVERHEAD` is at least `32`, and basis points values are below `10000`. +> Rate limiters are configured on token pools, regardless of whether you registered your token with Burn & Mint or +> Lock & Mint. In Lock & Mint, the `ERC20LockBox` is not rate-limited and is not updated by these +> tasks. + +> **CAUTION: Faster Than Finality must be allowed first** > -> **Fast-finality revert:** confirm the pool allows block-depth finality and the requested -> `finality=` is not below the pool minimum. +> 1. Updating the faster than finality bucket alone does **not** enable faster than finality transfers. +> The pool owner must also configure the pool's **allowed finality config** with +> `setFinalityConfig`. +> 2. Setting the allowed finality config is owner-only. Updating rate limiters can be done by the pool owner or the +> configured `rateLimitAdmin`. +> 3. For the full faster than finality setup flow, see the Configure Faster Than Finality section in either +> registration tutorial: > -> **No accrued fees:** confirm `FEE_TOKENS` includes LINK and the source token, the send -> completed, native fee token was not used, and flat or basis points fee values are non-zero. +> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat#optional-configure-faster-than-finality-for-the-token-pool) +> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat#optional-configure-fast-finality-for-the-token-pool) ---- +#### Check the pool's allowed finality config -# Manage Token Transfer Fees Using Hardhat -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat -Last Updated: 2026-05-05 +Use `getFinalityConfig` to confirm which faster than finality modes the pool currently allows. -## Guide Versions +[getFinalityConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/finality-config/getFinalityConfig.ts) -- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry) +View the finality config query task on GitHub. -- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat) +1. Check the Ethereum Sepolia pool: -CCIP 2.0 token pools let pool owners opt into **pool-level token transfer fee overrides** per destination lane. When an override is disabled, the [OnRamp](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/onRamp/OnRamp.sol) falls back to [`FeeQuoter`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/FeeQuoter.sol) defaults for that lane. In this tutorial you will: +```bash filename="Terminal" +npx hardhat getFinalityConfig --network sepolia +``` -1. Review the current fee configuration for both configured lanes. -2. Optionally set a fee admin who can withdraw accrued fees. -3. Configure lane-specific flat fees, transfer fees, destination gas overhead, and destination bytes overhead. -4. Send a LINK-paid transfer and inspect accrued fees in the source token pool. -5. Withdraw accrued fee tokens. -6. Disable the fee configuration on both lanes to return to `FeeQuoter` defaults. +Your output should look something like this: -> **NOTE: Defaults apply unless you opt in** +```bash filename="Terminal" +======================================== +⏱️ Get Finality Config +======================================== +Chain: Ethereum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View finality config +======================================== -## Before You Begin +Allowed Finality Config (raw): 0x00000000 -## Tutorial +Mode: WAIT_FOR_FINALITY (default) + Full finality is required. Faster Than Finality transfers are disabled. -> **TIP: Prerequisites** -> -> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the two -> tutorials first: -> -> [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) -> -> [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) -> -> Use the token pool address for the chain you are configuring. In a Burn & Mint setup, both pools are -> `BurnMintTokenPool` contracts. In a Lock & Mint setup, the source chain can use a -> `LockReleaseTokenPool` backed by an `ERC20LockBox`, while the destination chain uses a -> `BurnMintTokenPool`. This tutorial configures the token pool, not the lockbox. -> -> ```bash filename="Terminal" -> export ETHEREUM_SEPOLIA_TOKEN=0x... -> export ARBITRUM_SEPOLIA_TOKEN=0x... -> -> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... -> export ARBITRUM_SEPOLIA_TOKEN_POOL=0x... -> ``` +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` -> **CAUTION: Troubleshooting** -> -> **Owner revert:** confirm `KEYSTORE_NAME` is the owner of the source-chain token pool. -> -> **Fee admin withdrawal revert:** confirm the fee admin was set, the correct fee admin keystore is -> used, and the action is withdrawal rather than owner-only configuration. -> -> **Missing gas:** confirm the broadcaster has native gas on the source chain. -> -> **Missing LINK:** confirm the sender has source-chain LINK when using `--fee-token LINK`. -> -> **Missing lane:** confirm the pool lane was configured with `applyChainUpdates`. -> -> **Wrong lane or pool:** confirm `--network`, `--destchain`, and the source pool -> env var (or `--tokenpool`) match the intended source -> destination lane. -> -> **Invalid config:** confirm `--destgasoverhead` is not `0`, -> `--destbytesoverhead` is at least `32`, and basis points values are below `10000`. -> -> **Fast-finality revert:** confirm the pool allows block-depth finality and the requested -> `finality=` is not below the pool minimum. -> -> **No accrued fees:** confirm `--feetokens` includes LINK and the source token, the send -> completed, native fee token was not used, and flat or basis points fee values are non-zero. +`BLOCK_DEPTH (32)` sets a minimum for numeric requests: `--extra finality=32` (or higher) is allowed; `--extra finality=10` reverts with `InvalidRequestedFinality(requestedFinality, allowedFinality)`. ---- +2. Check the Arbitrum Sepolia pool: -# Set Token Pool Rate Limits Using Foundry -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry -Last Updated: 2026-04-27 +```bash filename="Terminal" +npx hardhat getFinalityConfig --network arbitrumSepolia +``` -## Guide Versions +> **NOTE: Token pool ABI** +> +> The Hardhat task reads shared TokenPool v2 functions, so the same command works for both Burn & Mint pools and +> Lock & Mint token pools. If you are not using the `{CHAIN}_TOKEN_POOL` environment variable, pass +> `--tokenpool 0xYourPool`. -- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry) +#### Approach 1: Allow Faster Than Finality and Configure the Faster Than Finality Bucket in One Command -- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-hardhat) +Use `setFinalityConfig` with rate limit flags to allow faster than finality and configure the faster than finality bucket in a single run. -In this tutorial you will: +[setFinalityConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/finality-config/setFinalityConfig.ts) -1. Review current standard rate limiter configurations for your deployed token pool. -2. Update outbound and inbound standard rate limiter configurations. -3. Verify the updated configurations. -4. Test standard rate limiter enforcement with a cross-chain token transfer. -5. Optionally configure and test rate limits for faster than finality transfers. +View the finality config update task on GitHub. -## Before You Begin +This example configures the Ethereum Sepolia pool for transfers to Arbitrum Sepolia. To configure the reverse direction, run the same task on Arbitrum Sepolia with `--destchain sepolia --network arbitrumSepolia`. -## Tutorial +```bash filename="Terminal" +npx hardhat setFinalityConfig \ + --blockdepth 32 \ + --destchain arbitrumSepolia \ + --outboundcapacity 5000000000000000000 \ + --outboundrate 50000000000000000 \ + --inboundcapacity 5500000000000000000 \ + --inboundrate 55000000000000000 \ + --network sepolia +``` -> **TIP: Prerequisites** -> -> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the -> registration tutorials first: -> -> [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) -> -> [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) -> -> Make sure your token and token pool addresses are exported as environment variables: -> -> ```bash -> export ETHEREUM_SEPOLIA_TOKEN=0x... -> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x... -> -> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... -> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x... -> ``` -> -> This tutorial supports both CCT registration models: -> -> In the Burn & Mint model, rate limiters are configured on the `BurnMintTokenPool` on each chain. +Your output should look something like this: + +> **NOTE: Output abbreviated** > -> In the Lock & Mint model, rate limiters are configured on the token pools: the source-chain -> `LockReleaseTokenPool` and the destination-chain `BurnMintTokenPool`. The -> `ERC20LockBox` is not rate-limited and is not updated by the scripts in this tutorial. +> `setFinalityConfig` also prints the lane's current and updated rate limits (faster than finality where +> enabled, standard otherwise). Those sections are omitted here for brevity. ---- +```bash filename="Terminal" +======================================== +⏱️ Set Finality Config +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: Set finality config +======================================== + + Current Finality Config: 0x00000000 + New Finality Config: 0x00000020 + Mode: BLOCK_DEPTH (32 blocks) + +[Step 1] Setting finality config on Ethereum Sepolia +⏳ Tx: 0x280399cd3dce1815a30ca3763736c7cbc4a8018bdb3ba1b650e728479640f5b6 +✅ Finality config set successfully! + +[Step 2] Updating rate limits (faster than finality bucket) on Ethereum Sepolia → Arbitrum Sepolia +New Configuration: + Outbound Enabled: true + Outbound Capacity: 5000000000000000000 + Outbound Rate: 50000000000000000 + Inbound Enabled: true + Inbound Capacity: 5500000000000000000 + Inbound Rate: 55000000000000000 + +⏳ Tx: 0xa69245f229f41313d597ea452fa7c9d8ead9e05bfe36aad5a997a6850d198ac5 +✅ Rate limits updated successfully! + +======================================== +✅ Configuration Complete on Ethereum Sepolia! +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Finality Config: 0x00000020 +Mode: BLOCK_DEPTH (32 blocks) +======================================== +``` + +#### Approach 2: Update the Faster Than Finality Bucket Directly + +If the pool already allows faster than finality, you can update the faster than finality bucket directly using `updateRateLimiters` with `--fastfinality`: -# Set Token Pool Rate Limits Using Hardhat -Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-hardhat -Last Updated: 2026-04-27 +```bash filename="Terminal" +npx hardhat updateRateLimiters \ + --destchain arbitrumSepolia \ + --fastfinality \ + --outboundcapacity 5000000000000000000 \ + --outboundrate 50000000000000000 \ + --inboundcapacity 5500000000000000000 \ + --inboundrate 55000000000000000 \ + --network sepolia +``` -## Guide Versions +#### Verify the faster than finality bucket -- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry) +Pass `--fastfinality` to `getCurrentRateLimits`. Each direction is shown independently: the faster than finality bucket for directions where it is enabled, and the standard bucket fallback for directions where it is not. -- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-hardhat) +```bash filename="Terminal" +npx hardhat getCurrentRateLimits \ + --destchain arbitrumSepolia \ + --fastfinality \ + --network sepolia +``` -In this tutorial you will: +Your output should look something like this: -1. Review current standard rate limiter configurations for your deployed token pool. -2. Update outbound and inbound standard rate limiter configurations. -3. Verify the updated configurations. -4. Test standard rate limiter enforcement with a cross-chain token transfer. -5. Optionally configure and test rate limits for faster than finality transfers. +```bash filename="Terminal" +======================================== +📊 Get Current Rate Limits +======================================== +Chain: Ethereum Sepolia +Remote Chain: Arbitrum Sepolia +Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +Action: View rate limits +Bucket: Faster Than Finality (standard finality fallback per direction if not enabled) +======================================== + +Pool Version: v2 + + Outbound [faster than finality]: + Enabled: true + Capacity: 5000000000000000000 + Rate: 50000000000000000 + Tokens: 5000000000000000000 + Inbound [faster than finality]: + Enabled: true + Capacity: 5500000000000000000 + Rate: 55000000000000000 + Tokens: 5500000000000000000 + +======================================== +Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9 +======================================== +``` + +#### Test a faster than finality transfer + +After the pool allows faster than finality and the faster than finality bucket is configured, send a small transfer with numeric block depth (for example `--extra finality=32`): -## Before You Begin +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1 \ + --receiver 0xYourReceiverAddress \ + --extra finality=32 \ + --wallet hardhat:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ARBITRUM_SEPOLIA_RPC_URL" +``` -## Tutorial +Your output should look something like this: -> **TIP: Prerequisites** -> -> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the -> registration tutorials first: -> -> [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) -> -> [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) -> -> Make sure your token and token pool addresses are exported as environment variables: -> -> ```bash -> # For tokens -> export ETHEREUM_SEPOLIA_TOKEN=0x... -> export ARBITRUM_SEPOLIA_TOKEN=0x... -> -> # For token pools -> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x... -> export ARBITRUM_SEPOLIA_TOKEN_POOL=0x... -> ``` -> -> This tutorial supports both CCT registration models: -> -> In the Burn & Mint model, rate limiters are configured on the `BurnMintTokenPool` on each chain. +```bash filename="Terminal" +Fee: 130129888907619n = 0.000130129888907619 ETH +✔ Enter password for Hardhat keystore 'PRIVATE_KEY' +🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x5f1c2d3e4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e , messageId => 0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2c4d +CCIP Explorer: https://ccip.chain.link/msg/0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2c4d +``` + +Re-run `getCurrentRateLimits` with `--fastfinality` to confirm the outbound faster than finality bucket changed after the transfer: + +```bash filename="Terminal" +npx hardhat getCurrentRateLimits \ + --destchain arbitrumSepolia \ + --fastfinality \ + --network sepolia +``` + +> **NOTE: Faster Than Finality troubleshooting** > -> In the Lock & Mint model, rate limiters are configured on the token pools: the source-chain -> `LockReleaseTokenPool` and the destination-chain `BurnMintTokenPool`. The -> `ERC20LockBox` is not rate-limited and is not updated by the tasks in this tutorial. +> 1. This test assumes the pool's allowed finality config allows numeric block depth. If the transfer reverts with +> `InvalidRequestedFinality(requestedFinality, allowedFinality)`, run +> `npx hardhat getFinalityConfig --network sepolia` and confirm the configured `BLOCK_DEPTH`, +> then retry with `--extra finality=32` (or higher than the configured minimum). +> 2. If you request a numeric value lower than the configured minimum block depth, the token pool will revert. --- diff --git a/src/content/data-feeds/llms-full.txt b/src/content/data-feeds/llms-full.txt index 48867e59325..3bb8088c94c 100644 --- a/src/content/data-feeds/llms-full.txt +++ b/src/content/data-feeds/llms-full.txt @@ -6455,28 +6455,22 @@ Instead of relying on block builders to bundle together the oracle update and th > **CAUTION: Important** > -> The final bundled transaction is **not submitted by the searcher**. It is submitted by the Chainlink oracle -> network. -> -> Searchers must have **bonded native tokens** with the Atlas smart contracts in order to reimburse the Chainlink -> oracle for the gas costs of both the **searcher operation** and the **oracle update**. -> -> Gas prices for these transactions are chosen by the Chainlink oracle. The selected gas price is provided to the -> searcher at the start of the auction. -> -> Searchers must sign for a `gasPrice` that is **exactly equal** to the gas price specified by the -> Chainlink oracle. -> -> Searchers must not attempt to use a gas limit for their operation that is greater than the allowed -> `SolverGasLimit`. Query this value by calling `getSolverGasLimit` on the -> `DappControl` -> contract. Sending `SolverOps` with a higher gas limit will not work. -> -> Since auction and `auctionSolution` payloads can exceed default read/write buffer sizes in popular -> WebSocket libraries, explicitly configure read and write buffers to greater than **10 KB**. -> -> Solvers should expect that their long-lived connections to the SVR endpoint will be disconnected, and should -> implement **auto-reconnect** functionality. +> - The final bundled transaction is **not submitted by the searcher**. It is submitted by the Chainlink oracle +> network. +> - Searchers must have **bonded native tokens** with the Atlas smart contracts in order to reimburse the Chainlink +> oracle for the gas costs of both the **searcher operation** and the **oracle update**. +> - Gas prices for these transactions are chosen by the Chainlink oracle. The selected gas price is provided to the +> searcher at the start of the auction. +> - Searchers must sign for a `gasPrice` that is **exactly equal** to the gas price specified by the +> Chainlink oracle. +> - Searchers must not attempt to use a gas limit for their operation that is greater than the allowed +> `SolverGasLimit`. Query this value by calling `getSolverGasLimit` on the +> `DappControl` +> contract. Sending `SolverOps` with a higher gas limit will not work. +> - Since auction and `auctionSolution` payloads can exceed default read/write buffer sizes in popular +> WebSocket libraries, explicitly configure read and write buffers to greater than **10 KB**. +> - Solvers should expect that their long-lived connections to the SVR endpoint will be disconnected, and should +> implement **auto-reconnect** functionality. ### Endpoints @@ -6781,10 +6775,9 @@ If you only bid on one auction, you have roughly a 50% chance of winning even if > **CAUTION: Required: Bid on Both Parallel Auctions** > -> Always submit bids to **both** auctions when two are created for the same liquidation opportunity. -> -> Avoid logic that limits bidding to a single auction per price update. -> Latency and timing differences between parallel auctions are expected and unavoidable. +> - Always submit bids to **both** auctions when two are created for the same liquidation opportunity. +> - Avoid logic that limits bidding to a single auction per price update. +> - Latency and timing differences between parallel auctions are expected and unavoidable. Duplicate auctions were introduced to improve landing guarantees by increasing the probability that at least one auction succeeds onchain. diff --git a/src/content/solutions/README.md b/src/content/solutions/README.md index d8764cac7d0..74d74837db3 100644 --- a/src/content/solutions/README.md +++ b/src/content/solutions/README.md @@ -104,6 +104,24 @@ That is the whole change. You do not touch `src/config/sidebar.ts`, any layout, Sidebar placement, the hub card, the hub filters, whether a navigation renders, and the legal disclaimer are all derived from what you wrote. +## Complete documentation for AI agents + +Every published solution automatically exposes `/solutions//llms-full.txt`. The right sidebar on every +page includes **More > Complete Your Solution docs (TXT)** to download it. + +The file is generated from the current content during `npm run build`, using the same MDX-to-Markdown conversion as +the product LLM files. It includes page titles, source URLs, and the page bodies. Single-page solutions include only +their landing page; multi-page solutions include every page in the order declared in `nav`. The landing-page summary and the complete disclaimer rendered by the layout are also included. Every page +keeps its own disclaimer; solution exports do not deduplicate disclaimers. The `.md` page exports use the same content +and conversion pipeline. No additional registration is needed when adding or updating a solution. + +`npm run generate:llms` also writes `src/content/solutions//llms-full.txt` for every published solution, +using the same generator as the Astro route. Hidden solutions are excluded. To generate only solutions, run +`npm run generate:llms -- --section solutions`; to generate one, use `--section solutions/`. +The command logs each generated solution and includes it in `reports/llms-report.json`. `npm run check:llms` checks +these files, including their page order and the complete disclaimer on every page. The build still generates the +download from the current sources, so a stale local TXT cannot make the site's download stale. + ## Copy an existing solution Two fixtures in this directory show both shapes: diff --git a/src/content/solutions/cross-chain-vault-adapter/llms-full.txt b/src/content/solutions/cross-chain-vault-adapter/llms-full.txt new file mode 100644 index 00000000000..85251a30736 --- /dev/null +++ b/src/content/solutions/cross-chain-vault-adapter/llms-full.txt @@ -0,0 +1,4490 @@ +# Cross-Chain Vault Adapter +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter + +## What is it + +A CCIP adapter that lets users on other chains deposit into and redeem from your existing ERC-4626 vault, while vault accounting stays on the hub chain, the chain where the vault and the adapter are deployed. + +[CCIP](/ccip) + +cross-chain, vaults + +[View on GitHub](https://github.com/smartcontractkit/cross-chain-vault-adapters) + +## What the adapter does + +With the Cross-Chain Vault Adapter, a vault team can accept deposits and redemptions from users on other chains. The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. The vault is the product, CCIP is the bridge, and the adapter is how users on other chains reach the vault. The vault holds one asset and earns yield. The adapter is a contract on the hub chain that receives tokens and instructions through CCIP and calls the vault for the user. + +Without the adapter, a user on a source chain who wants those shares in a vault on the hub chain must: + +1. Bridge the asset manually from the source chain to the hub chain, +2. Switch their wallet to the hub chain, +3. Get some native gas token on the hub chain, +4. Approve the vault on the hub chain, +5. Deposit into the vault on the hub chain, and, +6. Bridge the shares back to the source chain. + +With the Chainlink Cross-Chain Vault Adapter on the other hand, the user: + +1. Approves the CCIP Router and sends one CCIP message from the source chain. + +> **NOTE: Message payload** +> +> A single message carries: +> +> 1. The asset itself, and, +> 2. A 128-byte payload that names: the vault, the beneficiary, the minimum output, the delivery option, and a local +> refund address (in case the message fails on delivery and a refund is needed). + +And that's it. The user doesn't have to do anything else. + +The adapter deposits the asset, then delivers the shares on the hub chain or returns them to the user on the source chain. Returning the shares to the source chain completes the one-message experience: the user never has to touch the hub chain. A return requires the vault's share token to be a CCIP cross-chain token on both chains, as [Requirements](#requirements) lists. Redemptions work the same way, just in the reverse direction. [How it works](/solutions/cross-chain-vault-adapter/overview/how-it-works) covers each step. + +The following diagram shows a deposit from a source chain into a vault on the hub chain, with both delivery options. + +![A user's wallet on a source chain sends the asset and a payload through Chainlink CCIP to the adapter on the hub chain. The adapter deposits into the ERC-4626 vault and receives shares, then delivers them to the user's wallet on the hub chain, or returns them through CCIP to the source chain when the share token is a CCIP cross-chain token.](/images/solutions/cross-chain-vault-adapter/at-a-glance.svg) + +The repository also contains a separate [multibridge implementation](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/docs/multibridge/README.md) for vaults whose asset or share token moves over other cross-chain protocols. These docs cover the CCIP implementation. + +## Who it's for + +- **Vault teams**, such as issuers, protocols, and curators, that run an [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) vault on one chain and want deposits and redemptions from users on other chains connected by CCIP. +- **Integrators** who build deposit and redeem interfaces on source chains. They encode the payload and send the CCIP message, and deploy no contracts. + +The contract also supports Solana source chains. These docs cover EVM source chains. + +Asynchronous vaults, multi-asset vaults, and targets that are not ERC-4626 vaults need a fork; see [Use it as-is or customize it](#use-it-as-is-or-customize-it). An asynchronous vault that accepts synchronous ERC-4626 deposits needs the fork only for redemptions: deposits work with the adapter as-is. + +## Requirements + +The vault's **asset** is the token users deposit and receive when they redeem. **Shares** represent their position in the vault. Which tokens need CCIP support depends on where users send tokens from and where they want to receive the result: + +| If you want users to... | Tokens that must be CCIP-enabled | +| ------------------------------------------------------------------------ | -------------------------------- | +| Deposit from another chain and receive shares on the hub chain | Asset only | +| Deposit from another chain and receive shares back on the source chain | Asset and shares | +| Redeem from another chain and receive the asset on the hub chain | Shares only | +| Redeem from another chain and receive the asset back on the source chain | Shares and asset | + +Each token that crosses chains must be configured as a [CCIP cross-chain token (CCT)](/ccip/concepts/cross-chain-token/overview) between the source chain and the hub chain. + +**If your vault's asset is not CCIP-enabled, you can still offer redemptions from another chain with delivery on the hub chain.** Users send their CCIP-enabled shares to the adapter, and the adapter redeems them and delivers the asset locally. The asset does not cross chains in this flow. + +To enable cross-chain transfers of shares, the share token typically uses Lock & Release on the hub chain, because only the vault may mint shares, and Burn & Mint on source chains. See the [Lock & Mint tutorial](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry). + +Your vault and deployment must also meet these requirements: + +| Requirement | Needed for | Details | +| ------------------------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Standard ERC-4626 vault | All flows | Synchronous and single-asset. The adapter calls `deposit` and `redeem` itself as depositor and share owner, so the vault must accept a contract in both roles. | +| Standard ERC-20 behavior | All flows | The asset and share token must transfer exactly the amount requested. The adapter does not handle fee-on-transfer or rebasing tokens. | +| Shanghai-compatible hub chain | Deploying the adapter | The adapter bytecode uses the `PUSH0` opcode. | +| Native gas token held by the adapter | Returning output to the source chain | The adapter pays each return leg's CCIP fee from its own native gas token balance. If the balance is too low, the request becomes a failed message. | + +> **NOTE: Need the adapter to support cross-chain tokens from other protocols?** +> +> 1. The multi-bridge version of our adapter contract is compatible with cross-chain tokens from some other protocols. +> 2. Please reach out to the [Chainlink team](https://chain.link/ccip-contact) for support if your use case demands that. + +## What you deploy and operate + +You deploy one adapter on the hub chain with one call to `deploy` on the `CrossChainERC4626AdapterFactory`. That transaction sets the accepted source chains, allowlists your vault when you pass its address and enable it, sets the deposit and redeem switches and the adapter fees, and grants the admin, fee setter, and fee collector roles to your accounts. The factory then renounces all of its own roles. The [Factory contract](/solutions/cross-chain-vault-adapter/reference/factory-contract) reference lists the factory's deployed addresses and its inputs. One adapter can allowlist several vaults on the same chain, and each payload names the vault it targets. + +A lane's version is defined as the version of the CCIP OnRamp and OffRamp contracts that serve it, and the [CCIP Directory](/ccip/directory) shows it. The factory does not apply CCIP 2.0 lane settings or fund the adapter, so after deployment you: + +- Set the return format for each CCIP 2.0 lane to `GENERIC_EXTRA_ARGS_V3_BASIC`. +- Optionally set the inbound finality and [Cross-Chain Verifier (CCV)](/ccip/concepts/ccvs/overview) settings for each CCIP 2.0 source chain. +- Fund the adapter with the native gas token for return legs. + +Your team holds the three roles on the adapter: + +| Role | Controls | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `DEFAULT_ADMIN_ROLE` | Source chains, the vault allowlist, the deposit and redeem switches, CCIP 2.0 lane settings, roles, and withdrawing the entire native gas token balance | +| `FEE_SETTER_ROLE` | Adapter fees | +| `FEE_COLLECTOR_ROLE` | Withdrawing collected adapter fees | + +Roles use OpenZeppelin [`AccessControlEnumerable`](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControlEnumerable). Role changes take effect in one step, and renouncing the last admin locks the configuration permanently. + +You also monitor the `MessageFailed` event. A failed request leaves its tokens in the adapter until someone refunds or recovers them. Disabling a source chain, a vault, or processing does not pause CCIP delivery. Messages that arrive become failed messages. See [What counts as a failure](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#what-counts-as-a-failure). + +> **NOTE: Chainlink does not run your adapter** +> +> Chainlink does not deploy, operate, or control adapters. See [CCIP service +> responsibility](/ccip/concepts/service-responsibility). + +## Use it as-is or customize it + +Use the adapter as-is through the factory when your vault meets the [requirements](#requirements). Otherwise, fork the [repository](https://github.com/smartcontractkit/cross-chain-vault-adapters) and edit [`CrossChainERC4626Adapter.sol`](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/src/ccip/CrossChainERC4626Adapter.sol). The contract has no virtual hooks for processing, so you change `processMessage` and `_processTarget` directly. Common reasons to fork are asynchronous vaults (typically for the redemption path), multi-asset vaults, targets that are not ERC-4626 vaults, custom payload encoding, and custom validation of the source chain, sender, or beneficiary. + +A fork must keep these safety properties: + +- `ccipReceive` accepts calls only from the CCIP Router. +- Processing runs in a self-call (`this.processMessage`) inside `try`/`catch`, so a processing revert does not revert CCIP execution. +- Failed messages are stored and stay recoverable through cross-chain refund and local recovery. +- Address conversion checks reject malformed addresses, such as a beneficiary whose upper 96 bits are not zero. +- The failure handler (the `catch` path) must not revert on any input, because a revert there strands the tokens, as described in [Failures with no recovery path](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#failures-with-no-recovery-path). + +The contracts in `src/ccip` are audited. A fork is not the audited code and needs its own review and its own deployment, because the factory deploys only the original adapter. + +## Design choices + +| Choice | What it means | Trade-off | +| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Vault accounting stays on the hub chain | Every deposit and redemption executes against the vault on the hub chain. No copy of the vault or its share price runs elsewhere. | Each request waits for CCIP delivery and uses the vault's exchange rate at execution, not at send time. A [minimum output](/solutions/cross-chain-vault-adapter/overview/how-it-works#slippage-protection) bounds the difference. | +| No contracts on source chains | Users send a standard CCIP message. The adapter has no sender allowlist, so any address on an enabled source chain can use it. | Nothing checks the payload before the tokens reach the hub chain, and a malformed payload can strand tokens permanently. Build it as described in [What a user sends](/solutions/cross-chain-vault-adapter/overview/how-it-works#what-a-user-sends). | +| Processing failures are held, not reverted | When processing reverts, for example in the vault call, the minimum output check, or the return send, the adapter keeps the tokens and records a failed message. | The [CCIP Explorer](https://ccip.chain.link) still shows Success, so track `MessageFailed` instead. Nothing is retried or refunded automatically. | +| Permissionless cross-chain refund, optional local recovery | Anyone can call `refundFailedMessage` to return the tokens to the original sender on the source chain. The payload's local refund address can call `recoverFailedMessageLocally` instead. | The refund caller pays the CCIP fee in the native gas token. Refunds go to the original sender, not the beneficiary. Local recovery needs a local refund address set before sending. See [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery). | + +[Fees](/solutions/cross-chain-vault-adapter/overview/how-it-works#fees) explains the adapter fee, and [Limitations](/solutions/cross-chain-vault-adapter/reference/limitations) covers the remaining trade-offs. + +## Where to go next + +- [How it works](/solutions/cross-chain-vault-adapter/overview/how-it-works): the payload, both flows, delivery options, and fees. +- [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery): failed requests and how users get their tokens back. +- [Limitations](/solutions/cross-chain-vault-adapter/reference/limitations): what the adapter does not do. +- [Adapter contract](/solutions/cross-chain-vault-adapter/reference/adapter-contract): the `CrossChainERC4626Adapter` interface. +- [Factory contract](/solutions/cross-chain-vault-adapter/reference/factory-contract): the `CrossChainERC4626AdapterFactory` interface and deployed addresses. +- [Deploy the vault and set up its share token](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault): deploy an example ERC-4626 vault on Ethereum Sepolia and make its share token a CCIP cross-chain token. +- [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter): deploy, configure, and fund an adapter for that vault through the factory. +- [Deposit from a source chain](/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain): deposit into the vault from Arbitrum Sepolia through the adapter and receive the shares on Arbitrum Sepolia. +- [Redeem from a source chain](/solutions/cross-chain-vault-adapter/guides/redeem-from-source-chain): redeem the shares from Arbitrum Sepolia, receive CCIP-BnM back, and withdraw the adapter fees as the vault operator. + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. + +--- + +# How it works +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter/overview/how-it-works + +The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. + +A request to the Cross-Chain Vault Adapter is one CCIP message that carries the vault's asset or shares from a source chain to the hub chain. In the transaction that receives the message, the adapter calls the vault, then either delivers the output on the hub chain or sends it back to the source chain. This page follows a request that succeeds. [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery) covers requests that do not. + +## Architecture + +The vault is the product. It holds one asset and earns the yield. CCIP is the bridge that moves tokens between chains. The adapter is how users on other chains reach the vault. It turns an incoming CCIP message into a vault call. For how CCIP itself carries a message, see the [CCIP architecture overview](/ccip/concepts/architecture/overview). + +The following diagram shows the contracts in a deployment, where each one lives, and what the adapter holds: its configuration, its three roles, and its link to the hub chain's CCIP Router. + +![Components diagram. On the source chain, the user or app calls ccipSend on the CCIP Router. A CCIP lane connects that router with the CCIP Router on the hub chain. On the hub chain, the factory deploys the adapter, grants its roles, and keeps none. The adapter box lists its configuration (allowed source chains, allowed vaults, deposit and redeem switches, adapter fees, CCIP 2.0 settings) and its roles (DEFAULT_ADMIN_ROLE, FEE_SETTER_ROLE, FEE_COLLECTOR_ROLE). The adapter allowlists one or more ERC-4626 vaults and is fixed to the hub chain's CCIP Router.](/images/solutions/cross-chain-vault-adapter/architecture.svg) + +All three contracts live on the hub chain: + +- **Vault:** the team's existing [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) vault, unchanged. It holds one asset and issues the share token. +- **Adapter:** [`CrossChainERC4626Adapter`](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/src/ccip/CrossChainERC4626Adapter.sol) receives CCIP messages, calls the vault, and delivers or returns the output. The vault team deploys and operates it. +- **Factory:** [`CrossChainERC4626AdapterFactory`](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/src/ccip/CrossChainERC4626AdapterFactory.sol) deploys an adapter with its initial configuration in one transaction, then hands every role to the vault team's accounts. + +The adapter needs no contract on source chains. Users call `ccipSend` on the CCIP Router there directly. On the hub chain, the CCIP Router delivers messages to the adapter and sends the adapter's return legs. Shares that travel to or from a source chain do need a share token deployed there, as described in [Redeem flow](#redeem-flow). + +The adapter enforces the configuration shown in the diagram: + +- **Allowed source chains:** a chain type per CCIP chain selector, set with `setChainType`. A selector set to `NONE` is not allowed. +- **Allowed vaults:** set with `setTargetEnabled`. The payload names one of them. +- **Deposit and redeem switches:** `depositsEnabled` and `redeemsEnabled`, set together with `setProcessingEnabled`. +- **Adapter fees:** see [Fees](#fees). +- **CCIP 2.0 settings:** the inbound verifier and finality policy per source chain, and the return format and finality per destination. See [CCIP 2.0 lanes](#ccip-20-lanes). +- **Roles:** `DEFAULT_ADMIN_ROLE` changes the configuration, `FEE_SETTER_ROLE` sets adapter fees, and `FEE_COLLECTOR_ROLE` withdraws them. See [Roles](/solutions/cross-chain-vault-adapter/reference/adapter-contract#roles). + +One adapter can serve several vaults and several source chains. The adapter does not check who sent a message. Any sender on an allowed source chain can deposit into or redeem from any allowlisted vault. The CCIP Router address is fixed when the adapter is deployed, and only that router can call `ccipReceive`. + +## What a user sends + +A user sends one CCIP message to the adapter. The message must carry exactly one token in `tokenAmounts` and a payload of exactly 128 bytes in `data`. The token decides the action: + +- The vault's asset (the address returned by the vault's `asset()`) means deposit. +- The vault's share token (the vault address itself) means redeem. +- Any other token fails the request. + +The payload is the ABI encoding of four 32-byte fields: + +| Field | Type | Meaning | +| ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `target` | `address` | The vault to deposit into or redeem from. It must be on the adapter's allowlist. | +| `beneficiary` | `bytes32` | The account that receives the output: on the hub chain for local delivery, on the source chain for a return. It must be an EVM address left-padded with zeros, so the upper 12 bytes are zero. | +| `minimumOut` | `uint256` | The smallest acceptable output after the adapter fee, in output units: shares for a deposit, asset for a redemption. See [Slippage protection](#slippage-protection). | +| `deliveryAndRefund` | `uint256` | Where the output goes, and who can recover the tokens on the hub chain if the request fails. Bit 0 is the return-to-source flag, and bits 1 to 160 hold the local refund address. | + +The sender packs the last field as `(uint256(uint160(localRefundAddress)) << 1) | (returnToSourceChain ? 1 : 0)`. The adapter ignores bits 161 to 255. The local refund address is the hub-chain account allowed to take back the tokens if the request fails. The [message format](/solutions/cross-chain-vault-adapter/reference/adapter-contract#message-format) reference has the exact encoding. [Build the payload](/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain#build-the-payload) builds one with the repository's `pnpm ccip:build-payload` script. + +The rest of the message follows standard CCIP rules: + +- **Receiver:** the adapter address on the hub chain. +- **Fee token:** the sender pays the CCIP fee on the source chain in the native gas token or LINK. +- **Extra arguments:** on a CCIP 2.0 lane, the message carries `GenericExtraArgsV3`, encoded with [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec). + +The gas limit in the extra arguments pays for everything the adapter does on the hub chain: its checks, the vault call, and, when the output returns to the source chain, the outbound CCIP send. CCIP bills the sender for the gas limit, not the gas used. Measure the gas with a test message against your own vault, and give requests that return output to the source chain more headroom than local delivery, because only they include the outbound send. If the limit is too low, `ccipReceive` can run out of gas before it records the failure, and CCIP marks the message as failed. Recovery then needs CCIP [manual execution](/ccip/concepts/manual-execution) with a higher limit. + +The requested finality in the extra arguments must be one that both the adapter and the token pools allow. By default, the adapter accepts only messages that wait for finality. + +> **CAUTION: The payload is final once sent** +> +> Nobody can change the payload after the message leaves the source chain. Before the user signs, check that: +> +> - The message carries exactly one token: the vault's asset or its share token. +> - `data` is exactly 128 bytes. +> - The payload is built with a standard ABI encoder, such as Solidity `abi.encode`, ethers, viem, or `cast`. +> - `target` is a vault on the adapter's allowlist. +> - `beneficiary` is an EVM address whose upper 12 bytes are zero. +> - The local refund address is non-zero and is an account the user controls on the hub chain. +> +> Without a local refund address, the only way back for a failed request is a cross-chain refund to the original sender. A malformed payload can strand tokens permanently, as described in [Failures with no recovery path](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#failures-with-no-recovery-path). + +## Deposit flow + +A deposit turns the asset sent from the source chain into vault shares. The following diagram shows a deposit from a source chain into a vault on the hub chain, with the return-to-source branch dashed. + +![Deposit flow diagram with numbered steps. On the source chain, the user sends the asset and the payload. CCIP verifies and delivers the message. On the hub chain, in one transaction, the adapter validates the message, routes it as a deposit, deducts the adapter fee only when returning, deposits into the vault, and checks the output against minimumOut. The shares then go to the beneficiary on the hub chain, or, on the dashed branch, back to the beneficiary in a second CCIP message on the same source chain: the diagram draws a single source-chain region holding both the first and the last step, because a return goes only to the chain the request came from.](/images/solutions/cross-chain-vault-adapter/deposit-flow.svg) + +The request passes these checks and decisions in order. From the second item on, the first check that fails turns the request into a failed message, and [What counts as a failure](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#what-counts-as-a-failure) lists the error for each. + +1. **CCIP 2.0 policy, before the adapter runs.** On a CCIP 2.0 lane, the OffRamp on the hub chain checks the message against the adapter's [Cross-Chain Verifier (CCV)](/ccip/concepts/ccvs/overview) and finality policy for the source chain. If it passes, CCIP transfers the asset to the adapter and calls `ccipReceive`, which runs every later check in an isolated self-call, `processMessage`. +2. **Source chain and message shape.** The source chain's selector must not be set to `NONE`. The message must carry exactly one token and a 128-byte payload. +3. **Vault and amount.** The payload's `target` must be allowlisted, which the adapter checks before it looks at the token. The amount must be greater than zero. +4. **Action.** The token is the vault's asset, so the request is a deposit, and `depositsEnabled` must be true. +5. **Adapter fee, return only.** When bit 0 of `deliveryAndRefund` asks for a return, the adapter deducts the fee keyed by the source chain's selector and the share token. The fee must be smaller than the amount. +6. **Vault deposit.** The adapter deposits the rest with itself as the receiver, so it is the depositor and the first owner of the new shares. If the vault's `deposit` reverts, the request fails. +7. **Output.** The vault must mint more than zero shares and at least `minimumOut`. +8. **Delivery.** In both branches, the beneficiary's upper 12 bytes must be zero. For local delivery, the adapter transfers the shares on the hub chain. For a return, it quotes the return-leg CCIP fee, pays it from its own native gas token balance, and sends the shares to the source chain in a second CCIP message. + +The adapter's checks, the vault call, and the delivery all run in one transaction on the hub chain. A request that passes them ends with `MessageSucceeded`. If any check or call fails, the adapter rolls back the vault call, the fee, and the outbound send, keeps the inbound asset, and records a failed message. The [CCIP Explorer](https://ccip.chain.link) still shows Success, as [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#why-the-ccip-explorer-shows-success) explains. + +## Redeem flow + +A redemption turns shares sent from the source chain into the vault's asset. It follows the deposit flow with these differences: + +- **The user sends shares.** The vault's share token must be a [CCIP cross-chain token (CCT)](/ccip/concepts/cross-chain-token/overview) on the lane. A typical setup uses a Lock & Release pool on the hub chain, because only the vault may mint shares, and a Burn & Mint pool on the source chain. The [Lock & Mint tutorial](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) shows that setup. +- **The adapter calls `redeem`.** The token is the vault address, so the adapter routes the request as a redemption, which requires `redeemsEnabled`. It redeems the shares as both owner and receiver. +- **The adapter fee comes from the output.** When returning, the adapter deducts the fee from the redeemed assets instead of from the input. +- **`minimumOut` is in asset units.** The adapter compares it with the redeemed assets after the fee. +- **The output is the asset.** The adapter transfers it to the beneficiary on the hub chain or sends it back to the source chain. + +## Delivery options + +Bit 0 of `deliveryAndRefund` chooses where the output goes. The following table compares the two options. + +| | Local delivery | Return to the source chain | +| -------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------- | +| Where the output goes | The beneficiary on the hub chain | The beneficiary on the source chain the message came from | +| Adapter fee | None | Charged in asset units | +| CCIP fee for delivery | None, because there is no second message | Paid by the adapter from its native gas token balance on the hub chain | +| Tokens that must be CCIP-enabled for deposit | The asset | The asset and the share token | +| Tokens that must be CCIP-enabled for redeem | The share token | The share token and the asset | +| Extra latency | None | One more CCIP message, from the hub chain to the source chain | +| Event on the hub chain | `LocalTokenDelivered` | `MessageSent`, with the return message's CCIP message ID | + +A return goes only to the chain the request came from, never to a third chain. + +The return leg is a separate, token-only CCIP message with empty `data` and a gas limit of 0. The adapter builds it itself and never copies extra arguments from the user's message. Track it on the CCIP Explorer with the message ID from `MessageSent`. + +## Fees + +A request can involve up to four fees. The [CCIP billing page](/ccip/concepts/fees-and-billing) explains how CCIP prices a message. + +| Fee | Who pays | Paid in | When | Controlled by | +| ----------------------------- | --------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| CCIP fee for the user message | The sender, on the source chain | Native gas token or LINK | Every request | CCIP | +| Adapter fee | The user, from the deposited asset or the redeemed asset | The asset's smallest units | Only when the output returns to the source chain | Set by `FEE_SETTER_ROLE` with `setAssetFee` or `setAssetFees`; withdrawn by `FEE_COLLECTOR_ROLE` with `withdrawFee` | +| Return-leg CCIP fee | The adapter, from its native gas token balance on the hub chain | Native gas token | Every return to the source chain | CCIP | +| Refund CCIP fee | Whoever calls the cross-chain refund, through `msg.value` | Native gas token | Only for a [cross-chain refund](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#cross-chain-refund) of a failed message | CCIP; the adapter returns any excess `msg.value` to the caller | + +### Adapter fee + +The adapter fee is a flat amount, stored in `assetFees`. The adapter charges the fee on both deposits and redemptions whose output returns to the source chain, and it takes the fee from the deposited asset before a deposit or from the redeemed asset after a redemption. The fee is keyed by the source chain selector and the token that goes back: the share token for a deposit return, the asset for a redeem return. The amount is in the asset's smallest units even when shares go back. + +The adapter reads the fee when the message executes on the hub chain, so a fee change made while a message is in flight applies to that message. If the fee is equal to or larger than the deposit amount or the redeemed assets, the request fails. Fees accrue per asset in `collectedFees`, and the fee collector withdraws them with `withdrawFee`. + +### Return-leg CCIP fee + +The adapter quotes the return-leg fee from the CCIP Router at send time and pays it from its own native gas token balance, which the vault team funds by sending the native gas token to the adapter. If the balance is below the quote, the send reverts and the request becomes a failed message. Collected adapter fees are a separate balance. The adapter never converts them into the native gas token. + +## Slippage protection + +`minimumOut` is the smallest output the beneficiary accepts. It is measured in output units after the adapter fee: shares for a deposit, asset for a redemption. The adapter checks it after the vault call and before delivery. An output of zero always fails, even when `minimumOut` is 0. An output below `minimumOut` fails the request, which becomes a failed message that the user can [refund or recover](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery). + +To choose a value, call [`preview`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#preview) on the adapter on the hub chain. Pass the inbound token (the asset for a deposit, the share token for a redemption), the vault, the amount, the return flag, and the source chain selector, which the [CCIP Directory](/ccip/directory) lists. Pass the selector for local delivery too. For a non-zero amount, `preview` reverts when the selector is not configured. `preview` applies the same routing and adapter fee as a real request and asks the vault's `previewDeposit` or `previewRedeem` for the result. + +For some requests that would fail, such as one where the adapter fee consumes the amount, `preview` returns 0 instead of reverting. For others, such as one that names a vault that is not allowlisted, it reverts. The `preview` reference lists each condition in the order the function checks it. + +The vault's exchange rate can move between the send and the execution on the hub chain, a gap that includes the lane's CCIP latency. Subtract a tolerance for that movement from the `preview` result and use the remainder as `minimumOut`. [Build the payload](/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain#build-the-payload) uses a 1% tolerance. + +`minimumOut` compares the amounts the vault reports. It does not protect against fee-on-transfer or rebasing tokens, or against donation or share inflation effects on the vault. [Limitations](/solutions/cross-chain-vault-adapter/reference/limitations#vault-and-token-compatibility) lists which vaults and tokens the adapter supports. + +## CCIP 2.0 lanes + +A lane's version is the version of the CCIP OnRamp and OffRamp contracts that serve it, and the [CCIP Directory](/ccip/directory) shows it. + +The adapter implements the CCIP 2.0 receiver interface, [`IAny2EVMMessageReceiverV2`](/ccip/evm/api-reference/v2.0.0/i-any2-evm-message-receiver-v2). On a CCIP 2.0 lane, it controls which messages it accepts and how its own return legs are encoded. + +### Inbound settings + +The admin sets two values per source chain: + +- **CCV policy:** `setCCVsConfig` sets the required CCVs, the optional CCVs, and how many optional CCVs must attest. An empty configuration means the lane's default verifiers. +- **Allowed finality:** `setInboundFinality` sets which finality modes the adapter accepts. The default, `0x00000000`, accepts only messages that wait for finality, so CCIP rejects [faster-than-finality](/ccip/concepts/execution-latency/ftf-dapps) messages to the adapter. + +The CCIP 2.0 OffRamp reads both values through `getCCVsAndFinalityConfig` before it calls `ccipReceive`. A message that lacks the required CCV attestations or requests a finality the adapter does not allow fails at the CCIP level, before the adapter runs. The adapter records no failed message in that case. Recovery is described in [Failures the adapter can't catch](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#failures-the-adapter-cant-catch). CCIP 1.6 lanes ignore both inbound settings. + +### Outbound settings + +Return legs and cross-chain refunds are messages that the adapter sends, so the admin sets their format per destination chain: + +- **Return format:** `setEvmReturnLaneFormat` sets `GENERIC_EXTRA_ARGS_V3_BASIC` for a CCIP 2.0 lane. For a CCIP 1.6 lane, leave the format unset or set `LEGACY_EXTRA_ARGS_V2`. The adapter then sends `GenericExtraArgsV2` with a gas limit of 0 and out-of-order execution allowed. +- **Requested finality:** `setEvmReturnRequestedFinality` sets the finality for each destination and returned token. It accepts a non-zero value only after the destination's format is V3. `0x00000000` waits for finality and is the default. `0x00010000` waits for the `safe` tag. `0x00000001` to `0x0000FFFF` waits for that number of blocks. The token pools on the lane must allow the requested finality. + +Do not set the V3 format on a CCIP 1.6 lane. The CCIP 1.6 FeeQuoter accepts only the V1 and V2 extra arguments tags and reverts with `InvalidExtraArgsTag` for any other. The adapter quotes the fee before every send, so return requests to that chain become failed messages and cross-chain refunds to it revert. + +The following table lists the CCIP settings for an EVM source chain connected to the hub chain by CCIP 2.0 lanes. + +| Setting | Function | Value on a CCIP 2.0 lane | +| -------------------- | ------------------------------- | --------------------------------------------------------------------- | +| Allowed source chain | `setChainType` | The source chain's selector, chain type `EVM` | +| Inbound verifiers | `setCCVsConfig` | Empty lists to use the lane's default verifiers | +| Inbound finality | `setInboundFinality` | `0x00000000` (default) to accept only messages that wait for finality | +| Return format | `setEvmReturnLaneFormat` | `GENERIC_EXTRA_ARGS_V3_BASIC` | +| Return finality | `setEvmReturnRequestedFinality` | `0x00000000` (default) per returned token, or a value the pools allow | + +The factory can set the chain type at deployment, but none of the other four settings, so the admin sets them after deployment. For an example that sets the return format with `pnpm ccip:configure`, see [Set the return format and fund the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter#fund-the-adapter). + +## Where to go next + +- [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery): what happens when a request fails and how the tokens come back. +- [Limitations](/solutions/cross-chain-vault-adapter/reference/limitations): vault and token compatibility, operational limits, and when funds can get stuck. +- [Adapter contract](/solutions/cross-chain-vault-adapter/reference/adapter-contract): every function, event, and error of `CrossChainERC4626Adapter`. +- [Deploy the vault and set up its share token](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault) and [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter): the tutorials for the vault team. +- [Deposit from a source chain](/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain) and [Redeem from a source chain](/solutions/cross-chain-vault-adapter/guides/redeem-from-source-chain): the tutorials that send a deposit and a redemption through the adapter as a user on Arbitrum Sepolia. + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. + +--- + +# Failures and recovery +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter/overview/failures-and-recovery + +The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. + +In this guide, “vault operator” refers to the team that operates and configures the vault and its adapter, not Chainlink Labs, the Chainlink Foundation, or Chainlink node operators. + +> When a cross-chain deposit or redemption fails on the hub chain, the user gets the tokens back with a cross-chain refund to the source chain or a local recovery on the hub chain. The adapter, which is how users on other chains reach the vault, calls the vault inside the transaction that receives the CCIP message, so a failure leaves the tokens in the adapter, not in the vault. The adapter records the failure and holds the tokens until someone recovers them. Some failures happen inside CCIP before the adapter records anything. Most of those are fixed with CCIP manual execution. Two sender mistakes have no recovery path, and their tokens are permanently lost, as described in [Failures with no recovery path](#failures-with-no-recovery-path). + +## What counts as a failure + +A request fails when anything reverts while the adapter processes the message. `ccipReceive` calls `processMessage` inside a `try/catch`, so every revert in the following table becomes a failed message. The [errors reference](/solutions/cross-chain-vault-adapter/reference/adapter-contract#errors) lists every error with its parameters. + +| Error | Trigger | +| :-------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `InvalidChain` | The source chain is not configured on the adapter (its chain type is `NONE`) | +| `InvalidTokenCount` | The message carries no token, or more than one | +| `InvalidPayloadLength` | `message.data` is not exactly 128 bytes | +| `InvalidTarget` | The payload's `target` vault is not allowlisted | +| `InvalidTargetToken` | The token is neither the vault's asset nor its share token | +| `AmountIsZero` | The token amount is 0 | +| `DepositsDisabled`, `RedeemsDisabled` | Deposits or redemptions are disabled | +| `FeeExceedsAmount` | The adapter fee is greater than or equal to the amount it is taken from (return to the source chain only) | +| The vault's own error | The vault's `deposit` or `redeem` call reverts | +| `NoOutputReceived` | The vault returns zero shares or zero assets | +| `MinimumOutputNotMet` | The output, after the adapter fee, is below the payload's `minimumOut` | +| `InvalidEVMAddress` | The beneficiary is not a valid EVM address (its upper 12 bytes are not zero) | +| The token's own error | The transfer to a local beneficiary reverts | +| `InsufficientNativeBalance` | The adapter's native gas token balance does not cover the CCIP fee for the return leg | +| The router's, OnRamp's, or token pool's error | The CCIP Router rejects the return leg, for example because the lane is not supported, the token has no pool on the lane, an outbound rate limit is exhausted, or the lane does not accept the configured return format | + +When one of these happens: + +- Every effect of the processing rolls back: the vault call, the adapter fee, and any return leg. +- The adapter holds the inbound tokens: the asset for a failed deposit, the shares for a failed redemption. +- The adapter sets the message's state to `BASIC` and stores a failure record with the source chain selector, the sender, the token amounts, and the local refund address from the payload. +- The adapter emits `MessageFailed(messageId)`. + +The adapter never refunds a failed message on its own and never retries it. Fixing the cause, for example by enabling deposits, allowlisting the vault, or funding the adapter, does not reprocess the message. The tokens leave the adapter only through a cross-chain refund or a local recovery. + +> **CAUTION: Disabling processing is not a pause** +> +> Calling `setProcessingEnabled(false, false)`, disabling a vault with `setTargetEnabled`, or setting a chain's type to +> `NONE` does not stop CCIP from delivering messages. Messages already in flight, and messages sent afterward, still +> arrive and become failed messages that someone must refund or recover. + +## Why the CCIP Explorer shows Success + +The adapter catches the error, so `ccipReceive` returns normally and CCIP execution succeeds. The [CCIP Explorer](https://ccip.chain.link) shows such a message as **Success**. That status only means CCIP delivered the tokens to the adapter. The vault call may not have happened. + +To learn what the adapter did, look up the message on the hub chain by its CCIP message ID. This is the ID that `ccipSend` returned on the source chain and that the CCIP Explorer displays. Check either: + +- The adapter's events in the delivery transaction: `MessageSucceeded(messageId)` or `MessageFailed(messageId)`. +- The adapter's [`messageErrorCode`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#state-variables) value for the message ID: `0` is `NONE`, `1` is `BASIC` (failed, tokens waiting to be recovered), `2` is `RESOLVED`. + +The following command reads the adapter's state for a message with Foundry's `cast`. Replace the variables with your adapter address, the CCIP message ID, and an RPC URL for the hub chain: + +```bash +cast call $ADAPTER "messageErrorCode(bytes32)(uint8)" $MESSAGE_ID --rpc-url $HUB_CHAIN_RPC_URL +``` + +When processing succeeds, the same transaction also shows where the output went. `LocalTokenDelivered` means the beneficiary received the shares or assets on the hub chain. `MessageSent` means the adapter started a return leg. Its first field is the return leg's own CCIP message ID, which you track separately in the CCIP Explorer. [Deposit from a source chain](/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain#check-the-result) runs these checks on a real deposit and follows its return leg. + +## Cross-chain refund + +[`refundFailedMessage`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#refundfailedmessage) sends the inbound tokens back over CCIP to the address that sent the original message. + +- **Who can call it:** Anyone. The recipient comes from the stored failure record, not from the caller, so the user, the vault team's support desk, or a relayer can trigger it. +- **What comes back:** The original inbound tokens at the amount the adapter received: the asset for a failed deposit, the shares for a failed redemption. The adapter fee is not charged. +- **Where it goes:** The original `message.sender` on the original source chain. It does not go to the beneficiary or the local refund address. +- **Who pays:** The caller, in the native gas token through `msg.value`. The adapter's own native gas token balance is not used. If `msg.value` does not cover the fees actually paid, the call reverts with `InsufficientRecoveryFee` or `InsufficientNativeBalance`. The adapter returns any excess to the caller in the same transaction. The refund reverts with `RefundFailed` if the caller cannot receive the native gas token. +- **Result:** The state becomes `RESOLVED`. The adapter emits `MessageRefunded` and a `MessageSent` event that carries the refund's CCIP message ID. + +A refund of shares works only if the share token is a [CCIP cross-chain token (CCT)](/ccip/concepts/cross-chain-token/overview) on the lane back to the source chain. Returning shares to the source chain has the same requirement. + +### Refund sequence + +A refund takes three calls on the hub chain, each with the failed message's CCIP message ID: + +1. [`checkRefundEligibility(messageId)`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#checkrefundeligibility) returns `canRefund`, the recipient, the token, the amount, and a fee estimate. It returns `canRefund = false` when the message is not in the `BASIC` state, when the stored sender is not exactly 32 bytes, or when no token amount is above zero. It can also revert when the CCIP Router cannot quote the refund, for example when the lane is not supported or the token has no pool on it. Treat that revert as "not refundable right now". +2. [`estimateRefundFee(messageId)`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#estimaterefundfee) returns the fee estimate on its own. The estimate can be lower than the actual cost, because each refund send quotes its fee again when it executes. +3. `refundFailedMessage(messageId)`, sent with a `msg.value` of the estimate plus a margin. The unused amount comes back to the caller. + +### Refunds to contracts + +If a contract sent the original message, such as a sender contract or a smart account on the source chain, that contract receives the refund. The refund is a token-only transfer with empty data and a gas limit of 0, so CCIP does not call `ccipReceive` on the recipient. The contract must be able to move or use tokens it receives without a callback, or the tokens stay in that contract. + +### When a refund is unavailable + +A refund uses the same settings as a return leg to that chain: the return format set with `setEvmReturnLaneFormat` and the requested finality set with `setEvmReturnRequestedFinality` for the inbound token. If the lane, the token pool, or a rate limit cannot carry the token at that moment, the refund transaction reverts. The message stays `BASIC`, and anyone can try again later. + +Refunds keep working after the vault operator sets the source chain's type to `NONE`. The adapter records the source chain's family when the message fails, and infers it from the sender address if the chain was not configured at that time. + +## Local recovery + +[`recoverFailedMessageLocally`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#recoverfailedmessagelocally) transfers the inbound tokens to the local refund address on the hub chain. + +- **Who can call it:** Only the local refund address stored from the payload. Any other caller gets `UnauthorizedLocalRefund`. The address must be able to send that transaction, so use an account the user controls, not a contract that cannot make arbitrary calls. +- **What it needs:** A non-zero `localRefundAddress` in bits 1 to 160 of the payload's `deliveryAndRefund` field, set when the message is sent. See [What a user sends](/solutions/cross-chain-vault-adapter/overview/how-it-works#what-a-user-sends). It cannot be added after the message is sent. If it is zero, the call reverts with `NoLocalRefundAddress`. If the payload is not exactly 128 bytes, the adapter cannot read it and stores no local refund address. +- **What comes back:** The inbound tokens on the hub chain: the asset for a failed deposit, the shares for a failed redemption. The user can then deposit into or redeem from the vault directly. +- **Who pays:** No CCIP fee. The caller pays only hub-chain gas. +- **Result:** The state becomes `RESOLVED` and the adapter emits `MessageRecoveredLocally`. + +A local recovery takes two calls on the hub chain, each with the failed message's CCIP message ID: + +1. [`checkLocalRecoveryEligibility(messageId)`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#checklocalrecoveryeligibility) returns `canRecover`, the local refund address, the token, and the amount. It never reverts, and it does not check who the caller is. +2. `recoverFailedMessageLocally(messageId)`, sent from the local refund address with no `msg.value`. + +The following table compares the two recovery paths: + +| | Cross-chain refund | Local recovery | +| :-------------- | :------------------------------------------------------------------------ | :---------------------------------------------------- | +| Function | `refundFailedMessage` | `recoverFailedMessageLocally` | +| Who can call | Anyone | Only the local refund address | +| Where tokens go | The original sender on the source chain | The local refund address on the hub chain | +| Who pays | The caller pays the CCIP fees in the native gas token through `msg.value` | The caller pays hub-chain gas only | +| Set in advance | Nothing in the payload; the sender must be exactly 32 bytes | A non-zero `localRefundAddress` in a 128-byte payload | +| Depends on | The lane and token pool back to the source chain being available | The hub chain only | +| Event | `MessageRefunded` | `MessageRecoveredLocally` | + +Both paths stay open while the message is `BASIC`. Whichever succeeds first resolves the message, and the other then reverts with `MessageNotFailed`. + +Setting a local refund address on every message gives the user a recovery path that does not depend on the lane back to the source chain. The trade-off is that recovered tokens arrive on the hub chain, not the source chain. + +## Failures the adapter can't catch + +Some failures happen inside CCIP, before the adapter's `try/catch` can record anything. In the cases in the following table, the adapter emits no event, its state for the message stays `NONE`, and the tokens are not released on the hub chain. The CCIP Explorer shows the message as failed, usually as **Ready for manual execution**. After the cause is fixed, anyone can retry the message with [manual execution](/ccip/concepts/manual-execution). + +| Cause | Recovery | +| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | +| The gas limit in the user's message is too low, so `ccipReceive` runs out of gas, usually before the adapter can record a failure | Manual execution with a higher gas limit | +| On a CCIP 2.0 lane, the message requests a finality that the adapter's inbound finality setting does not allow | The vault operator updates `setInboundFinality`, then manual execution | +| On a CCIP 2.0 lane, the message lacks attestations from a required [Cross-Chain Verifier (CCV)](/ccip/concepts/ccvs/overview), or the optional CCV threshold is not met | The verifier attests or the vault operator updates `setCCVsConfig`, then manual execution | +| A token pool rate limit on the hub chain is exhausted, or the token's pool on the hub chain is missing or incompatible | Wait for the rate limit to refill or for the pool to be configured, then manual execution | + +The adapter's default inbound finality is `0x00000000`, which means wait for finality. On a CCIP 2.0 lane, CCIP rejects a [faster-than-finality](/ccip/concepts/execution-latency/ftf-dapps) message from a source chain until the vault operator allows it, and waiting for the source chain to finalize does not help. CCIP 1.6 lanes do not read the adapter's CCV or finality settings. + +A return leg or a refund is a separate CCIP message from the hub chain to the source chain. If it fails on the source chain, for example on a token pool rate limit, recover it with manual execution using the CCIP message ID from the adapter's `MessageSent` event. + +### Failures with no recovery path + +> **DANGER: Sender mistakes with no recovery path** +> +> Two sender mistakes have no recovery path, and can result in a permanent loss of funds: +> +> 1. A message with empty data and a gas limit of 0. CCIP treats a message with empty data and a gas limit of 0 as a token-only transfer and delivers the tokens to the adapter without calling `ccipReceive`. The adapter records nothing and has no function that returns those tokens. +> 2. A message with a malformed payload. Neither the adapter nor CCIP manual execution can return these tokens to the user, so the tokens are permanently lost. + +1. A payload can be exactly 128 bytes and still be malformed when its `target` word has non-zero upper 12 bytes, so it + is not a valid ABI-encoded address. `processMessage` reverts when it decodes the payload, and the adapter catches + that revert. While recording the failure, the adapter decodes the same bytes again to read the local refund address. + That second decode runs outside the `try/catch`, so `ccipReceive` itself reverts. CCIP then reverts the + destination-side token release together with the receiver call and marks the message failed. +2. Manual execution can raise the gas limit but cannot change the message, so every attempt reverts, with + `NoStateProgressMade` on a CCIP 2.0 lane or `ExecutionError` on a CCIP 1.6 lane. The tokens are never released or + minted on the hub chain. They stay locked in, or burned by, the token pool on the source chain. +3. Standard ABI encoders (Solidity `abi.encode`, ethers, viem, `cast`) cannot produce this payload; only hand-built + encoders can. + +> **Important:** Before a final signature, build the payload with a standard ABI encoder and check it against the [payload checklist](/solutions/cross-chain-vault-adapter/overview/how-it-works#what-a-user-sends). Also set a non-zero local refund address. Without one, a failed request whose cross-chain refund is also impossible leaves the tokens in the adapter permanently. + +## Message lifecycle + +The following diagram shows the states a message can reach after CCIP delivers it to the adapter, and the CCIP-level branch where the adapter records nothing: + +![Message lifecycle: CCIP delivers the message, and processing either succeeds (state NONE, MessageSucceeded) or fails (state BASIC, MessageFailed). From BASIC, a cross-chain refund (anyone calls, caller pays CCIP fees) or a local recovery (local refund address only, no fee) moves the message to RESOLVED. A dashed branch shows a CCIP-level failure, where ccipReceive reverts or the message is rejected before the adapter records anything, followed by manual execution after the cause is fixed, which is not possible for a malformed payload.](/images/solutions/cross-chain-vault-adapter/message-lifecycle.svg) + +The adapter tracks each message ID in `messageErrorCode` with three states: + +| State | Value | Meaning | Related events | +| :--------- | :---- | :------------------------------------------------------------------------------------------- | :--------------------------------------------- | +| `NONE` | `0` | The message has not arrived yet, or it was processed successfully | `MessageSucceeded` when processed | +| `BASIC` | `1` | Processing failed and the adapter holds the inbound tokens; refund or local recovery is open | `MessageFailed` | +| `RESOLVED` | `2` | The failed message was returned through a refund or a local recovery | `MessageRefunded` or `MessageRecoveredLocally` | + +`MessageSucceeded` tells the two meanings of `NONE` apart, and `MessageRefunded` or `MessageRecoveredLocally` shows which recovery path produced `RESOLVED`. While a message is `BASIC`, [`getFailedMessageRecord`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#getfailedmessagerecord) returns its stored source chain selector, sender, token amounts, and local refund address. + +To find the next action for a request, combine the CCIP Explorer status with the adapter's state: + +| CCIP Explorer status | Adapter state and event | Meaning | Action | +| :------------------------- | :------------------------------------ | :--------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- | +| Any status before Success | `NONE`, no event | CCIP has not delivered the message yet | Wait for CCIP | +| Success | `NONE`, `MessageSucceeded` | The request completed | For local delivery, nothing. For a return, track the return leg from `MessageSent` | +| Success | `BASIC`, `MessageFailed` | The adapter rejected the request and holds the tokens | Refund to the source chain or recover locally | +| Success | `RESOLVED`, `MessageRefunded` | The tokens were sent back to the source chain | Track the refund's CCIP message ID from `MessageSent` | +| Success | `RESOLVED`, `MessageRecoveredLocally` | The local refund address received the tokens on the hub chain | Nothing | +| Success | `NONE`, no adapter event | The message had empty data and a gas limit of 0, so the tokens reached the adapter unprocessed | None. The tokens are permanently lost, because the adapter has no function that returns them | +| Ready for manual execution | `NONE`, no event | CCIP execution failed and the adapter recorded nothing | Fix the cause, then use manual execution. For a malformed payload, manual execution always fails and the tokens are permanently lost | + +## Where to go next + +- [Limitations](/solutions/cross-chain-vault-adapter/reference/limitations): what the adapter does not do, and every case where funds can get stuck. +- [Adapter contract recovery functions](/solutions/cross-chain-vault-adapter/reference/adapter-contract#recovery-functions): signatures, return values, and errors for refunds and local recovery. +- [CCIP message lifecycle](/ccip/concepts/message-lifecycle): how CCIP verifies and executes a message before the adapter runs. + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. + +--- + +# Deploy the vault and set up its share token +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter/guides/deploy-the-vault + +In this tutorial, we will: + +1. Deploy `ExampleERC4626Vault` on Ethereum Sepolia and verify it on Etherscan. +2. Deposit 1 CCIP-BnM and receive 1 vCCIP-BnM. +3. Transform `vCCIP-BnM` into a cross-chain token using the CCIP protocol under the [Lock & Mint mechanism](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry), by deploying: + - A Lock & Release pool on Ethereum Sepolia, and, + - A Burn & Mint pool on Arbitrum Sepolia. +4. Return to this page, transfer 0.1 vCCIP-BnM to Arbitrum Sepolia, and confirm the balances on both chains. + +> An [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) vault holds one asset and gives depositors shares in return. The vault contract is itself an ERC-20 token contract, and the shares are its tokens, so the vault's address is also the share token's address. In this tutorial, the asset is CCIP-BnM and the share token is Vault CCIP-BnM (vCCIP-BnM). Two adapter features need the share token to be a [CCIP cross-chain token (CCT)](/ccip/concepts/cross-chain-token/overview) on the Arbitrum Sepolia lane: returning shares to Arbitrum Sepolia, and redeeming shares from Arbitrum Sepolia. + +The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. Here, Ethereum Sepolia is the hub chain and Arbitrum Sepolia is the source chain. + +## Before you begin + +You need: + +- [Foundry](https://book.getfoundry.sh/getting-started/installation), +- [Node.js](https://nodejs.org/en/download) 22.10 or later, +- pnpm 10, and, +- A Foundry keystore for your wallet, which deploys and owns the vault and the share token contracts in this tutorial. Use the same keystore in every tutorial of this solution. + +> **CAUTION: Before you continue** +> +> 1. **Ethereum Sepolia ETH**: required for Ethereum Sepolia transactions. Get it from the [Chainlink +> faucet](https://faucets.chain.link). +> 2. **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions in the `Lock & Mint` tutorial. Get it from +> the same faucet. +> 3. **CCIP-BnM on Ethereum Sepolia**: the vault's asset. [Get CCIP-BnM on Ethereum +> Sepolia](https://docs.chain.link/ccip/getting-started/test-tokens) from this page. +> 4. **Etherscan API key**: required for `--verify`, which publishes the source code of the contracts you deploy on +> Etherscan. One [Etherscan API key](https://etherscan.io/myapikey) works on Ethereum Sepolia and Arbitrum Sepolia. + +> **TIP: Bringing your own vault?** +> +> Skip only what you already have: +> +> - Complete [Set up your development environment](#set-up-your-development-environment) and [Set your shell variables](#set-your-shell-variables) in every case. Deploy the adapter uses the same repository, keystore, and variables. +> - If you have an ERC-4626 vault on Ethereum Sepolia that meets the [requirements](/solutions/cross-chain-vault-adapter#requirements): +> - skip **Get CCIP-BnM on Ethereum Sepolia** and **Deploy the vault**, and run `export VAULT=`. +> - Set `ETHEREUM_SEPOLIA_CCIP_BNM` to your vault's asset, and scale the amounts on this page to its decimals. +> - If you already hold at least 0.1 of your vault's shares on Ethereum Sepolia: +> - skip **Deposit 1 CCIP-BnM**. +> - If your share token already has pools on Ethereum Sepolia and Arbitrum Sepolia that are configured for each other and registered in the TokenAdminRegistry on both chains: +> - skip [Set up the share token in CCIP](#set-up-the-share-token-in-ccip) and run `export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=`. +> In [Confirm the balances](#confirm-the-balances), skip the lockbox check, and expect your balances to move by 0.1 instead of matching the values shown. +> - If you set up your own share token now, it must implement `getCCIPAdmin()` or OpenZeppelin's `owner()`, so the Lock & Mint tutorial's Claim Admin Role step can identify its administrator. +> - If you have already transferred shares from Ethereum Sepolia to Arbitrum Sepolia, skip [Test the share token](#test-the-share-token). + +### 1. Set up your development environment + +1. Install [ccip-cli](https://docs.chain.link/ccip/tools/cli) and check its version: + +```bash filename="Terminal" +npm install -g @chainlink/ccip-cli +ccip-cli --version +``` + +This tutorial was tested with ccip-cli 1.14.0. + +2. Check that Foundry is installed: + +```bash filename="Terminal" +forge --version +``` + +3. Enable pnpm through Corepack. The repository pins `pnpm@10.11.1`: + +```bash filename="Terminal" +corepack enable +``` + +4. Clone the adapter repository, install its dependencies, and build it: + +[Cross-chain vault adapters](https://github.com/smartcontractkit/cross-chain-vault-adapters) + +The adapter, its factory, the example vault, and their deploy scripts. + +```bash filename="Terminal" +git clone https://github.com/smartcontractkit/cross-chain-vault-adapters.git +cd cross-chain-vault-adapters +pnpm install +forge build +``` + +5. Create an encrypted Foundry keystore. Skip this step if `cast wallet list` already shows the keystore you want to use: + +```bash filename="Terminal" +cast wallet import your_keystore_name --interactive +``` + +6. Create a `.env` file from the example: + +```bash filename="Terminal" +cp .env.example .env +``` + +7. In `.env`, fill in these variables: + +```bash filename=".env" +ETHEREUM_SEPOLIA_RPC_URL=your_ethereum_sepolia_rpc_url +ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc_url +ETHERSCAN_API_KEY=your_etherscan_api_key +KEYSTORE_NAME=your_keystore_name +``` + +Leave the other variables as they are. +Run `source .env` to load the environment variables into your shell. + +### 2. Set your shell variables + +The `cast` and `ccip-cli` commands in these tutorials read the following variables from your shell. Use one terminal for this tutorial and the next one, and run these commands again if you open a new terminal: + +> You can fetch the keystore names available inside your Foundry keystore by using the `cast wallet list` command. + +```bash filename="Terminal" +export MY_ADDRESS=$(cast wallet address --account $KEYSTORE_NAME) +export ETHEREUM_SEPOLIA_ROUTER=0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 +export ETHEREUM_SEPOLIA_CCIP_BNM=0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 +``` + +> `(cast wallet address --account $KEYSTORE_NAME)` sets the public address of the keystore specified by `KEYSTORE_NAME` into your terminal. You will need it. + +The RPC variable names match the Lock & Mint tutorial, so the same values work in both repositories. The router and CCIP-BnM addresses come from the [Ethereum Sepolia page of the CCIP Directory](/ccip/directory/testnet/chain/ethereum-testnet-sepolia). + +### 3. Get CCIP-BnM on Ethereum Sepolia + +You have two ways to get CCIP-BnM tokens on Ethereum Sepolia to follow through the tutorial: + +1. Mint CCIP-BnM on Ethereum Sepolia from [CCIP test tokens](/ccip/getting-started/test-tokens). +2. Use the following cast command to call `drip` on the CCIP-BnM token: + +```bash filename="Terminal" +cast send $ETHEREUM_SEPOLIA_CCIP_BNM "drip(address)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME +``` + +Check that your wallet holds at least 1 CCIP-BnM. CCIP-BnM has 18 decimals, so 1 CCIP-BnM is `1000000000000000000`: + +```bash filename="Terminal" +cast call $ETHEREUM_SEPOLIA_CCIP_BNM "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +1000000000000000000 [1e18] +``` + +## Deploy and fund the vault + +### 1. Deploy the vault + +`ExampleERC4626Vault` combines OpenZeppelin's audited `ERC4626` and `Ownable` contracts with a constructor and no other code. +This implementation is deliberately minimal to serve as a tool to follow through the tutorials. Please use your own, audited implementation in production. +`Ownable` gives the vault an `owner()` function. The Lock & Mint tutorial's Claim Admin Role step uses it to register you as the share token's CCIP administrator. + +> **CAUTION: Use your own vault in production** +> +> `ExampleERC4626Vault` exists for these tutorials. The OpenZeppelin contracts it inherits are audited, but the vault +> itself is not, and it has no yield strategy. + +[ExampleERC4626Vault.sol](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/src/ccip/dev/ExampleERC4626Vault.sol) + +View the vault contract on GitHub. + +[DeployExampleERC4626Vault.s.sol](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/script/ccip/DeployExampleERC4626Vault.s.sol) + +View the deployment script on GitHub. + +1. From the root of the `cross-chain-vault-adapters` repository, deploy the vault with CCIP-BnM as its asset, and verify it on Etherscan: + +```bash filename="Terminal" +VAULT_ASSET=$ETHEREUM_SEPOLIA_CCIP_BNM \ + VAULT_NAME="Vault CCIP-BnM" \ + VAULT_SYMBOL=vCCIP-BnM \ + pnpm ccip:deploy-example-vault \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify \ + --delay 20 \ + --retries 12 \ + --gas-estimate-multiplier 700 +``` + +Etherscan needs time to index a new contract. `--delay 20 --retries 12` makes Forge retry verification up to 12 times, 20 seconds apart. + +The script makes your wallet the vault owner. To make another address the owner, set `VAULT_OWNER`. Without `--broadcast`, the script only simulates the deployment. + +Your output should look something like this: + +```bash filename="Terminal" +== Return == +vault: contract ExampleERC4626Vault 0xE4Ef963183C29401Ff6F67CC13B500dB1354cfB6 + +== Logs == + ExampleERC4626Vault: 0xE4Ef963183C29401Ff6F67CC13B500dB1354cfB6 + Asset: 0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 + Name / symbol: Vault CCIP-BnM vCCIP-BnM + Decimals: 18 + Owner: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 +``` + +2. Export the vault address from the `ExampleERC4626Vault` line: + +```bash filename="Terminal" +export VAULT= +``` + +3. Check that your wallet owns the vault. The output must equal `$MY_ADDRESS`: + +```bash filename="Terminal" +cast call $VAULT "owner()(address)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +### 2. Deposit 1 CCIP-BnM + +A deposit moves CCIP-BnM into the vault and mints vCCIP-BnM to the receiver. Amounts in these commands use 18 decimals: `1000000000000000000` is 1 token. + +1. Approve the vault to transfer 1 CCIP-BnM from your wallet: + +```bash filename="Terminal" +cast send $ETHEREUM_SEPOLIA_CCIP_BNM "approve(address,uint256)" $VAULT 1000000000000000000 \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME +``` + +2. Deposit 1 CCIP-BnM with your wallet as the receiver: + +```bash filename="Terminal" +cast send $VAULT "deposit(uint256,address)" 1000000000000000000 $MY_ADDRESS \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME +``` + +Each `cast send` prints a receipt. Check that it shows `status 1 (success)`. + +3. Check your vCCIP-BnM balance and the value of 1 vCCIP-BnM in CCIP-BnM: + +```bash filename="Terminal" +cast call $VAULT "balanceOf(address)(uint256)" $MY_ADDRESS --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $VAULT "convertToAssets(uint256)(uint256)" 1000000000000000000 --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +At this point: + +You hold 1 vCCIP-BnM, and 1 vCCIP-BnM converts to 1 CCIP-BnM. \ +Anyone can change this rate by transferring CCIP-BnM directly to the vault. The amounts in the next tutorials assume nobody has. + +## Set up the share token in CCIP + +The [Lock & Mint tutorial](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) makes a token a cross-chain token with a Lock & Release pool on Ethereum Sepolia and a Burn & Mint pool on Arbitrum Sepolia. +On Ethereum Sepolia, only the vault can mint vCCIP-BnM, so the pool locks and releases existing shares. On Arbitrum Sepolia, you deploy a new vCCIP-BnM token that its pool mints and burns. + +### 1. Prepare the Lock & Mint repository + +The Lock & Mint tutorial uses its own repository, [docs-cct-foundry](https://github.com/smartcontractkit/docs-cct-foundry). Work in the same terminal, so `VAULT` and the other shell variables stay set. + +1. Move to the directory that contains `cross-chain-vault-adapters`: + +```bash filename="Terminal" +cd .. +``` + +2. Follow the Lock & Mint tutorial's [Set up your development environment](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#set-up-your-development-environment) step. + +3. From the `docs-cct-foundry` directory, after `source .env`, point the Lock & Mint scripts at the vault as the Ethereum Sepolia token: + +```bash filename="Terminal" +export ETHEREUM_SEPOLIA_TOKEN=$VAULT +``` + +> You need to follow the rest of the tutorial as it is. You will essentially set up your Vault's token on Ethereum Sepolia as the canonical deployment and enable it to have cross-chain functionality via CCIP under the Lock & Mint mechanism. + +### 2. Run the Lock & Mint tutorial for vCCIP-BnM + +Follow the Lock & Mint tutorial from [Deploy Tokens](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#deploy-tokens), with these changes: + +| Lock & Mint step | What to do for vCCIP-BnM | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Deploy Tokens](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#deploy-tokens) | Skip the Ethereum Sepolia deployment, because the vault is the token there. Deploy only the Arbitrum Sepolia token with the command below, then export its address. Keep `ETHEREUM_SEPOLIA_TOKEN=$VAULT`. | +| [Deploy Token Pools](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#deploy-token-pools) | Run every step: the ERC20LockBox, the Lock & Release pool, and the lockbox authorization on Ethereum Sepolia, and the Burn & Mint pool on Arbitrum Sepolia. | +| [Claim Admin Role](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#claim-admin-role) | Run it on both chains with `CCIP_ADMIN_ADDRESS=$MY_ADDRESS`. On Ethereum Sepolia, the script reads the admin from the vault's `owner()`. | +| [Accept Admin Role](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#accept-admin-role) | Run it on both chains. | +| [Configure Token Pools](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#configure-token-pools) | Run it on both chains with the tutorial's rate limits. | +| [Configure Faster Than Finality](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#optional-configure-fast-finality-for-the-token-pool) | Skip it. These tutorials use default finality. | +| [Activate the Token Pool](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#activate-the-token-pool) | Run it on both chains. | +| [Mint Tokens](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#mint-tokens) | Skip it. Only deposits mint vCCIP-BnM on Ethereum Sepolia, and you already hold 1 vCCIP-BnM. | +| [Transfer Tokens Across Networks](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#transfer-tokens-across-networks) | Skip it. You transfer vCCIP-BnM in [Test the share token](#test-the-share-token). | +| [Hand Off Token Administration](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#optional-hand-off-token-administration-after-enablement) | Skip it. | + +Deploy the Arbitrum Sepolia token with the vault's name, symbol, and decimals, and no pre-mint. Because the decimals match, 0.1 vCCIP-BnM on Ethereum Sepolia arrives as 0.1 vCCIP-BnM on Arbitrum Sepolia: + +```bash filename="Terminal" +TOKEN_NAME="Vault CCIP-BnM" \ + TOKEN_SYMBOL=vCCIP-BnM \ + TOKEN_DECIMALS=18 \ + TOKEN_PRE_MINT=0 \ + forge script \ + script/deploy/DeployToken.s.sol \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME \ + --broadcast \ + --verify +``` + +Under Token Parameters, the output must show `Vault CCIP-BnM`, `vCCIP-BnM`, `18`, and `Pre-mint: 0`. Export the token's address: + +```bash filename="Terminal" +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN= +``` + +When Activate the Token Pool succeeds on both chains, return to this page. + +### 3. Check the registration + +1. Once your token pool is activated on both chains, go back to the adapter repository: + +```bash filename="Terminal" +cd ../cross-chain-vault-adapters +``` + +2. Read the pool that the TokenAdminRegistry on each chain holds for the share token: + +```bash filename="Terminal" +cast call 0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82 "getPool(address)(address)" $VAULT \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call 0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f "getPool(address)(address)" $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +1. The first address must equal `$ETHEREUM_SEPOLIA_TOKEN_POOL`, and, +2. The second must equal `$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL`. + +A zero address means Activate the Token Pool did not run on that chain. + +## Test the share token + +### 1. Transfer vCCIP-BnM to Arbitrum Sepolia + +Send 0.1 vCCIP-BnM from your wallet on Ethereum Sepolia to the same address on Arbitrum Sepolia. The Lock & Release pool locks the shares in the lockbox, and the Burn & Mint pool mints 0.1 vCCIP-BnM on Arbitrum Sepolia. +`ccip-cli` approves the router for the exact amount. Without `--fee-token`, it pays the CCIP fee in Ethereum Sepolia ETH: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia \ + --router $ETHEREUM_SEPOLIA_ROUTER \ + --dest ethereum-testnet-sepolia-arbitrum-1 \ + --transfer-tokens $VAULT=0.1 \ + --receiver $MY_ADDRESS \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +`--transfer-tokens` takes the amount in whole tokens, so `0.1` is 0.1 vCCIP-BnM. + +Your output should look something like this: + +```bash filename="Terminal" +✔ Enter password for Foundry keystore 'PRIVATE_KEY' +Estimated gasLimit for sender = 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 : 0 +Fee: 257831635941687n = 0.000257831635941687 ETH +🚀 Sending message to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x621f4cd397751b87bd8edb6bb90afb4e9bcfd274ccc228cb54e4d66d8bde787d , messageId => 0x81ef360a4235f6333e7725e38c171e018f14f73c19559aefb19ff70ab6ca3c9e +Lane: +┌────────────────┬──────────────────────────────────────────────┬───────────────────────────────────────┐ +│ (index) │ source │ dest │ +├────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┤ +│ name │ 'ethereum-testnet-sepolia' │ 'ethereum-testnet-sepolia-arbitrum-1' │ +│ chainId │ 11155111 │ 421614 │ +│ chainSelector │ 16015286601757825753n │ 3478487238524512106n │ +│ onRamp/version │ '0x8dcf17f298c881A547D91ca4aA3C2AD7568C6777' │ '2.0.0' │ +└────────────────┴──────────────────────────────────────────────┴───────────────────────────────────────┘ +Request (source): +┌────────────────────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├────────────────────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ messageId │ '0x81ef360a4235f6333e7725e38c171e018f14f73c19559aefb19ff70ab6ca3c9e' │ +│ origin │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ sender │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ receiver │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +``` + +Export the message ID from the output: + +```bash filename="Terminal" +export MESSAGE_ID= +``` + +### 2. Track the transfer + +Open the CCIP Explorer link from the output, or follow the message from the terminal with [`ccip-cli show`](https://docs.chain.link/ccip/tools/cli/show). `--wait` keeps the command running until the message executes on Arbitrum Sepolia: + +```bash filename="Terminal" +ccip-cli show $MESSAGE_ID \ + --wait \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +CCIP delivers the message after the source transaction reaches finality on Ethereum Sepolia, which typically takes about 20 minutes. + +### 3. Confirm the balances + +1. Check your vCCIP-BnM balance on Ethereum Sepolia: + +```bash filename="Terminal" +cast call $VAULT "balanceOf(address)(uint256)" $MY_ADDRESS --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +```bash filename="Terminal" +900000000000000000 [9e17] +``` + +If you set up the share token with the Lock & Mint tutorial in this terminal, `LOCK_BOX` holds the lockbox address from its Deploy ERC20LockBox step. Check that the lockbox holds the 0.1 vCCIP-BnM you sent: + +```bash filename="Terminal" +cast call $VAULT "balanceOf(address)(uint256)" $LOCK_BOX --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +```bash filename="Terminal" +100000000000000000 [1e17] +``` + +2. Check your vCCIP-BnM balance on Arbitrum Sepolia: + +```bash filename="Terminal" +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +```bash filename="Terminal" +100000000000000000 [1e17] +``` + +Your wallet holds 0.9 vCCIP-BnM on Ethereum Sepolia and 0.1 vCCIP-BnM on Arbitrum Sepolia. The lockbox holds the 0.1 vCCIP-BnM that backs the Arbitrum Sepolia supply. The vault still holds 1 CCIP-BnM against 1 vCCIP-BnM of total supply, so shares still convert 1:1. + +## Where to go next + +- [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter): deploy a Cross-Chain Vault Adapter for this vault on Ethereum Sepolia, with Arbitrum Sepolia as a source chain. Stay in this terminal and in the `cross-chain-vault-adapters` directory. +- [How it works](/solutions/cross-chain-vault-adapter/overview/how-it-works): how the adapter uses the vault and its share token for deposits and redemptions. + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. + +--- + +# Deploy the adapter +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter + +The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. Here, Ethereum Sepolia is the hub chain and Arbitrum Sepolia is the source chain. + +In this tutorial, we will: + +1. Deploy an adapter through the factory at `0x12A0f45738F3DDA931C4D3ECd1b267355e587921` on Ethereum Sepolia, with your vault allowlisted and Arbitrum Sepolia accepted as a source chain. +2. Set an adapter fee of 0.001 CCIP-BnM on output that returns to Arbitrum Sepolia. +3. Set the return format for the CCIP 2.0 lane to Arbitrum Sepolia, and fund the adapter with 0.01 Ethereum Sepolia ETH for return-leg CCIP fees. +4. Check the configuration and preview a deposit and a redemption. + +> This tutorial continues from [Deploy the vault and set up its share token](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault), which deploys Vault CCIP-BnM (vCCIP-BnM), an ERC-4626 vault on Ethereum Sepolia whose asset is CCIP-BnM. You can use your own vault implementation instead. + +## Before you begin + +You need: + +- The `cross-chain-vault-adapters` repository, cloned and built, with all the required variables being set in the `.env` file. [Set up your development environment](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault#set-up-your-development-environment) lists the tools and the steps. +- A Foundry keystore for your wallet, which deploys the adapter and receives its three roles in this tutorial. Use the same keystore in every tutorial of this solution. +- An ERC-4626 vault on Ethereum Sepolia. [Deploy the vault and set up its share token](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault) deploys one. + +> **CAUTION: Before you continue** +> +> - **Ethereum Sepolia ETH**: required for Ethereum Sepolia transactions. Get it from the [Chainlink faucet](https://faucets.chain.link). This tutorial also sends 0.01 Ethereum Sepolia ETH to the adapter for return-leg CCIP fees. +> - **Etherscan API key**: required for `--verify`, which publishes the source code of the contracts you deploy on Etherscan. Get one from [Etherscan](https://etherscan.io/myapikey). + +> **TIP: Bringing your own vault or adapter?** +> +> Skip only what you already have: +> +> - If you completed [Deploy the vault and set up its share token](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault) in this terminal, your shell variables are set. Continue with [Configure the deployment](#configure-the-deployment). +> - If you bring your own vault, it must meet the [requirements](/solutions/cross-chain-vault-adapter#requirements). +> Set `VAULT` to its address, and set `ETHEREUM_SEPOLIA_CCIP_BNM` to your vault's asset, as returned by `asset()`. +> Use that asset address wherever `.env` below shows the CCIP-BnM address. +> Adapter fees are in the asset's smallest units, so scale `1000000000000000` (0.001 of an 18-decimal token) to your asset's decimals. +> - If your vault's share token is not a cross-chain token yet, you can still deploy the adapter. +> But until and unless you enable the share token on the lane, users can deposit only with delivery on Ethereum Sepolia: the adapter cannot send shares to Arbitrum Sepolia, and users there hold no shares to redeem. +> - If you already deployed an adapter for your vault, run `export ADAPTER=` and continue with [Set the return format and fund the adapter](#fund-the-adapter). + +### 1. Set your shell variables + +Run every command in this tutorial from the root of the `cross-chain-vault-adapters` repository. Forge loads `.env` from there for every `forge script` run. + +The `cast` commands in this tutorial read the following variables from your shell. If you opened a new terminal since the previous tutorial, change to the repository root and set them again: + +```bash filename="Terminal" +export KEYSTORE_NAME=your_keystore_name +export ETHEREUM_SEPOLIA_RPC_URL=your_ethereum_sepolia_rpc_url +export ETHEREUM_SEPOLIA_CCIP_BNM=0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 +export VAULT= +``` + +Set `MY_ADDRESS` to your keystore's address. `cast` asks for the keystore password: + +```bash filename="Terminal" +export MY_ADDRESS=$(cast wallet address --account $KEYSTORE_NAME) +echo $MY_ADDRESS +``` + +Confirm that `VAULT` is your vault and that its asset is CCIP-BnM: + +```bash filename="Terminal" +cast call $VAULT "asset()(address)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $VAULT "symbol()(string)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +The output shows the CCIP-BnM address and the share symbol: + +```bash filename="Terminal" +0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 +"vCCIP-BnM" +``` + +## Deploy and configure the adapter + +### 1. Configure the deployment + +The `pnpm ccip:deploy` script reads its configuration from `.env`, calls `deploy` on the factory, and writes a deployment record. + +[DeployAndActivateCrossChainERC4626Adapter.s.sol](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/script/ccip/DeployAndActivateCrossChainERC4626Adapter.s.sol) + +View the deployment script on GitHub. + +Print your wallet address and the vault address, which you paste into `.env`: + +```bash filename="Terminal" +echo $MY_ADDRESS $VAULT +``` + +Open `.env` and set the following variables: + +```bash filename=".env" +# CCIP Router on Ethereum Sepolia +ROUTER=0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + +# Role holders. This tutorial gives all three roles to your wallet. +DEFAULT_ADMIN= +FEE_SETTER= +FEE_COLLECTOR= + +# Accept deposits and redemptions +DEPOSITS_ENABLED=true +REDEEMS_ENABLED=true + +# The vault the adapter may call. TARGET_ENABLED defaults to true. +VAULT_TARGET= + +# Accept messages from Arbitrum Sepolia (chain type 1 = EVM) +CHAIN_SELECTORS=3478487238524512106 +CHAIN_TYPES=1 + +# Adapter fee of 0.001 CCIP-BnM on output returned to Arbitrum Sepolia. +# Row 1: deposits that return shares. Row 2: redemptions that return CCIP-BnM. +FEE_DESTINATION_CHAIN_SELECTORS=3478487238524512106,3478487238524512106 +FEE_BRIDGED_TOKENS=,0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 +FEE_VALUES=1000000000000000,1000000000000000 + +# Deploy through the existing factory on Ethereum Sepolia +FACTORY=0x12A0f45738F3DDA931C4D3ECd1b267355e587921 +``` + +What each setting does: + +- `ROUTER` is represents the CCIP Router address on Ethereum Sepolia, which you can verify from the [CCIP Directory](/ccip/directory/testnet/chain/ethereum-testnet-sepolia). The adapter stores it as the immutable `ROUTER`, so a wrong router means deploying a new adapter. +- `DEFAULT_ADMIN`, `FEE_SETTER`, and `FEE_COLLECTOR` receive the adapter's three roles. The factory then renounces its own roles. + - See [Role handoff](/solutions/cross-chain-vault-adapter/reference/factory-contract#role-handoff). +- `VAULT_TARGET` connects the adapter to your vault: the factory adds the vault to the adapter's allowlist, `enabledTargets`. Each user payload names the vault it targets, and the adapter rejects vaults that are not allowlisted. +- `CHAIN_SELECTORS` and `CHAIN_TYPES` accept messages from Arbitrum Sepolia, whose selector number on the CCIP protocol is `3478487238524512106`. The adapter uses the same entry to encode return legs to that chain. +- `FEE_DESTINATION_CHAIN_SELECTORS`, `FEE_BRIDGED_TOKENS`, and `FEE_VALUES` are parallel lists, one fee row per position. +- The `FACTORY` variable being set makes the script deploy through an existing factory instead of deploying a new one. + +> **CAUTION: Use a multisig for the admin role in production** +> +> `DEFAULT_ADMIN_ROLE` controls source chains, the vault allowlist, the processing switches, and the adapter's native +> gas token balance. Role changes take effect in one step, and if the last admin renounces the role, nobody can change +> the configuration again. + +### 2. Simulate the deployment + +We recommend simulating the deployment against Ethereum Sepolia, before actually executing on-chain. Without the `--broadcast` flag, Forge sends no transaction, and `--sender` runs the simulation from your wallet address: + +```bash filename="Terminal" +pnpm ccip:deploy --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --sender $MY_ADDRESS +``` + +Before it simulates, the script performs a few checks: + +1. The `ROUTER` and `FACTORY` contracts have code, +2. `FACTORY` should be an instance of the `CrossChainERC4626AdapterFactory 1.0.0`, +3. `VAULT_TARGET` answers `asset()`, +4. That the chain type should be a valid number. + +A successful simulation prints the configuration it would deploy (illustrative): + +```bash filename="Terminal" +== Logs == + Dry run: nothing was deployed. Re-run with --broadcast to deploy and write the deployment record. + CrossChainERC4626AdapterFactory: 0x12A0f45738F3DDA931C4D3ECd1b267355e587921 + CrossChainERC4626Adapter: 0x... + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + Default admin: 0x... + Fee setter / collector: 0x... 0x... + Vault target (enabled): 0x... true + Deposits / redeems enabled: true true + Chain configs / fee rows: 1 2 +... +SIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more. +``` + +Check that `Chain configs / fee rows` shows `1 2`. The adapter address in the simulation matches the real one only if nobody else deploys through the factory before you broadcast, so read the real address from the deployment record after the broadcast. + +### 3. Broadcast the deployment + +Finally, we can deploy the adapter. Forge asks for your keystore password, sends one transaction that calls `deploy` on the factory, and verifies the new adapter on Etherscan: + +```bash filename="Terminal" +pnpm ccip:deploy --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --account $KEYSTORE_NAME --broadcast --verify --delay 20 --retries 12 +``` + +> **NOTE: About verification** +> +> - `--verify` reads `ETHERSCAN_API_KEY` from `.env`. + +Your output should look something like this: + +```bash filename="Terminal" +== Logs == + CrossChainERC4626AdapterFactory: 0x12A0f45738F3DDA931C4D3ECd1b267355e587921 + CrossChainERC4626Adapter: 0x... + ... + Deployment record: deployments/ccip/11155111.json + + Next steps: + 1. Fund the adapter with native gas for return legs (ADAPTER= FUND_NATIVE_WEI=...). + 2. For CCIP v2 lanes, set return-lane formats and finality with pnpm ccip:configure. + 3. Verify the result with pnpm ccip:check. +... +ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. +... +All (1) contracts were verified! +``` + +With `--broadcast`, the script writes a deployment record to `deployments/ccip/11155111.json`, where `11155111` is the Ethereum Sepolia chain ID. +You can print it using this command: + +```bash filename="Terminal" +cat deployments/ccip/11155111.json +``` + +The record lists the factory, the adapter, the router, the role holders, the vault, and the processing switches (illustrative): + +```json filename="deployments/ccip/11155111.json" +{ + "adapter": "0x...", + "chainId": 11155111, + "defaultAdmin": "0x...", + "depositsEnabled": true, + "factory": "0x12A0f45738F3DDA931C4D3ECd1b267355e587921", + "feeCollector": "0x...", + "feeSetter": "0x...", + "redeemsEnabled": true, + "router": "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59", + "vaultTarget": "0x..." +} +``` + +Export the adapter address for the next steps. `pnpm ccip:configure` and `pnpm ccip:check` also read `ADAPTER` from your shell: + +```bash filename="Terminal" +export ADAPTER= +echo $ADAPTER +``` + +Open `https://sepolia.etherscan.io/address/` and confirm that the **Contract** tab shows verified source code for `CrossChainERC4626Adapter`. + +### 4. Set the return format and fund the adapter + +The adapter pays the CCIP fee for each return leg to Arbitrum Sepolia from its own Ethereum Sepolia ETH balance. With no balance, every request that returns output fails with `InsufficientNativeBalance` and becomes a failed message. + +[ConfigureCrossChainERC4626Adapter.s.sol](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/script/ccip/ConfigureCrossChainERC4626Adapter.s.sol) + +View the configuration script on GitHub. + +The lane from Ethereum Sepolia to Arbitrum Sepolia is a CCIP 2.0 lane, and we need to tell the adapter that, using the `RETURN_LANE_FORMATS` param. + +We can do that in one run, alongside funding the adapter with Ethereum Sepolia ETH. + +```bash filename="Terminal" +RETURN_LANE_SELECTORS=3478487238524512106 \ +RETURN_LANE_FORMATS=2 \ +FUND_NATIVE_WEI=10000000000000000 \ + pnpm ccip:configure --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --account $KEYSTORE_NAME --broadcast +``` + +- Before it sends anything, the script checks that `ADAPTER` reports `CrossChainERC4626Adapter 1.0.0`, so a mistyped address does not receive the Ethereum Sepolia ETH. +- Setting the return format needs `DEFAULT_ADMIN_ROLE`. Funding needs no role: anyone can send Ethereum Sepolia ETH to the adapter. + +The script logs each setting it applied (illustrative): + +```bash filename="Terminal" +== Logs == + Return lane format set: 3478487238524512106 2 + Funded adapter (wei): 10000000000000000 +``` + +For what the return format and the other CCIP 2.0 settings control, see [CCIP 2.0 lanes](/solutions/cross-chain-vault-adapter/overview/how-it-works#ccip-20-lanes). + +## Verify the adapter + +### 1. Run the deployment check + +`pnpm ccip:check` reads the adapter and compares it with the values in `.env`: + +```bash filename="Terminal" +pnpm ccip:check --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +The report shows the switches, the adapter's Ethereum Sepolia ETH balance, the role holders, the vault, and the settings for Arbitrum Sepolia (illustrative): + +```bash filename="Terminal" +== Logs == + Adapter: 0x... + typeAndVersion: CrossChainERC4626Adapter 1.0.0 + Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 + Deposits enabled: true + Redeems enabled: true + Native balance: 10000000000000000 + DEFAULT_ADMIN holds role: 0x... true + FEE_SETTER holds role: 0x... true + FEE_COLLECTOR holds role: 0x... true + Vault target: 0x... true + Vault asset: 0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 + + Chain selector: 3478487238524512106 + chain type: EVM + return format: GenericExtraArgsV3 basic (CCIP v2 lane) + inbound finality: 0x00000000 + fee (share out): 1000000000000000 + fee (asset out): 1000000000000000 + + Warnings: 0 +``` + +Make sure that the report ends with `Warnings: 0`. The script prints each problem on a `WARNING:` line but still exits successfully, so read the count instead of relying on the exit code. An inbound finality of `0x00000000` is the default: the adapter accepts only messages that wait for finality. + +### 2. Read the configuration with cast + +Read the source chain, the vault allowlist, and the two fee rows directly from the adapter's public mappings: + +```bash filename="Terminal" +cast call $ADAPTER "chains(uint64)(uint8)" 3478487238524512106 --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $ADAPTER "enabledTargets(address)(bool)" $VAULT --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $ADAPTER "assetFees(uint64,address)(uint256)" 3478487238524512106 $VAULT --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $ADAPTER "assetFees(uint64,address)(uint256)" 3478487238524512106 $ETHEREUM_SEPOLIA_CCIP_BNM --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +The output has one line per call: + +```bash filename="Terminal" +1 +true +1000000000000000 [1e15] +1000000000000000 [1e15] +``` + +| Call | Value | Meaning | +| ------------------------------------- | ------------------ | ---------------------------------------------------------------------------- | +| `chains` | `1` | Arbitrum Sepolia is an accepted source chain of type EVM (`0` NONE, `2` SVM) | +| `enabledTargets` | `true` | The adapter may call your vault | +| `assetFees` with the vault address | `1000000000000000` | 0.001 CCIP-BnM fee on deposits whose shares return to Arbitrum Sepolia | +| `assetFees` with the CCIP-BnM address | `1000000000000000` | 0.001 CCIP-BnM fee on redemptions whose CCIP-BnM returns to Arbitrum Sepolia | + +### 3. Preview the adapter fee + +[`preview`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#preview) simulates a request with the same routing and adapter fee as a real message from Arbitrum Sepolia. Preview a deposit of 0.1 CCIP-BnM (`100000000000000000` wei) whose shares return to Arbitrum Sepolia: + +```bash filename="Terminal" +cast call $ADAPTER "preview(address,address,uint256,bool,uint64)(uint256)" \ + $ETHEREUM_SEPOLIA_CCIP_BNM $VAULT 100000000000000000 true 3478487238524512106 \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +The adapter deducts the 0.001 CCIP-BnM fee before it deposits, so 0.099 CCIP-BnM enters the vault, and the 1:1 vault mints 0.099 vCCIP-BnM: + +```bash filename="Terminal" +99000000000000000 [9.9e16] +``` + +Preview the same deposit with delivery on Ethereum Sepolia by passing `false`. The adapter charges no fee when output stays on Ethereum Sepolia: + +```bash filename="Terminal" +cast call $ADAPTER "preview(address,address,uint256,bool,uint64)(uint256)" \ + $ETHEREUM_SEPOLIA_CCIP_BNM $VAULT 100000000000000000 false 3478487238524512106 \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +The output is the full 0.1 vCCIP-BnM: + +```bash filename="Terminal" +100000000000000000 [1e17] +``` + +Preview a redemption of 0.1 vCCIP-BnM whose CCIP-BnM returns to Arbitrum Sepolia. For a redemption, the token is the vault address: + +```bash filename="Terminal" +cast call $ADAPTER "preview(address,address,uint256,bool,uint64)(uint256)" \ + $VAULT $VAULT 100000000000000000 true 3478487238524512106 \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +The vault redeems 0.1 vCCIP-BnM for 0.1 CCIP-BnM, and the adapter deducts the 0.001 CCIP-BnM fee: + +```bash filename="Terminal" +99000000000000000 [9.9e16] +``` + +A user sets `minimumOut` in the payload from this value minus a tolerance for exchange-rate movement. See [Slippage protection](/solutions/cross-chain-vault-adapter/overview/how-it-works#slippage-protection). + +> **NOTE: Where to go next** +> +> 1. [Deposit from a source chain](/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain): as a user on Arbitrum Sepolia, deposit 0.1 CCIP-BnM into the vault through this adapter and receive the shares on Arbitrum Sepolia. +> 2. Stay in this terminal and in the `cross-chain-vault-adapters` directory. + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. + +--- + +# Deposit from a source chain +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain + +The steps we undertook in the previous two tutorials make it possible for us to work with the Cross-Chain Vault Adapter and perform cross-chain deposits seamlessly. + +On this page, your wallet acts as a user on Arbitrum Sepolia: \ +It sends one CCIP message and deploys nothing. [Deposit flow](/solutions/cross-chain-vault-adapter/overview/how-it-works#deposit-flow) shows each step the adapter takes. + +> This tutorial continues from [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter), which deploys an adapter for Vault CCIP-BnM (vCCIP-BnM) that accepts requests from Arbitrum Sepolia and charges a 0.001 CCIP-BnM adapter fee on output that returns there. + +With the adapter deployed and configured, you will be able to deposit into an ERC-4626 vault on Ethereum Sepolia from Arbitrum Sepolia with one CCIP message, and receive the vault's shares on Arbitrum Sepolia. + +The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. Here, Ethereum Sepolia is the hub chain and Arbitrum Sepolia is the source chain. + +The tutorial walks you through four main steps: + +1. Get CCIP-BnM on Arbitrum Sepolia. +2. Preview a deposit of 0.1 CCIP-BnM and set a minimum output 1% below the preview. +3. Build the payload and send it with 0.1 CCIP-BnM from Arbitrum Sepolia to the adapter. +4. Confirm that the adapter deposited into the vault, find the return leg, and check that 0.099 vCCIP-BnM reached your wallet on Arbitrum Sepolia. + +## Before you begin + +You need: + +- ccip-cli, Foundry, Node.js, and the `cross-chain-vault-adapters` repository, set up in [Set up your development environment](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault#set-up-your-development-environment). +- A Foundry keystore for your wallet, which acts as a user on Arbitrum Sepolia in this tutorial. Use the same keystore in every tutorial of this solution. +- An ERC-4626 vault on Ethereum Sepolia whose share token is a CCIP cross-chain token between Ethereum Sepolia and Arbitrum Sepolia. [Deploy the vault and set up its share token](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault) sets one up. +- An adapter on Ethereum Sepolia for that vault that accepts requests from Arbitrum Sepolia and has deposits enabled. [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter) deploys one. + +> **CAUTION: Before you continue** +> +> - **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions and for the CCIP fee of your request, about 0.0023 Arbitrum Sepolia ETH at the time of writing. Get it from the [Chainlink faucet](https://faucets.chain.link). +> - **CCIP-BnM on Arbitrum Sepolia**: at least 0.1, the amount you deposit. [Get CCIP-BnM on Arbitrum Sepolia](#get-ccip-bnm-on-arbitrum-sepolia) shows how. +> - **Ethereum Sepolia ETH in the adapter**: at least 0.001 Ethereum Sepolia ETH, which pays the return leg's CCIP fee. [Check the adapter](#check-the-adapter) reads the balance. Topping it up takes Ethereum Sepolia ETH from your wallet. + +> **TIP: Bringing your own vault or adapter?** +> +> Skip only what you already have: +> +> - If you ran both previous tutorials in this terminal, run only the first command block in [Set your shell variables](#set-your-shell-variables), then continue with [Check the adapter](#check-the-adapter). +> - If you bring your own vault, adapter, or share token, set the variables in [Set your shell variables](#set-your-shell-variables) to your addresses. Set `ETHEREUM_SEPOLIA_CCIP_BNM` to your vault's asset on Ethereum Sepolia and `ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_CCIP_BNM` to the same token on Arbitrum Sepolia. In the `preview` call and in the payload builder's `AMOUNT`, scale `100000000000000000` (0.1 of an 18-decimal asset) to your asset's decimals. `ccip-cli` takes whole tokens, so `=0.1` stays the same. +> - Your adapter must accept Arbitrum Sepolia as a source chain, allowlist your vault, allow deposits, and hold Ethereum Sepolia ETH. Your share token must be a cross-chain token between Ethereum Sepolia and Arbitrum Sepolia. +> - If you already hold at least 0.1 CCIP-BnM on Arbitrum Sepolia, skip `drip` in [Get CCIP-BnM on Arbitrum Sepolia](#get-ccip-bnm-on-arbitrum-sepolia). If your starting balances differ from the values shown, compare the changes instead. If you did not set up the share token with the Lock & Mint tutorial, skip the lockbox check in [Confirm the balances](#confirm-the-balances). + +### 1. Set your shell variables + +Run the commands in this tutorial from the root of the `cross-chain-vault-adapters` repository, in the terminal you used for the previous tutorials. Set the Arbitrum Sepolia router and CCIP-BnM addresses, which this tutorial adds. + +> Refer to the [Arbitrum Sepolia page of the CCIP Directory](/ccip/directory/testnet/chain/ethereum-testnet-sepolia-arbitrum-1) for the latest addresses: + +```bash filename="Terminal" +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_ROUTER=0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_CCIP_BNM=0xA8C0c11bf64AF62CDCA6f93D3769B88BdD7cb93D +``` + +If you have a fresh terminal that may not have the context of all the values required, these are the values you will need to set up: + +```bash filename="Terminal" +export KEYSTORE_NAME=your_keystore_name +export ETHEREUM_SEPOLIA_RPC_URL=your_ethereum_sepolia_rpc_url +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc_url +export ETHEREUM_SEPOLIA_CCIP_BNM=0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 +export VAULT= +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN= +export LOCK_BOX= +export ADAPTER= +``` + +- `ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN` is the vCCIP-BnM token you deployed on Arbitrum Sepolia. +- `LOCK_BOX` is the lockbox from the Lock & Mint tutorial's Deploy `ERC20LockBox` step. +- `ADAPTER` is the adapter address you exported in [Broadcast the deployment](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter#broadcast-the-deployment), also stored as `adapter` in `deployments/ccip/11155111.json`. If you deployed your adapter another way, use that address instead. + +In a new terminal, also set `MY_ADDRESS` to your keystore's address. `cast` asks for the keystore password: + +```bash filename="Terminal" +export MY_ADDRESS=$(cast wallet address --account $KEYSTORE_NAME) +echo $MY_ADDRESS +``` + +### 2. Check the adapter + +1. Check that `ADAPTER` points to an adapter. Your request sends 0.1 CCIP-BnM to this address: + +```bash filename="Terminal" +cast call $ADAPTER "typeAndVersion()(string)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +```bash filename="Terminal" +"CrossChainERC4626Adapter 1.0.0" +``` + +2. Check the adapter's Ethereum Sepolia ETH balance. The adapter pays the return leg's CCIP fee from it: + +```bash filename="Terminal" +cast balance $ADAPTER --ether --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +[Set the return format and fund the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter#fund-the-adapter) sent the adapter 0.01 Ethereum Sepolia ETH: + +```bash filename="Terminal" +0.010000000000000000 +``` + +Make sure to have enough Ethereum Sepolia ETH in the adapter. With too little, the adapter cannot send the return leg, and your request becomes a failed message. If the balance is below 0.001 Ethereum Sepolia ETH, send the adapter more Ethereum Sepolia ETH. Any account can fund it: + +```bash filename="Terminal" +cast send $ADAPTER --value 0.01ether --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --account $KEYSTORE_NAME +``` + +### 3. Get CCIP-BnM on Arbitrum Sepolia + +Mint CCIP-BnM on Arbitrum Sepolia from [CCIP test tokens](/ccip/getting-started/test-tokens#evm-chains). Each mint calls `drip` on the token and sends 1 CCIP-BnM to your wallet. You can also call `drip` from the terminal: + +```bash filename="Terminal" +cast send $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_CCIP_BNM "drip(address)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \ + --account $KEYSTORE_NAME +``` + +Check your vCCIP-BnM and CCIP-BnM balances on Arbitrum Sepolia: + +```bash filename="Terminal" +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_CCIP_BNM "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +```bash filename="Terminal" +100000000000000000 [1e17] +1000000000000000000 [1e18] +``` + +You hold 0.1 vCCIP-BnM from the test transfer in Deploy the vault and set up its share token, and 1 CCIP-BnM from `drip`. The balances later on this page start from these values. + +## Build the request + +### 1. Preview the deposit + +`minimumOut` in the payload is the smallest amount of vCCIP-BnM, after the adapter fee, that you accept for your `0.1 CCIP-BnM`. \ +You're essentially defining the maximum slippage you're willing to tolerate. +The adapter checks it on Ethereum Sepolia when your request executes, about 20 minutes after you send it. By then, the vault's exchange rate or the adapter fee, which the adapter reads at execution, can have changed. If the output is below `minimumOut`, the adapter deposits nothing and sends nothing back: it keeps your 0.1 CCIP-BnM and records a failed message, which you then [refund or recover](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery). + +1. Preview the deposit with the adapter's [`preview`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#preview) function, and store the result: + +```bash filename="Terminal" +export DEPOSIT_PREVIEW=$(cast call $ADAPTER "preview(address,address,uint256,bool,uint64)(uint256)" \ + $ETHEREUM_SEPOLIA_CCIP_BNM $VAULT 100000000000000000 true 3478487238524512106 \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL | awk '{print $1}') +echo $DEPOSIT_PREVIEW +``` + +- The first argument is the token as the adapter receives it. CCIP delivers your CCIP-BnM from Arbitrum Sepolia as CCIP-BnM on Ethereum Sepolia, the vault's asset, so you pass `$ETHEREUM_SEPOLIA_CCIP_BNM`, not the Arbitrum Sepolia address. +- `100000000000000000` is 0.1 CCIP-BnM, `true` returns the output to the source chain, and `3478487238524512106` is the Arbitrum Sepolia chain selector. +- `awk` keeps the number and drops the `[9.9e16]` that `cast` appends. + +```bash filename="Terminal" +99000000000000000 +``` + +The adapter deducts its 0.001 CCIP-BnM fee and deposits 0.099 CCIP-BnM. The 1:1 vault mints 0.099 vCCIP-BnM. Do not continue if the output is `0` or `cast` prints an error. +A smaller tolerance rejects more requests when the rate moves, and a larger one accepts a worse rate. +The vault in these tutorials has no yield strategy, so its exchange rate changes only if someone transfers CCIP-BnM to it directly. [Build the payload](#build-the-payload) applies your tolerance to this preview to set `minimumOut`. + +### 2. Build the payload + +The payload is the `data` of your CCIP message: four 32-byte fields that tell the adapter what to do. For this deposit, they hold: + +| Field | Value | Meaning | +| ------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `target` | `$VAULT` | The vault to deposit into. It must be on the adapter's allowlist. | +| `beneficiary` | `$MY_ADDRESS` | The account that receives the vCCIP-BnM on Arbitrum Sepolia. | +| `minimumOut` | the preview minus 1% | The smallest acceptable output, in vCCIP-BnM. The builder subtracts the `TOLERANCE_BPS` tolerance from the preview. | +| `deliveryAndRefund` | your address and `true` | Bit 0 set to `1` returns the vCCIP-BnM to Arbitrum Sepolia. Bits 1 to 160 hold `$MY_ADDRESS` as the local refund address, which can take back the CCIP-BnM on Ethereum Sepolia if the request fails. The builder packs both. | + +The payload does not name the action. The token you send decides it: CCIP-BnM, the vault's asset, makes this request a deposit. + +1. Build the payload with the repository's `pnpm ccip:build-payload` script. It calls your adapter's `preview` with your vault, token, amount, source chain and return flag, subtracts the tolerance from the preview to set `minimumOut`, packs the return flag and your local refund address into `deliveryAndRefund`, and ABI-encodes the four fields: + +```bash filename="Terminal" +export DEPOSIT_PAYLOAD=$(REQUEST_TOKEN=$ETHEREUM_SEPOLIA_CCIP_BNM \ + REQUEST_ADAPTER=$ADAPTER \ + REQUEST_TARGET=$VAULT \ + AMOUNT=100000000000000000 \ + SOURCE_CHAIN_SELECTOR=3478487238524512106 \ + TOLERANCE_BPS=100 \ + MY_ADDRESS=$MY_ADDRESS \ + pnpm ccip:build-payload --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + | tee /dev/stderr \ + | awk '/PAYLOAD:/ {print $2}') +``` + +- `REQUEST_TOKEN` is the token as the adapter receives it. CCIP delivers your CCIP-BnM from Arbitrum Sepolia as CCIP-BnM on Ethereum Sepolia, the vault's asset, so you pass `$ETHEREUM_SEPOLIA_CCIP_BNM`, not the Arbitrum Sepolia address. +- `TOLERANCE_BPS=100` is a 1% tolerance: the script sets `minimumOut` to `preview * (10_000 - 100) / 10_000`. Use `50` for a 0.5% tolerance. +- `tee /dev/stderr` shows the script's output in your terminal, and `awk` stores the value of its `PAYLOAD:` line in `DEPOSIT_PAYLOAD`. + Your output should look something like this: + +```bash filename="Terminal" +== Logs == + Adapter: 0x3f73Cc1547F2606A20535Bd6620622107059a463 + Token: 0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 + Vault target: 0xE4Ef963183C29401Ff6F67CC13B500dB1354cfB6 + Amount: 100000000000000000 + Return to source: true + Source selector: 3478487238524512106 + Tolerance (bps): 100 + Preview: 99000000000000000 + Minimum out: 98010000000000000 + Beneficiary: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Local refund: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Delivery and refund: 664579538352300329284056227591069458130104865577 + Byte length: 128 + PAYLOAD: 0x000000000000000000000000e4ef963183c29401ff6f67cc13b500db1354cfb60000000000000000000000003a34637a41ab08519d30fdb65344aba8e9b2e994000000000000000000000000000000000000000000000000015c3393626fa0000000000000000000000000007468c6f4835610a33a61fb6ca6895751d365d329 +``` + +2. Check the output before you continue. The vault target must be your `VAULT`, the preview must be the value from the previous accordion, the minimum out must be `98010000000000000`, the beneficiary and the local refund must be your address, the return-to-source flag must be `true`, and the byte length must be `128`. The last line is the value of `DEPOSIT_PAYLOAD`. Do not continue if the preview is `0` or `forge` prints an error. + +> **CAUTION: The payload is final once sent** +> +> Nobody can change the payload after your message leaves Arbitrum Sepolia. If a check does not match, build the payload +> again before you send it. The [payload +> checklist](/solutions/cross-chain-vault-adapter/overview/how-it-works#what-a-user-sends) lists every rule. + +## Send the request + +### 1. Send the deposit + +Send 0.1 CCIP-BnM and the payload from Arbitrum Sepolia to the adapter on Ethereum Sepolia. `ccip-cli` first approves the Arbitrum Sepolia router for exactly 0.1 CCIP-BnM: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia-arbitrum-1 \ + --router $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_ROUTER \ + --dest ethereum-testnet-sepolia \ + --transfer-tokens $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_CCIP_BNM=0.1 \ + --receiver $ADAPTER \ + --data $DEPOSIT_PAYLOAD \ + --gas-limit 1500000 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +- `--transfer-tokens` takes the amount in whole tokens, so `0.1` is 0.1 CCIP-BnM. The Burn & Mint pool on Arbitrum Sepolia burns it, and CCIP delivers it on Ethereum Sepolia as CCIP-BnM, the vault's asset, so the adapter treats the request as a deposit. +- `--receiver $ADAPTER` sends the message to the adapter. Your wallet receives the vCCIP-BnM because it is the payload's `beneficiary`. +- `--data` sends the 128-byte payload as is, because it starts with `0x`. +- `--gas-limit 1500000` is the gas available on Ethereum Sepolia for everything the adapter does when it receives the message: its checks, the vault deposit, and the CCIP send of the return leg. CCIP bills you for the limit, not for the gas used. If the limit is too low, `ccipReceive` can run out of gas before the adapter records the failure, and CCIP marks the message as failed. The message then needs [manual execution](/ccip/concepts/manual-execution) with a higher limit. +- Without `--fee-token`, `ccip-cli` pays the CCIP fee in Arbitrum Sepolia ETH and prints it on the `Fee:` line. + +Your output should look something like this: + +```bash filename="Terminal" +Fee: 589279412957535n = 0.000589279412957535 ETH +🚀 Sending message to 0x3f73Cc1547F2606A20535Bd6620622107059a463 @ ethereum-testnet-sepolia , tx => 0xb1b36ceb18282e6758719439fa68677bfb464bd19edc43ef91a536be659908de , messageId => 0xc066972bedcc79e7a0072a1a259aa7f2362401b10e88333ba1fb20a63d12dd46 +Lane: +┌────────────────┬──────────────────────────────────────────────┬────────────────────────────┐ +│ (index) │ source │ dest │ +├────────────────┼──────────────────────────────────────────────┼────────────────────────────┤ +│ name │ 'ethereum-testnet-sepolia-arbitrum-1' │ 'ethereum-testnet-sepolia' │ +│ chainId │ 421614 │ 11155111 │ +│ chainSelector │ 3478487238524512106n │ 16015286601757825753n │ +│ onRamp/version │ '0x6B9a7cF69F90Ae2659bfe3069fba5Aa308A48cC4' │ '2.0.0' │ +└────────────────┴──────────────────────────────────────────────┴────────────────────────────┘ +Request (source): +┌────────────────────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├────────────────────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ messageId │ '0xc066972bedcc79e7a0072a1a259aa7f2362401b10e88333ba1fb20a63d12dd46' │ +│ origin │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ sender │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ receiver │ '0x3f73Cc1547F2606A20535Bd6620622107059a463' │ +``` + +Export the message ID from the output: + +```bash filename="Terminal" +export DEPOSIT_MESSAGE_ID= +``` + +### 2. Track the request + +Open the CCIP Explorer link from the output, or follow the request from the terminal with `ccip-cli show`. `--wait` keeps the command running until the request executes on Ethereum Sepolia: + +```bash filename="Terminal" +ccip-cli show $DEPOSIT_MESSAGE_ID \ + --wait \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +CCIP delivers the request after your transaction reaches finality on Arbitrum Sepolia, which typically takes about 20 minutes. + +Your output should look something like this: + +```bash filename="Terminal" +----------------OTHER LOGS---------------------------- + +CCIP Explorer: https://ccip.chain.link/msg/0xc066972bedcc79e7a0072a1a259aa7f2362401b10e88333ba1fb20a63d12dd46 +[SENT] Waiting for source chain finalization... +[SOURCE_FINALIZED] Source chain finalized +[SOURCE_FINALIZED] Waiting for commit on destination chain... +[SUCCESS] Message executed on destination chain +┌─────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├─────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ state │ '✅ success' │ +│ origin │ '0x44ec57f311544F371c7f81B167195c78a81F2496' │ +│ contract │ '0xc6A246A9AcdAaE651708706494720F79C3E5d0A1' │ +│ transactionHash │ '0x316d8252f6b40cae8edffa137871975b26df7d4bdf8a26ca1093b35944d352b1' │ +│ logIndex │ 159 │ +│ blockNumber │ 11862362 │ +│ timestamp │ '2026-10-07 10:23:00 (19m22s after request)' │ +└─────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` + +## Check the result + +In the transaction that executed your request, the CCIP-BnM pool on Ethereum Sepolia minted 0.1 CCIP-BnM to the adapter. The token the adapter received is the vault's asset, so the adapter treated the request as a deposit. In the same transaction, the adapter: + +1. Kept the 0.001 CCIP-BnM adapter fee. +2. Deposited the remaining 0.099 CCIP-BnM into the vault, which in turn minted 0.099 vCCIP-BnM back to the adapter. +3. The adapter then checked the 0.099 vCCIP-BnM against your `minimumOut` of 0.09801 vCCIP-BnM. +4. Sent the 0.099 vCCIP-BnM to your wallet on Arbitrum Sepolia in the return leg, and paid the CCIP fee from its Ethereum Sepolia ETH. +5. The Lock & Release pool locked the shares in the lockbox on Ethereum Sepolia, while you received the corresponding vCCIP-BnM on Arbitrum Sepolia. + +### 1. Confirm the vault deposit + +`[SUCCESS]` means that CCIP delivered your request to the adapter. It does not mean that the vault deposit happened. The adapter catches the errors that occur while it processes a request, so CCIP execution also succeeds for a request that the adapter rejected. [Why the CCIP Explorer shows Success](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#why-the-ccip-explorer-shows-success) explains this. Check the adapter's own result on Ethereum Sepolia. + +1. From the `[SUCCESS]` table, export the hash and the block number of the Ethereum Sepolia transaction that executed your request. Do not use the `transactionHash` and `blockNumber` rows of the request table above it: they belong to your send transaction on Arbitrum Sepolia. If you run `ccip-cli show` again later, the request table also shows the execution values as `receiptTransactionHash` and `receiptBlockNumber`: + +```bash filename="Terminal" +export DEPOSIT_EXECUTION_TX= +export DEPOSIT_EXECUTION_BLOCK= +``` + +2. Look for the adapter's `MessageSucceeded` event for your message ID in that block: + +```bash filename="Terminal" +cast logs --from-block $DEPOSIT_EXECUTION_BLOCK --to-block $DEPOSIT_EXECUTION_BLOCK --address $ADAPTER \ + "MessageSucceeded(bytes32 indexed messageId)" $DEPOSIT_MESSAGE_ID \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +- address: 0x3f73Cc1547F2606A20535Bd6620622107059a463 + blockHash: 0x8ee780be56adc65327d949a9e12eb1dd56ca52eadbcaa0d824d4d0f3de7c1d97 + blockNumber: 11862362 + data: 0x + logIndex: 157 + removed: false + topics: [ + 0xdf6958669026659bac75ba986685e11a7d271284989f565f2802522663e9a70f + 0xc066972bedcc79e7a0072a1a259aa7f2362401b10e88333ba1fb20a63d12dd46 + ] + transactionHash: 0x316d8252f6b40cae8edffa137871975b26df7d4bdf8a26ca1093b35944d352b1 + transactionIndex: 89 +``` + +The adapter emits `MessageSucceeded` only after it completes the whole request. In the same transaction, it also emits `TargetProcessed` for the vault deposit and `MessageSent` for the return leg. +When a request fails, the adapter rolls back those events and emits `MessageFailed` instead. + +3. Read the adapter's state for your message ID: + +```bash filename="Terminal" +cast call $ADAPTER "messageErrorCode(bytes32)(uint8)" $DEPOSIT_MESSAGE_ID --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +0 +``` + +1. `0` (`NONE`) confirms success only together with `MessageSucceeded`, because a message that has not arrived yet also reads `0`. +2. `1` (`BASIC`) means that the adapter rejected the request and holds your 0.1 CCIP-BnM. +3. In that case, [What counts as a failure](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#what-counts-as-a-failure) lists the possible causes. [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery) shows how to refund the CCIP-BnM to Arbitrum Sepolia, or recover it on Ethereum Sepolia with your wallet, the payload's local refund address. + +### 2. Find the return leg + +The adapter sent your vCCIP-BnM in the return leg, a second CCIP message with its own message ID, from the transaction that executed your request. Find the ID with the CCIP API, then confirm it on-chain. + +1. Search the CCIP API for the latest message that the adapter sent to your wallet. + > `--no-interactive` makes the command exit after it prints the result, instead of opening a menu: + +```bash filename="Terminal" +ccip-cli search messages $ADAPTER --receiver $MY_ADDRESS --limit 1 --no-interactive +``` + +Your output should look something like this: + +```bash filename="Terminal" +┌───────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├───────────┼──────────────────────────────────────────────────────────────────────┤ +│ messageId │ '0xe0977e67438c5116084634de44f79db5feb995582b3f7645abc380a82c6dc8c0' │ +│ status │ 'SENT' │ +│ source │ 'ethereum-testnet-sepolia [11155111]' │ +│ dest │ 'ethereum-testnet-sepolia-arbitrum-1 [421614]' │ +│ sender │ '0x3f73Cc1547F2606A20535Bd6620622107059a463' │ +│ receiver │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ txHash │ '0x316d8252f6b40cae8edffa137871975b26df7d4bdf8a26ca1093b35944d352b1' │ +│ timestamp │ '2026-10-07T10:23:00.000Z' │ +└───────────┴──────────────────────────────────────────────────────────────────────┘ + +1 message(s) found. +``` + +The `txHash` must equal `$DEPOSIT_EXECUTION_TX`. If the command finds no message, or `txHash` differs, the CCIP API has not indexed the return leg yet, so run the command again after a minute. Export the message ID: + +```bash filename="Terminal" +export DEPOSIT_RETURN_MESSAGE_ID= +``` + +2. Read the same ID from the adapter's `MessageSent` event. This way needs only your Ethereum Sepolia RPC, not the CCIP API: + +```bash filename="Terminal" +cast logs --from-block $DEPOSIT_EXECUTION_BLOCK --to-block $DEPOSIT_EXECUTION_BLOCK --address $ADAPTER \ + "MessageSent(bytes32 indexed messageId,uint64 indexed destinationChainSelector,uint8 indexed chainType,bytes32 beneficiary,address token,uint256 amount,uint256 fee)" \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +- address: 0x3f73Cc1547F2606A20535Bd6620622107059a463 + blockHash: 0x8ee780be56adc65327d949a9e12eb1dd56ca52eadbcaa0d824d4d0f3de7c1d97 + blockNumber: 11862362 + data: 0x0000000000000000000000003a34637a41ab08519d30fdb65344aba8e9b2e994000000000000000000000000e4ef963183c29401ff6f67cc13b500db1354cfb6000000000000000000000000000000000000000000000000015fb7f9b8c380000000000000000000000000000000000000000000000000000000ea7f1b127937 + logIndex: 156 + removed: false + topics: [ + 0xb6bd76b2fc831b8df833914080c10bae0c3951d7550d7c24a72e3d2fa8b7ff63 + 0xe0977e67438c5116084634de44f79db5feb995582b3f7645abc380a82c6dc8c0 + 0x000000000000000000000000000000000000000000000000304611b6affba76a + 0x0000000000000000000000000000000000000000000000000000000000000001 + ] + transactionHash: 0x316d8252f6b40cae8edffa137871975b26df7d4bdf8a26ca1093b35944d352b1 + transactionIndex: 89 +``` + +The second entry of `topics` is the return leg's message ID and must equal `$DEPOSIT_RETURN_MESSAGE_ID`. The third is the Arbitrum Sepolia chain selector, and the fourth is the chain type, `1` for EVM. + +3. Decode the log's `data` field. It holds the beneficiary, the token, the amount, and the CCIP fee that the adapter paid in Ethereum Sepolia ETH, in wei. Replace `` with the `data` value from the log: + +```bash filename="Terminal" +cast abi-decode --input "f(bytes32,address,uint256,uint256)" +``` + +The token is your vault address, because the vault contract is the vCCIP-BnM token on Ethereum Sepolia, and the amount is `99000000000000000 [9.9e16]`, the 0.099 vCCIP-BnM. `ccip-cli show $DEPOSIT_EXECUTION_TX`, with the same `--rpc` options, also finds the return leg, because it lists the CCIP messages sent in a transaction. + +### 3. Track the return leg + +Follow the return leg until it executes on Arbitrum Sepolia: + +```bash filename="Terminal" +ccip-cli show $DEPOSIT_RETURN_MESSAGE_ID \ + --wait \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +The return leg carries only tokens: its `data` is empty and its gas limit is 0, so CCIP delivers the vCCIP-BnM to your wallet without calling a contract. \ +The Burn & Mint pool on Arbitrum Sepolia mints it. CCIP delivers the return leg after the execution transaction reaches finality on Ethereum Sepolia, which typically takes about 20 minutes. + +Your output should look something like this: + +```bash filename="Terminal" +----------------OTHER LOGS---------------------------- +┌─────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├─────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ state │ '✅ success' │ +│ origin │ '0xD91Ae3D50Aa667d648ca93DF18AE36DA13eeedD5' │ +│ contract │ '0xC93218EB7B778bC0c13E5296140C8E4Fa1C440DA' │ +│ transactionHash │ '0x884ed79b184604ed399fe716dacb9d64cecf6b1d85d9778f17a4358dd5039965' │ +│ logIndex │ 18 │ +│ blockNumber │ 316683886 │ +│ timestamp │ '2026-10-07 10:41:45 (18m45s after request)' │ +└─────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` + +### 4. Confirm the balances + +1. Check your vCCIP-BnM and CCIP-BnM balances on Arbitrum Sepolia: + +```bash filename="Terminal" +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_CCIP_BNM "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +199000000000000000 [1.99e17] +900000000000000000 [9e17] +``` + +Your vCCIP-BnM grew by 0.099, from 0.1 to 0.199. Your CCIP-BnM dropped by the 0.1 you deposited, from 1 to 0.9. You paid 0.1 CCIP-BnM and received 0.099 vCCIP-BnM, and the 0.001 difference is the adapter fee. + +2. Check the vault's total assets and total share supply, and the fees that the adapter collected, on Ethereum Sepolia: + +```bash filename="Terminal" +cast call $VAULT "totalAssets()(uint256)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $VAULT "totalSupply()(uint256)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $ADAPTER "collectedFees(address)(uint256)" $ETHEREUM_SEPOLIA_CCIP_BNM --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +1099000000000000000 [1.099e18] +1099000000000000000 [1.099e18] +1000000000000000 [1e15] +``` + +Both vault totals grew by 0.099, from 1 to 1.099, so shares still convert 1:1. `collectedFees` holds the 0.001 CCIP-BnM adapter fee until the fee collector withdraws it with `withdrawFee`. + +If you set up the share token with the Lock & Mint tutorial, check that the lockbox holds 0.199 vCCIP-BnM, which backs your 0.199 vCCIP-BnM on Arbitrum Sepolia: + +```bash filename="Terminal" +cast call $VAULT "balanceOf(address)(uint256)" $LOCK_BOX --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +199000000000000000 [1.99e17] +``` + +> **NOTE: Where to go next** +> +> [Redeem from a source chain](/solutions/cross-chain-vault-adapter/guides/redeem-from-source-chain): redeem 0.1 +> vCCIP-BnM from Arbitrum Sepolia for CCIP-BnM, withdraw the adapter fees as the vault operator, and review all four +> CCIP messages. Stay in this terminal and in the `cross-chain-vault-adapters` directory. + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. + +--- + +# Redeem from a source chain +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter/guides/redeem-from-source-chain + +This tutorial continues from [Deposit from a source chain](/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain), which left your wallet with 0.199 vCCIP-BnM on Arbitrum Sepolia. + +The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. Here, Ethereum Sepolia is the hub chain and Arbitrum Sepolia is the source chain. + +In this tutorial, to simulate the redeem flow, you will: + +1. Preview a redemption of 0.1 vCCIP-BnM and set a minimum output with a 1% tolerance. +2. Send 0.1 vCCIP-BnM and a 128-byte payload from Arbitrum Sepolia to the adapter on Ethereum Sepolia. +3. Confirm that the adapter redeemed the shares on Ethereum Sepolia, and follow the return leg that carries 0.099 CCIP-BnM to Arbitrum Sepolia. +4. Acting as the vault team, withdraw the 0.002 CCIP-BnM of adapter fees that your deposit and this redemption paid. +5. Review the four CCIP messages of the deposit and the redemption, and reconcile the balances on both chains. + +## Before you begin + +You need: + +- ccip-cli, Foundry, Node.js, and the `cross-chain-vault-adapters` repository, set up in [Set up your development environment](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault#set-up-your-development-environment). +- A Foundry keystore for your wallet, which acts as a user on Arbitrum Sepolia and then withdraws the adapter fees as the fee collector in this tutorial. Use the same keystore in every tutorial of this solution. +- An ERC-4626 vault on Ethereum Sepolia whose share token is a CCIP cross-chain token between Ethereum Sepolia and Arbitrum Sepolia. [Deploy the vault and set up its share token](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault) sets one up. +- An adapter on Ethereum Sepolia for that vault that accepts requests from Arbitrum Sepolia and has redemptions enabled. [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter) deploys one. + +> **CAUTION: Before you continue** +> +> - **Ethereum Sepolia ETH**: required for Ethereum Sepolia transactions. Get it from the [Chainlink faucet](https://faucets.chain.link). In this tutorial, the fee withdrawal needs it. +> - **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions. Get it from the same faucet. The redemption request pays a CCIP fee of about 0.0023 Arbitrum Sepolia ETH at the time of writing. +> - **vCCIP-BnM on Arbitrum Sepolia**: at least 0.1, the amount you redeem. [Check the starting balances](#check-the-starting-balances) reads your balance. +> - **Ethereum Sepolia ETH in the adapter**: at least 0.001 Ethereum Sepolia ETH, which pays the return leg's CCIP fee. [Check the adapter](#check-the-adapter) reads the balance and shows how to top it up. + +> **TIP: Bringing your own vault or adapter?** +> +> Skip only what you already have: +> +> - If you completed [Deposit from a source chain](/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain) in this terminal, your shell variables are set. Continue with [Check the adapter](#check-the-adapter). +> - If you skipped Deposit from a source chain, run every command in [Set your shell variables](#set-your-shell-variables), then redeem the 0.1 vCCIP-BnM that [Deploy the vault and set up its share token](/solutions/cross-chain-vault-adapter/guides/deploy-the-vault) sent to Arbitrum Sepolia. Your balances then differ from the values shown, so compare the changes instead. +> - If you bring your own vault, adapter, or share token, set the variables in [Set your shell variables](#set-your-shell-variables) to your addresses. Set the two CCIP-BnM variables to your vault's asset on each chain. In the `preview` call and in the payload builder's `AMOUNT`, scale `100000000000000000` (0.1 of an 18-decimal share token) to your share token's decimals. `ccip-cli` takes whole tokens, so its `=0.1` stays the same. If you did not set up the share token with the Lock & Mint tutorial, skip the lockbox checks. +> - Your adapter must accept Arbitrum Sepolia as a source chain, allowlist your vault, allow redemptions, and hold Ethereum Sepolia ETH. Your share token must be a cross-chain token between Ethereum Sepolia and Arbitrum Sepolia. To withdraw the adapter fees, your wallet needs `FEE_COLLECTOR_ROLE` on the adapter. + +### 1. Set your shell variables + +Run the commands in this tutorial from the root of the `cross-chain-vault-adapters` repository, in the terminal you used for the previous tutorials. If you opened a new terminal, change to the repository root and set the variables from the previous tutorials again: + +```bash filename="Terminal" +export KEYSTORE_NAME=your_keystore_name +export ETHEREUM_SEPOLIA_RPC_URL=your_ethereum_sepolia_rpc_url +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc_url +export ETHEREUM_SEPOLIA_CCIP_BNM=0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_ROUTER=0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_CCIP_BNM=0xA8C0c11bf64AF62CDCA6f93D3769B88BdD7cb93D +export VAULT= +export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN= +export LOCK_BOX= +export ADAPTER= +``` + +- The Arbitrum Sepolia router and CCIP-BnM addresses come from the [Arbitrum Sepolia page of the CCIP Directory](/ccip/directory/testnet/chain/ethereum-testnet-sepolia-arbitrum-1). +- `ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN` is the vCCIP-BnM token you deployed on Arbitrum Sepolia with the Lock & Mint tutorial. `LOCK_BOX` is the lockbox from its Deploy ERC20LockBox step, which also saved the address under `script/deployments/lock-boxes/` in `docs-cct-foundry`. +- `ADAPTER` is the adapter address you exported in [Broadcast the deployment](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter#broadcast-the-deployment), also stored as `adapter` in `deployments/ccip/11155111.json`. If you deployed your adapter another way, use that address instead. + +In a new terminal, also set `MY_ADDRESS` to your keystore's address. `cast` asks for the keystore password: + +```bash filename="Terminal" +export MY_ADDRESS=$(cast wallet address --account $KEYSTORE_NAME) +echo $MY_ADDRESS +``` + +### 2. Check the adapter + +1. Check that `ADAPTER` points to an adapter: + +```bash filename="Terminal" +cast call $ADAPTER "typeAndVersion()(string)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +```bash filename="Terminal" +"CrossChainERC4626Adapter 1.0.0" +``` + +2. Check the adapter's Ethereum Sepolia ETH balance. The adapter pays the return leg's CCIP fee from it: + +```bash filename="Terminal" +cast balance $ADAPTER --ether --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +The balance is the 0.01 Ethereum Sepolia ETH that [Set the return format and fund the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter#fund-the-adapter) sent, plus any top-up, minus the CCIP fee of your deposit's return leg. +Make sure to have some Sepolia ETH in the adapter. +With too little, the adapter cannot send the return leg, and your request becomes a failed message. If the balance is below 0.001 Ethereum Sepolia ETH, top it up. Any account can send Ethereum Sepolia ETH to the adapter: + +```bash filename="Terminal" +cast send $ADAPTER --value 0.01ether --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --account $KEYSTORE_NAME +``` + +### 3. Check the starting balances + +Check your vCCIP-BnM and CCIP-BnM balances on Arbitrum Sepolia: + +```bash filename="Terminal" +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_CCIP_BNM "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Example output: + +```bash filename="Terminal" +199000000000000000 [1.99e17] +900000000000000000 [9e17] +``` + +You hold 0.199 vCCIP-BnM: 0.1 from the test transfer in Deploy the vault and set up its share token, and 0.099 from your deposit. +The 0.9 CCIP-BnM is what remains of the 1 CCIP-BnM that you minted with `drip` in Deposit from a source chain. The balances later on this page start from these values. + +## Build the request + +### 1. Preview the redemption + +`minimumOut` in the payload is the smallest amount of CCIP-BnM, after the adapter fee, that you accept for your 0.1 vCCIP-BnM. +The adapter checks it on Ethereum Sepolia when your request executes, about 20 minutes after you send it. By then, the vault's exchange rate or the adapter fee, which the adapter reads at execution, can have changed. +If the output is below `minimumOut`, the adapter redeems nothing and sends nothing back: it keeps your 0.1 vCCIP-BnM and records a failed message, which you then [refund or recover](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery). + +1. Preview the redemption with the adapter's [`preview`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#preview) function, and store the result: + +```bash filename="Terminal" +export REDEEM_PREVIEW=$(cast call $ADAPTER "preview(address,address,uint256,bool,uint64)(uint256)" \ + $VAULT $VAULT 100000000000000000 true 3478487238524512106 \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL | awk '{print $1}') +echo $REDEEM_PREVIEW +``` + +- The first argument is the token as the adapter receives it. CCIP delivers your vCCIP-BnM from Arbitrum Sepolia as vCCIP-BnM on Ethereum Sepolia, where the vault contract is the share token, so you pass `$VAULT` as both the token and the vault. +- `100000000000000000` is 0.1 vCCIP-BnM, `true` returns the output to the source chain, and `3478487238524512106` is the Arbitrum Sepolia chain selector. +- `awk` keeps the number and drops the `[9.9e16]` that `cast` appends. + +Example Output: + +```bash filename="Terminal" +99000000000000000 +``` + +The 1:1 vault redeems 0.1 vCCIP-BnM for 0.1 CCIP-BnM, and the adapter deducts its 0.001 CCIP-BnM fee from it. Do not continue if the output is `0` or `cast` prints an error. `preview` returns `0` when the adapter fee consumes the redeemed CCIP-BnM. It reverts with `InvalidTarget` when the vault is not allowlisted, `InvalidChain` when Arbitrum Sepolia is not a source chain, and `RedeemsDisabled` when redemptions are off. + +2. No separate command applies the tolerance: [Build the payload](#build-the-payload) subtracts it from the preview to set `minimumOut`. With the 1% tolerance this page uses, the minimum output is `98010000000000000` (0.09801 CCIP-BnM), which the builder prints. For a 0.5% tolerance, pass `TOLERANCE_BPS=50` to the builder instead of `100`. + +A smaller tolerance rejects more requests when the rate moves, and a larger one accepts a worse rate. The vault in these tutorials has no yield strategy, so its exchange rate changes only if someone transfers CCIP-BnM to it directly, and 1% is only an example. For your own vault, choose the tolerance from how far its exchange rate can move during CCIP delivery. See [Slippage protection](/solutions/cross-chain-vault-adapter/overview/how-it-works#slippage-protection). + +### 2. Build the payload + +The payload is the `data` of your CCIP message: four 32-byte fields that tell the adapter what to do. For this redemption, they hold: + +| Field | Value | Meaning | +| ------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `target` | `$VAULT` | The vault to redeem from. It must be on the adapter's allowlist. | +| `beneficiary` | `$MY_ADDRESS` | The account that receives the CCIP-BnM on Arbitrum Sepolia. | +| `minimumOut` | the preview minus 1% | The smallest acceptable output, in CCIP-BnM. The builder subtracts the `TOLERANCE_BPS` tolerance from the preview. | +| `deliveryAndRefund` | your address and `true` | Bit 0 set to `1` returns the CCIP-BnM to Arbitrum Sepolia. Bits 1 to 160 hold `$MY_ADDRESS` as the local refund address, which can take back the vCCIP-BnM on Ethereum Sepolia if the request fails. The builder packs both. | + +The payload does not name the action. The token you send decides it: vCCIP-BnM, the vault's share token, makes this request a redemption, so `minimumOut` counts CCIP-BnM. With the 1:1 vault and the equal adapter fees in these tutorials, this payload is byte for byte the same payload you built in [Deposit from a source chain](/solutions/cross-chain-vault-adapter/guides/deposit-from-source-chain). + +1. Build the payload with the repository's `pnpm ccip:build-payload` script. It calls your adapter's `preview` with your vault, token, amount, source chain and return flag, subtracts the tolerance from the preview to set `minimumOut`, packs the return flag and your local refund address into `deliveryAndRefund`, and ABI-encodes the four fields: + +```bash filename="Terminal" +export REDEEM_PAYLOAD=$(REQUEST_TOKEN=$VAULT \ + REQUEST_ADAPTER=$ADAPTER \ + REQUEST_TARGET=$VAULT \ + AMOUNT=100000000000000000 \ + SOURCE_CHAIN_SELECTOR=3478487238524512106 \ + TOLERANCE_BPS=100 \ + MY_ADDRESS=$MY_ADDRESS \ + pnpm ccip:build-payload --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + | tee /dev/stderr \ + | awk '/PAYLOAD:/ {print $2}') +``` + +- `REQUEST_TOKEN` is the token as the adapter receives it. CCIP delivers your vCCIP-BnM from Arbitrum Sepolia as vCCIP-BnM on Ethereum Sepolia, where the vault contract is the share token, so you pass `$VAULT` as the token. +- `TOLERANCE_BPS=100` is the 1% tolerance from the previous accordion: the script sets `minimumOut` to `preview * (10_000 - 100) / 10_000`. Use `50` for a 0.5% tolerance. +- `tee /dev/stderr` shows the script's output in your terminal, and `awk` stores the value of its `PAYLOAD:` line in `REDEEM_PAYLOAD`. + Your output should look something like this: + +```bash filename="Terminal" +== Logs == + Adapter: 0x3f73Cc1547F2606A20535Bd6620622107059a463 + Token: 0xE4Ef963183C29401Ff6F67CC13B500dB1354cfB6 + Vault target: 0xE4Ef963183C29401Ff6F67CC13B500dB1354cfB6 + Amount: 100000000000000000 + Return to source: true + Source selector: 3478487238524512106 + Tolerance (bps): 100 + Preview: 99000000000000000 + Minimum out: 98010000000000000 + Beneficiary: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Local refund: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 + Delivery and refund: 664579538352300329284056227591069458130104865577 + Byte length: 128 + PAYLOAD: 0x000000000000000000000000e4ef963183c29401ff6f67cc13b500db1354cfb60000000000000000000000003a34637a41ab08519d30fdb65344aba8e9b2e994000000000000000000000000000000000000000000000000015c3393626fa0000000000000000000000000007468c6f4835610a33a61fb6ca6895751d365d329 +``` + +2. Check the output before you continue. + - The vault target must be your `VAULT` (the token and the vault are the same address for a redemption), + - the preview must be the value from the previous accordion, + - the minimum out must be `98010000000000000`, + - the beneficiary and the local refund must be your address, + - the return-to-source flag must be `true`, + - the byte length must be `128`. + - The last line is the value of `REDEEM_PAYLOAD`: byte for byte the same payload as `DEPOSIT_PAYLOAD` in the deposit tutorial. Do not continue if the preview is `0` or `forge` prints an error. + +> **CAUTION: The payload is final once sent** +> +> Nobody can change the payload after your message leaves Arbitrum Sepolia. If a check does not match, build the payload +> again before you send it. The [payload +> checklist](/solutions/cross-chain-vault-adapter/overview/how-it-works#what-a-user-sends) lists every rule. + +## Send the request + +### 1. Send the redemption + +Send 0.1 vCCIP-BnM and the payload from Arbitrum Sepolia to the adapter on Ethereum Sepolia. `ccip-cli` first approves the Arbitrum Sepolia router for exactly 0.1 vCCIP-BnM: + +```bash filename="Terminal" +ccip-cli send \ + --source ethereum-testnet-sepolia-arbitrum-1 \ + --router $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_ROUTER \ + --dest ethereum-testnet-sepolia \ + --transfer-tokens $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0.1 \ + --receiver $ADAPTER \ + --data $REDEEM_PAYLOAD \ + --gas-limit 1500000 \ + --wallet foundry:$KEYSTORE_NAME \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +- `--transfer-tokens` takes the amount in whole tokens, so `0.1` is 0.1 vCCIP-BnM. The Burn & Mint pool on Arbitrum Sepolia burns it, and the Lock & Release pool on Ethereum Sepolia releases 0.1 vCCIP-BnM from the lockbox to the adapter. The adapter receives the vault's share token, so it treats the request as a redemption. +- `--receiver $ADAPTER` sends the message to the adapter. Your wallet receives the CCIP-BnM because it is the payload's `beneficiary`. +- `--data` sends the 128-byte payload as is, because it starts with `0x`. +- `--gas-limit 1500000` is the gas available on Ethereum Sepolia for everything the adapter does when it receives the message: its checks, the vault redemption, and the CCIP send of the return leg. CCIP bills you for the limit, not for the gas used. If the limit is too low, `ccipReceive` can run out of gas before the adapter records the failure, and CCIP marks the message as failed. The message then needs [manual execution](/ccip/concepts/manual-execution) with a higher limit. +- Without `--fee-token`, `ccip-cli` pays the CCIP fee in Arbitrum Sepolia ETH and prints it on the `Fee:` line. + +Your output should look something like this: + +```bash filename="Terminal" +Estimated gasLimit for sender = 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 : 744376 +Fee: 836394747647981n = 0.000836394747647981 ETH +🚀 Sending message to 0x3f73Cc1547F2606A20535Bd6620622107059a463 @ ethereum-testnet-sepolia , tx => 0xa46ff978365a06bd6f7500532ae0ef4b6de3bf5f876b98dfa265baf718df871c , messageId => 0xabad1355c344bcc670d783494829eaf68a197f85f7d263c64a0b66a34b5119ef +Lane: +┌────────────────┬──────────────────────────────────────────────┬────────────────────────────┐ +│ (index) │ source │ dest │ +├────────────────┼──────────────────────────────────────────────┼────────────────────────────┤ +│ name │ 'ethereum-testnet-sepolia-arbitrum-1' │ 'ethereum-testnet-sepolia' │ +│ chainId │ 421614 │ 11155111 │ +│ chainSelector │ 3478487238524512106n │ 16015286601757825753n │ +│ onRamp/version │ '0x6B9a7cF69F90Ae2659bfe3069fba5Aa308A48cC4' │ '2.0.0' │ +└────────────────┴──────────────────────────────────────────────┴────────────────────────────┘ +Request (source): +┌────────────────────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├────────────────────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ messageId │ '0xabad1355c344bcc670d783494829eaf68a197f85f7d263c64a0b66a34b5119ef' │ +│ origin │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ sender │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ receiver │ '0x3f73Cc1547F2606A20535Bd6620622107059a463' │ +``` + +Export the message ID from the output: + +```bash filename="Terminal" +export REDEEM_MESSAGE_ID= +``` + +### 2. Track the request + +Open the CCIP Explorer link from the output, or follow the request from the terminal with `ccip-cli show`. `--wait` keeps the command running until the request executes on Ethereum Sepolia: + +```bash filename="Terminal" +ccip-cli show $REDEEM_MESSAGE_ID \ + --wait \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +CCIP delivers the request after your transaction reaches finality on Arbitrum Sepolia, which typically takes about 20 minutes. + +Once your request has finalized, your output should look something like this: + +```bash filename="Terminal" +---------------------OTHER LOGS---------------------------------------------------------- + +[SENT] Waiting for source chain finalization... +[SOURCE_FINALIZED] Source chain finalized +[SOURCE_FINALIZED] Waiting for commit on destination chain... +[SUCCESS] Message executed on destination chain +┌─────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├─────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ state │ '✅ success' │ +│ origin │ '0x64d20bbd0a280307884A99C43E8baf9fC5D5BC11' │ +│ contract │ '0xc6A246A9AcdAaE651708706494720F79C3E5d0A1' │ +│ transactionHash │ '0xfd83d4a1704797b75ad228810f0c8dd79e8e774bed22cb06d155116c5afeecd5' │ +│ logIndex │ 93 │ +│ blockNumber │ 11863985 │ +│ timestamp │ '2026-10-07 15:49:24 (17m10s after request)' │ +└─────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` + +## Check the result + +In the transaction that executed your request, the Lock & Release pool on Ethereum Sepolia released 0.1 vCCIP-BnM from the lockbox to the adapter. +These are shares that the vault minted on Ethereum Sepolia: the lockbox held them while the same amount existed on Arbitrum Sepolia, where the Burn & Mint pool burned your 0.1 vCCIP-BnM when you sent the request. +The token the adapter received is the vault's share token, so the adapter treated the request as a redemption. + +In the same transaction, the adapter: + +1. Redeemed the 0.1 vCCIP-BnM from the vault, which burned the shares and paid 0.1 CCIP-BnM to the adapter. +2. Kept the 0.001 CCIP-BnM adapter fee from the redeemed CCIP-BnM. +3. Checked the remaining 0.099 CCIP-BnM against your `minimumOut` of 0.09801 CCIP-BnM. +4. Sent the 0.099 CCIP-BnM to your wallet on Arbitrum Sepolia in the return leg, and paid its CCIP fee from its Ethereum Sepolia ETH. The CCIP-BnM pool on Ethereum Sepolia burned the CCIP-BnM. + +### 1. Confirm the vault redemption + +`[SUCCESS]` means that CCIP delivered your request to the adapter. It does not mean that the vault redemption happened. The adapter catches the errors that occur while it processes a request, so CCIP execution also succeeds for a request that the adapter rejected. [Why the CCIP Explorer shows Success](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#why-the-ccip-explorer-shows-success) explains this. Check the adapter's own result on Ethereum Sepolia. + +1. From the `[SUCCESS]` table, export the hash and the block number of the Ethereum Sepolia transaction that executed your request. Do not use the `transactionHash` and `blockNumber` rows of the request table above it: they belong to your send transaction on Arbitrum Sepolia. If you run `ccip-cli show` again later, the request table also shows the execution values as `receiptTransactionHash` and `receiptBlockNumber`: + +```bash filename="Terminal" +export REDEEM_EXECUTION_TX= +export REDEEM_EXECUTION_BLOCK= +``` + +2. Look for the adapter's `MessageSucceeded` event for your message ID in that block: + +```bash filename="Terminal" +cast logs --from-block $REDEEM_EXECUTION_BLOCK --to-block $REDEEM_EXECUTION_BLOCK --address $ADAPTER \ + "MessageSucceeded(bytes32 indexed messageId)" $REDEEM_MESSAGE_ID \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +- address: 0x3f73Cc1547F2606A20535Bd6620622107059a463 + blockHash: 0x075286b104df9919e1eb6c395f9683017abe1fcdbc93cd765f92bc3b058559cd + blockNumber: 11863985 + data: 0x + logIndex: 91 + removed: false + topics: [ + 0xdf6958669026659bac75ba986685e11a7d271284989f565f2802522663e9a70f + 0xabad1355c344bcc670d783494829eaf68a197f85f7d263c64a0b66a34b5119ef + ] + transactionHash: 0xfd83d4a1704797b75ad228810f0c8dd79e8e774bed22cb06d155116c5afeecd5 + transactionIndex: 53 +``` + +The adapter emits `MessageSucceeded` only after it completes the whole request. In the same transaction, it also emitted `TargetProcessed` for the vault redemption and `MessageSent` for the return leg. When a request fails, the adapter rolls back those events and emits `MessageFailed` instead. + +3. Read the adapter's state for your message ID: + +```bash filename="Terminal" +cast call $ADAPTER "messageErrorCode(bytes32)(uint8)" $REDEEM_MESSAGE_ID --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +```bash filename="Terminal" +0 +``` + +1. `0` (`NONE`) confirms success only together with `MessageSucceeded`, because a message that has not arrived yet also reads `0`. +2. `1` (`BASIC`) means that the adapter rejected the request and holds your 0.1 vCCIP-BnM. In that case, [What counts as a failure](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#what-counts-as-a-failure) lists the possible causes. +3. [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery) shows how to refund the vCCIP-BnM to Arbitrum Sepolia, or recover it on Ethereum Sepolia with your wallet, the payload's local refund address. + +### 2. Find the return leg + +The adapter sent your CCIP-BnM in the return leg, a second CCIP message with its own message ID, from the transaction that executed your request. Find the ID with the CCIP API, then confirm it on-chain. + +1. Search the CCIP API for the latest message that the adapter sent to your wallet. `--no-interactive` makes the command exit after it prints the result, instead of opening a menu: + +```bash filename="Terminal" +ccip-cli search messages $ADAPTER --receiver $MY_ADDRESS --limit 1 --no-interactive +``` + +Your output should look something like this: + +```bash filename="Terminal" +┌───────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├───────────┼──────────────────────────────────────────────────────────────────────┤ +│ messageId │ '0xd8f1f245b1884df38538b295def1b0bafe16a116893c3371102e88ad78243662' │ +│ status │ 'SENT' │ +│ source │ 'ethereum-testnet-sepolia [11155111]' │ +│ dest │ 'ethereum-testnet-sepolia-arbitrum-1 [421614]' │ +│ sender │ '0x3f73Cc1547F2606A20535Bd6620622107059a463' │ +│ receiver │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ txHash │ '0xfd83d4a1704797b75ad228810f0c8dd79e8e774bed22cb06d155116c5afeecd5' │ +│ timestamp │ '2026-10-07T15:49:24.000Z' │ +└───────────┴──────────────────────────────────────────────────────────────────────┘ + +1 message(s) found. +``` + +The `txHash` must equal `$REDEEM_EXECUTION_TX`. If it differs, the CCIP API has not indexed this return leg yet and still shows the return leg of your deposit, so run the command again after a minute. Export the message ID: + +```bash filename="Terminal" +export REDEEM_RETURN_MESSAGE_ID= +``` + +2. Read the same ID from the adapter's `MessageSent` event. This way needs only your Ethereum Sepolia RPC, not the CCIP API: + +```bash filename="Terminal" +cast logs --from-block $REDEEM_EXECUTION_BLOCK --to-block $REDEEM_EXECUTION_BLOCK --address $ADAPTER \ + "MessageSent(bytes32 indexed messageId,uint64 indexed destinationChainSelector,uint8 indexed chainType,bytes32 beneficiary,address token,uint256 amount,uint256 fee)" \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Your output should look something like this: + +```bash filename="Terminal" +- address: 0x3f73Cc1547F2606A20535Bd6620622107059a463 + blockHash: 0x075286b104df9919e1eb6c395f9683017abe1fcdbc93cd765f92bc3b058559cd + blockNumber: 11863985 + data: 0x0000000000000000000000003a34637a41ab08519d30fdb65344aba8e9b2e994000000000000000000000000fd57b4ddbf88a4e07ff4e34c487b99af2fe82a05000000000000000000000000000000000000000000000000015fb7f9b8c380000000000000000000000000000000000000000000000000000000ea7f1b127937 + logIndex: 90 + removed: false + topics: [ + 0xb6bd76b2fc831b8df833914080c10bae0c3951d7550d7c24a72e3d2fa8b7ff63 + 0xd8f1f245b1884df38538b295def1b0bafe16a116893c3371102e88ad78243662 + 0x000000000000000000000000000000000000000000000000304611b6affba76a + 0x0000000000000000000000000000000000000000000000000000000000000001 + ] + transactionHash: 0xfd83d4a1704797b75ad228810f0c8dd79e8e774bed22cb06d155116c5afeecd5 + transactionIndex: 53 +``` + +The second entry of `topics` is the return leg's message ID and must equal `$REDEEM_RETURN_MESSAGE_ID`. The third is the Arbitrum Sepolia chain selector, and the fourth is the chain type, `1` for EVM. + +3. Decode the log's `data` field. It holds the beneficiary, the token, the amount, and the CCIP fee that the adapter paid in Ethereum Sepolia ETH, in wei. Replace `` with the `data` value from the log: + +```bash filename="Terminal" +cast abi-decode --input "f(bytes32,address,uint256,uint256)" +``` + +### 3. Track the return leg + +Follow the return leg until it executes on Arbitrum Sepolia: + +```bash filename="Terminal" +ccip-cli show $REDEEM_RETURN_MESSAGE_ID \ + --wait \ + --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \ + --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL" +``` + +The return leg carries only tokens: its `data` is empty and its gas limit is 0, so CCIP delivers the CCIP-BnM to your wallet without calling a contract. The CCIP-BnM pool on Arbitrum Sepolia mints it. CCIP delivers the return leg after the execution transaction reaches finality on Ethereum Sepolia, which typically takes about 20 minutes. + +Your output should look something like this: + +```bash filename="Terminal" +----------------------OTHER LOGS--------------------------------------- +[SENT] Waiting for source chain finalization... +[SOURCE_FINALIZED] Source chain finalized +[SOURCE_FINALIZED] Waiting for commit on destination chain... +[SUCCESS] Message executed on destination chain +┌─────────────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├─────────────────┼──────────────────────────────────────────────────────────────────────┤ +│ state │ '✅ success' │ +│ origin │ '0x40699f4108fC51a4c50BDA0BF3Aae904FB4e9F1B' │ +│ contract │ '0xC93218EB7B778bC0c13E5296140C8E4Fa1C440DA' │ +│ transactionHash │ '0x888e522f8a6b6032e8d4ad5034cd9630168308e5a3885c436cdb533ec77b06b7' │ +│ logIndex │ 3 │ +│ blockNumber │ 316762230 │ +│ timestamp │ '2026-10-07 16:08:19 (18m55s after request)' │ +└─────────────────┴──────────────────────────────────────────────────────────────────────┘ +``` + +### 4. Confirm the balances + +1. Check your vCCIP-BnM and CCIP-BnM balances on Arbitrum Sepolia: + +```bash filename="Terminal" +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_CCIP_BNM "balanceOf(address)(uint256)" $MY_ADDRESS \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +99000000000000000 [9.9e16] +999000000000000000 [9.99e17] +``` + +Your vCCIP-BnM dropped by the 0.1 you redeemed, from 0.199 to 0.099. Your CCIP-BnM grew by 0.099, from 0.9 to 0.999. You redeemed 0.1 vCCIP-BnM for 0.099 CCIP-BnM, and the 0.001 difference is the adapter fee. + +2. Check the vault's total assets and total share supply on Ethereum Sepolia: + +```bash filename="Terminal" +cast call $VAULT "totalAssets()(uint256)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $VAULT "totalSupply()(uint256)" --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +999000000000000000 [9.99e17] +999000000000000000 [9.99e17] +``` + +Both vault totals dropped by 0.1, from 1.099 to 0.999: the vault burned 0.1 vCCIP-BnM and paid out 0.1 CCIP-BnM, so shares still convert 1:1. + +If you set up the share token with the Lock & Mint tutorial, check that the lockbox holds 0.099 vCCIP-BnM, which backs your 0.099 vCCIP-BnM on Arbitrum Sepolia: + +```bash filename="Terminal" +cast call $VAULT "balanceOf(address)(uint256)" $LOCK_BOX --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +99000000000000000 [9.9e16] +``` + +## Withdraw the adapter fees + +Now you will act as the vault team. The adapter keeps each adapter fee in the vault's asset, CCIP-BnM, and adds it to `collectedFees`. Only an account with `FEE_COLLECTOR_ROLE` can withdraw the fees, with `withdrawFee`. +[Configure the deployment](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter#configure-the-deployment) gave that role to your wallet through `FEE_COLLECTOR`. + +### 1. Check the collected fees + +Store the collected CCIP-BnM fees, then check the adapter's CCIP-BnM balance and whether your wallet holds `FEE_COLLECTOR_ROLE`: + +```bash filename="Terminal" +export COLLECTED_FEES=$(cast call $ADAPTER "collectedFees(address)(uint256)" $ETHEREUM_SEPOLIA_CCIP_BNM \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL | awk '{print $1}') +echo $COLLECTED_FEES +cast call $ETHEREUM_SEPOLIA_CCIP_BNM "balanceOf(address)(uint256)" $ADAPTER --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $ADAPTER "hasRole(bytes32,address)(bool)" $(cast keccak FEE_COLLECTOR_ROLE) $MY_ADDRESS \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +2000000000000000 +2000000000000000 [2e15] +true +``` + +- The adapter collected 0.001 CCIP-BnM from your deposit, before it deposited, and 0.001 CCIP-BnM from this redemption, out of the redeemed CCIP-BnM. +- Its CCIP-BnM balance equals its collected fees, because every other CCIP-BnM it received went into the vault or back to Arbitrum Sepolia. +- `FEE_COLLECTOR_ROLE` is the keccak-256 hash of the string `FEE_COLLECTOR_ROLE`, which `cast keccak` computes. If the output is `false`, run the withdrawal from the account that holds the role. + +### 2. Withdraw the fees + +1. Withdraw all collected fees to your wallet. The arguments are the asset, the recipient, and the amount in the asset's smallest units: + +```bash filename="Terminal" +cast send $ADAPTER "withdrawFee(address,address,uint256)" $ETHEREUM_SEPOLIA_CCIP_BNM $MY_ADDRESS $COLLECTED_FEES \ + --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \ + --account $KEYSTORE_NAME +``` + +Check that the receipt's `status` is `1 (success)`. + +The recipient can be any address other than the zero address, such as a treasury, and you can withdraw part of the fees. An amount of 0 reverts with `AmountIsZero`, an amount above `collectedFees` reverts with `InsufficientFeeBalance`, and a call from an account without `FEE_COLLECTOR_ROLE` reverts with `AccessControlUnauthorizedAccount`. See [`withdrawFee`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#withdrawfee). + +2. Check the collected fees and your CCIP-BnM balance on Ethereum Sepolia: + +```bash filename="Terminal" +cast call $ADAPTER "collectedFees(address)(uint256)" $ETHEREUM_SEPOLIA_CCIP_BNM --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $ETHEREUM_SEPOLIA_CCIP_BNM "balanceOf(address)(uint256)" $MY_ADDRESS --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +Example Output: + +```bash filename="Terminal" +0 +2000000000000000 [2e15] +``` + +The collected fees are back to 0, and your wallet received the 0.002 CCIP-BnM. Your balance is exactly `2000000000000000` only if your wallet held no other CCIP-BnM on Ethereum Sepolia. + +## Review the round trip + +The values below assume that you followed all four tutorials with the amounts shown, starting with no vCCIP-BnM or CCIP-BnM in your wallet. + +### 1. List the four CCIP messages + +The deposit and the redemption each took two CCIP messages: a request that you sent and paid for on Arbitrum Sepolia, and a return leg that the adapter sent and paid for on Ethereum Sepolia. + +| Message | Direction | Sender and receiver | Tokens | CCIP fee paid by | Adapter fee | +| ------------------------ | ------------------------------------ | -------------------------- | ------------------------------- | ------------------------------------ | --------------------------------------------------- | +| 1. Deposit request | Arbitrum Sepolia to Ethereum Sepolia | Your wallet to the adapter | 0.1 CCIP-BnM, with the payload | You, in Arbitrum Sepolia ETH | 0.001 CCIP-BnM, deducted before the deposit | +| 2. Deposit return leg | Ethereum Sepolia to Arbitrum Sepolia | The adapter to your wallet | 0.099 vCCIP-BnM | The adapter, in Ethereum Sepolia ETH | None | +| 3. Redemption request | Arbitrum Sepolia to Ethereum Sepolia | Your wallet to the adapter | 0.1 vCCIP-BnM, with the payload | You, in Arbitrum Sepolia ETH | 0.001 CCIP-BnM, deducted from the redeemed CCIP-BnM | +| 4. Redemption return leg | Ethereum Sepolia to Arbitrum Sepolia | The adapter to your wallet | 0.099 CCIP-BnM | The adapter, in Ethereum Sepolia ETH | None | + +The two tokens cross the lane differently: + +- CCIP-BnM has a Burn & Mint pool on both chains, so messages 1 and 4 burned it on the source chain and minted it on the destination chain. +- vCCIP-BnM has a Lock & Release pool on Ethereum Sepolia and a Burn & Mint pool on Arbitrum Sepolia. Message 2 locked 0.099 vCCIP-BnM in the lockbox and minted it on Arbitrum Sepolia. Message 3 burned 0.1 vCCIP-BnM on Arbitrum Sepolia and released it from the lockbox. + +List your requests to the adapter and the adapter's return legs to you through the CCIP API: + +```bash filename="Terminal" +ccip-cli search messages $MY_ADDRESS --receiver $ADAPTER --limit 2 --no-interactive +ccip-cli search messages $ADAPTER --receiver $MY_ADDRESS --limit 2 --no-interactive +``` + +Your output should look something like this: + +```bash filename="Terminal" +2 message(s) found. +(node:58794) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. +(Use `node --trace-deprecation ...` to show where the warning was created) +┌───────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├───────────┼──────────────────────────────────────────────────────────────────────┤ +│ messageId │ '0xd8f1f245b1884df38538b295def1b0bafe16a116893c3371102e88ad78243662' │ +│ status │ 'SUCCESS' │ +│ source │ 'ethereum-testnet-sepolia [11155111]' │ +│ dest │ 'ethereum-testnet-sepolia-arbitrum-1 [421614]' │ +│ sender │ '0x3f73Cc1547F2606A20535Bd6620622107059a463' │ +│ receiver │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ txHash │ '0xfd83d4a1704797b75ad228810f0c8dd79e8e774bed22cb06d155116c5afeecd5' │ +│ timestamp │ '2026-10-07T15:49:24.000Z' │ +└───────────┴──────────────────────────────────────────────────────────────────────┘ +┌───────────┬──────────────────────────────────────────────────────────────────────┐ +│ (index) │ Values │ +├───────────┼──────────────────────────────────────────────────────────────────────┤ +│ messageId │ '0xe0977e67438c5116084634de44f79db5feb995582b3f7645abc380a82c6dc8c0' │ +│ status │ 'SUCCESS' │ +│ source │ 'ethereum-testnet-sepolia [11155111]' │ +│ dest │ 'ethereum-testnet-sepolia-arbitrum-1 [421614]' │ +│ sender │ '0x3f73Cc1547F2606A20535Bd6620622107059a463' │ +│ receiver │ '0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994' │ +│ txHash │ '0x316d8252f6b40cae8edffa137871975b26df7d4bdf8a26ca1093b35944d352b1' │ +│ timestamp │ '2026-10-07T10:23:00.000Z' │ +└───────────┴──────────────────────────────────────────────────────────────────────┘ + +2 message(s) found. +``` + +Each command lists the newest message first: the first command prints messages 3 and 1, and the second prints messages 4 and 2. All four show the status `SUCCESS`. + +### 2. Reconcile the final balances + +1. Read the two balances that this tutorial has not checked yet: your vCCIP-BnM on Ethereum Sepolia, and the vCCIP-BnM supply on Arbitrum Sepolia: + +```bash filename="Terminal" +cast call $VAULT "balanceOf(address)(uint256)" $MY_ADDRESS --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +cast call $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN "totalSupply()(uint256)" \ + --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL +``` + +```bash filename="Terminal" +900000000000000000 [9e17] +99000000000000000 [9.9e16] +``` + +2. Read the adapter's Ethereum Sepolia ETH balance, in wei: + +```bash filename="Terminal" +cast balance $ADAPTER --rpc-url $ETHEREUM_SEPOLIA_RPC_URL +``` + +The balance is 0.01 Ethereum Sepolia ETH (`10000000000000000` wei) minus the `fee` values of the `MessageSent` events for messages 2 and 4, plus any top-up you sent. + +The following table lists every balance at the end of the series and how it got there: + +| Chain | Balance | Final value | How it got there | +| ---------------- | ------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------ | +| Ethereum Sepolia | Your vCCIP-BnM | 0.9 | 1 from your deposit in Deploy the vault and set up its share token, minus the 0.1 test transfer | +| Ethereum Sepolia | vCCIP-BnM in the lockbox | 0.099 | 0.1 locked by the test transfer, plus 0.099 locked by message 2, minus 0.1 released by message 3 | +| Ethereum Sepolia | Vault total supply and total assets | 0.999 each | 1 from your deposit, plus 0.099 deposited by message 1, minus 0.1 redeemed by message 3 | +| Ethereum Sepolia | Adapter collected fees | 0 | 0.001 each from messages 1 and 3, then withdrawn | +| Ethereum Sepolia | Your CCIP-BnM | 0.002 | The adapter fees you withdrew | +| Arbitrum Sepolia | vCCIP-BnM total supply, all in your wallet | 0.099 | 0.1 from the test transfer, plus 0.099 from message 2, minus 0.1 burned by message 3 | +| Arbitrum Sepolia | Your CCIP-BnM | 0.999 | 1 minted in Deposit from a source chain, minus 0.1 sent in message 1, plus 0.099 from message 4 | + +The vault's 0.999 vCCIP-BnM supply is your 0.9 on Ethereum Sepolia plus the 0.099 in the lockbox, and the lockbox holds exactly the 0.099 vCCIP-BnM that exists on Arbitrum Sepolia. The vault holds 0.999 CCIP-BnM against 0.999 vCCIP-BnM, so shares still convert 1:1. The round trip cost you the CCIP fees of messages 1 and 3, in Arbitrum Sepolia ETH, and 0.002 CCIP-BnM in adapter fees. Because your wallet also acted as the vault team, the adapter fees came back to it through `withdrawFee`. + +## Where to go next + +- [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery): what happens to a request that fails a check, such as the minimum output, and how its tokens come back. +- [What a user sends](/solutions/cross-chain-vault-adapter/overview/how-it-works#what-a-user-sends): the payload checklist for an app that sends requests for its users. +- [Fee functions](/solutions/cross-chain-vault-adapter/reference/adapter-contract#fee-functions): change the adapter fee per chain and token with `setAssetFee`. +- [Limitations](/solutions/cross-chain-vault-adapter/reference/limitations): what to check before you use the adapter with your own vault in production. + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. + +--- + +# Limitations +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter/reference/limitations + +Use this page to check whether your vault and tokens fit the Cross-Chain Vault Adapter as shipped, and to find the conditions that turn requests into failed messages or leave tokens stuck. The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. The vault stays the product and CCIP stays the bridge, so the limits below sit in the adapter, the path that users on other chains take to reach the vault, and in its CCIP settings. For a request that succeeds, see [How it works](/solutions/cross-chain-vault-adapter/overview/how-it-works). For how tokens come back after a failure, see [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery). + +## Vault and token compatibility + +The adapter works with a standard synchronous, single-asset [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) vault. Which tokens need CCIP support depends on the flow you want to offer; see [Requirements](/solutions/cross-chain-vault-adapter#requirements). If your vault's asset is not CCIP-enabled, users can still send CCIP-enabled shares from another chain and receive the redeemed asset on the hub chain, where the asset stays local. Deposits from another chain and redemptions that return the asset to the source chain require a CCIP-enabled asset. + +The cases below need a fork of the adapter or are not supported. + +| Case | What happens | What to do | +| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Asynchronous (request-based) vault | The adapter calls `deposit` and `redeem` and needs the output in the same transaction. A vault that queues requests does not return shares or assets in that call, so the message fails. | Fork the adapter for redemptions. Deposits need no fork when the vault's ERC-4626 `deposit` mints shares synchronously. | +| Multi-asset vault | The adapter routes by comparing the bridged token with the vault's single `asset()`. Any other token reverts `InvalidTargetToken`, and the message fails. | Fork the adapter. | +| Target that is not ERC-4626 | The adapter calls `asset()`, `deposit`, and `redeem` on the payload's target. If the target does not implement them, the call reverts and the message fails. | Fork the adapter. | +| Vault that restricts depositors, or applies per-account limits or cooldowns | The adapter is the depositor, the initial share owner, and the redeemer, so the vault sees one account for every user. Per-account caps apply to all users combined. Allowlisting the adapter in the vault opens the vault to any sender on a configured source chain. | Fork the adapter and add your own sender or beneficiary checks. | +| Fee-on-transfer, rebasing, or other non-standard ERC-20 asset or share token | The adapter assumes a transfer moves exactly the stated amount. Adapter fee accounting and `minimumOut` checks use nominal amounts, so they drift from real balances. | Use standard ERC-20 tokens only. | +| Share token that is not a [CCIP cross-chain token (CCT)](/ccip/concepts/cross-chain-token/overview) | Deposits that ask to return shares to the source chain fail, because the return leg cannot bridge shares, and the whole message becomes a failed message. Users cannot move shares to another chain, so redeeming from another chain is not possible. Deposits with local delivery still work, because they need only the asset to be CCIP-enabled. | Register the share token as a CCT on both chains, typically Lock & Release on the hub chain and Burn & Mint on the other. | +| Hub chain without the Shanghai upgrade | The audited bytecode uses the `PUSH0` opcode, which only Shanghai-or-later chains support. | Deploy only on chains that support `PUSH0`. Recompiling for an older EVM version changes the audited bytecode. | + +To register the share token as a CCT, follow the [Lock & Mint tutorial](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry). Only the vault may mint shares, which is why the hub-chain side typically uses Lock & Release. + +## Fees and accounting + +The adapter fee is set with `setAssetFee` or `setAssetFees` and stored in `assetFees`. For how the adapter charges it, including why a fee change affects messages in flight and why a fee at or above the amount fails the message, see [Fees](/solutions/cross-chain-vault-adapter/overview/how-it-works#fees). The accounting limits below come on top of that. + +- **Vaults that share an asset share fee state.** Redeem-return fees are keyed by the asset, so every vault on one adapter with that asset uses the same redeem-return fee for a destination. `collectedFees` is keyed by the asset too, so their fees pool in one balance. Deposit-return fees are keyed by each vault's share token and stay separate. +- **Only accounted fees can be withdrawn.** [`withdrawFee`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#withdrawfee) moves at most `collectedFees[asset]`. The adapter has no general ERC-20 sweep. +- **Adapter fees do not pay CCIP fees.** The adapter pays each return leg's CCIP fee from its own native gas token balance. Collected adapter fees stay in the asset. Fund the adapter with the native gas token separately. If the balance is below the router's fee, the return reverts `InsufficientNativeBalance` and the whole message fails. +- **`minimumOut` checks nominal amounts.** It compares against the amount the vault returns from `deposit` or `redeem`, after the adapter fee, not against balance changes. See [Slippage protection](/solutions/cross-chain-vault-adapter/overview/how-it-works#slippage-protection). + +## Operational limits + +| Limit | What it means | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| No pause | Turning processing off does not stop CCIP delivery. See [What counts as a failure](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#what-counts-as-a-failure). | +| No retry | The adapter never reprocesses a failed message. Re-enabling a vault, chain, or processing switch does not help messages that already failed. | +| No automatic refund | Someone must call [`refundFailedMessage`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#refundfailedmessage) and pay its CCIP fee, or the local refund address must call [`recoverFailedMessageLocally`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#recoverfailedmessagelocally). | +| No sender allowlist | Any sender on a configured source chain can deposit into or redeem from allowlisted vaults. The adapter checks only the source chain and the target. | +| One token, one vault | A message must carry exactly one token, or it fails with `InvalidTokenCount`. One adapter can allowlist several vaults, and the payload's target picks one per message. | +| Global processing switches | `depositsEnabled` and `redeemsEnabled` apply to every vault on the adapter. | +| Two destinations only | Output goes to a beneficiary on the hub chain or back to the chain the message came from. The adapter cannot route output to a third chain. | +| Native gas token only | The adapter pays CCIP fees for return legs in the hub chain's native gas token, never in LINK. Cross-chain refunds are paid by the caller. | +| Token-only returns | Return legs carry no data and a gas limit of 0, so a beneficiary contract on the source chain receives tokens without a callback. | + +## Configuration risks + +The first four risks concern CCIP 2.0 lane settings. [CCIP 2.0 lanes](/solutions/cross-chain-vault-adapter/overview/how-it-works#ccip-20-lanes) explains each setting. + +- **V3 return format on a CCIP 1.6 lane.** The router rejects every return leg and cross-chain refund to that chain. Set `GENERIC_EXTRA_ARGS_V3_BASIC` with [`setEvmReturnLaneFormat`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#setevmreturnlaneformat) only for CCIP 2.0 lanes; correcting the format unblocks later refunds. +- **Return finality the token pool does not allow.** Return legs and refunds of that token revert. Check the pool's allowed finality before you call [`setEvmReturnRequestedFinality`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#setevmreturnrequestedfinality). +- **Inbound finality or Cross-Chain Verifier (CCV) settings that do not match senders.** CCIP rejects the message before the adapter runs, so the adapter's recovery functions do not apply. Fix [`setInboundFinality`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#setinboundfinality) or [`setCCVsConfig`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#setccvsconfig), then use [manual execution](/ccip/concepts/manual-execution). +- **CCIP 2.0 settings left at their defaults.** The factory applies none of these settings and does not fund the adapter. Until the admin sets them, return legs use `GenericExtraArgsV2`, return finality cannot be requested, the adapter rejects faster-than-finality messages, and an unfunded adapter fails return legs with `InsufficientNativeBalance`. See [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter#fund-the-adapter). +- **The CCIP Router is fixed at deployment.** `ROUTER` is immutable, so moving to a different CCIP Router means deploying and configuring a new adapter. Failed messages on the old adapter stay there until someone refunds or recovers them from it. +- **Refunds use the chain family recorded at failure time.** If the chain type was misconfigured when a message failed, a later `setChainType` fix does not change how that message is refunded, so use local recovery. Setting a chain to `NONE` after a failure does not block its refund. +- **`recoverNative` takes the whole native gas token balance.** [`recoverNative`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#recovernative) has no amount parameter. Afterward, every return leg fails with `InsufficientNativeBalance` until the adapter is funded again. +- **Role changes take effect in one step.** The adapter uses OpenZeppelin [AccessControlEnumerable](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControlEnumerable), so `grantRole`, `revokeRole`, and `renounceRole` need no acceptance step. If the last `DEFAULT_ADMIN_ROLE` holder renounces, no one can change admin-controlled configuration or grant roles again. Fee setters and collectors keep only their own functions. Grant the new admin before you revoke the old one. +- **The factory and the constructor grant roles differently.** Deployed directly, the constructor gives `defaultAdmin` all three roles, in addition to granting `feeSetter` and `feeCollector` their own. Deployed through the factory, each role goes only to its configured address, and the factory renounces its own roles. With separate addresses, a factory-deployed admin cannot set or withdraw adapter fees until it grants itself those roles. See [Role handoff](/solutions/cross-chain-vault-adapter/reference/factory-contract#role-handoff). +- **Changes apply to messages in flight.** Disabling a chain, a vault, or a processing switch fails every message that arrives afterward, including messages sent before the change. A fee change likewise applies to messages sent before it. + +## When funds can get stuck + +In these scenarios, tokens are lost or stranded, or one of the two recovery paths is gone. The payload checklist in [What a user sends](/solutions/cross-chain-vault-adapter/overview/how-it-works#what-a-user-sends) prevents the first two. + +| Scenario | Why | Prevention | +| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| No local refund address, and a cross-chain refund is not possible | Local recovery needs `localRefundAddress` in the payload when the message is sent. It cannot be added later, and a payload that is not exactly 128 bytes leaves it zero. A cross-chain refund fails if, for example, the router no longer supports the return lane or the token has no pool for it. | Always set `localRefundAddress` to an address the user controls on the hub chain. | +| Malformed payload | A hand-built payload can make the message fail inside CCIP with no recovery path, as described in [Failures with no recovery path](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#failures-with-no-recovery-path). | Build the payload with a standard ABI encoder, such as Solidity `abi.encode`, ethers, viem, or cast. | +| Refund recipient is a contract that cannot use the returned tokens | A cross-chain refund always goes to the original `message.sender` on the source chain, not to the beneficiary or the local refund address. | Send from an account or contract that can move refunded tokens, or set `localRefundAddress`. | +| Tokens sent to the adapter outside a normal request | An ERC-20 transfer straight to the adapter, or a CCIP message with empty data and a gas limit of 0, delivers tokens without calling `ccipReceive`. The adapter records nothing, `withdrawFee` withdraws only accounted fees, and there is no ERC-20 sweep. | Send tokens only in CCIP messages with a 128-byte payload and a non-zero gas limit. `recoverNative` recovers the native gas token. | +| Admin role renounced while configuration is broken | No one can fix settings such as a wrong return lane format, so cross-chain refunds that depend on them stay blocked. Only local recovery remains, and only for messages that set a local refund address. | Verify the configuration with test messages before any admin change, and keep at least one `DEFAULT_ADMIN_ROLE` holder. | + +## Customizing the adapter + +If your vault does not fit, fork the adapter rather than modifying the vault. See [Use it as-is or customize it](/solutions/cross-chain-vault-adapter#use-it-as-is-or-customize-it) for when to fork. + +- **There are no hooks to override.** `processMessage` and `_processTarget` are not `virtual`. Edit them in your copy of [`CrossChainERC4626Adapter.sol`](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/src/ccip/CrossChainERC4626Adapter.sol). +- **Keep the safety checks.** Keep the router check in `ccipReceive`, the self-call that isolates `processMessage` in a `try`/`catch`, failed-message storage with both recovery paths, and the beneficiary address checks. The failure handler, the `catch` path in `ccipReceive`, must never revert on any input, or tokens can be stranded as in the malformed payload case above. +- **Keep the compiler settings.** The audited build uses solc 0.8.24, `via_ir`, `optimizer_runs = 1`, and `evm_version = cancun`, as pinned in [`foundry.toml`](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/foundry.toml). +- **A fork is not the audited code.** The audit covers the contracts in `src/ccip` as published. Any change to the source or the compiler settings changes the bytecode, so get your fork its own security review before you deploy it. + +## Versions covered + +This page describes the following versions. The contracts in `src/ccip` are audited. + +| Component | Version | +| --------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `CrossChainERC4626Adapter` | 1.0.0 (`typeAndVersion` is `CrossChainERC4626Adapter 1.0.0`) | +| `CrossChainERC4626AdapterFactory` | 1.0.0 (`typeAndVersion` is `CrossChainERC4626AdapterFactory 1.0.0`) | +| `@chainlink/contracts-ccip` | 2.0.0 | +| `@chainlink/contracts` | 1.5.0 (`ITypeAndVersion` only) | +| OpenZeppelin Contracts | 5.0.2 (`AccessControlEnumerable`, `IERC4626`, `ReentrancyGuard`) and 4.8.3 (`IERC20`, `SafeERC20`) | +| Compiler | solc 0.8.24, `via_ir`, `optimizer_runs = 1`, `evm_version = cancun` (requires `PUSH0`) | +| CCIP lanes | CCIP 1.6 lanes (`GenericExtraArgsV2` return legs) and CCIP 2.0 lanes (`GenericExtraArgsV3` return legs) | +| Hub chains | EVM chains with the Shanghai upgrade or later | + +For exact signatures, events, and errors, continue to [Adapter contract](/solutions/cross-chain-vault-adapter/reference/adapter-contract) and [Factory contract](/solutions/cross-chain-vault-adapter/reference/factory-contract). + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. + +--- + +# Adapter contract +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter/reference/adapter-contract + +Use this reference to check the exact signatures, access rules, revert conditions, and events of `CrossChainERC4626Adapter`. The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. The adapter receives CCIP token transfers on the hub chain, deposits the asset into or redeems shares from an allowlisted ERC-4626 vault, and delivers the output on the hub chain or bridges it back to the source chain. For the same request explained step by step, read [How it works](/solutions/cross-chain-vault-adapter/overview/how-it-works) and [Failures and recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery). + +This contract provides: + +- a CCIP receiver that runs the vault interaction in an isolated self-call and records failures instead of reverting +- deposit and redeem routing, selected by the token the message carries +- local delivery on the hub chain or return to the source chain, with a flat adapter fee on returns +- cross-chain refund and local recovery for failed messages +- per-source-chain [Cross-Chain Verifier (CCV)](/ccip/concepts/ccvs/overview) and finality settings for CCIP 2.0 lanes +- configuration split across `DEFAULT_ADMIN_ROLE`, `FEE_SETTER_ROLE`, and `FEE_COLLECTOR_ROLE` + +> **NOTE: Who calls the adapter** +> +> The vault team deploys the adapter through `CrossChainERC4626AdapterFactory` on the hub chain. The CCIP Router calls +> `ccipReceive` to deliver each message. + +## Usage boundary + +- Only the CCIP Router set at construction (`ROUTER`) can call `ccipReceive`. Any other caller gets `InvalidRouter`. +- `processMessage` is `external` only so that `ccipReceive` can wrap it in `try`/`catch`. Any caller other than the adapter itself gets `OnlySelf`. +- The CCIP OffRamp calls `supportsInterface` to confirm the adapter is a CCIP receiver. On CCIP 2.0 lanes it also reads `getCCVsAndFinalityConfig` before it delivers a message. +- Users do not call the adapter to deposit or redeem. They send a CCIP message from the source chain to the adapter's address, with the payload described in [Message format](#message-format). +- Users and integrators call the [integrator views](#integrator-views) and the two [recovery functions](#recovery-functions). Anyone can call `refundFailedMessage`. Only the local refund address stored for a message can call `recoverFailedMessageLocally`. +- Role holders call the [configuration functions](#configuration-functions) and the [fee functions](#fee-functions). +- Deploy the adapter through the [factory](/solutions/cross-chain-vault-adapter/reference/factory-contract) instead of calling the constructor directly. See [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter). +- The message-processing logic has no `virtual` hooks. To change it, fork the contract and edit `processMessage` and its private helpers. See [Use it as-is or customize it](/solutions/cross-chain-vault-adapter#use-it-as-is-or-customize-it). + +## Contract + +Source: [`src/ccip/CrossChainERC4626Adapter.sol`](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/src/ccip/CrossChainERC4626Adapter.sol) + +| Property | Value | +| ---------------- | ----------------------------------------------------------------------- | +| Contract | `CrossChainERC4626Adapter` | +| `typeAndVersion` | `CrossChainERC4626Adapter 1.0.0` | +| License | MIT | +| Audit status | Audited | +| Compiler | solc 0.8.24, `via_ir` enabled, optimizer runs `1`, EVM version `cancun` | +| Minimum EVM | Shanghai upgrade (`PUSH0`) | + +The audited bytecode is identical to a shanghai build, and `PUSH0` is the only opcode it uses that pre-Shanghai chains lack. Deploy the adapter only on chains that support the Shanghai upgrade. Recompiling with other settings produces bytecode that differs from the audited build. The pinned settings are in [`foundry.toml`](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/foundry.toml). + +The contract imports these dependencies: + +| Package | Version | Imports | +| --------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------- | +| `@chainlink/contracts-ccip` | 2.0.0 | `Client`, `ExtraArgsCodec`, `FinalityCodec`, `IRouterClient`, `IAny2EVMMessageReceiver`, `IAny2EVMMessageReceiverV2` | +| `@chainlink/contracts` | 1.5.0 | `ITypeAndVersion` | +| OpenZeppelin Contracts | 5.0.2 | `AccessControlEnumerable`, `ReentrancyGuard`, `IERC4626` | +| OpenZeppelin Contracts | 4.8.3 | `IERC20`, `SafeERC20` | + +## Inheritance + +- [`IAny2EVMMessageReceiverV2`](/ccip/evm/api-reference/v2.0.0/i-any2-evm-message-receiver-v2): `ccipReceive` and `getCCVsAndFinalityConfig` +- `AccessControlEnumerable` (OpenZeppelin 5.0.2): roles and role enumeration. See [Roles](#roles). +- `ReentrancyGuard` (OpenZeppelin 5.0.2): the `nonReentrant` modifier on `refundFailedMessage`, `recoverFailedMessageLocally`, `withdrawFee`, and `recoverNative` +- [`ITypeAndVersion`](/ccip/evm/api-reference/v2.0.0/i-type-and-version): `typeAndVersion` + +The adapter does not inherit CCIP's `CCIPReceiver` base contract. It implements the receiver interface and its own router check. + +## Constructor + +A direct deployment runs this constructor: + +```solidity +constructor(address router_, address defaultAdmin, address feeSetter, address feeCollector) +``` + +| Parameter | Type | Description | +| -------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `router_` | `address` | CCIP Router on the hub chain, stored as the immutable `ROUTER`. Get the address from the CCIP Directory for [testnet](/ccip/directory/testnet) or [mainnet](/ccip/directory/mainnet). | +| `defaultAdmin` | `address` | Receives `DEFAULT_ADMIN_ROLE`, `FEE_SETTER_ROLE`, and `FEE_COLLECTOR_ROLE`. | +| `feeSetter` | `address` | Receives `FEE_SETTER_ROLE`. | +| `feeCollector` | `address` | Receives `FEE_COLLECTOR_ROLE`. | + +**Reverts with:** + +- `InvalidRouter(address(0))` when `router_` is the zero address. +- `InvalidAdmin()` when `defaultAdmin` is the zero address. +- `InvalidFeeSetter()` when `feeSetter` is the zero address. +- `InvalidFeeCollector()` when `feeCollector` is the zero address. + +**Emits:** + +- `RoleGranted` for each new role grant: three for `defaultAdmin`, plus one each for `feeSetter` and `feeCollector` when they differ from `defaultAdmin`. + +The constructor checks only that `router_` is non-zero. It does not verify that the address is a CCIP Router, and `ROUTER` cannot change after deployment. A wrong router means deploying a new adapter. + +A new adapter starts with deposits and redeems disabled, no chain configured, no vault allowlisted, no adapter fee, no CCV or inbound finality settings, the `UNSET` (legacy) return format on every lane, and no native gas token balance. + +Deploy through the [factory](/solutions/cross-chain-vault-adapter/reference/factory-contract) instead. It sets chain types, the vault allowlist entry, the deposit and redeem switches, and adapter fees in the same transaction, then grants each role only to its configured address. Through the factory, `defaultAdmin` receives the fee roles only if you also set it as `feeSetter` or `feeCollector`. See [Role handoff](/solutions/cross-chain-vault-adapter/reference/factory-contract#role-handoff). + +## Roles + +The adapter defines two roles and inherits `DEFAULT_ADMIN_ROLE`: + +| Role | Constant | Value | Granted by | Can call | +| ------------- | -------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Admin | `DEFAULT_ADMIN_ROLE` | `0x00` | Constructor (`defaultAdmin`), factory (`defaultAdmin`), or an existing admin | `setChainType`, `setTargetEnabled`, `setProcessingEnabled`, `setCCVsConfig`, `setInboundFinality`, `setEvmReturnLaneFormat`, `setEvmReturnRequestedFinality`, `recoverNative`, and `grantRole` and `revokeRole` for all three roles | +| Fee setter | `FEE_SETTER_ROLE` | `keccak256("FEE_SETTER_ROLE")` | Constructor (`feeSetter` and `defaultAdmin`), factory (`feeSetter` only), or an admin | `setAssetFee`, `setAssetFees` | +| Fee collector | `FEE_COLLECTOR_ROLE` | `keccak256("FEE_COLLECTOR_ROLE")` | Constructor (`feeCollector` and `defaultAdmin`), factory (`feeCollector` only), or an admin | `withdrawFee` | + +The adapter inherits role management from OpenZeppelin [`AccessControlEnumerable`](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControlEnumerable): `grantRole`, `revokeRole`, `renounceRole`, `hasRole`, `getRoleAdmin`, `getRoleMember`, and `getRoleMemberCount`. The adapter never changes role admins, so `DEFAULT_ADMIN_ROLE` is the admin of all three roles. Use `getRoleMemberCount` and `getRoleMember` to list the current holders of a role. + +Role changes take effect in a single step. There is no pending-admin acceptance and no delay, so granting `DEFAULT_ADMIN_ROLE` to a wrong address hands over control in that transaction. `renounceRole(role, callerConfirmation)` requires `callerConfirmation` to equal the caller. If the last holder of `DEFAULT_ADMIN_ROLE` renounces it or has it revoked, no account can call the admin functions or change roles again. Fee setters and fee collectors keep their roles. + +## Message format + +Every message sent to the adapter must carry exactly one token and a `data` field that decodes to `Payload`. For how a user builds the full CCIP message on the source chain, see [What a user sends](/solutions/cross-chain-vault-adapter/overview/how-it-works#what-a-user-sends). + +The adapter decodes `message.data` into this struct, shown here without its source comments: + +```solidity +struct Payload { + address target; + bytes32 beneficiary; + uint256 minimumOut; + uint256 deliveryAndRefund; +} +``` + +| Field | Type | Description | +| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `target` | `address` | Vault to call. Must be allowlisted with [`setTargetEnabled`](#settargetenabled), or processing fails with `InvalidTarget`. | +| `beneficiary` | `bytes32` | Recipient of the output: on the hub chain for local delivery, on the source chain for a return. For an EVM address, left-pad it to 32 bytes; if the upper 96 bits are not zero, processing fails with `InvalidEVMAddress`. | +| `minimumOut` | `uint256` | Smallest acceptable output after the adapter fee, in output units: shares for a deposit, asset for a redemption. Below it, processing fails with `MinimumOutputNotMet`. `0` disables the check, but a zero output still fails with `NoOutputReceived`. | +| `deliveryAndRefund` | `uint256` | Delivery choice and local refund address, packed into one word as shown in the next table. | + +`deliveryAndRefund` uses this bit layout: + +| Bits | Field | Meaning | +| ---------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | `returnToSourceChain` | `1` bridges the output back to `message.sourceChainSelector`. `0` delivers it to `beneficiary` on the hub chain. | +| 1 to 160 | `localRefundAddress` | Hub-chain address that can call [`recoverFailedMessageLocally`](#recoverfailedmessagelocally) if processing fails. `address(0)` disables local recovery. | +| 161 to 255 | Unused | Ignored on decode and not validated. | + +The adapter enforces these rules on every message: + +- `message.destTokenAmounts` must hold exactly one entry. Otherwise processing fails with `InvalidTokenCount`. +- `message.data` must be exactly `CCIP_MESSAGE_PAYLOAD_LENGTH` (128) bytes, the length of `abi.encode` over the four static fields. Otherwise processing fails with `InvalidPayloadLength`. +- The token selects the operation. The vault's `asset()` means deposit. The vault address, which is also the share token, means redeem. Any other token fails with `InvalidTargetToken`. + +This Solidity snippet builds `message.data` the same way as the `_encodePayload` and `_packDeliveryAndRefund` helpers in the repository's [adapter tests](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/test/ccip/CrossChainERC4626Adapter.t.sol): + +```solidity +uint256 deliveryAndRefund = (uint256(uint160(localRefundAddress)) << 1) | (returnToSourceChain ? 1 : 0); + +bytes memory data = abi.encode( + vault, // target + bytes32(uint256(uint160(beneficiary))), // beneficiary, left-padded EVM address + minimumOut, + deliveryAndRefund +); +``` + +In TypeScript, encode the same four values as the ABI tuple `(address, bytes32, uint256, uint256)` with viem or ethers. + +> **CAUTION: Validate the payload before you send** +> +> Check every message against the payload checklist in [What a user +> sends](/solutions/cross-chain-vault-adapter/overview/how-it-works#what-a-user-sends), including a non-zero +> `localRefundAddress`, which cannot be added after the message is sent. A malformed payload can strand funds +> permanently, as explained in [Failures with no recovery +> path](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#failures-with-no-recovery-path). + +## Types + +All types below are declared inside `CrossChainERC4626Adapter`. Messages and token amounts use `Client.Any2EVMMessage` and `Client.EVMTokenAmount` from the CCIP [`Client`](/ccip/evm/api-reference/v2.0.0/client) library. + +### ChainType + +The adapter records a chain family for each remote chain selector with this enum: + +```solidity +enum ChainType { + NONE, + EVM, + SVM +} +``` + +| Value | Meaning | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `NONE` (0) | Selector not configured. Messages from it fail with `InvalidChain`, and `preview` reverts for it when the amount is not zero. | +| `EVM` (1) | EVM chain family. Return legs and refunds to this selector use an EVM receiver encoding and the lane's EVM `extraArgs` format. | +| `SVM` (2) | Solana Virtual Machine chain family. | + +### EvmReturnExtraArgsFormat + +The adapter encodes `extraArgs` for return legs and refunds to an EVM chain according to this enum, set per destination with [`setEvmReturnLaneFormat`](#setevmreturnlaneformat): + +```solidity +enum EvmReturnExtraArgsFormat { + UNSET, + LEGACY_EXTRA_ARGS_V2, + GENERIC_EXTRA_ARGS_V3_BASIC +} +``` + +| Value | Encoding | Use on | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| `UNSET` (0) | Same as `LEGACY_EXTRA_ARGS_V2`. This is the default and cannot be written with `setEvmReturnLaneFormat`. | CCIP 1.6 lanes | +| `LEGACY_EXTRA_ARGS_V2` (1) | `Client.GenericExtraArgsV2` with `gasLimit` 0 and `allowOutOfOrderExecution` true | CCIP 1.6 lanes | +| `GENERIC_EXTRA_ARGS_V3_BASIC` (2) | `ExtraArgsCodec._getBasicEncodedExtraArgsV3(0, requestedFinality)`, with the finality from `evmReturnRequestedFinality`. See [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec). | CCIP 2.0 lanes | + +### ErrorCode + +The adapter tracks the processing outcome of each inbound message ID with this enum: + +```solidity +enum ErrorCode { + NONE, + BASIC, + RESOLVED +} +``` + +| Value | Meaning | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `NONE` (0) | Not received yet, or processed successfully. A `MessageSucceeded` event for the message ID tells the two apart. | +| `BASIC` (1) | Processing failed. The adapter holds the inbound tokens, and a cross-chain refund or local recovery is available. | +| `RESOLVED` (2) | A failed message was refunded cross-chain or recovered locally. A `MessageRefunded` or `MessageRecoveredLocally` event for the message ID tells which. | + +### Payload + +`Payload` is the struct encoded in `message.data`. See [Message format](#message-format). + +### FailedMessageRecord + +When processing fails, the adapter stores this record for the message ID: + +```solidity +struct FailedMessageRecord { + uint64 sourceChainSelector; + bytes sender; + Client.EVMTokenAmount[] destTokenAmounts; + address localRefundAddress; +} +``` + +| Field | Type | Description | +| --------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `sourceChainSelector` | `uint64` | Chain the message came from. Cross-chain refunds go back to this chain. | +| `sender` | `bytes` | Original `message.sender`. CCIP encodes an EVM sender as 32 bytes. Cross-chain refunds go to this address. | +| `destTokenAmounts` | `Client.EVMTokenAmount[]` | Tokens and amounts the adapter received and now holds. | +| `localRefundAddress` | `address` | Decoded from `deliveryAndRefund` when the payload is exactly 128 bytes, otherwise `address(0)`. The only address that can call `recoverFailedMessageLocally`. | + +The record does not include `message.data`. Read it with [`getFailedMessageRecord`](#getfailedmessagerecord). + +### CCVConfig + +The adapter stores its CCIP 2.0 verifier policy per source chain in this struct: + +```solidity +struct CCVConfig { + address[] requiredCCVs; + address[] optionalCCVs; + uint8 optionalThreshold; +} +``` + +| Field | Type | Description | +| ------------------- | ----------- | ------------------------------------------------------------------------------------------ | +| `requiredCCVs` | `address[]` | CCVs that must all attest to a message. `address(0)` stands for the lane's default CCVs. | +| `optionalCCVs` | `address[]` | CCVs from which at least `optionalThreshold` must attest. | +| `optionalThreshold` | `uint8` | Number of optional CCVs that must attest. Greater than 0 when `optionalCCVs` is non-empty. | + +Set it with [`setCCVsConfig`](#setccvsconfig) and read it with [`getCCVsAndFinalityConfig`](#getccvsandfinalityconfig). + +## State variables + +Every public state variable has a getter with the same name. The adapter declares these: + +| Getter | Returns | Description | +| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ROUTER()` | `address` | Immutable CCIP Router. The only caller allowed into `ccipReceive`, and the router used for `getFee` and `ccipSend` on return legs and refunds. | +| `typeAndVersion()` | `string` | Constant `"CrossChainERC4626Adapter 1.0.0"`. | +| `DEFAULT_ADMIN_ROLE()` | `bytes32` | Inherited constant `0x00`. | +| `FEE_SETTER_ROLE()` | `bytes32` | Constant `keccak256("FEE_SETTER_ROLE")`. | +| `FEE_COLLECTOR_ROLE()` | `bytes32` | Constant `keccak256("FEE_COLLECTOR_ROLE")`. | +| `CCIP_MESSAGE_PAYLOAD_LENGTH()` | `uint256` | Constant `128`. Required length of `message.data`. | +| `depositsEnabled()` | `bool` | Whether deposits are processed, for every vault on the adapter. | +| `redeemsEnabled()` | `bool` | Whether redemptions are processed, for every vault on the adapter. | +| `inboundFinality(uint64 sourceChainSelector)` | `bytes4` | Allowed finality for messages from the source chain on CCIP 2.0 lanes. Default `0x00000000` accepts only messages that wait for finality. | +| `chains(uint64 chainSelector)` | `ChainType` | Chain family of a remote selector. `NONE` means not configured. | +| `enabledTargets(address target)` | `bool` | Whether a vault is allowlisted. | +| `assetFees(uint64 destinationChainSelector, address bridgedToken)` | `uint256` | Adapter fee in the asset's smallest units, keyed by the return chain and the token bridged back. | +| `collectedFees(address asset)` | `uint256` | Adapter fees accrued and not yet withdrawn. | +| `refundChainFamilySnapshot(bytes32 messageId)` | `ChainType` | Chain family recorded when a message failed. Cross-chain refunds use it instead of the current `chains` value. Deleted when the message resolves. | +| `messageErrorCode(bytes32 messageId)` | `ErrorCode` | Processing outcome of an inbound message. | +| `evmReturnExtraArgsFormat(uint64 destinationChainSelector)` | `EvmReturnExtraArgsFormat` | Return and refund `extraArgs` format for a destination chain. | +| `evmReturnRequestedFinality(uint64 destinationChainSelector, address token)` | `bytes4` | Requested finality for return legs and refunds of `token` to a destination, used only on `GENERIC_EXTRA_ARGS_V3_BASIC` lanes. | + +`ccvConfigs` and `failedMessageRecords` are `internal`. Read them through [`getCCVsAndFinalityConfig`](#getccvsandfinalityconfig) and [`getFailedMessageRecord`](#getfailedmessagerecord). + +The adapter accepts the native gas token through `receive() external payable`. That balance pays the CCIP fees for return legs. The admin withdraws it with [`recoverNative`](#recovernative). + +## Receiver functions + +### ccipReceive + +Receives a message from the CCIP Router and processes it in an isolated self-call. + +```solidity +function ccipReceive(Client.Any2EVMMessage calldata message) external onlyRouter +``` + +> Calls `this.processMessage(message)` inside `try`/`catch`. +> +> - On success, emits `MessageSucceeded`. `messageErrorCode` stays `NONE`. +> - On a revert, sets `messageErrorCode` to `BASIC`, stores a [`FailedMessageRecord`](#failedmessagerecord), records the source chain's family in `refundChainFamilySnapshot`, and emits `MessageFailed`. If `chains` has no entry for the source chain, the family is inferred from the sender's format. The one exception, a malformed `target` word, is listed under **Reverts with**. +> +> When processing fails, every effect of `processMessage` rolls back: the vault call, the fee, and any outbound send. The adapter keeps the inbound tokens. CCIP execution still succeeds, so the [CCIP Explorer](https://ccip.chain.link) shows the message as Success. The `catch` is bare, so the adapter does not store or emit the revert reason. Nothing retries the message, and re-enabling a vault or chain does not reprocess it. See [Why the CCIP Explorer shows Success](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#why-the-ccip-explorer-shows-success). + +**Access:** `ROUTER` only. + +**Parameters:** + +| Parameter | Type | Description | +| --------- | -------------------------------- | --------------------------------------------------------------------------------------- | +| `message` | `Client.Any2EVMMessage calldata` | Inbound message: message ID, source chain selector, sender, data, and delivered tokens. | + +**Returns:** none. + +**Reverts with:** + +- `InvalidRouter(msg.sender)` when the caller is not `ROUTER`. +- Out of gas when the gas limit set on the source chain does not cover the call. CCIP marks the message failed. Recover it with [manual execution](/ccip/concepts/manual-execution) at a higher gas limit. +- A decode revert in the `catch` block when a 128-byte payload has a `target` word with non-zero upper 12 bytes. The message can never execute. See [Failures with no recovery path](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#failures-with-no-recovery-path). + +When `ccipReceive` reverts, CCIP reverts the token transfer and marks the message failed at the CCIP level. See [Failures the adapter can't catch](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#failures-the-adapter-cant-catch). + +**Emits:** + +- `MessageSucceeded(messageId)` after the events from a successful `processMessage`. +- `MessageFailed(messageId)` when `processMessage` reverts. + +*** + +### processMessage + +Validates an inbound message, calls the vault, and delivers the output. + +```solidity +function processMessage(Client.Any2EVMMessage calldata message) + external + onlySelf + onlyValidChain(message.sourceChainSelector) +``` + +> Runs in this order: +> +> 1. Requires the caller to be the adapter, and `chains[message.sourceChainSelector]` to be configured (not `NONE`). +> 2. Requires exactly one entry in `message.destTokenAmounts`. +> 3. Requires `message.data` to be exactly 128 bytes, then decodes it as `Payload`. +> 4. Requires `enabledTargets[payload.target]`. +> 5. Requires a non-zero token amount. +> 6. Reads `asset()` from the vault and routes by token: +> - **Asset (deposit):** requires `depositsEnabled`. When returning to the source chain, subtracts the adapter fee from the input. Calls `deposit(assets, address(this))` with the remaining amount. +> - **Share token (redeem):** requires `redeemsEnabled`. Calls `redeem(shares, address(this), address(this))`. When returning to the source chain, subtracts the adapter fee from the redeemed assets. +> 7. Requires a non-zero output of at least `minimumOut`, then emits `TargetProcessed`. +> 8. Delivers the output. With `returnToSourceChain` set, it sends a CCIP message to `beneficiary` on `message.sourceChainSelector`. Otherwise it transfers the output to `beneficiary` on the hub chain and emits `LocalTokenDelivered`. +> +> The vault sees the adapter as the caller, receiver, and owner. The adapter fee is read from `assetFees[message.sourceChainSelector][bridgedToken]` when the message executes. `bridgedToken` is the share token for a deposit return and the asset for a redemption return. Fees accrue in `collectedFees[asset]`. Local delivery pays no adapter fee. + +The adapter pays each return leg's CCIP fee from its own native gas token balance. A return leg is a token-only CCIP message, and for an EVM destination it has these fields: + +| Field | Value | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `receiver` | `abi.encode(beneficiary)` | +| `data` | Empty | +| `tokenAmounts` | One entry: the output token and the output amount after the adapter fee | +| `extraArgs` | Built from the adapter's configuration, never from message data. `GenericExtraArgsV3` with gas limit 0 and `evmReturnRequestedFinality[destination][token]` on a `GENERIC_EXTRA_ARGS_V3_BASIC` lane; otherwise `GenericExtraArgsV2` with gas limit 0 and out-of-order execution allowed. | +| `feeToken` | `address(0)`, so the CCIP fee is paid in the native gas token, never LINK | + +**Access:** the adapter itself (`onlySelf`). + +**Parameters:** + +| Parameter | Type | Description | +| --------- | -------------------------------- | ---------------------------------------------- | +| `message` | `Client.Any2EVMMessage calldata` | Inbound message, forwarded from `ccipReceive`. | + +**Returns:** none. + +**Reverts with** (when called from `ccipReceive`, every revert below except `OnlySelf` and the decode revert becomes a failed message): + +- `OnlySelf()` when any address other than the adapter calls it. +- `InvalidChain(sourceChainSelector)` when the source chain is not configured. +- `InvalidTokenCount(tokenCount)` when the message does not carry exactly one token. +- `InvalidPayloadLength(length, 128)` when `message.data` is not 128 bytes. +- A decode revert without error data when the payload is 128 bytes but `target` does not fit in 160 bits. This case does not become a failed message. See [`ccipReceive`](#ccipreceive) and [Failures with no recovery path](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#failures-with-no-recovery-path). +- `InvalidTarget(target)` when the vault is not allowlisted. +- `AmountIsZero()` when the token amount is zero. +- `DepositsDisabled()` or `RedeemsDisabled()` when that path is switched off. +- `InvalidTargetToken(target, token)` when the token is neither the vault's asset nor its share token. +- `FeeExceedsAmount(amount, fee)` when the adapter fee is greater than or equal to the deposit input or the redeemed assets. +- `NoOutputReceived()` when the vault returns zero shares or zero assets. +- `MinimumOutputNotMet(minimumOut, actualOut)` when the output after the fee is below `minimumOut`. +- `InvalidEVMAddress(beneficiary)` when the upper 96 bits of `beneficiary` are not zero. +- `InsufficientNativeBalance(requiredFee, availableBalance)` when the adapter's native gas token balance is below the router fee for the return leg. +- Errors from the vault (for example, a deposit limit), the tokens, the CCIP Router, or a token pool (for example, a rate limit on the return lane). + +**Emits:** + +- `TargetProcessed` after the vault call. +- `LocalTokenDelivered` for local delivery, or `MessageSent` for a return leg. + +These events roll back with the rest of `processMessage` when a later step reverts. + +*** + +### getCCVsAndFinalityConfig + +Returns the verifier and finality policy that CCIP 2.0 OffRamps apply to messages from a source chain. + +```solidity +function getCCVsAndFinalityConfig(uint64 sourceChainSelector, bytes calldata) + external + view + virtual + returns ( + address[] memory requiredCCVs, + address[] memory optionalCCVs, + uint8 optionalThreshold, + bytes4 allowedFinalityConfig + ) +``` + +> Returns the stored `CCVConfig` and `inboundFinality` for `sourceChainSelector`. The second parameter, the sender, is unnamed and ignored, so every sender on a source chain gets the same policy. +> +> On CCIP 2.0 lanes the OffRamp calls this function before `ccipReceive`. If a message lacks the required CCV attestations, or requests a finality that `allowedFinalityConfig` does not allow, the message fails at the CCIP level and the adapter never sees it. CCIP 1.6 lanes do not call this function. +> +> Empty CCV lists with a zero threshold make the OffRamp use the lane's default CCVs. An `allowedFinalityConfig` of `0x00000000`, the default, accepts only messages that wait for finality. + +**Access:** anyone (view). + +**Parameters:** + +| Parameter | Type | Description | +| --------------------- | ---------------- | ------------------------------------ | +| `sourceChainSelector` | `uint64` | Source chain of the inbound message. | +| (unnamed) | `bytes calldata` | Sender on the source chain. Ignored. | + +**Returns:** + +| Name | Type | Description | +| ----------------------- | ------------------ | ---------------------------------------------------------------------- | +| `requiredCCVs` | `address[] memory` | CCVs that must all attest. | +| `optionalCCVs` | `address[] memory` | Optional CCVs. | +| `optionalThreshold` | `uint8` | Number of optional CCVs that must attest. | +| `allowedFinalityConfig` | `bytes4` | `inboundFinality[sourceChainSelector]`, in the `FinalityCodec` layout. | + +**Reverts with:** none. + +**Emits:** none. + +*** + +### supportsInterface + +Reports which interfaces the adapter implements, per ERC-165. + +```solidity +function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControlEnumerable) + returns (bool supported) +``` + +> Returns `true` for `IAny2EVMMessageReceiver`, `IAny2EVMMessageReceiverV2`, `IAccessControlEnumerable`, `IAccessControl`, and `IERC165`. The CCIP OffRamp uses this check to decide whether to call `ccipReceive`, and on CCIP 2.0 lanes whether to read `getCCVsAndFinalityConfig`. + +**Access:** anyone (view). + +**Parameters:** + +| Parameter | Type | Description | +| ------------- | -------- | ----------------------------- | +| `interfaceId` | `bytes4` | ERC-165 interface identifier. | + +**Returns:** + +| Name | Type | Description | +| ----------- | ------ | --------------------------------------------- | +| `supported` | `bool` | `true` if the adapter supports the interface. | + +**Reverts with:** none. + +**Emits:** none. + +## Integrator views + +### preview + +Simulates a deposit or redemption, including the adapter fee, so a UI can show the expected output. + +```solidity +function preview( + address token, + address vaultTarget, + uint256 amount, + bool returnToSourceChain, + uint64 assetFeeDestinationChainSelector +) external view returns (uint256 received) +``` + +> Call it on the hub chain with the source chain's selector. It follows the routing and fee rules of `processMessage` and calls the vault's `previewDeposit` or `previewRedeem`. +> +> - Returns `0` instead of reverting when `amount` is `0`, when the adapter fee is greater than or equal to the amount (deposit) or the redeemed assets (redemption), or when the vault preview returns `0`. +> - Requires `assetFeeDestinationChainSelector` to be configured in `chains`, even when `returnToSourceChain` is `false`. +> - Does not apply `minimumOut`. Vault state and the adapter fee can change before the message executes, so derive `minimumOut` from this value with a tolerance. See [Slippage protection](/solutions/cross-chain-vault-adapter/overview/how-it-works#slippage-protection). +> - Does not catch vault reverts. If `asset()`, `previewDeposit`, or `previewRedeem` reverts, `preview` reverts. + +**Access:** anyone (view). + +**Parameters:** + +| Parameter | Type | Description | +| ---------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `token` | `address` | Inbound token: the vault's asset for a deposit, or the vault (share token) address for a redemption. | +| `vaultTarget` | `address` | Allowlisted vault, as in `Payload.target`. | +| `amount` | `uint256` | Inbound amount, in `token` units. | +| `returnToSourceChain` | `bool` | `true` applies the adapter fee for a return to the source chain. `false` simulates local delivery, with no adapter fee. | +| `assetFeeDestinationChainSelector` | `uint64` | Source chain selector of the message. Must be configured in `chains`. Also keys `assetFees` when `returnToSourceChain` is `true`. | + +**Returns:** + +| Name | Type | Description | +| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------ | +| `received` | `uint256` | Expected output after the adapter fee: shares for a deposit, asset for a redemption. `0` when the request is not viable. | + +**Reverts with:** + +- `InvalidTarget(vaultTarget)` when the vault is not allowlisted. This check runs before the zero-amount check. +- `InvalidChain(assetFeeDestinationChainSelector)` when the selector is not configured and `amount` is not zero. +- `DepositsDisabled()` or `RedeemsDisabled()` when that path is switched off. +- `InvalidTargetToken(vaultTarget, token)` when `token` is neither the asset nor the share token. +- Errors from the vault's `asset()`, `previewDeposit`, or `previewRedeem`. + +**Emits:** none. + +*** + +### checkRefundEligibility + +Reports whether a failed message can be refunded cross-chain, and estimates the fee. + +```solidity +function checkRefundEligibility(bytes32 messageId) + external + view + returns (bool canRefund, bytes32 originalSender, address token, uint256 tokenAmount, uint256 requiredFee) +``` + +> Returns `canRefund = false` with zeroed fields when the message is not `BASIC`, when the stored sender is not exactly 32 bytes, or when no inbound token amount is positive. Otherwise it returns `canRefund = true`, the original sender, the first token with a non-zero amount, and `requiredFee`: the sum of one router `getFee` quote per non-zero token. +> +> This view can revert. After the early checks it calls the router's `getFee`, which reverts when, for example, the router has no lane back to the source chain, the configured return format does not match the lane, a token pool does not allow the requested finality, or the token has no pool on that lane. Treat a revert as "not refundable right now". +> +> `requiredFee` is an estimate. Send more than this as `msg.value` to `refundFailedMessage`; the adapter returns the excess. + +**Access:** anyone (view). + +**Parameters:** + +| Parameter | Type | Description | +| ----------- | --------- | --------------------------------- | +| `messageId` | `bytes32` | ID of the failed inbound message. | + +**Returns:** + +| Name | Type | Description | +| ---------------- | --------- | ------------------------------------------------------------------------------------ | +| `canRefund` | `bool` | Whether a cross-chain refund is possible now, when the call does not revert. | +| `originalSender` | `bytes32` | Sender of the original message. The refund goes to this address on the source chain. | +| `token` | `address` | First inbound token with a non-zero amount. | +| `tokenAmount` | `uint256` | Amount of `token`. | +| `requiredFee` | `uint256` | Estimated total fee for all refund sends, in the native gas token. | + +**Reverts with:** + +- `InvalidEVMAddress(originalSender)` when the refund goes to an EVM chain and the upper 96 bits of the stored sender are not zero. +- Router, OnRamp, and token pool errors from `getFee`. See [Errors from dependencies](#errors-from-dependencies). + +**Emits:** none. + +*** + +### estimateRefundFee + +Estimates the total fee, in the native gas token, to refund a failed message cross-chain. + +```solidity +function estimateRefundFee(bytes32 messageId) external view returns (uint256) +``` + +> Sums one router `getFee` quote per inbound token with a non-zero amount, using the current router state. The estimate can be lower than the cost at execution, because `refundFailedMessage` re-quotes each send and fees can change in between. Pad `msg.value` above this value. + +**Access:** anyone (view). + +**Parameters:** + +| Parameter | Type | Description | +| ----------- | --------- | --------------------------------- | +| `messageId` | `bytes32` | ID of the failed inbound message. | + +**Returns:** + +| Type | Description | +| --------- | ------------------------------------------------------------------------------ | +| `uint256` | Estimated total fee for all refund sends, in the hub chain's native gas token. | + +**Reverts with:** + +- `MessageNotFailed(messageId)` when the message is not `BASIC`. +- `NoRefundableTokenAmounts()` when no inbound token amount is positive. +- `InvalidSenderAddressFormat()` when the stored sender is not exactly 32 bytes. +- `InvalidEVMAddress(originalSender)` when the refund goes to an EVM chain and the upper 96 bits of the stored sender are not zero. +- Router, OnRamp, and token pool errors from `getFee`. + +**Emits:** none. + +*** + +### checkLocalRecoveryEligibility + +Reports whether a failed message can be recovered on the hub chain, and by which address. + +```solidity +function checkLocalRecoveryEligibility(bytes32 messageId) + external + view + returns (bool canRecover, address localRefundAddress, address token, uint256 tokenAmount) +``` + +> Returns `canRecover = false` with zeroed fields when the message is not `BASIC`, when no local refund address was stored, or when no inbound token amount is positive. Otherwise it returns `true`, the local refund address, and the first token with a non-zero amount. It does not check the caller, and it never reverts. + +**Access:** anyone (view). + +**Parameters:** + +| Parameter | Type | Description | +| ----------- | --------- | --------------------------------- | +| `messageId` | `bytes32` | ID of the failed inbound message. | + +**Returns:** + +| Name | Type | Description | +| -------------------- | --------- | ------------------------------------------------------------- | +| `canRecover` | `bool` | Whether the local refund address can recover the message now. | +| `localRefundAddress` | `address` | The only address that can call `recoverFailedMessageLocally`. | +| `token` | `address` | First inbound token with a non-zero amount. | +| `tokenAmount` | `uint256` | Amount of `token`. | + +**Reverts with:** none. + +**Emits:** none. + +*** + +### getFailedMessageRecord + +Returns the stored record of a failed message. + +```solidity +function getFailedMessageRecord(bytes32 messageId) external view returns (FailedMessageRecord memory record) +``` + +> The record exists only while `messageErrorCode[messageId]` is `BASIC`. For a message that never failed, or one that was refunded or recovered, the call returns a record with zero values. Read `messageErrorCode` to tell these states apart. + +**Access:** anyone (view). + +**Parameters:** + +| Parameter | Type | Description | +| ----------- | --------- | -------------------------- | +| `messageId` | `bytes32` | ID of the inbound message. | + +**Returns:** + +| Name | Type | Description | +| -------- | ---------------------------- | -------------------------------------------------- | +| `record` | `FailedMessageRecord memory` | See [`FailedMessageRecord`](#failedmessagerecord). | + +**Reverts with:** none. + +**Emits:** none. + +## Recovery functions + +### refundFailedMessage + +Sends the inbound tokens of a failed message back to the original sender on the source chain. + +```solidity +function refundFailedMessage(bytes32 messageId) external payable nonReentrant +``` + +> Anyone can call it, and the caller pays the CCIP fees through `msg.value`. The recipient comes from the stored record, not from the caller. It is the original `message.sender` on `sourceChainSelector`. It is not the beneficiary and not the local refund address. +> +> The function: +> +> 1. Requires the message to be `BASIC`, at least one non-zero token amount, and a sender of exactly 32 bytes. +> 2. Sets the message to `RESOLVED` and deletes its record and chain family snapshot. +> 3. Sends each non-zero token in its own token-only CCIP message, with the same fields as a return leg (see [`processMessage`](#processmessage)). It uses the chain family recorded at failure time, the lane's return format, and `evmReturnRequestedFinality` for the inbound token: the asset for a failed deposit, the share token for a failed redemption. +> 4. Requires `msg.value` to cover the total fee paid, then sends any excess `msg.value` back to the caller. +> +> The adapter's own native gas token balance is not consumed. If any step reverts, the whole transaction reverts and the message stays `BASIC`. If the original sender is a contract, the tokens arrive at that contract's address on the source chain, and the contract is not called. + +**Access:** anyone. + +**Parameters:** + +| Parameter | Type | Description | +| ----------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `messageId` | `bytes32` | ID of the failed inbound message. | +| `msg.value` | native gas token | Fee for all refund sends. Start from [`estimateRefundFee`](#estimaterefundfee) or [`checkRefundEligibility`](#checkrefundeligibility) and add a margin. | + +**Returns:** none. + +**Reverts with:** + +- `MessageNotFailed(messageId)` when the message is not `BASIC`: it was never received, it succeeded, or it is already resolved. +- `NoRefundableTokenAmounts()` when no inbound token amount is positive. +- `InvalidSenderAddressFormat()` when the stored sender is not exactly 32 bytes. +- `InvalidEVMAddress(originalSender)` when the refund goes to an EVM chain and the upper 96 bits of the stored sender are not zero. +- `InsufficientNativeBalance(requiredFee, availableBalance)` when the adapter's native gas token balance, which includes `msg.value`, is below the router fee for a send. +- `InsufficientRecoveryFee(requiredFee, providedFee)` when `msg.value` is below the total fee paid. +- `RefundFailed()` when sending the excess `msg.value` back to the caller fails. +- `ReentrancyGuardReentrantCall()` on a reentrant call. +- Router, OnRamp, and token pool errors from `getFee` or `ccipSend`. + +**Emits:** + +- `MessageSent` for each token sent. +- `MessageRefunded(messageId, sourceChainSelector, originalSender)`. + +*** + +### recoverFailedMessageLocally + +Transfers the inbound tokens of a failed message to its local refund address on the hub chain. + +```solidity +function recoverFailedMessageLocally(bytes32 messageId) external nonReentrant +``` + +> Only the `localRefundAddress` stored in the failed-message record can call it. It costs no CCIP fee, and the function is not `payable`. It sets the message to `RESOLVED`, deletes the record and chain family snapshot, transfers each non-zero inbound token to the caller, and emits `MessageRecoveredLocally`. The tokens are the ones that arrived: the asset for a failed deposit, shares for a failed redemption. +> +> The local refund address comes from the payload's `deliveryAndRefund` field and cannot be set or changed afterward. Whichever recovery function succeeds first resolves the message; the other then reverts with `MessageNotFailed`. See [Local recovery](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#local-recovery). + +**Access:** the stored `localRefundAddress` only. + +**Parameters:** + +| Parameter | Type | Description | +| ----------- | --------- | --------------------------------- | +| `messageId` | `bytes32` | ID of the failed inbound message. | + +**Returns:** none. + +**Reverts with:** + +- `MessageNotFailed(messageId)` when the message is not `BASIC`. +- `NoLocalRefundAddress(messageId)` when no local refund address was stored. +- `UnauthorizedLocalRefund(caller, localRefundAddress)` when the caller is not the stored local refund address. +- `NoRefundableTokenAmounts()` when no inbound token amount is positive. +- `ReentrancyGuardReentrantCall()` on a reentrant call. +- Token transfer errors. + +**Emits:** + +- `MessageRecoveredLocally(messageId, localRefundAddress)`. + +## Configuration functions + +### setChainType + +Configures how the adapter treats a remote chain selector. + +```solidity +function setChainType(uint64 chainSelector, ChainType chainType) external onlyRole(DEFAULT_ADMIN_ROLE) +``` + +> Writes `chains[chainSelector]`. The adapter processes inbound messages only from selectors set to a value other than `NONE`, and uses the value to encode return legs to that selector. Set `EVM` for an EVM source chain, such as Arbitrum Sepolia (selector `3478487238524512106`) for an adapter on Ethereum Sepolia. +> +> Setting a selector to `NONE` does not stop CCIP delivery. Messages that arrive afterward fail with `InvalidChain` and become failed messages that users must refund or recover. Cross-chain refunds of earlier failures use the chain family recorded at failure time. The function does not validate the selector. + +**Access:** `DEFAULT_ADMIN_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| --------------- | ----------- | --------------------------------------------------- | +| `chainSelector` | `uint64` | CCIP chain selector of the remote chain. | +| `chainType` | `ChainType` | `EVM`, `SVM`, or `NONE` to reject messages from it. | + +**Returns:** none. + +**Reverts with:** + +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `ChainTypeSet(chainSelector, chainType)`. + +*** + +### setTargetEnabled + +Adds a vault to the allowlist or removes it. + +```solidity +function setTargetEnabled(address target, bool enabled) external onlyRole(DEFAULT_ADMIN_ROLE) +``` + +> Writes `enabledTargets[target]`. The payload's `target` selects the vault, so one adapter can serve several vaults. The function accepts any non-zero address and does not check that it is an ERC-4626 vault. Enable only vaults whose asset and share token behave as standard ERC-20 tokens, without fee-on-transfer or rebasing. See [Vault and token compatibility](/solutions/cross-chain-vault-adapter/reference/limitations#vault-and-token-compatibility). +> +> Disabling a vault does not stop delivery. Messages that arrive for it fail with `InvalidTarget` and become failed messages. + +**Access:** `DEFAULT_ADMIN_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| --------- | --------- | --------------------------------------- | +| `target` | `address` | Vault address. | +| `enabled` | `bool` | `true` to allowlist, `false` to remove. | + +**Returns:** none. + +**Reverts with:** + +- `InvalidTarget(address(0))` when `target` is the zero address. +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `TargetEnabled(target, enabled)`. + +*** + +### setProcessingEnabled + +Switches deposits and redemptions on or off for every vault on the adapter. + +```solidity +function setProcessingEnabled(bool depositsEnabled_, bool redeemsEnabled_) external onlyRole(DEFAULT_ADMIN_ROLE) +``` + +> Sets `depositsEnabled` and `redeemsEnabled`. Both start as `false` on a directly deployed adapter. +> +> This is not a pause. CCIP keeps delivering, and messages that arrive while their path is off, including messages already in flight, fail with `DepositsDisabled` or `RedeemsDisabled` and become failed messages. Switching the path back on does not reprocess them. See [What counts as a failure](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#what-counts-as-a-failure). + +**Access:** `DEFAULT_ADMIN_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| ------------------ | ------ | ------------------------------- | +| `depositsEnabled_` | `bool` | Whether to process deposits. | +| `redeemsEnabled_` | `bool` | Whether to process redemptions. | + +**Returns:** none. + +**Reverts with:** + +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `ProcessingEnabledSet(depositsEnabled, redeemsEnabled)`. + +*** + +### setCCVsConfig + +Sets the CCV policy for messages from a source chain on CCIP 2.0 lanes. + +```solidity +function setCCVsConfig( + uint64 sourceChainSelector, + address[] calldata requiredCCVs, + address[] calldata optionalCCVs, + uint8 optionalThreshold_ +) external onlyRole(DEFAULT_ADMIN_ROLE) +``` + +> Replaces the stored `CCVConfig` for `sourceChainSelector`, which `getCCVsAndFinalityConfig` returns to the OffRamp. CCIP 1.6 lanes ignore it. Empty lists with a zero threshold mean the lane's default CCVs. In `requiredCCVs`, `address(0)` stands for the lane's default CCVs. +> +> The function validates the lists: +> +> - `optionalThreshold_` must be greater than 0 when `optionalCCVs` is not empty, and no greater than `optionalCCVs.length`. +> - No address can appear twice in one list, or in both lists. +> - `optionalCCVs` cannot contain `address(0)`. +> +> It does not check that the CCVs exist or serve the lane. If a required CCV never attests, messages from that source chain fail at the CCIP level before they reach the adapter. Fix the configuration, then retry them with manual execution. + +**Access:** `DEFAULT_ADMIN_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| --------------------- | -------------------- | ----------------------------------------- | +| `sourceChainSelector` | `uint64` | Source chain the policy applies to. | +| `requiredCCVs` | `address[] calldata` | CCVs that must all attest. | +| `optionalCCVs` | `address[] calldata` | Optional CCVs. | +| `optionalThreshold_` | `uint8` | Number of optional CCVs that must attest. | + +**Returns:** none. + +**Reverts with:** + +- `OptionalCCVsRequirePositiveThreshold(optionalCCVCount)` when `optionalCCVs` is not empty and the threshold is 0. +- `InvalidOptionalThreshold(optionalThreshold, optionalCCVCount)` when the threshold is greater than the number of optional CCVs. +- `DuplicateCCV(ccv)` when an address repeats within or across the lists. +- `InvalidOptionalCCV()` when `optionalCCVs` contains `address(0)`. +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `CCVsConfigSet(sourceChainSelector, requiredCCVs, optionalCCVs, optionalThreshold)`. + +*** + +### setInboundFinality + +Sets which requested finality the adapter accepts for messages from a source chain on CCIP 2.0 lanes. + +```solidity +function setInboundFinality(uint64 sourceChainSelector, bytes4 allowedFinalityConfig) + external + onlyRole(DEFAULT_ADMIN_ROLE) +``` + +> Writes `inboundFinality[sourceChainSelector]`, which `getCCVsAndFinalityConfig` returns as `allowedFinalityConfig`. CCIP 2.0 OffRamps compare each message's requested finality against it; CCIP 1.6 lanes ignore it. The default, `0x00000000`, accepts only messages that wait for finality, so [faster-than-finality](/ccip/concepts/execution-latency/ftf-dapps) messages from that source chain fail at the CCIP level. +> +> The value uses the `FinalityCodec` layout. Bit 16 is the `safe` tag flag, and the low 16 bits are a block depth. A message that requests the `safe` tag is accepted when the flag is set. A message that requests N blocks is accepted when the configured depth is non-zero and N is at least that depth. A message that waits for finality is always accepted. The function performs no validation. + +**Access:** `DEFAULT_ADMIN_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| ----------------------- | -------- | ------------------------------------------------ | +| `sourceChainSelector` | `uint64` | Source chain the setting applies to. | +| `allowedFinalityConfig` | `bytes4` | Allowed finality, in the `FinalityCodec` layout. | + +**Returns:** none. + +**Reverts with:** + +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `InboundFinalitySet(sourceChainSelector, allowedFinalityConfig)`. + +*** + +### setEvmReturnLaneFormat + +Sets the `extraArgs` format for return legs and refunds to an EVM destination chain. + +```solidity +function setEvmReturnLaneFormat(uint64 destinationChainSelector, EvmReturnExtraArgsFormat format) + external + onlyRole(DEFAULT_ADMIN_ROLE) +``` + +> `destinationChainSelector` is the source chain of the inbound messages whose outputs and refunds go back to it. Set `GENERIC_EXTRA_ARGS_V3_BASIC` on CCIP 2.0 lanes, such as Arbitrum Sepolia for an adapter on Ethereum Sepolia. Leave the format unset or set `LEGACY_EXTRA_ARGS_V2` on CCIP 1.6 lanes. +> +> Setting `GENERIC_EXTRA_ARGS_V3_BASIC` on a CCIP 1.6 lane makes return legs and refunds revert at the router. Return-to-source messages then become failed messages, and cross-chain refunds to that chain revert until the format is corrected. +> +> `UNSET` cannot be written. To return to the legacy encoding, set `LEGACY_EXTRA_ARGS_V2`. + +**Access:** `DEFAULT_ADMIN_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| -------------------------- | -------------------------- | -------------------------------------------------------- | +| `destinationChainSelector` | `uint64` | Chain that return legs and refunds go to. | +| `format` | `EvmReturnExtraArgsFormat` | `LEGACY_EXTRA_ARGS_V2` or `GENERIC_EXTRA_ARGS_V3_BASIC`. | + +**Returns:** none. + +**Reverts with:** + +- `InvalidEvmReturnExtraArgsFormat()` when `format` is `UNSET`. +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `EvmReturnLaneFormatSet(destinationChainSelector, format)`. + +*** + +### setEvmReturnRequestedFinality + +Sets the finality that return legs and refunds of one token request on a CCIP 2.0 lane. + +```solidity +function setEvmReturnRequestedFinality( + uint64 destinationChainSelector, + address token, + bytes4 requestedFinalityForV3 +) external onlyRole(DEFAULT_ADMIN_ROLE) +``` + +> `token` is the token bridged on that leg. It is the share token for deposit returns and failed-redemption refunds, and the asset for redemption returns and failed-deposit refunds. +> +> A non-zero value requires the lane format for `destinationChainSelector` to already be `GENERIC_EXTRA_ARGS_V3_BASIC`. On any other format, passing `0x00000000` clears the stored value. On a V3 lane, the value must select exactly one mode. The token pools on the lane must allow the requested finality. If they do not, the router rejects the send, return legs become failed messages, and refunds revert. + +The requested finality takes one of these values: + +| Value | Meaning | +| ---------------------------- | --------------------------------------- | +| `0x00000000` | Wait for finality. This is the default. | +| `0x00010000` | Wait for the `safe` tag. | +| `0x00000001` to `0x0000FFFF` | Wait for that many blocks. | + +Other single flags in bits 17 to 31 pass the adapter's validation, but token pools and CCVs reject modes they do not implement. + +**Access:** `DEFAULT_ADMIN_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| -------------------------- | --------- | -------------------------------------------------- | +| `destinationChainSelector` | `uint64` | Chain that return legs and refunds go to. | +| `token` | `address` | Token bridged on the leg. Must be non-zero. | +| `requestedFinalityForV3` | `bytes4` | Requested finality, in the `FinalityCodec` layout. | + +**Returns:** none. + +**Reverts with:** + +- `InvalidTarget(address(0))` when `token` is the zero address. +- `UnexpectedRequestedFinalityForLegacyFormat(requestedFinalityForV3)` when the value is non-zero and the lane format is not `GENERIC_EXTRA_ARGS_V3_BASIC`. +- `RequestedFinalityCanOnlyHaveOneMode(encodedFinality)` from `FinalityCodec` when the value combines a flag with a block depth or sets more than one flag. +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `EvmReturnRequestedFinalitySet(destinationChainSelector, token, requestedFinalityForV3)`, with `0x00000000` when the call clears the value on a non-V3 lane. + +*** + +### recoverNative + +Sends the adapter's entire native gas token balance to a recipient. + +```solidity +function recoverNative(address recipient) external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) +``` + +> There is no amount parameter, so the call always sends the full balance. Afterward the adapter cannot pay CCIP fees, so return-to-source messages fail with `InsufficientNativeBalance` until someone funds it again. + +**Access:** `DEFAULT_ADMIN_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| ----------- | --------- | -------------------------------------- | +| `recipient` | `address` | Receives the native gas token balance. | + +**Returns:** none. + +**Reverts with:** + +- `InvalidRecipient()` when `recipient` is the zero address. +- `AmountIsZero()` when the balance is zero. +- `RecoverNativeFailed()` when the transfer to `recipient` fails. +- `ReentrancyGuardReentrantCall()` on a reentrant call. +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `NativeRecovered(recipient, amount)`. + +## Fee functions + +### setAssetFee + +Sets the adapter fee for returns of one token to one chain. + +```solidity +function setAssetFee(uint64 destinationChainSelector, address bridgedToken, uint256 fee) + external + onlyRole(FEE_SETTER_ROLE) +``` + +> Writes `assetFees[destinationChainSelector][bridgedToken]`. The adapter fee is a flat amount in the asset's smallest units on both paths. It applies only when the output returns to the source chain. +> +> - **Deposit return:** the key is the share token (the vault address). The fee is taken from the inbound asset before the deposit. +> - **Redemption return:** the key is the asset. The fee is taken from the redeemed assets. +> +> There is no upper bound. A fee greater than or equal to the amount makes those messages fail with `FeeExceedsAmount`. The fee is read when a message executes, so a change applies to messages already in flight. You can set a fee before configuring the chain with `setChainType`. Set `0` to remove a fee. See [Fees](/solutions/cross-chain-vault-adapter/overview/how-it-works#fees). + +**Access:** `FEE_SETTER_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| -------------------------- | --------- | ---------------------------------------------------------------------------------------- | +| `destinationChainSelector` | `uint64` | Chain the output returns to, which is the source chain of the inbound message. | +| `bridgedToken` | `address` | Token bridged back, which is the share token for deposits and the asset for redemptions. | +| `fee` | `uint256` | Fee in the asset's smallest units. | + +**Returns:** none. + +**Reverts with:** + +- `InvalidTarget(address(0))` when `bridgedToken` is the zero address. +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `AssetFeeSet(destinationChainSelector, bridgedToken, fee)`. + +*** + +### setAssetFees + +Sets several adapter fees in one call. + +```solidity +function setAssetFees( + uint64[] calldata destinationChainSelectors, + address[] calldata bridgedTokens, + uint256[] calldata fees +) external onlyRole(FEE_SETTER_ROLE) +``` + +> Each index applies one `setAssetFee` row with the same validation. A zero token in any row reverts the whole call. + +**Access:** `FEE_SETTER_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| --------------------------- | -------------------- | ------------------------------------------- | +| `destinationChainSelectors` | `uint64[] calldata` | Return chain per row. | +| `bridgedTokens` | `address[] calldata` | Token bridged back per row. | +| `fees` | `uint256[] calldata` | Fee per row, in the asset's smallest units. | + +**Returns:** none. + +**Reverts with:** + +- `FeeConfigLengthMismatch()` when the three arrays differ in length. +- `InvalidTarget(address(0))` when any `bridgedTokens` entry is the zero address. +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `AssetFeeSet(destinationChainSelector, bridgedToken, fee)` for each row. + +*** + +### withdrawFee + +Transfers accrued adapter fees to a recipient. + +```solidity +function withdrawFee(address asset, address recipient, uint256 amount) + external + nonReentrant + onlyRole(FEE_COLLECTOR_ROLE) +``` + +> Reduces `collectedFees[asset]` by `amount` and transfers that amount. Partial withdrawals are allowed. The function withdraws only fees the adapter has accounted for. The adapter has no general ERC-20 sweep, so tokens sent to it directly, outside a CCIP message, cannot be recovered. + +**Access:** `FEE_COLLECTOR_ROLE`. + +**Parameters:** + +| Parameter | Type | Description | +| ----------- | --------- | -------------------------------------- | +| `asset` | `address` | Asset whose fees to withdraw. | +| `recipient` | `address` | Receives the fees. | +| `amount` | `uint256` | Amount, in the asset's smallest units. | + +**Returns:** none. + +**Reverts with:** + +- `InvalidRecipient()` when `recipient` is the zero address. +- `AmountIsZero()` when `amount` is zero. +- `InsufficientFeeBalance(availableBalance, requestedAmount)` when `amount` exceeds `collectedFees[asset]`. +- Token transfer errors, for example when the adapter holds less of the asset than its accounting allows. +- `ReentrancyGuardReentrantCall()` on a reentrant call. +- `AccessControlUnauthorizedAccount(account, neededRole)` when the caller lacks the role. + +**Emits:** + +- `FeeWithdrawn(asset, recipient, amount)`. + +## Events + +The adapter declares these 17 events: + +```solidity +event MessageSent( + bytes32 indexed messageId, + uint64 indexed destinationChainSelector, + ChainType indexed chainType, + bytes32 beneficiary, + address token, + uint256 amount, + uint256 fee +); + +event MessageSucceeded(bytes32 indexed messageId); + +event MessageFailed(bytes32 indexed messageId); + +event MessageRefunded( + bytes32 indexed messageId, uint64 indexed destinationChainSelector, bytes32 indexed beneficiary +); + +event MessageRecoveredLocally(bytes32 indexed messageId, address indexed localRefundAddress); + +event TargetProcessed( + bytes32 indexed messageId, + address indexed target, + address indexed inputToken, + address outputToken, + uint256 inputAmount, + uint256 outputAmount +); + +event TargetEnabled(address indexed target, bool enabled); +event ChainTypeSet(uint64 indexed chainSelector, ChainType chainType); + +event ProcessingEnabledSet(bool depositsEnabled, bool redeemsEnabled); +event AssetFeeSet(uint64 indexed destinationChainSelector, address indexed bridgedToken, uint256 fee); +event CCVsConfigSet( + uint64 indexed sourceChainSelector, address[] requiredCCVs, address[] optionalCCVs, uint8 optionalThreshold +); + +event InboundFinalitySet(uint64 indexed sourceChainSelector, bytes4 allowedFinalityConfig); + +event EvmReturnLaneFormatSet(uint64 indexed destinationChainSelector, EvmReturnExtraArgsFormat format); + +event EvmReturnRequestedFinalitySet( + uint64 indexed destinationChainSelector, address indexed token, bytes4 requestedFinalityForV3 +); + +event FeeWithdrawn(address indexed asset, address indexed recipient, uint256 amount); + +event NativeRecovered(address indexed recipient, uint256 amount); + +event LocalTokenDelivered( + bytes32 indexed messageId, address indexed token, address indexed beneficiary, uint256 amount +); +``` + +| Event | Emitted when | Use it to | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `MessageSucceeded` | `processMessage` completes inside `ccipReceive`. | Mark an inbound message as processed. | +| `MessageFailed` | `processMessage` reverts and the adapter records a failed message. | Alert on failures and offer the user a cross-chain refund or local recovery. The CCIP Explorer still shows Success. | +| `TargetProcessed` | The vault deposit or redemption completes, before delivery. | Record the inbound amount and the output after the adapter fee for each message. | +| `LocalTokenDelivered` | The output is transferred to the beneficiary on the hub chain. | Confirm local delivery. | +| `MessageSent` | The adapter sends a CCIP message: a return leg, or one leg of a cross-chain refund. | Track the outbound message ID on the CCIP Explorer and the fee paid in the native gas token. `messageId` here is the outbound ID. | +| `MessageRefunded` | `refundFailedMessage` completes. `destinationChainSelector` is the original source chain and `beneficiary` the original sender. | Close a failed message as refunded. | +| `MessageRecoveredLocally` | `recoverFailedMessageLocally` completes. | Close a failed message as recovered on the hub chain. | +| `TargetEnabled` | `setTargetEnabled` runs. | Review the vault allowlist. | +| `ChainTypeSet` | `setChainType` runs. | Review which source chains are configured. | +| `ProcessingEnabledSet` | `setProcessingEnabled` runs. | Warn users before they send a request that will fail. | +| `AssetFeeSet` | `setAssetFee` runs, or once per row of `setAssetFees`. | Update the adapter fee shown in a UI. | +| `CCVsConfigSet` | `setCCVsConfig` runs. | Review the CCV policy per source chain. | +| `InboundFinalitySet` | `setInboundFinality` runs. | Review which faster-than-finality modes are accepted. | +| `EvmReturnLaneFormatSet` | `setEvmReturnLaneFormat` runs. | Review return formats per lane. | +| `EvmReturnRequestedFinalitySet` | `setEvmReturnRequestedFinality` runs. | Review return finality per lane and token. | +| `FeeWithdrawn` | `withdrawFee` runs. | Reconcile fee withdrawals. | +| `NativeRecovered` | `recoverNative` runs. | Raise an alert, because the adapter can no longer pay for return legs until it is funded again. | + +`TargetProcessed`, `LocalTokenDelivered`, and the return-leg `MessageSent` are emitted inside `processMessage`. When processing fails they roll back, so a failed message emits only `MessageFailed`. On success, the inbound message's `TargetProcessed` and `MessageSucceeded` appear in the same transaction as the return leg's `MessageSent`, which links the inbound and outbound message IDs. + +The adapter also inherits `RoleGranted` and `RoleRevoked` from OpenZeppelin `AccessControl`. Watch them for role changes. `RoleAdminChanged` is part of the ABI, but the adapter never emits it because it never changes a role's admin. + +## Errors + +### Adapter errors + +The adapter declares these 35 custom errors: + +```solidity +error InvalidRouter(address router); +error InvalidAdmin(); +error InvalidFeeSetter(); +error InvalidFeeCollector(); +error InvalidRecipient(); +error InvalidChain(uint64 chainSelector); +error OnlySelf(); +error AmountIsZero(); +error InvalidEVMAddress(bytes32 beneficiary); +error InvalidSenderAddressFormat(); +error InsufficientNativeBalance(uint256 requiredFee, uint256 availableBalance); +error InsufficientRecoveryFee(uint256 requiredFee, uint256 providedFee); +error InsufficientFeeBalance(uint256 availableBalance, uint256 requestedAmount); +error MessageNotFailed(bytes32 messageId); +error InvalidTarget(address target); +error InvalidTargetToken(address target, address token); +error InvalidTokenCount(uint256 tokenCount); +error InvalidPayloadLength(uint256 length, uint256 expected); +error FeeConfigLengthMismatch(); +error InvalidEvmReturnExtraArgsFormat(); +error UnexpectedRequestedFinalityForLegacyFormat(bytes4 requestedFinalityForV3); +error InvalidOptionalThreshold(uint8 optionalThreshold, uint256 optionalCCVCount); +error OptionalCCVsRequirePositiveThreshold(uint256 optionalCCVCount); +error DuplicateCCV(address ccv); +error InvalidOptionalCCV(); +error DepositsDisabled(); +error RedeemsDisabled(); +error FeeExceedsAmount(uint256 amount, uint256 fee); +error MinimumOutputNotMet(uint256 minimumOut, uint256 actualOut); +error NoOutputReceived(); +error RefundFailed(); +error RecoverNativeFailed(); +error NoRefundableTokenAmounts(); +error NoLocalRefundAddress(bytes32 messageId); +error UnauthorizedLocalRefund(address caller, address localRefundAddress); +``` + +"Becomes a failed message" is yes when the error is raised while `processMessage` handles an inbound message, because `ccipReceive` catches it and records a failed message. Errors raised by views, recovery functions, and setters revert the call directly. + +| Error | Raised by | Meaning | Becomes a failed message | +| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------- | +| `InvalidRouter` | Constructor, `ccipReceive` | The router is the zero address at construction, or the caller of `ccipReceive` is not `ROUTER`. | No | +| `InvalidAdmin` | Constructor | `defaultAdmin` is the zero address. The factory reuses this error. | No | +| `InvalidFeeSetter` | Constructor | `feeSetter` is the zero address. The factory reuses this error. | No | +| `InvalidFeeCollector` | Constructor | `feeCollector` is the zero address. The factory reuses this error. | No | +| `InvalidRecipient` | `withdrawFee`, `recoverNative` | The recipient is the zero address. | No | +| `InvalidChain` | `processMessage`, `preview` | The source chain selector is `NONE` in `chains`. | Yes from `processMessage` | +| `OnlySelf` | `processMessage` | An address other than the adapter called `processMessage`. | No | +| `AmountIsZero` | `processMessage`, `withdrawFee`, `recoverNative` | The inbound amount, the withdrawal amount, or the native gas token balance is zero. | Yes from `processMessage` | +| `InvalidEVMAddress` | `processMessage`, `refundFailedMessage`, `estimateRefundFee`, `checkRefundEligibility` | The beneficiary or the original sender has non-zero upper 96 bits. | Yes from `processMessage` | +| `InvalidSenderAddressFormat` | `refundFailedMessage`, `estimateRefundFee` | The stored sender is not exactly 32 bytes. | No | +| `InsufficientNativeBalance` | `processMessage` (return leg), `refundFailedMessage` | The native gas token balance available for a send is below the router fee. | Yes from `processMessage` | +| `InsufficientRecoveryFee` | `refundFailedMessage` | `msg.value` is below the total fee paid for the refund. | No | +| `InsufficientFeeBalance` | `withdrawFee` | `amount` exceeds `collectedFees[asset]`. | No | +| `MessageNotFailed` | `refundFailedMessage`, `recoverFailedMessageLocally`, `estimateRefundFee` | The message is not `BASIC`. | No | +| `InvalidTarget` | `processMessage`, `preview`, `setTargetEnabled`, `setAssetFee`, `setAssetFees`, `setEvmReturnRequestedFinality` | The vault is not allowlisted, or a setter received the zero address. | Yes from `processMessage` | +| `InvalidTargetToken` | `processMessage`, `preview` | The token is neither the vault's asset nor its share token. | Yes from `processMessage` | +| `InvalidTokenCount` | `processMessage` | The message does not carry exactly one token. | Yes | +| `InvalidPayloadLength` | `processMessage` | `message.data` is not 128 bytes. | Yes | +| `FeeConfigLengthMismatch` | `setAssetFees` | The three arrays differ in length. | No | +| `InvalidEvmReturnExtraArgsFormat` | `setEvmReturnLaneFormat` | The format is `UNSET`. | No | +| `UnexpectedRequestedFinalityForLegacyFormat` | `setEvmReturnRequestedFinality` | A non-zero finality was set for a lane whose format is not `GENERIC_EXTRA_ARGS_V3_BASIC`. | No | +| `InvalidOptionalThreshold` | `setCCVsConfig` | The threshold is greater than the number of optional CCVs. | No | +| `OptionalCCVsRequirePositiveThreshold` | `setCCVsConfig` | Optional CCVs were given with a threshold of 0. | No | +| `DuplicateCCV` | `setCCVsConfig` | An address repeats within or across the CCV lists. | No | +| `InvalidOptionalCCV` | `setCCVsConfig` | `optionalCCVs` contains the zero address. | No | +| `DepositsDisabled` | `processMessage`, `preview` | Deposits are switched off. | Yes from `processMessage` | +| `RedeemsDisabled` | `processMessage`, `preview` | Redemptions are switched off. | Yes from `processMessage` | +| `FeeExceedsAmount` | `processMessage` | The adapter fee is greater than or equal to the deposit input or the redeemed assets. `preview` returns `0` instead. | Yes | +| `MinimumOutputNotMet` | `processMessage` | The output after the adapter fee is below `minimumOut`. | Yes | +| `NoOutputReceived` | `processMessage` | The vault returned zero shares or zero assets. | Yes | +| `RefundFailed` | `refundFailedMessage` | Sending the excess `msg.value` back to the caller failed. | No | +| `RecoverNativeFailed` | `recoverNative` | The native gas token transfer to the recipient failed. | No | +| `NoRefundableTokenAmounts` | `refundFailedMessage`, `recoverFailedMessageLocally`, `estimateRefundFee` | No inbound token amount is positive. | No | +| `NoLocalRefundAddress` | `recoverFailedMessageLocally` | No local refund address was stored for the message. | No | +| `UnauthorizedLocalRefund` | `recoverFailedMessageLocally` | The caller is not the stored local refund address. | No | + +### Errors from dependencies + +Callers can also see these errors from inherited contracts, libraries, and CCIP contracts: + +| Error | Source | Raised when | +| ---------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `AccessControlUnauthorizedAccount(address account, bytes32 neededRole)` | OpenZeppelin `AccessControl` | The caller lacks the role for a configuration or fee function, or the admin role for `grantRole` or `revokeRole`. | +| `AccessControlBadConfirmation()` | OpenZeppelin `AccessControl` | `renounceRole` is called with an account other than the caller. | +| `ReentrancyGuardReentrantCall()` | OpenZeppelin `ReentrancyGuard` | `refundFailedMessage`, `recoverFailedMessageLocally`, `withdrawFee`, or `recoverNative` is re-entered. | +| `RequestedFinalityCanOnlyHaveOneMode(bytes4 encodedFinality)` | `FinalityCodec`, in `setEvmReturnRequestedFinality` | The requested finality combines a flag with a block depth or sets more than one flag. | +| `UnsupportedDestinationChain(uint64 destChainSelector)` | CCIP Router, in `getFee` and `ccipSend` | The hub-chain router has no lane to the return or refund destination. | +| `BadARMSignal()` | CCIP Router, in `ccipSend` | The hub chain is cursed through the RMN contract, so sends revert until the curse is lifted. | +| `InvalidRequestedFinality(bytes4 requestedFinality, bytes4 allowedFinality)` | `FinalityCodec`, raised by a token pool in `getFee` or `ccipSend` | The token pool does not allow the return leg's requested finality. | + +OnRamps, fee quoters, token pools, vaults, and tokens can raise other errors during a send or vault call. `SafeERC20` from OpenZeppelin 4.8.3 reverts with string messages such as `SafeERC20: ERC20 operation did not succeed`. Inside `processMessage`, all of these are caught and the message becomes a failed message. In refund functions and views, they revert the call. + +## Security notes + +**Processing failures are caught.** A revert in `processMessage` does not revert CCIP execution, except for the malformed payload described below. The adapter keeps the tokens as a failed message, and the CCIP Explorer shows Success. Detect failures with `MessageFailed` or `messageErrorCode`, not with the CCIP Explorer status. Nothing refunds or retries a failed message automatically. + +**No sender allowlist.** Any sender on a chain configured in `chains` can deposit into or redeem from any allowlisted vault. `getCCVsAndFinalityConfig` ignores the sender. The adapter gates requests only by source chain, vault, and the deposit and redeem switches. The vault sees the adapter as its depositor, so a vault that allowlists depositors admits every cross-chain user once it allowlists the adapter. + +**Admin powers.** `DEFAULT_ADMIN_ROLE` holders can change chain types, the vault allowlist, the deposit and redeem switches, CCV and finality policy, and return formats; withdraw the entire native gas token balance; and grant or revoke every role. `FEE_SETTER_ROLE` holders can change the adapter fee, which applies to messages already in flight; `minimumOut` is the user's protection against that. Every change takes effect in the transaction that makes it. No role can transfer the tokens of a failed message. Only `refundFailedMessage` and `recoverFailedMessageLocally` move them, and `withdrawFee` is limited to `collectedFees`. + +**Reentrancy.** `refundFailedMessage`, `recoverFailedMessageLocally`, `withdrawFee`, and `recoverNative` use `nonReentrant`. Both recovery functions mark the message `RESOLVED` and delete its record before they move tokens. + +**The adapter pays CCIP fees only in the native gas token.** Return legs pay the CCIP fee from the adapter's native gas token balance, never in LINK. When the balance is below the fee, return-to-source messages fail with `InsufficientNativeBalance`. Cross-chain refunds are paid from the caller's `msg.value`. The sender pays the source-chain CCIP fee in the native gas token or LINK. + +**Outputs return only to the source chain.** Return legs and refunds go to the inbound message's source chain. The adapter cannot route output to a third chain. Refunds go to the original sender, so a refund for a message sent through a contract arrives at that contract. Return-leg `extraArgs` come from the adapter's configuration, never from message data. + +**Malformed payloads.** A hand-built payload can make `ccipReceive` itself revert and strand the tokens permanently, as explained in [Failures with no recovery path](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery#failures-with-no-recovery-path). + +**Token-only messages.** A CCIP message with empty `data` and a gas limit of 0 is a token-only transfer. The OffRamp delivers the tokens to the adapter without calling `ccipReceive`, so the adapter records nothing and has no function that returns them. An ERC-20 transfer sent straight to the adapter ends up in the same state. See [When funds can get stuck](/solutions/cross-chain-vault-adapter/reference/limitations#when-funds-can-get-stuck). + +**Forks.** If you fork the adapter, keep the router check in `ccipReceive`, the self-call isolation of `processMessage`, failed-message storage and both recovery paths, and the beneficiary address checks. The failure handler, the `catch` path in `ccipReceive`, must never revert on any input. See [Customizing the adapter](/solutions/cross-chain-vault-adapter/reference/limitations#customizing-the-adapter). + +## Related contracts + +- [Factory contract](/solutions/cross-chain-vault-adapter/reference/factory-contract): deploys and configures adapters +- [Limitations](/solutions/cross-chain-vault-adapter/reference/limitations): what the adapter does not do +- [`IAny2EVMMessageReceiverV2`](/ccip/evm/api-reference/v2.0.0/i-any2-evm-message-receiver-v2): the CCIP 2.0 receiver interface the adapter implements +- [`Client`](/ccip/evm/api-reference/v2.0.0/client): CCIP message structs and `GenericExtraArgsV2` +- [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec): `GenericExtraArgsV3` encoding for return legs on CCIP 2.0 lanes +- [`CCIPReceiver`](/ccip/evm/api-reference/v2.0.0/ccip-receiver): CCIP's base receiver, which the adapter does not inherit +- [`CCVConfigValidation`](/ccip/evm/api-reference/v2.0.0/ccv-config-validation): CCV list rules applied by CCIP +- [OpenZeppelin `AccessControlEnumerable`](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControlEnumerable): role management +- [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626): the tokenized vault standard + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. + +--- + +# Factory contract +Source: https://docs.chain.link/solutions/cross-chain-vault-adapter/reference/factory-contract + +The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. + +`CrossChainERC4626AdapterFactory` deploys a `CrossChainERC4626Adapter` on the hub chain, applies its initial configuration, and hands its roles to your accounts in one transaction. + +This contract provides: + +- deployment of an adapter bound to the CCIP Router you pass in +- the initial chain types, vault allowlist entry, deposit and redeem switches, and adapter fees +- a role handoff that leaves the factory with no roles on the adapter + +> **NOTE: Who calls the factory** +> +> The vault team calls `deploy` once per adapter, on the hub chain. Users and integrators never call the factory. They +> send CCIP messages to the deployed adapter, as described in [How it +> works](/solutions/cross-chain-vault-adapter/overview/how-it-works). To deploy an adapter for your vault, see [Deploy +> the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter). + +## Usage boundary + +- Anyone can call `deploy`. The factory has no owner, allowlist, or fee. +- After `deploy` returns, the factory holds no role on the adapter and cannot change it. +- The factory keeps no registry of adapters. The `AdapterDeployed` event is the only on-chain record that links an adapter to this factory. +- The factory sets the initial configuration only. Send later changes to the adapter directly, from the account that holds the required role. See the [configuration functions](/solutions/cross-chain-vault-adapter/reference/adapter-contract#configuration-functions) on the adapter. + +## Contract + +[`src/ccip/CrossChainERC4626AdapterFactory.sol`](https://github.com/smartcontractkit/cross-chain-vault-adapters/blob/main/src/ccip/CrossChainERC4626AdapterFactory.sol) + +- Version: `CrossChainERC4626AdapterFactory 1.0.0`, returned by [`typeAndVersion`](#typeandversion). +- The factory and the adapter are audited. +- Both are compiled with solc 0.8.24 for the cancun EVM version, and their bytecode uses the `PUSH0` opcode. Deploy them only on chains that support the Shanghai upgrade. + +## Deployed addresses + +| Network | Address | +| ---------------- | -------------------------------------------- | +| Ethereum mainnet | `0x1b79038DCCbeE0E406eA0c1bA758A2cF696b38D4` | +| Ethereum Sepolia | `0x12A0f45738F3DDA931C4D3ECd1b267355e587921` | + +You can deploy your own factory with the repository's `pnpm ccip:deploy-factory` script. + +## Inheritance + +- [`ITypeAndVersion`](/ccip/evm/api-reference/v2.0.0/i-type-and-version) + +## Functions + +### deploy + +Deploys and configures a `CrossChainERC4626Adapter` in one transaction. + +```solidity +function deploy(DeploymentConfig calldata config) external returns (address adapterAddress) +``` + +> The factory holds all three roles while it applies `config`, then grants each role to its configured account and renounces its own. Any revert undoes the whole transaction, so no adapter is created. The numbered steps after **Emits** list the exact order. +> +> `deploy` leaves the CCIP 2.0 settings at their defaults and does not fund the adapter. See [Security notes](#security-notes). + +**Access:** anyone. + +**Parameters:** + +| Parameter | Type | Description | +| --------- | -------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `config` | [`DeploymentConfig`](#deploymentconfig) `calldata` | Router, role accounts, vault, processing switches, and batches of chain and fee rows. | + +**Returns:** + +| Name | Type | Description | +| ---------------- | --------- | -------------------------------- | +| `adapterAddress` | `address` | Address of the deployed adapter. | + +**Reverts with:** + +- `InvalidAdmin`, `InvalidFeeSetter`, or `InvalidFeeCollector` when the matching role account is the zero address. +- `InvalidRouter(address(0))` from the adapter constructor when `config.router` is the zero address. +- `InvalidTarget(address(0))` from the adapter's `setAssetFee` when a `FeeConfig` row has a zero `bridgedToken`. + +**Emits:** + +- `AdapterDeployed` from the factory. +- The adapter's own events from the configuration calls: `ChainTypeSet` per chain row, `TargetEnabled` when `vaultTarget` is set, `ProcessingEnabledSet`, and `AssetFeeSet` per fee row. See the adapter's [events](/solutions/cross-chain-vault-adapter/reference/adapter-contract#events). +- OpenZeppelin `RoleGranted` and `RoleRevoked` events from the adapter for the role handoff. + +`deploy` runs these steps in order: + +1. Reverts if `defaultAdmin`, `feeSetter`, or `feeCollector` is the zero address. These checks run before the adapter constructor, so a call with a zero router and a zero admin reverts with `InvalidAdmin`. +2. Deploys the adapter with `CREATE`, passing `config.router` and the factory's own address as admin, fee setter, and fee collector. The factory now holds all three roles. +3. Calls `setChainType` once for each `chainConfigs` row. +4. Calls `setTargetEnabled(vaultTarget, targetEnabled)` only when `vaultTarget` is not the zero address. Otherwise it skips the call and ignores `targetEnabled`. +5. Calls `setProcessingEnabled(depositsEnabled, redeemsEnabled)`. This call always runs. +6. Calls `setAssetFee` once for each `feeConfigs` row. +7. Hands off the roles, as described in [Role handoff](#role-handoff). +8. Emits `AdapterDeployed` and returns the adapter address. + +Rows apply in array order. A later row for the same chain selector, or the same selector and token pair, overwrites an earlier one. + +*** + +### typeAndVersion + +Returns the factory's type and version, declared as a public constant: + +```solidity +string public constant override typeAndVersion = "CrossChainERC4626AdapterFactory 1.0.0"; +``` + +**Access:** anyone. + +**Returns:** `string`, the contract type and version. The repository's `pnpm ccip:deploy` script checks this value before it deploys through an existing factory. + +## Types + +The three structs are declared in the factory: + +```solidity +struct ChainConfig { + uint64 chainSelector; + CrossChainERC4626Adapter.ChainType chainType; +} + +struct FeeConfig { + uint64 destinationChainSelector; + address bridgedToken; + uint256 fee; +} + +struct DeploymentConfig { + address router; + address defaultAdmin; + address feeSetter; + address feeCollector; + address vaultTarget; + bool targetEnabled; + bool depositsEnabled; + bool redeemsEnabled; + ChainConfig[] chainConfigs; + FeeConfig[] feeConfigs; +} +``` + +### DeploymentConfig + +| Field | Type | Description | +| ----------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `router` | `address` | CCIP Router on the hub chain. It becomes the adapter's immutable `ROUTER`, the only caller allowed to deliver messages. Must not be zero. Get the address from the CCIP Directory for [testnet](/ccip/directory/testnet) or [mainnet](/ccip/directory/mainnet). | +| `defaultAdmin` | `address` | Receives `DEFAULT_ADMIN_ROLE`. Must not be zero. | +| `feeSetter` | `address` | Receives `FEE_SETTER_ROLE`. Must not be zero. | +| `feeCollector` | `address` | Receives `FEE_COLLECTOR_ROLE`. Must not be zero. | +| `vaultTarget` | `address` | ERC-4626 vault to allowlist. Pass the zero address to skip. | +| `targetEnabled` | `bool` | Value passed to `setTargetEnabled` for `vaultTarget`. Ignored when `vaultTarget` is zero. | +| `depositsEnabled` | `bool` | Whether the adapter processes deposits. | +| `redeemsEnabled` | `bool` | Whether the adapter processes redemptions. | +| `chainConfigs` | `ChainConfig[]` | Source chains to configure. Can be empty. | +| `feeConfigs` | `FeeConfig[]` | Adapter fee rows. Can be empty. | + +### ChainConfig + +| Field | Type | Description | +| --------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `chainSelector` | `uint64` | CCIP chain selector of a source chain. | +| `chainType` | `CrossChainERC4626Adapter.ChainType` | `NONE` (0) disables the chain, `EVM` (1) is an EVM chain, `SVM` (2) is the Solana Virtual Machine chain family. Messages from a chain whose type is `NONE` become [failed messages](/solutions/cross-chain-vault-adapter/overview/failures-and-recovery) in the adapter. | + +### FeeConfig + +Each row becomes one `setAssetFee` call. The code calls the adapter fee the asset fee. + +| Field | Type | Description | +| -------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `destinationChainSelector` | `uint64` | Chain the output returns to, which is the source chain of the inbound message. | +| `bridgedToken` | `address` | Token bridged back, which is the share token (the vault address) for deposit returns and the asset for redeem returns. Must not be zero. | +| `fee` | `uint256` | Flat adapter fee in the asset's smallest units. The adapter charges it only when the user asks for return to the source chain. | + +The fee is staged even when the chain in that row has no chain type yet. + +### Example configuration + +The following JSON illustrates the fields of a `DeploymentConfig` for an adapter on Ethereum Sepolia that accepts messages from Arbitrum Sepolia, with deposits and redemptions enabled and an adapter fee on both return legs. No repository script reads this JSON. Instead, `pnpm ccip:deploy` builds the struct from environment variables. Chain selectors and fees are strings because `uint64` and `uint256` values can exceed the JavaScript safe integer range. + +```json +{ + "router": "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59", + "defaultAdmin": "", + "feeSetter": "", + "feeCollector": "", + "vaultTarget": "", + "targetEnabled": true, + "depositsEnabled": true, + "redeemsEnabled": true, + "chainConfigs": [{ "chainSelector": "3478487238524512106", "chainType": 1 }], + "feeConfigs": [ + { + "destinationChainSelector": "3478487238524512106", + "bridgedToken": "", + "fee": "1000000000000000" + }, + { + "destinationChainSelector": "3478487238524512106", + "bridgedToken": "", + "fee": "1000000000000000" + } + ] +} +``` + +- `0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59` is the CCIP Router on Ethereum Sepolia, and `3478487238524512106` is the Arbitrum Sepolia chain selector. +- Replace ``, ``, and the three role placeholders with your own addresses. +- A `fee` of `1000000000000000` is 0.001 of an 18-decimal asset. It matches the 0.001 CCIP-BnM adapter fee in [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter#configure-the-deployment). +- The first fee row applies when shares return to Arbitrum Sepolia. That requires the share token to be a [CCIP cross-chain token (CCT)](/ccip/concepts/cross-chain-token/overview) on both chains. + +## Events + +The factory declares one event: + +```solidity +event AdapterDeployed( + address indexed adapter, + address indexed router, + address indexed defaultAdmin, + address feeSetter, + address feeCollector, + address vaultTarget, + bool targetEnabled, + bool depositsEnabled, + bool redeemsEnabled +); +``` + +| Field | Type | Indexed | Description | +| ----------------- | --------- | ------- | ---------------------------------------------------------------- | +| `adapter` | `address` | Yes | Address of the new adapter. | +| `router` | `address` | Yes | `config.router`. | +| `defaultAdmin` | `address` | Yes | Account that received `DEFAULT_ADMIN_ROLE`. | +| `feeSetter` | `address` | No | Account that received `FEE_SETTER_ROLE`. | +| `feeCollector` | `address` | No | Account that received `FEE_COLLECTOR_ROLE`. | +| `vaultTarget` | `address` | No | `config.vaultTarget`, or the zero address if none was set. | +| `targetEnabled` | `bool` | No | `config.targetEnabled`, emitted even when `vaultTarget` is zero. | +| `depositsEnabled` | `bool` | No | `config.depositsEnabled`. | +| `redeemsEnabled` | `bool` | No | `config.redeemsEnabled`. | + +The chain and fee rows are not in this event. They appear as the adapter's `ChainTypeSet` and `AssetFeeSet` events in the same transaction. + +## Errors + +The factory declares no errors. It reverts with errors declared in `CrossChainERC4626Adapter`, listed on the adapter's [errors](/solutions/cross-chain-vault-adapter/reference/adapter-contract#errors): + +| Error | Raised by | When | +| ------------------------------- | --------------------- | -------------------------------------------- | +| `InvalidAdmin()` | Factory | `defaultAdmin` is the zero address. | +| `InvalidFeeSetter()` | Factory | `feeSetter` is the zero address. | +| `InvalidFeeCollector()` | Factory | `feeCollector` is the zero address. | +| `InvalidRouter(address router)` | Adapter constructor | `router` is the zero address. | +| `InvalidTarget(address target)` | Adapter `setAssetFee` | A `FeeConfig` row has a zero `bridgedToken`. | + +## Role handoff + +The factory hands off the roles in this order: + +1. The adapter constructor grants `DEFAULT_ADMIN_ROLE`, `FEE_SETTER_ROLE`, and `FEE_COLLECTOR_ROLE` to the factory. +2. The factory applies the configuration. `setAssetFee` needs `FEE_SETTER_ROLE`; the other calls need `DEFAULT_ADMIN_ROLE`. +3. The factory grants `DEFAULT_ADMIN_ROLE` to `defaultAdmin`, `FEE_SETTER_ROLE` to `feeSetter`, and `FEE_COLLECTOR_ROLE` to `feeCollector`. +4. The factory renounces `FEE_COLLECTOR_ROLE`, then `FEE_SETTER_ROLE`, then `DEFAULT_ADMIN_ROLE`. + +Each role ends with exactly one holder, unless you pass the same account for several roles. Deploying the adapter with its constructor gives a different result: + +| Role | Through the factory | Through the constructor | +| -------------------- | ------------------- | --------------------------------- | +| `DEFAULT_ADMIN_ROLE` | `defaultAdmin` | `defaultAdmin` | +| `FEE_SETTER_ROLE` | `feeSetter` only | `feeSetter` and `defaultAdmin` | +| `FEE_COLLECTOR_ROLE` | `feeCollector` only | `feeCollector` and `defaultAdmin` | + +`DEFAULT_ADMIN_ROLE` is the admin of both fee roles, so `defaultAdmin` can grant or revoke them later. The adapter uses OpenZeppelin [`AccessControlEnumerable`](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControlEnumerable), so grant, revoke, and renounce each take effect in one step, with no two-step transfer. If the last `DEFAULT_ADMIN_ROLE` holder renounces it, the adapter's configuration is locked permanently. Use a multisig for `defaultAdmin`. + +Do not pass the factory's own address for any role. The factory renounces its roles in step 4, so that role would end with no holder. + +## Security notes + +**Permissionless deployment.** Anyone can deploy an adapter that points at any router and any vault. An `AdapterDeployed` event from this factory does not show who controls the adapter. Before you trust an adapter, check its role holders with `getRoleMember` and its `ROUTER`. + +**Address derivation.** The factory deploys with `CREATE`, so the adapter address depends on the factory address and its nonce. Each call to `deploy` from anyone increments the nonce. Read the address from the return value or the `AdapterDeployed` event instead of precomputing it. Do not assume an adapter has the same address on two chains. + +**No input validation beyond zero checks.** The factory does not check that `router` has code or that `vaultTarget` is an ERC-4626 vault. The repository's `pnpm ccip:deploy` script checks both before it broadcasts. The router must have code, and the vault must return a non-zero `asset()`. `ROUTER` is immutable, so a wrong router means deploying a new adapter. A `defaultAdmin` you do not control also means deploying a new adapter, because nobody else can change the configuration. + +**Settings the factory does not apply.** The factory does not configure CCIP 2.0 settings and does not fund the adapter. After deployment, the `defaultAdmin` account sets these on the adapter: + +- the return format for each CCIP 2.0 lane, with [`setEvmReturnLaneFormat`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#setevmreturnlaneformat) +- the requested finality of return legs, with [`setEvmReturnRequestedFinality`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#setevmreturnrequestedfinality), if you change the default +- the inbound finality, with [`setInboundFinality`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#setinboundfinality), if you change the default +- the Cross-Chain Verifier (CCV) policy, with [`setCCVsConfig`](/solutions/cross-chain-vault-adapter/reference/adapter-contract#setccvsconfig), if the lane's default verifiers are not enough + +Anyone can also send the native gas token to the adapter, which pays the CCIP fee for return legs. A new adapter holds none. The repository's `pnpm ccip:configure` script applies these settings and the funding. For what each setting does and which lanes need it, see [CCIP 2.0 lanes](/solutions/cross-chain-vault-adapter/overview/how-it-works#ccip-20-lanes). For an example that sets the return format and funds the adapter, see [Deploy the adapter](/solutions/cross-chain-vault-adapter/guides/deploy-the-adapter#fund-the-adapter). + +## Related contracts + +- [Adapter contract](/solutions/cross-chain-vault-adapter/reference/adapter-contract): the `CrossChainERC4626Adapter` that `deploy` creates +- [`ITypeAndVersion`](/ccip/evm/api-reference/v2.0.0/i-type-and-version) +- [`IAny2EVMMessageReceiverV2`](/ccip/evm/api-reference/v2.0.0/i-any2-evm-message-receiver-v2): the receiver interface the adapter implements +- [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec): encodes the `GenericExtraArgsV3` return format +- [OpenZeppelin `AccessControlEnumerable`](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControlEnumerable): role management on the adapter + +## Disclaimer + +Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you acknowledge and agree to these terms. diff --git a/src/layouts/SolutionLayout.astro b/src/layouts/SolutionLayout.astro index 73c3d6fa81d..b827b80ca41 100644 --- a/src/layouts/SolutionLayout.astro +++ b/src/layouts/SolutionLayout.astro @@ -5,6 +5,7 @@ import PageContent from "~/components/PageContent/PageContent.astro" import StickyHeader from "~/components/StickyHeader/StickyHeader" import TableOfContents from "~/components/TableOfContents/TableOfContents" import { CopyPageLink } from "~/components/RightSidebar/CopyPageLink" +import MoreMenu from "~/components/RightSidebar/MoreMenu.astro" import SolutionNavigation from "~/components/Solutions/SolutionNavigation.astro" import SolutionSidebar from "~/components/Solutions/SolutionSidebar.astro" import SolutionSummary from "~/components/Solutions/SolutionSummary.astro" @@ -79,6 +80,7 @@ const formattedTitle = `${frontmatter.title} | ${solution.title} | ${CONFIG.SITE
    + diff --git a/src/lib/markdown/__tests__/transformMarkdown.test.ts b/src/lib/markdown/__tests__/transformMarkdown.test.ts index cb9ead03366..d7a78e50ef5 100644 --- a/src/lib/markdown/__tests__/transformMarkdown.test.ts +++ b/src/lib/markdown/__tests__/transformMarkdown.test.ts @@ -701,3 +701,61 @@ describe("getPageLanguage", () => { expect(getPageLanguage("/path/to/file.mdx")).toBeNull() }) }) + +describe("solution content projections", () => { + it.each([ + "
    1. First item.
    2. [Second item](https://example.com).
    ", + "
      \n
    1. First item.
    2. \n
    3. [Second item](https://example.com).
    4. \n
    ", + '', + ])("preserves ordered HTML lists, including their links", async (source) => { + const result = await transformMarkdown(source, "fixture.mdx") + expect(result).toContain("1. First item.") + expect(result).toContain("2. [Second item](https://example.com).") + expect(result).not.toContain(" { + const result = await transformMarkdown( + ` + + +Repository description. + + +\`\`\`bash +forge create ExampleVault +\`\`\` + +`, + "fixture.mdx" + ) + expect(result).toContain("## 1. Deploy") + expect(result).toContain("[Repository](https://example.com/repo) New") + expect(result).toContain("Repository description.") + expect(result).toContain("```bash\nforge create ExampleVault\n```") + expect(result).not.toContain(" { + it("keeps callout list items as bullets without standalone encoded semicolon separators", async () => { + const result = await transformMarkdown( + ``, + "fixture.mdx" + ) + expect(result).toContain("> - The final transaction is submitted by the oracle.") + expect(result).toContain("> - Searchers must sign for an **exactly equal** gas price.") + expect(result).not.toContain("; ") + expect(result).not.toMatch(/^\s*>?\s*;\s*$/m) + }) +}) diff --git a/src/lib/markdown/buildMarkdownArtifact.ts b/src/lib/markdown/buildMarkdownArtifact.ts index 6882cd0e355..1a63fde172c 100644 --- a/src/lib/markdown/buildMarkdownArtifact.ts +++ b/src/lib/markdown/buildMarkdownArtifact.ts @@ -4,6 +4,7 @@ import { transformPageToMarkdown } from "./transformMarkdown.js" import type { MarkdownArtifact } from "./types.js" import { extractFrontmatter, getIsoStringOrUndefined, toCanonicalUrl, toContentRelative } from "./utils.js" import { isSunsetDocsPath } from "../../config/sunset.js" +import { withSolutionLayoutContent } from "./solutionContent.js" const SITE_BASE = "https://docs.chain.link" const CONTENT_ROOT = path.resolve("src/content") @@ -129,14 +130,15 @@ https://docs.chain.link/data-feeds/deprecating-feeds siteBase: options.siteBase ?? SITE_BASE, targetLanguage: options.targetLanguage, } + const completeBody = withSolutionLayoutContent(body, mdxAbsPath) try { return { transformMode: "normal", - markdown: await transformPageToMarkdown(body, mdxAbsPath, transformOptions), + markdown: await transformPageToMarkdown(completeBody, mdxAbsPath, transformOptions), } } catch { - const sanitizedBody = stripRuntimeMdxSyntax(body) + const sanitizedBody = stripRuntimeMdxSyntax(completeBody) try { return { diff --git a/src/lib/markdown/componentHandlers.ts b/src/lib/markdown/componentHandlers.ts index fa55c9bf970..55b007905ef 100644 --- a/src/lib/markdown/componentHandlers.ts +++ b/src/lib/markdown/componentHandlers.ts @@ -1002,10 +1002,51 @@ export function handleAccordion(node: MdxJsxNode, parent: Parent, index: number) titleSlot.parent.children.splice(titleSlot.parent.children.indexOf(titleSlot.node), 1) } const label = `${typeof number === "number" ? `${number}. ` : ""}${slotTitle || title}` - const replacement: Node[] = [headingNode(depth === 4 ? 4 : 3, label), ...((node as Parent).children || [])] + const headingDepth = node.name === "AnchoredAccordion" && depth !== 3 && depth !== 4 ? 2 : depth === 4 ? 4 : 3 + const replacement: Node[] = [headingNode(headingDepth, label), ...((node as Parent).children || [])] return replaceNode(node, parent, index, replacement) } +export function handleGitHubCard(node: MdxJsxNode, parent: Parent, index: number): number | void { + const title = staticAttribute(node, "title") + const href = staticAttribute(node, "href") + const badge = staticAttribute(node, "badge") + if (typeof title !== "string" || typeof href !== "string" || badge === NON_STATIC) return dropNode(parent, index) + const label: Node[] = [linkNode(title, href)] + if (typeof badge === "string") label.push(textNode(` ${badge}`)) + return replaceNode(node, parent, index, [paragraphNode(label), ...((node as Parent).children || [])]) +} + +export function handleHtmlList(node: MdxJsxNode, parent: Parent, index: number): number | void { + const start = staticAttribute(node, "start") + const parsedStart = typeof start === "number" || typeof start === "string" ? Number(start) : 1 + const items: Parent[] = [] + const children = ((node as Parent).children || []).flatMap((child) => + child.type === "paragraph" ? (child as Parent).children : [child] + ) + for (const child of children) { + if ( + (child.type !== "mdxJsxFlowElement" && child.type !== "mdxJsxTextElement") || + (child as MdxJsxNode).name !== "li" + ) { + continue + } + const item: Parent = { type: "listItem", children: [] } + // Reuse the flow/phrasing normalization used by all component projections. + replaceNode({ type: "mdxJsxFlowElement" }, item, 0, (child as Parent).children || []) + items.push(item) + } + return replaceNode(node, parent, index, [ + { + type: "list", + ordered: node.name === "ol", + start: node.name === "ol" && Number.isInteger(parsedStart) ? parsedStart : undefined, + spread: false, + children: items, + } as Parent, + ]) +} + export function handleAddress(node: MdxJsxNode, parent: Parent, index: number): number | void { const contractUrl = staticAttribute(node, "contractUrl") const address = staticAttribute(node, "address") diff --git a/src/lib/markdown/llms.ts b/src/lib/markdown/llms.ts new file mode 100644 index 00000000000..e6be92aa3f6 --- /dev/null +++ b/src/lib/markdown/llms.ts @@ -0,0 +1,119 @@ +import { transformPageBodyToMarkdown } from "./buildMarkdownArtifact.js" +import { unescapeMarkdown } from "./formatters.js" +import type { MarkdownArtifact } from "./types.js" +import { + extractFrontmatter, + getIsoStringOrUndefined, + inferTitleFromPath, + toCanonicalUrl, + toContentRelative, +} from "./utils.js" + +const SITE_BASE = "https://docs.chain.link" + +// Body text uses transformPageBodyToMarkdown, the same function as the Markdown route. +// unescapeMarkdown only cleans the plain-text llms dump after that transform. +export async function renderLlmsPageMarkdown( + raw: string, + absFile: string, + section: string, + targetLanguage?: string +): Promise<{ markdown: string; transformMode: MarkdownArtifact["transformMode"] }> { + const { body, fmTitle, fmLastModified } = extractFrontmatter(raw) + const relFromContent = toContentRelative(absFile) + const sourceUrl = toCanonicalUrl(section, relFromContent, SITE_BASE) + const title = fmTitle || inferTitleFromPath(relFromContent) + const lastModified = getIsoStringOrUndefined(fmLastModified) + const transformed = await transformPageBodyToMarkdown(body, absFile, { + siteBase: SITE_BASE, + targetLanguage, + }) + const cleanedPlain = unescapeMarkdown(transformed.markdown) + const headerLines = [ + `# ${title}`, + `Source: ${sourceUrl}`, + ...(lastModified ? [`Last Updated: ${lastModified}`] : []), + "", + ] + + return { + markdown: [...headerLines, cleanedPlain.trim()].join("\n"), + transformMode: transformed.transformMode, + } +} + +const CODE_EXAMPLE_DISCLAIMER_MARKERS = [ + "do not use the code in this example", + "this code represents an example", + "example code", + "educational example", + "educational purposes", + "community examples", +] + +function isCodeExampleDisclaimer(plain: string): boolean { + if (!plain.includes("as is") || !plain.includes("as available")) return false + return CODE_EXAMPLE_DISCLAIMER_MARKERS.some((marker) => plain.includes(marker)) +} + +function isFenceLine(line: string): boolean { + const trimmed = line.trim() + return trimmed.startsWith("```") || trimmed.startsWith("~~~") +} + +function blockquotePlainText(lines: string[]): string { + return lines + .map((line) => line.replace(/^\s*>\s?/, "")) + .join(" ") + .replace(/\s+/g, " ") + .trim() + .toLowerCase() +} + +function omitRepeatedDisclaimerBlocks(markdown: string, seen: Set): string { + const lines = markdown.split("\n") + const kept: string[] = [] + let index = 0 + let inFence = false + + while (index < lines.length) { + const line = lines[index] + if (isFenceLine(line)) { + inFence = !inFence + kept.push(line) + index += 1 + continue + } + + if (!inFence && line.trim().startsWith(">")) { + const block: string[] = [] + while (index < lines.length && !isFenceLine(lines[index]) && lines[index].trim().startsWith(">")) { + block.push(lines[index]) + index += 1 + } + const plain = blockquotePlainText(block) + if (isCodeExampleDisclaimer(plain)) { + if (seen.has(plain)) { + if (kept.length > 0 && kept[kept.length - 1].trim() === "") kept.pop() + continue + } + seen.add(plain) + } + kept.push(...block) + continue + } + + kept.push(line) + index += 1 + } + + return kept.join("\n") +} + +// Keep the first copy of each code-example disclaimer where it already appears. +// Drop later copies of that same text. +export function assembleLlmsDocument(pages: string[], options: { deduplicateDisclaimers?: boolean } = {}): string { + if (options.deduplicateDisclaimers === false) return pages.join("\n\n---\n\n") + "\n" + const seen = new Set() + return pages.map((page) => omitRepeatedDisclaimerBlocks(page, seen)).join("\n\n---\n\n") + "\n" +} diff --git a/src/lib/markdown/solutionContent.ts b/src/lib/markdown/solutionContent.ts new file mode 100644 index 00000000000..c9ba8212073 --- /dev/null +++ b/src/lib/markdown/solutionContent.ts @@ -0,0 +1,34 @@ +import path from "node:path" +import { ALL_REGISTERED_SOLUTIONS, SOLUTION_PRODUCTS, toPageId } from "../../config/solutions/index.ts" +import { SOLUTION_DISCLAIMER } from "../../config/solutions/disclaimer.ts" +import { formatHeading, formatLink } from "./formatters.js" + +/** Include the content that SolutionLayout adds outside the MDX body in every Markdown/TXT export. */ +export function withSolutionLayoutContent(body: string, sourcePath: string): string { + const relative = path.relative(path.resolve("src/content/solutions"), sourcePath).split(path.sep).join("/") + if (relative.startsWith("../") || path.isAbsolute(relative)) return body + const id = relative.replace(/\.mdx?$/, "") + const slug = id.split("/")[0] + const solution = ALL_REGISTERED_SOLUTIONS.find((solution) => solution.slug === slug) + if (!solution) return body + + const sections: string[] = [] + if (toPageId(slug, id) === "index") { + const products = solution.products.map((key) => SOLUTION_PRODUCTS[key]).filter(Boolean) + sections.push( + formatHeading(2, "What is it") + solution.description, + products.map((product) => formatLink(product.label, product.href)).join(" · "), + solution.categories.join(", "), + solution.repoUrl ? formatLink("View on GitHub", solution.repoUrl) : "" + ) + } + sections.push( + body.trim(), + formatHeading(2, "Disclaimer") + + SOLUTION_DISCLAIMER.beforeTerms + + " " + + formatLink(SOLUTION_DISCLAIMER.termsLabel, SOLUTION_DISCLAIMER.termsUrl) + + SOLUTION_DISCLAIMER.afterTerms + ) + return sections.filter(Boolean).join("\n\n") +} diff --git a/src/lib/markdown/solutionLlms.ts b/src/lib/markdown/solutionLlms.ts new file mode 100644 index 00000000000..8804b6d6379 --- /dev/null +++ b/src/lib/markdown/solutionLlms.ts @@ -0,0 +1,60 @@ +import fs from "node:fs/promises" +import path from "node:path" +import type { Solution } from "../../config/solutions/index.ts" +import { collectSolutionPages, getOrderedSolutionPages, type SolutionPage } from "../../utils/solutions.js" +import { assembleLlmsDocument, renderLlmsPageMarkdown } from "./llms.js" +import { extractFrontmatter, toContentRelative } from "./utils.js" + +const SOLUTIONS_ROOT = path.resolve("src/content/solutions") + +export function solutionLlmsOutputPath(slug: string, outputRoot = SOLUTIONS_ROOT): string { + return path.join(outputRoot, slug, "llms-full.txt") +} + +/** Match the solutions collection's MD/MDX discovery without requiring Astro's virtual modules. */ +export async function readSolutionLlmsPages(solution: Solution, contentRoot = SOLUTIONS_ROOT): Promise { + const pages: SolutionPage[] = [] + async function walk(directory: string): Promise { + const entries = await fs.readdir(directory, { withFileTypes: true }) + for (const entry of entries) { + const filePath = path.join(directory, entry.name) + if (entry.isDirectory()) { + await walk(filePath) + } else if (entry.isFile() && /\.mdx?$/.test(entry.name) && entry.name !== "README.md") { + const { fmTitle } = extractFrontmatter(await fs.readFile(filePath, "utf8")) + if (!fmTitle) throw new Error(`[solutions] "${filePath}" has no page title.`) + pages.push({ + id: path + .relative(contentRoot, filePath) + .split(path.sep) + .join("/") + .replace(/\.mdx?$/, ""), + filePath, + data: { title: fmTitle }, + }) + } + } + } + await walk(path.join(contentRoot, solution.slug)) + return pages +} + +/** Both the CLI and the Astro endpoint use this exact conversion, order, and disclaimer policy. */ +export async function renderSolutionLlmsDocument(solution: Solution, pages: readonly SolutionPage[]): Promise { + const orderedPages = getOrderedSolutionPages(solution, collectSolutionPages(solution.slug, pages)) + const outputs: string[] = [] + for (const entry of orderedPages) { + if (!entry.filePath) { + throw new Error(`[solutions] "${entry.id}" has no source file for its LLM documentation.`) + } + const absFile = path.resolve(entry.filePath) + const raw = await fs.readFile(absFile, "utf8") + const rendered = await renderLlmsPageMarkdown(raw, absFile, "solutions") + if (rendered.transformMode !== "normal") { + console.warn(`[llms] ${toContentRelative(absFile)}: transform mode ${rendered.transformMode}`) + } + outputs.push(rendered.markdown) + } + // Preserve the complete disclaimer and any MDX callouts on every solution page. + return assembleLlmsDocument(outputs, { deduplicateDisclaimers: false }) +} diff --git a/src/lib/markdown/transformMarkdown.ts b/src/lib/markdown/transformMarkdown.ts index d1fd5de2805..5cd89280e9e 100644 --- a/src/lib/markdown/transformMarkdown.ts +++ b/src/lib/markdown/transformMarkdown.ts @@ -7,7 +7,7 @@ import remarkParse from "remark-parse" import remarkMdx from "remark-mdx" import remarkGfm from "remark-gfm" import remarkStringify from "remark-stringify" -import { visit } from "unist-util-visit" +import { SKIP, visit } from "unist-util-visit" import type { Node, Parent, Literal } from "unist" import type { TransformConfig, MdxJsxNode, ComponentContext } from "./types.js" import { @@ -27,6 +27,8 @@ import { handlePackageManagerTabs, handleFragment, handleAccordion, + handleGitHubCard, + handleHtmlList, handleAddress, handleCallout, handleAnyApiCallout, @@ -124,7 +126,7 @@ export async function transformMarkdown( processor, } - visit(tree, (node: Node, index: number | undefined, parent: Parent | undefined) => { + const projectNode = (node: Node, index: number | undefined, parent: Parent | undefined) => { if (!parent || typeof index !== "number") return // Handle CodeHighlightBlockMulti @@ -180,10 +182,17 @@ export async function transformMarkdown( return handlePackageManagerTabs(node as MdxJsxNode, parent, index) } - if (node.type === "mdxJsxFlowElement" && (node as MdxJsxNode).name === "Accordion") { + if ( + node.type === "mdxJsxFlowElement" && + ((node as MdxJsxNode).name === "Accordion" || (node as MdxJsxNode).name === "AnchoredAccordion") + ) { return handleAccordion(node as MdxJsxNode, parent, index) } + if (node.type === "mdxJsxFlowElement" && (node as MdxJsxNode).name === "GitHubCard") { + return handleGitHubCard(node as MdxJsxNode, parent, index) + } + if (node.type === "mdxJsxFlowElement" && (node as MdxJsxNode).name === "Callout") { return handleCallout(node as MdxJsxNode, parent, index, context) } @@ -256,6 +265,10 @@ export async function transformMarkdown( const nodeName = mdxNode.name const children = (mdxNode as Parent).children || [] + if (nodeName === "ol" || (nodeName === "ul" && mdxNode.type === "mdxJsxFlowElement")) { + return handleHtmlList(mdxNode, parent, index) + } + if (nodeName === "a") { const hrefAttribute = mdxNode.attributes?.find((attribute) => attribute.name === "href") const href = @@ -366,6 +379,11 @@ export async function transformMarkdown( } // Note: We preserve link nodes as-is so they're rendered as markdown links [text](url) + } + visit(tree, (node, index, parent) => { + const nextIndex = projectNode(node, index, parent) + // A replaced node is detached. Traverse its replacement instead of mutating its old children. + return typeof nextIndex === "number" ? ([SKIP, nextIndex] as [typeof SKIP, number]) : undefined }) }) .use(remarkStringify, { diff --git a/src/pages/solutions/[slug]/llms-full.txt.ts b/src/pages/solutions/[slug]/llms-full.txt.ts new file mode 100644 index 00000000000..78f003bc389 --- /dev/null +++ b/src/pages/solutions/[slug]/llms-full.txt.ts @@ -0,0 +1,33 @@ +import type { APIRoute, GetStaticPaths } from "astro" +import { SOLUTIONS, type Solution } from "../../../config/solutions/index.ts" +import { renderSolutionLlmsDocument } from "../../../lib/markdown/solutionLlms.js" +import { getOrderedSolutionPages, getSolutionPages, type SolutionEntry } from "../../../utils/solutions.js" + +export const prerender = true + +type Props = { solution: Solution; pages: SolutionEntry[] } + +export const getStaticPaths: GetStaticPaths = async () => { + return Promise.all( + SOLUTIONS.map(async (solution) => { + const pages = await getSolutionPages(solution.slug) + const orderedPages = getOrderedSolutionPages(solution, pages) + + return { + params: { slug: solution.slug }, + props: { solution, pages: orderedPages }, + } + }) + ) +} + +export const GET: APIRoute = async ({ props }) => { + const { solution, pages } = props as Props + const document = await renderSolutionLlmsDocument(solution, pages) + return new Response(document, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Content-Disposition": `inline; filename="${solution.slug}-llms-full.txt"`, + }, + }) +} diff --git a/src/scripts/check-markdown-fidelity.test.ts b/src/scripts/check-markdown-fidelity.test.ts index c1a0597815f..37369309980 100644 --- a/src/scripts/check-markdown-fidelity.test.ts +++ b/src/scripts/check-markdown-fidelity.test.ts @@ -752,3 +752,107 @@ describe("stable report JSON", () => { }) }) }) + +describe("solution component fidelity", () => { + const source = `
    1. First item.
    2. Second item.
    + + + +\`\`\`bash +forge create ExampleVault +\`\`\` + + +Repository description. + + +` + const complete = + "1. First item.\n2. Second item.\n\n### 2. Deploy\n\n```bash\nforge create ExampleVault\n```\n\n[Repository](https://example.com/repo)\n\nRepository description." + + test("checks ordered lists, anchored accordion bodies, and GitHub cards independently", () => { + const findings = compareSourceToArtifact("fixture", "src/content/fixture.mdx", source, artifact(complete)) + expect(findings.length).toBeGreaterThan(5) + expect(blockingFindings("focused", findings)).toEqual([]) + }) + + test("detects missing commands inside an anchored accordion", () => { + const missing = complete.replace("```bash\nforge create ExampleVault\n```", "") + const findings = compareSourceToArtifact("fixture", "src/content/fixture.mdx", source, artifact(missing)) + expect(findings).toContainEqual( + expect.objectContaining({ status: "missing", expected: "forge create ExampleVault" }) + ) + }) +}) + +test("checks unordered lists inside callouts without requiring artificial separators", () => { + const source = `` + const output = "> **CAUTION: Important**\n>\n> - First requirement.\n> - Second requirement." + expect( + blockingFindings("focused", compareSourceToArtifact("fixture", "fixture.mdx", source, artifact(output))) + ).toEqual([]) +}) + +test.each(["ol", "ul"])("coalesces projected inline HTML in %s items before a repeated later value", (list) => { + const source = ` + +## Before you begin + +Follow the required steps and keep all values in one place for chain1. + +## Later reference + +\`GAS_LIMIT\`` + const marker = list === "ol" ? "1." : "-" + const nextMarker = list === "ol" ? "2." : "-" + const output = `> **NOTE** +> +> ${marker} Foundry scripts default to **400,000** gas unless you set \`GAS_LIMIT\` (and fall back if estimation fails). +> ${nextMarker} Test all scenarios before deployment. + +## Before you begin + +Follow the **required** steps and keep all values in one place for chain1. + +## Later reference + +\`GAS_LIMIT\`` + + const findings = compareSourceToArtifact("fixture", "fixture.mdx", source, artifact(output)) + expect(blockingFindings("focused", findings)).toEqual([]) + expect(findings).toContainEqual(expect.objectContaining({ status: "present", expected: "Before you begin" })) + const missing = compareSourceToArtifact( + "fixture", + "fixture.mdx", + source, + artifact(output.replace("Test all scenarios before deployment.", "")) + ) + expect(missing).toContainEqual( + expect.objectContaining({ status: "missing", expected: "Test all scenarios before deployment." }) + ) +}) + +test("keeps diagnostics for dynamic inline HTML attributes and unprojected elements", () => { + const source = "Keep this fact and unprojected emphasis." + const analysis = analyzeSourceMarkdown(source, "fixture.mdx") + expect(analysis.diagnostics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ status: "unverifiable", name: "strong.title" }), + expect.objectContaining({ status: "unverifiable", name: "em" }), + ]) + ) +}) diff --git a/src/scripts/generate-llms.test.ts b/src/scripts/generate-llms.test.ts index aa5e63b6dbe..8c989071561 100644 --- a/src/scripts/generate-llms.test.ts +++ b/src/scripts/generate-llms.test.ts @@ -1,9 +1,153 @@ import path from "node:path" +import fs from "node:fs/promises" +import os from "node:os" import { describe, expect, test } from "@jest/globals" import { transformPageBodyToMarkdown } from "@lib/markdown/buildMarkdownArtifact.js" import { unescapeMarkdown } from "@lib/markdown/formatters.js" import { extractFrontmatter } from "@lib/markdown/utils.js" -import { assembleLlmsDocument, renderLlmsPageMarkdown } from "./generate-llms.js" +import { + assembleLlmsDocument, + generateSolutionLlmsFiles, + orderPages, + renderLlmsPageMarkdown, + selectLlmsTargets, +} from "./generate-llms.js" +import { SOLUTIONS } from "../config/solutions/index.ts" +import { crossChainVaultAdapter } from "../config/solutions/cross-chain-vault-adapter.ts" +import { demoSinglePage } from "../config/solutions/demo-single-page.ts" +import { demoMultiPage } from "../config/solutions/demo-multi-page.ts" +import { collectSolutionPages } from "../utils/solutions.js" +import { readSolutionLlmsPages, renderSolutionLlmsDocument } from "../lib/markdown/solutionLlms.js" +import { llmsFilesToValidate, validateLlmsFile } from "./validate-llms.js" +import { SOLUTION_DISCLAIMER } from "../config/solutions/disclaimer.ts" + +test("sidebar links order discovered pages without adding a solution to a product export", () => { + const first = path.resolve("src/content/ccip/first.mdx") + const second = path.resolve("src/content/ccip/second.mdx") + const solution = path.resolve("src/content/solutions/example/index.mdx") + expect(orderPages("ccip", [first, second], [second, solution, second, first])).toEqual([second, first]) +}) + +describe("solution CLI exports", () => { + test("includes all published solutions by default and supports solution-only and product-only selection", () => { + const published = SOLUTIONS.filter((solution) => !solution.hidden) + expect(selectLlmsTargets().solutions).toEqual(published) + expect(selectLlmsTargets().sections).toContain("ccip") + expect(selectLlmsTargets("solutions")).toEqual({ sections: [], solutions: published }) + expect(selectLlmsTargets(`solutions/${crossChainVaultAdapter.slug}`)).toEqual({ + sections: [], + solutions: [crossChainVaultAdapter], + }) + expect(selectLlmsTargets("ccip")).toEqual({ sections: ["ccip"], solutions: [] }) + expect(() => selectLlmsTargets(`solutions/${demoSinglePage.slug}`)).toThrow("Unknown or hidden solution") + expect(() => selectLlmsTargets("solutions/does-not-exist")).toThrow("Unknown or hidden solution") + expect( + llmsFilesToValidate() + .filter((file) => file.solution) + .map((file) => file.solution) + ).toEqual(published) + expect(llmsFilesToValidate().find((file) => file.label === "cre-go")?.path).toContain("llms-full-go.txt") + }) + + test.each([demoSinglePage, demoMultiPage])( + "writes $slug only when published, including its complete page set", + async (fixture) => { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "solution-llms-cli-")) + try { + expect(await generateSolutionLlmsFiles([fixture], directory)).toEqual([]) + expect(await fs.readdir(directory)).toEqual([]) + const published = { ...fixture, hidden: false } + const reports = await generateSolutionLlmsFiles([published], directory) + const document = await fs.readFile(path.join(directory, fixture.slug, "llms-full.txt"), "utf8") + const pageCount = fixture.nav?.flatMap((group) => group.pages).length ?? 1 + expect(reports).toHaveLength(1) + expect(reports[0].pagesProcessed).toBe(pageCount) + expect(reports[0].bytes).toBe(Buffer.byteLength(document)) + expect(document.match(/^Source: /gm)).toHaveLength(pageCount) + expect(document.match(/^## Disclaimer$/gm)).toHaveLength(pageCount) + expect(document).toContain(`[${SOLUTION_DISCLAIMER.termsLabel}](${SOLUTION_DISCLAIMER.termsUrl})`) + expect( + await validateLlmsFile({ + path: path.join(directory, fixture.slug, "llms-full.txt"), + label: fixture.slug, + maxBytes: 5_000_000, + solution: published, + }) + ).toEqual({ errors: [], warnings: [] }) + } finally { + await fs.rm(directory, { recursive: true, force: true }) + } + } + ) + + test("validates real generated pages and detects missing pages, duplicates, and incomplete disclaimers", async () => { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "solution-llms-validation-")) + try { + await generateSolutionLlmsFiles([crossChainVaultAdapter], directory) + const file = { + path: path.join(directory, crossChainVaultAdapter.slug, "llms-full.txt"), + label: crossChainVaultAdapter.slug, + maxBytes: 5_000_000, + solution: crossChainVaultAdapter, + } + const document = await fs.readFile(file.path, "utf8") + expect(await validateLlmsFile(file)).toEqual({ errors: [], warnings: [] }) + const headers = [...document.matchAll(/^# [^\n]+\nSource: [^\n]+\n/gm)] + expect(headers).toHaveLength(10) + await fs.writeFile(file.path, document.slice(headers[1].index), "utf8") + expect((await validateLlmsFile(file)).errors).toContain( + `${file.label}: solution pages are missing, duplicated, or out of navigation order` + ) + await fs.writeFile(file.path, document + document.slice(headers[0].index, headers[1].index), "utf8") + expect((await validateLlmsFile(file)).errors).toContain( + `${file.label}: solution pages are missing, duplicated, or out of navigation order` + ) + await fs.writeFile(file.path, document.replace(SOLUTION_DISCLAIMER.afterTerms, ""), "utf8") + expect((await validateLlmsFile(file)).errors.some((error) => error.includes("complete disclaimer missing"))).toBe( + true + ) + } finally { + await fs.rm(directory, { recursive: true, force: true }) + } + }, 30_000) + + test("discovers MD/MDX pages without treating the generated TXT or README as solution pages", async () => { + const pages = await readSolutionLlmsPages(crossChainVaultAdapter) + expect(pages).toHaveLength(10) + expect(pages.every((page) => page.filePath?.endsWith(".mdx"))).toBe(true) + }) + + test("rejects ambiguous sources and navigation that would omit or duplicate content", async () => { + const slug = crossChainVaultAdapter.slug + const first = { id: `${slug}/index`, data: { title: "First" } } + const second = { id: `${slug}/second`, data: { title: "Second" } } + expect(() => collectSolutionPages(slug, [first, { ...first, id: slug }])).toThrow("more than one source") + await expect( + renderSolutionLlmsDocument({ ...crossChainVaultAdapter, nav: undefined }, [first, second]) + ).rejects.toThrow("declares no") + await expect(renderSolutionLlmsDocument({ ...crossChainVaultAdapter, nav: undefined }, [second])).rejects.toThrow( + "has no landing page" + ) + await expect( + renderSolutionLlmsDocument( + { ...crossChainVaultAdapter, nav: [{ group: "Pages", pages: ["index", "index", "second"] }] }, + [first, second] + ) + ).rejects.toThrow("more than once") + await expect( + renderSolutionLlmsDocument({ ...crossChainVaultAdapter, nav: [{ group: "Pages", pages: ["index"] }] }, [ + first, + second, + ]) + ).rejects.toThrow("not listed in the nav") + await expect( + renderSolutionLlmsDocument( + { ...crossChainVaultAdapter, nav: [{ group: "Pages", pages: ["index", "missing"] }] }, + [first, second] + ) + ).rejects.toThrow("but no page exists") + }) +}) describe("renderLlmsPageMarkdown", () => { test("uses the same page body as the Markdown route", async () => { @@ -69,6 +213,12 @@ describe("assembleLlmsDocument", () => { expect(document.match(/This guide is an example\./g)).toHaveLength(1) }) + test("preserves a disclaimer on every solution page when deduplication is disabled", () => { + const document = assembleLlmsDocument([page("One"), page("Two")], { deduplicateDisclaimers: false }) + expect(document.match(/Educational Example Disclaimer/g)).toHaveLength(2) + expect(document.slice(document.indexOf("# Two"))).toContain(disclaimer) + }) + test("keeps a different repeated callout and a disclaimer inside a code fence", () => { const note = "> **NOTE: Authentication required**\n>\n> Log in first." const fenced = [ diff --git a/src/scripts/generate-llms.ts b/src/scripts/generate-llms.ts index 4deb98abe0c..ba34fa579bb 100644 --- a/src/scripts/generate-llms.ts +++ b/src/scripts/generate-llms.ts @@ -6,127 +6,24 @@ import { fileURLToPath } from "node:url" import { LLM_SECTIONS_CONFIG, SUPPORTED_LLM_SECTIONS, type LlmsSectionConfig } from "../config/llms.js" import { SIDEBAR } from "../config/sidebar.js" import type { SectionEntry, SectionContent } from "../config/sidebar.js" -import { transformPageBodyToMarkdown } from "../lib/markdown/buildMarkdownArtifact.js" -import { unescapeMarkdown } from "../lib/markdown/formatters.js" -import type { MarkdownArtifact } from "../lib/markdown/types.js" +import { assembleLlmsDocument, renderLlmsPageMarkdown } from "../lib/markdown/llms.js" +import { SOLUTIONS, type Solution } from "../config/solutions/index.ts" +import { + readSolutionLlmsPages, + renderSolutionLlmsDocument, + solutionLlmsOutputPath, +} from "../lib/markdown/solutionLlms.js" + import { extractFrontmatter, - getIsoStringOrUndefined, getPageLanguage, - inferTitleFromPath, shouldIncludeInLanguageFile, - toCanonicalUrl, toContentRelative, } from "../lib/markdown/utils.js" -const SITE_BASE = "https://docs.chain.link" - -// Body text uses transformPageBodyToMarkdown, the same function as the Markdown route. -// unescapeMarkdown only cleans the plain-text llms dump after that transform. -export async function renderLlmsPageMarkdown( - raw: string, - absFile: string, - section: string, - targetLanguage?: string -): Promise<{ markdown: string; transformMode: MarkdownArtifact["transformMode"] }> { - const { body, fmTitle, fmLastModified } = extractFrontmatter(raw) - const relFromContent = toContentRelative(absFile) - const sourceUrl = toCanonicalUrl(section, relFromContent, SITE_BASE) - const title = fmTitle || inferTitleFromPath(relFromContent) - const lastModified = getIsoStringOrUndefined(fmLastModified) - const transformed = await transformPageBodyToMarkdown(body, absFile, { - siteBase: SITE_BASE, - targetLanguage, - }) - const cleanedPlain = unescapeMarkdown(transformed.markdown) - const headerLines = [ - `# ${title}`, - `Source: ${sourceUrl}`, - ...(lastModified ? [`Last Updated: ${lastModified}`] : []), - "", - ] - - return { - markdown: [...headerLines, cleanedPlain.trim()].join("\n"), - transformMode: transformed.transformMode, - } -} - -// Current pages and the wording in smartcontractkit/documentation#4200. -const CODE_EXAMPLE_DISCLAIMER_MARKERS = [ - "do not use the code in this example", - "this code represents an example", - "example code", - "educational example", - "educational purposes", - "community examples", -] - -function isCodeExampleDisclaimer(plain: string): boolean { - if (!plain.includes("as is") || !plain.includes("as available")) return false - return CODE_EXAMPLE_DISCLAIMER_MARKERS.some((marker) => plain.includes(marker)) -} - -function isFenceLine(line: string): boolean { - const trimmed = line.trim() - return trimmed.startsWith("```") || trimmed.startsWith("~~~") -} - -function blockquotePlainText(lines: string[]): string { - return lines - .map((line) => line.replace(/^\s*>\s?/, "")) - .join(" ") - .replace(/\s+/g, " ") - .trim() - .toLowerCase() -} - -function omitRepeatedDisclaimerBlocks(markdown: string, seen: Set): string { - const lines = markdown.split("\n") - const kept: string[] = [] - let index = 0 - let inFence = false - - while (index < lines.length) { - const line = lines[index] - if (isFenceLine(line)) { - inFence = !inFence - kept.push(line) - index += 1 - continue - } - - if (!inFence && line.trim().startsWith(">")) { - const block: string[] = [] - while (index < lines.length && !isFenceLine(lines[index]) && lines[index].trim().startsWith(">")) { - block.push(lines[index]) - index += 1 - } - const plain = blockquotePlainText(block) - if (isCodeExampleDisclaimer(plain)) { - if (seen.has(plain)) { - if (kept.length > 0 && kept[kept.length - 1].trim() === "") kept.pop() - continue - } - seen.add(plain) - } - kept.push(...block) - continue - } +export { assembleLlmsDocument, renderLlmsPageMarkdown } from "../lib/markdown/llms.js" - kept.push(line) - index += 1 - } - - return kept.join("\n") -} - -// Keep the first copy of each code-example disclaimer where it already appears. -// Drop later copies of that same text. -export function assembleLlmsDocument(pages: string[]): string { - const seen = new Set() - return pages.map((page) => omitRepeatedDisclaimerBlocks(page, seen)).join("\n\n---\n\n") + "\n" -} +const SITE_BASE = "https://docs.chain.link" type SectionReport = { section: string @@ -141,11 +38,57 @@ type Report = { startedAt: string; finishedAt?: string; siteBase: string; sectio type CliArgs = { section?: string pagesFile?: string + reportPath?: string +} + +export function selectLlmsTargets(section?: string): { sections: string[]; solutions: Solution[] } { + const solutions = SOLUTIONS.filter((solution) => !solution.hidden) + if (!section) return { sections: [...SUPPORTED_LLM_SECTIONS], solutions } + if (section === "solutions") return { sections: [], solutions } + if (section.startsWith("solutions/")) { + const selected = solutions.filter((solution) => `solutions/${solution.slug}` === section) + if (!selected.length) throw new Error(`[llms] Unknown or hidden solution: ${section}`) + return { sections: [], solutions: selected } + } + return { sections: [section], solutions: [] } +} + +export async function generateSolutionLlmsFiles( + solutions: readonly Solution[], + outputRoot?: string +): Promise { + const reports: SectionReport[] = [] + for (const solution of solutions) { + if (solution.hidden) continue + const pages = await readSolutionLlmsPages(solution) + const document = await renderSolutionLlmsDocument(solution, pages) + const outPath = solutionLlmsOutputPath(solution.slug, outputRoot) + let prevBytes: number | undefined + try { + prevBytes = (await fs.stat(outPath)).size + } catch {} + await fs.mkdir(path.dirname(outPath), { recursive: true }) + await fs.writeFile(outPath, document, "utf8") + const bytes = Buffer.byteLength(document, "utf8") + reports.push({ + section: `solutions/${solution.slug}`, + pagesProcessed: pages.length, + outputPath: path.relative(process.cwd(), outPath), + bytes, + prevBytes, + deltaBytes: prevBytes === undefined ? undefined : bytes - prevBytes, + }) + console.log(`[llms] solutions/${solution.slug} → /solutions/${solution.slug}/llms-full.txt (${pages.length} pages)`) + } + return reports } async function main() { const args = parseArgs(process.argv.slice(2)) - const sections = args.section ? [args.section] : [...SUPPORTED_LLM_SECTIONS] + const { sections, solutions } = selectLlmsTargets(args.section) + if (args.pagesFile && args.section?.startsWith("solutions")) { + throw new Error("[llms] --pages is only supported for product sections; solutions use their complete navigation.") + } const report: Report = { startedAt: new Date().toISOString(), @@ -258,8 +201,9 @@ async function main() { } } + report.sections.push(...(await generateSolutionLlmsFiles(solutions))) report.finishedAt = new Date().toISOString() - await writeReports(report) + await writeReports(report, args.reportPath) console.log("LLM generation complete.") } @@ -269,6 +213,7 @@ function parseArgs(argv: string[]): CliArgs { const a = argv[i] if (a === "--section") args.section = argv[++i] if (a === "--pages") args.pagesFile = argv[++i] + if (a === "--report") args.reportPath = argv[++i] } return args } @@ -296,9 +241,11 @@ async function walk(dir: string, out: string[]) { } } -function orderPages(section: string, pages: string[], explicitOrder?: string[]): string[] { +export function orderPages(section: string, pages: string[], explicitOrder?: string[]): string[] { if (explicitOrder && explicitOrder.length > 0) { - const absOrder = explicitOrder.map((p) => path.resolve(p)) + const discovered = new Set(pages.map((p) => path.resolve(p))) + // Navigation determines order, not scope: linked solutions must stay in their own TXT exports. + const absOrder = [...new Set(explicitOrder.map((p) => path.resolve(p)).filter((p) => discovered.has(p)))] const set = new Set(absOrder) const rest = pages.filter((p) => !set.has(path.resolve(p))).sort() return [...absOrder, ...rest] @@ -340,10 +287,9 @@ function getSidebarFileOrder(sectionKey: string): string[] { return filePaths } -async function writeReports(report: Report) { - const reportsDir = path.resolve("reports") - await fs.mkdir(reportsDir, { recursive: true }) - const jsonPath = path.join(reportsDir, "llms-report.json") +async function writeReports(report: Report, reportPath?: string) { + const jsonPath = path.resolve(reportPath ?? "reports/llms-report.json") + await fs.mkdir(path.dirname(jsonPath), { recursive: true }) await fs.writeFile(jsonPath, JSON.stringify(report, null, 2), "utf-8") } diff --git a/src/scripts/validate-llms.ts b/src/scripts/validate-llms.ts index 8225a880379..cfc1f305317 100644 --- a/src/scripts/validate-llms.ts +++ b/src/scripts/validate-llms.ts @@ -1,62 +1,91 @@ #!/usr/bin/env node import fs from "fs/promises" import path from "path" +import { fileURLToPath } from "node:url" import { SUPPORTED_LLM_SECTIONS, LLM_SECTIONS_CONFIG } from "../config/llms.js" +import { SOLUTIONS, toPagePath, type Solution } from "../config/solutions/index.ts" +import { SOLUTION_DISCLAIMER } from "../config/solutions/disclaimer.ts" +import { solutionLlmsOutputPath } from "../lib/markdown/solutionLlms.js" -async function main() { - const errors: string[] = [] - const warnings: string[] = [] - const SOFT_LIMIT_BYTES = 3_000_000 // ~3 MB warn threshold - const HARD_LIMIT_BYTES = 5_000_000 // ~5 MB fail threshold +type LlmsFile = { path: string; label: string; maxBytes: number; solution?: Solution } +const SOFT_LIMIT_BYTES = 3_000_000 // ~3 MB warn threshold +const HARD_LIMIT_BYTES = 5_000_000 // ~5 MB fail threshold +export function llmsFilesToValidate(): LlmsFile[] { + const files: LlmsFile[] = [] for (const section of SUPPORTED_LLM_SECTIONS) { - const cfg = LLM_SECTIONS_CONFIG[section as keyof typeof LLM_SECTIONS_CONFIG] - const languages = cfg?.languages || [] - - const filesToCheck: Array<{ path: string; label: string }> = [] - - if (languages.length > 0) { - // Check language-specific files - for (const lang of languages) { - filesToCheck.push({ - path: path.resolve(`src/content/${section}/llms-full-${lang}.txt`), - label: `${section}-${lang}`, - }) - } - } else { - // Check single file (backward compatibility) - filesToCheck.push({ - path: path.resolve(`src/content/${section}/llms-full.txt`), - label: section, + const cfg = LLM_SECTIONS_CONFIG[section] + const filenames = cfg.languages?.length ? cfg.languages.map((lang) => `llms-full-${lang}.txt`) : ["llms-full.txt"] + for (const filename of filenames) { + files.push({ + path: path.resolve("src/content", section, filename), + label: filename === "llms-full.txt" ? section : `${section}-${filename.slice(10, -4)}`, + maxBytes: cfg.maxBytes ?? HARD_LIMIT_BYTES, }) } + } + for (const solution of SOLUTIONS.filter((solution) => !solution.hidden)) { + files.push({ + path: solutionLlmsOutputPath(solution.slug), + label: `solutions/${solution.slug}`, + maxBytes: HARD_LIMIT_BYTES, + solution, + }) + } + return files +} - for (const { path: filePath, label } of filesToCheck) { - try { - const content = await fs.readFile(filePath, "utf-8") - const sizeBytes = Buffer.byteLength(content, "utf-8") - if (sizeBytes > HARD_LIMIT_BYTES) { - errors.push(`${label}: file size ${sizeBytes} bytes exceeds hard limit ${HARD_LIMIT_BYTES}`) - } else if (sizeBytes > SOFT_LIMIT_BYTES) { - warnings.push(`${label}: file size ${sizeBytes} bytes exceeds soft limit ${SOFT_LIMIT_BYTES}`) - } - if (!content.trim()) errors.push(`${label}: file is empty`) - - // Basic link checks (absolute links) - const links = Array.from(content.matchAll(/\((https?:[^)]+)\)/g)).map((m) => m[1]) - const hasBadTracking = links.some((u) => /[?&]utm_/i.test(u)) - if (hasBadTracking) warnings.push(`${label}: found tracking params in links`) +export async function validateLlmsFile(file: LlmsFile): Promise<{ errors: string[]; warnings: string[] }> { + const { path: filePath, label, maxBytes, solution } = file + const errors: string[] = [] + const warnings: string[] = [] + let content: string + try { + content = await fs.readFile(filePath, "utf8") + } catch { + return { errors: [`${label}: file not found at ${filePath}`], warnings } + } + const sizeBytes = Buffer.byteLength(content, "utf8") + if (sizeBytes > maxBytes) errors.push(`${label}: file size ${sizeBytes} bytes exceeds hard limit ${maxBytes}`) + else if (sizeBytes > SOFT_LIMIT_BYTES) + warnings.push(`${label}: file size ${sizeBytes} bytes exceeds soft limit ${SOFT_LIMIT_BYTES}`) + if (!content.trim()) errors.push(`${label}: file is empty`) - // Validate ISO dates when present - for (const m of content.matchAll(/^Last Updated:\s*(.+)$/gm)) { - const val = m[1].trim() - const d = new Date(val) - if (isNaN(d.getTime())) warnings.push(`${label}: invalid Last Updated format '${val}'`) - } - } catch { - errors.push(`${label}: file not found at ${filePath}`) - } + const links = Array.from(content.matchAll(/\((https?:[^)]+)\)/g)).map((match) => match[1]) + if (links.some((url) => /[?&]utm_/i.test(url))) warnings.push(`${label}: found tracking params in links`) + for (const match of content.matchAll(/^Last Updated:\s*(.+)$/gm)) { + if (isNaN(new Date(match[1].trim()).getTime())) + warnings.push(`${label}: invalid Last Updated format '${match[1].trim()}'`) + } + if (solution) { + const headers = [...content.matchAll(/^# [^\n]+\nSource: (https:\/\/docs\.chain\.link\/solutions\/[^\n]+)\n/gm)] + const expectedUrls = (solution.nav?.flatMap((group) => group.pages) ?? ["index"]).map( + (pageId) => `https://docs.chain.link${toPagePath(solution.slug, pageId)}` + ) + if (JSON.stringify(headers.map((header) => header[1])) !== JSON.stringify(expectedUrls)) { + errors.push(`${label}: solution pages are missing, duplicated, or out of navigation order`) } + const normalize = (text: string) => text.replace(/\s+/g, " ").trim() + const disclaimer = normalize( + SOLUTION_DISCLAIMER.beforeTerms + + ` [${SOLUTION_DISCLAIMER.termsLabel}](${SOLUTION_DISCLAIMER.termsUrl})` + + SOLUTION_DISCLAIMER.afterTerms + ) + headers.forEach((header, index) => { + const page = content.slice(header.index, headers[index + 1]?.index ?? content.length) + if (!normalize(page).includes(disclaimer)) errors.push(`${label}: complete disclaimer missing from ${header[1]}`) + }) + } + return { errors, warnings } +} + +async function main() { + const errors: string[] = [] + const warnings: string[] = [] + for (const file of llmsFilesToValidate()) { + const result = await validateLlmsFile(file) + errors.push(...result.errors) + warnings.push(...result.warnings) } if (errors.length) { @@ -71,7 +100,9 @@ async function main() { console.log("LLM validation complete.") } -main().catch((err) => { - console.error(err) - process.exit(1) -}) +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((err) => { + console.error(err) + process.exit(1) + }) +} diff --git a/src/utils/solutionLlms.test.ts b/src/utils/solutionLlms.test.ts new file mode 100644 index 00000000000..d8fd88b89dc --- /dev/null +++ b/src/utils/solutionLlms.test.ts @@ -0,0 +1,188 @@ +import fs from "node:fs/promises" +import path from "node:path" +import os from "node:os" +import type { APIContext, GetStaticPathsResult } from "astro" +import type { CollectionEntry } from "astro:content" +import { beforeEach, describe, expect, jest, test } from "@jest/globals" +import { ALL_REGISTERED_SOLUTIONS, SOLUTIONS, toPagePath } from "../config/solutions/index.ts" +import { crossChainVaultAdapter } from "../config/solutions/cross-chain-vault-adapter.ts" +import { demoSinglePage } from "../config/solutions/demo-single-page.ts" +import { extractFrontmatter } from "../lib/markdown/utils.js" +import { buildMarkdownArtifact } from "../lib/markdown/buildMarkdownArtifact.js" +import { cleanText, unescapeMarkdown } from "../lib/markdown/formatters.js" +import { SOLUTION_DISCLAIMER } from "../config/solutions/disclaimer.ts" +import { blockingFindings, compareSourceToArtifact } from "../scripts/check-markdown-fidelity.js" +import { generateSolutionLlmsFiles } from "../scripts/generate-llms.js" +import { solutionLlmsOutputPath } from "../lib/markdown/solutionLlms.js" + +type SolutionEntry = CollectionEntry<"solutions"> +let entries: SolutionEntry[] = [] + +// Register Astro's virtual module with both Jest's resolver and its ESM loader. +jest.mock("astro:content", () => ({ getCollection: async () => entries }), { virtual: true }) +jest.unstable_mockModule("astro:content", () => ({ getCollection: async () => entries }), { virtual: true }) +const { GET, getStaticPaths } = await import("../pages/solutions/[slug]/llms-full.txt.js") + +async function readEntries( + slug: string, + directory = path.resolve("src/content/solutions", slug) +): Promise { + const files = await fs.readdir(directory, { withFileTypes: true }) + const result: SolutionEntry[] = [] + for (const file of files) { + const filePath = path.join(directory, file.name) + if (file.isDirectory()) { + result.push(...(await readEntries(slug, filePath))) + } else if (/\.mdx?$/.test(file.name) && file.name !== "README.md") { + const raw = await fs.readFile(filePath, "utf-8") + const { fmTitle, body } = extractFrontmatter(raw) + if (!fmTitle) throw new Error(`Missing fixture title: ${filePath}`) + result.push({ + id: path.relative(path.resolve("src/content/solutions"), filePath).replace(/\.mdx?$/, ""), + collection: "solutions", + filePath, + body, + data: { title: fmTitle }, + }) + } + } + return result +} + +const fixtures = (await Promise.all(ALL_REGISTERED_SOLUTIONS.map((solution) => readEntries(solution.slug)))).flat() +// These integration checks convert a complete solution, including imported code samples. +const CORPUS_TEST_TIMEOUT = 30_000 + +beforeEach(() => { + // Reverse filesystem order so the endpoint must use the manifest's navigation order. + entries = [...fixtures].reverse() +}) + +async function getPaths(): Promise { + return getStaticPaths({} as Parameters[0]) +} + +async function getResponse(props: APIContext["props"]): Promise { + const response = await GET({ props } as APIContext) + if (!(response instanceof Response)) throw new Error("Expected a TXT response") + return response +} + +describe("solution llms-full.txt", () => { + test("creates a route for every published solution and keeps hidden fixtures private", async () => { + const paths = await getPaths() + expect(paths.map((route) => route.params.slug).sort()).toEqual(SOLUTIONS.map((solution) => solution.slug).sort()) + for (const solution of ALL_REGISTERED_SOLUTIONS.filter((solution) => solution.hidden)) { + expect(paths.some((route) => route.params.slug === solution.slug)).toBe(false) + } + }) + + test( + "includes every Cross-Chain Vault Adapter page once, in navigation order, with code and callouts", + async () => { + const paths = await getPaths() + const route = paths.find((route) => route.params.slug === crossChainVaultAdapter.slug) + if (!route) throw new Error("Missing Cross-Chain Vault Adapter route") + const response = await getResponse(route.props ?? {}) + const document = await response.text() + + expect(response.headers.get("Content-Type")).toBe("text/plain; charset=utf-8") + const sourceUrls = [...document.matchAll(/^Source: (https:\/\/docs\.chain\.link\/solutions\/\S+)$/gm)].map( + (match) => match[1] + ) + expect(sourceUrls).toEqual( + (crossChainVaultAdapter.nav ?? []) + .flatMap((group) => group.pages) + .map((pageId) => `https://docs.chain.link${toPagePath(crossChainVaultAdapter.slug, pageId)}`) + ) + expect(new Set(sourceUrls).size).toBe(sourceUrls.length) + expect(document).toContain("What the adapter does") + expect(document).toContain("```solidity") + expect(document).toContain("Message payload") + expect(document).toContain("/images/solutions/cross-chain-vault-adapter/at-a-glance.svg") + expect(document).not.toMatch(/^import /m) + expect(document).not.toContain(" { + // Publish the fixture in memory to exercise automatic route discovery too. + const singlePageSolution = { ...demoSinglePage, hidden: false } + SOLUTIONS.push(singlePageSolution) + try { + const route = (await getPaths()).find((route) => route.params.slug === singlePageSolution.slug) + if (!route) throw new Error("Missing single-page solution route") + const response = await getResponse(route.props ?? {}) + const document = await response.text() + expect(document.match(/^Source: /gm)).toHaveLength(1) + expect(document).toContain(`Source: https://docs.chain.link/solutions/${demoSinglePage.slug}`) + expect(document).toContain(`# ${demoSinglePage.title}`) + expect(document.match(/^## Disclaimer$/gm)).toHaveLength(1) + expect(cleanText(document)).toContain(cleanText(SOLUTION_DISCLAIMER.beforeTerms)) + expect(document).toContain(`[${SOLUTION_DISCLAIMER.termsLabel}](${SOLUTION_DISCLAIMER.termsUrl})`) + expect(document).not.toContain("Source: https://docs.chain.link/solutions/cross-chain-vault-adapter") + } finally { + SOLUTIONS.splice(SOLUTIONS.indexOf(singlePageSolution), 1) + } + }) + + test("fails generation when a page referenced by navigation is missing", async () => { + entries = entries.filter((entry) => entry.id !== `${crossChainVaultAdapter.slug}/reference/factory-contract`) + await expect(getPaths()).rejects.toThrow('nav references "reference/factory-contract" but no page exists') + }) + + test.each(SOLUTIONS)( + "$title: checks every TXT page against its source and its Markdown export", + async (solution) => { + const route = (await getPaths()).find((route) => route.params.slug === solution.slug) + if (!route) throw new Error(`Missing solution route: ${solution.slug}`) + const document = await (await getResponse(route.props ?? {})).text() + // The standalone CLI reads files; Astro reads the collection. Their complete TXT must still match. + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "solution-llms-parity-")) + try { + await generateSolutionLlmsFiles([solution], directory) + expect(await fs.readFile(solutionLlmsOutputPath(solution.slug, directory), "utf8")).toBe(document) + expect(await fs.readFile(solutionLlmsOutputPath(solution.slug), "utf8")).toBe(document) + } finally { + await fs.rm(directory, { recursive: true, force: true }) + } + const headers = [...document.matchAll(/^# [^\n]+\nSource: https:\/\/docs\.chain\.link\/(solutions\/[^\n]+)\n/gm)] + const pages = entries.filter((entry) => entry.id.startsWith(`${solution.slug}/`) || entry.id === solution.slug) + expect(headers).toHaveLength(pages.length) + const disclaimer = + SOLUTION_DISCLAIMER.beforeTerms + + ` [${SOLUTION_DISCLAIMER.termsLabel}](${SOLUTION_DISCLAIMER.termsUrl})` + + SOLUTION_DISCLAIMER.afterTerms + + for (let index = 0; index < headers.length; index++) { + const requestPath = headers[index][1] + const page = document.slice(headers[index].index, headers[index + 1]?.index ?? document.length).trim() + const artifact = await buildMarkdownArtifact(requestPath) + if (!artifact?.sourcePath) throw new Error(`Missing Markdown source: ${requestPath}`) + expect(artifact.transformMode).toBe("normal") + const sourcePath = path.resolve("src/content", artifact.sourcePath) + const raw = await fs.readFile(sourcePath, "utf8") + const findings = compareSourceToArtifact(requestPath, sourcePath, raw, { + ...artifact, + sourcePath, + markdown: page, + }) + expect(blockingFindings("focused", findings)).toEqual([]) + const markdownBody = artifact.markdown.split( + "> For the complete documentation index, see [llms.txt](/llms.txt).\n" + )[1] + expect(page).toContain(unescapeMarkdown(markdownBody).trim()) + expect(page.match(/^## Disclaimer$/gm)).toHaveLength(1) + expect(cleanText(page)).toContain(cleanText(disclaimer)) + expect(page).toContain(`[${SOLUTION_DISCLAIMER.termsLabel}](${SOLUTION_DISCLAIMER.termsUrl})`) + } + expect(document.match(/^## Disclaimer$/gm)).toHaveLength(pages.length) + expect(document).toContain(solution.description) + if (solution.repoUrl) expect(document).toContain(`[View on GitHub](${solution.repoUrl})`) + }, + CORPUS_TEST_TIMEOUT + ) +}) diff --git a/src/utils/solutions.ts b/src/utils/solutions.ts index 71a2f2a524f..7c7b66bac3b 100644 --- a/src/utils/solutions.ts +++ b/src/utils/solutions.ts @@ -1,22 +1,34 @@ -import { getCollection, type CollectionEntry } from "astro:content" -import { toPageId, toPagePath } from "~/config/solutions/index.ts" -import type { Solution } from "~/config/solutions/index.ts" +import type { CollectionEntry } from "astro:content" +import { toPageId, toPagePath } from "../config/solutions/index.ts" +import type { Solution } from "../config/solutions/index.ts" const README = "src/content/solutions/README.md" export type SolutionEntry = CollectionEntry<"solutions"> +/** The page information needed by navigation and by the standalone LLM generator. */ +export type SolutionPage = { id: string; filePath?: string; data: { title: string } } export type SolutionNavItem = { title: string; url: string } export type ResolvedNavGroup = { section: string; contents: SolutionNavItem[] } /** All content pages belonging to one solution, keyed by page id ("index", "users/failed-transfers", ...). */ export async function getSolutionPages(slug: string): Promise> { + // Keep Astro's virtual module out of standalone Node scripts that reuse the pure helpers below. + const { getCollection } = await import("astro:content") const all = await getCollection("solutions") - const pages = new Map() + return collectSolutionPages(slug, all) +} + +export function collectSolutionPages(slug: string, entries: readonly T[]): Map { + const pages = new Map() - for (const entry of all) { + for (const entry of entries) { const id = entry.id.replace(/\.mdx?$/, "") if (id !== slug && !id.startsWith(`${slug}/`)) continue - pages.set(toPageId(slug, entry.id), entry) + const pageId = toPageId(slug, entry.id) + if (pages.has(pageId)) { + throw new Error(`[solutions] "${slug}" has more than one source for page "${pageId}". See ${README}.`) + } + pages.set(pageId, entry) } if (!pages.has("index")) { @@ -29,7 +41,7 @@ export async function getSolutionPages(slug: string): Promise): boolean { +export function isMultiPage(pages: ReadonlyMap): boolean { return pages.size > 1 } @@ -37,7 +49,10 @@ export function isMultiPage(pages: Map): boolean { * Resolves the left navigation, or null when the solution is single-page and no sidebar * should render. Validates the manifest against what is actually on disk, in both directions. */ -export function buildSolutionNav(solution: Solution, pages: Map): ResolvedNavGroup[] | null { +export function buildSolutionNav( + solution: Solution, + pages: ReadonlyMap +): ResolvedNavGroup[] | null { const { slug } = solution if (!isMultiPage(pages)) { @@ -89,8 +104,23 @@ export function buildSolutionNav(solution: Solution, pages: Map( + solution: Solution, + pages: ReadonlyMap +): T[] { + buildSolutionNav(solution, pages) + const pageIds = solution.nav?.flatMap((group) => group.pages) ?? ["index"] + return pageIds.map((pageId) => { + const entry = pages.get(pageId) + if (!entry) throw new Error(`[solutions] "${solution.slug}" is missing page "${pageId}".`) + return entry + }) +} + /** Fails the build when a registered manifest has no content directory. */ export async function assertRegisteredSolutionsHaveContent(solutions: Solution[]): Promise { + const { getCollection } = await import("astro:content") const all = await getCollection("solutions") for (const solution of solutions) { const has = all.some((e) => { From 54469b6604ce4e40c97963fcd4e157af78644dbd Mon Sep 17 00:00:00 2001 From: Karim <98668332+khadni@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:18:32 -0500 Subject: [PATCH 2/6] align Markdown fidelity checks with shared conversion --- src/scripts/check-markdown-fidelity.ts | 56 +++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/src/scripts/check-markdown-fidelity.ts b/src/scripts/check-markdown-fidelity.ts index 4fb07d9c80a..e040fa33e0a 100644 --- a/src/scripts/check-markdown-fidelity.ts +++ b/src/scripts/check-markdown-fidelity.ts @@ -118,6 +118,17 @@ const processor = unified().use(remarkParse).use(remarkMdx).use(remarkGfm) const containerElements: Record = { div: true, Fragment: true, + ol: true, + li: true, +} +const projectedInlineElements: Record = { + b: true, + strong: true, + code: true, + sub: true, + span: true, + br: true, + nobr: true, } const selectorComponents = { @@ -535,7 +546,16 @@ export function analyzeSourceMarkdown( const inlineBlock = (node: Node): Node | null => { let current: Node | undefined = node + let inlineElement: Node | null = null while (current) { + if ( + (current.type === "mdxJsxFlowElement" || current.type === "mdxJsxTextElement") && + projectedInlineElements[String((current as Node & { name?: unknown }).name ?? "")] + ) { + inlineElement ??= current + current = parentByNode.get(current) + continue + } if ( current.type === "paragraph" || current.type === "tableCell" || @@ -546,7 +566,7 @@ export function analyzeSourceMarkdown( } current = parentByNode.get(current) } - return null + return inlineElement } const textGroup = (node: Node): string | undefined => { @@ -712,7 +732,16 @@ export function analyzeSourceMarkdown( inspect({ type: "root", children: childrenOf(node) } as Parent) return SKIP } - if (containerElements[name]) { + if (containerElements[name] || (name === "ul" && node.type === "mdxJsxFlowElement")) { + if (name === "li") breakTextGroup(node) + for (const failure of expressionAttributes(node)) { + addDiagnostic("unverifiable", `${name}.${failure.name}`, node, `Dynamic JSX attribute (${failure.syntax})`) + } + inspect({ type: "root", children: childrenOf(node) } as Parent) + if (name === "li") breakTextGroup(node) + return SKIP + } + if (projectedInlineElements[name]) { for (const failure of expressionAttributes(node)) { addDiagnostic("unverifiable", `${name}.${failure.name}`, node, `Dynamic JSX attribute (${failure.syntax})`) } @@ -1045,9 +1074,25 @@ export function analyzeSourceMarkdown( return SKIP } - if (name === "Accordion") { + if (name === "GitHubCard") { + const title = staticAttribute(node, "title") + const href = staticAttribute(node, "href") + const badge = staticAttribute(node, "badge") + if (title.syntax || typeof title.value !== "string" || href.syntax || typeof href.value !== "string") { + addDiagnostic("unverifiable", name, node, "Dynamic or missing GitHub card title or href") + } else { + addFact("link", title.value, node, href.value) + } + if (badge.syntax) addDiagnostic("unverifiable", name, node, "Dynamic GitHub card badge") + else if (typeof badge.value === "string") addFact("text", badge.value, node) + inspect({ type: "root", children: childrenOf(node) } as Parent) + return SKIP + } + + if (name === "Accordion" || name === "AnchoredAccordion") { const title = staticAttribute(node, "title") const number = staticAttribute(node, "number") + const depth = staticAttribute(node, "depth") if ( title.syntax || typeof title.value !== "string" || @@ -1056,13 +1101,14 @@ export function analyzeSourceMarkdown( ) { addDiagnostic( "unverifiable", - "Accordion", + name, node, `Dynamic accordion heading (${title.syntax ?? number.syntax ?? "missing title"})` ) } else { const prefix = number.found ? `${number.value}. ` : "" - addFact("heading", `${prefix}${title.value}`, node, undefined, 3) + const level = depth.value === 4 ? 4 : depth.value === 3 || name === "Accordion" ? 3 : 2 + addFact("heading", `${prefix}${title.value}`, node, undefined, level) } inspect({ type: "root", children: childrenOf(node) } as Parent) return SKIP From efa63e5e6bc8517d60b53c08596eff4d0d631beb Mon Sep 17 00:00:00 2001 From: Karim <98668332+khadni@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:10:50 -0500 Subject: [PATCH 3/6] fix: restore static Astro content imports to fix solution builds --- src/lib/markdown/solutionLlms.ts | 2 +- src/scripts/generate-llms.test.ts | 2 +- src/utils/solutionPages.ts | 109 ++++++++++++++++++++++++++++ src/utils/solutions.ts | 114 ++---------------------------- 4 files changed, 116 insertions(+), 111 deletions(-) create mode 100644 src/utils/solutionPages.ts diff --git a/src/lib/markdown/solutionLlms.ts b/src/lib/markdown/solutionLlms.ts index 8804b6d6379..59f55b28b53 100644 --- a/src/lib/markdown/solutionLlms.ts +++ b/src/lib/markdown/solutionLlms.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises" import path from "node:path" import type { Solution } from "../../config/solutions/index.ts" -import { collectSolutionPages, getOrderedSolutionPages, type SolutionPage } from "../../utils/solutions.js" +import { collectSolutionPages, getOrderedSolutionPages, type SolutionPage } from "../../utils/solutionPages.js" import { assembleLlmsDocument, renderLlmsPageMarkdown } from "./llms.js" import { extractFrontmatter, toContentRelative } from "./utils.js" diff --git a/src/scripts/generate-llms.test.ts b/src/scripts/generate-llms.test.ts index 8c989071561..84c4f4cb575 100644 --- a/src/scripts/generate-llms.test.ts +++ b/src/scripts/generate-llms.test.ts @@ -16,7 +16,7 @@ import { SOLUTIONS } from "../config/solutions/index.ts" import { crossChainVaultAdapter } from "../config/solutions/cross-chain-vault-adapter.ts" import { demoSinglePage } from "../config/solutions/demo-single-page.ts" import { demoMultiPage } from "../config/solutions/demo-multi-page.ts" -import { collectSolutionPages } from "../utils/solutions.js" +import { collectSolutionPages } from "../utils/solutionPages.js" import { readSolutionLlmsPages, renderSolutionLlmsDocument } from "../lib/markdown/solutionLlms.js" import { llmsFilesToValidate, validateLlmsFile } from "./validate-llms.js" import { SOLUTION_DISCLAIMER } from "../config/solutions/disclaimer.ts" diff --git a/src/utils/solutionPages.ts b/src/utils/solutionPages.ts new file mode 100644 index 00000000000..904332770d0 --- /dev/null +++ b/src/utils/solutionPages.ts @@ -0,0 +1,109 @@ +import { toPageId, toPagePath } from "../config/solutions/index.ts" +import type { Solution } from "../config/solutions/index.ts" + +const README = "src/content/solutions/README.md" + +/** The page information shared by Astro navigation and the standalone LLM generator. */ +export type SolutionPage = { id: string; filePath?: string; data: { title: string } } +export type SolutionNavItem = { title: string; url: string } +export type ResolvedNavGroup = { section: string; contents: SolutionNavItem[] } + +export function collectSolutionPages(slug: string, entries: readonly T[]): Map { + const pages = new Map() + + for (const entry of entries) { + const id = entry.id.replace(/\.mdx?$/, "") + if (id !== slug && !id.startsWith(`${slug}/`)) continue + const pageId = toPageId(slug, entry.id) + if (pages.has(pageId)) { + throw new Error(`[solutions] "${slug}" has more than one source for page "${pageId}". See ${README}.`) + } + pages.set(pageId, entry) + } + + if (!pages.has("index")) { + throw new Error( + `[solutions] "${slug}" has no landing page. Create src/content/solutions/${slug}/index.mdx. See ${README}.` + ) + } + + return pages +} + +/** A solution is multi-page when it has more than its landing page. This is the ONLY definition. */ +export function isMultiPage(pages: ReadonlyMap): boolean { + return pages.size > 1 +} + +/** + * Resolves the left navigation, or null when the solution is single-page and no sidebar + * should render. Validates the manifest against what is actually on disk, in both directions. + */ +export function buildSolutionNav( + solution: Solution, + pages: ReadonlyMap +): ResolvedNavGroup[] | null { + const { slug } = solution + + if (!isMultiPage(pages)) { + if (solution.nav) { + throw new Error( + `[solutions] "${slug}" has a single page but declares "nav" in src/config/solutions/${slug}.ts. ` + + `Single-page solutions render no sidebar, so "nav" would do nothing. Remove it. See ${README}.` + ) + } + return null + } + + if (!solution.nav) { + throw new Error( + `[solutions] "${slug}" has ${pages.size} pages but declares no "nav" in src/config/solutions/${slug}.ts. ` + + `Multi-page solutions must declare their navigation. See ${README}.` + ) + } + + const declared = new Set() + const groups: ResolvedNavGroup[] = solution.nav.map((group) => ({ + section: group.group, + contents: group.pages.map((pageId) => { + const entry = pages.get(pageId) + if (!entry) { + throw new Error( + `[solutions] "${slug}" nav references "${pageId}" but no page exists for it. ` + + `Expected src/content/solutions/${slug}/${pageId}.mdx. See ${README}.` + ) + } + if (declared.has(pageId)) { + throw new Error(`[solutions] "${slug}" lists "${pageId}" more than once in its nav. See ${README}.`) + } + declared.add(pageId) + // RecursiveSidebar prepends Astro.site.pathname, so strip the leading slash. See §1.6. + return { title: entry.data.title, url: toPagePath(slug, pageId).slice(1) } + }), + })) + + for (const pageId of pages.keys()) { + if (!declared.has(pageId)) { + throw new Error( + `[solutions] src/content/solutions/${slug}/${pageId}.mdx exists but is not listed in the nav of ` + + `src/config/solutions/${slug}.ts. Every page must appear exactly once. See ${README}.` + ) + } + } + + return groups +} + +/** Validate the complete page set and return it in the order declared by the manifest. */ +export function getOrderedSolutionPages( + solution: Solution, + pages: ReadonlyMap +): T[] { + buildSolutionNav(solution, pages) + const pageIds = solution.nav?.flatMap((group) => group.pages) ?? ["index"] + return pageIds.map((pageId) => { + const entry = pages.get(pageId) + if (!entry) throw new Error(`[solutions] "${solution.slug}" is missing page "${pageId}".`) + return entry + }) +} diff --git a/src/utils/solutions.ts b/src/utils/solutions.ts index 7c7b66bac3b..4ea19ceb5be 100644 --- a/src/utils/solutions.ts +++ b/src/utils/solutions.ts @@ -1,126 +1,22 @@ -import type { CollectionEntry } from "astro:content" -import { toPageId, toPagePath } from "../config/solutions/index.ts" +import { getCollection, type CollectionEntry } from "astro:content" import type { Solution } from "../config/solutions/index.ts" +import { collectSolutionPages } from "./solutionPages.js" + +export { collectSolutionPages, isMultiPage, buildSolutionNav, getOrderedSolutionPages } from "./solutionPages.js" +export type { SolutionPage, SolutionNavItem, ResolvedNavGroup } from "./solutionPages.js" const README = "src/content/solutions/README.md" export type SolutionEntry = CollectionEntry<"solutions"> -/** The page information needed by navigation and by the standalone LLM generator. */ -export type SolutionPage = { id: string; filePath?: string; data: { title: string } } -export type SolutionNavItem = { title: string; url: string } -export type ResolvedNavGroup = { section: string; contents: SolutionNavItem[] } /** All content pages belonging to one solution, keyed by page id ("index", "users/failed-transfers", ...). */ export async function getSolutionPages(slug: string): Promise> { - // Keep Astro's virtual module out of standalone Node scripts that reuse the pure helpers below. - const { getCollection } = await import("astro:content") const all = await getCollection("solutions") return collectSolutionPages(slug, all) } -export function collectSolutionPages(slug: string, entries: readonly T[]): Map { - const pages = new Map() - - for (const entry of entries) { - const id = entry.id.replace(/\.mdx?$/, "") - if (id !== slug && !id.startsWith(`${slug}/`)) continue - const pageId = toPageId(slug, entry.id) - if (pages.has(pageId)) { - throw new Error(`[solutions] "${slug}" has more than one source for page "${pageId}". See ${README}.`) - } - pages.set(pageId, entry) - } - - if (!pages.has("index")) { - throw new Error( - `[solutions] "${slug}" has no landing page. Create src/content/solutions/${slug}/index.mdx. See ${README}.` - ) - } - - return pages -} - -/** A solution is multi-page when it has more than its landing page. This is the ONLY definition. */ -export function isMultiPage(pages: ReadonlyMap): boolean { - return pages.size > 1 -} - -/** - * Resolves the left navigation, or null when the solution is single-page and no sidebar - * should render. Validates the manifest against what is actually on disk, in both directions. - */ -export function buildSolutionNav( - solution: Solution, - pages: ReadonlyMap -): ResolvedNavGroup[] | null { - const { slug } = solution - - if (!isMultiPage(pages)) { - if (solution.nav) { - throw new Error( - `[solutions] "${slug}" has a single page but declares "nav" in src/config/solutions/${slug}.ts. ` + - `Single-page solutions render no sidebar, so "nav" would do nothing. Remove it. See ${README}.` - ) - } - return null - } - - if (!solution.nav) { - throw new Error( - `[solutions] "${slug}" has ${pages.size} pages but declares no "nav" in src/config/solutions/${slug}.ts. ` + - `Multi-page solutions must declare their navigation. See ${README}.` - ) - } - - const declared = new Set() - const groups: ResolvedNavGroup[] = solution.nav.map((group) => ({ - section: group.group, - contents: group.pages.map((pageId) => { - const entry = pages.get(pageId) - if (!entry) { - throw new Error( - `[solutions] "${slug}" nav references "${pageId}" but no page exists for it. ` + - `Expected src/content/solutions/${slug}/${pageId}.mdx. See ${README}.` - ) - } - if (declared.has(pageId)) { - throw new Error(`[solutions] "${slug}" lists "${pageId}" more than once in its nav. See ${README}.`) - } - declared.add(pageId) - // RecursiveSidebar prepends Astro.site.pathname, so strip the leading slash. See §1.6. - return { title: entry.data.title, url: toPagePath(slug, pageId).slice(1) } - }), - })) - - for (const pageId of pages.keys()) { - if (!declared.has(pageId)) { - throw new Error( - `[solutions] src/content/solutions/${slug}/${pageId}.mdx exists but is not listed in the nav of ` + - `src/config/solutions/${slug}.ts. Every page must appear exactly once. See ${README}.` - ) - } - } - - return groups -} - -/** Validate the complete page set and return it in the order declared by the manifest. */ -export function getOrderedSolutionPages( - solution: Solution, - pages: ReadonlyMap -): T[] { - buildSolutionNav(solution, pages) - const pageIds = solution.nav?.flatMap((group) => group.pages) ?? ["index"] - return pageIds.map((pageId) => { - const entry = pages.get(pageId) - if (!entry) throw new Error(`[solutions] "${solution.slug}" is missing page "${pageId}".`) - return entry - }) -} - /** Fails the build when a registered manifest has no content directory. */ export async function assertRegisteredSolutionsHaveContent(solutions: Solution[]): Promise { - const { getCollection } = await import("astro:content") const all = await getCollection("solutions") for (const solution of solutions) { const has = all.some((e) => { From 0c872329991fa6e03c6daa3853649c26dff58a31 Mon Sep 17 00:00:00 2001 From: Karim <98668332+khadni@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:28:56 -0500 Subject: [PATCH 4/6] fix: preserve nested GitHub cards in Markdown and LLM exports --- src/content/ccip/llms-full.txt | 22 +++++++ .../__tests__/transformMarkdown.test.ts | 47 ++++++++++++++ src/lib/markdown/transformMarkdown.ts | 2 +- src/scripts/check-markdown-fidelity.test.ts | 65 +++++++++++++++++++ src/scripts/check-markdown-fidelity.ts | 2 + 5 files changed, 137 insertions(+), 1 deletion(-) diff --git a/src/content/ccip/llms-full.txt b/src/content/ccip/llms-full.txt index 0db5574b44c..7e8cd9dc152 100644 --- a/src/content/ccip/llms-full.txt +++ b/src/content/ccip/llms-full.txt @@ -77929,7 +77929,10 @@ Choose your starting point based on your blockchain platform: Source: https://docs.chain.link/ccip/v1 Last Updated: 2025-05-19 +[Introducing CCIP 2.0](https://chain.link/blog/introducing-ccip-2-0) New +Read the CCIP 2.0 announcement on the Chainlink blog: additive security with cross-chain verifiers, built-in +compliance, and configurable settlement speeds. --- @@ -104700,6 +104703,14 @@ This page is an overview of the developer tools available for testing and integr Convenient Github templates to get your app from 0 to 1 in the shortest time possible. +[Foundry Starter Kit](https://github.com/smartcontractkit/ccip-starter-kit-foundry) + +CCIP Foundry starter kit for Solidity-native workflows. + +[Hardhat Starter Kit](https://github.com/smartcontractkit/ccip-starter-kit-hardhat) + +CCIP Hardhat starter kit for TypeScript-based workflows. + ## Follow along Repos The following repos contain all the code that is referenced inside CCIP 2.0 EVM tutorials. Feel free to clone them and follow along with the tutorials. @@ -104717,6 +104728,14 @@ CCIP tutorials for application developers using Hardhat and Foundry. For token issuers who want to create/manage their own token on Chainlink CCIP. +[Cross-Chain Token (CCT): Foundry](https://github.com/smartcontractkit/docs-cct-foundry) + +Cross-Chain Token (CCT) tutorials using Foundry. + +[Cross-Chain Token (CCT): Hardhat](https://github.com/smartcontractkit/docs-cct-hardhat) + +Cross-Chain Token (CCT) tutorials using Hardhat. + ## Transfer Tokens using CCIP You can use our Transporter UI to conveniently transfer tokens across chains using CCIP. @@ -129714,7 +129733,10 @@ Locate the CCIP-BnM contract address in the [CCIP Directory](/ccip/directory/tes Source: https://docs.chain.link/ccip Last Updated: 2025-05-19 +[Introducing CCIP 2.0](https://chain.link/blog/introducing-ccip-2-0) New +Read the CCIP 2.0 announcement on the Chainlink blog: additive security with cross-chain verifiers, built-in +compliance, and configurable settlement speeds. --- diff --git a/src/lib/markdown/__tests__/transformMarkdown.test.ts b/src/lib/markdown/__tests__/transformMarkdown.test.ts index d7a78e50ef5..d230b1886dd 100644 --- a/src/lib/markdown/__tests__/transformMarkdown.test.ts +++ b/src/lib/markdown/__tests__/transformMarkdown.test.ts @@ -460,6 +460,53 @@ Choose an installation path. expect(result).not.toContain("Fragment") }) + it.each(["GitHubCards", "OverviewWrapper"])( + "preserves cards inside %s and continues with siblings", + async (wrapper) => { + const result = await transformMarkdown( + `Before wrapper. + +<${wrapper}> + + First repository description. + + + Second repository description. + + + +After wrapper.`, + "/fake/page.mdx" + ) + + expect(result).toBe( + [ + "Before wrapper.", + "[First repository](https://example.com/first) New", + "First repository description.", + "[Second repository](https://example.com/second)", + "Second repository description.", + "After wrapper.", + ].join("\n\n") + "\n" + ) + } + ) + + it("preserves a card inside nested overview and card containers", async () => { + const result = await transformMarkdown( + ` + + + Repository description. + + +`, + "/fake/page.mdx" + ) + + expect(result).toBe("[Repository](https://example.com/repo)\n\nRepository description.\n") + }) + it("projects Accordion number, title, and body", async () => { const result = await transformMarkdown( ` diff --git a/src/lib/markdown/transformMarkdown.ts b/src/lib/markdown/transformMarkdown.ts index ba8185faca7..08d376e067a 100644 --- a/src/lib/markdown/transformMarkdown.ts +++ b/src/lib/markdown/transformMarkdown.ts @@ -235,7 +235,7 @@ export async function transformMarkdown( if ( (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && - (node as MdxJsxNode).name === "Fragment" + ["Fragment", "GitHubCards", "OverviewWrapper"].includes((node as MdxJsxNode).name ?? "") ) { return handleFragment(node as MdxJsxNode, parent, index) } diff --git a/src/scripts/check-markdown-fidelity.test.ts b/src/scripts/check-markdown-fidelity.test.ts index 37369309980..5eef8e36f78 100644 --- a/src/scripts/check-markdown-fidelity.test.ts +++ b/src/scripts/check-markdown-fidelity.test.ts @@ -753,6 +753,71 @@ describe("stable report JSON", () => { }) }) +describe("card container fidelity", () => { + const source = (wrapper: string) => `<${wrapper}> + + First repository description. + + + Second repository description. + +` + const firstCard = "[First repository](https://example.com/first) New\n\nFirst repository description." + const secondCard = "[Second repository](https://example.com/second)\n\nSecond repository description." + + test.each(["GitHubCards", "OverviewWrapper"])("inventories cards inside %s", (wrapper) => { + const findings = compareSourceToArtifact( + "fixture", + "src/content/fixture.mdx", + source(wrapper), + artifact(`${firstCard}\n\n${secondCard}`) + ) + + expect(blockingFindings("focused", findings)).toEqual([]) + expect(findings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ status: "present", expected: "First repository -> https://example.com/first" }), + expect.objectContaining({ status: "present", expected: "New" }), + expect.objectContaining({ status: "present", expected: "First repository description." }), + expect.objectContaining({ status: "present", expected: "Second repository -> https://example.com/second" }), + expect.objectContaining({ status: "present", expected: "Second repository description." }), + ]) + ) + }) + + test.each(["GitHubCards", "OverviewWrapper"])("detects a missing card inside %s", (wrapper) => { + const findings = compareSourceToArtifact( + "fixture", + "src/content/fixture.mdx", + source(wrapper), + artifact(secondCard) + ) + + expect(findings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ status: "missing", expected: "First repository -> https://example.com/first" }), + expect.objectContaining({ status: "missing", expected: "First repository description." }), + expect.objectContaining({ status: "present", expected: "Second repository -> https://example.com/second" }), + ]) + ) + }) + + test("reads nested overview and card containers", () => { + const nested = `${source("GitHubCards")}` + const findings = compareSourceToArtifact( + "fixture", + "src/content/fixture.mdx", + nested, + artifact(`${firstCard}\n\n${secondCard}`) + ) + + expect(blockingFindings("focused", findings)).toEqual([]) + expect(findings).toContainEqual( + expect.objectContaining({ status: "present", expected: "First repository -> https://example.com/first" }) + ) + }) +}) + describe("solution component fidelity", () => { const source = `
    1. First item.
    2. Second item.
    diff --git a/src/scripts/check-markdown-fidelity.ts b/src/scripts/check-markdown-fidelity.ts index aa0fb765a26..4b880c26f52 100644 --- a/src/scripts/check-markdown-fidelity.ts +++ b/src/scripts/check-markdown-fidelity.ts @@ -118,6 +118,8 @@ const processor = unified().use(remarkParse).use(remarkMdx).use(remarkGfm) const containerElements: Record = { div: true, Fragment: true, + GitHubCards: true, + OverviewWrapper: true, ol: true, li: true, } From 391bf75e382428087c9f99073c428e1fa4ad19cb Mon Sep 17 00:00:00 2001 From: Karim <98668332+khadni@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:45:59 -0500 Subject: [PATCH 5/6] fix: normalize inline JSX spaces in Markdown and LLM exports --- src/content/ccip/llms-full.txt | 259 ++++++------------ src/content/chainlink-nodes/llms-full.txt | 3 +- src/content/crec/llms-full.txt | 3 +- src/content/data-feeds/llms-full.txt | 10 +- src/content/data-streams/llms-full.txt | 33 +-- .../__tests__/transformMarkdown.test.ts | 63 +++++ src/lib/markdown/transformMarkdown.ts | 29 ++ src/scripts/check-markdown-fidelity.test.ts | 25 +- 8 files changed, 214 insertions(+), 211 deletions(-) diff --git a/src/content/ccip/llms-full.txt b/src/content/ccip/llms-full.txt index 7e8cd9dc152..08dbc05ca43 100644 --- a/src/content/ccip/llms-full.txt +++ b/src/content/ccip/llms-full.txt @@ -106294,8 +106294,7 @@ You will send `CCIP-BnM` tokens and a `string` payload from *Ethereum Sepolia* t > **NOTE: Gas limit for defensive contracts** > > 1. Defensive error handling (try/catch, failed-message storage) requires more gas than standard receivers. -> 2. Foundry scripts default to **400,000** gas. Hardhat scripts estimate gas unless you set -> `GAS_LIMIT` (and fall back to 400,000 if estimation fails). +> 2. Foundry scripts default to **400,000** gas. Hardhat scripts estimate gas unless you set `GAS_LIMIT` (and fall back to 400,000 if estimation fails). > 3. Make sure to test all scenarios, not just success paths, to accurately estimate the required gas limit. ## Before you begin @@ -113868,8 +113867,7 @@ cast wallet import your_keystore_name --interactive > **NOTE: Note** > -> 1. For more information on how to create an encrypted Foundry keystore, refer to the -> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 1. For more information on how to create an encrypted Foundry keystore, refer to the [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). > 2. `cast wallet import` will throw an error if a keystore with the same name already exists. > 3. Use `cast wallet list` to check previously created keystores. @@ -114058,8 +114056,7 @@ Use the `DeployAdvancedPoolHooks.s.sol` script to deploy a new hooks contract. > > The `DeployAdvancedPoolHooks.s.sol` script reads `script/input/advanced-pool-hooks.json` when > environment variables are omitted. Inspect the file and clear any stale addresses you do not intend to use. Because -> allowlist enablement is permanent, prefer setting `ALLOWLIST` and `AUTHORIZED_CALLERS` -> explicitly. +> allowlist enablement is permanent, prefer setting `ALLOWLIST` and `AUTHORIZED_CALLERS` explicitly. [DeployAdvancedPoolHooks.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/DeployAdvancedPoolHooks.s.sol) @@ -114074,8 +114071,7 @@ Deployment inputs: > > 1. Allowlist mode is set at deployment time and **cannot be changed later**. > 2. If you deploy with a non-empty `ALLOWLIST`, the allowlist is permanently enabled. -> 3. If you deploy with an empty allowlist, calling `applyAllowListUpdates()` later will revert with -> `AllowListNotEnabled()`. +> 3. If you deploy with an empty allowlist, calling `applyAllowListUpdates()` later will revert with `AllowListNotEnabled()`. In this example, you will deploy hooks on Ethereum Sepolia with your deployer address on the allowlist, and authorize your token pool as a caller: @@ -114153,8 +114149,7 @@ export POOL_HOOKS=0xHooksAddress > - A broadcast deploy records the hooks address in the project store (`project/.json`, > under `addresses.active.poolHooks` and the `deployments` entry), so later scripts resolve it > automatically with no manual export. -> - If you lose the session export, read the address from the project store, or from the append-only history ledger at -> `history/advanced-pool-hooks//` (the `POOL_HOOKS` key in the JSON file). +> - If you lose the session export, read the address from the project store, or from the append-only history ledger at `history/advanced-pool-hooks//` (the `POOL_HOOKS` key in the JSON file). > - For a pool or token deployed outside this repository, run `make adopt-token` to record it in the same > registry. Environment variable exports still work as temporary overrides, but they are not saved. @@ -114249,8 +114244,7 @@ New Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeD > > - Updating authorized callers requires the hooks contract **owner**. > - Attaching or detaching hooks requires the token pool **owner**. -> - If either command reverts with `OnlyCallableByOwner()`, the wallet stored in `KEYSTORE_NAME` -> is not the current owner of the target contract. +> - If either command reverts with `OnlyCallableByOwner()`, the wallet stored in `KEYSTORE_NAME` is not the current owner of the target contract. ### 6. Verify the setup @@ -114669,8 +114663,7 @@ npx hardhat keystore set PRIVATE_KEY > > 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it > asks you to enter your private key as hidden text. -> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in -> `.env`. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in `.env`. > 3. Use `npx hardhat keystore list` to check previously created keystores. ## Tutorial @@ -114829,8 +114822,7 @@ Deployment flags: > > 1. Allowlist mode is set at deployment time and **cannot be changed later**. > 2. If you deploy with a non-empty `--allowlist`, the allowlist is permanently enabled. -> 3. If you deploy with an empty allowlist, calling `applyAllowListUpdates()` later will revert with -> `AllowListNotEnabled()`. +> 3. If you deploy with an empty allowlist, calling `applyAllowListUpdates()` later will revert with `AllowListNotEnabled()`. In this example, you will deploy hooks on Ethereum Sepolia with your deployer address on the allowlist, and authorize your token pool as a caller: @@ -114996,8 +114988,7 @@ New Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeD > > - Updating authorized callers requires the hooks contract **owner**. > - Attaching or detaching hooks requires the token pool **owner**. -> - If either command reverts with `OnlyCallableByOwner()`, the wallet stored in `KEYSTORE_NAME` -> is not the current owner of the target contract. +> - If either command reverts with `OnlyCallableByOwner()`, the wallet stored in `KEYSTORE_NAME` is not the current owner of the target contract. ### 6. Verify the setup @@ -115299,11 +115290,9 @@ In this tutorial you will: > **NOTE: What this tutorial does not repeat** > -> - Token and pool deployment, admin registration, and lane configuration: complete a registration tutorial first (see -> [Confirm prerequisites](#confirm-prerequisites-addresses-and-permissions)). +> - Token and pool deployment, admin registration, and lane configuration: complete a registration tutorial first (see [Confirm prerequisites](#confirm-prerequisites-addresses-and-permissions)). > - The ACE Platform workflow (engine creation, target detection, contract-type assignment, policy instances, -> protections, extractor mappings): follow -> [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools) for each step. +> protections, extractor mappings): follow [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools) for each step. ## Before You Begin @@ -115353,8 +115342,7 @@ cast wallet import your_keystore_name --interactive > **NOTE: Note** > -> 1. For more information on how to create an encrypted Foundry keystore, refer to the -> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 1. For more information on how to create an encrypted Foundry keystore, refer to the [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). > 2. `cast wallet import` will throw an error if a keystore with the same name already exists. > 3. Use `cast wallet list` to check previously created keystores. @@ -115416,8 +115404,7 @@ Use two different recipient addresses. The destination policy will deny only `DE > **NOTE: ACE access required** > -> If ACE is not yet enabled for your Chainlink Platform organization, -> [request access](https://chain.link/contact) and complete the [ACE account +> If ACE is not yet enabled for your Chainlink Platform organization, [request access](https://chain.link/contact) and complete the [ACE account > setup](/ace/getting-started/account-setup) before creating a Policy Engine. You also need a Policy Engine on each chain. [Create a policy engine](/ace/guides/policy-manager/manage-engines#create-a-policy-engine), include Ethereum Sepolia and Arbitrum Sepolia, wait until the engine is **Active**, and copy its onchain address for each chain. @@ -115494,8 +115481,7 @@ export DEST_POOL_HOOKS=0xDestinationHooksAddress > - A broadcast deploy records the hooks address in the project store (`project/.json`, > under `addresses.active.poolHooks` and the `deployments` entry), so later scripts resolve it > automatically with no manual export. -> - If you lose the session export, read the address from the project store, or from the append-only history ledger at -> `history/advanced-pool-hooks//` (the `POOL_HOOKS` key in the JSON file). +> - If you lose the session export, read the address from the project store, or from the append-only history ledger at `history/advanced-pool-hooks//` (the `POOL_HOOKS` key in the JSON file). > - For a pool or token deployed outside this repository, run `make adopt-token` to record it in the same > registry. Environment variable exports still work as temporary overrides, but they are not saved. @@ -115524,8 +115510,7 @@ script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol \ > **NOTE: Why AUTHORIZED_CALLERS at deploy time** > > Passing the pool as an authorized caller at deploy time authorizes it in the same transaction the hooks are created -> in, so the first hook call cannot revert for a missing caller. If you deployed without it, add the pool with -> `UpdateAuthorizedCallers.s.sol` (`POOL_HOOKS= ADD_ADDRESSES=`) before +> in, so the first hook call cannot revert for a missing caller. If you deployed without it, add the pool with `UpdateAuthorizedCallers.s.sol` (`POOL_HOOKS= ADD_ADDRESSES=`) before > attaching. ### 3. Set or update the policy engine @@ -115766,11 +115751,9 @@ In this tutorial you will: > **NOTE: What this tutorial does not repeat** > -> - Token and pool deployment, admin registration, and lane configuration: complete a registration tutorial first (see -> [Confirm prerequisites](#confirm-prerequisites-addresses-and-permissions)). +> - Token and pool deployment, admin registration, and lane configuration: complete a registration tutorial first (see [Confirm prerequisites](#confirm-prerequisites-addresses-and-permissions)). > - The ACE Platform workflow (engine creation, target detection, contract-type assignment, policy instances, -> protections, extractor mappings): follow -> [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools) for each step. +> protections, extractor mappings): follow [Protect CCIP Token Pools with ACE](/ace/guides/policy-manager/ccip-token-pools) for each step. ## Before You Begin @@ -115850,8 +115833,7 @@ npx hardhat keystore set PRIVATE_KEY > > 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it > asks you to enter your private key as hidden text. -> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in -> `.env`. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in `.env`. > 3. Use `npx hardhat keystore list` to check previously created keystores. ## Tutorial @@ -115880,8 +115862,7 @@ Use two different recipient addresses. The destination policy will deny only `DE > **NOTE: ACE access required** > -> If ACE is not yet enabled for your Chainlink Platform organization, -> [request access](https://chain.link/contact) and complete the [ACE account +> If ACE is not yet enabled for your Chainlink Platform organization, [request access](https://chain.link/contact) and complete the [ACE account > setup](/ace/getting-started/account-setup) before creating a Policy Engine. You also need a Policy Engine on each chain. [Create a policy engine](/ace/guides/policy-manager/manage-engines#create-a-policy-engine), include Ethereum Sepolia and Arbitrum Sepolia, wait until the engine is **Active**, and copy its onchain address for each chain. @@ -115969,8 +115950,7 @@ npx hardhat updateAdvancedPoolHooks \ > **NOTE: Why --authorizedcallers at deploy time** > > Passing the pool as an authorized caller at deploy time authorizes it in the same transaction the hooks are created -> in, so the first hook call cannot revert for a missing caller. If you deployed without it, add the pool with -> `updateAuthorizedCallers` (`--poolhooks --add `) before attaching. +> in, so the first hook call cannot revert for a missing caller. If you deployed without it, add the pool with `updateAuthorizedCallers` (`--poolhooks --add `) before attaching. ### 3. Set or update the policy engine @@ -116437,8 +116417,7 @@ typeAndVersion: BurnMintTokenPool 1.6.1 > **NOTE: What to check before continuing** > -> - **administrator** should match the account/multisig you control (this account must execute -> `setPool` during cutover). +> - **administrator** should match the account/multisig you control (this account must execute `setPool` during cutover). > - **pendingAdministrator** should typically be `0x0`. If it’s set, resolve it before > migrating to avoid confusion. > - **typeAndVersion** should indicate a v1.5.x or v1.6.x `BurnMintTokenPool`. @@ -116840,8 +116819,7 @@ pool. > **NOTE: Who must run this step** > -> The signer running `setPool` must be the token’s `TokenAdminRegistry` -> **administrator** on that chain (as shown by `getTokenConfig` in Step 1). +> The signer running `setPool` must be the token’s `TokenAdminRegistry` **administrator** on that chain (as shown by `getTokenConfig` in Step 1). [SetPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/SetPool.s.sol) @@ -117175,8 +117153,7 @@ npx hardhat keystore set > > 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it > asks you to enter your private key as hidden text. -> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in -> `.env`. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in `.env`. > 3. Use `npx hardhat keystore list` to check previously created keystores. ### 2. What this guide assumes @@ -117302,8 +117279,7 @@ typeAndVersion: BurnMintTokenPool 1.6.1 > **NOTE: What to check before continuing** > -> - **administrator** should match the account/multisig you control (this account must execute -> `setPool` during cutover). +> - **administrator** should match the account/multisig you control (this account must execute `setPool` during cutover). > - **pendingAdministrator** should typically be `0x0`. If it's set, resolve it before > migrating to avoid confusion. > - **typeAndVersion** should indicate a v1.5.x or v1.6.x `BurnMintTokenPool`. @@ -117687,8 +117663,7 @@ pool. > **NOTE: Who must run this step** > -> The signer running `setPool` must be the token's `TokenAdminRegistry` -> **administrator** on that chain (as shown by `getTokenConfig` in Step 1). +> The signer running `setPool` must be the token's `TokenAdminRegistry` **administrator** on that chain (as shown by `getTokenConfig` in Step 1). [setPool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/setup/setPool.ts) @@ -118121,8 +118096,7 @@ typeAndVersion: BurnMintTokenPool 1.6.1 > **NOTE: What to check before continuing** > -> - **administrator** should match the account/multisig you control (this account must execute -> `setPool` during cutover). +> - **administrator** should match the account/multisig you control (this account must execute `setPool` during cutover). > - **pendingAdministrator** should typically be `0x0`. If it’s set, resolve it before > migrating to avoid confusion. > - **typeAndVersion** should indicate a v1.5.x or v1.6.x `LockReleaseTokenPool` on Ethereum @@ -118314,15 +118288,13 @@ export ARBITRUM_SEPOLIA_V2_POOL=0xNewArbitrumV2BurnMintPool > **CAUTION: If the role grant is skipped** > > The deploy script tries to call `grantMintAndBurnRoles` on your Arbitrum token. If your token predates that -> interface, the script prints a warning instead of `✅ Roles granted successfully!` and you must grant -> **mint** and **burn** roles to `$ARBITRUM_SEPOLIA_V2_POOL` yourself, using +> interface, the script prints a warning instead of `✅ Roles granted successfully!` and you must grant **mint** and **burn** roles to `$ARBITRUM_SEPOLIA_V2_POOL` yourself, using > whatever access-control mechanism your token exposes. The new pool cannot mint or burn until it holds these roles. > **NOTE: Dual-role window** > > Until post-migration cleanup, **both** v1 and v2 Burn & Mint pools may hold mint/burn roles on your -> Arbitrum token. The new Ethereum Sepolia pool is deployed and authorized on the lockbox but is still -> **empty** (no liquidity yet). Your CCIP routing still points to the v1 pools until Step 8. +> Arbitrum token. The new Ethereum Sepolia pool is deployed and authorized on the lockbox but is still **empty** (no liquidity yet). Your CCIP routing still points to the v1 pools until Step 8. ### 4. Step 4 (Required): Pause outbound transfers on the v1 pools @@ -118670,8 +118642,7 @@ pool. > **NOTE: Who must run this step** > -> The signer running `setPool` must be the token’s `TokenAdminRegistry` -> **administrator** on that chain (as shown by `getTokenConfig` in Step 1). +> The signer running `setPool` must be the token’s `TokenAdminRegistry` **administrator** on that chain (as shown by `getTokenConfig` in Step 1). [SetPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/SetPool.s.sol) @@ -118926,9 +118897,7 @@ In this tutorial you will: > **NOTE: If you do not explicitly configure these features, default behavior applies** > -> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting -> `finality` or using `finality=finalized`) unless you opt into faster than finality via -> `finality` (for example, `finality=` for block-depth faster than finality). +> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting `finality` or using `finality=finalized`) unless you opt into faster than finality via `finality` (for example, `finality=` for block-depth faster than finality). > 2. **Opt-in changes.** New v2 behavior only applies when you explicitly configure it (for example, > pool-level fees or faster than finality). @@ -118979,8 +118948,7 @@ cast wallet import your_keystore_name --interactive > **NOTE: Note** > -> 1. For more information on how to create an encrypted Foundry keystore, refer to the -> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 1. For more information on how to create an encrypted Foundry keystore, refer to the [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). > 2. `cast wallet import` will throw an error if a keystore with the same name already exists. > 3. Use `cast wallet list` to check previously created keystores. @@ -119171,12 +119139,10 @@ forge script \ > > 1. `--verify` requires `ETHERSCAN_API_KEY` to be set. > 2. `ROLES_RECIPIENT` is optional, defaults to deployer's address. -> 3. `CCIP_ADMIN_ADDRESS` is optional, defaults to broadcaster, reused in -> [Claim Admin Role](#claim-admin-role) step. +> 3. `CCIP_ADMIN_ADDRESS` is optional, defaults to broadcaster, reused in [Claim Admin Role](#claim-admin-role) step. > 4. `TOKEN_PRE_MINT_RECIPIENT` is optional, applies only when `TOKEN_PRE_MINT > 0`, defaults > to broadcaster. -> 5. `TOKEN_PRE_MINT` is required to be set to a value greater than 0, only if -> `TOKEN_PRE_MINT_RECIPIENT` is set. +> 5. `TOKEN_PRE_MINT` is required to be set to a value greater than 0, only if `TOKEN_PRE_MINT_RECIPIENT` is set. Your output should look something like this: @@ -119291,8 +119257,7 @@ script/deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.jso > values from your current shell environment. > 2. Run the `export` commands so subsequent scripts in this terminal session can discover the correct > addresses. -> 3. You can also pass the addresses directly as inline environment variables `TOKEN=0x...` / -> `TOKEN_POOL=0x...` on any script that requires them. +> 3. You can also pass the addresses directly as inline environment variables `TOKEN=0x...` / `TOKEN_POOL=0x...` on any script that requires them. ```bash filename="Terminal" export ETHEREUM_SEPOLIA_TOKEN=0x... @@ -119446,8 +119411,7 @@ This process involves interacting with the [`RegistryModuleOwnerCustom`](https:/ > > 1. If your token implements `getCCIPAdmin()`, the script calls `registerAdminViaGetCCIPAdmin()` > . -> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the script calls -> `registerAdminViaOwner()` instead. +> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the script calls `registerAdminViaOwner()` instead. > > No extra configuration is needed; both patterns are fully supported. @@ -119459,8 +119423,7 @@ View the admin claim script on GitHub. > **NOTE: Note** > -> 1. If you do not set `CCIP_ADMIN_ADDRESS`, the script defaults it to the broadcaster (the signer used in -> `--account`). +> 1. If you do not set `CCIP_ADMIN_ADDRESS`, the script defaults it to the broadcaster (the signer used in `--account`). > 2. If you set `CCIP_ADMIN_ADDRESS` to a different address, the `ClaimAdmin.s.sol` transaction > must be signed by that same address (use that address's keystore in `--account`), or the claim step > will revert. @@ -119797,8 +119760,7 @@ forge script \ > **NOTE: Pool-level finality vs. transaction-level finality** > -> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via -> `setAllowedFinalityConfig(bytes4)`. +> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via `setAllowedFinalityConfig(bytes4)`. > > - Default = faster than finality is **disabled**. The pool only allows transfers that use default > finality (omit `finality` or use `finality=finalized`). @@ -120242,10 +120204,8 @@ ccip-cli send \ > **NOTE: Understand Minimum Block Depth Requirements 🧑‍🏫** > > 1. The last command failed because the pool's **allowed finality config** requires a larger block depth -> for numeric faster than finality (for example, if you configured `BLOCK_DEPTH=32`) but we requested -> `finality=7`. -> 2. Try running the command again with an appropriate value for `finality` (for example, -> `--extra finality=20`), or omit `finality` to use default finality ( +> for numeric faster than finality (for example, if you configured `BLOCK_DEPTH=32`) but we requested `finality=7`. +> 2. Try running the command again with an appropriate value for `finality` (for example, `--extra finality=20`), or omit `finality` to use default finality ( > `finality=finalized`). > 3. If you see a revert like `InvalidRequestedFinality(requestedFinality, allowedFinality)`, it means the > requested mode (or depth) is not permitted by the pool. @@ -120361,9 +120321,7 @@ In this tutorial you will: > **NOTE: If you do not explicitly configure these features, default behavior applies** > -> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting -> `finality` or using `finality=finalized`) unless you opt into faster than finality via -> `finality` (for example, `finality=` for block-depth faster than finality). +> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting `finality` or using `finality=finalized`) unless you opt into faster than finality via `finality` (for example, `finality=` for block-depth faster than finality). > 2. **Opt-in changes.** New v2 behavior only applies when you explicitly configure it (for example, > pool-level fees or faster than finality). @@ -120446,8 +120404,7 @@ npx hardhat keystore set > > 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it > asks you to enter your private key as hidden text. -> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in -> `.env`. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in `.env`. > 3. Use `npx hardhat keystore list` to check previously created keystores. ### 2. How to configure deployment parameters? @@ -120850,8 +120807,7 @@ This process involves interacting with the [`RegistryModuleOwnerCustom`](https:/ > The task auto-detects how your token exposes its admin and uses the appropriate registration method: > > 1. If your token implements `getCCIPAdmin()`, the task calls `registerAdminViaGetCCIPAdmin()`. -> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the task calls -> `registerAdminViaOwner()` instead. +> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the task calls `registerAdminViaOwner()` instead. > > No extra configuration is needed: both patterns are fully supported. @@ -121176,8 +121132,7 @@ npx hardhat getSupportedChains --network arbitrumSepolia > **NOTE: Pool-level finality vs. transaction-level finality** > -> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via -> `setAllowedFinalityConfig(bytes4)`. +> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via `setAllowedFinalityConfig(bytes4)`. > > - Default = faster than finality is **disabled**. The pool only allows transfers that use default > finality (omit `finality` or use `finality=finalized`). @@ -121549,8 +121504,7 @@ CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b > **NOTE: chainId vs. chainSelector** > -> The `chainId` column shows the EVM chain ID (for example, `11155111` for Sepolia). The -> `chainSelector` is CCIP's own chain identifier used internally by the protocol. You do not need to set +> The `chainId` column shows the EVM chain ID (for example, `11155111` for Sepolia). The `chainSelector` is CCIP's own chain identifier used internally by the protocol. You do not need to set > chain selectors yourself; the CLI and tasks resolve them automatically from the chain name. 2. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia using **faster than finality**: @@ -121612,10 +121566,8 @@ Revert.allowedFinality = 32n > **NOTE: Understand Minimum Block Depth Requirements 🧑‍🏫** > > 1. The last command failed because the pool's **allowed finality config** requires a larger block depth -> for numeric faster than finality (for example, if you configured `--blockdepth 32`) but we requested -> `finality=7`. -> 2. Try running the command again with an appropriate value for `finality` (for example, -> `--extra finality=20`), or omit `finality` to use default finality ( +> for numeric faster than finality (for example, if you configured `--blockdepth 32`) but we requested `finality=7`. +> 2. Try running the command again with an appropriate value for `finality` (for example, `--extra finality=20`), or omit `finality` to use default finality ( > `finality=finalized`). > 3. If you see a revert like `InvalidRequestedFinality(requestedFinality, allowedFinality)`, it means the > requested mode (or depth) is not permitted by the pool. @@ -121710,9 +121662,7 @@ In this tutorial you will: > **NOTE: If you do not explicitly configure these features, default behavior applies** > -> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting -> `finality` or using `finality=finalized`) unless you opt into faster than finality via -> `finality` (for example, `finality=32`). +> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting `finality` or using `finality=finalized`) unless you opt into faster than finality via `finality` (for example, `finality=32`). > 2. **Opt-in changes.** New v2 behavior only applies when you explicitly configure it (for example, > pool-level fees or faster than finality). @@ -121762,8 +121712,7 @@ cast wallet import your_keystore_name --interactive > **NOTE: Note** > -> 1. For more information on how to create an encrypted Foundry keystore, refer to the -> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 1. For more information on how to create an encrypted Foundry keystore, refer to the [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). > 2. `cast wallet import` will throw an error if a keystore with the same name already exists. > 3. Use `cast wallet list` to check previously created keystores. @@ -121961,8 +121910,7 @@ forge script \ > broadcaster is used. This same variable is reused in the [Claim Admin Role](#claim-admin-role) step. > 4. `TOKEN_PRE_MINT_RECIPIENT` is optional and applies only when `TOKEN_PRE_MINT > 0`. If not > set, pre-minted tokens default to the broadcaster. -> 5. `TOKEN_PRE_MINT` is required to be set to a value greater than 0, only if -> `TOKEN_PRE_MINT_RECIPIENT` is set. +> 5. `TOKEN_PRE_MINT` is required to be set to a value greater than 0, only if `TOKEN_PRE_MINT_RECIPIENT` is set. Your output should look something like this: @@ -122071,8 +122019,7 @@ script/deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.jso > values from your current shell environment. > 2. Run the `export` commands so subsequent scripts in this terminal session can discover the correct > addresses. -> 3. You can also pass the addresses directly as inline environment variables `TOKEN=0x...` / -> `TOKEN_POOL=0x...` on any script that requires them. +> 3. You can also pass the addresses directly as inline environment variables `TOKEN=0x...` / `TOKEN_POOL=0x...` on any script that requires them. ```bash filename="Terminal" export ETHEREUM_SEPOLIA_TOKEN=0x... @@ -122346,8 +122293,7 @@ This process involves interacting with the [`RegistryModuleOwnerCustom`](https:/ > > 1. If your token implements `getCCIPAdmin()`, the script calls `registerAdminViaGetCCIPAdmin()` > . -> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the script calls -> `registerAdminViaOwner()` instead. +> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the script calls `registerAdminViaOwner()` instead. > > No extra configuration is needed;both patterns are fully supported. @@ -122359,8 +122305,7 @@ View the admin claim script on GitHub. > **NOTE: Note** > -> 1. If you do not set `CCIP_ADMIN_ADDRESS`, the script defaults it to the broadcaster (the signer used in -> `--account`). +> 1. If you do not set `CCIP_ADMIN_ADDRESS`, the script defaults it to the broadcaster (the signer used in `--account`). > 2. If you set `CCIP_ADMIN_ADDRESS` to a different address, the `ClaimAdmin.s.sol` transaction > must be signed by that same address (use that address's keystore in `--account`), or the claim step > will revert. @@ -122697,8 +122642,7 @@ forge script \ > **NOTE: Pool-level finality vs. transaction-level finality** > -> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via -> `setAllowedFinalityConfig(bytes4)`. +> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via `setAllowedFinalityConfig(bytes4)`. > > - Default = faster than finality is **disabled**. The pool only allows transfers that use default > finality (omit `finality` or use `finality=finalized`). @@ -123080,10 +123024,8 @@ CCIP Explorer: https://ccip.chain.link/msg/0x5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c > **NOTE: Understand Minimum Block Depth Requirements 🧑‍🏫** > > 1. If a transfer reverts with `InvalidRequestedFinality(requestedFinality, allowedFinality)`, it usually -> means the pool requires a larger block depth than you requested (for example, the pool minimum is `32` -> but you requested a smaller value). -> 2. Use a value of at least `32` (for example, `--extra finality=32`), or omit -> `finality` for default finality. +> means the pool requires a larger block depth than you requested (for example, the pool minimum is `32` but you requested a smaller value). +> 2. Use a value of at least `32` (for example, `--extra finality=32`), or omit `finality` for default finality. > 3. Other reverts may indicate the requested depth (or mode) is not permitted by the pool. #### 3. (Optional) Hand Off Token Administration After Enablement @@ -123197,9 +123139,7 @@ In this tutorial you will: > **NOTE: If you do not explicitly configure these features, default behavior applies** > -> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting -> `finality` or using `finality=finalized`) unless you opt into faster than finality via -> `finality` (for example, `finality=32`). +> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting `finality` or using `finality=finalized`) unless you opt into faster than finality via `finality` (for example, `finality=32`). > 2. **Opt-in changes.** New v2 behavior only applies when you explicitly configure it (for example, > pool-level fees or faster than finality). @@ -123282,8 +123222,7 @@ npx hardhat keystore set > > 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it > asks you to enter your private key as hidden text. -> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in -> `.env`. +> 2. The name you choose (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in `.env`. > 3. Use `npx hardhat keystore list` to check previously created keystores. ### 2. How to configure deployment parameters? @@ -123790,8 +123729,7 @@ This process involves interacting with the [`RegistryModuleOwnerCustom`](https:/ > The task auto-detects how your token exposes its admin and uses the appropriate registration method: > > 1. If your token implements `getCCIPAdmin()`, the task calls `registerAdminViaGetCCIPAdmin()`. -> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the task calls -> `registerAdminViaOwner()` instead. +> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the task calls `registerAdminViaOwner()` instead. > > No extra configuration is needed: both patterns are fully supported. @@ -124114,8 +124052,7 @@ npx hardhat getSupportedChains --network arbitrumSepolia > **NOTE: Pool-level finality vs. transaction-level finality** > -> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via -> `setAllowedFinalityConfig(bytes4)`. +> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via `setAllowedFinalityConfig(bytes4)`. > > - Default = faster than finality is **disabled**. The pool only allows transfers that use default > finality (omit `finality` or use `finality=finalized`). @@ -124478,10 +124415,8 @@ CCIP Explorer: https://ccip.chain.link/msg/0x5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c > **NOTE: Understand Minimum Block Depth Requirements 🧑‍🏫** > > 1. If a transfer reverts with `InvalidRequestedFinality(requestedFinality, allowedFinality)`, it usually -> means the pool requires a larger block depth than you requested (for example, the pool minimum is `32` -> but you requested a smaller value). -> 2. Use a value of at least `32` (for example, `--extra finality=32`), or omit -> `finality` for default finality. +> means the pool requires a larger block depth than you requested (for example, the pool minimum is `32` but you requested a smaller value). +> 2. Use a value of at least `32` (for example, `--extra finality=32`), or omit `finality` for default finality. > 3. Other reverts may indicate the requested depth (or mode) is not permitted by the pool. #### 3. (Optional) Hand Off Token Administration After Enablement @@ -124629,8 +124564,7 @@ cast wallet import your_fee_admin_keystore_name --interactive > **NOTE: Keystore** > -> 1. For more information on encrypted Foundry keystores, refer to the -> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 1. For more information on encrypted Foundry keystores, refer to the [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). > 2. `cast wallet import` will throw an error if a keystore with the same name already exists. > 3. Use `cast wallet list` to check previously created keystores. @@ -124686,10 +124620,7 @@ npm install && forge build > - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry) > - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) > -> Use the token pool address for the chain you are configuring. In a Burn & Mint setup, both pools are -> `BurnMintTokenPool` contracts. In a Lock & Mint setup, the source chain can use a -> `LockReleaseTokenPool` backed by an `ERC20LockBox`, while the destination chain uses a -> `BurnMintTokenPool`. This tutorial configures the token pool, not the lockbox. +> Use the token pool address for the chain you are configuring. In a Burn & Mint setup, both pools are `BurnMintTokenPool` contracts. In a Lock & Mint setup, the source chain can use a `LockReleaseTokenPool` backed by an `ERC20LockBox`, while the destination chain uses a `BurnMintTokenPool`. This tutorial configures the token pool, not the lockbox. > > ```bash filename="Terminal" > export ETHEREUM_SEPOLIA_TOKEN=0x... @@ -125002,8 +124933,7 @@ View the fee config update script on GitHub. > > - **Flat fee:** values are in `0.01 USD (1 cent)` units. For example, `1` means > $0.01 and `100` means $1.00. -> - **Basis points (bps):** `1 basis points = 0.01%`, `100 basis points = 1%`, and -> `10000 basis points = 100%`. Values must be below `10000`. +> - **Basis points (bps):** `1 basis points = 0.01%`, `100 basis points = 1%`, and `10000 basis points = 100%`. Values must be below `10000`. > **CAUTION: Validation rules** > @@ -125677,10 +125607,8 @@ Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d > - **Missing lane:** confirm the pool lane was configured with `ApplyChainUpdates.s.sol`. > - **Wrong lane or pool:** confirm `--rpc-url`, `DEST_CHAIN`, and the source pool > env var match the intended source -> destination lane. -> - **Invalid config:** confirm `DEST_GAS_OVERHEAD` is not `0`, -> `DEST_BYTES_OVERHEAD` is at least `32`, and basis points values are below `10000`. -> - **Fast-finality revert:** confirm the pool allows block-depth finality and the requested -> `finality=` is not below the pool minimum. +> - **Invalid config:** confirm `DEST_GAS_OVERHEAD` is not `0`, `DEST_BYTES_OVERHEAD` is at least `32`, and basis points values are below `10000`. +> - **Fast-finality revert:** confirm the pool allows block-depth finality and the requested `finality=` is not below the pool minimum. > - **No accrued fees:** confirm `FEE_TOKENS` includes LINK and the source token, the send > completed, native fee token was not used, and flat or basis points fee values are non-zero. @@ -125806,8 +125734,7 @@ npx hardhat keystore set FEE_ADMIN_PRIVATE_KEY > > 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it > asks you to enter your private key as hidden text. -> 2. The name you choose, such as `PRIVATE_KEY`, is what you set as `KEYSTORE_NAME` in -> `.env`. +> 2. The name you choose, such as `PRIVATE_KEY`, is what you set as `KEYSTORE_NAME` in `.env`. > 3. Use `npx hardhat keystore list` to check previously created keystores. ## Tutorial @@ -125820,10 +125747,7 @@ npx hardhat keystore set FEE_ADMIN_PRIVATE_KEY > - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat) > - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat) > -> Use the token pool address for the chain you are configuring. In a Burn & Mint setup, both pools are -> `BurnMintTokenPool` contracts. In a Lock & Mint setup, the source chain can use a -> `LockReleaseTokenPool` backed by an `ERC20LockBox`, while the destination chain uses a -> `BurnMintTokenPool`. This tutorial configures the token pool, not the lockbox. +> Use the token pool address for the chain you are configuring. In a Burn & Mint setup, both pools are `BurnMintTokenPool` contracts. In a Lock & Mint setup, the source chain can use a `LockReleaseTokenPool` backed by an `ERC20LockBox`, while the destination chain uses a `BurnMintTokenPool`. This tutorial configures the token pool, not the lockbox. > > ```bash filename="Terminal" > export ETHEREUM_SEPOLIA_TOKEN=0x... @@ -126129,8 +126053,7 @@ View the fee config update task on GitHub. > > - **Flat fee:** values are in `0.01 USD (1 cent)` units. For example, `1` means > $0.01 and `100` means $1.00. -> - **Basis points (bps):** `1 basis points = 0.01%`, `100 basis points = 1%`, and -> `10000 basis points = 100%`. Values must be below `10000`. +> - **Basis points (bps):** `1 basis points = 0.01%`, `100 basis points = 1%`, and `10000 basis points = 100%`. Values must be below `10000`. > **CAUTION: Validation rules** > @@ -126790,10 +126713,8 @@ Both lanes should show `isEnabled: false` and zeroed fee fields. The OnRamp will > - **Missing lane:** confirm the pool lane was configured with `applyChainUpdates`. > - **Wrong lane or pool:** confirm `--network`, `--destchain`, and the source pool > env var (or `--tokenpool`) match the intended source -> destination lane. -> - **Invalid config:** confirm `--destgasoverhead` is not `0`, -> `--destbytesoverhead` is at least `32`, and basis points values are below `10000`. -> - **Fast-finality revert:** confirm the pool allows block-depth finality and the requested -> `finality=` is not below the pool minimum. +> - **Invalid config:** confirm `--destgasoverhead` is not `0`, `--destbytesoverhead` is at least `32`, and basis points values are below `10000`. +> - **Fast-finality revert:** confirm the pool allows block-depth finality and the requested `finality=` is not below the pool minimum. > - **No accrued fees:** confirm `--feetokens` includes LINK and the source token, the send > completed, native fee token was not used, and flat or basis points fee values are non-zero. @@ -126864,8 +126785,7 @@ cast wallet import your_keystore_name --interactive > **NOTE: Note** > -> 1. For more information on how to create an encrypted Foundry keystore, refer to the -> [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). +> 1. For more information on how to create an encrypted Foundry keystore, refer to the [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations). > 2. `cast wallet import` will throw an error if a keystore with the same name already exists. > 3. Use `cast wallet list` to check previously created keystores. @@ -126928,9 +126848,7 @@ npm install && forge build > This tutorial supports both CCT registration models: > > - In the Burn & Mint model, rate limiters are configured on the `BurnMintTokenPool` on each chain. -> - In the Lock & Mint model, rate limiters are configured on the token pools: the source-chain -> `LockReleaseTokenPool` and the destination-chain `BurnMintTokenPool`. The -> `ERC20LockBox` is not rate-limited and is not updated by the scripts in this tutorial. +> - In the Lock & Mint model, rate limiters are configured on the token pools: the source-chain `LockReleaseTokenPool` and the destination-chain `BurnMintTokenPool`. The `ERC20LockBox` is not rate-limited and is not updated by the scripts in this tutorial. ### 1. Review Current Rate Limiter Configurations @@ -127205,8 +127123,7 @@ Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d > > 1. We highly recommend that devs have at least some limits set up. We advise against completely disabling rate > limiters unless absolutely necessary. -> 2. **Informational-only for this tutorial:** If you disable rate limiters here, the later -> **Test the Rate Limiter Configurations** section will not behave as described (it will not revert +> 2. **Informational-only for this tutorial:** If you disable rate limiters here, the later **Test the Rate Limiter Configurations** section will not behave as described (it will not revert > with `TokenMaxCapacityExceeded` / `TokenRateLimitReached`). ```bash filename="Terminal" @@ -127453,8 +127370,7 @@ This section applies to [`TokenPool`](https://github.com/smartcontractkit/chainl > **CAUTION: Faster Than Finality must be allowed first** > > 1. Updating the faster than finality bucket alone does **not** enable faster than finality transfers. -> The pool owner must also configure the pool's **allowed finality config** with -> `SetFinalityConfig.s.sol`. +> The pool owner must also configure the pool's **allowed finality config** with `SetFinalityConfig.s.sol`. > 2. Setting the allowed finality config is owner-only. Updating rate limiters can be done by the pool owner or the > configured `rateLimitAdmin`. > 3. For the full faster than finality setup flow, see the Configure Faster Than Finality section in either @@ -127684,10 +127600,7 @@ DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \ > **NOTE: Faster Than Finality troubleshooting** > -> 1. This test assumes the pool's allowed finality config allows numeric block depth. If the transfer reverts with -> `InvalidRequestedFinality(requestedFinality, allowedFinality)`, run -> `GetFinalityConfig.s.sol` and confirm the configured `BLOCK_DEPTH`, then retry with -> `--extra finality=32` (or higher than the configured minimum). +> 1. This test assumes the pool's allowed finality config allows numeric block depth. If the transfer reverts with `InvalidRequestedFinality(requestedFinality, allowedFinality)`, run `GetFinalityConfig.s.sol` and confirm the configured `BLOCK_DEPTH`, then retry with `--extra finality=32` (or higher than the configured minimum). > 2. If you request a numeric value lower than the configured minimum block depth, the token pool will revert. --- @@ -127789,8 +127702,7 @@ npx hardhat keystore set > > 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it > asks you to enter your private key as hidden text. -> 2. The name you choose (e.g. ``) is what you set as -> `KEYSTORE_NAME` in `.env`. +> 2. The name you choose (e.g. ``) is what you set as `KEYSTORE_NAME` in `.env`. > 3. Use `npx hardhat keystore list` to check previously created keystores. ## Tutorial @@ -127818,9 +127730,7 @@ npx hardhat keystore set > This tutorial supports both CCT registration models: > > - In the Burn & Mint model, rate limiters are configured on the `BurnMintTokenPool` on each chain. -> - In the Lock & Mint model, rate limiters are configured on the token pools: the source-chain -> `LockReleaseTokenPool` and the destination-chain `BurnMintTokenPool`. The -> `ERC20LockBox` is not rate-limited and is not updated by the tasks in this tutorial. +> - In the Lock & Mint model, rate limiters are configured on the token pools: the source-chain `LockReleaseTokenPool` and the destination-chain `BurnMintTokenPool`. The `ERC20LockBox` is not rate-limited and is not updated by the tasks in this tutorial. ### 1. Review Current Rate Limiter Configurations @@ -128089,8 +127999,7 @@ Transaction: 0x2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70812 > > 1. We highly recommend that devs have at least some limits set up. We advise against completely disabling rate > limiters unless absolutely necessary. -> 2. **Informational-only for this tutorial:** If you disable rate limiters here, the later -> **Test the Rate Limiter Configurations** section will not behave as described (it will not revert +> 2. **Informational-only for this tutorial:** If you disable rate limiters here, the later **Test the Rate Limiter Configurations** section will not behave as described (it will not revert > with `TokenMaxCapacityExceeded` / `TokenRateLimitReached`). ```bash filename="Terminal" @@ -128328,8 +128237,7 @@ This section applies to [`TokenPool`](https://github.com/smartcontractkit/chainl > **CAUTION: Faster Than Finality must be allowed first** > > 1. Updating the faster than finality bucket alone does **not** enable faster than finality transfers. -> The pool owner must also configure the pool's **allowed finality config** with -> `setFinalityConfig`. +> The pool owner must also configure the pool's **allowed finality config** with `setFinalityConfig`. > 2. Setting the allowed finality config is owner-only. Updating rate limiters can be done by the pool owner or the > configured `rateLimitAdmin`. > 3. For the full faster than finality setup flow, see the Configure Faster Than Finality section in either @@ -128384,8 +128292,7 @@ npx hardhat getFinalityConfig --network arbitrumSepolia > **NOTE: Token pool ABI** > > The Hardhat task reads shared TokenPool v2 functions, so the same command works for both Burn & Mint pools and -> Lock & Mint token pools. If you are not using the `{CHAIN}_TOKEN_POOL` environment variable, pass -> `--tokenpool 0xYourPool`. +> Lock & Mint token pools. If you are not using the `{CHAIN}_TOKEN_POOL` environment variable, pass `--tokenpool 0xYourPool`. #### Approach 1: Allow Faster Than Finality and Configure the Faster Than Finality Bucket in One Command @@ -128548,9 +128455,7 @@ npx hardhat getCurrentRateLimits \ > **NOTE: Faster Than Finality troubleshooting** > -> 1. This test assumes the pool's allowed finality config allows numeric block depth. If the transfer reverts with -> `InvalidRequestedFinality(requestedFinality, allowedFinality)`, run -> `npx hardhat getFinalityConfig --network sepolia` and confirm the configured `BLOCK_DEPTH`, +> 1. This test assumes the pool's allowed finality config allows numeric block depth. If the transfer reverts with `InvalidRequestedFinality(requestedFinality, allowedFinality)`, run `npx hardhat getFinalityConfig --network sepolia` and confirm the configured `BLOCK_DEPTH`, > then retry with `--extra finality=32` (or higher than the configured minimum). > 2. If you request a numeric value lower than the configured minimum block depth, the token pool will revert. diff --git a/src/content/chainlink-nodes/llms-full.txt b/src/content/chainlink-nodes/llms-full.txt index bbe416a2872..2b68f3e3345 100644 --- a/src/content/chainlink-nodes/llms-full.txt +++ b/src/content/chainlink-nodes/llms-full.txt @@ -1898,8 +1898,7 @@ Webhook jobs can be initiated by HTTP request, either by a user or external init > **NOTE** > > The `ExternalInitiatorsEnabled` config key is unused. It is retained for the legacy external initiator feature that -> fed the `webhook` job type. See -> [ExternalInitiatorsEnabled](/chainlink-nodes/v1/node-config#externalinitiatorsenabled) in the config +> fed the `webhook` job type. See [ExternalInitiatorsEnabled](/chainlink-nodes/v1/node-config#externalinitiatorsenabled) in the config > reference for details. This is an example webhook job: diff --git a/src/content/crec/llms-full.txt b/src/content/crec/llms-full.txt index 0d356d862d9..5ee2006a953 100644 --- a/src/content/crec/llms-full.txt +++ b/src/content/crec/llms-full.txt @@ -7411,8 +7411,7 @@ security: > **NOTE: SDK auth header** > -> The Go SDK applies this header for you automatically: just pass the key to -> `crec.NewClient(baseURL, apiKey, ...)`. You only need to set the header manually when calling the REST API +> The Go SDK applies this header for you automatically: just pass the key to `crec.NewClient(baseURL, apiKey, ...)`. You only need to set the header manually when calling the REST API > directly (for example, with `curl`). Treat API keys as secrets: they grant full access to your organisation's channels, watchers, wallets, and operations. Rotate any key that may have been exposed. diff --git a/src/content/data-feeds/llms-full.txt b/src/content/data-feeds/llms-full.txt index 3bb8088c94c..4247e40abbb 100644 --- a/src/content/data-feeds/llms-full.txt +++ b/src/content/data-feeds/llms-full.txt @@ -926,8 +926,7 @@ For more information about evaluating data quality, assessing liquidity and dist > **NOTE: Need more details?** > -> For additional information about data sourcing, please -> [contact us](https://chain.link/contact?ref_id=pricefeeds). +> For additional information about data sourcing, please [contact us](https://chain.link/contact?ref_id=pricefeeds). --- @@ -6191,8 +6190,7 @@ Source: https://docs.chain.link/data-feeds/svr-feeds > **TIP: SVR Now Available on Base, Arbitrum, and BNB Chain** > > Chainlink SVR has expanded beyond Ethereum Mainnet and is now available on Base, Arbitrum, and BNB Chain via the -> Atlas auction system. See the -> [Atlas searcher onboarding guide](/data-feeds/svr-feeds/searcher-onboarding-atlas) for integration +> Atlas auction system. See the [Atlas searcher onboarding guide](/data-feeds/svr-feeds/searcher-onboarding-atlas) for integration > details. [Chainlink Smart Value Recapture](https://blog.chain.link/chainlink-smart-value-recapture-svr/) (SVR) Feeds introduce a novel and secure way to recapture Oracle Extractable Value (OEV)—a subset of non-toxic Maximal Extractable Value (MEV) associated with oracle updates that is most commonly observed during the liquidation process of lending protocols. The value recaptured by SVR provides DeFi protocols with an additional revenue stream while also supporting the economic sustainability of Chainlink oracle. SVR Feeds are read in the same manner as standard Chainlink Data Feeds (see [Using Data Feeds](/data-feeds/using-data-feeds)), and simply require that users specify an SVR-enabled feed address. @@ -6463,9 +6461,7 @@ Instead of relying on block builders to bundle together the oracle update and th > searcher at the start of the auction. > - Searchers must sign for a `gasPrice` that is **exactly equal** to the gas price specified by the > Chainlink oracle. -> - Searchers must not attempt to use a gas limit for their operation that is greater than the allowed -> `SolverGasLimit`. Query this value by calling `getSolverGasLimit` on the -> `DappControl` +> - Searchers must not attempt to use a gas limit for their operation that is greater than the allowed `SolverGasLimit`. Query this value by calling `getSolverGasLimit` on the `DappControl` > contract. Sending `SolverOps` with a higher gas limit will not work. > - Since auction and `auctionSolution` payloads can exceed default read/write buffer sizes in popular > WebSocket libraries, explicitly configure read and write buffers to greater than **10 KB**. diff --git a/src/content/data-streams/llms-full.txt b/src/content/data-streams/llms-full.txt index a32712b0323..6dc7d750488 100644 --- a/src/content/data-streams/llms-full.txt +++ b/src/content/data-streams/llms-full.txt @@ -155,8 +155,7 @@ Chainlink Data Streams uses a subscription-based billing option. > **CAUTION: [Deprecated] Pay-per-verification model** > -> The pay-per-verification billing model has been deprecated. To learn more about the subscription model, please -> [contact us](https://chainlinkcommunity.typeform.com/datastreams?#ref_id=docs). +> The pay-per-verification billing model has been deprecated. To learn more about the subscription model, please [contact us](https://chainlinkcommunity.typeform.com/datastreams?#ref_id=docs). --- @@ -446,8 +445,7 @@ Instead of aggregating only raw market data, calculated streams compute derived > **CAUTION: Important Considerations** > > Calculated streams behave differently from standard streams in important ways. Before integrating a calculated stream, -> review the [key differences](#key-differences-from-standard-streams) and -> [risks and mitigation](#risks-and-mitigation) sections to understand how these differences may affect your +> review the [key differences](#key-differences-from-standard-streams) and [risks and mitigation](#risks-and-mitigation) sections to understand how these differences may affect your > application. ## Why use calculated streams? @@ -573,8 +571,7 @@ Temporary differences between the calculated stream and other data sources are e > **NOTE: Developer Responsibilities** > > When using calculated streams, you inherit all responsibilities associated with the underlying inputs plus additional -> responsibilities for monitoring calculation health and managing time-sensitivity requirements. Review the -> [Developer Responsibilities](/data-streams/developer-responsibilities) page for comprehensive guidance on +> responsibilities for monitoring calculation health and managing time-sensitivity requirements. Review the [Developer Responsibilities](/data-streams/developer-responsibilities) page for comprehensive guidance on > risk assessment and mitigation techniques. *** @@ -3549,8 +3546,7 @@ To learn more about the Data Streams API authentication, see the [Data Streams A > **NOTE: Type Stripping Warning** > > When running the example, you might see a warning about "Type Stripping is an experimental feature". This is normal -> with Node.js v20+ and doesn't affect the functionality of the code. You can suppress this warning by running with -> NODE_NO_WARNINGS=1 ts-node auth-example.ts instead. +> with Node.js v20+ and doesn't affect the functionality of the code. You can suppress this warning by running with NODE_NO_WARNINGS=1 ts-node auth-example.ts instead. **Example code**: @@ -3813,8 +3809,7 @@ While this example demonstrates the authentication mechanism, production applica > **NOTE: Type Stripping Warning** > > When running the example, you might see a warning about "Type Stripping is an experimental feature". This is normal -> with Node.js v20+ and doesn't affect the functionality of the code. You can suppress this warning by running with -> NODE_NO_WARNINGS=1 ts-node ws-auth-example.ts instead. +> with Node.js v20+ and doesn't affect the functionality of the code. You can suppress this warning by running with NODE_NO_WARNINGS=1 ts-node ws-auth-example.ts instead. **Example code**: @@ -6469,8 +6464,7 @@ Standard crypto prices DEX state crypto prices -`price`, `bid`, `ask` (identical fields; -[details](/data-streams/concepts/dex-state-price-streams#how-to-use-dex-state-price-streams)) +`price`, `bid`, `ask` (identical fields; [details](/data-streams/concepts/dex-state-price-streams#how-to-use-dex-state-price-streams)) [Exchange Rate](/data-streams/exchange-rate-streams) @@ -6492,8 +6486,7 @@ Real-world asset prices (single price point) Real-world asset prices (multiple price points and enhanced market data) -`mid`, `bid`, `ask`, `bidVolume`, `askVolume`, -`lastTradedPrice`, `marketStatus` +`mid`, `bid`, `ask`, `bidVolume`, `askVolume`, `lastTradedPrice`, `marketStatus` [SmartData](/data-streams/smartdata-streams) @@ -6509,8 +6502,7 @@ Net Asset Value (NAV), Proof of Reserve (POR) Tokenized equities -`price`, `tokenizedPrice`, `marketStatus`, `currentMultiplier`, -`newMultiplier` +`price`, `tokenizedPrice`, `marketStatus`, `currentMultiplier`, `newMultiplier` ## Cryptocurrency @@ -6662,8 +6654,7 @@ Chainlink Data Streams that use the RWA Advanced (v11) schema adhere to the stru > **CAUTION: Use marketStatus, not timestamps** > -> Always use `marketStatus` to determine whether a market is open. Do **not** use -> `observationsTimestamp`, `lastSeenTimestampNs`, or other timestamp fields — timestamps indicate +> Always use `marketStatus` to determine whether a market is open. Do **not** use `observationsTimestamp`, `lastSeenTimestampNs`, or other timestamp fields — timestamps indicate > when data was last recorded, not whether the market is currently active. The v11 schema defines six numeric values (`0`–`5`), but **the values that appear on a given feed depend on its trading-hours model**. Check which mapping applies to your stream before implementing logic. For full trading schedules, see [Market Hours](/data-streams/market-hours). @@ -7961,8 +7952,7 @@ After the key is generated, the **Credentials** tab displays a **one-time privat > **CAUTION: Credentials are shown only once** > -> The Password and API Key are not stored in the portal. Copy both values and store them securely before clicking -> **Dismiss**. If you lose them, you must rotate your credentials to generate new ones. +> The Password and API Key are not stored in the portal. Copy both values and store them securely before clicking **Dismiss**. If you lose them, you must rotate your credentials to generate new ones. #### Two services, one username, two passwords @@ -8063,8 +8053,7 @@ Click **Manage Subscription** to open the Stripe customer portal for invoice his > **NOTE: Manual review** > > Cancellations are reviewed and confirmed manually. Once confirmed, your subscription will not renew at the end of the -> current billing period and you will not be charged again. Feed access remains active through the -> **Period End** date shown in your subscription. +> current billing period and you will not be charged again. Feed access remains active through the **Period End** date shown in your subscription. Cancelled subscriptions move to the **Inactive** tab once the period ends. diff --git a/src/lib/markdown/__tests__/transformMarkdown.test.ts b/src/lib/markdown/__tests__/transformMarkdown.test.ts index d230b1886dd..e7faf4d7abe 100644 --- a/src/lib/markdown/__tests__/transformMarkdown.test.ts +++ b/src/lib/markdown/__tests__/transformMarkdown.test.ts @@ -806,3 +806,66 @@ Searchers must sign for an **exactly equal** gas price. expect(result).not.toMatch(/^\s*>?\s*;\s*$/m) }) }) + +describe("inline JSX space serialization", () => { + it.each(["ol", "ul"])("keeps a JSX space and source newline inline in %s callout items", async (list) => { + const source = `` + const result = await transformMarkdown(source, "fixture.mdx") + const first = list === "ol" ? "1." : "-" + const second = list === "ol" ? "2." : "-" + + expect(result).toBe( + `> **NOTE: Gas limit**\n>\n> ${first} Foundry scripts default to **400,000** gas. Hardhat scripts estimate gas unless you set \`GAS_LIMIT\` (and fall back to 400,000 if estimation fails).\n> ${second} Test all scenarios before deployment.\n` + ) + expect(result).not.toContain(" ") + }) + + it("keeps the Data Feeds callout's JSX spaces next to inline code", async () => { + const source = `` + + expect(await transformMarkdown(source, "fixture.mdx")).toBe( + "> **CAUTION: Important**\n>\n> - Searchers must not exceed the allowed `SolverGasLimit`. Query `getSolverGasLimit` on the `DappControl` contract.\n" + ) + }) + + it.each([ + ['Use A{" "}\nB.', "Use `A` `B`.\n"], + ['Read{" "}\nthe guide.', "Read [the guide](https://example.com).\n"], + ['Read\n{" "}next.', "Read next.\n"], + ['Read{" "}\r\nnext.', "Read next.\n"], + ])("keeps an inline separator across a source line break: %s", async (source, expected) => { + expect(await transformMarkdown(source, "fixture.mdx")).toBe(expected) + }) + + it.each([ + ['Before \n{" "}after.', "Before\\\n after.\n"], + ['Before{" "}\\\nafter.', "Before \\\nafter.\n"], + ['First{" "}\n\nSecond paragraph.', "First \n\nSecond paragraph.\n"], + ['Use a{" "}\nb.', "Use `a \nb`.\n"], + ['before{"\\n"}{" "}CODE.', "before\n `CODE`.\n"], + ['before{"\\u00a0"}CODE.', "before\u00a0`CODE`.\n"], + ['before{" "}CODE.', "before `CODE`.\n"], + ])( + "preserves explicit breaks, paragraph boundaries, code, and other literal spaces: %s", + async (source, expected) => { + expect(await transformMarkdown(source, "fixture.mdx")).toBe(expected) + } + ) + + it("leaves JSX-looking text inside inline and fenced code unchanged", async () => { + const source = 'Inline `{" "}`.\n\n```jsx\nset{" "}\n GAS_LIMIT\n```\n\nAfter.' + expect(await transformMarkdown(source, "fixture.mdx")).toBe(source + "\n") + }) +}) diff --git a/src/lib/markdown/transformMarkdown.ts b/src/lib/markdown/transformMarkdown.ts index 08d376e067a..31b8dc71bf4 100644 --- a/src/lib/markdown/transformMarkdown.ts +++ b/src/lib/markdown/transformMarkdown.ts @@ -54,6 +54,34 @@ function staticMdxString(value: unknown): string | undefined { return typeof staticValue === "string" ? staticValue : undefined } +/** Keep an explicit JSX space inline with adjacent source text, including soft source line breaks. */ +function replaceInlineJsxSpace(parent: Parent, index: number): number { + const previous = parent.children[index - 1] + const next = parent.children[index + 1] + const previousText = previous?.type === "text" ? (previous as Literal) : undefined + const nextText = next?.type === "text" ? (next as Literal) : undefined + + // Generated text (for example, {"\n"}) keeps its intentional whitespace. + // Hard breaks and block boundaries are separate nodes and are never crossed. + if (previousText?.position) { + previousText.value = String(previousText.value).replace(/[ \t]*\r?\n[ \t]*$/, "") + } + if (nextText?.position) { + nextText.value = String(nextText.value).replace(/^[ \t]*\r?\n[ \t]*/, "") + } + if (previousText) { + previousText.value = String(previousText.value) + " " + (nextText ? String(nextText.value) : "") + parent.children.splice(index, nextText ? 2 : 1) + return index + } + if (nextText) { + nextText.value = " " + String(nextText.value) + parent.children.splice(index, 1) + return index + } + return replaceNode({ type: "mdxTextExpression" }, parent, index, [{ type: "text", value: " " } as Literal]) +} + /** * Preprocess CcipCommon components by inlining their content * Inlined MDX components continue through the normal remark AST visitor @@ -347,6 +375,7 @@ export async function transformMarkdown( const value = staticMdxString(node) if (value === undefined) return replaceNode(node, parent, index, []) if (node.type === "mdxFlowExpression" && !value.trim()) return replaceNode(node, parent, index, []) + if (node.type === "mdxTextExpression" && value === " ") return replaceInlineJsxSpace(parent, index) return replaceNode(node, parent, index, [{ type: "text", value } as Literal]) } diff --git a/src/scripts/check-markdown-fidelity.test.ts b/src/scripts/check-markdown-fidelity.test.ts index 5eef8e36f78..c8a5672295f 100644 --- a/src/scripts/check-markdown-fidelity.test.ts +++ b/src/scripts/check-markdown-fidelity.test.ts @@ -870,7 +870,8 @@ Second requirement. test.each(["ol", "ul"])("coalesces projected inline HTML in %s items before a repeated later value", (list) => { const source = ` @@ -911,6 +912,28 @@ Follow the **required** steps and keep all values in one place for chain1. ) }) +test("checks the Data Feeds JSX-space callout and detects a missing inline code value", () => { + const source = `` + const output = + "> **CAUTION: Important**\n>\n> - Searchers must not exceed the allowed `SolverGasLimit`. Query `getSolverGasLimit` on the `DappControl` contract." + + expect( + blockingFindings("focused", compareSourceToArtifact("fixture", "fixture.mdx", source, artifact(output))) + ).toEqual([]) + const incomplete = output.replace("`SolverGasLimit`", "") + expect( + compareSourceToArtifact("fixture", "fixture.mdx", source, artifact(incomplete)).some( + (finding) => finding.status === "missing" && finding.expected?.includes("SolverGasLimit") + ) + ).toBe(true) +}) + test("keeps diagnostics for dynamic inline HTML attributes and unprojected elements", () => { const source = "Keep this fact and unprojected emphasis." const analysis = analyzeSourceMarkdown(source, "fixture.mdx") From b5caedb9553c4928169fccabaa83532c8bf166fc Mon Sep 17 00:00:00 2001 From: Karim <98668332+khadni@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:05:23 -0500 Subject: [PATCH 6/6] fix: preserve explicit accordion anchors in Markdown and LLM exports --- src/content/ccip/llms-full.txt | 212 +++++++++--------- .../cross-chain-vault-adapter/llms-full.txt | 6 +- .../__tests__/transformMarkdown.test.ts | 87 +++++++ src/lib/markdown/componentHandlers.ts | 7 +- src/lib/markdown/transformMarkdown.ts | 16 ++ src/scripts/check-markdown-fidelity.test.ts | 114 ++++++++++ src/scripts/check-markdown-fidelity.ts | 134 ++++++++--- src/scripts/generate-llms.test.ts | 27 +++ 8 files changed, 461 insertions(+), 142 deletions(-) diff --git a/src/content/ccip/llms-full.txt b/src/content/ccip/llms-full.txt index 08dbc05ca43..032cb67280c 100644 --- a/src/content/ccip/llms-full.txt +++ b/src/content/ccip/llms-full.txt @@ -103850,14 +103850,14 @@ leveraging the power of CCIP 2.0's infrastructure. - [Transporter UI (Testnet)](https://test.transporter.io/) - [Transporter UI (Mainnet)](https://www.transporter.io/) -### 1. Connect Your Wallet +### 1. Connect Your Wallet 1. The Transporter UI supports most popular wallets, please make sure you have some funds on your desired chain in an account that is imported into one of these wallets. 2. Click the `Connect +` button on the top right of the Transporter UI to connect your wallet. ![Chainlink CCIP Transporter Connect Wallet](/images/ccip/tutorials/getting-started/Transporter-ui-1.png) -### 2. Select Lane and Approve Allowance +### 2. Select Lane and Approve Allowance 1. Select the source and destination chains from the network dropdowns. 2. This tutorial uses the testnet version of the Transporter UI, your view may differ depending on the version of the UI you are using, and the types of lanes that are @@ -103870,14 +103870,14 @@ leveraging the power of CCIP 2.0's infrastructure. ![Chainlink CCIP Transporter Approve Allowance](/images/ccip/tutorials/getting-started/Transporter-ui-3.png) -### 3. (Optional): Pay Gas Fee using LINK +### 3. (Optional): Pay Gas Fee using LINK 1. The UI allows you to pay for the transfer using `LINK` tokens, as shown in the screenshot attached. 2. If you select this option, the UI might ask for an additional approval of the `LINK` token, to pay for the gas fees. ![Chainlink CCIP Transporter Pay Gas Fee using LINK](/images/ccip/tutorials/getting-started/Transporter-ui-4.png) -### 4. Initiate Transfer +### 4. Initiate Transfer 1. After all the settings are configured, and allowances approved, click on the `Send` button to initiate the transfer. 2. Approve the transfer in the wallet popup. @@ -103908,7 +103908,7 @@ You will need: This section goes through the code for the `Sender` and `Receiver` contracts needed to complete the tutorial. We will use the same contracts for all three development environments. -#### 1. Sender code +#### 1. Sender code The sender contract interacts with CCIP to send data cross-chain. Key elements are explained below. @@ -104040,7 +104040,7 @@ The `sendMessage` function: > > - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact). -#### 2. Receiver code +#### 2. Receiver code The receiver contract interacts with CCIP to receive data on the destination chain. @@ -104154,7 +104154,7 @@ Send and verify a cross-chain message using CCIP in under 10 minutes, with your Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework. -## 1. Bootstrap a new Foundry project +## 1. Bootstrap a new Foundry project 1. Open a new terminal in a directory of your choice and run this command to initialize a new Foundry project: @@ -104210,7 +104210,7 @@ sepolia = "ENTER_YOUR_SEPOLIA_RPC_URL_HERE" arbitrumSepolia = "ENTER_YOUR_ARBITRUM_SEPOLIA_RPC_URL_HERE" ``` -## 2. Set up the contracts +## 2. Set up the contracts 1. Create a new directory named `src` at the root of the project if it doesn't already exist. 2. Create a new file named `Sender.sol` in this directory and paste the [sender contract code](#sender-code) inside it. @@ -104221,7 +104221,7 @@ arbitrumSepolia = "ENTER_YOUR_ARBITRUM_SEPOLIA_RPC_URL_HERE" forge build ``` -## 3. Send a cross-chain message +## 3. Send a cross-chain message 1. Create a new directory named `script` at the root of the project if it doesn't already exist. 2. Create a new file named `SendCrossChainMessage.s.sol` in this directory and paste the following code inside it: @@ -104323,7 +104323,7 @@ forge script script/SendCrossChainMessage.s.sol:SendCrossChainMessage --broadcas > - The `-vvvv` verbosity flag gives detailed runtime logs. > - The `--multi` flag sends transactions to multiple chains. -## 4. Verify message delivery +## 4. Verify message delivery 1. Create a new file named `VerifyCrossChainMessage.s.sol` in the `script` directory and paste the following code inside it: @@ -104376,7 +104376,7 @@ forge script script/VerifyCrossChainMessage.s.sol:VerifyCrossChainMessage Best for a `TypeScript`-based scripting workflow where you deploy contracts, send a CCIP message, and verify delivery from the command line. -## 1. Bootstrap a new Hardhat project +## 1. Bootstrap a new Hardhat project 1. Open a new terminal in a directory of your choice and run this command: @@ -104442,7 +104442,7 @@ The output of `npx hardhat keystore list` should look like this: ![Hardhat keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-1.png) -## 2. Set up the contracts +## 2. Set up the contracts 1. Create a new directory named `contracts` for your smart contracts if it doesn't already exist. 2. Create a new file named `Sender.sol` in this directory and paste the [sender contract code](#sender-code) inside it. @@ -104453,7 +104453,7 @@ The output of `npx hardhat keystore list` should look like this: npx hardhat build ``` -## 3. Send a cross-chain message +## 3. Send a cross-chain message 1. Create a new directory named `scripts` at the root of the project if it doesn't already exist. 2. Create a new file named `send-cross-chain-message.ts` in this directory and paste the following code inside it: @@ -104540,7 +104540,7 @@ console.log("Run the verification script after a few minutes to check if the mes npx hardhat run scripts/send-cross-chain-message.ts ``` -## 4. Verify message delivery +## 4. Verify message delivery 1. Wait for a few minutes for the message to be delivered to the receiver contract. @@ -104588,7 +104588,7 @@ npx hardhat run scripts/verify-cross-chain-message.ts Best for **Web3-native** workflows that prefer a browser-based IDE. -## 1. Deploy the sender contract +## 1. Deploy the sender contract Deploy the `Sender.sol` contract on *Ethereum Sepolia*. To see a detailed explanation of this contract, read the [Sender code](#sender-code) section. @@ -104613,7 +104613,7 @@ Deploy the `Sender.sol` contract on *Ethereum Sepolia*. To see a detailed explan 6. Open MetaMask and send 1 LINK to the contract address that you copied. Your contract will pay CCIP fees in LINK. -## 2. Deploy the receiver contract +## 2. Deploy the receiver contract Deploy the receiver contract on *Arbitrum Sepolia*. You will use this contract to receive data from the sender on *Ethereum Sepolia*. To see a detailed explanation of this contract, read the [Receiver code](#receiver-code) section. @@ -104640,7 +104640,7 @@ Deploy the receiver contract on *Arbitrum Sepolia*. You will use this contract t You now have one *sender* contract on *Ethereum Sepolia* and one *receiver* contract on *Arbitrum Sepolia*. You sent `1` LINK to the *sender* contract to pay the CCIP fees. Next, send data from the sender contract to the receiver contract. -## 3. Send data +## 3. Send data Send a `Hello World!` string from your contract on *Ethereum Sepolia* to the contract you deployed on *Arbitrum Sepolia*: @@ -104677,7 +104677,7 @@ After the transaction is finalized on the source chain, it will take a few minut ![Chainlink CCIP Explorer transaction details success](/images/ccip/tutorials/ccip-explorer-tx-details-success.webp) -## 4. Read data +## 4. Read data Read data stored by the receiver contract on *Arbitrum Sepolia*: @@ -105420,7 +105420,7 @@ Best for **Solidity-native** workflows that prefer a modular, powerful scripting > Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a > Foundry dev environment from scratch. -#### 1. Bootstrap a new project +#### 1. Bootstrap a new project [Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -105486,7 +105486,7 @@ source .env forge build ``` -#### 2. Deploy your contracts +#### 2. Deploy your contracts 1. Deploy `ProgrammableTokenTransfers` on both *Ethereum Sepolia* (source) and *Arbitrum Sepolia* (destination): @@ -105542,7 +105542,7 @@ export ETHEREUM_SEPOLIA_CONTRACT=0x2a0C1c7E6fD5b3A9b8C7d6E5f4A3B2c1D0eF9a8B && e Check out the complete script code on Github. -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality As a best practice, configure allowlists before sending messages. This script: @@ -105627,7 +105627,7 @@ 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 +#### 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. @@ -105656,7 +105656,7 @@ Recipient: 0x1A2b3C4d5E6f708192A3b4C5d6E7f8091A2b3C4D Check out the faucet script on Github. -#### 5. Trigger failure: send with low \`GAS_LIMIT\` and \`BLOCK_DEPTH=32\` +#### 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: @@ -105729,7 +105729,7 @@ https://ccip.chain.link/#/side-drawer/msg/0x8f3a1c9e5b7d2f4a6c8e0b3d5f7a9c1e3b5d Check out the complete script code on Github. -#### 6. Option A : Manual execute in CCIP Explorer (confirm + trigger) +#### 6. Option A : Manual execute in CCIP Explorer (confirm + trigger) Use [CCIP Explorer](https://ccip.chain.link/) to confirm the message is **Ready for manual execution** and trigger manual execution. @@ -105743,7 +105743,7 @@ Use [CCIP Explorer](https://ccip.chain.link/) to confirm the message is **Ready 4. 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) +#### 7. Option B : Manual execute with CCIP CLI (show + manual-exec) Use `ccip-cli` to confirm eligibility and retry execution from the terminal. @@ -105819,7 +105819,7 @@ Receipt (dest): > retry (not the transaction gas limit). Use `--gas-limit 0` to reuse the original message gas limit. A > non-zero override must not be lower than the original message gas limit. -#### 8. Verify receipt after manual execution +#### 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: @@ -105863,7 +105863,7 @@ Best for developers who want a mature, TypeScript-based smart contract developme > Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a > Hardhat dev environment from scratch. -#### 1. Bootstrap a new project +#### 1. Bootstrap a new project [CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -105925,7 +105925,7 @@ npm install -g @chainlink/ccip-cli ccip-cli --version ``` -#### 2. Deploy your contracts +#### 2. Deploy your contracts Deploy `ProgrammableTokenTransfers` on both chains: @@ -105975,7 +105975,7 @@ export ETHEREUM_SEPOLIA_CONTRACT= && export ARBITRUM_SE Check out the complete script code on Github. -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality ##### Finality configuration @@ -106039,7 +106039,7 @@ 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 +#### 4. Fund your wallet with test tokens and gas Drip `CCIP-BnM` to your wallet on *Ethereum Sepolia*: @@ -106070,7 +106070,7 @@ Check out the faucet script on Github. > The manual execution retry transaction is sent on *Arbitrum Sepolia*. Make sure the wallet you use has enough > *Arbitrum Sepolia ETH* to pay gas. -#### 5. Trigger failure: send with low \`GAS_LIMIT\` and \`BLOCK_DEPTH=32\` +#### 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. @@ -106139,7 +106139,7 @@ https://ccip.chain.link/#/side-drawer/msg/0x6b8d0f2a4c6e8b1d3f5a7c9e2b4d6f8a0c1e Check out the complete script code on Github. -#### 6. Option A : Manual execute in CCIP Explorer (confirm + trigger) +#### 6. Option A : Manual execute in CCIP Explorer (confirm + trigger) Use [CCIP Explorer](https://ccip.chain.link/) to confirm the message is **Ready for manual execution** and trigger manual execution. @@ -106153,7 +106153,7 @@ Use [CCIP Explorer](https://ccip.chain.link/) to confirm the message is **Ready 4. 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) +#### 7. Option B : Manual execute with CCIP CLI (show + manual-exec) Use `ccip-cli` to confirm eligibility and retry execution from the terminal. @@ -106235,7 +106235,7 @@ Receipt (dest): > retry (not the transaction gas limit). Use `--gas-limit 0` to reuse the original message gas limit. A > non-zero override must not be lower than the original message gas limit. -#### 8. Verify receipt after manual execution +#### 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: @@ -106688,7 +106688,7 @@ Best for **Solidity-native** workflows that prefer a modular, powerful scripting > Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a > Foundry dev environment from scratch. -#### 1. Bootstrap a new Foundry project +#### 1. Bootstrap a new Foundry project [Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -106754,7 +106754,7 @@ source .env forge build ``` -#### 2. Deploy your contracts +#### 2. Deploy your contracts In this section, you will deploy the `ProgrammableDefensiveTokenTransfers` contracts on the source and destination chains. @@ -106832,7 +106832,7 @@ Check out the complete file on Github here: Check out the complete script code on Github. -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality As a best practice, allowlist specific contracts to prevent unauthorized message delivery between your sender and receiver. @@ -106925,7 +106925,7 @@ Arbitrum Sepolia can receive messages from Ethereum Sepolia ⚠️ Messages will fail on destination (simRevert=true) to demonstrate defensive handling ``` -#### 4. Fund your wallet with test tokens +#### 4. Fund your wallet with test tokens [DripBnMToken.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/faucet/DripBnMToken.s.sol) @@ -106950,7 +106950,7 @@ CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS= forge script foun > If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the > [Chainlink faucet](https://faucets.chain.link/). -#### 5. Send a message +#### 5. Send a message `SendMessage.s.sol` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: @@ -107106,7 +107106,7 @@ https://ccip.chain.link/#/side-drawer/msg/0x2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192 Check out the complete script code on Github. -#### 6. Check failed messages and recover tokens +#### 6. Check failed messages and recover tokens After sending the message, wait for it to arrive on the destination chain (typically 10-20 minutes). Because `s_simRevert` is `true`, the message will fail and the tokens will be locked in the receiver contract. @@ -107212,7 +107212,7 @@ Best for developers who want a mature, TypeScript-based smart contract developme > Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a > Hardhat dev environment from scratch. -#### 1. Bootstrap a new Hardhat project +#### 1. Bootstrap a new Hardhat project [CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -107271,7 +107271,7 @@ npx hardhat keystore set your_keystore_name > > When prompted for your private key, include the `0x` prefix. -#### 2. Deploy your contracts +#### 2. Deploy your contracts [deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-defensive-token-transfers/deploy/deploy.ts) @@ -107343,7 +107343,7 @@ export ETHEREUM_SEPOLIA_CONTRACT= && \ export ARBITRUM_SEPOLIA_CONTRACT= ``` -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality As a best practice, allowlist specific contracts to prevent unauthorized message delivery between your sender and receiver. @@ -107434,7 +107434,7 @@ Arbitrum Sepolia can receive messages from Ethereum Sepolia ⚠️ Messages will fail on destination (simRevert=true) to demonstrate defensive handling ``` -#### 4. Fund your wallet with test tokens +#### 4. Fund your wallet with test tokens [drip-bnm-token.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/faucet/drip-bnm-token.ts) @@ -107452,7 +107452,7 @@ npx hardhat run hardhat/scripts/faucet/drip-bnm-token.ts If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the [Chainlink faucet](https://faucets.chain.link/). -#### 5. Send a message +#### 5. Send a message `send-message.ts` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: @@ -107621,7 +107621,7 @@ https://ccip.chain.link/#/side-drawer/msg/0x4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4 Check out the complete script code on Github. -#### 6. Check failed messages and recover tokens +#### 6. Check failed messages and recover tokens After sending the message, wait for it to arrive on the destination chain (typically 10-20 minutes). Because `s_simRevert` is `true`, the message will fail and the tokens will be locked in the receiver contract. @@ -108087,7 +108087,7 @@ Best for **Solidity-native** workflows that prefer a modular, powerful scripting > Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a > Foundry dev environment from scratch. -#### 1. Bootstrap a new Foundry project +#### 1. Bootstrap a new Foundry project [Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -108157,7 +108157,7 @@ source .env forge build ``` -#### 2. Deploy your contracts +#### 2. Deploy your contracts In this section, you will deploy the contracts on the source and destination chains. @@ -108228,7 +108228,7 @@ Check out the complete file on Github here: Check out the complete script code on Github. -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair. @@ -108327,7 +108327,7 @@ forge script foundry/scripts/tutorials/programmable-token-transfers/configure/Co --account $KEYSTORE_NAME --broadcast -vv ``` -#### 4. Fund your wallet with test tokens +#### 4. Fund your wallet with test tokens [DripBnMToken.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/faucet/DripBnMToken.s.sol) @@ -108354,7 +108354,7 @@ forge script foundry/scripts/faucet/DripBnMToken.s.sol \ > If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the > [Chainlink faucet](https://faucets.chain.link/). -#### 5. Send a message +#### 5. Send a message `SendMessage.s.sol` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: @@ -108501,7 +108501,7 @@ https://ccip.chain.link/#/side-drawer/msg/0xb2c3d4e5f60718293a4b5c6d7e8f90a1b2c3 Check out the complete script code on Github. -#### 6. Verify receipt on the destination chain +#### 6. Verify receipt on the destination chain The `GetLastReceivedMessageDetails.s.sol` script queries the receiver contract for the last received message. This is a read-only operation: no transaction is broadcast. @@ -108549,7 +108549,7 @@ Best for developers who want a mature, TypeScript-based smart contract developme > Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a > Hardhat dev environment from scratch. -#### 1. Bootstrap a new Hardhat project +#### 1. Bootstrap a new Hardhat project [CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -108608,7 +108608,7 @@ npx hardhat keystore set your_keystore_name > > When prompted for your private key, include the `0x` prefix. -#### 2. Deploy your contracts +#### 2. Deploy your contracts [deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/deploy/deploy.ts) @@ -108680,7 +108680,7 @@ export ETHEREUM_SEPOLIA_CONTRACT= && \ export ARBITRUM_SEPOLIA_CONTRACT= ``` -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair. @@ -108766,7 +108766,7 @@ ALLOWED_BLOCK_DEPTH=32 \ npx hardhat run hardhat/scripts/tutorials/programmable-token-transfers/configure/configure.ts ``` -#### 4. Fund your wallet with test tokens +#### 4. Fund your wallet with test tokens [drip-bnm-token.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/faucet/drip-bnm-token.ts) @@ -108784,7 +108784,7 @@ npx hardhat run hardhat/scripts/faucet/drip-bnm-token.ts If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the [Chainlink faucet](https://faucets.chain.link/). -#### 5. Send a message +#### 5. Send a message `send-message.ts` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: @@ -108937,7 +108937,7 @@ https://ccip.chain.link/#/side-drawer/msg/0xd4e5f60718293a4b5c6d7e8f90a1b2c3d4e5 Check out the complete script code on Github. -#### 6. Verify receipt on the destination chain +#### 6. Verify receipt on the destination chain [get-last-received-message-details.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/programmable-token-transfers/interact/get-last-received-message-details.ts) @@ -109259,7 +109259,7 @@ Best for Solidity-native workflows that prefer a modular, powerful scripting fra > Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a > Foundry dev environment from scratch. -#### 1. Bootstrap a new Foundry project +#### 1. Bootstrap a new Foundry project [Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -109320,7 +109320,7 @@ source .env forge build ``` -#### 2. Deploy your contracts (A and B) +#### 2. Deploy your contracts (A and B) [Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/deploy/Deploy.s.sol) @@ -109391,7 +109391,7 @@ export ETHEREUM_SEPOLIA_CONTRACT=0x... # MessageTracker export ARBITRUM_SEPOLIA_CONTRACT=0x... # Acknowledger ``` -#### 3. Fund the Acknowledger (required) +#### 3. Fund the Acknowledger (required) The Acknowledger pays CCIP fees for acknowledgment messages with native gas held by the contract. Fund it with Arbitrum Sepolia ETH before sending messages. @@ -109406,7 +109406,7 @@ cast send $ARBITRUM_SEPOLIA_CONTRACT \ --chain arbitrum-sepolia ``` -#### 4. Configure allowlists and finality +#### 4. Configure allowlists and finality [Configure.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/configure/Configure.s.sol) @@ -109488,7 +109488,7 @@ Ethereum Sepolia (MessageTracker) can send messages to Arbitrum Sepolia (Acknowl Arbitrum Sepolia (Acknowledger) can receive messages from Ethereum Sepolia and send acknowledgments back ``` -#### 5. Send a message (A→B) +#### 5. Send a message (A→B) [SendMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/SendMessage.s.sol) @@ -109635,7 +109635,7 @@ Copy the `CCIP messageId` from the output. | `MESSAGE_ID` | Message ID to query status (used by the status script) | Not set | | `CHAIN` | Chain name identifier used by the withdraw script (the chain where the Acknowledger is deployed) | Not set | -#### 6. Track status (B→A acknowledgment) +#### 6. Track status (B→A acknowledgment) [GetMessageStatus.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/GetMessageStatus.s.sol) @@ -109674,7 +109674,7 @@ https://ccip.chain.link/#/side-drawer/msg/0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f809 ======================================== ``` -#### 7. Withdraw from Acknowledger (optional cleanup) +#### 7. Withdraw from Acknowledger (optional cleanup) [WithdrawFromAcknowledger.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/WithdrawFromAcknowledger.s.sol) @@ -109720,7 +109720,7 @@ Best for devs looking for a mature, TypeScript-based smart contract development > Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a > Hardhat dev environment from scratch. -#### 1. Bootstrap a new Hardhat project +#### 1. Bootstrap a new Hardhat project [CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -109774,7 +109774,7 @@ npx hardhat keystore set your_keystore_name > > When prompted for your private key, include the `0x` prefix. -#### 2. Deploy your contracts (A and B) +#### 2. Deploy your contracts (A and B) [deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/deploy/deploy.ts) @@ -109841,7 +109841,7 @@ export ETHEREUM_SEPOLIA_CONTRACT=0x... # MessageTracker export ARBITRUM_SEPOLIA_CONTRACT=0x... # Acknowledger ``` -#### 3. Fund the Acknowledger (required) +#### 3. Fund the Acknowledger (required) The Acknowledger pays CCIP fees for acknowledgment messages with native gas held by the contract. Fund it with Arbitrum Sepolia ETH before sending messages. @@ -109857,7 +109857,7 @@ cast send $ARBITRUM_SEPOLIA_CONTRACT \ If you don't have `cast` installed, you can fund the contract from any wallet by sending Arbitrum Sepolia ETH to `$ARBITRUM_SEPOLIA_CONTRACT`. -#### 4. Configure allowlists and finality +#### 4. Configure allowlists and finality [configure.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/configure/configure.ts) @@ -109940,7 +109940,7 @@ SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA FEE_TOKEN=LINK GAS_LIM ======================================== ``` -#### 5. Send a message (A→B) +#### 5. Send a message (A→B) [send-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/send-message.ts) @@ -110083,7 +110083,7 @@ Copy the `CCIP messageId` from the output. | `MESSAGE_ID` | Message ID to query status (used by the status script) | Not set | | `CHAIN` | Chain name identifier used by the withdraw script (the chain where the Acknowledger is deployed) | Not set | -#### 6. Track status (B→A acknowledgment) +#### 6. Track status (B→A acknowledgment) [get-message-status.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/get-message-status.ts) @@ -110116,7 +110116,7 @@ https://ccip.chain.link/#/side-drawer/msg/0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f809 ======================================== ``` -#### 7. Withdraw from Acknowledger (optional cleanup) +#### 7. Withdraw from Acknowledger (optional cleanup) [withdraw-from-acknowledger.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/withdraw-from-acknowledger.ts) @@ -110476,7 +110476,7 @@ Best for **Solidity-native** workflows that prefer a modular, powerful scripting > Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a > Foundry dev environment from scratch. -#### 1. Bootstrap a new Foundry project +#### 1. Bootstrap a new Foundry project [Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -110537,7 +110537,7 @@ source .env forge build ``` -#### 2. Deploy your contracts +#### 2. Deploy your contracts [Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data/deploy/Deploy.s.sol) @@ -110613,7 +110613,7 @@ export ETHEREUM_SEPOLIA_CONTRACT= && \ export ARBITRUM_SEPOLIA_CONTRACT= ``` -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair. @@ -110705,7 +110705,7 @@ forge script foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s --broadcast -vv ``` -#### 4. Send a message +#### 4. Send a message `SendMessage.s.sol` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: @@ -110851,7 +110851,7 @@ Check out the complete script code on Github. Check out the lane-aware extraArgs helper on Github. -#### 5. Verify receipt on the destination chain +#### 5. Verify receipt on the destination chain The `GetLastReceivedMessageDetails.s.sol` script queries the receiver contract for the last received message. This is a read-only operation: no transaction is broadcast. @@ -110896,7 +110896,7 @@ Best for developers who want a mature, TypeScript-based smart contract developme > Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a > Hardhat dev environment from scratch. -#### 1. Bootstrap a new Hardhat project +#### 1. Bootstrap a new Hardhat project [CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -110955,7 +110955,7 @@ npx hardhat keystore set your_keystore_name > > When prompted for your private key, include the `0x` prefix. -#### 2. Deploy your contracts +#### 2. Deploy your contracts [deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data/deploy/deploy.ts) @@ -111027,7 +111027,7 @@ export ETHEREUM_SEPOLIA_CONTRACT= && \ export ARBITRUM_SEPOLIA_CONTRACT= ``` -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair. @@ -111113,7 +111113,7 @@ SOURCE_CHAIN=ARBITRUM_SEPOLIA DEST_CHAIN=ETHEREUM_SEPOLIA ALLOWED_FINALITY_CONFI npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/configure/configure.ts ``` -#### 4. Send a message +#### 4. Send a message `send-message.ts` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: @@ -111255,7 +111255,7 @@ Check out the complete script code on Github. Check out the lane-aware extraArgs helper used by Hardhat scripts. -#### 5. Verify receipt on the destination chain +#### 5. Verify receipt on the destination chain [get-last-received-message-details.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data/interact/get-last-received-message-details.ts) @@ -111544,7 +111544,7 @@ Best for **Solidity-native** workflows that prefer a modular, powerful scripting > Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a > Foundry dev environment from scratch. -#### 1. Bootstrap a new Foundry project +#### 1. Bootstrap a new Foundry project [Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -111614,7 +111614,7 @@ source .env forge build ``` -#### 2. Deploy your contract +#### 2. Deploy your contract In this section, you will deploy `TokenTransferor` on the source chain (*Ethereum Sepolia*). \ Unlike the programmable token transfers tutorial, no destination contract is needed because tokens are transferred to an address (EOA or contract) on the destination chain. @@ -111669,7 +111669,7 @@ export ETHEREUM_SEPOLIA_CONTRACT= Check out the complete script code on Github. -#### 3. Configure allowlists +#### 3. Configure allowlists As a best practice, allowlist the destination chain before sending tokens. @@ -111714,7 +111714,7 @@ Ethereum Sepolia can send tokens to Arbitrum Sepolia ** Next Step: Transfer Tokens ** ``` -#### 4. Fund your wallet with test tokens +#### 4. Fund your wallet with test tokens [DripBnMToken.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/faucet/DripBnMToken.s.sol) @@ -111739,7 +111739,7 @@ CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS= forge script foun > If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the > [Chainlink faucet](https://faucets.chain.link/). -#### 5. Transfer Tokens +#### 5. Transfer Tokens `SendMessage.s.sol` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: @@ -111876,7 +111876,7 @@ Required CCIP fee (in token units): 26110523180283030 Check out the complete script code on Github. -#### 6. Verify transfer on the destination chain +#### 6. Verify transfer on the destination chain After sending the transfer, you can verify it was successful: @@ -111899,7 +111899,7 @@ Best for developers who want a mature, TypeScript-based smart contract developme > Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a > Hardhat dev environment from scratch. -#### 1. Bootstrap a new Hardhat project +#### 1. Bootstrap a new Hardhat project [CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip) @@ -111947,7 +111947,7 @@ npx hardhat keystore set your_keystore_name > > When prompted for your private key, include the `0x` prefix. -#### 2. Deploy your contract +#### 2. Deploy your contract [deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/token-transfers/deploy/deploy.ts) @@ -111999,7 +111999,7 @@ Before moving on, export the address of the deployed contract so that it's avail export ETHEREUM_SEPOLIA_CONTRACT= ``` -#### 3. Configure allowlists +#### 3. Configure allowlists As a best practice, allowlist the destination chain before sending tokens. @@ -112044,7 +112044,7 @@ SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA RECEIVER_ADDRESS=$RECE ======================================== ``` -#### 4. Fund your wallet with test tokens +#### 4. Fund your wallet with test tokens [drip-bnm-token.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/faucet/drip-bnm-token.ts) @@ -112061,7 +112061,7 @@ npx hardhat run hardhat/scripts/faucet/drip-bnm-token.ts If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the [Chainlink faucet](https://faucets.chain.link/). -#### 5. Transfer Tokens +#### 5. Transfer Tokens `send-message.ts` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script: @@ -112201,7 +112201,7 @@ https://ccip.chain.link/#/side-drawer/msg/0xe3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a Check out the complete script code on Github. -#### 6. Verify transfer on the destination chain +#### 6. Verify transfer on the destination chain After sending the transfer, you can verify it was successful: @@ -112372,7 +112372,7 @@ Best for **Solidity-native** workflows that prefer a modular, powerful scripting > Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a > Foundry dev environment from scratch. -#### 1. Bootstrap a new Foundry project +#### 1. Bootstrap a new Foundry project [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) @@ -112433,7 +112433,7 @@ source .env forge build ``` -#### 2. Deploy your contracts +#### 2. Deploy your contracts `Deploy.s.sol` handles all three deployments across both chains in a single run: @@ -112521,7 +112521,7 @@ export ETHEREUM_SEPOLIA_CONTRACT= && export ARBITRUM_SEPOLI View the complete script on GitHub. -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality The sender and receiver start out refusing this lane. `Configure.s.sol` introduces them to each other and sets the finality policy in one run: @@ -112613,7 +112613,7 @@ The script closes by printing ready-to-run send commands for both LINK and nativ View the complete script on GitHub. -#### 4. Send USDC with data +#### 4. Send USDC with data `SendMessage.s.sol` carries out the transfer end to end: @@ -112752,7 +112752,7 @@ The next step redeems the `STK` tokens after the message settles and staking suc View the complete script on GitHub. -#### 5. Redeem STK for USDC +#### 5. Redeem STK for USDC After the CCIP Explorer shows that the message has been successfully processed and the beneficiary has received `STK`, the beneficiary can redeem those tokens on *Arbitrum Sepolia*. @@ -112818,7 +112818,7 @@ Best for developers who want a mature, TypeScript-based smart contract developme > Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a > Hardhat dev environment from scratch. -#### 1. Bootstrap a new Hardhat project +#### 1. Bootstrap a new Hardhat project [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) @@ -112873,7 +112873,7 @@ source .env npx hardhat keystore set your_keystore_name ``` -#### 2. Deploy your contracts +#### 2. Deploy your contracts `deploy.ts` handles all three deployments across both chains in a single run: @@ -112958,7 +112958,7 @@ export ETHEREUM_SEPOLIA_CONTRACT= && export ARBITRUM_SEPOLI View the complete script on GitHub. -#### 3. Configure allowlists and finality +#### 3. Configure allowlists and finality The sender and receiver start out refusing this lane. `configure.ts` introduces them to each other and sets the finality policy in one run: @@ -113048,7 +113048,7 @@ The script closes by printing ready-to-run send commands for both LINK and nativ View the complete script on GitHub. -#### 4. Send USDC with data +#### 4. Send USDC with data `send-message.ts` carries out the transfer end to end: @@ -113181,7 +113181,7 @@ The next step redeems the `STK` tokens after the message settles and staking suc View the complete script on GitHub. -#### 5. Redeem STK for USDC +#### 5. Redeem STK for USDC After the CCIP Explorer shows that the message has been successfully processed and the beneficiary has received `STK`, the beneficiary can redeem those tokens on *Arbitrum Sepolia*. @@ -122623,7 +122623,7 @@ forge script \ --rpc-url $ETHEREUM_SEPOLIA_RPC_URL ``` -#### 4. (Optional) Configure Faster Than Finality for the Token Pool +#### 4. (Optional) Configure Faster Than Finality for the Token Pool 1. CCIP 2.0 supports **faster than finality** as an opt-in feature, letting token issuers control the speed vs. security tradeoff for cross-chain transfers of their token. @@ -124033,7 +124033,7 @@ npx hardhat getSupportedChains --network sepolia npx hardhat getSupportedChains --network arbitrumSepolia ``` -#### 4. (Optional) Configure Faster Than Finality for the Token Pool +#### 4. (Optional) Configure Faster Than Finality for the Token Pool 1. CCIP 2.0 supports **faster than finality** as an opt-in feature, letting token issuers control the speed vs. security tradeoff for cross-chain transfers of their token. diff --git a/src/content/solutions/cross-chain-vault-adapter/llms-full.txt b/src/content/solutions/cross-chain-vault-adapter/llms-full.txt index 85251a30736..c22f426ba6a 100644 --- a/src/content/solutions/cross-chain-vault-adapter/llms-full.txt +++ b/src/content/solutions/cross-chain-vault-adapter/llms-full.txt @@ -823,7 +823,7 @@ Anyone can change this rate by transferring CCIP-BnM directly to the vault. The The [Lock & Mint tutorial](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry) makes a token a cross-chain token with a Lock & Release pool on Ethereum Sepolia and a Burn & Mint pool on Arbitrum Sepolia. On Ethereum Sepolia, only the vault can mint vCCIP-BnM, so the pool locks and releases existing shares. On Arbitrum Sepolia, you deploy a new vCCIP-BnM token that its pool mints and burns. -### 1. Prepare the Lock & Mint repository +### 1. Prepare the Lock & Mint repository The Lock & Mint tutorial uses its own repository, [docs-cct-foundry](https://github.com/smartcontractkit/docs-cct-foundry). Work in the same terminal, so `VAULT` and the other shell variables stay set. @@ -843,7 +843,7 @@ export ETHEREUM_SEPOLIA_TOKEN=$VAULT > You need to follow the rest of the tutorial as it is. You will essentially set up your Vault's token on Ethereum Sepolia as the canonical deployment and enable it to have cross-chain functionality via CCIP under the Lock & Mint mechanism. -### 2. Run the Lock & Mint tutorial for vCCIP-BnM +### 2. Run the Lock & Mint tutorial for vCCIP-BnM Follow the Lock & Mint tutorial from [Deploy Tokens](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#deploy-tokens), with these changes: @@ -1253,7 +1253,7 @@ echo $ADAPTER Open `https://sepolia.etherscan.io/address/` and confirm that the **Contract** tab shows verified source code for `CrossChainERC4626Adapter`. -### 4. Set the return format and fund the adapter +### 4. Set the return format and fund the adapter The adapter pays the CCIP fee for each return leg to Arbitrum Sepolia from its own Ethereum Sepolia ETH balance. With no balance, every request that returns output fails with `InsufficientNativeBalance` and becomes a failed message. diff --git a/src/lib/markdown/__tests__/transformMarkdown.test.ts b/src/lib/markdown/__tests__/transformMarkdown.test.ts index e7faf4d7abe..8c8a0ceb861 100644 --- a/src/lib/markdown/__tests__/transformMarkdown.test.ts +++ b/src/lib/markdown/__tests__/transformMarkdown.test.ts @@ -531,6 +531,93 @@ Body remains visible. expect(withTitleSlot).toContain("Body remains visible.") }) + it.each([2, 3, 4])("preserves an explicit AnchoredAccordion id at heading depth %s", async (depth) => { + const result = await transformMarkdown( + `[Fund the adapter](#fund-the-adapter) + + +Body instructions. +`, + "/fake/page.mdx" + ) + + expect(result).toBe( + `[Fund the adapter](#fund-the-adapter)\n\n${"#".repeat(depth)} 4. Set the return format and fund the adapter \n\nBody instructions.\n` + ) + }) + + it.each(['{"static-id"}', "{`static-id`}"])("preserves a static expression anchorId=%s", async (attribute) => { + const result = await transformMarkdown( + `\nInstructions.\n`, + "/fake/page.mdx" + ) + + expect(result).toBe('## Deploy \n\nInstructions.\n') + }) + + it.each(["", 'anchorId=""', "anchorId={dynamicId}", "anchorId={42}"])( + "keeps accordion content without generating an anchor for %s", + async (attribute) => { + const result = await transformMarkdown( + `\nInstructions.\n`, + "/fake/page.mdx" + ) + + expect(result).toBe("## Deploy\n\nInstructions.\n") + } + ) + + it("does not give a plain Accordion an AnchoredAccordion id", async () => { + const result = await transformMarkdown( + '\nInstructions.\n', + "/fake/page.mdx" + ) + + expect(result).toBe("### Deploy\n\nInstructions.\n") + }) + + it("escapes an anchor id as HTML attribute content", async () => { + const anchorId = 'a&"<>\\_b' + const result = await transformMarkdown( + `\nInstructions.\n`, + "/fake/page.mdx" + ) + + expect(result).toBe('## Deploy \n\nInstructions.\n') + }) + + it("keeps each nested accordion anchor on its own heading", async () => { + const result = await transformMarkdown( + ` +Outer instructions. + + +Inner instructions. + + + +After accordions.`, + "/fake/page.mdx" + ) + + expect(result).toBe( + '## Outer \n\nOuter instructions.\n\n### Inner \n\nInner instructions.\n\nAfter accordions.\n' + ) + }) + + it("continues dropping source HTML anchors while preserving generated accordion anchors", async () => { + const result = await transformMarkdown( + ` + + +Instructions. +`, + "/fake/page.mdx" + ) + + expect(result).toBe('## Deploy \n\nInstructions.\n') + }) + it("projects Address with exact URLs and static truncation", async () => { const result = await transformMarkdown( `Exact:
    diff --git a/src/lib/markdown/componentHandlers.ts b/src/lib/markdown/componentHandlers.ts index 6fc84b65b0b..26f81c1fdd7 100644 --- a/src/lib/markdown/componentHandlers.ts +++ b/src/lib/markdown/componentHandlers.ts @@ -1003,7 +1003,12 @@ export function handleAccordion(node: MdxJsxNode, parent: Parent, index: number) } const label = `${typeof number === "number" ? `${number}. ` : ""}${slotTitle || title}` const headingDepth = node.name === "AnchoredAccordion" && depth !== 3 && depth !== 4 ? 2 : depth === 4 ? 4 : 3 - const replacement: Node[] = [headingNode(headingDepth, label), ...((node as Parent).children || [])] + const heading = headingNode(headingDepth, label) + const anchorId = staticAttribute(node, "anchorId") + if (node.name === "AnchoredAccordion" && typeof anchorId === "string" && anchorId.length > 0) { + heading.data = { markdownAnchorId: anchorId } + } + const replacement: Node[] = [heading, ...((node as Parent).children || [])] return replaceNode(node, parent, index, replacement) } diff --git a/src/lib/markdown/transformMarkdown.ts b/src/lib/markdown/transformMarkdown.ts index 31b8dc71bf4..c92f505a73e 100644 --- a/src/lib/markdown/transformMarkdown.ts +++ b/src/lib/markdown/transformMarkdown.ts @@ -419,6 +419,22 @@ export async function transformMarkdown( // A replaced node is detached. Traverse its replacement instead of mutating its old children. return typeof nextIndex === "number" ? ([SKIP, nextIndex] as [typeof SKIP, number]) : undefined }) + // Materialize generated anchors after projection, which continues to drop source HTML. + visit(tree, "heading", (node) => { + const heading = node as Parent + const anchorId = (heading.data as { markdownAnchorId?: unknown } | undefined)?.markdownAnchorId + if (typeof anchorId !== "string") return + const escapedId = anchorId + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(//g, ">") + .replace(/\\/g, "\") + heading.children.push( + { type: "text", value: " " } as Literal, + { type: "html", value: `` } as Literal + ) + }) }) .use(remarkStringify, { fences: true, diff --git a/src/scripts/check-markdown-fidelity.test.ts b/src/scripts/check-markdown-fidelity.test.ts index c8a5672295f..c675272520d 100644 --- a/src/scripts/check-markdown-fidelity.test.ts +++ b/src/scripts/check-markdown-fidelity.test.ts @@ -850,6 +850,120 @@ Repository description. }) }) +describe("explicit anchored accordion IDs", () => { + const title = "4. Set the return format and fund the adapter" + const source = (attribute = 'anchorId="fund-the-adapter"', component = "AnchoredAccordion") => + `<${component} title="Set the return format and fund the adapter" number={4} depth={3} ${attribute}> + +Fund the adapter. [Return to this step](#fund-the-adapter). + +` + const output = (anchor = '') => + `### ${title}${anchor ? ` ${anchor}` : ""}\n\nFund the adapter. [Return to this step](#fund-the-adapter).` + const compare = (input: string, markdown: string) => + compareSourceToArtifact("fixture", "src/content/fixture.mdx", input, artifact(markdown)) + + test("inventories the exact anchor together with its heading and depth", () => { + const analysis = analyzeSourceMarkdown(source()) + expect(analysis.facts).toContainEqual( + expect.objectContaining({ kind: "anchor", value: "fund-the-adapter", heading: title, depth: 3 }) + ) + const findings = compare(source(), output()) + expect(blockingFindings("focused", findings)).toEqual([]) + expect(findings).toContainEqual(expect.objectContaining({ status: "present", expected: "fund-the-adapter" })) + }) + + test.each(['anchorId={"fund-the-adapter"}', "anchorId={`fund-the-adapter`}"])( + "reads a static expression ID: %s", + (attribute) => { + expect(blockingFindings("focused", compare(source(attribute), output()))).toEqual([]) + } + ) + + test.each(["", ''])("detects a missing or changed ID: %s", (anchor) => { + const findings = compare(source(), output(anchor)) + expect(findings).toContainEqual(expect.objectContaining({ status: "missing", expected: "fund-the-adapter" })) + expect(findings).toContainEqual(expect.objectContaining({ status: "present", expected: title })) + expect(findings).toContainEqual( + expect.objectContaining({ status: "present", expected: "Return to this step -> #fund-the-adapter" }) + ) + }) + + test.each([ + `${output("")}\n\n### Another step `, + `${output("")}\n\n## ${title} `, + `${output("")}\n\n`, + ])("rejects an ID placed on another heading or outside a heading", (markdown) => { + const findings = compare(source(), markdown) + expect(findings).toContainEqual(expect.objectContaining({ status: "missing", expected: "fund-the-adapter" })) + expect(findings).toContainEqual( + expect.objectContaining({ status: "unverifiable", reason: "Served Markdown contains residual runtime syntax" }) + ) + }) + + test("decodes escaped attribute characters without changing the ID", () => { + const id = 'deploy&"quoted"\\_safe' + const input = source(`anchorId={${JSON.stringify(id)}}`) + const markdown = output('') + const findings = compare(input, markdown) + expect(blockingFindings("focused", findings)).toEqual([]) + expect(findings).toContainEqual(expect.objectContaining({ status: "present", expected: id })) + }) + + test.each(["anchorId={runtimeId}", "anchorId={getId()}", "anchorId={42}", "anchorId={null}", "anchorId"])( + "reports an unverifiable ID without losing the heading or body: %s", + (attribute) => { + const findings = compare(source(attribute), output("")) + expect(findings).toContainEqual( + expect.objectContaining({ status: "unverifiable", name: "AnchoredAccordion.anchorId" }) + ) + expect(findings).toContainEqual(expect.objectContaining({ status: "present", expected: title })) + expect(findings).toContainEqual(expect.objectContaining({ status: "present", expected: "Fund the adapter." })) + expect(analyzeSourceMarkdown(source(attribute)).facts.some((fact) => fact.kind === "anchor")).toBe(false) + } + ) + + test.each(["", 'anchorId=""', 'anchorId={""}'])("does not invent an ID for %s", (attribute) => { + expect(blockingFindings("focused", compare(source(attribute), output("")))).toEqual([]) + expect(analyzeSourceMarkdown(source(attribute)).facts.some((fact) => fact.kind === "anchor")).toBe(false) + }) + + test("keeps ordinary Accordion behavior unchanged", () => { + expect( + blockingFindings("focused", compare(source('anchorId="fund-the-adapter"', "Accordion"), output(""))) + ).toEqual([]) + expect( + analyzeSourceMarkdown(source('anchorId="fund-the-adapter"', "Accordion")).facts.some( + (fact) => fact.kind === "anchor" + ) + ).toBe(false) + }) + + test.each([ + '', + '', + 'Visible runtime content', + '', + '', + ])("does not exempt unsafe or unrelated anchor HTML: %s", (anchor) => { + const findings = compare(source(), output(anchor)) + expect(findings).toContainEqual(expect.objectContaining({ status: "missing", expected: "fund-the-adapter" })) + expect(findings).toContainEqual( + expect.objectContaining({ status: "unverifiable", reason: "Served Markdown contains residual runtime syntax" }) + ) + }) + + test.each(['', "", "{runtimeValue}"])( + "reports unexpected runtime syntax inside an ordinary heading: %s", + (runtime) => { + const findings = compare("## Visible heading", `## Visible heading ${runtime}`) + expect(findings).toContainEqual( + expect.objectContaining({ status: "unverifiable", reason: "Served Markdown contains residual runtime syntax" }) + ) + } + ) +}) + test("checks unordered lists inside callouts without requiring artificial separators", () => { const source = `