// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "../vendor/openzeppelin/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "../vendor/openzeppelin/ReentrancyGuardUpgradeable.sol"; import "../ccip/IRouterClient.sol"; import {CheckpointStorage} from "./storage/CheckpointStorage.sol"; import {CheckpointLeaf} from "./libraries/CheckpointLeaf.sol"; import {CheckpointFlags} from "./libraries/CheckpointFlags.sol"; import {ICheckpointExtension} from "./interfaces/ICheckpointExtension.sol"; import {IChain138MainnetCheckpoint} from "./interfaces/IChain138MainnetCheckpoint.sol"; import {CheckpointEIP712} from "./libraries/CheckpointEIP712.sol"; import {CheckpointHubConfig} from "./libraries/CheckpointHubConfig.sol"; import {CheckpointErrors} from "./libraries/CheckpointErrors.sol"; import {CheckpointPaymentsLib} from "./libraries/CheckpointPaymentsLib.sol"; import {ExtensionIds} from "./libraries/ExtensionIds.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; /** * @title Chain138MainnetCheckpoint * @notice Upgradeable hub: state + payment batches from Chain 138 (default 10 txs). * @dev UUPS + EIP-7201 storage + extension registry. cW mint remains on CWMultiTokenBridge. */ contract Chain138MainnetCheckpoint is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable, IChain138MainnetCheckpoint { using CheckpointStorage for CheckpointStorage.CheckpointStorageStruct; uint256 public constant IMPLEMENTATION_VERSION = 4; uint64 public constant CHAIN_138 = 138; bytes32 public constant SUBMITTER_ROLE = keccak256("SUBMITTER_ROLE"); bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); bytes32 public constant EXTENSION_ADMIN_ROLE = keccak256("EXTENSION_ADMIN_ROLE"); bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant CHECKPOINT_ATTEST_TYPEHASH = keccak256("BatchAttestation(uint64 chainId,uint64 batchId,uint256 checkpointBlock,bytes32 blockHash,bytes32 stateRoot,bytes32 paymentsRoot,uint64 previousBatchId)"); uint32 public constant HOOK_BEFORE_SUBMIT = 1 << 0; uint32 public constant HOOK_AFTER_SUBMIT = 1 << 1; uint32 public constant HOOK_ON_CCIP = 1 << 2; uint32 public constant HOOK_VERIFY_LEAF = 1 << 3; event HubConfigApplied(CheckpointHubConfig.HubConfig config); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize( address admin, address ccipRouter, uint64 sourceChainSelector, address batchEmitterOnSource ) external initializer { __AccessControl_init(); __ReentrancyGuard_init(); __UUPSUpgradeable_init(); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(UPGRADER_ROLE, admin); _grantRole(SUBMITTER_ROLE, admin); _grantRole(EXTENSION_ADMIN_ROLE, admin); _grantRole(PAUSER_ROLE, admin); _grantRole(EMERGENCY_ROLE, admin); CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); $.chainId = CHAIN_138; $.batchSize = 10; $.maxBatchWaitSeconds = 300; $.requireValidatorSigs = true; $.allowCalldataOnlySubmit = true; $.allowCCIPIngress = true; $.enforcePreviousBatchId = true; $.ccipRouter = ccipRouter; $.expectedSourceChainSelector = sourceChainSelector; $.batchEmitterOnSource = batchEmitterOnSource; } function initializeV2(address legacyMirror, address legacyTether, address attestationSigner) external reinitializer(2) { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); $.legacyMirrorV1 = legacyMirror; $.legacyTetherV1 = legacyTether; $.submitterAttestationSigner = attestationSigner; } // --- views --- function getConfig() external view returns ( uint16 batchSize, uint32 maxBatchWaitSeconds, uint256 minPaymentValueWei, bool requireValidatorSigs, bool allowCalldataOnlySubmit, bool allowCCIPIngress ) { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); return ( $.batchSize, $.maxBatchWaitSeconds, $.minPaymentValueWei, $.requireValidatorSigs, $.allowCalldataOnlySubmit, $.allowCCIPIngress ); } function getFullConfig() external view returns (CheckpointHubConfig.HubConfig memory cfg) { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); cfg.chainId = $.chainId; cfg.batchSize = $.batchSize; cfg.maxBatchWaitSeconds = $.maxBatchWaitSeconds; cfg.minPaymentValueWei = $.minPaymentValueWei; cfg.requireValidatorSigs = $.requireValidatorSigs; cfg.allowCalldataOnlySubmit = $.allowCalldataOnlySubmit; cfg.allowCCIPIngress = $.allowCCIPIngress; cfg.enforcePreviousBatchId = $.enforcePreviousBatchId; cfg.ccipRouter = $.ccipRouter; cfg.sourceChainSelector = $.expectedSourceChainSelector; cfg.batchEmitterOnSource = $.batchEmitterOnSource; cfg.legacyMirrorV1 = $.legacyMirrorV1; cfg.legacyTetherV1 = $.legacyTetherV1; cfg.submitterAttestationSigner = $.submitterAttestationSigner; } function applyConfig(CheckpointHubConfig.HubConfig calldata cfg) external onlyRole(DEFAULT_ADMIN_ROLE) { CheckpointHubConfig.validate(cfg); _applyHubConfig(cfg); emit HubConfigApplied(cfg); } function extensionCount() external view returns (uint256) { return CheckpointStorage.get().extensionList.length; } function getExtension(bytes32 extensionId) external view returns (address module, uint32 hooks, bool active) { CheckpointStorage.ExtensionConfig storage cfg = CheckpointStorage.get().extensions[extensionId]; return (cfg.module, cfg.hooks, cfg.active); } function getExtensionList() external view returns (bytes32[] memory) { return CheckpointStorage.get().extensionList; } function getLatestCheckpoint() external view returns (CheckpointStorage.CheckpointHeader memory) { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); return $.checkpoints[$.latestBatchId]; } function getCheckpoint(uint64 batchId) external view returns (CheckpointStorage.CheckpointHeader memory) { return CheckpointStorage.get().checkpoints[batchId]; } function getLatestBatchId() external view returns (uint64) { return CheckpointStorage.get().latestBatchId; } function latestCheckpointBlock() external view returns (uint256) { return CheckpointStorage.get().latestCheckpointBlock; } function verifyPaymentInBatch( uint64 batchId, CheckpointLeaf.PaymentLeafV1 calldata leaf, bytes32[] calldata proof ) external view returns (bool) { CheckpointStorage.CheckpointHeader storage h = CheckpointStorage.get().checkpoints[batchId]; if (h.batchId == 0) revert CheckpointErrors.UnknownBatch(); bytes32 leafHash = CheckpointLeaf.paymentLeafV1(h.chainId, leaf); CheckpointStorage.CheckpointHeader memory hm = h; if (_extensionVerifyLeaf(hm, leaf, proof)) return true; return CheckpointLeaf.verifyMerkle(h.paymentsRoot, leafHash, proof); } function isTxIncluded(bytes32 txHash) external view returns (bool included, uint64 batchId) { batchId = CheckpointStorage.get().txHashToBatchId[txHash]; included = batchId != 0; } // --- submit --- function paused() external view returns (bool) { return CheckpointStorage.get().paused; } function pause() external onlyRole(PAUSER_ROLE) { CheckpointStorage.get().paused = true; } function unpause() external onlyRole(PAUSER_ROLE) { CheckpointStorage.get().paused = false; } function submitCheckpoint( CheckpointStorage.CheckpointHeader calldata header, bytes calldata validatorSignatures, bytes32[] calldata txHashes, bytes calldata extensionData ) external onlyRole(SUBMITTER_ROLE) nonReentrant { _requireNotPaused(); _runBeforeSubmitExtensions(header, validatorSignatures, extensionData); _submit(header, validatorSignatures, txHashes, bytes32(0), false, false, extensionData); _runExtensions(HOOK_AFTER_SUBMIT, header, extensionData); } function submitCheckpointByRelayer( CheckpointStorage.CheckpointHeader calldata header, bytes calldata validatorSignatures, bytes32[] calldata txHashes, bytes calldata extensionData, bytes calldata submitterSignature ) external onlyRole(RELAYER_ROLE) nonReentrant { _requireNotPaused(); _verifySubmitterAttestation(header, submitterSignature); CheckpointStorage.CheckpointHeader memory h = header; h.flags |= CheckpointFlags.RELAYER_SUBMIT; _runBeforeSubmitExtensions(h, validatorSignatures, extensionData); _submit(h, validatorSignatures, txHashes, bytes32(0), false, false, extensionData); _runExtensions(HOOK_AFTER_SUBMIT, h, extensionData); } function submitCheckpointWithLeaves( CheckpointStorage.CheckpointHeader calldata header, bytes calldata validatorSignatures, bytes32[] calldata txHashes, CheckpointLeaf.PaymentLeafV1[] calldata leaves ) external onlyRole(SUBMITTER_ROLE) nonReentrant { _requireNotPaused(); CheckpointPaymentsLib.assertPaymentsRootV1(header.chainId, header.paymentsRoot, leaves); bytes memory leafPayload = abi.encode(leaves); _runBeforeSubmitExtensions(header, validatorSignatures, leafPayload); _submit(header, validatorSignatures, txHashes, bytes32(0), false, false, leafPayload); _runExtensions(HOOK_AFTER_SUBMIT, header, leafPayload); } function forceCheckpoint( CheckpointStorage.CheckpointHeader calldata header, bytes calldata validatorSignatures, bytes32[] calldata txHashes, bytes calldata extensionData ) external onlyRole(EMERGENCY_ROLE) nonReentrant { CheckpointStorage.CheckpointHeader memory h = header; h.flags |= CheckpointFlags.EMERGENCY; _runBeforeSubmitExtensions(h, validatorSignatures, extensionData); _submit(h, validatorSignatures, txHashes, bytes32(0), false, true, extensionData); _runExtensions(HOOK_AFTER_SUBMIT, h, extensionData); } function submitCheckpointCommitment( CheckpointStorage.CheckpointHeader calldata header, bytes calldata validatorSignatures, bytes32 contentURI, bytes calldata extensionData ) external onlyRole(SUBMITTER_ROLE) nonReentrant { _requireNotPaused(); CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); if (!$.allowCalldataOnlySubmit) revert CheckpointErrors.CalldataOnlyDisabled(); _runBeforeSubmitExtensions(header, validatorSignatures, extensionData); _submit(header, validatorSignatures, new bytes32[](0), contentURI, true, false, extensionData); _runExtensions(HOOK_AFTER_SUBMIT, header, extensionData); } function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external nonReentrant { _requireNotPaused(); CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); if (!$.allowCCIPIngress) revert CheckpointErrors.CcipDisabled(); if (msg.sender != $.ccipRouter) revert CheckpointErrors.OnlyRouter(); if (message.sourceChainSelector != $.expectedSourceChainSelector) revert CheckpointErrors.BadSelector(); address sender = message.sender.length >= 32 ? address(uint160(uint256(bytes32(message.sender)))) : address(bytes20(message.sender)); if (sender != $.batchEmitterOnSource) revert CheckpointErrors.BadEmitter(); ( CheckpointStorage.CheckpointHeader memory header, bytes memory validatorSignatures, bytes32[] memory txHashes, bytes32 contentURI, bytes memory extensionData ) = abi.decode( message.data, (CheckpointStorage.CheckpointHeader, bytes, bytes32[], bytes32, bytes) ); header.flags |= CheckpointFlags.CCIP_INGRESS; _runExtensions(HOOK_ON_CCIP, header, message.data); _runBeforeSubmitExtensions(header, validatorSignatures, extensionData); _submit( header, validatorSignatures, txHashes, contentURI, CheckpointFlags.has(header.flags, CheckpointFlags.CALLDATA_ONLY), false, extensionData ); _runExtensions(HOOK_AFTER_SUBMIT, header, extensionData); } // --- extensions --- function registerExtension(bytes32 extensionId, address module, uint32 hooks) external onlyRole(EXTENSION_ADMIN_ROLE) { if (module == address(0)) revert CheckpointErrors.ZeroModule(); CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); if ($.extensions[extensionId].active) revert CheckpointErrors.ExtensionExists(); $.extensions[extensionId] = CheckpointStorage.ExtensionConfig({module: module, hooks: hooks, active: true}); $.extensionList.push(extensionId); emit ExtensionRegistered(extensionId, module, hooks); } function revokeExtension(bytes32 extensionId) external onlyRole(EXTENSION_ADMIN_ROLE) { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); if (!$.extensions[extensionId].active) revert CheckpointErrors.ExtensionMissing(); delete $.extensions[extensionId]; bytes32[] storage list = $.extensionList; for (uint256 i = 0; i < list.length; i++) { if (list[i] == extensionId) { list[i] = list[list.length - 1]; list.pop(); break; } } emit ExtensionRevoked(extensionId); } function setConfig( uint16 batchSize, uint32 maxBatchWaitSeconds, uint256 minPaymentValueWei, bool requireValidatorSigs, bool allowCalldataOnlySubmit, bool allowCCIPIngress ) external onlyRole(DEFAULT_ADMIN_ROLE) { if (batchSize == 0 || batchSize > 256) revert CheckpointErrors.BatchSize(); CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); $.batchSize = batchSize; $.maxBatchWaitSeconds = maxBatchWaitSeconds; $.minPaymentValueWei = minPaymentValueWei; $.requireValidatorSigs = requireValidatorSigs; $.allowCalldataOnlySubmit = allowCalldataOnlySubmit; $.allowCCIPIngress = allowCCIPIngress; } function setExtensionActive(bytes32 extensionId, bool active) external onlyRole(EXTENSION_ADMIN_ROLE) { CheckpointStorage.ExtensionConfig storage cfg = CheckpointStorage.get().extensions[extensionId]; if (cfg.module == address(0)) revert CheckpointErrors.ExtensionMissing(); cfg.active = active; } function updateExtensionHooks(bytes32 extensionId, uint32 hooks) external onlyRole(EXTENSION_ADMIN_ROLE) { CheckpointStorage.ExtensionConfig storage cfg = CheckpointStorage.get().extensions[extensionId]; if (cfg.module == address(0)) revert CheckpointErrors.ExtensionMissing(); cfg.hooks = hooks; } function _authorizeUpgrade(address) internal override onlyRole(UPGRADER_ROLE) {} function _applyHubConfig(CheckpointHubConfig.HubConfig calldata cfg) internal { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); $.chainId = cfg.chainId; $.batchSize = cfg.batchSize; $.maxBatchWaitSeconds = cfg.maxBatchWaitSeconds; $.minPaymentValueWei = cfg.minPaymentValueWei; $.requireValidatorSigs = cfg.requireValidatorSigs; $.allowCalldataOnlySubmit = cfg.allowCalldataOnlySubmit; $.allowCCIPIngress = cfg.allowCCIPIngress; $.enforcePreviousBatchId = cfg.enforcePreviousBatchId; if (cfg.ccipRouter != address(0)) $.ccipRouter = cfg.ccipRouter; if (cfg.sourceChainSelector != 0) $.expectedSourceChainSelector = cfg.sourceChainSelector; if (cfg.batchEmitterOnSource != address(0)) $.batchEmitterOnSource = cfg.batchEmitterOnSource; if (cfg.legacyMirrorV1 != address(0)) $.legacyMirrorV1 = cfg.legacyMirrorV1; if (cfg.legacyTetherV1 != address(0)) $.legacyTetherV1 = cfg.legacyTetherV1; if (cfg.submitterAttestationSigner != address(0)) { $.submitterAttestationSigner = cfg.submitterAttestationSigner; } } // --- internal --- function _requireNotPaused() internal view { if (CheckpointStorage.get().paused) revert CheckpointErrors.Paused(); } function _submit( CheckpointStorage.CheckpointHeader memory header, bytes memory validatorSignatures, bytes32[] memory txHashes, bytes32 contentURI, bool calldataOnly, bool emergency, bytes memory extensionData ) internal { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); if ($.processedBatchIds[header.batchId]) revert CheckpointErrors.BatchDone(); if (header.chainId != $.chainId) revert CheckpointErrors.BadChain(); if (header.batchId <= $.latestBatchId) revert CheckpointErrors.BatchOrder(); if ($.enforcePreviousBatchId && $.latestBatchId > 0 && header.previousBatchId != $.latestBatchId) { revert CheckpointErrors.PrevBatch(); } if (header.endBlock > 0 && header.endBlock < header.startBlock) revert CheckpointErrors.BadBlocks(); if (header.txCount == 0 || header.txCount > $.batchSize) revert CheckpointErrors.TxCount(); if (!emergency && !CheckpointFlags.has(header.flags, CheckpointFlags.PARTIAL_BATCH) && header.txCount != $.batchSize) { revert CheckpointErrors.IncompleteBatch(); } if (header.paymentsRoot == bytes32(0)) revert CheckpointErrors.PaymentsRoot(); if (header.stateRoot == bytes32(0)) revert CheckpointErrors.StateRoot(); if (header.blockHash == bytes32(0)) revert CheckpointErrors.BlockHash(); if ($.requireValidatorSigs && validatorSignatures.length == 0) revert CheckpointErrors.Signatures(); if ($.minPaymentValueWei > 0 && extensionData.length > 0) { CheckpointPaymentsLib.enforceMinPaymentValueV1(extensionData, $.minPaymentValueWei); } bytes32 proofHash = keccak256( abi.encodePacked( header.batchId, header.paymentsRoot, header.stateRoot, header.checkpointBlock, validatorSignatures ) ); if ($.processedProofHashes[proofHash]) revert CheckpointErrors.Replay(); $.processedProofHashes[proofHash] = true; if (calldataOnly) { header.flags |= CheckpointFlags.CALLDATA_ONLY; } if (contentURI != bytes32(0)) { header.flags |= CheckpointFlags.HAS_CONTENT_URI; header.contentURI = contentURI; } header.submittedAt = uint64(block.timestamp); header.submitter = msg.sender; for (uint256 i = 0; i < txHashes.length; i++) { if (txHashes[i] == bytes32(0)) revert CheckpointErrors.ZeroTx(); if ($.txHashToBatchId[txHashes[i]] != 0) revert CheckpointErrors.TxSeen(); $.txHashToBatchId[txHashes[i]] = header.batchId; } $.checkpoints[header.batchId] = header; $.processedBatchIds[header.batchId] = true; $.latestBatchId = header.batchId; $.latestCheckpointBlock = header.checkpointBlock; $.latestPaymentsRoot = header.paymentsRoot; emit CheckpointSubmitted( header.batchId, header.checkpointBlock, header.paymentsRoot, header.txCount, header.flags, header.contentURI ); } /// @dev Validator extension expects ECDSA bytes; other extensions expect leaf/extension payload. function _runBeforeSubmitExtensions( CheckpointStorage.CheckpointHeader memory header, bytes memory validatorSignatures, bytes memory extensionData ) internal { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); bytes32[] storage list = $.extensionList; bytes32 validatorId = ExtensionIds.VALIDATOR_SIG; CheckpointStorage.ExtensionConfig storage validatorCfg = $.extensions[validatorId]; if (validatorCfg.active && (validatorCfg.hooks & HOOK_BEFORE_SUBMIT) != 0) { ICheckpointExtension(validatorCfg.module).beforeSubmit(header, validatorSignatures); } for (uint256 i = 0; i < list.length; i++) { bytes32 id = list[i]; if (id == validatorId) continue; CheckpointStorage.ExtensionConfig storage cfg = $.extensions[id]; if (!cfg.active || (cfg.hooks & HOOK_BEFORE_SUBMIT) == 0) continue; ICheckpointExtension(cfg.module).beforeSubmit(header, extensionData); } } function _runExtensions( uint32 hook, CheckpointStorage.CheckpointHeader memory header, bytes memory data ) internal { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); bytes32[] storage list = $.extensionList; for (uint256 i = 0; i < list.length; i++) { CheckpointStorage.ExtensionConfig storage cfg = $.extensions[list[i]]; if (!cfg.active || (cfg.hooks & hook) == 0) continue; if (hook == HOOK_BEFORE_SUBMIT) { ICheckpointExtension(cfg.module).beforeSubmit(header, data); } else if (hook == HOOK_AFTER_SUBMIT) { ICheckpointExtension(cfg.module).afterSubmit(header, data); } else if (hook == HOOK_ON_CCIP) { ICheckpointExtension(cfg.module).onCCIPReceive(data); } } } function _verifySubmitterAttestation( CheckpointStorage.CheckpointHeader calldata header, bytes calldata submitterSignature ) internal view { if (submitterSignature.length != 65) revert CheckpointErrors.SubmitterSigLen(); bytes32 digest = CheckpointEIP712.digest(address(this), block.chainid, header); address signer = ECDSA.recover(digest, submitterSignature); CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); if ($.submitterAttestationSigner != address(0)) { if (signer != $.submitterAttestationSigner) revert CheckpointErrors.AttestSigner(); return; } if (!hasRole(SUBMITTER_ROLE, signer)) revert CheckpointErrors.SubmitterRole(); } function _extensionVerifyLeaf( CheckpointStorage.CheckpointHeader memory h, CheckpointLeaf.PaymentLeafV1 calldata leaf, bytes32[] calldata proof ) internal view returns (bool) { CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get(); bytes32[] storage list = $.extensionList; for (uint256 i = 0; i < list.length; i++) { CheckpointStorage.ExtensionConfig storage cfg = $.extensions[list[i]]; if (!cfg.active || (cfg.hooks & HOOK_VERIFY_LEAF) == 0) continue; if (ICheckpointExtension(cfg.module).verifyLeaf(h, leaf, proof)) return true; } return false; } }