CCIP 2.0 behavior in Chainlink Local
Chainlink Local v0.3.x (V3) targets CCIP 2.0: it pins @chainlink/contracts-ccip 2.0.0, and both the local-mode router and the fork simulator apply the CCIP 2.0 message rules. This page describes what those rules are in each mode, so you know what a passing local test proves before you move to a fork or a live lane.
Read the CCIP 2.0 concepts for the protocol itself; this page covers only the Chainlink Local behavior.
Local mode (no fork)
CCIPLocalSimulator deploys a CCIPLocalRouter that delivers messages synchronously inside ccipSend. The router mirrors MockCCIPRouter from @chainlink/contracts-ccip 2.0.0 (same ABI, events and behaviour) and additionally applies the CCIP 2.0 OnRamp and OffRamp message rules, so a test that passes locally does not fail on a fork or in production.
Enforced at send time
- Finality: the requested finality of
ExtraArgsV3must use a single mode (the OnRamp check). A Faster-Than-Finality message that is not a token-only transfer is only delivered to a receiver whosegetCCVsAndFinalityConfigallows the requested finality (the OffRamp check). Receivers withoutIAny2EVMMessageReceiverV2accept finalized messages only. - One token per message: more than one token in
tokenAmountsreverts withCanOnlySendOneTokenPerMessage. - No zero amounts: a zero-amount token reverts with
CannotSendZeroTokens. - No V3
tokenReceiveron EVM lanes:GenericExtraArgsV3.tokenReceiverreverts withTokenReceiverNotAllowed. - Receiver CCV config validation: duplicate required/optional CCVs and
optionalThreshold > optionalCCVs.lengthare rejected the same way the OffRamp rejects them. - Gas limit: only V1/V2 extraArgs gas limits above
uint32revertMessageGasLimitTooHigh. Local mode has no lane config, so lower lane caps are not enforced.
Not simulated in local mode
- Token pools: pool finality policies, rate limits and pool-required CCVs are not enforced; tokens go directly to the receiver.
- CCVs and block confirmations: receiver CCV lists are validated, not verified. There are no CCV verifiers, so no quorum check happens.
- The supported-token list:
getSupportedTokensis informational. - Manual execution:
NO_EXECUTION_ADDRESSmessages are executed immediately. - The lane's
maxPerMsgGasLimit: only theuint32ceiling above is enforced.
Use fork mode to test these against the real contracts.
Fees and message IDs
The local router charges only what router.setFee(...) sets (default 0), in the fee token you choose (native or LINK). It returns a mock message ID that is not calculated from the message contents the way a real message ID is, so the CCIPMessageSent event some tools look for is never emitted locally.
Fork mode
CCIPLocalSimulatorFork (Foundry) and the Hardhat 3 JavaScript helper route messages on forked networks for every CCIP era: pre-1.6 (EVM2EVMOnRamp), 1.6 and CCIP 2.0 (CCV-based lanes, which live testnet lanes run today).
CCIP 2.0 routing
On CCIP 2.0 lanes the destination OffRamp selects the CCVs and executes the message. The default V2VerificationMode is OFFRAMP_DERIVED: the CCV selection is derived from the destination OffRamp's getCCVsForMessage(encodedMessage) and the raw encoded message is executed through the permissionless execute entrypoint, so routing does not depend on the local MessageV1 codec. CCV attestations are simulated with a synthetic no-op verifier.
Fast Transfer messages with data are only delivered to receivers that opt in through getCCVsAndFinalityConfig, as in production.
Strict routing and message status
Routing is strict by default: switchChainAndRouteMessage reverts with CCIPLocalSimulatorFork__MessageNotRouted(messageId, reason) when a captured message cannot be routed to any of the given forks, and with CCIPLocalSimulatorFork__MessageExecutionFailed(messageId, reason) (the decoded revert data) when it does not execute successfully. Call setStrictRouting(false) to record failures instead and read them with getMessageStatus(messageId), which returns NOT_FOUND, QUEUED, FAILED or SUCCESS.
With several destinations, either pass every destination fork at once with the uint256[] forkIds overload, or call switchChainAndRouteMessage once per destination fork: a 1.6 or 2.0 message to a chain that is not in the call stays QUEUED and is routed by the call that includes its destination fork.
CCIP 2.0 routers
Some chains have a dedicated CCIP 2.0 router deployed next to the router that getNetworkDetails returns (the Register is generated from the CCIP docs API and only carries one router per chain). Read it with getCCIPV2RouterAddress(chainId) (Ethereum Sepolia and Avalanche Fuji are pre-registered) or set it with setCCIPV2RouterAddress. Send through the CCIP 2.0 router to use its CCIP 2.0 lanes; messages sent through either router are routed by switchChainAndRouteMessage.
Manual execution
NO_EXECUTION_ADDRESS messages follow the CCIP 2.0 manual execution path: with the default V2ExecutionMode.RESPECT_NO_EXEC, they are queued instead of executed. List them with getPendingV2MessageIds and execute one with executePendingV2Message(messageId).
Not simulated in fork mode
Real CCV attestations, RMN and executor fees are not modelled; CCVs on the lane are replaced by a synthetic verifier, and executePendingV2Message executes without charging the executor fee a real manual execution costs. Pinned fork state never sees testnet upgrades, so green tests only prove your code against that snapshot.
Requirements
- Fork tests need
evm_version = "cancun"or later (deployed CCIP 2.0 contracts use Cancun opcodes;parisfails withEvmError: NotActivated) and Foundry >= 1.5.1. - Hardhat 3 (Node.js 22) is required for the JavaScript fork helper; Hardhat 2 is not supported in
0.3.x(the contracts do not compile,HH411). Hardhat 2 users stay on0.2.x(npm install @chainlink/local@0.2.9), which supports pre-1.6 fork routing only.