From 83a910366bef653af394430a9b94c501495ee032 Mon Sep 17 00:00:00 2001 From: SyedAsadKazmi Date: Thu, 13 Aug 2026 07:32:42 +0530 Subject: [PATCH] Add custom CCV support to foundry/send-arbitrary-data tutorial --- .../send-arbitrary-data/Messenger.sol | 36 +- .../tutorials/helper/ExtraArgsHelper.s.sol | 465 ++++++++++++++---- .../tutorials/send-arbitrary-data/README.md | 143 ++++++ .../configure/Configure.s.sol | 56 ++- .../interact/TestInOrderExecution.s.sol | 214 -------- .../interact/TestOutOfOrderExecution.s.sol | 205 -------- package-lock.json | 11 +- package.json | 2 +- 8 files changed, 609 insertions(+), 523 deletions(-) delete mode 100644 foundry/scripts/tutorials/send-arbitrary-data/interact/TestInOrderExecution.s.sol delete mode 100644 foundry/scripts/tutorials/send-arbitrary-data/interact/TestOutOfOrderExecution.s.sol diff --git a/contracts/tutorials/send-arbitrary-data/Messenger.sol b/contracts/tutorials/send-arbitrary-data/Messenger.sol index ccfe87b..12263ea 100644 --- a/contracts/tutorials/send-arbitrary-data/Messenger.sol +++ b/contracts/tutorials/send-arbitrary-data/Messenger.sol @@ -24,6 +24,7 @@ contract Messenger is CCIPReceiver, OwnerIsCreator { error SenderNotAllowedForChain(uint64 sourceChainSelector, address sender); error InvalidReceiverAddress(); error InsufficientNativeForFees(uint256 provided, uint256 required); + error InvalidOptionalCcvThreshold(uint8 threshold, uint256 optionalCount); event MessageSent( bytes32 indexed messageId, @@ -38,6 +39,10 @@ contract Messenger is CCIPReceiver, OwnerIsCreator { event AllowedFinalityConfigSet(uint64 indexed sourceChainSelector, bytes4 allowedFinalityConfig); + event CCVsSet( + uint64 indexed sourceChainSelector, address[] requiredCCVs, address[] optionalCCVs, uint8 optionalThreshold + ); + bytes32 private s_lastReceivedMessageId; address private s_lastReceivedSender; string private s_lastReceivedText; @@ -45,6 +50,9 @@ contract Messenger is CCIPReceiver, OwnerIsCreator { mapping(uint64 => bool) public allowlistedDestinationChains; mapping(uint64 => mapping(address => bool)) public allowlistedChainSenders; mapping(uint64 => bytes4) private s_allowedFinalityConfig; + mapping(uint64 => address[]) private s_requiredCCVs; + mapping(uint64 => address[]) private s_optionalCCVs; + mapping(uint64 => uint8) private s_optionalThreshold; /// @notice Constructor initializes the contract with the router address. /// @param _router The address of the router contract. @@ -113,6 +121,28 @@ contract Messenger is CCIPReceiver, OwnerIsCreator { emit AllowedFinalityConfigSet(_sourceChainSelector, _allowedFinalityConfig); } + /// @notice Sets required and optional CCVs for messages from a given source chain. + /// @dev Callable only by the owner. Stored values are returned to the OffRamp via + /// getCCVsAndFinalityConfig when the sender is allowlisted. + /// @param _sourceChainSelector The source chain selector. + /// @param _requiredCCVs CCV addresses that must attest for the message to be accepted. + /// @param _optionalCCVs CCV addresses from which a quorum may be selected. + /// @param _optionalThreshold Minimum number of optional CCVs that must attest. + function setCCVs( + uint64 _sourceChainSelector, + address[] calldata _requiredCCVs, + address[] calldata _optionalCCVs, + uint8 _optionalThreshold + ) external onlyOwner { + if (_optionalThreshold > _optionalCCVs.length) { + revert InvalidOptionalCcvThreshold(_optionalThreshold, _optionalCCVs.length); + } + s_requiredCCVs[_sourceChainSelector] = _requiredCCVs; + s_optionalCCVs[_sourceChainSelector] = _optionalCCVs; + s_optionalThreshold[_sourceChainSelector] = _optionalThreshold; + emit CCVsSet(_sourceChainSelector, _requiredCCVs, _optionalCCVs, _optionalThreshold); + } + /// @notice Returns CCVs and the allowed finality config for the given source chain and sender. /// @dev Called by the OffRamp via _getCCVsFromReceiver. Reverts if the sender is not allowlisted. /// @param sourceChainSelector The source chain selector. @@ -133,9 +163,9 @@ contract Messenger is CCIPReceiver, OwnerIsCreator { revert SenderNotAllowedForChain(sourceChainSelector, decodedSender); } - requiredCCVs = new address[](0); - optionalCCVs = new address[](0); - optionalThreshold = 0; + requiredCCVs = s_requiredCCVs[sourceChainSelector]; + optionalCCVs = s_optionalCCVs[sourceChainSelector]; + optionalThreshold = s_optionalThreshold[sourceChainSelector]; allowedFinalityConfig = s_allowedFinalityConfig[sourceChainSelector]; } diff --git a/foundry/scripts/tutorials/helper/ExtraArgsHelper.s.sol b/foundry/scripts/tutorials/helper/ExtraArgsHelper.s.sol index ed43377..e61c01e 100644 --- a/foundry/scripts/tutorials/helper/ExtraArgsHelper.s.sol +++ b/foundry/scripts/tutorials/helper/ExtraArgsHelper.s.sol @@ -24,10 +24,44 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// lane's) allowed finality config and the receiver's allowed finality config before /// encoding the final extraArgs bytes. /// +/// Additionally, the helper queries the receiver's required/optional CCV configuration +/// (exposed via `getCCVsAndFinalityConfig`) and emits an informational warning when the +/// receiver has custom CCV requirements. This is a WARNING rather than a hard revert because +/// the receiver's required CCVs are destination-chain addresses while `CCV_ADDRESSES` lists +/// source-chain entry addresses — a direct comparison cannot determine whether the supplied +/// CCVs will satisfy the receiver's policy (the source→destination mapping is performed +/// off-chain by the executor). The warning alerts the sender to a potential `RequiredCCVMissing` +/// / `OptionalCCVQuorumNotReached` failure that would leave the message stuck at verification. +/// When the receiver has no custom CCVs the check is a no-op. +/// /// @dev Uses Foundry fork-switching (vm.createFork / vm.selectFork) to query the /// destination chain for the receiver constraint. The source fork is always restored /// after the destination query. abstract contract ExtraArgsHelper is Script { + struct _BuildParams { + address sourceRouter; + uint64 destChainSelector; + uint64 sourceChainSelector; + address sender; + address receiver; + string destRpcUrl; + uint32 gasLimit; + bytes4 requestedFinalityConfig; + address[] ccvs; + bytes[] ccvArgs; + } + + /// @dev Captured receiver CCV + finality configuration from `getCCVsAndFinalityConfig`. + /// `hasConstraint` is false when the receiver is an EOA or does not implement the + /// V2 interface, in which case all CCV/finality fields are zeroed/empty. + struct _ReceiverConfig { + bool hasConstraint; + address[] requiredCcvs; + address[] optionalCcvs; + uint8 optionalThreshold; + bytes4 allowedFinalityConfig; + } + // ───────────────────────────────────────────────────────────────────────── // Public API // ───────────────────────────────────────────────────────────────────────── @@ -65,6 +99,10 @@ abstract contract ExtraArgsHelper is Script { /// @param requestedFinalityConfig Finality config encoded via FinalityCodec /// (bytes4(0) = WAIT_FOR_FINALITY_FLAG = default full finality). /// @return extraArgs ABI-encoded extraArgs bytes ready to pass into ccipSend. + /// + /// CCV env vars (optional): + /// CCV_ADDRESSES — comma-separated CCV addresses (unset/empty → lane defaults). + /// CCV_ARGS — comma-separated hex blobs, one per CCV (unset/empty → empty args). function buildExtraArgs( address sourceRouter, uint64 destChainSelector, @@ -76,41 +114,98 @@ abstract contract ExtraArgsHelper is Script { uint32 gasLimit, bytes4 requestedFinalityConfig ) internal returns (bytes memory) { + _BuildParams memory p = _BuildParams({ + sourceRouter: sourceRouter, + destChainSelector: destChainSelector, + sourceChainSelector: sourceChainSelector, + sender: sender, + receiver: receiver, + destRpcUrl: destRpcUrl, + gasLimit: gasLimit, + requestedFinalityConfig: requestedFinalityConfig, + ccvs: _parseCcvs(), + ccvArgs: _parseCcvArgs() + }); + // ── 1. Default finality early exit ────────────────────────────────── - if (requestedFinalityConfig == FinalityCodec.WAIT_FOR_FINALITY_FLAG) { + if (p.requestedFinalityConfig == FinalityCodec.WAIT_FOR_FINALITY_FLAG) { + // Default finality is always permitted, so the finality check is skipped. CCVs are + // still validated against the receiver's required/optional config — a receiver with + // custom required CCVs would cause the message to be stuck at verification if those + // CCVs are not supplied, regardless of the finality mode. + _queryAndEnsureReceiverCcvs(p); console.log( string.concat( unicode"✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=", - vm.toString(uint256(gasLimit)), + vm.toString(uint256(p.gasLimit)), ", finalityConfig=0x00000000)." ) ); - return ExtraArgsCodec._getBasicEncodedExtraArgsV3(gasLimit, FinalityCodec.WAIT_FOR_FINALITY_FLAG); + return _encodeV3ExtraArgs(p.gasLimit, FinalityCodec.WAIT_FOR_FINALITY_FLAG, p.ccvs, p.ccvArgs); } if (token != address(0)) { - return _buildWithToken( - sourceRouter, - destChainSelector, - token, - sourceChainSelector, - sender, - receiver, - destRpcUrl, - gasLimit, - requestedFinalityConfig + return _buildWithToken(token, p); + } + return _buildMessageOnly(p); + } + + // ───────────────────────────────────────────────────────────────────────── + // CCV validation + // ───────────────────────────────────────────────────────────────────────── + + /// @dev Queries the receiver's CCV + finality config and emits an informational warning when + /// the receiver has custom required/optional CCVs and the sender has not passed + /// `CCV_ADDRESSES`. This is a WARNING, not a hard revert, because: + /// + /// - The receiver's required/optional CCVs are DESTINATION-chain addresses (set via + /// `setCCVs` / `REQUIRED_CCV_ADDRESSES` in Configure). + /// - `CCV_ADDRESSES` lists SOURCE-chain entry addresses (Default CCV Resolver and/or + /// source Custom CCV). + /// - These are different contracts on different chains, so a direct address comparison + /// cannot determine whether the supplied source-chain CCVs will satisfy the receiver's + /// destination-chain requirements. The source→destination CCV mapping is performed + /// OFF-CHAIN by the executor (e.g. Symbiotic maps source Custom CCV → destination + /// Custom CCV automatically). + /// - The OffRamp's `RequiredCCVMissing` / `OptionalCCVQuorumNotReached` checks compare + /// destination-chain addresses (supplied by the executor) against the receiver's + /// destination-chain required list — there is no on-chain way to replicate this from + /// the source chain. + /// + /// Therefore this helper can only warn that a mismatch MAY cause the message to be stuck + /// at verification; it cannot definitively validate the CCV policy pre-flight. When the + /// receiver has no custom CCVs the check is a no-op. + function _queryAndEnsureReceiverCcvs(_BuildParams memory p) internal { + _ReceiverConfig memory rc = _queryReceiverConfig(p.destRpcUrl, p.receiver, p.sourceChainSelector, p.sender); + if (!rc.hasConstraint) return; + _warnReceiverCcvPolicy(rc, p.ccvs); + } + + /// @dev Emits an informational warning when the receiver has custom CCV requirements. See + /// `_queryAndEnsureReceiverCcvs` for why this is a warning rather than a hard revert. + /// No-op when the receiver has no custom required/optional CCVs. + function _warnReceiverCcvPolicy(_ReceiverConfig memory rc, address[] memory providedCcvs) internal pure { + if (rc.requiredCcvs.length == 0 && rc.optionalThreshold == 0) return; + + if (providedCcvs.length == 0) { + console.log( + string.concat( + unicode"⚠️ Receiver has custom CCV requirements but CCV_ADDRESSES is not set. The executor will ", + "only supply lane default CCVs, which may not satisfy the receiver's required/optional CCV policy. ", + "If the message is stuck at verification, set CCV_ADDRESSES to the source-chain Default CCV ", + "Resolver and/or source Custom CCV that correspond to the receiver's required CCVs." + ) + ); + } else { + console.log( + string.concat( + unicode"ℹ️ Receiver has custom CCV requirements. CCV_ADDRESSES was provided — ensure the source-chain ", + unicode"CCVs you passed correspond to the receiver's required/optional CCVs (the source→destination CCV ", + "mapping is performed off-chain by the executor). If the message is stuck at verification, verify ", + "the CCV alignment per the tutorial's Configure vs Send table." + ) ); } - return _buildMessageOnly( - sourceRouter, - destChainSelector, - sourceChainSelector, - sender, - receiver, - destRpcUrl, - gasLimit, - requestedFinalityConfig - ); } // ───────────────────────────────────────────────────────────────────────── @@ -124,61 +219,101 @@ abstract contract ExtraArgsHelper is Script { /// - If pool does NOT implement getAllowedFinalityConfig() → pool has no finality constraint, /// but the lane may still be v2.0 (e.g. CCTP/USDCTokenPool which handles finality via /// Circle attestation). Probe router.getFee() with V3 extraArgs to determine lane version. - function _buildWithToken( - address sourceRouter, - uint64 destChainSelector, - address token, - uint64 sourceChainSelector, - address sender, - address receiver, - string memory destRpcUrl, + function _encodeV3ExtraArgs( uint32 gasLimit, - bytes4 requestedFinalityConfig - ) private returns (bytes memory) { + bytes4 requestedFinalityConfig, + address[] memory ccvs, + bytes[] memory ccvArgs + ) private pure returns (bytes memory) { + if (ccvs.length == 0) { + return ExtraArgsCodec._getBasicEncodedExtraArgsV3(gasLimit, requestedFinalityConfig); + } + + if (ccvArgs.length == 0) { + ccvArgs = new bytes[](ccvs.length); + } + require(ccvs.length == ccvArgs.length, "CCV/ccvArgs length mismatch"); + + return ExtraArgsCodec._encodeGenericExtraArgsV3( + ExtraArgsCodec.GenericExtraArgsV3({ + gasLimit: gasLimit, + requestedFinalityConfig: requestedFinalityConfig, + ccvs: ccvs, + ccvArgs: ccvArgs, + executor: Client.NO_EXECUTION_ADDRESS, + executorArgs: "", + tokenReceiver: "", + tokenArgs: "" + }) + ); + } + + /// @dev When V3 getFee fails with custom CCVs, distinguish invalid CCV config from pre-v2.0 lanes. + function _revertIfCustomCcvsRejected(_BuildParams memory p, Client.EVM2AnyMessage memory probeMsg) private view { + bytes memory basicV3 = + _encodeV3ExtraArgs(p.gasLimit, p.requestedFinalityConfig, new address[](0), new bytes[](0)); + probeMsg.extraArgs = basicV3; + try IRouterClient(p.sourceRouter).getFee(p.destChainSelector, probeMsg) { + revert( + "CCV_ADDRESSES rejected by lane. Each address must be a source-chain CCV entry " + "contract that implements getOutboundImplementation (not an implementation address). " + "Omit CCV_ADDRESSES to use lane defaults, or pass CCV router/proxy addresses only." + ); + } catch { + revert("CCV_ADDRESSES set but lane rejected V3 extraArgs. Verify this is a v2.0+ lane."); + } + } + + function _buildWithToken(address token, _BuildParams memory p) private returns (bytes memory) { (bool poolHasConstraint, bytes4 poolAllowedFinalityConfig) = - _queryPoolAllowedFinalityConfig(sourceRouter, destChainSelector, token); + _queryPoolAllowedFinalityConfig(p.sourceRouter, p.destChainSelector, token); if (!poolHasConstraint) { // Pool does not implement getAllowedFinalityConfig() — no pool-side finality // constraint. Probe V3 extraArgs via router.getFee() to confirm lane version. console.log("Token pool ALLOWED_FINALITY_CONFIG: undefined (pool has no constraint or pre-v2.0 pool)."); - bytes memory v3Args = ExtraArgsCodec._getBasicEncodedExtraArgsV3(gasLimit, requestedFinalityConfig); + bytes memory v3Args = _encodeV3ExtraArgs(p.gasLimit, p.requestedFinalityConfig, p.ccvs, p.ccvArgs); Client.EVM2AnyMessage memory probeMsg = Client.EVM2AnyMessage({ - receiver: abi.encode(receiver), + receiver: abi.encode(p.receiver), data: "", tokenAmounts: new Client.EVMTokenAmount[](0), extraArgs: v3Args, feeToken: address(0) }); - try IRouterClient(sourceRouter).getFee(destChainSelector, probeMsg) { - // Lane is v2.0+, no pool constraint — skip pool validation, check receiver only. + try IRouterClient(p.sourceRouter).getFee(p.destChainSelector, probeMsg) { + // Lane is v2.0+, no pool constraint — check receiver finality + warn on CCVs. console.log("V3 extraArgs accepted by lane (v2.0+ lane, no pool constraint)."); - (bool rcvHasConstraint, bytes4 rcvAllowed) = - _queryReceiverAllowedFinalityConfig(destRpcUrl, receiver, sourceChainSelector, sender); - if (rcvHasConstraint) { - FinalityCodec._ensureRequestedFinalityAllowed(requestedFinalityConfig, rcvAllowed); + _ReceiverConfig memory rc = + _queryReceiverConfig(p.destRpcUrl, p.receiver, p.sourceChainSelector, p.sender); + if (rc.hasConstraint) { + FinalityCodec._ensureRequestedFinalityAllowed(p.requestedFinalityConfig, rc.allowedFinalityConfig); + _warnReceiverCcvPolicy(rc, p.ccvs); } console.log( string.concat( unicode"✅ Using V3 extraArgs with FTF (gasLimit=", - vm.toString(uint256(gasLimit)), + vm.toString(uint256(p.gasLimit)), ", finalityConfig=", - _fmtFinalityConfig(requestedFinalityConfig), + _fmtFinalityConfig(p.requestedFinalityConfig), ")." ) ); return v3Args; - } catch {} + } catch { + if (p.ccvs.length > 0) { + _revertIfCustomCcvsRejected(p, probeMsg); + } + } // V3 rejected — pre-v2.0 lane. console.log( string.concat( unicode"✅ Pre-v2.0 lane. Using V2 extraArgs (gasLimit=", - vm.toString(uint256(gasLimit)), + vm.toString(uint256(p.gasLimit)), ", allowOutOfOrderExecution=true)." ) ); return Client._argsToBytes( - Client.GenericExtraArgsV2({gasLimit: uint256(gasLimit), allowOutOfOrderExecution: true}) + Client.GenericExtraArgsV2({gasLimit: uint256(p.gasLimit), allowOutOfOrderExecution: true}) ); } @@ -187,39 +322,31 @@ abstract contract ExtraArgsHelper is Script { ); // Reverts with FinalityCodec.InvalidRequestedFinality if not permitted. - FinalityCodec._ensureRequestedFinalityAllowed(requestedFinalityConfig, poolAllowedFinalityConfig); + FinalityCodec._ensureRequestedFinalityAllowed(p.requestedFinalityConfig, poolAllowedFinalityConfig); - (bool hasConstraint, bytes4 receiverAllowed) = - _queryReceiverAllowedFinalityConfig(destRpcUrl, receiver, sourceChainSelector, sender); - if (hasConstraint) { - FinalityCodec._ensureRequestedFinalityAllowed(requestedFinalityConfig, receiverAllowed); + _ReceiverConfig memory receiverCfg = + _queryReceiverConfig(p.destRpcUrl, p.receiver, p.sourceChainSelector, p.sender); + if (receiverCfg.hasConstraint) { + FinalityCodec._ensureRequestedFinalityAllowed(p.requestedFinalityConfig, receiverCfg.allowedFinalityConfig); + _warnReceiverCcvPolicy(receiverCfg, p.ccvs); } console.log( string.concat( unicode"✅ Using V3 extraArgs with FTF (gasLimit=", - vm.toString(uint256(gasLimit)), + vm.toString(uint256(p.gasLimit)), ", finalityConfig=", - _fmtFinalityConfig(requestedFinalityConfig), + _fmtFinalityConfig(p.requestedFinalityConfig), ")." ) ); - return ExtraArgsCodec._getBasicEncodedExtraArgsV3(gasLimit, requestedFinalityConfig); + return _encodeV3ExtraArgs(p.gasLimit, p.requestedFinalityConfig, p.ccvs, p.ccvArgs); } /// @dev Message-only path: detect lane version via router.getFee() probe, validate, encode. - function _buildMessageOnly( - address sourceRouter, - uint64 destChainSelector, - uint64 sourceChainSelector, - address sender, - address receiver, - string memory destRpcUrl, - uint32 gasLimit, - bytes4 requestedFinalityConfig - ) private returns (bytes memory) { + function _buildMessageOnly(_BuildParams memory p) private returns (bytes memory) { Client.EVM2AnyMessage memory probeMsg = Client.EVM2AnyMessage({ - receiver: abi.encode(receiver), + receiver: abi.encode(p.receiver), data: abi.encode(""), tokenAmounts: new Client.EVMTokenAmount[](0), extraArgs: "", @@ -227,48 +354,52 @@ abstract contract ExtraArgsHelper is Script { }); // ── Probe V3 ──────────────────────────────────────────────────────── - bytes memory v3Args = ExtraArgsCodec._getBasicEncodedExtraArgsV3(gasLimit, requestedFinalityConfig); + bytes memory v3Args = _encodeV3ExtraArgs(p.gasLimit, p.requestedFinalityConfig, p.ccvs, p.ccvArgs); probeMsg.extraArgs = v3Args; - try IRouterClient(sourceRouter).getFee(destChainSelector, probeMsg) { + try IRouterClient(p.sourceRouter).getFee(p.destChainSelector, probeMsg) { console.log("V3 extraArgs accepted by lane."); - (bool hasConstraint, bytes4 receiverAllowed) = - _queryReceiverAllowedFinalityConfig(destRpcUrl, receiver, sourceChainSelector, sender); - if (hasConstraint) { - FinalityCodec._ensureRequestedFinalityAllowed(requestedFinalityConfig, receiverAllowed); + _ReceiverConfig memory rc = _queryReceiverConfig(p.destRpcUrl, p.receiver, p.sourceChainSelector, p.sender); + if (rc.hasConstraint) { + FinalityCodec._ensureRequestedFinalityAllowed(p.requestedFinalityConfig, rc.allowedFinalityConfig); + _warnReceiverCcvPolicy(rc, p.ccvs); } console.log( string.concat( unicode"✅ Using V3 extraArgs with FTF (gasLimit=", - vm.toString(uint256(gasLimit)), + vm.toString(uint256(p.gasLimit)), ", finalityConfig=", - _fmtFinalityConfig(requestedFinalityConfig), + _fmtFinalityConfig(p.requestedFinalityConfig), ")." ) ); return v3Args; - } catch {} + } catch { + if (p.ccvs.length > 0) { + _revertIfCustomCcvsRejected(p, probeMsg); + } + } // ── V2 fallback ───────────────────────────────────────────────────── bytes memory v2Args = Client._argsToBytes( - Client.GenericExtraArgsV2({gasLimit: uint256(gasLimit), allowOutOfOrderExecution: true}) + Client.GenericExtraArgsV2({gasLimit: uint256(p.gasLimit), allowOutOfOrderExecution: true}) ); probeMsg.extraArgs = v2Args; - try IRouterClient(sourceRouter).getFee(destChainSelector, probeMsg) { + try IRouterClient(p.sourceRouter).getFee(p.destChainSelector, probeMsg) { console.log( string.concat( "Pre-v2.0 lane detected. Using V2 extraArgs (gasLimit=", - vm.toString(uint256(gasLimit)), + vm.toString(uint256(p.gasLimit)), ", allowOutOfOrderExecution=true)." ) ); console.log( string.concat( "Note: requested finality config ", - _fmtFinalityConfig(requestedFinalityConfig), + _fmtFinalityConfig(p.requestedFinalityConfig), " cannot be enforced with V2 extraArgs." ) ); @@ -303,44 +434,84 @@ abstract contract ExtraArgsHelper is Script { } } - /// @dev Switches to a temporary destination-chain fork, queries the receiver's - /// allowed finality config, then restores the source fork. - function _queryReceiverAllowedFinalityConfig( + /// @dev Switches to a temporary destination-chain fork, queries the receiver's CCV and + /// finality configuration via `getCCVsAndFinalityConfig`, then restores the source fork. + /// + /// Returns `_ReceiverConfig({hasConstraint: false, ...})` when the receiver is an EOA + /// or does not implement the V2 interface, so callers can uniformly skip validation. + function _queryReceiverConfig( string memory destRpcUrl, address receiver, uint64 sourceChainSelector, address sender - ) private returns (bool hasConstraint, bytes4 receiverAllowedFinalityConfig) { + ) private returns (_ReceiverConfig memory rc) { uint256 sourceForkId = vm.activeFork(); uint256 destForkId = vm.createFork(destRpcUrl); vm.selectFork(destForkId); if (receiver.code.length == 0) { - console.log("Receiver is an EOA \xe2\x80\x94 no receiver constraint."); + console.log(unicode"Receiver is an EOA — no receiver constraint."); vm.selectFork(sourceForkId); - return (false, bytes4(0)); + return rc; // hasConstraint = false, all fields zeroed/empty } try IAny2EVMMessageReceiverV2(receiver) .getCCVsAndFinalityConfig(sourceChainSelector, abi.encode(sender)) returns ( - address[] memory, address[] memory, uint8, bytes4 allowedFinalityConfig + address[] memory requiredCcvs, + address[] memory optionalCcvs, + uint8 optionalThreshold, + bytes4 allowedFinalityConfig ) { console.log( string.concat("Receiver contract ALLOWED_FINALITY_CONFIG: ", _fmtFinalityConfig(allowedFinalityConfig)) ); - hasConstraint = true; - receiverAllowedFinalityConfig = allowedFinalityConfig; + if (requiredCcvs.length > 0) { + console.log( + string.concat( + "Receiver required CCVs (", + vm.toString(requiredCcvs.length), + "):", + _formatAddressList(requiredCcvs) + ) + ); + } + if (optionalCcvs.length > 0) { + console.log( + string.concat( + "Receiver optional CCVs (threshold ", + vm.toString(uint256(optionalThreshold)), + " of ", + vm.toString(optionalCcvs.length), + "):", + _formatAddressList(optionalCcvs) + ) + ); + } + rc = _ReceiverConfig({ + hasConstraint: true, + requiredCcvs: requiredCcvs, + optionalCcvs: optionalCcvs, + optionalThreshold: optionalThreshold, + allowedFinalityConfig: allowedFinalityConfig + }); } catch { console.log( - "Receiver contract does not implement getCCVsAndFinalityConfig \xe2\x80\x94 no receiver constraint." + unicode"Receiver contract does not implement getCCVsAndFinalityConfig — no receiver constraint." ); - hasConstraint = false; - receiverAllowedFinalityConfig = bytes4(0); } vm.selectFork(sourceForkId); } + /// @dev Renders an address array as a single concatenated string of " addr1 addr2 ...". + function _formatAddressList(address[] memory addrs) private pure returns (string memory) { + string memory out = ""; + for (uint256 i = 0; i < addrs.length; i++) { + out = string.concat(out, " ", vm.toString(addrs[i])); + } + return out; + } + // ───────────────────────────────────────────────────────────────────────── // Formatting helpers // ───────────────────────────────────────────────────────────────────────── @@ -403,6 +574,118 @@ abstract contract ExtraArgsHelper is Script { return FinalityCodec._encodeBlockDepth(uint16(blockDepth)); } + /// @dev Reads CCV_ADDRESSES env var (comma-separated). Unset or empty → empty array (lane defaults). + function _parseCcvs() internal view returns (address[] memory) { + return _parseAddressCsv(vm.envOr("CCV_ADDRESSES", string(""))); + } + + /// @dev Reads CCV_ARGS env var (comma-separated hex). Unset or empty → empty array (empty args per CCV). + function _parseCcvArgs() internal view returns (bytes[] memory) { + return _parseHexBytesCsv(vm.envOr("CCV_ARGS", string(""))); + } + + /// @dev Reads REQUIRED_CCV_ADDRESSES env var for receiver configuration. Unset or empty → empty array. + function _parseRequiredCcvs() internal view returns (address[] memory) { + return _parseAddressCsv(vm.envOr("REQUIRED_CCV_ADDRESSES", string(""))); + } + + /// @dev Reads OPTIONAL_CCV_ADDRESSES env var for receiver configuration. Unset or empty → empty array. + function _parseOptionalCcvs() internal view returns (address[] memory) { + return _parseAddressCsv(vm.envOr("OPTIONAL_CCV_ADDRESSES", string(""))); + } + + /// @dev Reads OPTIONAL_CCV_THRESHOLD env var for receiver configuration. Unset → 0. + function _parseOptionalCcvThreshold() internal view returns (uint8) { + return uint8(vm.envOr("OPTIONAL_CCV_THRESHOLD", uint256(0))); + } + + /// @dev Parses a comma-separated list of addresses. Empty string → empty array. + function _parseAddressCsv(string memory csv) private pure returns (address[] memory) { + bytes memory csvBytes = bytes(csv); + if (csvBytes.length == 0) return new address[](0); + + uint256 segmentCount = _csvSegmentCount(csvBytes); + address[] memory addresses = new address[](segmentCount); + uint256 idx = 0; + uint256 i = 0; + while (i <= csvBytes.length) { + uint256 start = i; + while (i < csvBytes.length && csvBytes[i] != 0x2C) i++; + (uint256 trimStart, uint256 trimEnd) = _trimSegmentBounds(csvBytes, start, i); + if (trimEnd > trimStart) { + addresses[idx++] = vm.parseAddress(_substring(csvBytes, trimStart, trimEnd)); + } + if (i >= csvBytes.length) break; + i++; + } + if (idx != segmentCount) { + address[] memory trimmed = new address[](idx); + for (uint256 j = 0; j < idx; j++) { + trimmed[j] = addresses[j]; + } + return trimmed; + } + return addresses; + } + + /// @dev Parses a comma-separated list of hex byte strings. Empty string → empty array. + function _parseHexBytesCsv(string memory csv) private pure returns (bytes[] memory) { + bytes memory csvBytes = bytes(csv); + if (csvBytes.length == 0) return new bytes[](0); + + uint256 segmentCount = _csvSegmentCount(csvBytes); + bytes[] memory args = new bytes[](segmentCount); + uint256 idx = 0; + uint256 i = 0; + while (i <= csvBytes.length) { + uint256 start = i; + while (i < csvBytes.length && csvBytes[i] != 0x2C) i++; + (uint256 trimStart, uint256 trimEnd) = _trimSegmentBounds(csvBytes, start, i); + if (trimEnd > trimStart) { + string memory segment = _substring(csvBytes, trimStart, trimEnd); + args[idx++] = keccak256(bytes(segment)) == keccak256("0x") ? bytes("") : vm.parseBytes(segment); + } + if (i >= csvBytes.length) break; + i++; + } + if (idx != segmentCount) { + bytes[] memory trimmed = new bytes[](idx); + for (uint256 j = 0; j < idx; j++) { + trimmed[j] = args[j]; + } + return trimmed; + } + return args; + } + + function _csvSegmentCount(bytes memory csvBytes) private pure returns (uint256) { + if (csvBytes.length == 0) return 0; + uint256 count = 1; + for (uint256 i = 0; i < csvBytes.length; i++) { + if (csvBytes[i] == 0x2C) count++; + } + return count; + } + + function _trimSegmentBounds(bytes memory csvBytes, uint256 start, uint256 end) + private + pure + returns (uint256 trimStart, uint256 trimEnd) + { + trimStart = start; + trimEnd = end; + while (trimStart < trimEnd && csvBytes[trimStart] == 0x20) trimStart++; + while (trimEnd > trimStart && csvBytes[trimEnd - 1] == 0x20) trimEnd--; + } + + function _substring(bytes memory data, uint256 start, uint256 end) private pure returns (string memory) { + bytes memory slice = new bytes(end - start); + for (uint256 i = 0; i < end - start; i++) { + slice[i] = data[start + i]; + } + return string(slice); + } + // ───────────────────────────────────────────────────────────────────────── // CSV parsing (shared with Configure scripts) // ───────────────────────────────────────────────────────────────────────── diff --git a/foundry/scripts/tutorials/send-arbitrary-data/README.md b/foundry/scripts/tutorials/send-arbitrary-data/README.md index 7323f88..f5f123b 100644 --- a/foundry/scripts/tutorials/send-arbitrary-data/README.md +++ b/foundry/scripts/tutorials/send-arbitrary-data/README.md @@ -10,6 +10,7 @@ Send text messages between blockchains using Chainlink CCIP with single commands - **Multiple Networks**: Pre-configured for Ethereum Sepolia, Mantle Sepolia, Arbitrum Sepolia, Base Sepolia, and Polygon Amoy - **Unified Send**: One send script supports native and ERC-20 fee payment via `FEE_TOKEN` env var - **Lane-Aware ExtraArgs**: Automatic V3-first / V2-fallback detection for cross-chain message encoding +- **Optional CCV Support**: Configure receiver CCV policy at setup time and pass CCV addresses in message extraArgs at send time, with a pre-flight warning that alerts you when the receiver's CCV policy may not be satisfied (helps prevent messages stuck at verification) ## Prerequisites @@ -156,6 +157,139 @@ CHAIN=MANTLE_SEPOLIA \ forge script foundry/scripts/tutorials/send-arbitrary-data/interact/GetLastReceivedMessageDetails.s.sol:GetLastReceivedMessageDetails ``` +## Optional: Cross-Chain Verifiers (CCVs) + +CCIP v2.0+ lanes support **Cross-Chain Verifiers (CCVs)** — contracts that attest to messages before they are executed on the destination chain. This tutorial exposes CCVs at two independent layers: + +| Layer | When | Env vars | Purpose | +|-------|------|----------|---------| +| **Receiver policy** | Configure (destination chain) | `REQUIRED_CCV_ADDRESSES`, `OPTIONAL_CCV_ADDRESSES`, `OPTIONAL_CCV_THRESHOLD` | Tells the OffRamp which verifiers the receiver *requires* for a given source chain | +| **Sender extraArgs** | Send (source chain) | `CCV_ADDRESSES`, `CCV_ARGS` | Selects which verifiers to use for this specific message (`CCV_ADDRESSES` unset → lane defaults) | + +When the receiver CCV env vars are unset, Configure skips `setCCVs`. CCVs require a **v2.0+ lane** (V3 extraArgs); pre-v2.0 lanes revert if `CCV_ADDRESSES` is set. + +### Pre-flight CCV validation (Send) + +The send script queries the receiver's `getCCVsAndFinalityConfig` before sending and emits an **informational warning** when the receiver has custom required/optional CCVs. This alerts you to a potential `RequiredCCVMissing` / `OptionalCCVQuorumNotReached` failure on-chain (which would leave the message **stuck at verification**) before you pay the CCIP fee. + +The check runs on every send (regardless of whether `CCV_ADDRESSES` is set), because a receiver with custom required CCVs will reject execution even when the executor supplies only lane default CCVs — the defaults may not include the receiver's custom required CCVs. Concretely: + +- **Receiver has no custom CCVs** (Configure ran without `REQUIRED_CCV_ADDRESSES` / `OPTIONAL_CCV_ADDRESSES`) → no warning; `CCV_ADDRESSES` unset uses lane defaults, as before. +- **Receiver requires custom CCVs** and `CCV_ADDRESSES` is **unset** → a `⚠️` warning is logged, explaining that omitting `CCV_ADDRESSES` means the executor only supplies lane defaults, which may not include the required CCV. +- **Receiver requires custom CCVs** and `CCV_ADDRESSES` is **set** → an `ℹ️` reminder is logged to verify the source-chain CCVs you passed correspond to the receiver's required/optional CCVs. + +> **Why a warning and not a hard revert?** The receiver's required/optional CCVs are **destination-chain** addresses (set in Configure), while `CCV_ADDRESSES` lists **source-chain** entry addresses (Default CCV Resolver and/or source Custom CCV). These are different contracts on different chains, so a direct address comparison on the source chain cannot determine whether the supplied CCVs will satisfy the receiver's policy. The source→destination CCV mapping is performed **off-chain by the executor** (e.g. Symbiotic maps source Custom CCV → destination Custom CCV automatically), and the OffRamp's `RequiredCCVMissing` check compares destination-chain addresses supplied by the executor — there is no on-chain way to replicate this from the source chain. Always align the verifiers per the [Configure vs Send](#configure-vs-send-bidirectional-lanes) table. + +In production, the verifiers you configure on the receiver should align with those you pass at send time — but **Configure and Send use different address forms** (see below). + +### CCV roles (terminology) + +| Role | Use in Configure | Use in Send | +|------|------------------|-------------| +| **Default CCV Resolver** | **No** — do not pass in `REQUIRED_CCV_ADDRESSES` | **Yes** — include it whenever you set `CCV_ADDRESSES`, otherwise Default CCV verification is skipped (see below) | +| **Custom CCV** (e.g. Symbiotic) — source chain | No | Yes — pair with the resolver on the **source** chain | +| **Custom CCV** (e.g. Symbiotic) — destination chain | Yes — **only** this chain's custom CCV on the receiver | No — destination custom CCVs are receiver policy only | +| **Default CCV implementation** | No | No — do not pass; resolver resolves to this internally | + +**Why not include the Default CCV Resolver in Configure?** At execute time the OffRamp normalizes the resolver entry point to the **implementation** identity on the destination chain. Listing the resolver in `REQUIRED_CCV_ADDRESSES` causes a policy mismatch (`RequiredCCVMissing`) even when Default CCV attestation succeeded. + +**Why include the Default CCV Resolver in `CCV_ADDRESSES`?** The OnRamp stores the Default CCV Resolver in its `defaultCCVs` config, which is only used as a **fallback when `CCV_ADDRESSES` is unset**. Once you set `CCV_ADDRESSES`, your list **replaces** `defaultCCVs` entirely — the OnRamp does not merge them. So if you pass only a custom CCV, Default CCV verification is skipped. To keep both, always include `` alongside your custom CCV. (The OnRamp's `laneMandatedCCVs` list is separate and always merged in, but on standard lanes it is empty — the Default CCV Resolver lives in `defaultCCVs`, not `laneMandatedCCVs`.) + +**Important:** `CCV_ADDRESSES` (send) must list **source-chain entry contracts** only — the Default CCV Resolver and your source-chain Custom CCV. Do not pass Default CCV implementation addresses or destination-chain Custom CCV addresses — the OnRamp calls `getOutboundImplementation` on each listed address on the source chain. + +The Default CCV Resolver is often deployed at the **same address on both chains** (deterministic deployment), but each chain has its own contract instance. + +### Configure vs Send (bidirectional lanes) + +Configure **each receiver separately** — one `Configure.s.sol` run per direction. Use **destination-chain Custom CCV only** in `REQUIRED_CCV_ADDRESSES` (no resolver). + +| Direction | Configure (`SOURCE_CHAIN` → `DEST_CHAIN`) | `REQUIRED_CCV_ADDRESSES` on receiver | +|-----------|-------------------------------------------|--------------------------------------| +| Eth Sepolia → Base Sepolia | `ETHEREUM_SEPOLIA` → `BASE_SEPOLIA` | `` (Base Symbiotic) | +| Base Sepolia → Eth Sepolia | `BASE_SEPOLIA` → `ETHEREUM_SEPOLIA` | `` (Eth Symbiotic) | + +| Direction | Send (`CCV_ADDRESSES`) | +|-----------|------------------------| +| Eth Sepolia → Base Sepolia | `,` (Eth Symbiotic) | +| Base Sepolia → Eth Sepolia | `,` (Base Symbiotic) | + +Symbiotic attestation maps source Custom CCV → destination Custom CCV automatically. + +### Configure receiver CCVs (optional) + +Add these env vars to **Step 2** when running `Configure.s.sol`. Look up addresses for your lane in CCIP docs or chain tooling. + +**Base Sepolia receiver** (messages from Eth Sepolia): + +```bash +SOURCE_CHAIN=ETHEREUM_SEPOLIA \ +DEST_CHAIN=BASE_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \ +ALLOWED_BLOCK_DEPTH=1 \ +REQUIRED_CCV_ADDRESSES= \ +forge script foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol:Configure \ + --account $KEYSTORE_NAME \ + --broadcast -vv +``` + +**Ethereum Sepolia receiver** (messages from Base Sepolia): + +```bash +SOURCE_CHAIN=BASE_SEPOLIA \ +DEST_CHAIN=ETHEREUM_SEPOLIA \ +ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH \ +ALLOWED_BLOCK_DEPTH=1 \ +REQUIRED_CCV_ADDRESSES= \ +forge script foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol:Configure \ + --account $KEYSTORE_NAME \ + --broadcast -vv +``` + +- **`REQUIRED_CCV_ADDRESSES`** — destination-chain Custom CCV address only (do **not** include the Default CCV Resolver or the source-chain Custom CCV) +- **`OPTIONAL_CCV_ADDRESSES`** — comma-separated optional CCV addresses; a quorum may be selected from this set +- **`OPTIONAL_CCV_THRESHOLD`** — minimum number of optional CCVs that must attest (default `0`; must be `<=` optional CCV count) + +If all three CCV env vars are unset/empty, the script logs a skip message and does not call `setCCVs`. + +### Pass CCVs when sending + +Add `CCV_ADDRESSES` to **Step 3** — **source-chain verifiers only**: include the Default CCV Resolver **and** the source chain's Custom CCV. + +> **Always include the Default CCV Resolver when setting `CCV_ADDRESSES`.** The OnRamp only uses its `defaultCCVs` (which contains the Default CCV Resolver) as a fallback when `CCV_ADDRESSES` is unset. Once you set `CCV_ADDRESSES`, your list replaces `defaultCCVs` entirely — so omitting the resolver skips Default CCV verification. Pass both: `,`. +> +> **Recommended when the receiver has custom CCVs:** if Configure set `REQUIRED_CCV_ADDRESSES` (or an `OPTIONAL_CCV_THRESHOLD` > 0) on the receiver, the send script logs a `⚠️` warning if `CCV_ADDRESSES` is unset, because the executor will only supply lane default CCVs which may not satisfy the receiver's policy (the message could be stuck at verification). Set `CCV_ADDRESSES` to the source-chain Default CCV Resolver and Custom CCV that correspond to the receiver's required CCVs. See [Pre-flight CCV validation](#pre-flight-ccv-validation-send). + +**From source chain A → destination chain B:** + +```bash +SOURCE_CHAIN=ETHEREUM_SEPOLIA \ +DEST_CHAIN=BASE_SEPOLIA \ +GAS_LIMIT=200000 \ +BLOCK_DEPTH=1 \ +CCV_ADDRESSES=, \ +MESSAGE="Hello World From Foundry Script for CCIP 2.0!" \ +forge script foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage.s.sol:SendMessage \ + --account $KEYSTORE_NAME \ + --broadcast -vv +``` + +**From source chain B → destination chain A** — same resolver address, but use the **source-chain Custom CCV** when sending from B: + +```bash +SOURCE_CHAIN=BASE_SEPOLIA \ +DEST_CHAIN=ETHEREUM_SEPOLIA \ +GAS_LIMIT=200000 \ +BLOCK_DEPTH=1 \ +CCV_ADDRESSES=, \ +MESSAGE="Hello World From Foundry Script for CCIP 2.0!" \ +forge script foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage.s.sol:SendMessage \ + --account $KEYSTORE_NAME \ + --broadcast -vv +``` + +- **`CCV_ADDRESSES`** — comma-separated **source-chain** CCV entry addresses for V3 extraArgs. **Always include the Default CCV Resolver** when setting this (your list replaces the OnRamp's `defaultCCVs`, so omitting the resolver skips Default CCV verification). Unset/empty → lane defaults (Default CCV Resolver only), **but** if the receiver has custom required CCVs (set via `REQUIRED_CCV_ADDRESSES` in Configure) the send logs a `⚠️` warning that the message may be stuck at verification. Set this to `,`. +- **`CCV_ARGS`** — comma-separated hex blobs, one per CCV address. **Unset/empty by default** → each CCV gets empty args (`0x`). Only set when a CCV needs non-empty custom args. Order must match `CCV_ADDRESSES` when set. + ## Finality Options Three env vars control finality. Use exactly one — they are mutually exclusive: @@ -194,6 +328,11 @@ forge script foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage. | `MESSAGE` | No | `Hello World...` | Text message to send | | `ALLOWED_FINALITY_CONFIG` | No | — | Comma-separated finality modes for receiver (options: `WAIT_FOR_SAFE`, `BLOCK_DEPTH`). Unset = default finality only | | `ALLOWED_BLOCK_DEPTH` | No | `10` | Minimum block depth; required when `BLOCK_DEPTH` is in `ALLOWED_FINALITY_CONFIG` | +| `REQUIRED_CCV_ADDRESSES` | No | — | Destination-chain Custom CCV only (Configure). Do not include the Default CCV Resolver. Unset = skip `setCCVs` | +| `OPTIONAL_CCV_ADDRESSES` | No | — | Optional Custom CCV addresses for the receiver (Configure). Unset = none | +| `OPTIONAL_CCV_THRESHOLD` | No | `0` | Minimum optional CCVs that must attest (Configure). Must be `<=` optional CCV count | +| `CCV_ADDRESSES` | No | — | Source-chain Default CCV Resolver and Custom CCV for V3 extraArgs (SendMessage). **Always include the Default CCV Resolver** when set (your list replaces the OnRamp's defaultCCVs). Unset = lane defaults, but the send logs a `⚠️` warning if the receiver has custom required CCVs that lane defaults may not satisfy | +| `CCV_ARGS` | No | empty (`0x` per CCV) | Comma-separated hex args, one per CCV in `CCV_ADDRESSES`. Unset = empty args for each CCV | ## Project Structure @@ -227,3 +366,7 @@ export MANTLE_SEPOLIA_CONTRACT=0x... - Contract addresses not set in environment variables - Incorrect chain names (use `ETHEREUM_SEPOLIA`, not `SEPOLIA`) - Destination chain or sender not allowlisted (run Configure.s.sol) +- CCV verifier mismatch between receiver policy and send extraArgs — the send script logs a `⚠️`/`ℹ️` warning pre-flight; if ignored, the message may be stuck at verification on-chain (OffRamp `RequiredCCVMissing` / `OptionalCCVQuorumNotReached`) +- `CCV_ADDRESSES` unset while the receiver has custom required CCVs (set `CCV_ADDRESSES` to include source-chain verifiers that correspond to the receiver's required/optional CCV policy, or re-Configure the receiver without `REQUIRED_CCV_ADDRESSES`) +- Default CCV Resolver omitted from `CCV_ADDRESSES` when sending (your list replaces the OnRamp's `defaultCCVs`, so omitting the resolver skips Default CCV verification — always include `` alongside your custom CCV) +- `CCV_ADDRESSES` set on a pre-v2.0 lane (V3 extraArgs required for CCVs) diff --git a/foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol b/foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol index 61c5047..dc5ca30 100644 --- a/foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol +++ b/foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol @@ -10,6 +10,28 @@ import {ExtraArgsHelper} from "../../helper/ExtraArgsHelper.s.sol"; contract Configure is ExtraArgsHelper { HelperConfig public helperConfig; + /// @dev Configures receiver CCVs from env vars. Returns true if CCVs were actually set + /// (i.e. setCCVs was called), false if all CCV env vars were unset and setCCVs was skipped. + function _setCCVs(Messenger receiver, uint64 sourceChainSelector) internal returns (bool configured) { + address[] memory requiredCCVs = _parseRequiredCcvs(); + address[] memory optionalCCVs = _parseOptionalCcvs(); + uint8 optionalThreshold = _parseOptionalCcvThreshold(); + + if (requiredCCVs.length == 0 && optionalCCVs.length == 0 && optionalThreshold == 0) { + console.log( + unicode"No receiver CCVs configured (REQUIRED_CCV_ADDRESSES / OPTIONAL_CCV_ADDRESSES unset) — skipping setCCVs." + ); + return false; + } + + require(optionalThreshold <= optionalCCVs.length, "OPTIONAL_CCV_THRESHOLD exceeds OPTIONAL_CCV_ADDRESSES count"); + + console.log("Setting receiver CCVs..."); + receiver.setCCVs(sourceChainSelector, requiredCCVs, optionalCCVs, optionalThreshold); + console.log(unicode"✅ Receiver CCVs configured."); + return true; + } + function run() external { string memory sourceChainName = vm.envString("SOURCE_CHAIN"); string memory destChainName = vm.envString("DEST_CHAIN"); @@ -33,7 +55,9 @@ contract Configure is ExtraArgsHelper { require(depth <= uint256(FinalityCodec.MAX_BLOCK_DEPTH), "ALLOWED_BLOCK_DEPTH exceeds maximum (65535)"); } allowedFinalityConfig = bytes4(uint32(depth)); - if (wantSafe) allowedFinalityConfig = allowedFinalityConfig | FinalityCodec.WAIT_FOR_SAFE_FLAG; + if (wantSafe) { + allowedFinalityConfig = allowedFinalityConfig | FinalityCodec.WAIT_FOR_SAFE_FLAG; + } if (allowedFinalityConfig == FinalityCodec.WAIT_FOR_FINALITY_FLAG) { finalityHint = "BLOCK_DEPTH=DEFAULT"; configDesc = "default finality"; @@ -146,6 +170,7 @@ contract Configure is ExtraArgsHelper { ) ); receiver.setAllowedFinalityConfig(sourceConfig.chainSelector, allowedFinalityConfig); + console.log( string.concat( unicode"✅ Allowed finality config set to ", @@ -157,6 +182,8 @@ contract Configure is ExtraArgsHelper { ) ); + bool ccvsConfigured = _setCCVs(receiver, sourceConfig.chainSelector); + vm.stopBroadcast(); console.log(""); @@ -181,9 +208,28 @@ contract Configure is ExtraArgsHelper { helperConfig.getChainName(sourceChainId) ) ); + // Build a CCV hint for the suggested send commands. When CCVs were configured on the + // receiver, the sender should pass CCV_ADDRESSES (source-chain verifiers) or the message + // may be stuck at verification (the send script logs a warning if CCV_ADDRESSES is unset). + // Use a placeholder since Configure only knows the destination-chain custom CCV, not the + // source-chain entry addresses. + string memory ccvHint = ""; + if (ccvsConfigured) { + ccvHint = string.concat( + "CCV_ADDRESSES=<", sourceChainName, "_DEFAULT_CCV_RESOLVER>,<", sourceChainName, "_CUSTOM_CCV> " + ); + } + console.log(""); console.log("** Next Step: Send Messages **"); console.log(""); + if (ccvsConfigured) { + console.log(unicode"⚠️ Receiver CCVs were configured — pass CCV_ADDRESSES when sending"); + console.log( + unicode" (source-chain Default CCV Resolver and/or Custom CCV), or the message may be stuck at verification." + ); + console.log(""); + } console.log("Send a message paying with LINK:"); console.log( string.concat( @@ -193,7 +239,9 @@ contract Configure is ExtraArgsHelper { destChainName, " FEE_TOKEN=LINK GAS_LIMIT=200000 ", finalityHint, - " MESSAGE='Hello World From Foundry Script for CCIP 2.0!'", + " ", + ccvHint, + "MESSAGE='Hello World From Foundry Script for CCIP 2.0!'", " forge script foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage.s.sol:SendMessage --account $KEYSTORE_NAME --broadcast -vv" ) ); @@ -207,7 +255,9 @@ contract Configure is ExtraArgsHelper { destChainName, " GAS_LIMIT=200000 ", finalityHint, - " MESSAGE='Hello World From Foundry Script for CCIP 2.0!'", + " ", + ccvHint, + "MESSAGE='Hello World From Foundry Script for CCIP 2.0!'", " forge script foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage.s.sol:SendMessage --account $KEYSTORE_NAME --broadcast -vv" ) ); diff --git a/foundry/scripts/tutorials/send-arbitrary-data/interact/TestInOrderExecution.s.sol b/foundry/scripts/tutorials/send-arbitrary-data/interact/TestInOrderExecution.s.sol deleted file mode 100644 index 887b284..0000000 --- a/foundry/scripts/tutorials/send-arbitrary-data/interact/TestInOrderExecution.s.sol +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -// ┌─────────────────────────────────────────────────────────────────────────────┐ -// │ TEMP TEST SCRIPT — allowOutOfOrderExecution=false on CCIP (V2 extraArgs) │ -// │ │ -// │ Sends two messages from the same sender using V2 extraArgs with │ -// │ allowOutOfOrderExecution=false (in-order execution enforced): │ -// │ │ -// │ Msg 1 gasLimit=1 → ccipReceive runs OOG on destination │ -// │ → CCIP marks message FAILED (stuck) │ -// │ │ -// │ Msg 2 normal gas → BLOCKED on destination because Msg 1 from the same │ -// │ sender is still stuck (in-order enforcement) │ -// │ → proves allowOutOfOrderExecution=false blocks later │ -// │ messages from the same sender until the earlier one │ -// │ is resolved (retried or skipped by admin) │ -// │ │ -// │ NOTE: allowOutOfOrderExecution=false is deprecated as of early 2026. │ -// │ Only use this on lanes where Out-of-Order Execution is "Optional". │ -// │ On lanes where it is "Required", setting false will revert. │ -// │ │ -// │ Target lane: ETHEREUM_SEPOLIA ↔ ARBITRUM_SEPOLIA │ -// │ │ -// │ Prerequisites: │ -// │ - Messenger contracts deployed on both chains via deploy/ scripts │ -// │ - Chains and sender allowlisted on the destination Messenger contract │ -// │ - ETHEREUM_SEPOLIA_CONTRACT and ARBITRUM_SEPOLIA_CONTRACT env vars set │ -// │ │ -// │ Usage: │ -// │ SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ │ -// │ FEE_TOKEN=LINK GAS_LIMIT=200000 \ │ -// │ forge script interact/TestInOrderExecution.s.sol \ │ -// │ --account $KEYSTORE_NAME --broadcast -vv │ -// └─────────────────────────────────────────────────────────────────────────────┘ - -import {Script, console} from "forge-std/Script.sol"; -import {Messenger} from "../../../../../contracts/tutorials/send-arbitrary-data/Messenger.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {HelperConfig} from "../../../HelperConfig.s.sol"; - -contract TestInOrderExecution is Script { - HelperConfig public helperConfig; - - // gasLimit used for Msg 1 — low enough to run out of gas in ccipReceive on destination. - // gasLimit=1 is well below the minimum required for any meaningful EVM execution, so - // the OffRamp will attempt to call ccipReceive but the call immediately runs OOG and - // CCIP marks the message as FAILED (stuck). - uint32 internal constant FAILING_GAS_LIMIT = 1; - - string internal constant MSG1_TEXT = "Msg 1 - Intentional OOG (gasLimit=1, allowOutOfOrderExecution=false)"; - string internal constant MSG2_TEXT = "Msg 2 - Blocked by stuck Msg 1 (allowOutOfOrderExecution=false)"; - - struct TestParams { - address payable sourceContract; - address payable destContract; - uint64 destChainSelector; - address feeToken; - string feeTokenLabel; - uint32 normalGasLimit; - uint256 sourceChainId; - uint256 destChainId; - } - - function run() external { - string memory sourceChainName = vm.envString("SOURCE_CHAIN"); - string memory destChainName = vm.envString("DEST_CHAIN"); - - vm.createSelectFork(vm.envString(string.concat(sourceChainName, "_RPC_URL"))); - - helperConfig = new HelperConfig(); - vm.makePersistent(address(helperConfig)); - - TestParams memory p; - p.sourceChainId = helperConfig.parseChainName(sourceChainName); - p.destChainId = helperConfig.parseChainName(destChainName); - p.sourceContract = helperConfig.getDeployedContract(p.sourceChainId); - p.destContract = helperConfig.getDeployedContract(p.destChainId); - p.destChainSelector = helperConfig.getNetworkConfig(p.destChainId).chainSelector; - p.normalGasLimit = uint32(vm.envOr("GAS_LIMIT", uint256(200_000))); - - // ── Validate prerequisites ─────────────────────────────────────────────── - require( - p.sourceContract != address(0), - string.concat("Source contract not set. Set ", sourceChainName, "_CONTRACT env var") - ); - require( - p.destContract != address(0), - string.concat("Destination contract not set. Set ", destChainName, "_CONTRACT env var") - ); - require( - p.sourceContract.code.length > 0, - string.concat("No contract deployed at source address: ", vm.toString(p.sourceContract)) - ); - - // ── Resolve fee token ──────────────────────────────────────────────────── - string memory feeTokenEnv = vm.envOr("FEE_TOKEN", string("NATIVE")); - HelperConfig.NetworkConfig memory sourceConfig = helperConfig.getNetworkConfig(p.sourceChainId); - if (keccak256(bytes(feeTokenEnv)) == keccak256(bytes("LINK"))) { - p.feeToken = sourceConfig.link; - p.feeTokenLabel = "LINK"; - } else if (keccak256(bytes(feeTokenEnv)) == keccak256(bytes("NATIVE"))) { - p.feeToken = address(0); - p.feeTokenLabel = string.concat("Native (", helperConfig.getNativeCurrencySymbol(p.sourceChainId), ")"); - } else { - revert(string.concat('Invalid FEE_TOKEN "', feeTokenEnv, '". Use "LINK" or "NATIVE".')); - } - - _executeTest(p); - } - - function _executeTest(TestParams memory p) internal { - // ── Encode V2 extraArgs with allowOutOfOrderExecution=false ────────────── - // - // V2 extraArgs allow the sender to opt into in-order execution per message. - // With allowOutOfOrderExecution=false, the CCIP OffRamp will NOT execute Msg 2 - // until Msg 1 from the same sender has been successfully executed (or skipped by - // an admin after a governance process). - // - // Msg 1: gasLimit=1 → OffRamp attempts ccipReceive, immediately runs OOG → FAILED - // Msg 2: normal gas → sits in UNTOUCHED state on dest until Msg 1 is resolved - bytes memory failArgs = Client._argsToBytes( - Client.GenericExtraArgsV2({gasLimit: uint256(FAILING_GAS_LIMIT), allowOutOfOrderExecution: false}) - ); - bytes memory normalArgs = Client._argsToBytes( - Client.GenericExtraArgsV2({gasLimit: uint256(p.normalGasLimit), allowOutOfOrderExecution: false}) - ); - - // ── Print summary ──────────────────────────────────────────────────────── - console.log(""); - console.log("========================================================"); - console.log(unicode"🧪 Test: allowOutOfOrderExecution=false (in-order)"); - console.log("========================================================"); - console.log("Source chain :", helperConfig.getChainName(p.sourceChainId)); - console.log("Dest chain :", helperConfig.getChainName(p.destChainId)); - console.log("Sender :", p.sourceContract); - console.log("Receiver :", p.destContract); - console.log("Fee token :", p.feeTokenLabel); - console.log("extraArgs version : V2 (allowOutOfOrderExecution=false)"); - console.log("Msg 1 gasLimit :", vm.toString(uint256(FAILING_GAS_LIMIT)), "(will OOG on dest -> FAILED)"); - console.log("Msg 2 gasLimit :", vm.toString(uint256(p.normalGasLimit)), "(blocked until Msg 1 resolved)"); - console.log("========================================================"); - console.log(""); - - // ── Pre-calculate fees (read-only, outside broadcast) ──────────────────── - uint256 fee1 = - Messenger(p.sourceContract).getFee(p.destChainSelector, p.destContract, MSG1_TEXT, p.feeToken, failArgs); - uint256 fee2 = - Messenger(p.sourceContract).getFee(p.destChainSelector, p.destContract, MSG2_TEXT, p.feeToken, normalArgs); - - console.log("CCIP fee Msg 1:", fee1); - console.log("CCIP fee Msg 2:", fee2); - console.log(""); - - // ── Broadcast ──────────────────────────────────────────────────────────── - vm.startBroadcast(); - - bytes32 msgId1; - bytes32 msgId2; - - if (p.feeToken != address(0)) { - // ERC-20 fee path: approve combined total upfront then send both messages. - console.log("[Step 1] Approving fee token spend (fee1 + fee2)..."); - require(IERC20(p.feeToken).approve(p.sourceContract, fee1 + fee2), unicode"❌ Fee token approval failed"); - - console.log("[Step 2] Sending Msg 1 (gasLimit=1, allowOutOfOrderExecution=false)..."); - msgId1 = Messenger(p.sourceContract) - .sendMessage(p.destChainSelector, p.destContract, MSG1_TEXT, p.feeToken, failArgs); - - console.log("[Step 3] Sending Msg 2 (normal gas, allowOutOfOrderExecution=false)..."); - msgId2 = Messenger(p.sourceContract) - .sendMessage(p.destChainSelector, p.destContract, MSG2_TEXT, p.feeToken, normalArgs); - } else { - // Native fee path: pass msg.value per send. - console.log("[Step 1] Sending Msg 1 with native fee (gasLimit=1, allowOutOfOrderExecution=false)..."); - msgId1 = Messenger(p.sourceContract).sendMessage{value: fee1}( - p.destChainSelector, p.destContract, MSG1_TEXT, address(0), failArgs - ); - - console.log("[Step 2] Sending Msg 2 with native fee (normal gas, allowOutOfOrderExecution=false)..."); - msgId2 = Messenger(p.sourceContract).sendMessage{value: fee2}( - p.destChainSelector, p.destContract, MSG2_TEXT, address(0), normalArgs - ); - } - - vm.stopBroadcast(); - - // ── Final report ───────────────────────────────────────────────────────── - console.log(""); - console.log("========================================================"); - console.log(unicode"✅ Both messages sent!"); - console.log("========================================================"); - console.log(""); - console.log("Msg 1 (expect FAILED on dest - OOG):"); - console.log(" messageId :", vm.toString(msgId1)); - console.log(" explorer :", helperConfig.getCCIPExplorerUrl(msgId1)); - console.log(""); - console.log("Msg 2 (expect BLOCKED until Msg 1 is resolved):"); - console.log(" messageId :", vm.toString(msgId2)); - console.log(" explorer :", helperConfig.getCCIPExplorerUrl(msgId2)); - console.log(""); - console.log("Interpretation:"); - console.log(" Msg 1 will appear as FAILED because ccipReceive ran out of gas"); - console.log(" (gasLimit=1 is far too low for any execution)."); - console.log(" Msg 2 will remain UNTOUCHED/blocked on the destination because"); - console.log(" allowOutOfOrderExecution=false enforces in-order execution -"); - console.log(" the OffRamp will not process Msg 2 until Msg 1 from the same"); - console.log(" sender is successfully executed or skipped via admin governance."); - console.log(" Compare with TestOutOfOrderExecution.s.sol where Msg 2 succeeds"); - console.log(" immediately despite Msg 1 being stuck."); - console.log("========================================================"); - } -} diff --git a/foundry/scripts/tutorials/send-arbitrary-data/interact/TestOutOfOrderExecution.s.sol b/foundry/scripts/tutorials/send-arbitrary-data/interact/TestOutOfOrderExecution.s.sol deleted file mode 100644 index e8806e9..0000000 --- a/foundry/scripts/tutorials/send-arbitrary-data/interact/TestOutOfOrderExecution.s.sol +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -// ┌─────────────────────────────────────────────────────────────────────────────┐ -// │ TEMP TEST SCRIPT — allowOutOfOrderExecution on CCIP v2 (V3 extraArgs) │ -// │ │ -// │ Sends two messages from the same sender on a CCIP v2.0+ lane: │ -// │ │ -// │ Msg 1 gasLimit=1 → ccipReceive runs OOG on destination │ -// │ → CCIP marks message FAILED (stuck) │ -// │ │ -// │ Msg 2 normal gas → executes successfully even though Msg 1 is stuck │ -// │ → proves allowOutOfOrderExecution=true (default on │ -// │ V3-extraArgs / CCIP v2.0+ lanes) │ -// │ │ -// │ Target lane: ETHEREUM_SEPOLIA ↔ ARBITRUM_SEPOLIA (CCIP v2.0+ lane) │ -// │ │ -// │ Prerequisites: │ -// │ - Messenger contracts deployed on both chains via deploy/ scripts │ -// │ - Chains and sender allowlisted on the destination Messenger contract │ -// │ - ETHEREUM_SEPOLIA_CONTRACT and ARBITRUM_SEPOLIA_CONTRACT env vars set │ -// │ │ -// │ Usage: │ -// │ SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \ │ -// │ FEE_TOKEN=LINK GAS_LIMIT=200000 BLOCK_DEPTH=32 \ │ -// │ forge script interact/TestOutOfOrderExecution.s.sol \ │ -// │ --account $KEYSTORE_NAME --broadcast -vv │ -// └─────────────────────────────────────────────────────────────────────────────┘ - -import {console} from "forge-std/Script.sol"; -import {Messenger} from "../../../../../contracts/tutorials/send-arbitrary-data/Messenger.sol"; -import {ExtraArgsCodec} from "@chainlink/contracts-ccip/contracts/libraries/ExtraArgsCodec.sol"; -import {ExtraArgsHelper} from "../../helper/ExtraArgsHelper.s.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {HelperConfig} from "../../../HelperConfig.s.sol"; - -contract TestOutOfOrderExecution is ExtraArgsHelper { - HelperConfig public helperConfig; - - // gasLimit used for Msg 1 — low enough to run out of gas in ccipReceive on destination. - // gasLimit=1 is well below the minimum required for any meaningful EVM execution, so - // the OffRamp will attempt to call ccipReceive but the call immediately runs OOG and - // CCIP marks the message as FAILED. - uint32 internal constant FAILING_GAS_LIMIT = 1; - - string internal constant MSG1_TEXT = "Msg 1 - Intentional OOG (gasLimit=1)"; - string internal constant MSG2_TEXT = "Msg 2 - Normal execution (allowOutOfOrderExecution=true)"; - - struct TestParams { - address payable sourceContract; - address payable destContract; - uint64 destChainSelector; - address feeToken; - string feeTokenLabel; - uint32 normalGasLimit; - bytes4 requestedFinalityConfig; - uint256 sourceChainId; - uint256 destChainId; - } - - function run() external { - string memory sourceChainName = vm.envString("SOURCE_CHAIN"); - string memory destChainName = vm.envString("DEST_CHAIN"); - - vm.createSelectFork(vm.envString(string.concat(sourceChainName, "_RPC_URL"))); - - helperConfig = new HelperConfig(); - vm.makePersistent(address(helperConfig)); - - TestParams memory p; - p.sourceChainId = helperConfig.parseChainName(sourceChainName); - p.destChainId = helperConfig.parseChainName(destChainName); - p.sourceContract = helperConfig.getDeployedContract(p.sourceChainId); - p.destContract = helperConfig.getDeployedContract(p.destChainId); - p.destChainSelector = helperConfig.getNetworkConfig(p.destChainId).chainSelector; - p.normalGasLimit = uint32(vm.envOr("GAS_LIMIT", uint256(200_000))); - p.requestedFinalityConfig = _parseFinalityConfig(); - - // ── Validate prerequisites ─────────────────────────────────────────────── - require( - p.sourceContract != address(0), - string.concat("Source contract not set. Set ", sourceChainName, "_CONTRACT env var") - ); - require( - p.destContract != address(0), - string.concat("Destination contract not set. Set ", destChainName, "_CONTRACT env var") - ); - require( - p.sourceContract.code.length > 0, - string.concat("No contract deployed at source address: ", vm.toString(p.sourceContract)) - ); - - // ── Resolve fee token ──────────────────────────────────────────────────── - string memory feeTokenEnv = vm.envOr("FEE_TOKEN", string("NATIVE")); - HelperConfig.NetworkConfig memory sourceConfig = helperConfig.getNetworkConfig(p.sourceChainId); - if (keccak256(bytes(feeTokenEnv)) == keccak256(bytes("LINK"))) { - p.feeToken = sourceConfig.link; - p.feeTokenLabel = "LINK"; - } else if (keccak256(bytes(feeTokenEnv)) == keccak256(bytes("NATIVE"))) { - p.feeToken = address(0); - p.feeTokenLabel = string.concat("Native (", helperConfig.getNativeCurrencySymbol(p.sourceChainId), ")"); - } else { - revert(string.concat('Invalid FEE_TOKEN "', feeTokenEnv, '". Use "LINK" or "NATIVE".')); - } - - _executeTest(p); - } - - function _executeTest(TestParams memory p) internal { - // ── Encode V3 extraArgs for both messages ──────────────────────────────── - // - // CCIP v2.0+ lanes use V3 extraArgs. On these lanes, allowOutOfOrderExecution - // is enabled by default: a FAILED Msg 1 does NOT block Msg 2 from the same sender. - // - // Msg 1: gasLimit=1 → OffRamp attempts ccipReceive, immediately runs OOG → FAILED - // Msg 2: normal gas → OffRamp executes ccipReceive successfully - // - // Both messages use the finality config parsed from BLOCK_DEPTH (default: WAIT_FOR_FINALITY_FLAG). - bytes memory failArgs = ExtraArgsCodec._getBasicEncodedExtraArgsV3(FAILING_GAS_LIMIT, p.requestedFinalityConfig); - bytes memory normalArgs = - ExtraArgsCodec._getBasicEncodedExtraArgsV3(p.normalGasLimit, p.requestedFinalityConfig); - - // ── Print summary ──────────────────────────────────────────────────────── - console.log(""); - console.log("========================================================"); - console.log(unicode"🧪 Test: allowOutOfOrderExecution on CCIP v2 lane"); - console.log("========================================================"); - console.log("Source chain :", helperConfig.getChainName(p.sourceChainId)); - console.log("Dest chain :", helperConfig.getChainName(p.destChainId)); - console.log("Sender :", p.sourceContract); - console.log("Receiver :", p.destContract); - console.log("Fee token :", p.feeTokenLabel); - console.log("Msg 1 gasLimit:", vm.toString(uint256(FAILING_GAS_LIMIT)), "(will OOG on dest -> FAILED)"); - console.log("Msg 2 gasLimit:", vm.toString(uint256(p.normalGasLimit)), "(normal execution)"); - console.log("========================================================"); - console.log(""); - - // ── Pre-calculate fees (read-only, outside broadcast) ──────────────────── - uint256 fee1 = - Messenger(p.sourceContract).getFee(p.destChainSelector, p.destContract, MSG1_TEXT, p.feeToken, failArgs); - uint256 fee2 = - Messenger(p.sourceContract).getFee(p.destChainSelector, p.destContract, MSG2_TEXT, p.feeToken, normalArgs); - - console.log("CCIP fee Msg 1:", fee1); - console.log("CCIP fee Msg 2:", fee2); - console.log(""); - - // ── Broadcast ──────────────────────────────────────────────────────────── - vm.startBroadcast(); - - bytes32 msgId1; - bytes32 msgId2; - - if (p.feeToken != address(0)) { - // ERC-20 fee path: approve combined total upfront then send both messages. - // The Messenger contract uses safeTransferFrom internally, so the allowance - // is consumed by fee1 on the first call and fee2 on the second. - console.log("[Step 1] Approving fee token spend (fee1 + fee2)..."); - require(IERC20(p.feeToken).approve(p.sourceContract, fee1 + fee2), unicode"❌ Fee token approval failed"); - - console.log("[Step 2] Sending Msg 1 (gasLimit=1, expect FAILED on dest)..."); - msgId1 = Messenger(p.sourceContract) - .sendMessage(p.destChainSelector, p.destContract, MSG1_TEXT, p.feeToken, failArgs); - - console.log("[Step 3] Sending Msg 2 (normal gas, expect SUCCESS)..."); - msgId2 = Messenger(p.sourceContract) - .sendMessage(p.destChainSelector, p.destContract, MSG2_TEXT, p.feeToken, normalArgs); - } else { - // Native fee path: pass msg.value per send. - console.log("[Step 1] Sending Msg 1 with native fee (gasLimit=1, expect FAILED on dest)..."); - msgId1 = Messenger(p.sourceContract).sendMessage{value: fee1}( - p.destChainSelector, p.destContract, MSG1_TEXT, address(0), failArgs - ); - - console.log("[Step 2] Sending Msg 2 with native fee (normal gas, expect SUCCESS)..."); - msgId2 = Messenger(p.sourceContract).sendMessage{value: fee2}( - p.destChainSelector, p.destContract, MSG2_TEXT, address(0), normalArgs - ); - } - - vm.stopBroadcast(); - - // ── Final report ───────────────────────────────────────────────────────── - console.log(""); - console.log("========================================================"); - console.log(unicode"✅ Both messages sent!"); - console.log("========================================================"); - console.log(""); - console.log("Msg 1 (expect FAILED on dest):"); - console.log(" messageId :", vm.toString(msgId1)); - console.log(" explorer :", helperConfig.getCCIPExplorerUrl(msgId1)); - console.log(""); - console.log("Msg 2 (expect SUCCESS despite Msg 1 stuck):"); - console.log(" messageId :", vm.toString(msgId2)); - console.log(" explorer :", helperConfig.getCCIPExplorerUrl(msgId2)); - console.log(""); - console.log("Interpretation:"); - console.log(" Msg 1 will appear as FAILED because ccipReceive ran out of gas"); - console.log(" (gasLimit=1 is far too low for any execution)."); - console.log(" Msg 2 will appear as SUCCESS even though it was sent AFTER Msg 1"); - console.log(" from the same sender. This is allowOutOfOrderExecution=true in"); - console.log(" action - the default behavior on CCIP v2.0+ (V3 extraArgs) lanes."); - console.log("========================================================"); - } -} diff --git a/package-lock.json b/package-lock.json index a36cfeb..fa38aaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@chainlink/contracts": "1.5.0", - "@chainlink/contracts-ccip": "2.0.0-beta.0", + "@chainlink/contracts-ccip": "2.0.0", "@openzeppelin/contracts": "5.3.0" }, "devDependencies": { @@ -408,16 +408,15 @@ } }, "node_modules/@chainlink/contracts-ccip": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@chainlink/contracts-ccip/-/contracts-ccip-2.0.0-beta.0.tgz", - "integrity": "sha512-mmEJgqKNdDiRdUDHmxh8L7LJO80LHizvLnbXmoCKaWORJy5VEW5hriTtULZ5BcC/C/0RRSiKu8c2qdf2asr31Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@chainlink/contracts-ccip/-/contracts-ccip-2.0.0.tgz", + "integrity": "sha512-P0KvQtZSYC1LevMSS16jOOSsqZG4g0n/MJdcWGmE0Z5U01NVYd1MnTQJOBPsbu1NWR79DBXPXLvyr9tR5y+tiw==", "license": "BUSL-1.1", "dependencies": { "@chainlink/ace": "1.0.0", "@chainlink/contracts": "1.5.0", "@openzeppelin/contracts-4.8.3": "npm:@openzeppelin/contracts@4.8.3", - "@openzeppelin/contracts-5.3.0": "npm:@openzeppelin/contracts@5.3.0", - "semver": "^7.7.3" + "@openzeppelin/contracts-5.3.0": "npm:@openzeppelin/contracts@5.3.0" }, "engines": { "node": ">=20", diff --git a/package.json b/package.json index abd9899..5154458 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@chainlink/contracts": "1.5.0", - "@chainlink/contracts-ccip": "2.0.0-beta.0", + "@chainlink/contracts-ccip": "2.0.0", "@openzeppelin/contracts": "5.3.0" }, "devDependencies": {