Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 32 additions & 30 deletions chains/evm/.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ BurnToAddressMintTokenPool_lockOrBurn:test_LockOrBurn() (gas: 241476)
BurnWithFromMintTokenPool_lockOrBurn:test_constructor() (gas: 23892)
BurnWithFromMintTokenPool_lockOrBurn:test_lockOrBurn() (gas: 246012)
CCIPClientExampleWithCCVs_applyCCVConfigUpdates:test_applyCCVConfigUpdates() (gas: 166808)
CCIPClientExample_sanity:test_ImmutableExamples() (gas: 2138256)
CCIPClientExample_sanity:test_ImmutableExamples() (gas: 2147132)
CCTPMessageTransmitterProxy_configureAllowedCallers:test_configureAllowedCallers() (gas: 66306)
CCTPMessageTransmitterProxy_getAllowedCallers:test_configureAllowedCallers() (gas: 75959)
CCTPMessageTransmitterProxy_getCCTPTransmitter:test_getCCTPTransmitter() (gas: 10899)
Expand Down Expand Up @@ -213,15 +213,15 @@ MockRouterTest:test_ccipSendWithLinkFeeTokenAndValidMsgValue() (gas: 127926)
MockRouterTest:test_ccipSendWithSufficientNativeFeeTokens() (gas: 44327)
OffRamp_applySourceChainConfigUpdates:test_applySourceChainConfigUpdates_multipleChains() (gas: 464460)
OffRamp_applySourceChainConfigUpdates:test_applySourceChainConfigUpdates_updateExistingChain() (gas: 155371)
OffRamp_constructor:test_constructor() (gas: 4454945)
OffRamp_constructor:test_constructor() (gas: 4502885)
OffRamp_ensureCCVQuorumIsReached:test_ensureCCVQuorumIsReached_AllCCVsFound() (gas: 84824)
OffRamp_ensureCCVQuorumIsReached:test_ensureCCVQuorumIsReached_OptionalIsAlsoRequired() (gas: 58188)
OffRamp_ensureCCVQuorumIsReached:test_ensureCCVQuorumIsReached_Success_OptionalCCVsFound() (gas: 55471)
OffRamp_execute:test_execute() (gas: 111477)
OffRamp_execute:test_execute() (gas: 112177)
OffRamp_execute:test_execute_InsufficientGasToCompleteTx_setsToFailure() (gas: 92073)
OffRamp_execute:test_execute_ReentrancyGuardReentrantCall_Fails() (gas: 1032367)
OffRamp_execute:test_execute_RunsOutOfGasAndSetsStateToFailure() (gas: 109908)
OffRamp_execute:test_execute_WithReceiver() (gas: 442664)
OffRamp_execute:test_execute_ReentrancyGuardReentrantCall_Fails() (gas: 1033400)
OffRamp_execute:test_execute_RunsOutOfGasAndSetsStateToFailure() (gas: 104991)
OffRamp_execute:test_execute_WithReceiver() (gas: 443363)
OffRamp_getAllSourceChainConfigs:test_getAllSourceChainConfigs_ReturnsMultipleChains() (gas: 225835)
OffRamp_getAllSourceChainConfigs:test_getAllSourceChainConfigs_ReturnsSingleChain() (gas: 38266)
OffRamp_getCCVsFromPool:test_getCCVsFromPool_ReturnsDefaultCCVs_WhenPoolDoesNotSupportV2() (gas: 286649)
Expand All @@ -232,21 +232,21 @@ OffRamp_getCCVsFromReceiver:test_getCCVsFromReceiver_contractV2_fallsBackToDefau
OffRamp_getCCVsFromReceiver:test_getCCVsFromReceiver_contractV2_usesReceiverValues() (gas: 360061)
OffRamp_getCCVsFromReceiver:test_getCCVsFromReceiver_noContract_fallsBackToDefaults() (gas: 30666)
OffRamp_getSourceChainConfig:test_getSourceChainConfig_ReturnsCorrectConfig() (gas: 30153)
OffRamp_getStaticConfig:test_getStaticConfig_MatchesConstructorValues() (gas: 4454213)
OnRampTokenPoolReentrancy:test_OnRampTokenPoolReentrancy() (gas: 241647)
OffRamp_getStaticConfig:test_getStaticConfig_MatchesConstructorValues() (gas: 4502148)
OnRampTokenPoolReentrancy:test_OnRampTokenPoolReentrancy() (gas: 242754)
OnRamp_applyDestChainConfigUpdates:test_applyDestChainConfigUpdates_AllowsZeroRouterToPause() (gas: 127813)
OnRamp_applyDestChainConfigUpdates:test_applyDestChainConfigUpdates_SetsConfigAndEmitsEvent() (gas: 204078)
OnRamp_constructor:test_constructor() (gas: 3952202)
OnRamp_forwardFromRouter:test_forwardFromRouter_SequenceNumberPersistsAndIncrements() (gas: 217237)
OnRamp_forwardFromRouter:test_forwardFromRouter_oldExtraArgs() (gas: 119429)
OnRamp_constructor:test_constructor() (gas: 4062090)
OnRamp_forwardFromRouter:test_forwardFromRouter_SequenceNumberPersistsAndIncrements() (gas: 223795)
OnRamp_forwardFromRouter:test_forwardFromRouter_oldExtraArgs() (gas: 122707)
OnRamp_getCCVsForPool:test_getCCVsForPool_PassesThroughAddressZeroSentinel() (gas: 297523)
OnRamp_getCCVsForPool:test_getCCVsForPool_ReturnsDefaultCCVs_WhenPoolDoesNotSupportV2() (gas: 263649)
OnRamp_getCCVsForPool:test_getCCVsForPool_ReturnsDefaultCCVs_WhenPoolDoesNotSupportV2() (gas: 263778)
OnRamp_getCCVsForPool:test_getCCVsForPool_ReturnsDefaultCCVs_WhenPoolReturnsEmptyArray() (gas: 221111)
OnRamp_getCCVsForPool:test_getCCVsForPool_ReturnsPoolCCVs_WhenPoolSupportsV2() (gas: 290366)
OnRamp_getFee:test_getFee_WithCustomExecutorAndCCVs() (gas: 53597)
OnRamp_getFee:test_getFee_WithLaneMandatedCCVs() (gas: 122488)
OnRamp_getFee:test_getFee_WithV3ExtraArgs_CustomCCV_SkipsDefaults() (gas: 53179)
OnRamp_getFee:test_getFee_WithV3ExtraArgs_EmptyCCVs_UsesDefaults() (gas: 50636)
OnRamp_getFee:test_getFee_WithCustomExecutorAndCCVs() (gas: 54289)
OnRamp_getFee:test_getFee_WithLaneMandatedCCVs() (gas: 124287)
OnRamp_getFee:test_getFee_WithV3ExtraArgs_CustomCCV_SkipsDefaults() (gas: 53871)
OnRamp_getFee:test_getFee_WithV3ExtraArgs_EmptyCCVs_UsesDefaults() (gas: 51743)
OnRamp_mergeCCVLists:test_mergeCCVLists_DedupUserAndMandatoryCCVs() (gas: 41789)
OnRamp_mergeCCVLists:test_mergeCCVLists_DedupUserAndPoolCCVs() (gas: 23677)
OnRamp_mergeCCVLists:test_mergeCCVLists_NoChangesWhenPoolCCVAlreadyInRequired() (gas: 23474)
Expand All @@ -260,13 +260,13 @@ OnRamp_parseExtraArgsWithDefaults:test_parseExtraArgsWithDefaults_V3WithEmptyReq
OnRamp_parseExtraArgsWithDefaults:test_parseExtraArgsWithDefaults_V3WithUserProvidedCCVs() (gas: 48579)
OnRamp_setDynamicConfig:test_SetDynamicConfig() (gas: 35513)
OnRamp_setDynamicConfig:test_SetDynamicConfig_MultipleUpdates() (gas: 41788)
OnRamp_withdrawFeeTokens:test_withdrawFeeTokens() (gas: 216614)
OnRamp_withdrawFeeTokens:test_withdrawFeeTokens_MultipleTokens() (gas: 1494944)
PingPong_ccipReceive:test_CcipReceive() (gas: 151977)
OnRamp_withdrawFeeTokens:test_withdrawFeeTokens() (gas: 216621)
OnRamp_withdrawFeeTokens:test_withdrawFeeTokens_MultipleTokens() (gas: 1494962)
PingPong_ccipReceive:test_CcipReceive() (gas: 154193)
PingPong_setOutOfOrderExecution:test_OutOfOrderExecution() (gas: 20879)
PingPong_setPaused:test_Pausing() (gas: 18247)
PingPong_startPingPong:test_StartPingPong_With_OOO() (gas: 158765)
PingPong_startPingPong:test_StartPingPong_With_Sequenced_Ordered() (gas: 153719)
PingPong_startPingPong:test_StartPingPong_With_OOO() (gas: 160981)
PingPong_startPingPong:test_StartPingPong_With_Sequenced_Ordered() (gas: 155935)
Proxy_fallback:test_fallback() (gas: 17349)
Proxy_setTarget:test_setTarget() (gas: 19905)
RMNProxy_constructor:test_Constructor() (gas: 294118)
Expand All @@ -292,17 +292,17 @@ RegistryModuleOwnerCustom_registerAdminViaGetCCIPAdmin:test_registerAdminViaGetC
RegistryModuleOwnerCustom_registerAdminViaOwner:test_registerAdminViaOwner() (gas: 128616)
Router_applyRampUpdates:test_applyRampUpdates_OffRampUpdatesWithRouting() (gas: 10941199)
Router_applyRampUpdates:test_applyRampUpdates_OnRampDisable() (gas: 58667)
Router_ccipSend:test_CCIPSendLinkFeeNoTokenSuccess_gas() (gas: 127239)
Router_ccipSend:test_CCIPSendLinkFeeOneTokenSuccess_gas() (gas: 244268)
Router_ccipSend:test_CCIPSendLinkFeeNoTokenSuccess_gas() (gas: 129455)
Router_ccipSend:test_CCIPSendLinkFeeOneTokenSuccess_gas() (gas: 246489)
Router_ccipSend:test_InvalidMsgValue() (gas: 28520)
Router_ccipSend:test_NativeFeeToken() (gas: 154148)
Router_ccipSend:test_NativeFeeToken() (gas: 156364)
Router_ccipSend:test_NativeFeeTokenInsufficientValue() (gas: 34636)
Router_ccipSend:test_NativeFeeTokenOverpay() (gas: 155632)
Router_ccipSend:test_NativeFeeTokenOverpay() (gas: 157848)
Router_ccipSend:test_NativeFeeTokenZeroValue() (gas: 26738)
Router_ccipSend:test_NonLinkFeeToken() (gas: 228515)
Router_ccipSend:test_WrappedNativeFeeToken() (gas: 157024)
Router_ccipSend:test_ccipSend_nativeFeeNoTokenSuccess_gas() (gas: 133161)
Router_ccipSend:test_ccipSend_nativeFeeOneTokenSuccess_gas() (gas: 250212)
Router_ccipSend:test_NonLinkFeeToken() (gas: 230731)
Router_ccipSend:test_WrappedNativeFeeToken() (gas: 159240)
Router_ccipSend:test_ccipSend_nativeFeeNoTokenSuccess_gas() (gas: 135377)
Router_ccipSend:test_ccipSend_nativeFeeOneTokenSuccess_gas() (gas: 252433)
Router_constructor:test_Constructor() (gas: 13301)
Router_getArmProxy:test_getArmProxy() (gas: 10647)
Router_getFee:test_GetFeeSupportedChain() (gas: 20949)
Expand Down Expand Up @@ -446,4 +446,6 @@ USDCTokenPool_lockOrBurn:test_LockOrBurn_LegacySourcePoolDataFormat() (gas: 1445
USDCTokenPool_lockOrBurn:test_LockOrBurn_MintRecipientOverride() (gas: 160102)
USDCTokenPool_releaseOrMint:test_ReleaseOrMintRealTx() (gas: 265877)
USDCTokenPool_supportsInterface:test_SupportsInterface() (gas: 10210)
e2e:test_e2e() (gas: 462298)
VersionedVerifierResolver_applyInboundImplementationUpdates:test_applyInboundImplementationUpdates() (gas: 54391)
VersionedVerifierResolver_applyOutboundImplementationUpdates:test_applyOutboundImplementationUpdates() (gas: 47481)
e2e:test_e2e() (gas: 495911)
152 changes: 152 additions & 0 deletions chains/evm/contracts/ccvs/VersionedVerifierResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

import {ICrossChainVerifierResolver} from "../interfaces/ICrossChainVerifierResolver.sol";
import {Ownable2StepMsgSender} from "@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol";

import {IERC165} from "@openzeppelin/[email protected]/utils/introspection/IERC165.sol";

/// @notice Resolves and returns the appropriate verifier contract for the given outbound / inbound traffic.
contract VersionedVerifierResolver is ICrossChainVerifierResolver, Ownable2StepMsgSender {
error InvalidCCVDataLength();
error InvalidDestChainSelector(uint64 destChainSelector);
error InvalidVersion(bytes4 version);
error InboundImplementationAlreadyExists(bytes4 version);
error InboundImplementationNotFound(bytes4 version);
error OutboundImplementationAlreadyExists(uint64 destChainSelector);
error OutboundImplementationNotFound(uint64 destChainSelector);
error ZeroAddressNotAllowed();

event InboundImplementationAdded(bytes4 version, address verifier);
event InboundImplementationRemoved(bytes4 version);
event OutboundImplementationAdded(uint64 destChainSelector, address verifier);
event OutboundImplementationRemoved(uint64 destChainSelector);

struct InboundImplementationArgs {
bytes4 version; // ────╮ Verifier version
address verifier; // ──╯ Address of the verifier contract
}

struct OutboundImplementationArgs {
uint64 destChainSelector; // ──╮ Destination chain selector
address verifier; // ──────────╯ Address of the verifier contract
}

/// @notice maps verifier versions to their implementation addresses, applied to inbound traffic
mapping(bytes4 => address) private s_inboundImplementations;
/// @notice maps destination chain selectors to their implementation addresses, applied to outbound traffic
mapping(uint64 => address) private s_outboundImplementations;

constructor() Ownable2StepMsgSender() {}

/// @inheritdoc ICrossChainVerifierResolver
function getInboundImplementation(
bytes calldata ccvData
) external view returns (address) {
if (ccvData.length < 4) {
revert InvalidCCVDataLength();
}
return _getInboundImplementationForVersion(bytes4(ccvData[:4]));
}

/// @notice Returns the verifier contract for a given version.
/// @param version The version of the verifier contract.
/// @return verifierAddress The address of the verifier contract.
function getInboundImplementationForVersion(
bytes4 version
) external view returns (address) {
return _getInboundImplementationForVersion(version);
}

/// @dev Internal function that enables reuse between functions.
/// Validates that the inbound implementation exists before returning it.
/// @param version The version of the verifier contract.
/// @return verifierAddress The address of the verifier contract.
function _getInboundImplementationForVersion(
bytes4 version
) internal view returns (address) {
if (s_inboundImplementations[version] == address(0)) {
revert InboundImplementationNotFound(version);
}
return s_inboundImplementations[version];
}

/// @inheritdoc ICrossChainVerifierResolver
function getOutboundImplementation(
uint64 destChainSelector
) external view returns (address) {
if (s_outboundImplementations[destChainSelector] == address(0)) {
revert OutboundImplementationNotFound(destChainSelector);
}
return s_outboundImplementations[destChainSelector];
}

/// @notice Updates inbound implementations.
/// @param versionsToRemove Versions that must no longer be supported by the resolver.
/// @param implementationsToAdd New verifier contracts and their versions.
function applyInboundImplementationUpdates(
bytes4[] calldata versionsToRemove,
InboundImplementationArgs[] calldata implementationsToAdd
) external onlyOwner {
for (uint256 i = 0; i < versionsToRemove.length; i++) {
bytes4 version = versionsToRemove[i];
if (s_inboundImplementations[version] == address(0)) {
revert InboundImplementationNotFound(version);
}
delete s_inboundImplementations[version];
emit InboundImplementationRemoved(version);
}
for (uint256 i = 0; i < implementationsToAdd.length; i++) {
InboundImplementationArgs memory implementation = implementationsToAdd[i];
if (implementation.verifier == address(0)) {
revert ZeroAddressNotAllowed();
}
if (implementation.version == bytes4(0)) {
revert InvalidVersion(implementation.version);
}
if (s_inboundImplementations[implementation.version] != address(0)) {
revert InboundImplementationAlreadyExists(implementation.version);
}
s_inboundImplementations[implementation.version] = implementation.verifier;
emit InboundImplementationAdded(implementation.version, implementation.verifier);
}
}

/// @notice Updates outbound implementations.
/// @param chainsToRemove Destinations that must no longer be supported by the resolver.
/// @param implementationsToAdd New verifier contracts and their destination chain selectors.
function applyOutboundImplementationUpdates(
uint64[] calldata chainsToRemove,
OutboundImplementationArgs[] calldata implementationsToAdd
) external onlyOwner {
for (uint256 i = 0; i < chainsToRemove.length; i++) {
uint64 destChainSelector = chainsToRemove[i];
if (s_outboundImplementations[destChainSelector] == address(0)) {
revert OutboundImplementationNotFound(destChainSelector);
}
delete s_outboundImplementations[destChainSelector];
emit OutboundImplementationRemoved(destChainSelector);
}
for (uint256 i = 0; i < implementationsToAdd.length; i++) {
OutboundImplementationArgs memory implementation = implementationsToAdd[i];
if (implementation.verifier == address(0)) {
revert ZeroAddressNotAllowed();
}
if (implementation.destChainSelector == 0) {
revert InvalidDestChainSelector(implementation.destChainSelector);
}
if (s_outboundImplementations[implementation.destChainSelector] != address(0)) {
revert OutboundImplementationAlreadyExists(implementation.destChainSelector);
}
s_outboundImplementations[implementation.destChainSelector] = implementation.verifier;
emit OutboundImplementationAdded(implementation.destChainSelector, implementation.verifier);
}
}

/// @inheritdoc IERC165
function supportsInterface(
bytes4 interfaceId
) external pure returns (bool) {
return interfaceId == type(ICrossChainVerifierResolver).interfaceId || interfaceId == type(IERC165).interfaceId;
}
}
28 changes: 28 additions & 0 deletions chains/evm/contracts/interfaces/ICrossChainVerifierResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

import {IERC165} from "@openzeppelin/[email protected]/utils/introspection/IERC165.sol";

/// @notice Resolves and returns the appropriate verifier contract for the given outbound / inbound traffic.
interface ICrossChainVerifierResolver is IERC165 {
/// @notice Returns the appropriate verifier contract based on the given ccvData.
/// @dev The OffRamp is responsible for calling this function using the ccvData it receives from the executor.
/// If the verifier specified by the executor is actually a resolver, the OffRamp will call this function to get the actual verifier contract.
/// Verifiers can build resolvers that process the ccvData in accordance with how their verifier forms ccvData. For example, their verifier may
/// prefix the ccvData with a version identifier, which the resolver can parse to determine the correct verifier contract.
/// @param ccvData The ccvData formed by the verifier.
/// @return verifierAddress The address of the verifier contract.
function getInboundImplementation(
bytes calldata ccvData
) external view returns (address);

/// @notice Returns the appropriate verifier contract based on the given destChainSelector.
/// @dev The OnRamp is responsible for calling this function using the destChainSelector specified by the sender.
/// If the verifier specified by the sender is actually a resolver, the OnRamp will call this function to get the actual verifier contract.
/// For example, resolvers can maintain a simple mapping of destChainSelector to verifier contract address.
/// @param destChainSelector The destChainSelector for a message.
/// @return verifierAddress The address of the verifier contract.
function getOutboundImplementation(
uint64 destChainSelector
) external view returns (address);
}
8 changes: 7 additions & 1 deletion chains/evm/contracts/offRamp/OffRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity ^0.8.24;

import {IAny2EVMMessageReceiver} from "../interfaces/IAny2EVMMessageReceiver.sol";
import {IAny2EVMMessageReceiverV2} from "../interfaces/IAny2EVMMessageReceiverV2.sol";

import {ICrossChainVerifierResolver} from "../interfaces/ICrossChainVerifierResolver.sol";
import {ICrossChainVerifierV1} from "../interfaces/ICrossChainVerifierV1.sol";
import {IPoolV1} from "../interfaces/IPool.sol";
import {IPoolV2} from "../interfaces/IPoolV2.sol";
Expand Down Expand Up @@ -282,7 +284,11 @@ contract OffRamp is ITypeAndVersion, Ownable2StepMsgSender {
_ensureCCVQuorumIsReached(message.sourceChainSelector, receiver, message.tokenTransfer, message.finality, ccvs);

for (uint256 i = 0; i < ccvsToQuery.length; ++i) {
ICrossChainVerifierV1(ccvsToQuery[i]).verifyMessage({
address ccvAddress = ccvsToQuery[i];
Copy link
Member

@0xsuryansh 0xsuryansh Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolver requires the first 4 bytes of ccvData to encode a version, but the blobs we emit are whatever the underlying verifier returns (e.g. the
current committee verifier returns ""). When we attempt to route verification through the resolver will revert with InvalidCCVDataLength right ?

I think if we are going with this "versioning" approach then CommitVerifier should prepend the version in verifierReturnData

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, meant to add this in.

Copy link
Collaborator

@RensR RensR Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The source CCVData is not the same as what is sent on dest. Offchain is responsible for getting the dest data and consuming source. There is no requirement that the version comes from source

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the DD I posed a couple options we can take for the CommitteeVerifier specifically:

  • CommitteeVerifier returns version as part of forwardToVerifier return data. We will use bytes4 to represent the version, as we’ve established precedent here with USDC. This information is then included in CCIPMessageSent.receiptBlobs and subsequently parsed by each verifier node. The aggregator would then need to validate that each verifier node pulled the same version before it can prepend the version to the signatures and write to storage.
  • The aggregator service stores the verifier version assigned to each destination chain and prepends it to the ccvData for a particular VerifierResult.

I prefer the first because option 2 creates duplicated config. I've got offchain sign off on it as well.

if (ccvAddress._supportsInterfaceReverting(type(ICrossChainVerifierResolver).interfaceId)) {
ccvAddress = ICrossChainVerifierResolver(ccvAddress).getInboundImplementation(ccvData[ccvDataIndex[i]]);
}
ICrossChainVerifierV1(ccvAddress).verifyMessage({
message: message,
messageId: messageId,
ccvData: ccvData[ccvDataIndex[i]]
Expand Down
Loading
Loading