Skip to content

CCIP Contracts 2.0.0 support in Chainlink Local (V3) - #68

Open
andrejrakic wants to merge 6 commits into
devrel-205/cl-local-v3from
feat/v3-ccip-2-0
Open

andrejrakic wants to merge 6 commits into
devrel-205/cl-local-v3from
feat/v3-ccip-2-0

Conversation

@andrejrakic

@andrejrakic andrejrakic commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Makes Chainlink Local support CCIP 2.0 (CCV-based) lanes, the protocol version every tested testnet and mainnet lane now runs, as the basis for the breaking V3 release 0.3.0-beta.0:

  • Fork mode (Solidity): CCIPLocalSimulatorFork routes CCIP 2.0 lanes and keeps pre-1.6 and 1.6 working.
  • Local mode: now enforces the CCIP 2.0 receiver-finality rules, like production.
  • Hardhat 3 JavaScript fork helpers: rebuilt, with 1.6 and 2.0 support.

V3 targets Hardhat 3 for JavaScript tooling. Hardhat 2 JavaScript users stay on 0.2.x; see the support matrix in the CHANGELOG.

Base: devrel-205/cl-local-v3 (after #66). This branch contains:

  1. @magiodev-cll's OffRamp-derived CCV execution (feat: OffRamp-derived CCV selection for live CCIP 2.0 fork routing magiodev-cll/chainlink-local#3, cherry-picked as 0d79fee, authorship preserved), adopted as the default CCIP 2.0 routing path.
  2. Merge of origin/develop (dee9195): brings in Correct v1.6 sender encoding in CCIPLocalSimulatorFork #65 (v1.6 sender encoding) and the v0.2.10-beta prep. Correct v1.6 sender encoding in CCIPLocalSimulatorFork #65 is ported into CCIPForkAdapterV1dot6 because V3 moved that logic into the adapter.
  3. Fixes and the dependency bump (this commit):
    • Blocker from chore: sync develop into cl-local-v3 (refresh #54) #66 fixed: _findOffRampForOnRamp reverted whenever a router listed a CCIP 2.0 OffRamp. The lookup is now typeAndVersion-dispatched and can't revert (details under Fixes).
    • lib/chainlink-ccipcontracts-ccip-v2.0.0 (c2c125c), with @chainlink/contracts-ccip 2.0.0 in package.json. Includes the bytes4 finality / FinalityCodec migration.
    • Two new fork suites: Sepolia↔Fuji over CCIP 2.0 (messages, token, Fast Transferss), and a 1.6 regression pinned to historical blocks.
    • Dedicated CCIP 2.0 routers are now routed.
    • evm_version = cancun, plus docs and the version bump to 0.3.0-beta.0.
    • A mainnet CCIP 2.0 fork test (Ethereum → Arbitrum One).
    • Local-mode receiver finality: a new CCIPLocalRouter replaces upstream MockCCIPRouter in CCIPLocalSimulator, with the same ABI.
    • Hardhat 3 JS helpers: scripts/CCIPLocalSimulatorFork.js and scripts/data-streams/*.js were CommonJS with require("hardhat") and could not load under V3's ESM + Hardhat 3. They are now ESM and take a NetworkConnection. The CCIP helper gains 1.6 and 2.0 routing.
    • npm files casing fixed for the CCIP helper.
    • Removed dead code:
      • abi/*.json: no longer used, and no longer shipped in the npm package, which is breaking for direct importers.
      • Hardhat 2 scripts/examples/*.ts.
      • Hardhat 2 test/**/*.spec.ts. ClientReportsVerifier.spec.ts is replaced by DataStreamsHelpers.test.js.

Breaking changes

  • @chainlink/contracts-ccip 1.6.2 → 2.0.0. Finality moves from uint16 block confirmations to a bytes4 FinalityCodec config, which changes the wire format:
    • GenericExtraArgsV3: blockConfirmationsrequestedFinalityConfig, and the base size goes from 17 to 19 bytes.
    • MessageV1.finality is now bytes4.
    • Receivers: getCCVsAndMinBlockDepthgetCCVsAndFinalityConfig, returning bytes4.
    • Token pools: setMinBlockConfirmationssetAllowedFinalityConfig(bytes4).
  • The default V2VerificationMode is OFFRAMP_DERIVED (was HYBRID). STRICT, HYBRID and SYNTHETIC_ONLY are still available.
  • Fast Transfers delivery follows production rules. A Fast Transfers message with data is delivered only to receivers that opt in via getCCVsAndFinalityConfig. Otherwise the OffRamp records FAILURE and the message is not delivered.
  • Fork mode needs evm_version = cancun. Deployed 2.0 contracts use Cancun opcodes, and paris fails with NotActivated.
  • Fork tests need Foundry ≥ 1.5.1. forge 1.1.0 panics in revm journaled_state.rs:402 when an OffRamp 2.0 execution records FAILURE.
  • Local mode enforces CCIP 2.0 finality.
    • ccipSend reverts with InvalidRequestedFinality when Fast Transfers data targets a receiver that doesn't opt in.
    • It reverts with RequestedFinalityCanOnlyHaveOneMode for a malformed finality config.
    • Token-only transfers are unaffected.
  • The Hardhat JS helpers use a Hardhat 3 API, which needs @nomicfoundation/hardhat-ethers:
    • getCCIPMessages(connection, receipt) replaces getEvm2EvmMessage(receipt).
    • routeMessage(connection, routerAddresses, sent, options) replaces routeMessage(routerAddress, message).
    • requestLinkFromTheFaucet(connection, …) takes the connection first.
    • new MockReportGenerator(connection, initialPrice) takes the connection first.
    • The Data Streams faucet helpers take the connection first.
    • Hardhat 2 JS is not supported in V3.
  • abi/*.json is no longer shipped in the npm package. Import ABIs from the @chainlink/contracts-ccip / @chainlink/contracts artifacts instead.
  • IRouterFork.OffRamp is removed. getOffRamps() returns CCIPForkAdapterTypes.RouterOffRamp[], which is ABI-identical.
  • Test-helper API changes (not shipped in the npm package, but commonly copied):
    • EncodeExtraArgsOffchain.encodeV3 and encodeV3Basic take a bytes4 finality config.
    • New encodeV3BasicBlockDepth(uint32, uint16).
    • BasicMessageReceiverWithCCVs.setMinBlockDepthsetAllowedFinalityConfig(uint64, bytes4).

Migration guide

See CHANGELOG.md → Unreleased → Migration guide (0.2.x -> 0.3.0). In short:

  1. Bump the deps. Foundry users pin lib/chainlink-ccip to contracts-ccip-v2.0.0.
  2. Set evm_version = "cancun", use Foundry ≥ 1.5.1, and use Node 22 for Hardhat 3.
  3. Migrate V3 extraArgs:
    • _getBasicEncodedExtraArgsV3(gas, FinalityCodec.WAIT_FOR_FINALITY_FLAG) for finality.
    • _getBasicEncodedExtraArgsV3BlockDepth(gas, n) for a Fast Transfers.
    • _getBasicEncodedExtraArgsV3FastConfirmationRule(gas) to wait for the safe tag.
  4. V2 receivers: getCCVsAndMinBlockDepthgetCCVsAndFinalityConfig returning bytes4. A receiver must opt in to receive Fast Transfers data messages.
  5. Pools: setMinBlockConfirmations(n)setAllowedFinalityConfig(FinalityCodec._encodeBlockDepth(n)).
  6. Nothing extra is needed for CCIP 2.0 lanes in the default mode. To restore the old default, call setV2VerificationMode(HYBRID).
  7. To use a dedicated CCIP 2.0 router, send through getCCIPV2RouterAddress(block.chainid), and deploy receivers with the destination chain's CCIP 2.0 router.

What changed and why

Fixes

  • OffRamp lookup can't revert anymore (the chore: sync develop into cl-local-v3 (refresh #54) #66 blocker).
    • Root cause: OffRamps 1.6 and 2.0 share the getSourceChainConfig(uint64) selector but return different structs. A mismatched return payload fails to decode in the calling frame, and try/catch doesn't catch that, so any router listing a 2.0 OffRamp reverted the whole routing call.
    • Same bug class: OffRamp 1.6 and 2.0 also define getStaticConfig() with the pre-1.6 selector but a 5-word struct instead of 7. So _findOffRampForOnRamp also reverted on a 1.6-only lane with two OffRamps, whenever the non-matching one came first.
    • Fix: dispatch on typeAndVersion.
      • OffRamp 2.* is matched against onRamps[].
      • OffRamp 1.6* is matched against onRamp.
      • EVM2EVMOffRamp 1.* is matched via getStaticConfig.
      • If typeAndVersion is unreadable, every shape is probed.
      • An unknown version or shape is skipped.
    • Every decode runs in an external self-call inside try. @magiodev-cll's _findOffRampForLaneV2 is folded into this single lookup.
    • Live routers list OffRamps 1.2.0, 1.5.0, 1.6.0 and 2.0.0 for the same source, which is exactly this case.
  • OFFRAMP_DERIVED marks messages processed only on SUCCESS.
    • OffRamp 2.0's execute doesn't revert when verification or the receiver fails on a first attempt. It records FAILURE, and Zp/ga review2 #3 treated that as delivered.
    • Success is now read from getExecutionState(keccak256(encodedMessage)).
    • Only the OffRamp bound to the emitting OnRamp is executed. Zp/ga review2 #3 tried every OffRamp on the router.
    • RESPECT_NO_EXEC / MANUAL_ONLY queueing now works in this mode too. It needs only the event receipts, not the codec.
  • The derived path needs no lane mocking. CCVs that are owner-configurable verifier resolvers (the production shape) are pointed at the synthetic fork verifier, the same mechanism HYBRID already used. So live lanes, and pool- or receiver-required CCVs, work without setLaneDefaultCCVs. setLaneDefaultCCVs + CCVNoOpVerifier from Zp/ga review2 #3 still work.
  • Unexpected responses no longer revert routing:
    • an odd getCCVsForMessage return shape
    • a resolver owner() response on a CCV with no code (a high-level try doesn't catch the no-code check)
    • a router returning junk from getOffRamps
  • Correct v1.6 sender encoding in CCIPLocalSimulatorFork #65 is ported to V3: Any2EVMRampMessage.sender = abi.encode(address).
    • I reviewed it critically: 32 bytes is correct. It matches Client.Any2EVMMessage.sender ("abi.decode(sender) if coming from an EVM chain") and local mode's abi.encode(msg.sender).
    • Verified non-vacuous: the ported unit test fails with 20 != 32 before the port. The new 1.6 fork test also fails with the pre-Correct v1.6 sender encoding in CCIPLocalSimulatorFork #65 encoding, because the receiver's abi.decode reverts and the message isn't delivered.
  • Duplicates from the chore: sync develop into cl-local-v3 (refresh #54) #66 resolution removed:
    • IRouterFork.OffRampCCIPForkAdapterTypes.RouterOffRamp.
    • The simulator's _decodeEVMAddress and InvalidEVMAddressEncoding are gone. The V2 path takes the source OnRamp from entry.emitter, which the OnRamp itself stamps as onRampAddress.

Dependency bump

  • lib/chainlink-ccip: 347f511c2c125c (contracts-ccip-v2.0.0).
  • @chainlink/contracts-ccip: 2.0.0, pinned exactly like the other Chainlink deps. @chainlink/contracts stays at 1.5.0, which is what contracts-ccip 2.0.0 depends on.
  • These are unchanged between the old pin and v2.0.0: the CCIPMessageSent event, the Receipt struct, OffRamp SourceChainConfig, and the execute / getCCVsForMessage signatures. V2 event detection is unaffected.
  • CCVNoOpVerifier's mirror types already used bytes4 finality. A new unit test pins its selectors to the upstream ICrossChainVerifierV1 / ICrossChainVerifierResolver.

Test changes

  • The live CCIP-BnM pool now requires a Fast Transfers block depth of at least 32. The Fast Transfers token tests read getAllowedFinalityConfig() from the live pool instead of hard-coding depth 1.
  • The HelloWorld Fast Transfers and programmable-token Fast Transfers tests now use an opted-in receiver (BasicMessageReceiverWithCCVs), as OffRamp 2.0 requires. There is a new negative test showing that a receiver which doesn't opt in is not delivered.
  • CCIPForkAdaptersUnit pins its harness to HYBRID, since those tests cover the opt-in local-codec path.

Credit

Thanks to @magiodev-cll for the develop → V3 refresh (#66), and for the OffRamp-derived CCV execution (magiodev-cll#3). The latter is cherry-picked here with authorship preserved and is now the default CCIP 2.0 routing path.

Nalon and others added 6 commits September 21, 2026 17:51
* fix: Use abi.encode for v1.6 message sender in CCIPLocalSimulatorFork

fix: Use abi.encode for v1.6 message sender in CCIPLocalSimulatorFork

`_executePostV1dot6` built `Internal.Any2EVMRampMessage.sender` with
`abi.encodePacked(address)` (20 bytes). Production v1.6 lanes with an EVM
source chain deliver it as `abi.encode(address)` (32-byte word), which is
what `Client.Any2EVMMessage.sender` documents ("abi.decode(sender) if
coming from an EVM chain") and what the OffRamp passes straight through to
the receiver.

The 20-byte form broke receivers two ways during fork testing: an
`abi.decode(message.sender, (address))` reverted, and a raw-bytes
comparison against `abi.encode(trustedRemote)` fell through to the
untrusted-sender branch.

This completes the 20-vs-32-byte encoding corrections made in 0.2.9 for
`receiver` and `destTokenAddress`, and restores parity with local mode,
where `MockCCIPRouter` already uses `abi.encode(msg.sender)`.

Adds a unit regression test that drives `_executePostV1dot6` against a
recording OffRamp mock and asserts the delivered `sender` is 32 bytes and
decodes back to the original address. Verified non-vacuous: it fails with
`20 != 32` without the fix.

Closes #62

* test: Assert v1.6 sender matches an encoded trusted remote

Receivers consume `Client.Any2EVMMessage.sender` in two ways, and they fail
differently when it arrives as 20 packed bytes instead of a 32-byte ABI word:

- `abi.decode(sender, (address))` reverts, so the message is never delivered.
- `keccak256(sender) == keccak256(abi.encode(trustedRemote))` returns false,
  so the receiver runs without error and silently takes its untrusted-sender
  branch

The existing assertions covered only the first. This pins the raw-bytes
comparison as well, so both documented consumption patterns are checked
against the bytes `_executePostV1dot6` actually produces.
Adds V2VerificationMode.OFFRAMP_DERIVED: the required CCV list comes from the
destination OffRamp (getCCVsForMessage) and the raw encoded message is executed
through the permissionless execute entrypoint, so fork routing does not depend
on the local MessageV1 codec. Keeps V2 fork tests working when the on-chain wire
format moves ahead of the pinned chainlink-ccip (today: finality uint16 -> bytes4,
chainlink-ccip #1898).

- CCIPLocalSimulatorFork: new mode + _routeV2MessageOffRampDerived /
  _executeOffRampDerivedV2; destination chain resolved from the event topics with
  no MessageV1 decode; falls back across candidate OffRamps and marks processed
  only on success.
- Fork-only test helpers ported onto V3: getOffRampForLane, setLaneDefaultCCVs,
  IOffRampSourceConfigV2Fork, plus the CCVNoOpVerifier test double.
- IOffRampExecuteV2 gains the bytes-based execute entrypoint.
- New e2e: test/e2e/ccip/CCIPv2Fork.t.sol (Sepolia -> Arb Sepolia message and
  token transfer over the live 2.0 lane with a no-op default CCV).
- Unit 42/42; live e2e 2/2 with public RPCs.
Brings in fdb0cc7 (Correct v1.6 sender encoding, #65) and dd13ac9
(v0.2.10-beta prep).

Conflict resolution:
- src/ccip/CCIPLocalSimulatorFork.sol: keep V3's adapter layout; drop
  develop's inline _executePreV1dot6/_executePostV1dot6 (V3 moved that
  logic into src/ccip/adapters/).
- Port #65 into CCIPForkAdapterV1dot6._toAny2EVMMessage: sender is now
  abi.encode(address) (32-byte word) instead of abi.encodePacked.
- test/unit/ccip/CCIPLocalSimulatorForkRouting.t.sol: port #65's
  regression test onto the adapter mirror types (pinned chainlink-ccip
  no longer ships the 1.6 Internal ramp structs). Verified it fails
  with 20 != 32 before the port.
- package.json / package-lock.json: keep V3's 0.3.0-beta version.
- CHANGELOG.md: keep both the Unreleased and 0.2.10-beta sections.
- Pin chainlink-ccip to contracts-ccip-v2.0.0 and @chainlink/contracts-ccip
  to 2.0.0; migrate finality to bytes4 FinalityCodec (extraArgs V3,
  receivers, pools, test helpers).
- CCIPLocalSimulatorFork: make OFFRAMP_DERIVED (magiodev-cll#3)
  the default V2 mode; mark processed only on SUCCESS, execute only the lane
  OffRamp, honour the NO_EXEC queue, synthetic results for resolver CCVs.
- Fix the OffRamp lookup reverting on mixed-era router lists: dispatch on
  typeAndVersion, isolate every decode, skip unknown shapes.
- Route dedicated CCIP 2.0 routers (getCCIPV2RouterAddress /
  setCCIPV2RouterAddress; Sepolia and Fuji seeded).
- Local mode: CCIPLocalRouter enforces CCIP 2.0 receiver finality for
  Fast Transfers.
- Hardhat 3 JS helpers: CCIP (all eras) and Data Streams rebuilt as ESM
  taking a network connection; fix npm files casing.
- Remove dead code: abi/*.json (no longer shipped), Hardhat 2 examples and
  .spec.ts tests, duplicate IRouterFork.OffRamp and _decodeEVMAddress.
- evm_version = cancun; version 0.3.0-beta.0.
- Tests: unit tests for lookup, V2 routing and local finality; fork suites
  for Sepolia<->Fuji 2.0, mainnet 2.0, pinned-block 1.6; Hardhat 3 JS tests.
- Docs: CHANGELOG (breaking changes, support matrix, migration, known
  limitations), README, AGENTS.md, api_reference.

BREAKING CHANGE: see CHANGELOG.md "Breaking changes", "Removed" and
"Migration guide".
@andrejrakic
andrejrakic marked this pull request as ready for review September 23, 2026 14:34
@andrejrakic
andrejrakic requested a review from a team as a code owner September 23, 2026 14:34

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants