Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 41 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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({
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)",
Expand Down
8 changes: 7 additions & 1 deletion abi/full/contracts/L2DeployManager.sol/L2DeployManager.json
Original file line number Diff line number Diff line change
@@ -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)",
Expand All @@ -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)"
]
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down Expand Up @@ -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)",
Expand Down
16 changes: 0 additions & 16 deletions abi/json/contracts/L1DeployManager.sol/L1DeployManager.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
137 changes: 136 additions & 1 deletion abi/json/contracts/L2DeployManager.sol/L2DeployManager.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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"
},
{
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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": [
{
Expand Down
Loading