diff --git a/README.md b/README.md index 0b1ed64..4b3c695 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,11 @@ A cross-chain smart contract bytecode repository system that enables secure, ver **šŸ“” L2DeployManager** - L2 deployment receiver that ensures bytecode integrity -- CCIP message receiver for secure cross-chain bytecode synchronization +- CCIP message receiver for cross-chain bytecode requests: L1 commits only `(bytecodeHash, initCodeHash)` per message, keeping the receive cost constant (~30k gas) and well under CCIP's per-message gas cap regardless of contract size +- Permissionless `uploadBytecode` function: anyone can supply the full init code on L2, accepted only if `keccak256(initCode)` matches the hash committed from L1 — integrity is enforced cryptographically without restricting who pays the upload gas - SSTORE2-based storage system for gas-efficient bytecode persistence on L2s - CREATE2 deployment matching L1 addresses for consistent multi-chain presence - Factory integration for specialized contract types deployment (Comet, Market, etc.) -- Automatic bytecode validation and verification before storage - Developers can request deployment access through L1DeployManager **šŸ­ BaseFactory** - Abstract deployment factory with standardized patterns @@ -139,9 +139,10 @@ BytecodeRepository provides a **three-layer solution**: **How it works**: -- L1DeployManager encodes bytecode and version information into CCIP messages. +- L1DeployManager sends a compact `(bytecodeHash, initCodeHash)` commitment via CCIP — not the bytecode itself. This keeps the cross-chain payload tiny and the L2 receive gas constant, fitting comfortably inside CCIP's per-message gas limit even for the largest contracts (full Comet exceeds 4M gas to write, well past CCIP's cap). - Messages are validated by Chainlink's Risk Management Network. -- L2DeployManager receives and validates messages before storing bytecode. +- L2DeployManager receives the commitment and records `bytecodeRequested[bytecodeHash] = initCodeHash`. +- Anyone may then call `uploadBytecode(version, initCode)` on L2DeployManager. The init code is accepted only if `keccak256(initCode)` matches the committed `initCodeHash`, guaranteeing the stored bytes are exactly what L1 audited. #### 3. CREATE2 Deterministic Deployment @@ -473,10 +474,14 @@ npx hardhat run scripts/cli/submitAuditReport.ts --network ethereum -- \ ### Phase 4: Cross-chain Distribution 🌐 -**Step 1: L1 to L2 Transmission** +Cross-chain distribution is a **two-step** process: L1 commits the bytecode hash via CCIP, then anyone uploads the matching init code on L2. Splitting the steps lets the system ship arbitrarily large contracts — the CCIP message itself is constant-size. + +**Step 1: L1 commits the bytecode hash via CCIP** + +`sendBytecodeToOtherChain` reads the audit-verified `initCodeHash` from VersionController and sends only the `(bytecodeHash, initCodeHash)` pair across CCIP — never the full bytecode. ```solidity -// Send audited bytecode to L2 networks. Any user with the developer role can initiate the operation for any audited bytecode. +// Audited bytecode version to make available on another chain. Any user with the developer role can initiate the operation for any audited bytecode. BytecodeVersion memory bytecodeVersion = BytecodeVersion({ contractType: "Comet", version: VersionWithAlternative({ @@ -485,20 +490,41 @@ BytecodeVersion memory bytecodeVersion = BytecodeVersion({ }) }); -// Send to Arbitrum -l1DeployManager.sendBytecodeToChain(42161, bytecodeVersion); +// L2 _ccipReceive consumes ~30k gas; 100k is a comfortable margin for ABI/decode overhead +uint256 ccipGasLimit = 100_000; -// Send to Polygon -l1DeployManager.sendBytecodeToChain(137, bytecodeVersion); +// Commit the hash to Arbitrum, Polygon, Optimism. msg.value pays the CCIP fee +// (or the contract's donated balance is used if available). +l1DeployManager.sendBytecodeToOtherChain{ value: ccipFee }(bytecodeVersion, 42161, ccipGasLimit); +l1DeployManager.sendBytecodeToOtherChain{ value: ccipFee }(bytecodeVersion, 137, ccipGasLimit); +l1DeployManager.sendBytecodeToOtherChain{ value: ccipFee }(bytecodeVersion, 10, ccipGasLimit); + +// After CCIP delivery, L2DeployManager._ccipReceive records: +// bytecodeRequested[bytecodeHash] = initCodeHash +// and emits BytecodeRequested(messageId, bytecodeHash, initCodeHash). +``` -// Send to Optimism -l1DeployManager.sendBytecodeToChain(10, bytecodeVersion); +**Step 2: Anyone uploads the actual init code on L2** -// At this point, no additional actions is required. -//CCIP handles the routing of the message. -//L2DeployManager validates received message and stores the bytecode. +Once the request is recorded on L2, any account can supply the bytes. The contract accepts the upload only if `keccak256(initCode)` equals the `initCodeHash` committed by L1 — so the upload is permissionless without weakening integrity. Front-runners cannot inject malicious bytecode; they would just be doing the work for someone else. + +```solidity +// initCode can be obtained from VersionController on L1 (getVerifiedBytecode) or +// from any public source matching the audited release. +bytes memory initCode = /* full init code matching the L1-committed hash */; + +l2DeployManager.uploadBytecode(bytecodeVersion, initCode); + +// L2DeployManager: +// 1. Recomputes bytecodeHash from version +// 2. Reverts if no matching request is pending (BytecodeNotRequested) +// 3. Reverts if keccak256(initCode) != bytecodeRequested[bytecodeHash] (InvalidBytecode) +// 4. Stores the init code via SSTORE2 (chunked across multiple data contracts) +// 5. Clears bytecodeRequested[bytecodeHash] and emits BytecodeUploaded(bytecodeHash) ``` +After this step the bytecode is available for `deploy()` calls on L2DeployManager and any factory that uses the BytecodeProvider interface. + ### Phase 5: Deployment Scenarios šŸš€ #### Scenario A: Arbitrary Contract Deployment diff --git a/abi/full/contracts/L1DeployManager.sol/L1DeployManager.json b/abi/full/contracts/L1DeployManager.sol/L1DeployManager.json index 4c24694..5c0421f 100644 --- a/abi/full/contracts/L1DeployManager.sol/L1DeployManager.json +++ b/abi/full/contracts/L1DeployManager.sol/L1DeployManager.json @@ -1,7 +1,6 @@ [ "constructor(address _versionController, address _routerClient)", "error AddressEmptyCode(address target)", - "error BytecodeAlreadySent(uint256 _chainId, bytes32 _bytecodeHash)", "error CantRevokeDeveloper(address _account)", "error Create2EmptyBytecode()", "error ERC1967InvalidImplementation(address implementation)", diff --git a/abi/full/contracts/L2DeployManager.sol/L2DeployManager.json b/abi/full/contracts/L2DeployManager.sol/L2DeployManager.json index 465848a..b63449a 100644 --- a/abi/full/contracts/L2DeployManager.sol/L2DeployManager.json +++ b/abi/full/contracts/L2DeployManager.sol/L2DeployManager.json @@ -1,20 +1,25 @@ [ "constructor(uint64 _sourceChainSelector, address _l1DeployManager, address _router, address _localTimelock)", + "error BytecodeAlreadyUploaded(bytes32 _bytecodeHash)", "error BytecodeIsEmpty()", + "error BytecodeNotRequested(bytes32 _bytecodeHash)", "error Create2EmptyBytecode()", "error FailedDeployment()", "error InitCodeIsEmpty()", "error InsufficientBalance(uint256 balance, uint256 needed)", + "error InvalidBytecode(bytes32 _bytecodeHash, bytes32 _initCodeHash)", "error InvalidRouter(address router)", "error InvalidSender()", "error OnlyDeveloperOrGovernor()", "error OnlyTimelock()", "error ZeroAddress()", - "event BytecodeReceived(bytes32 _messageId, bytes32 _bytecodeHash)", + "event BytecodeRequested(bytes32 _messageId, bytes32 _bytecodeHash, bytes32 _initCodeHash)", + "event BytecodeUploaded(bytes32 _bytecodeHash)", "event ContractDeployed(tuple(bytes32 contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) version) _bytecodeVersion, bytes _constructorParams, address _newContract, address _deployer)", "event DeveloperAccessGranted(address _developer)", "event DeveloperRevoked(address _account)", "function DEVELOPER_ACCESS_DURATION() view returns (uint256)", + "function bytecodeRequested(bytes32) view returns (bytes32)", "function ccipReceive(tuple(bytes32 messageId, uint64 sourceChainSelector, bytes sender, bytes data, tuple(address token, uint256 amount)[] destTokenAmounts) message)", "function computeAddress(tuple(bytes32 contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) version) _bytecodeVersion, bytes32 _salt, bytes _constructorParams, address _deployer) view returns (address)", "function deploy(tuple(bytes32 contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) version) _bytecodeVersion, bytes32 _salt, bytes _constructorParams) payable returns (address)", @@ -26,5 +31,6 @@ "function localTimelock() view returns (address)", "function sourceChainSelector() view returns (uint64)", "function supportsInterface(bytes4 interfaceId) view returns (bool)", + "function uploadBytecode(tuple(bytes32 contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) version) _bytecodeVersion, bytes _initCode)", "function versionExists(tuple(bytes32 contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) version) _version) view returns (bool)" ] diff --git a/abi/full/contracts/VersionController.sol/VersionController.json b/abi/full/contracts/VersionController.sol/VersionController.json index a62b0a8..93a15cd 100644 --- a/abi/full/contracts/VersionController.sol/VersionController.json +++ b/abi/full/contracts/VersionController.sol/VersionController.json @@ -27,6 +27,7 @@ "error NonExistingVersion(bytes32 _contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) _version)", "error NotAuthorizedForContractType(bytes32 _contractType, address _caller)", "error NotDeveloper(address _account)", + "error NotGovernorOrGuadian(address _account)", "error NotInitializing()", "error NotSubDeveloper(address _subDeveloper)", "error SameKeyDeveloper(address _keyDeveloper)", @@ -81,6 +82,7 @@ "function getRoleMembers(bytes32 role) view returns (address[])", "function getSubDevsForKeyDeveloper(address _keyDeveloper) view returns (address[])", "function getVerifiedBytecode(tuple(bytes32 contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) version) _version) view returns (bytes)", + "function getVerifiedInitCodeHash(tuple(bytes32 contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) version) _version) view returns (bytes32)", "function grantRole(bytes32 role, address account)", "function hasRole(bytes32 role, address account) view returns (bool)", "function initialize(address _initialAdmin, address _guardian)", diff --git a/abi/json/contracts/L1DeployManager.sol/L1DeployManager.json b/abi/json/contracts/L1DeployManager.sol/L1DeployManager.json index 595162b..692015b 100644 --- a/abi/json/contracts/L1DeployManager.sol/L1DeployManager.json +++ b/abi/json/contracts/L1DeployManager.sol/L1DeployManager.json @@ -26,22 +26,6 @@ "name": "AddressEmptyCode", "type": "error" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_chainId", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "_bytecodeHash", - "type": "bytes32" - } - ], - "name": "BytecodeAlreadySent", - "type": "error" - }, { "inputs": [ { diff --git a/abi/json/contracts/L2DeployManager.sol/L2DeployManager.json b/abi/json/contracts/L2DeployManager.sol/L2DeployManager.json index 34c5c45..4e7febb 100644 --- a/abi/json/contracts/L2DeployManager.sol/L2DeployManager.json +++ b/abi/json/contracts/L2DeployManager.sol/L2DeployManager.json @@ -25,11 +25,33 @@ "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_bytecodeHash", + "type": "bytes32" + } + ], + "name": "BytecodeAlreadyUploaded", + "type": "error" + }, { "inputs": [], "name": "BytecodeIsEmpty", "type": "error" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_bytecodeHash", + "type": "bytes32" + } + ], + "name": "BytecodeNotRequested", + "type": "error" + }, { "inputs": [], "name": "Create2EmptyBytecode", @@ -61,6 +83,22 @@ "name": "InsufficientBalance", "type": "error" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_bytecodeHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_initCodeHash", + "type": "bytes32" + } + ], + "name": "InvalidBytecode", + "type": "error" + }, { "inputs": [ { @@ -106,9 +144,28 @@ "internalType": "bytes32", "name": "_bytecodeHash", "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "_initCodeHash", + "type": "bytes32" } ], - "name": "BytecodeReceived", + "name": "BytecodeRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "_bytecodeHash", + "type": "bytes32" + } + ], + "name": "BytecodeUploaded", "type": "event" }, { @@ -222,6 +279,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "bytecodeRequested", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -588,6 +664,65 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "contractType", + "type": "bytes32" + }, + { + "components": [ + { + "components": [ + { + "internalType": "uint64", + "name": "major", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "minor", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "patch", + "type": "uint64" + } + ], + "internalType": "struct Types.Version", + "name": "version", + "type": "tuple" + }, + { + "internalType": "string", + "name": "alternative", + "type": "string" + } + ], + "internalType": "struct Types.VersionWithAlternative", + "name": "version", + "type": "tuple" + } + ], + "internalType": "struct Types.BytecodeVersion", + "name": "_bytecodeVersion", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "_initCode", + "type": "bytes" + } + ], + "name": "uploadBytecode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/abi/json/contracts/VersionController.sol/VersionController.json b/abi/json/contracts/VersionController.sol/VersionController.json index a38ab94..50bf1fd 100644 --- a/abi/json/contracts/VersionController.sol/VersionController.json +++ b/abi/json/contracts/VersionController.sol/VersionController.json @@ -352,6 +352,17 @@ "name": "NotDeveloper", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + } + ], + "name": "NotGovernorOrGuadian", + "type": "error" + }, { "inputs": [], "name": "NotInitializing", @@ -1492,6 +1503,66 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "contractType", + "type": "bytes32" + }, + { + "components": [ + { + "components": [ + { + "internalType": "uint64", + "name": "major", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "minor", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "patch", + "type": "uint64" + } + ], + "internalType": "struct Types.Version", + "name": "version", + "type": "tuple" + }, + { + "internalType": "string", + "name": "alternative", + "type": "string" + } + ], + "internalType": "struct Types.VersionWithAlternative", + "name": "version", + "type": "tuple" + } + ], + "internalType": "struct Types.BytecodeVersion", + "name": "_version", + "type": "tuple" + } + ], + "name": "getVerifiedInitCodeHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/abi/minimal/contracts/L1DeployManager.sol/L1DeployManager.json b/abi/minimal/contracts/L1DeployManager.sol/L1DeployManager.json index 1d03af3..96c68df 100644 --- a/abi/minimal/contracts/L1DeployManager.sol/L1DeployManager.json +++ b/abi/minimal/contracts/L1DeployManager.sol/L1DeployManager.json @@ -1,7 +1,6 @@ [ "constructor(address,address)", "error AddressEmptyCode(address)", - "error BytecodeAlreadySent(uint256,bytes32)", "error CantRevokeDeveloper(address)", "error Create2EmptyBytecode()", "error ERC1967InvalidImplementation(address)", diff --git a/abi/minimal/contracts/L2DeployManager.sol/L2DeployManager.json b/abi/minimal/contracts/L2DeployManager.sol/L2DeployManager.json index 7ea3bdf..bcceaf5 100644 --- a/abi/minimal/contracts/L2DeployManager.sol/L2DeployManager.json +++ b/abi/minimal/contracts/L2DeployManager.sol/L2DeployManager.json @@ -1,20 +1,25 @@ [ "constructor(uint64,address,address,address)", + "error BytecodeAlreadyUploaded(bytes32)", "error BytecodeIsEmpty()", + "error BytecodeNotRequested(bytes32)", "error Create2EmptyBytecode()", "error FailedDeployment()", "error InitCodeIsEmpty()", "error InsufficientBalance(uint256,uint256)", + "error InvalidBytecode(bytes32,bytes32)", "error InvalidRouter(address)", "error InvalidSender()", "error OnlyDeveloperOrGovernor()", "error OnlyTimelock()", "error ZeroAddress()", - "event BytecodeReceived(bytes32,bytes32)", + "event BytecodeRequested(bytes32,bytes32,bytes32)", + "event BytecodeUploaded(bytes32)", "event ContractDeployed(tuple(bytes32,tuple(tuple(uint64,uint64,uint64),string)),bytes,address,address)", "event DeveloperAccessGranted(address)", "event DeveloperRevoked(address)", "function DEVELOPER_ACCESS_DURATION() view returns (uint256)", + "function bytecodeRequested(bytes32) view returns (bytes32)", "function ccipReceive(tuple(bytes32,uint64,bytes,bytes,tuple(address,uint256)[]))", "function computeAddress(tuple(bytes32,tuple(tuple(uint64,uint64,uint64),string)),bytes32,bytes,address) view returns (address)", "function deploy(tuple(bytes32,tuple(tuple(uint64,uint64,uint64),string)),bytes32,bytes) payable returns (address)", @@ -26,5 +31,6 @@ "function localTimelock() view returns (address)", "function sourceChainSelector() view returns (uint64)", "function supportsInterface(bytes4) view returns (bool)", + "function uploadBytecode(tuple(bytes32,tuple(tuple(uint64,uint64,uint64),string)),bytes)", "function versionExists(tuple(bytes32,tuple(tuple(uint64,uint64,uint64),string))) view returns (bool)" ] diff --git a/abi/minimal/contracts/VersionController.sol/VersionController.json b/abi/minimal/contracts/VersionController.sol/VersionController.json index f210007..78faa67 100644 --- a/abi/minimal/contracts/VersionController.sol/VersionController.json +++ b/abi/minimal/contracts/VersionController.sol/VersionController.json @@ -27,6 +27,7 @@ "error NonExistingVersion(bytes32,tuple(tuple(uint64,uint64,uint64),string))", "error NotAuthorizedForContractType(bytes32,address)", "error NotDeveloper(address)", + "error NotGovernorOrGuadian(address)", "error NotInitializing()", "error NotSubDeveloper(address)", "error SameKeyDeveloper(address)", @@ -81,6 +82,7 @@ "function getRoleMembers(bytes32) view returns (address[])", "function getSubDevsForKeyDeveloper(address) view returns (address[])", "function getVerifiedBytecode(tuple(bytes32,tuple(tuple(uint64,uint64,uint64),string))) view returns (bytes)", + "function getVerifiedInitCodeHash(tuple(bytes32,tuple(tuple(uint64,uint64,uint64),string))) view returns (bytes32)", "function grantRole(bytes32,address)", "function hasRole(bytes32,address) view returns (bool)", "function initialize(address,address)", diff --git a/contracts/L1DeployManager.sol b/contracts/L1DeployManager.sol index cb92ca5..694eb98 100644 --- a/contracts/L1DeployManager.sol +++ b/contracts/L1DeployManager.sol @@ -31,7 +31,7 @@ import { IL1DeployManager } from "./interfaces/IL1DeployManager.sol"; * 1. Donate ETH to the contract to subsidize cross-chain message costs for developers and community deployments. * 2. Query chain configurations and deployment status for transparency and integration planning. * - The contract validates bytecode audit status before deployment, ensuring only auditor-verified contracts reach production networks. - * - CCIP message encoding includes bytecode hash and full initCode, with automatic chunking via SSTORE2 for large contracts exceeding network limits. + * - CCIP message encoding includes bytecode hash and initCode hash, requesting an upload of bytecode on other chain. * - Address computation matches L2DeployManager behavior exactly, guaranteeing identical contract addresses across all supported networks. * - The contract serves as the canonical L1 coordinator for the BytecodeRepository ecosystem, bridging audited bytecode storage with multi-chain deployment execution. */ @@ -146,11 +146,14 @@ contract L1DeployManager is IL1DeployManager, UUPSUpgradeable { _bytecodeVersion.contractType, _bytecodeVersion.version ); - if (isVersionSentToChain[_chainId][bytecodeHash]) revert BytecodeAlreadySent(_chainId, bytecodeHash); _ccipSend( _chainId, _gasLimit, - abi.encode(MessageType.SEND_BYTECODE, bytecodeHash, versionController.getVerifiedBytecode(_bytecodeVersion)) + abi.encode( + MessageType.SEND_BYTECODE, + bytecodeHash, + versionController.getVerifiedInitCodeHash(_bytecodeVersion) + ) ); isVersionSentToChain[_chainId][bytecodeHash] = true; diff --git a/contracts/L2DeployManager.sol b/contracts/L2DeployManager.sol index 434e3e9..f5792c6 100644 --- a/contracts/L2DeployManager.sol +++ b/contracts/L2DeployManager.sol @@ -25,7 +25,7 @@ import { IL2DeployManager } from "./interfaces/IL2DeployManager.sol"; * 3. Query stored bytecode availability to verify cross-chain synchronization status and plan deployments. * 4. Retrieve bytecode for custom deployment logic or verification purposes through the BytecodeProvider interface. * - The contract automatically handles: - * 1. CCIP message reception and validation from trusted L1DeployManager to ensure bytecode authenticity and prevent malicious injections. + * 1. CCIP message reception and validation from trusted L1DeployManager to ensure init code hash authenticity and prevent malicious injections. * 2. Bytecode storage using SSTORE2 with automatic chunking for large contracts exceeding network gas limits or size constraints. * 3. Address computation using identical salt generation as L1DeployManager, guaranteeing cross-chain address consistency. * 4. Integration with factory contracts via BytecodeProvider interface for specialized deployment patterns and protocol-specific logic. @@ -43,6 +43,8 @@ contract L2DeployManager is IL2DeployManager, IBytecodeProvider, CCIPReceiver { address public immutable l1DeployManager; /// @notice The address of local timelock. address public immutable localTimelock; + /// @notice Hash of bytecode, which should be uploaded. If non-empty, an init code which generates the exact hash must be uploaded. + mapping(bytes32 => bytes32) public bytecodeRequested; /// @notice Address pointers at which parts of bytecode are stored. Contract types => pointers. mapping(bytes32 => address[]) private storedBytecodePtrs; /// @notice A timestamp until which the account has a developer role on current chain. @@ -88,6 +90,25 @@ contract L2DeployManager is IL2DeployManager, IBytecodeProvider, CCIPReceiver { return newContract; } + /// @notice Uploads init code for a requested bytecode version. + /// @dev Uploading must first be requested via L1 through CCIP. + /// @dev The hash of init code must match the hash stored in _ccipReceive. + /// @dev Anyone can upload bytecode as long as valid init code is provided. + /// @param _bytecodeVersion Version of bytecode for which to upload. + /// @param _initCode Valid init code matching stored init code hash to upload. + function uploadBytecode(Types.BytecodeVersion calldata _bytecodeVersion, bytes calldata _initCode) external { + bytes32 bytecodeHash = BytecodeStore._computeBytecodeHash( + _bytecodeVersion.contractType, + _bytecodeVersion.version + ); + bytes32 initCodeHash = keccak256(_initCode); + if (bytecodeRequested[bytecodeHash] == bytes32(0)) revert BytecodeNotRequested(bytecodeHash); + if (bytecodeRequested[bytecodeHash] != initCodeHash) revert InvalidBytecode(bytecodeHash, initCodeHash); + storedBytecodePtrs[bytecodeHash] = BytecodeStore._writeInitCode(_initCode); + delete bytecodeRequested[bytecodeHash]; + emit BytecodeUploaded(bytecodeHash); + } + /* View functions */ /// @notice Returns a bytecode of the specified version. @@ -135,7 +156,7 @@ contract L2DeployManager is IL2DeployManager, IBytecodeProvider, CCIPReceiver { /// @notice Helper function for receiving messages from L1DeployManager. /// @dev The sender of the message from Ethereum must be L1DeployManager. - /// @param any2EvmMessage params necessary for the cross-chain message. Data contains bytecode hash and its bytecode for SEND_BYTECODE. + /// @param any2EvmMessage params necessary for the cross-chain message. Data contains bytecode hash and its init code hash for SEND_BYTECODE. /// and address of developer for BECOME_DEVELOPER or REVOKE_DEVELOPER. function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override { if ( @@ -144,13 +165,14 @@ contract L2DeployManager is IL2DeployManager, IBytecodeProvider, CCIPReceiver { ) revert InvalidSender(); MessageType mt = abi.decode(any2EvmMessage.data, (MessageType)); if (mt == MessageType.SEND_BYTECODE) { - (, bytes32 bytecodeHash, bytes memory initCode) = abi.decode( + (, bytes32 bytecodeHash, bytes32 initCodeHash) = abi.decode( any2EvmMessage.data, - (MessageType, bytes32, bytes) + (MessageType, bytes32, bytes32) ); - storedBytecodePtrs[bytecodeHash] = BytecodeStore._writeInitCode(initCode); + if (storedBytecodePtrs[bytecodeHash].length != 0) revert BytecodeAlreadyUploaded(bytecodeHash); + bytecodeRequested[bytecodeHash] = initCodeHash; - emit BytecodeReceived(any2EvmMessage.messageId, bytecodeHash); + emit BytecodeRequested(any2EvmMessage.messageId, bytecodeHash, initCodeHash); } else if (mt == MessageType.BECOME_DEVELOPER) { (, address developer) = abi.decode(any2EvmMessage.data, (MessageType, address)); developerUntil[developer] = block.timestamp + DEVELOPER_ACCESS_DURATION; diff --git a/contracts/VersionController.sol b/contracts/VersionController.sol index ab7b8e9..ee0216e 100644 --- a/contracts/VersionController.sol +++ b/contracts/VersionController.sol @@ -152,10 +152,9 @@ contract VersionController is /// @dev Correctness of contract type should be checked by the Governance before calling this function. /// @param _contractTypes An array containing types of contracts to assign developer for. /// @param _keyDeveloper An address of key developer to assign. address(0) is allowed to remove developer for contract type. - function assignDeveloperForContractTypes( - bytes32[] calldata _contractTypes, - address _keyDeveloper - ) external onlyRole(DEFAULT_ADMIN_ROLE) { + function assignDeveloperForContractTypes(bytes32[] calldata _contractTypes, address _keyDeveloper) external { + if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender) && !hasRole(GUARDIAN_ROLE, msg.sender)) + revert NotGovernorOrGuadian(msg.sender); uint256 contractTypesLengths = _contractTypes.length; if (contractTypesLengths == 0) revert ZeroLength(); if (_keyDeveloper != address(0)) _grantRole(KEY_DEVELOPER_ROLE, _keyDeveloper); @@ -511,6 +510,15 @@ contract VersionController is ); } + /// @notice Returns the hash of a verified bytecode of a specified contract type and version. + /// @dev Throws and error if bytecode is not verified by at least one auditor. + /// @param _version A bytecode version for which to return init code hash. + /// @return Init code hash of specified contract type and version. + function getVerifiedInitCodeHash(BytecodeVersion calldata _version) external view returns (bytes32) { + if (!isBytecodeVerified(_version)) revert BytecodeNotVerified(_version); + return bytecodes[computeBytecodeHash(_version.contractType, _version.version)].initCodeHash; + } + /// @notice Returns all alternative versions for given contract type. /// @param _contractType A type of contract for which to return alternative versions. /// @return Array containing all the alternative versions of given contract type. diff --git a/contracts/interfaces/IL1DeployManager.sol b/contracts/interfaces/IL1DeployManager.sol index ad16447..7a85581 100644 --- a/contracts/interfaces/IL1DeployManager.sol +++ b/contracts/interfaces/IL1DeployManager.sol @@ -20,7 +20,6 @@ interface IL1DeployManager { error OnlyGuardian(); error OnlyDeveloper(); error OnlyDeveloperOrGovernor(); - error BytecodeAlreadySent(uint256 _chainId, bytes32 _bytecodeHash); error ZeroAddress(); error CantRevokeDeveloper(address _account); diff --git a/contracts/interfaces/IL2DeployManager.sol b/contracts/interfaces/IL2DeployManager.sol index 7b897f2..2a78fb3 100644 --- a/contracts/interfaces/IL2DeployManager.sol +++ b/contracts/interfaces/IL2DeployManager.sol @@ -4,7 +4,8 @@ pragma solidity 0.8.30; import { Types } from "./Types.sol"; interface IL2DeployManager { - event BytecodeReceived(bytes32 _messageId, bytes32 _bytecodeHash); + event BytecodeRequested(bytes32 _messageId, bytes32 _bytecodeHash, bytes32 _initCodeHash); + event BytecodeUploaded(bytes32 _bytecodeHash); event ContractDeployed( Types.BytecodeVersion _bytecodeVersion, bytes _constructorParams, @@ -19,6 +20,9 @@ interface IL2DeployManager { error BytecodeIsEmpty(); error OnlyDeveloperOrGovernor(); error ZeroAddress(); + error BytecodeAlreadyUploaded(bytes32 _bytecodeHash); + error InvalidBytecode(bytes32 _bytecodeHash, bytes32 _initCodeHash); + error BytecodeNotRequested(bytes32 _bytecodeHash); /// @notice A type of message to receive from Ethereum. enum MessageType { diff --git a/contracts/interfaces/IVersionController.sol b/contracts/interfaces/IVersionController.sol index 1fe7476..58e44c9 100644 --- a/contracts/interfaces/IVersionController.sol +++ b/contracts/interfaces/IVersionController.sol @@ -38,6 +38,7 @@ interface IVersionController is IAccessControl, Types, IBytecodeProvider { error ZeroLength(); error AdminCantAddSubDevs(); error ConflictingRoles(address _account); + error NotGovernorOrGuadian(address _account); // Governor function assignDeveloperForContractTypes(bytes32[] calldata _contractTypes, address _keyDeveloper) external; @@ -98,4 +99,6 @@ interface IVersionController is IAccessControl, Types, IBytecodeProvider { function versionExists(bytes32 _bytecodeHash) external view returns (bool); function getAllAlternativeVersions(bytes32 _contractType) external view returns (VersionWithAlternative[] memory); + + function getVerifiedInitCodeHash(Types.BytecodeVersion calldata _version) external view returns (bytes32); } diff --git a/docs/L1DeployManager.md b/docs/L1DeployManager.md index f32d2d3..532c789 100644 --- a/docs/L1DeployManager.md +++ b/docs/L1DeployManager.md @@ -20,7 +20,7 @@ This contract orchestrates smart contract deployments on Ethereum L1 and facilit 1. Donate ETH to the contract to subsidize cross-chain message costs for developers and community deployments. 2. Query chain configurations and deployment status for transparency and integration planning. - The contract validates bytecode audit status before deployment, ensuring only auditor-verified contracts reach production networks. -- CCIP message encoding includes bytecode hash and full initCode, with automatic chunking via SSTORE2 for large contracts exceeding network limits. +- CCIP message encoding includes bytecode hash and initCode hash, requesting an upload of bytecode on other chain. - Address computation matches L2DeployManager behavior exactly, guaranteeing identical contract addresses across all supported networks. - The contract serves as the canonical L1 coordinator for the BytecodeRepository ecosystem, bridging audited bytecode storage with multi-chain deployment execution. diff --git a/docs/L2DeployManager.md b/docs/L2DeployManager.md index 035c7d7..5b961e4 100644 --- a/docs/L2DeployManager.md +++ b/docs/L2DeployManager.md @@ -14,7 +14,7 @@ This contract receives audited bytecode from L1 via Chainlink CCIP and enables s 3. Query stored bytecode availability to verify cross-chain synchronization status and plan deployments. 4. Retrieve bytecode for custom deployment logic or verification purposes through the BytecodeProvider interface. - The contract automatically handles: - 1. CCIP message reception and validation from trusted L1DeployManager to ensure bytecode authenticity and prevent malicious injections. + 1. CCIP message reception and validation from trusted L1DeployManager to ensure init code hash authenticity and prevent malicious injections. 2. Bytecode storage using SSTORE2 with automatic chunking for large contracts exceeding network gas limits or size constraints. 3. Address computation using identical salt generation as L1DeployManager, guaranteeing cross-chain address consistency. 4. Integration with factory contracts via BytecodeProvider interface for specialized deployment patterns and protocol-specific logic. @@ -55,6 +55,14 @@ address localTimelock The address of local timelock. +### bytecodeRequested + +```solidity +mapping(bytes32 => bytes32) bytecodeRequested +``` + +Hash of bytecode, which should be uploaded. If non-empty, an init code which generates the exact hash must be uploaded. + ### developerUntil ```solidity @@ -96,6 +104,25 @@ Bytecode will be deployed through the appropriate Factory if it is set. Otherwis | _salt | bytes32 | A value necessary to generate a unique salt for Create2. | | _constructorParams | bytes | parameters necessary to deploy a specified contract. | +### uploadBytecode + +```solidity +function uploadBytecode(struct Types.BytecodeVersion _bytecodeVersion, bytes _initCode) external +``` + +Uploads init code for a requested bytecode version. + +_Uploading must first be requested via L1 through CCIP. +The hash of init code must match the hash stored in _ccipReceive. +Anyone can upload bytecode as long as valid init code is provided._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _bytecodeVersion | struct Types.BytecodeVersion | Version of bytecode for which to upload. | +| _initCode | bytes | Valid init code matching stored init code hash to upload. | + ### getVerifiedBytecode ```solidity @@ -182,5 +209,5 @@ _The sender of the message from Ethereum must be L1DeployManager._ | Name | Type | Description | | ---- | ---- | ----------- | -| any2EvmMessage | struct Client.Any2EVMMessage | params necessary for the cross-chain message. Data contains bytecode hash and its bytecode for SEND_BYTECODE. and address of developer for BECOME_DEVELOPER or REVOKE_DEVELOPER. | +| any2EvmMessage | struct Client.Any2EVMMessage | params necessary for the cross-chain message. Data contains bytecode hash and its init code hash for SEND_BYTECODE. and address of developer for BECOME_DEVELOPER or REVOKE_DEVELOPER. | diff --git a/docs/VersionController.md b/docs/VersionController.md index 9df68f2..39966d7 100644 --- a/docs/VersionController.md +++ b/docs/VersionController.md @@ -698,6 +698,28 @@ _Throws and error if bytecode is not verified by at least one auditor._ | ---- | ---- | ----------- | | [0] | bytes | A bytecode of specified contract type and version. | +### getVerifiedInitCodeHash + +```solidity +function getVerifiedInitCodeHash(struct Types.BytecodeVersion _version) external view returns (bytes32) +``` + +Returns the hash of a verified bytecode of a specified contract type and version. + +_Throws and error if bytecode is not verified by at least one auditor._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _version | struct Types.BytecodeVersion | A bytecode version for which to return init code hash. | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | Init code hash of specified contract type and version. | + ### getAllAlternativeVersions ```solidity diff --git a/docs/interfaces/IL1DeployManager.md b/docs/interfaces/IL1DeployManager.md index 8cadb97..179eede 100644 --- a/docs/interfaces/IL1DeployManager.md +++ b/docs/interfaces/IL1DeployManager.md @@ -62,12 +62,6 @@ error OnlyDeveloper() error OnlyDeveloperOrGovernor() ``` -### BytecodeAlreadySent - -```solidity -error BytecodeAlreadySent(uint256 _chainId, bytes32 _bytecodeHash) -``` - ### ZeroAddress ```solidity diff --git a/docs/interfaces/IL2DeployManager.md b/docs/interfaces/IL2DeployManager.md index 8a90c83..26a4d14 100644 --- a/docs/interfaces/IL2DeployManager.md +++ b/docs/interfaces/IL2DeployManager.md @@ -2,10 +2,16 @@ ## IL2DeployManager -### BytecodeReceived +### BytecodeRequested ```solidity -event BytecodeReceived(bytes32 _messageId, bytes32 _bytecodeHash) +event BytecodeRequested(bytes32 _messageId, bytes32 _bytecodeHash, bytes32 _initCodeHash) +``` + +### BytecodeUploaded + +```solidity +event BytecodeUploaded(bytes32 _bytecodeHash) ``` ### ContractDeployed @@ -56,6 +62,24 @@ error OnlyDeveloperOrGovernor() error ZeroAddress() ``` +### BytecodeAlreadyUploaded + +```solidity +error BytecodeAlreadyUploaded(bytes32 _bytecodeHash) +``` + +### InvalidBytecode + +```solidity +error InvalidBytecode(bytes32 _bytecodeHash, bytes32 _initCodeHash) +``` + +### BytecodeNotRequested + +```solidity +error BytecodeNotRequested(bytes32 _bytecodeHash) +``` + ### MessageType A type of message to receive from Ethereum. diff --git a/docs/interfaces/IVersionController.md b/docs/interfaces/IVersionController.md index 1db9c7c..1e6a3ac 100644 --- a/docs/interfaces/IVersionController.md +++ b/docs/interfaces/IVersionController.md @@ -188,6 +188,12 @@ error AdminCantAddSubDevs() error ConflictingRoles(address _account) ``` +### NotGovernorOrGuadian + +```solidity +error NotGovernorOrGuadian(address _account) +``` + ### assignDeveloperForContractTypes ```solidity @@ -308,3 +314,9 @@ function versionExists(bytes32 _bytecodeHash) external view returns (bool) function getAllAlternativeVersions(bytes32 _contractType) external view returns (struct Types.VersionWithAlternative[]) ``` +### getVerifiedInitCodeHash + +```solidity +function getVerifiedInitCodeHash(struct Types.BytecodeVersion _version) external view returns (bytes32) +``` + diff --git a/scripts/config/networkConfig.ts b/scripts/config/networkConfig.ts index 3e01448..c2280c6 100644 --- a/scripts/config/networkConfig.ts +++ b/scripts/config/networkConfig.ts @@ -58,7 +58,7 @@ export const NETWORK_CONFIGS: Record = { destinationChainSelector: "4051577828743386545", // Polygon CCIP selector timelock: "0xCC3E7c85Bb0EE4f09380e041fee95a0caeDD4a02", // Polygon Timelock cometProxyAdmin: "0xd712ACe4ca490D4F3E92992Ecf3DE12251b975F9", - assetListFactory: "0xF372E84282FD0F5c631076aD8b9Da6B901E53c78", + assetListFactory: "0x62623C1374D12F946a9CA8597a137BbfBE015665", l1DeployManager: "0x1234567890123456789012345678901234567890" // Will be set from L1 deployment }, @@ -86,18 +86,6 @@ export const NETWORK_CONFIGS: Record = { l1DeployManager: "0x1234567890123456789012345678901234567890" // Will be set from L1 deployment }, - ronin: { - name: "ronin", - chainId: 2020, - ccipRouter: "0x46527571D5D1B68eE7Eb60B18A32e6C60DcEAf99", - sourceChainSelector: "5009297550715157269", // Ethereum Mainnet CCIP selector - destinationChainSelector: "6916147374840168594", // Ronin CCIP selector - timelock: "0xBbb0Ebd903fafbb8fFF58B922fD0CD85E251ac2c", - cometProxyAdmin: "0xfa64A82a3d13D4c05d5133E53b2EbB8A0FA9c3F6", - assetListFactory: "0x84fc63de5d127e9c074c1da6591ee8fa70a60de1", - l1DeployManager: "0x1234567890123456789012345678901234567890" // Will be set from L1 deployment - }, - unichain: { name: "unichain", chainId: 130, @@ -118,7 +106,7 @@ export const NETWORK_CONFIGS: Record = { destinationChainSelector: "1556008542357238666", // Mantle CCIP selector timelock: "0x16C7B5C1b10489F4B111af11de2Bd607c9728107", // Mantle Timelock cometProxyAdmin: "0xe268B436E75648aa0639e2088fa803feA517a0c7", - assetListFactory: "0x0dAf7A2772C84A82D1D46a4b628151e6D7F5b202", + assetListFactory: "0xB88e4078AAc88F10C0Ca71086ddCF512Ec54498a", l1DeployManager: "0x1234567890123456789012345678901234567890" // Will be set from L1 deployment }, diff --git a/scripts/deployL1.ts b/scripts/deployL1.ts index 46b7c83..6c182a9 100644 --- a/scripts/deployL1.ts +++ b/scripts/deployL1.ts @@ -13,7 +13,7 @@ import { DeploymentManager, waitForConfirmations, logDeploymentStep } from "./ut * * Production Configuration: * - Governor: 0x6d903f6003cca6255D85CcA4D3B5E5146dC33925 (Ethereum Mainnet Timelock) - * - Guardian: 0x7d903f6003cca6255D85CcA4D3B5E5146dC33926 (Guardian for cooldown resets) + * - Guardian: 0xbbf3f1421D886E9b2c5D716B5192aC998af2012c (Guardian for cooldown resets) * - CCIP Router: 0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D (Ethereum Mainnet CCIP Router) * * Usage: @@ -65,117 +65,117 @@ async function main() { const deployedContracts: Record = {}; try { - // // 1. Deploy VersionController (upgradeable) - // logDeploymentStep(1, 4, "Deploying VersionController (upgradeable)..."); - // const VersionController = await ethers.getContractFactory("VersionController"); - - // console.log("Deploying proxy and implementation..."); - // const versionController = await upgrades.deployProxy( - // VersionController, - // [initialAdmin, GUARDIAN_ADDRESS], // initializer arguments - // { - // initializer: "initialize", - // kind: "uups" - // } - // ); - - // // Wait for deployment - // await versionController.waitForDeployment(); - // const deploymentTx = versionController.deploymentTransaction(); - // if (deploymentTx) { - // await waitForConfirmations(deploymentTx, 1, "VersionController"); - // } - - // const versionControllerAddress = await versionController.getAddress(); - // deployedContracts.VersionController = versionControllerAddress; - - // // Save deployment artifact - // await deploymentManager.saveDeployment( - // "VersionController", - // versionController, - // deploymentTx, - // [initialAdmin, GUARDIAN_ADDRESS], - // true // isUpgradeable - // ); - - // const versionControllerImplAddress = await upgrades.erc1967.getImplementationAddress(versionControllerAddress); - // console.log("VersionController Proxy:", versionControllerAddress); - // console.log("VersionController Implementation:", versionControllerImplAddress); - - // // 2. Deploy L1DeployManager (upgradeable) - // logDeploymentStep(2, 4, "Deploying L1DeployManager (upgradeable)..."); - // const L1DeployManager = await ethers.getContractFactory("L1DeployManager"); - - // console.log("Deploying proxy and implementation..."); - // const l1DeployManager = await upgrades.deployProxy( - // L1DeployManager, - // [], // L1DeployManager.initialize() takes no parameters - // { - // initializer: "initialize", - // kind: "uups", - // constructorArgs: [versionControllerAddress, CCIP_ROUTER_ADDRESS] // Constructor arguments for immutable variables - // } - // ); - - // // Wait for deployment - // await l1DeployManager.waitForDeployment(); - // const l1DeploymentTx = l1DeployManager.deploymentTransaction(); - // if (l1DeploymentTx) { - // await waitForConfirmations(l1DeploymentTx, 1, "L1DeployManager"); - // } - - // const l1DeployManagerAddress = await l1DeployManager.getAddress(); - // deployedContracts.L1DeployManager = l1DeployManagerAddress; - - // // Save deployment artifact - // await deploymentManager.saveDeployment( - // "L1DeployManager", - // l1DeployManager, - // l1DeploymentTx, - // [versionControllerAddress, CCIP_ROUTER_ADDRESS], - // true // isUpgradeable - // ); - - // const l1DeployManagerImplAddress = await upgrades.erc1967.getImplementationAddress(l1DeployManagerAddress); - // console.log("L1DeployManager Proxy:", l1DeployManagerAddress); - // console.log("L1DeployManager Implementation:", l1DeployManagerImplAddress); - // console.log(""); - - // // 3. Deploy MarketFactory (non-upgradeable) - // logDeploymentStep(3, 4, "Deploying MarketFactory..."); - // const MarketFactory = await ethers.getContractFactory("MarketFactory"); - - // const marketFactoryArgs = [ - // versionControllerAddress, // bytecodeProvider - // COMET_PROXY_ADMIN, // cometProxyAdmin - // ASSET_LIST_FACTORY, // assetListFactory - // GOVERNOR_ADDRESS // timelock (using governor as timelock) - // ]; - - // console.log("Deploying contract..."); - // const marketFactory = await MarketFactory.deploy(...marketFactoryArgs); - - // // Wait for deployment - // await marketFactory.waitForDeployment(); - // const marketFactoryTx = marketFactory.deploymentTransaction(); - // if (marketFactoryTx) { - // await waitForConfirmations(marketFactoryTx, 1, "MarketFactory"); - // } - - // const marketFactoryAddress = await marketFactory.getAddress(); - // deployedContracts.MarketFactory = marketFactoryAddress; - - // // Save deployment artifact - // await deploymentManager.saveDeployment( - // "MarketFactory", - // marketFactory, - // marketFactoryTx, - // marketFactoryArgs, - // false // isUpgradeable - // ); - - // console.log("MarketFactory:", marketFactoryAddress); - // console.log(""); + // 1. Deploy VersionController (upgradeable) + logDeploymentStep(1, 4, "Deploying VersionController (upgradeable)..."); + const VersionController = await ethers.getContractFactory("VersionController"); + + console.log("Deploying proxy and implementation..."); + const versionController = await upgrades.deployProxy( + VersionController, + [initialAdmin, GUARDIAN_ADDRESS], // initializer arguments + { + initializer: "initialize", + kind: "uups" + } + ); + + // Wait for deployment + await versionController.waitForDeployment(); + const deploymentTx = versionController.deploymentTransaction(); + if (deploymentTx) { + await waitForConfirmations(deploymentTx, 1, "VersionController"); + } + + const versionControllerAddress = await versionController.getAddress(); + deployedContracts.VersionController = versionControllerAddress; + + // Save deployment artifact + await deploymentManager.saveDeployment( + "VersionController", + versionController, + deploymentTx, + [initialAdmin, GUARDIAN_ADDRESS], + true // isUpgradeable + ); + + const versionControllerImplAddress = await upgrades.erc1967.getImplementationAddress(versionControllerAddress); + console.log("VersionController Proxy:", versionControllerAddress); + console.log("VersionController Implementation:", versionControllerImplAddress); + + // 2. Deploy L1DeployManager (upgradeable) + logDeploymentStep(2, 4, "Deploying L1DeployManager (upgradeable)..."); + const L1DeployManager = await ethers.getContractFactory("L1DeployManager"); + + console.log("Deploying proxy and implementation..."); + const l1DeployManager = await upgrades.deployProxy( + L1DeployManager, + [], // L1DeployManager.initialize() takes no parameters + { + initializer: "initialize", + kind: "uups", + constructorArgs: [versionControllerAddress, CCIP_ROUTER_ADDRESS] // Constructor arguments for immutable variables + } + ); + + // Wait for deployment + await l1DeployManager.waitForDeployment(); + const l1DeploymentTx = l1DeployManager.deploymentTransaction(); + if (l1DeploymentTx) { + await waitForConfirmations(l1DeploymentTx, 1, "L1DeployManager"); + } + + const l1DeployManagerAddress = await l1DeployManager.getAddress(); + deployedContracts.L1DeployManager = l1DeployManagerAddress; + + // Save deployment artifact + await deploymentManager.saveDeployment( + "L1DeployManager", + l1DeployManager, + l1DeploymentTx, + [versionControllerAddress, CCIP_ROUTER_ADDRESS], + true // isUpgradeable + ); + + const l1DeployManagerImplAddress = await upgrades.erc1967.getImplementationAddress(l1DeployManagerAddress); + console.log("L1DeployManager Proxy:", l1DeployManagerAddress); + console.log("L1DeployManager Implementation:", l1DeployManagerImplAddress); + console.log(""); + + // 3. Deploy MarketFactory (non-upgradeable) + logDeploymentStep(3, 4, "Deploying MarketFactory..."); + const MarketFactory = await ethers.getContractFactory("MarketFactory"); + + const marketFactoryArgs = [ + versionControllerAddress, // bytecodeProvider + COMET_PROXY_ADMIN, // cometProxyAdmin + ASSET_LIST_FACTORY, // assetListFactory + GOVERNOR_ADDRESS // timelock (using governor as timelock) + ]; + + console.log("Deploying contract..."); + const marketFactory = await MarketFactory.deploy(...marketFactoryArgs); + + // Wait for deployment + await marketFactory.waitForDeployment(); + const marketFactoryTx = marketFactory.deploymentTransaction(); + if (marketFactoryTx) { + await waitForConfirmations(marketFactoryTx, 1, "MarketFactory"); + } + + const marketFactoryAddress = await marketFactory.getAddress(); + deployedContracts.MarketFactory = marketFactoryAddress; + + // Save deployment artifact + await deploymentManager.saveDeployment( + "MarketFactory", + marketFactory, + marketFactoryTx, + marketFactoryArgs, + false // isUpgradeable + ); + + console.log("MarketFactory:", marketFactoryAddress); + console.log(""); // 4. Deploy CometFactoryV2 (non-upgradeable) logDeploymentStep(4, 4, "Deploying CometFactoryV2..."); diff --git a/scripts/deployL2.ts b/scripts/deployL2.ts index 5adb9c7..f8fa434 100644 --- a/scripts/deployL2.ts +++ b/scripts/deployL2.ts @@ -145,7 +145,7 @@ async function loadDeploymentConfig(): Promise { }, alternative: "" }, - withAssetList: false // Set to true if extended asset list support is needed + withAssetList: true // Set to true if extended asset list support is needed }; console.log(" Deployment Configuration:"); diff --git a/scripts/renounceAdminRoleL1.ts b/scripts/renounceAdminRoleL1.ts new file mode 100644 index 0000000..e68ecdd --- /dev/null +++ b/scripts/renounceAdminRoleL1.ts @@ -0,0 +1,186 @@ +/** + * L1 Admin Role Renunciation Script + * + * Final step of the L1 deployment lifecycle. Renounces the deployer's DEFAULT_ADMIN_ROLE + * on VersionController so that only the permanent governor retains admin authority. + * + * Run AFTER: + * 1. deployL1.ts - deploys VersionController + L1DeployManager + factories + * 2. deployL2.ts - deploys L2DeployManager on each L2 (per-chain) + * 3. setConfigsL1.ts - configures L2 chains, grants DEFAULT_ADMIN_ROLE to governor, + * assigns key developer, grants auditor roles + * + * Pre-flight checks (abort if any fail): + * - Network is Ethereum Mainnet (chain ID 1) + * - Permanent governor currently holds DEFAULT_ADMIN_ROLE on VersionController + * (CRITICAL: without this check, renouncing could leave the system with no admin) + * - DEFAULT_ADMIN_ROLE has at least 2 holders (deployer + governor at minimum) + * + * Idempotency: + * - If the deployer no longer has DEFAULT_ADMIN_ROLE, the script exits successfully + * without sending a transaction. + * + * Post-state verification: + * - Deployer no longer holds DEFAULT_ADMIN_ROLE + * - Governor still holds DEFAULT_ADMIN_ROLE + * + * IMPORTANT: This action is IRREVERSIBLE. After execution, only the governor (a smart + * contract / timelock) can grant DEFAULT_ADMIN_ROLE going forward. + * + * Usage: + * ```bash + * npx hardhat run scripts/renounceAdminRoleL1.ts --network mainnet + * ``` + * + * NOTE: PRODUCTION ONLY (Ethereum Mainnet, chain ID 1). + */ + +import { ethers } from "hardhat"; +import fs from "fs"; +import path from "path"; + +// Must match the value used in setConfigsL1.ts +const L1_GOVERNOR_ADDRESS = "0x6d903f6003cca6255D85CcA4D3B5E5146dC33925"; // Permanent governor + +const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; + +async function main() { + console.log("L1 ADMIN ROLE RENUNCIATION SCRIPT"); + console.log("═".repeat(60)); + + const network = await ethers.provider.getNetwork(); + const [deployer] = await ethers.getSigners(); + + console.log(`\nNetwork: ${network.name} (Chain ID: ${network.chainId})`); + console.log(`Deployer: ${deployer.address}`); + console.log(`Permanent Governor: ${L1_GOVERNOR_ADDRESS}`); + console.log(""); + + if (network.chainId !== 1n) { + throw new Error( + `Unsupported network with chain ID: ${network.chainId}. ` + + `This script is designed for production deployment on Ethereum Mainnet only (chain ID 1).` + ); + } + + try { + const deploymentPath = path.join(__dirname, "..", "deployments", network.name, "deployments.json"); + if (!fs.existsSync(deploymentPath)) { + throw new Error(`No deployments found for network: ${network.name}`); + } + + const deployments = JSON.parse(fs.readFileSync(deploymentPath, "utf8")); + const versionControllerAddress = deployments.contracts?.VersionController?.address; + if (!versionControllerAddress) { + throw new Error("VersionController not found in deployments"); + } + + console.log(`VersionController: ${versionControllerAddress}`); + console.log(""); + + const versionController = await ethers.getContractAt("VersionController", versionControllerAddress); + + // Idempotency check: if deployer already renounced, exit successfully + console.log("Step 1: Checking deployer's current role..."); + const deployerHasRole = await versionController.hasRole(DEFAULT_ADMIN_ROLE, deployer.address); + if (!deployerHasRole) { + console.log(" ā„¹ļø Deployer does not hold DEFAULT_ADMIN_ROLE — nothing to renounce."); + console.log(""); + + // Still verify governor has the role for operator confidence + const governorHasRole = await versionController.hasRole(DEFAULT_ADMIN_ROLE, L1_GOVERNOR_ADDRESS); + if (!governorHasRole) { + throw new Error(`CRITICAL: Governor ${L1_GOVERNOR_ADDRESS} does NOT have DEFAULT_ADMIN_ROLE!`); + } + console.log(" āœ“ Governor retains DEFAULT_ADMIN_ROLE — system is in expected state."); + return; + } + console.log(" āœ“ Deployer currently holds DEFAULT_ADMIN_ROLE"); + console.log(""); + + // CRITICAL pre-flight: governor must hold the role before deployer renounces + console.log("Step 2: Verifying permanent governor holds DEFAULT_ADMIN_ROLE..."); + const governorHasRole = await versionController.hasRole(DEFAULT_ADMIN_ROLE, L1_GOVERNOR_ADDRESS); + if (!governorHasRole) { + throw new Error( + `CRITICAL: Governor ${L1_GOVERNOR_ADDRESS} does NOT have DEFAULT_ADMIN_ROLE.\n` + + `Renouncing now would brick admin access. Run setConfigsL1.ts first to grant the governor admin.` + ); + } + console.log(` āœ“ Governor holds DEFAULT_ADMIN_ROLE: ${L1_GOVERNOR_ADDRESS}`); + console.log(""); + + // Enumerate all current admin holders for transparency + console.log("Step 3: Enumerating current DEFAULT_ADMIN_ROLE holders..."); + const adminCount = await versionController.getRoleMemberCount(DEFAULT_ADMIN_ROLE); + if (adminCount < 2n) { + throw new Error( + `CRITICAL: Only ${adminCount} account(s) hold DEFAULT_ADMIN_ROLE. ` + + `Renouncing would leave the system with no admin.` + ); + } + const admins: string[] = []; + for (let i = 0n; i < adminCount; i++) { + admins.push(await versionController.getRoleMember(DEFAULT_ADMIN_ROLE, i)); + } + console.log(` Current admins (${adminCount}):`); + admins.forEach((addr) => { + const tag = + addr.toLowerCase() === L1_GOVERNOR_ADDRESS.toLowerCase() + ? " (governor)" + : addr.toLowerCase() === deployer.address.toLowerCase() + ? " (deployer — to be renounced)" + : " (unexpected — review before proceeding)"; + console.log(` - ${addr}${tag}`); + }); + console.log(""); + + // Renounce + console.log("Step 4: Renouncing deployer's DEFAULT_ADMIN_ROLE..."); + console.log("āš ļø WARNING: This action is IRREVERSIBLE"); + console.log(` Deployer: ${deployer.address}`); + + const renounceTx = await versionController.renounceRole(DEFAULT_ADMIN_ROLE, deployer.address); + console.log(` Transaction hash: ${renounceTx.hash}`); + const receipt = await renounceTx.wait(1); + console.log(` āœ“ Confirmed in block ${receipt?.blockNumber}`); + console.log(""); + + // Post-state verification + console.log("Step 5: Verifying post-renunciation state..."); + const stillHasRole = await versionController.hasRole(DEFAULT_ADMIN_ROLE, deployer.address); + if (stillHasRole) { + throw new Error("Role renunciation failed — deployer still has DEFAULT_ADMIN_ROLE"); + } + console.log(" āœ“ Deployer no longer has DEFAULT_ADMIN_ROLE"); + + const governorStillHasRole = await versionController.hasRole(DEFAULT_ADMIN_ROLE, L1_GOVERNOR_ADDRESS); + if (!governorStillHasRole) { + throw new Error(`CRITICAL: Governor ${L1_GOVERNOR_ADDRESS} no longer has DEFAULT_ADMIN_ROLE!`); + } + console.log(` āœ“ Governor retains DEFAULT_ADMIN_ROLE: ${L1_GOVERNOR_ADDRESS}`); + console.log(""); + + console.log("═".repeat(60)); + console.log("šŸŽ‰ ADMIN ROLE RENUNCIATION COMPLETED SUCCESSFULLY!"); + console.log("═".repeat(60)); + console.log(""); + console.log(`Deployer ${deployer.address} no longer has admin authority on VersionController.`); + console.log(`Going forward, only the governor (${L1_GOVERNOR_ADDRESS}) can grant DEFAULT_ADMIN_ROLE.`); + console.log(""); + } catch (error) { + console.error("\nāŒ Renunciation failed:", error); + process.exit(1); + } +} + +process.on("unhandledRejection", (reason, promise) => { + console.error("Unhandled Rejection at:", promise, "reason:", reason); + process.exit(1); +}); + +if (require.main === module) { + main(); +} + +export default main; diff --git a/scripts/setConfigsL1.ts b/scripts/setConfigsL1.ts index e85d0e2..19d96af 100644 --- a/scripts/setConfigsL1.ts +++ b/scripts/setConfigsL1.ts @@ -5,10 +5,13 @@ * - Configures L2 chain settings in L1DeployManager * - Assigns key developers to contract types * - Grants auditor roles - * - Transfers admin role to permanent governor + * - Grants DEFAULT_ADMIN_ROLE to the permanent governor * * IMPORTANT: This script requires the deployer to have DEFAULT_ADMIN_ROLE in VersionController. - * The deployer receives this role during initial deployment (deployL1.ts). + * The deployer receives this role during initial deployment (deployL1.ts). The deployer KEEPS + * this role after this script runs so that configuration can be re-run or extended (e.g. add a + * new auditor) without going through the governor. Renunciation is performed separately by + * `scripts/renounceAdminRoleL1.ts` once the operator has verified the on-chain state. * * Flow: * 1. Validate developer and auditor addresses (warn about placeholders) @@ -20,15 +23,15 @@ * 7. Assign key developer to all 12 contract types * 8. Grant AUDITOR_ROLE to configured auditors * 9. Display role assignment summary - * 10. Renounce DEFAULT_ADMIN_ROLE from deployer (permanent governor retains role) * * Role Management: - * - Deployer starts with DEFAULT_ADMIN_ROLE (granted in deployL1.ts) + * - Deployer starts with DEFAULT_ADMIN_ROLE (granted in deployL1.ts) and retains it after this script * - This script grants DEFAULT_ADMIN_ROLE to permanent governor * - One key developer is assigned to all contract types and receives KEY_DEVELOPER_ROLE * - Auditors receive AUDITOR_ROLE for bytecode verification - * - Deployer renounces their DEFAULT_ADMIN_ROLE (unless deployer IS the governor) - * - Final state: Only permanent governor has DEFAULT_ADMIN_ROLE + * - Deployer's DEFAULT_ADMIN_ROLE is renounced by a separate script (renounceAdminRoleL1.ts) + * once the system has been verified + * - Final state (after renounceAdminRoleL1.ts): Only permanent governor has DEFAULT_ADMIN_ROLE * * Contract Types (all assigned to single key developer): * - CometWithAssetList, CometExtWithAssetList @@ -40,7 +43,7 @@ * * BEFORE RUNNING: * 1. Update KEY_DEVELOPER address (replace placeholder 0x1234567890123456789012345678901234567890) - * 2. Update auditor addresses (AUDITOR_1, AUDITOR_2) + * 2. Update auditor addresses in the AUDITORS array * 3. Ensure L2 contracts are already deployed * 4. Verify deployer has sufficient gas * @@ -67,35 +70,28 @@ const L1_GOVERNOR_ADDRESS = "0x6d903f6003cca6255D85CcA4D3B5E5146dC33925"; // Per // TODO: Replace with actual developer address before deployment const KEY_DEVELOPER = "0x1234567890123456789012345678901234567890"; // Main key developer for all contract types -// Auditor addresses -// TODO: Replace with actual auditor addresses before deployment -const AUDITOR_1 = "0x7234567890123456789012345678901234567890"; // Primary auditor +// Auditors to grant AUDITOR_ROLE. +// TODO: Replace placeholder addresses with actual auditor addresses before deployment. +// Add or remove entries as needed. +const AUDITORS = [ + { address: "0x7234567890123456789012345678901234567890", name: "Certora Auditor" }, + { address: "0x7234567890123456789012345678901234567890", name: "Secondary Auditor" } +]; // All contract types to be assigned to the key developer const CONTRACT_TYPES = [ - // Comet contracts + // Comet contracts (Service patch) "CometWithAssetList", "CometExtWithAssetList", - // Governance - "CompoundGovernor", - - // Core system + // Core BR system "VersionController", "L1DeployManager", "L2DeployManager", // Factories "MarketFactory", - "CometFactoryV2", - - // Streaming - "Streamer", - "StreamerFactory", - - // Utilities - "CometMultiplier", - "CometCollateralSwap" + "CometFactoryV2" // CAPO //"ChainlinkCorrelatedAssetsPriceOracle", @@ -105,9 +101,6 @@ const CONTRACT_TYPES = [ //"WstETHCorrelatedAssetsPriceOracle" ]; -// Auditors to grant roles -const AUDITORS = [{ address: AUDITOR_1, name: "Certora Auditor" }]; - // ChainConfig struct matches IL1DeployManager.sol:41-44 interface ChainConfig { l2DeployManager: string; @@ -182,7 +175,7 @@ async function validateDeployerRole(versionController: any, deployerAddress: str } /** - * Validate governor retains DEFAULT_ADMIN_ROLE after renunciation + * Validate governor holds DEFAULT_ADMIN_ROLE */ async function validateGovernorRole(versionController: any, governorAddress: string): Promise { const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; @@ -501,7 +494,7 @@ async function main() { } console.log(""); - // Validate governor has role before deployer renounces + // Validate governor now holds the admin role console.log("Step 5: Validating permanent governor role..."); await validateGovernorRole(versionController, L1_GOVERNOR_ADDRESS); console.log(""); @@ -516,30 +509,6 @@ async function main() { // Display role assignments summary await displayRolesSummary(versionController); - // Renounce deployer's DEFAULT_ADMIN_ROLE - console.log("Step 9: Renouncing deployer's DEFAULT_ADMIN_ROLE..."); - console.log("āš ļø WARNING: This action is IRREVERSIBLE"); - - console.log(` Deployer: ${deployer.address}`); - console.log(` Renouncing role: DEFAULT_ADMIN_ROLE`); - - const renounceTx = await versionController.renounceRole(DEFAULT_ADMIN_ROLE, deployer.address); - console.log(` Transaction hash: ${renounceTx.hash}`); - - const renounceReceipt = await renounceTx.wait(1); - console.log(` āœ“ Confirmed in block ${renounceReceipt?.blockNumber}`); - - // Verify deployer no longer has role - const stillHasRole = await versionController.hasRole(DEFAULT_ADMIN_ROLE, deployer.address); - if (stillHasRole) { - throw new Error("Role renunciation failed - deployer still has DEFAULT_ADMIN_ROLE"); - } - console.log(" āœ“ Deployer no longer has DEFAULT_ADMIN_ROLE"); - - // Final validation that governor retains role - await validateGovernorRole(versionController, L1_GOVERNOR_ADDRESS); - - console.log(""); console.log("═".repeat(60)); console.log("šŸŽ‰ L1 CHAIN CONFIGURATION COMPLETED SUCCESSFULLY!"); console.log("═".repeat(60)); @@ -549,18 +518,14 @@ async function main() { console.log(` - Permanent Governor has DEFAULT_ADMIN_ROLE: ${L1_GOVERNOR_ADDRESS}`); console.log(` - Assigned key developer to ${CONTRACT_TYPES.length} contract type(s)`); console.log(` - Granted AUDITOR_ROLE to ${AUDITORS.length} auditor(s)`); - - if (deployer.address.toLowerCase() !== L1_GOVERNOR_ADDRESS.toLowerCase()) { - console.log(` - Deployer role renounced: ${deployer.address}`); - } else { - console.log(` - Deployer IS permanent governor (role retained)`); - } + console.log(` - Deployer ${deployer.address} retains DEFAULT_ADMIN_ROLE`); + console.log(" (renounce separately via scripts/renounceAdminRoleL1.ts after verification)"); console.log(""); - console.log("Role Management Complete:"); + console.log("Role Management Status:"); console.log(" āœ“ Permanent governor has DEFAULT_ADMIN_ROLE"); - console.log(" āœ“ Deployer admin role properly transferred/renounced"); console.log(" āœ“ Key developer assigned to all contract types"); console.log(" āœ“ Auditor roles granted"); + console.log(" ⚠ Deployer admin role NOT yet renounced — run renounceAdminRoleL1.ts when ready"); console.log(""); console.log("Next Steps:"); console.log(" 1. Verify L2 chain configurations:"); @@ -574,6 +539,8 @@ async function main() { console.log(" 5. Developers can now upload bytecode for their assigned contract types"); console.log(" 6. Auditors can verify uploaded bytecode with EIP-712 signatures"); console.log(" 7. Test cross-chain bytecode transmission"); + console.log(" 8. Once the system is verified, renounce deployer admin:"); + console.log(` npx hardhat run scripts/renounceAdminRoleL1.ts --network mainnet`); console.log(""); } catch (error) { console.error("\nāŒ Configuration failed:", error); diff --git a/test/CCIPGasMeasurement.ts b/test/CCIPGasMeasurement.ts new file mode 100644 index 0000000..ec2566c --- /dev/null +++ b/test/CCIPGasMeasurement.ts @@ -0,0 +1,179 @@ +import { ethers, upgrades } from "hardhat"; +import { loadFixture, time } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { + CometInitCode, + CometExtInitCode, + CometWithExtendedAssetListInitCode, + CometExtAssetList +} from "./testData.json"; +import { prepareAuditReportSignature } from "./helpers"; +import type { Developers } from "./helpers"; + +/** + * Measures the gas consumed on the destination chain by L2DeployManager._ccipReceive + * when receiving a SEND_BYTECODE message. The MockCCIPRouter executes the receive + * locally via CallWithExactGas and emits MsgExecuted(success, retData, gasUsed), + * which is exactly the gas budget that real CCIP would bill against the configured + * gasLimit on the destination chain. + * + * Run: pnpm hardhat test test/CCIPGasMeasurement.ts + */ + +const mockRouterFee = ethers.parseEther("0.1"); +const sepoliaSelector = "16015286601757825753"; +const mockChainSelectorId = "1234567890"; +const mockOtherChainId = 123456; +// Probe budget for the inner _ccipReceive call. Must satisfy: +// gasleft() at CallWithExactGas time >= PROBE_GAS_LIMIT + GAS_FOR_CALL_EXACT_CHECK (5k) +// The L1 tx itself burns a few hundred-k gas before reaching that point (encoding the +// initCode, fee calc, etc.), and Hardhat's default block gas limit is 30M, so we cap +// the probe well below 30M and override the tx gasLimit to the block max. +const PROBE_GAS_LIMIT = 10_000_000; +const TX_GAS_LIMIT = 29_900_000; + +interface BytecodeCase { + label: string; + initCode: string; +} + +const CASES: BytecodeCase[] = [ + { label: "CometWithAssetList ", initCode: CometWithExtendedAssetListInitCode }, + { label: "CometExtWithAssetList", initCode: CometExtAssetList }, + { label: "Comet (no asset list)", initCode: CometInitCode }, + { label: "CometExt (legacy) ", initCode: CometExtInitCode } +]; + +describe("CCIP gas measurement — sendBytecodeToOtherChain", function () { + const fixture = async () => { + const signers = await ethers.getSigners(); + const governor = signers[0]; + const guardian = signers[1]; + const auditor = signers[2]; + const WOOF: Developers = { + keyDeveloper: signers[5], + subDevelopers: signers.slice(6, 9), + // One contract type per case — keeps versioning simple + contractTypes: CASES.map((_, i) => ethers.encodeBytes32String(`CT_${i}`)) + }; + const localTimelockL2 = signers[9]; + + const versionController = await upgrades.deployProxy( + await ethers.getContractFactory("VersionController"), + [await governor.getAddress(), await guardian.getAddress()], + { kind: "uups" } + ); + + const AUDITOR_ROLE = await versionController.AUDITOR_ROLE(); + await versionController.connect(governor).grantRole(AUDITOR_ROLE, auditor); + + const KEY_DEVELOPER_ROLE = await versionController.KEY_DEVELOPER_ROLE(); + await versionController.connect(governor).grantRole(KEY_DEVELOPER_ROLE, WOOF.keyDeveloper); + + await versionController + .connect(governor) + .assignDeveloperForContractTypes(WOOF.contractTypes, WOOF.keyDeveloper); + + const mockRouter = await (await ethers.getContractFactory("MockCCIPRouter")).deploy(); + await mockRouter.setFee(mockRouterFee); + + const l1DeployManager = await upgrades.deployProxy(await ethers.getContractFactory("L1DeployManager"), [], { + kind: "uups", + constructorArgs: [await versionController.getAddress(), await mockRouter.getAddress()] + }); + + const l2DeployManager = await ( + await ethers.getContractFactory("L2DeployManager") + ).deploy(sepoliaSelector, l1DeployManager, mockRouter, localTimelockL2); + + await l1DeployManager.connect(governor).setChainConfig(mockOtherChainId, { + l2DeployManager: l2DeployManager, + destinationChainSelector: mockChainSelectorId + }); + + // Release + verify v1.0.0 of each contract type with its assigned bytecode + const URL = "https://example.com/source"; + const auditReportURL = "https://example.com/audit"; + + for (let i = 0; i < CASES.length; i++) { + const ct = WOOF.contractTypes[i]; + const initCode = CASES[i].initCode; + + await versionController.connect(WOOF.keyDeveloper).releaseBytecode({ + contractType: ct, + initCode, + sourceURL: URL + }); + + const version = { version: { major: 1, minor: 0, patch: 0 }, alternative: "" }; + const bytecodeHash = await versionController.computeBytecodeHash(ct, version); + const sig = await prepareAuditReportSignature( + bytecodeHash, + ethers.keccak256(initCode), + auditReportURL, + await versionController.getAddress(), + auditor + ); + await versionController + .connect(WOOF.keyDeveloper) + .verifyBytecode({ contractType: ct, version }, auditReportURL, sig); + } + + // Cooldown bypass not needed — releaseBytecode is initial release for each type + return { WOOF, l1DeployManager, l2DeployManager, mockRouter, versionController }; + }; + + const restore = async () => await loadFixture(fixture); + + it("Reports gas consumed by _ccipReceive for each bytecode size", async () => { + const { WOOF, l1DeployManager, mockRouter } = await restore(); + + const results: { label: string; bytes: number; gasUsed: bigint; recommended: bigint }[] = []; + const safetyBps = 2500n; // 25% safety margin + + for (let i = 0; i < CASES.length; i++) { + const ct = WOOF.contractTypes[i]; + const version = { version: { major: 1, minor: 0, patch: 0 }, alternative: "" }; + + const tx = await l1DeployManager + .connect(WOOF.keyDeveloper) + .sendBytecodeToOtherChain({ contractType: ct, version }, mockOtherChainId, PROBE_GAS_LIMIT, { + value: mockRouterFee, + gasLimit: TX_GAS_LIMIT + }); + const receipt = await tx.wait(); + + // Find MsgExecuted(bool success, bytes retData, uint256 gasUsed) from MockCCIPRouter + const iface = mockRouter.interface; + let gasUsed = 0n; + for (const log of receipt!.logs) { + if (log.address.toLowerCase() !== (await mockRouter.getAddress()).toLowerCase()) continue; + try { + const parsed = iface.parseLog({ topics: [...log.topics], data: log.data }); + if (parsed?.name === "MsgExecuted") { + gasUsed = parsed.args.gasUsed as bigint; + break; + } + } catch { + /* not a MockCCIPRouter event */ + } + } + + const bytes = (CASES[i].initCode.length - 2) / 2; + const recommended = (gasUsed * (10000n + safetyBps)) / 10000n; + results.push({ label: CASES[i].label, bytes, gasUsed, recommended }); + } + + // Pretty-print the report + console.log("\n ── CCIP _ccipReceive gas usage on L2 ──"); + console.log(" bytecode bytes gasUsed gasLimit (+25%)"); + console.log(" ───────────────────── ────── ───────── ────────────────"); + for (const r of results) { + console.log( + ` ${r.label} ${r.bytes.toString().padStart(6)} ${r.gasUsed.toString().padStart(9)} ${r.recommended + .toString() + .padStart(16)}` + ); + } + console.log(""); + }); +}); diff --git a/test/DeployManagers.ts b/test/DeployManagers.ts index 8226534..eedb9cd 100644 --- a/test/DeployManagers.ts +++ b/test/DeployManagers.ts @@ -12,7 +12,7 @@ const mockRouterFee = ethers.parseEther("0.1"); const sepoliaSelector = "16015286601757825753"; const mockChainSelectorId = "1234567890"; const mockOtherChainId = 123456; -const gasLimit = 5_000_000; +const gasLimit = 100_000; describe("L1/L2 DeployManager", function () { const fixture = async () => { @@ -83,6 +83,7 @@ describe("L1/L2 DeployManager", function () { version: { major: 1, minor: 0, patch: 0 }, alternative: "" }; + const initCodeHash_1_0_0 = ethers.keccak256(CometInitCode); const bytecodeHash_1_0_0 = await versionController.computeBytecodeHash(WOOF.contractTypes[0], version); const auditReport = "AUDIT_REPORT_URL"; const signature = await prepareAuditReportSignature( @@ -170,6 +171,7 @@ describe("L1/L2 DeployManager", function () { l2DeployManager, bytecodeVersion_1_0_0, bytecodeHash_1_0_0, + initCodeHash_1_0_0, mockBaseToken, mockCollateralToken, constantPriceFeedAddr, @@ -179,35 +181,32 @@ describe("L1/L2 DeployManager", function () { const restore = async () => await loadFixture(fixture); - it("Should send bytecode to other chain", async () => { - const { WOOF, l1DeployManager, l2DeployManager, versionController, bytecodeVersion_1_0_0, bytecodeHash_1_0_0 } = - await restore(); + it("Should send bytecode hash to other chain and create a request", async () => { + const { + WOOF, + l1DeployManager, + l2DeployManager, + versionController, + bytecodeVersion_1_0_0, + bytecodeHash_1_0_0, + initCodeHash_1_0_0 + } = await restore(); await l1DeployManager .connect(WOOF.keyDeveloper) .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }); const expectedBytecode = await versionController.getVerifiedBytecode(bytecodeVersion_1_0_0); - expect(await l2DeployManager.getVerifiedBytecode(bytecodeVersion_1_0_0)).to.equal(expectedBytecode); - expect(await l2DeployManager.versionExists(bytecodeVersion_1_0_0)).to.be.true; + // Bytecode should not be verified until uploadBytecode is called + await expect(l2DeployManager.getVerifiedBytecode(bytecodeVersion_1_0_0)).revertedWithCustomError( + l2DeployManager, + "BytecodeIsEmpty" + ); + expect(await l2DeployManager.versionExists(bytecodeVersion_1_0_0)).to.be.false; + expect(await l2DeployManager.bytecodeRequested(bytecodeHash_1_0_0)).to.equal(initCodeHash_1_0_0); expect(await l1DeployManager.isVersionSentToChain(mockOtherChainId, bytecodeHash_1_0_0)).to.be.true; }); - it("Should not let send same bytecode to same chain more than once", async () => { - const { WOOF, l1DeployManager, bytecodeVersion_1_0_0, bytecodeHash_1_0_0 } = await restore(); - await l1DeployManager - .connect(WOOF.keyDeveloper) - .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }); - // Try to send one more time - await expect( - l1DeployManager - .connect(WOOF.subDevelopers[0]) - .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }) - ) - .revertedWithCustomError(l1DeployManager, "BytecodeAlreadySent") - .withArgs(mockOtherChainId, bytecodeHash_1_0_0); - }); - it("Should not send same bytecode if not enough ETH funds sent", async () => { const { WOOF, l1DeployManager, bytecodeVersion_1_0_0 } = await restore(); const insufficientValue = ethers.parseEther("0.099"); @@ -227,7 +226,6 @@ describe("L1/L2 DeployManager", function () { await expect( l1DeployManager.connect(users[0]).setChainConfig(2, { l2DeployManager: ethers.ZeroAddress, - gasLimit: 120_000, destinationChainSelector: 1000 }) ).revertedWithCustomError(l1DeployManager, "OnlyGovernor"); @@ -305,11 +303,107 @@ describe("L1/L2 DeployManager", function () { ); }); - it("Should deploy contract on L2 after bytecode is sent and developer access is granted", async () => { + it("Should upload bytecode on L2 after requesting", async () => { + const { WOOF, l1DeployManager, l2DeployManager, versionController, bytecodeVersion_1_0_0, bytecodeHash_1_0_0 } = + await restore(); + const verifiedBytecode = await versionController.getVerifiedBytecode(bytecodeVersion_1_0_0); + + // First send bytecode to L2 + await l1DeployManager + .connect(WOOF.keyDeveloper) + .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }); + expect(await l2DeployManager.bytecodeRequested(bytecodeHash_1_0_0)).to.equal( + ethers.keccak256(verifiedBytecode) + ); + + // Upload bytecode on L2 + await l2DeployManager.uploadBytecode(bytecodeVersion_1_0_0, verifiedBytecode); + + // Do not expect bytecode any longer + expect(await l2DeployManager.bytecodeRequested(bytecodeHash_1_0_0)).to.equal(ethers.ZeroHash); + expect(await l2DeployManager.versionExists(bytecodeVersion_1_0_0)).to.be.true; + }); + + it("Should not let upload bytecode if it is not requested", async () => { + const { WOOF, l1DeployManager, l2DeployManager, versionController, bytecodeVersion_1_0_0 } = await restore(); + const verifiedBytecode = await versionController.getVerifiedBytecode(bytecodeVersion_1_0_0); + + // Upload reverts when bytecode was never requested + await expect(l2DeployManager.uploadBytecode(bytecodeVersion_1_0_0, verifiedBytecode)).revertedWithCustomError( + l2DeployManager, + "BytecodeNotRequested" + ); + + // First send bytecode to L2 + await l1DeployManager + .connect(WOOF.keyDeveloper) + .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }); + + // Upload bytecode on L2 + await l2DeployManager.uploadBytecode(bytecodeVersion_1_0_0, verifiedBytecode); + + // Uploading second time reverts + await expect(l2DeployManager.uploadBytecode(bytecodeVersion_1_0_0, verifiedBytecode)).revertedWithCustomError( + l2DeployManager, + "BytecodeNotRequested" + ); + }); + + it("Should not let upload invalid bytecode", async () => { + const { WOOF, l1DeployManager, l2DeployManager, bytecodeVersion_1_0_0 } = await restore(); + + // First send bytecode to L2 + await l1DeployManager + .connect(WOOF.keyDeveloper) + .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }); + + // Try to upload invalid bytecode + const invalidBytecode = CometExtInitCode; + await expect(l2DeployManager.uploadBytecode(bytecodeVersion_1_0_0, invalidBytecode)).revertedWithCustomError( + l2DeployManager, + "InvalidBytecode" + ); + }); + + it("Should not let request already uploaded bytecode", async () => { + const { + WOOF, + l1DeployManager, + l2DeployManager, + versionController, + bytecodeVersion_1_0_0, + bytecodeHash_1_0_0, + mockRouter + } = await restore(); + + // First send bytecode to L2 + await l1DeployManager + .connect(WOOF.keyDeveloper) + .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }); + + // Upload bytecode on L2 + const verifiedBytecode = await versionController.getVerifiedBytecode(bytecodeVersion_1_0_0); + await l2DeployManager.uploadBytecode(bytecodeVersion_1_0_0, verifiedBytecode); + + // Try to send again + const selector = ethers.id("BytecodeAlreadyUploaded(bytes32)").slice(0, 10); + const encodedArg = ethers.AbiCoder.defaultAbiCoder().encode(["bytes32"], [bytecodeHash_1_0_0]); + const revertData = selector + encodedArg.slice(2); + await expect( + l1DeployManager + .connect(WOOF.keyDeveloper) + .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }) + ) + .revertedWithCustomError(mockRouter, "ReceiverError") + .withArgs(revertData); + }); + + it("Should deploy contract on L2 after bytecode is sent and uploaded and developer access is granted", async () => { const { WOOF, l1DeployManager, l2DeployManager, + versionController, bytecodeVersion_1_0_0, mockBaseToken, mockCollateralToken, @@ -322,6 +416,10 @@ describe("L1/L2 DeployManager", function () { .connect(WOOF.keyDeveloper) .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }); + // Upload bytecode on L2 + const verifiedBytecode = await versionController.getVerifiedBytecode(bytecodeVersion_1_0_0); + await l2DeployManager.uploadBytecode(bytecodeVersion_1_0_0, verifiedBytecode); + // Become Developer on L2 await l1DeployManager .connect(WOOF.keyDeveloper) @@ -453,6 +551,7 @@ describe("L1/L2 DeployManager", function () { WOOF, l1DeployManager, l2DeployManager, + versionController, bytecodeVersion_1_0_0, mockBaseToken, mockCollateralToken, @@ -465,6 +564,10 @@ describe("L1/L2 DeployManager", function () { .connect(WOOF.keyDeveloper) .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }); + // Upload bytecode on L2 + const verifiedBytecode = await versionController.getVerifiedBytecode(bytecodeVersion_1_0_0); + await l2DeployManager.uploadBytecode(bytecodeVersion_1_0_0, verifiedBytecode); + // Prepare Comet constructor parameters const cometConfiguration = { governor: governor.address, @@ -562,6 +665,7 @@ describe("L1/L2 DeployManager", function () { WOOF, l1DeployManager, l2DeployManager, + versionController, bytecodeVersion_1_0_0, users, mockBaseToken, @@ -575,6 +679,10 @@ describe("L1/L2 DeployManager", function () { .connect(WOOF.keyDeveloper) .sendBytecodeToOtherChain(bytecodeVersion_1_0_0, mockOtherChainId, gasLimit, { value: mockRouterFee }); + // Upload bytecode on L2 + const verifiedBytecode = await versionController.getVerifiedBytecode(bytecodeVersion_1_0_0); + await l2DeployManager.uploadBytecode(bytecodeVersion_1_0_0, verifiedBytecode); + // Prepare Comet constructor parameters const cometConfiguration = { governor: governor.address, diff --git a/test/VersionController.ts b/test/VersionController.ts index 6e3f815..04961a3 100644 --- a/test/VersionController.ts +++ b/test/VersionController.ts @@ -922,12 +922,12 @@ describe("VersionController", function () { .withArgs(users[0], await versionController.AUDITOR_ROLE()); }); - it("Should not let non-admin assign dev for contract type", async () => { + it("Should not let non-admin or non-guardian assign dev for contract type", async () => { const { users, versionController } = await restore(); const newContractType = ethers.encodeBytes32String("New_Contract_Type"); await expect(versionController.connect(users[0]).assignDeveloperForContractTypes([newContractType], users[1])) - .revertedWithCustomError(versionController, "AccessControlUnauthorizedAccount") - .withArgs(users[0], await versionController.DEFAULT_ADMIN_ROLE()); + .revertedWithCustomError(versionController, "NotGovernorOrGuadian") + .withArgs(users[0]); }); it("Should not let key developer assign dev for contract type (admin-only function)", async () => { @@ -937,8 +937,19 @@ describe("VersionController", function () { .connect(devTeam2.keyDeveloper) .assignDeveloperForContractTypes([WOOF.contractTypes[0]], devTeam3.keyDeveloper) ) - .revertedWithCustomError(versionController, "AccessControlUnauthorizedAccount") - .withArgs(devTeam2.keyDeveloper, await versionController.DEFAULT_ADMIN_ROLE()); + .revertedWithCustomError(versionController, "NotGovernorOrGuadian") + .withArgs(devTeam2.keyDeveloper); + }); + + it("Should let guardian assign dev for contract type", async () => { + const { guardian, devTeam2, versionController } = await restore(); + const newContractType = ethers.encodeBytes32String("New_Contract_Type"); + await versionController + .connect(guardian) + .assignDeveloperForContractTypes([newContractType], devTeam2.keyDeveloper); + expect(await versionController.contractTypeKeyDeveloper(newContractType)).to.equal(devTeam2.keyDeveloper); + const registeredTypes = await versionController.getRegisteredContractTypes(); + expect(registeredTypes[registeredTypes.length - 1]).to.equal(newContractType); }); it("Should not let revoked key developer assign dev for contract type", async () => {