Skip to content

Commit d04ebf5

Browse files
feat: multichain deploy scripts (#1487)
**Motivation:** We need deploy scripts for multichain **Modifications:** Adds a `crossChainDeployLib` from: #1474. This library uses `createX` for deterministic deployments. In order to keep the same address on all `destinationChains`, we deploy contracts from the same multisig on each chain. Add `testnet-base-sepolia` to CI for deploy scripts. Adds a `deploy_globalRootConfirmerSet` script. This script initiates a global root confirmer set (ie. generator) and updates the `network.toml` file used in the deploy scripts. It is only intended to be used on `preprod` and `testnet`. To distinguish between a source and destination chain we add a `SOURCE_CHAIN: bool` and `DESTINATION_CHAIN: bool` to the Zeus Config. We skip parts of the deploy if a chain is not a source or destination. Deploys the contracts in 5 steps 1. `deploySourceChain.s.sol`: Deploys `KeyRegistrar`, `ReleaseManager`, `CrossChainRegistry`. 2. `deployDestinationChainProxies.s.sol`: Deploys _empty_ proxy contracts for `OperatorTableUpdater`, `BN254CertificateVerifier`, `ECDSACertificateVerifier`. _Note: in order to have deterministic proxies, we need to ensure the `initCode` is the same. Thus, the implementation is the same `emptyContract` and the proxyAdmin is initially the `multichainDeployerMultisig`_. 3. `deployDestinationChainImpls`: Deploys implementations using EOA 4. `instantiateDestinationChainProxies`: Upgrades the proxies to the actual implementations. Transfer proxy admin to the actual `proxyAdmin` 5. `configureCrossChainRegistry`: Updates the `crossChainRegistry` on the `SOURCE_CHAIN` to whitelist proper chainIds **Result:** Deploy scripts --------- Co-authored-by: clandestine.eth <[email protected]>
1 parent d37c98f commit d04ebf5

24 files changed

+2112
-357
lines changed

.github/workflows/foundry.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ jobs:
7272
esac
7373
env:
7474
FOUNDRY_PROFILE: ${{ matrix.suite == 'Fork' && 'forktest' || 'medium' }}
75-
RPC_MAINNET: https://billowing-capable-sound.quiknode.pro/
75+
RPC_MAINNET: ${{ secrets.RPC_MAINNET }}
76+
RPC_HOLESKY: ${{ secrets.RPC_HOLESKY }}
7677

7778
# -----------------------------------------------------------------------
7879
# Forge Storage Diff

.github/workflows/validate-deployment-scripts.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
fail-fast: true
2020
matrix:
21-
env: [preprod, testnet, mainnet, testnet-sepolia, testnet-hoodi]
21+
env: [preprod, testnet, mainnet, testnet-sepolia, testnet-hoodi, testnet-base-sepolia]
2222

2323
steps:
2424
# Check out repository with all submodules for complete codebase access.
@@ -67,7 +67,7 @@ jobs:
6767
- name: Validate Solidity Scripts
6868
run: |
6969
# Find all .sol files under /script/releases
70-
RELEASE_FILES=$(find script/releases -type f -name "*.sol" ! -name "Env.sol" 2>/dev/null || echo "")
70+
RELEASE_FILES=$(find script/releases -type f -name "*.sol" ! -name "Env.sol" ! -name "CrosschainDeployLib.sol" 2>/dev/null || echo "")
7171
7272
# Combine file lists
7373
FILES="$RELEASE_FILES"
@@ -88,6 +88,8 @@ jobs:
8888
RPC_URL="${{ secrets.RPC_SEPOLIA }}"
8989
elif [ "${{ matrix.env }}" = "testnet-hoodi" ]; then
9090
RPC_URL="${{ secrets.RPC_HOODI }}"
91+
elif [ "${{ matrix.env }}" = "testnet-base-sepolia" ]; then
92+
RPC_URL="${{ secrets.RPC_BASE_SEPOLIA }}"
9193
elif [ "${{ matrix.env }}" = "mainnet" ]; then
9294
RPC_URL="${{ secrets.RPC_MAINNET }}"
9395
fi

pkg/bindings/IReleaseManager/binding.go

Lines changed: 200 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/bindings/ReleaseManager/binding.go

Lines changed: 201 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/bindings/ReleaseManagerStorage/binding.go

Lines changed: 200 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignore wallet files to prevent sensitive information from being committed
2+
*.wallet.json
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.27;
3+
4+
import "@openzeppelin/contracts/utils/Strings.sol";
5+
import "src/test/utils/OperatorWalletLib.sol";
6+
import "src/test/utils/Random.sol";
7+
import "src/contracts/interfaces/IOperatorTableCalculator.sol";
8+
import "src/contracts/interfaces/ICrossChainRegistry.sol";
9+
10+
import "src/contracts/libraries/Merkle.sol";
11+
12+
import "forge-std/Script.sol";
13+
import "forge-std/Test.sol";
14+
15+
// forge script script/deploy/multichain/deploy_globalRootConfirmerSet.s.sol --sig "run(string memory)" $NETWORK
16+
contract DeployGlobalRootConfirmerSet is Script, Test {
17+
using Strings for *;
18+
using Merkle for bytes32[];
19+
using BN254 for BN254.G1Point;
20+
21+
address internal constant AVS = 0xDA29BB71669f46F2a779b4b62f03644A84eE3479;
22+
23+
function run(string memory network, string memory salt) public {
24+
/**
25+
*
26+
* WALLET CREATION
27+
*
28+
*/
29+
require(_strEq(network, "preprod") || _strEq(network, "testnet"), "Invalid network");
30+
31+
// 1. Create a BN254 Wallet using random salt
32+
Operator memory operator = OperatorWalletLib.createOperator(salt);
33+
34+
/**
35+
*
36+
* Create the `BN254OperatorInfo` struct
37+
*
38+
*/
39+
40+
// 1. Generate the `BN254OperatorInfo` struct
41+
IOperatorTableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo;
42+
43+
// 2. Set the numOperators and totalWeights
44+
operatorSetInfo.numOperators = 1;
45+
uint256[] memory weights = new uint256[](1);
46+
weights[0] = 1;
47+
operatorSetInfo.totalWeights = weights;
48+
49+
// 3. Set the apk
50+
BN254.G1Point memory aggregatePubkey;
51+
aggregatePubkey = aggregatePubkey.plus(operator.signingKey.publicKeyG1);
52+
operatorSetInfo.aggregatePubkey = aggregatePubkey;
53+
54+
// 4. Set the operatorInfoTreeRoot
55+
bytes32[] memory operatorInfoLeaves = new bytes32[](1);
56+
operatorInfoLeaves[0] = keccak256(
57+
abi.encode(
58+
IOperatorTableCalculatorTypes.BN254OperatorInfo({
59+
pubkey: operator.signingKey.publicKeyG1,
60+
weights: weights
61+
})
62+
)
63+
);
64+
operatorSetInfo.operatorInfoTreeRoot = operatorInfoLeaves.merkleizeKeccak();
65+
66+
/**
67+
*
68+
* Create the `operatorSetConfig` struct
69+
*
70+
*/
71+
ICrossChainRegistry.OperatorSetConfig memory operatorSetConfig;
72+
operatorSetConfig.owner = operator.key.addr;
73+
operatorSetConfig.maxStalenessPeriod = 0;
74+
75+
/**
76+
*
77+
* OUTPUT - OPERATOR SET INFO (TOML FORMAT)
78+
*
79+
*/
80+
81+
// Write operator set info to TOML file
82+
_writeOperatorSetToml(network, operatorSetInfo, operatorSetConfig);
83+
84+
/**
85+
*
86+
* OUTPUT - BLS WALLET
87+
*
88+
*/
89+
90+
// Write operator data to a separate function to avoid stack too deep
91+
_writeOperatorData(operator, network);
92+
}
93+
94+
function _writeOperatorData(Operator memory operator, string memory network) internal {
95+
string memory operator_object = "operator";
96+
97+
// Serialize regular wallet info
98+
string memory wallet_object = "wallet";
99+
vm.serializeUint(wallet_object, "privateKey", operator.key.privateKey);
100+
string memory walletOutput = vm.serializeAddress(wallet_object, "address", operator.key.addr);
101+
102+
// Serialize BLS wallet info
103+
string memory blsWallet_object = "blsWallet";
104+
vm.serializeUint(blsWallet_object, "privateKey", operator.signingKey.privateKey);
105+
106+
// Serialize publicKeyG1
107+
string memory publicKeyG1_object = "publicKeyG1";
108+
vm.serializeUint(publicKeyG1_object, "x", operator.signingKey.publicKeyG1.X);
109+
string memory publicKeyG1Output = vm.serializeUint(publicKeyG1_object, "y", operator.signingKey.publicKeyG1.Y);
110+
vm.serializeString(blsWallet_object, "publicKeyG1", publicKeyG1Output);
111+
112+
// Serialize publicKeyG2
113+
string memory publicKeyG2_object = "publicKeyG2";
114+
vm.serializeUint(publicKeyG2_object, "x0", operator.signingKey.publicKeyG2.X[0]);
115+
vm.serializeUint(publicKeyG2_object, "x1", operator.signingKey.publicKeyG2.X[1]);
116+
vm.serializeUint(publicKeyG2_object, "y0", operator.signingKey.publicKeyG2.Y[0]);
117+
string memory publicKeyG2Output =
118+
vm.serializeUint(publicKeyG2_object, "y1", operator.signingKey.publicKeyG2.Y[1]);
119+
string memory blsWalletOutput = vm.serializeString(blsWallet_object, "publicKeyG2", publicKeyG2Output);
120+
121+
// Combine wallet and blsWallet into operator object
122+
vm.serializeString(operator_object, "wallet", walletOutput);
123+
string memory operatorOutput = vm.serializeString(operator_object, "blsWallet", blsWalletOutput);
124+
125+
// Write to separate file
126+
string memory walletOutputPath = string.concat("script/deploy/multichain/", network, ".wallet.json");
127+
vm.writeJson(operatorOutput, walletOutputPath);
128+
}
129+
130+
function _writeOperatorSetToml(
131+
string memory network,
132+
IOperatorTableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo,
133+
ICrossChainRegistry.OperatorSetConfig memory operatorSetConfig
134+
) internal {
135+
// Build JSON object using serializeJson
136+
string memory json_obj = "toml_output";
137+
138+
// Top level fields
139+
vm.serializeUint(json_obj, "globalRootConfirmationThreshold", 10_000);
140+
vm.serializeUint(json_obj, "referenceTimestamp", block.timestamp);
141+
142+
// globalRootConfirmerSet object
143+
string memory confirmerSet_obj = "globalRootConfirmerSet";
144+
vm.serializeString(confirmerSet_obj, "avs", AVS.toHexString());
145+
string memory confirmerSetOutput = vm.serializeUint(confirmerSet_obj, "id", 0);
146+
vm.serializeString(json_obj, "globalRootConfirmerSet", confirmerSetOutput);
147+
148+
// globalRootConfirmerSetConfig object
149+
string memory confirmerSetConfig_obj = "globalRootConfirmerSetConfig";
150+
vm.serializeUint(confirmerSetConfig_obj, "maxStalenessPeriod", operatorSetConfig.maxStalenessPeriod);
151+
string memory confirmerSetConfigOutput =
152+
vm.serializeAddress(confirmerSetConfig_obj, "owner", operatorSetConfig.owner);
153+
vm.serializeString(json_obj, "globalRootConfirmerSetConfig", confirmerSetConfigOutput);
154+
155+
// globalRootConfirmerSetInfo object
156+
string memory confirmerSetInfo_obj = "globalRootConfirmerSetInfo";
157+
vm.serializeUint(confirmerSetInfo_obj, "numOperators", operatorSetInfo.numOperators);
158+
vm.serializeBytes32(confirmerSetInfo_obj, "operatorInfoTreeRoot", operatorSetInfo.operatorInfoTreeRoot);
159+
vm.serializeUint(confirmerSetInfo_obj, "totalWeights", operatorSetInfo.totalWeights);
160+
161+
// aggregatePubkey nested object
162+
string memory aggregatePubkey_obj = "aggregatePubkey";
163+
vm.serializeString(aggregatePubkey_obj, "X", operatorSetInfo.aggregatePubkey.X.toString());
164+
string memory aggregatePubkeyOutput =
165+
vm.serializeString(aggregatePubkey_obj, "Y", operatorSetInfo.aggregatePubkey.Y.toString());
166+
167+
string memory confirmerSetInfoOutput =
168+
vm.serializeString(confirmerSetInfo_obj, "aggregatePubkey", aggregatePubkeyOutput);
169+
string memory finalJson = vm.serializeString(json_obj, "globalRootConfirmerSetInfo", confirmerSetInfoOutput);
170+
171+
// Write TOML file using writeToml
172+
string memory outputPath = string.concat("script/releases/v1.7.0-multichain/configs/", network, ".toml");
173+
vm.writeToml(finalJson, outputPath);
174+
}
175+
176+
function _strEq(string memory a, string memory b) internal pure returns (bool) {
177+
return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b));
178+
}
179+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.12;
3+
4+
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
5+
import "src/test/mocks/EmptyContract.sol";
6+
7+
/// @dev https://github.com/pcaversaccio/createx/tree/main
8+
ICreateX constant createx = ICreateX(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed);
9+
10+
interface ICreateX {
11+
function deployCreate2(bytes32 salt, bytes memory initCode) external payable returns (address newContract);
12+
function computeCreate2Address(
13+
bytes32 salt,
14+
bytes32 initCodeHash
15+
) external view returns (address computedAddress);
16+
}
17+
18+
library CrosschainDeployLib {
19+
using CrosschainDeployLib for *;
20+
21+
/// -----------------------------------------------------------------------
22+
/// Write
23+
/// -----------------------------------------------------------------------
24+
25+
/*
26+
* @notice Deploys a crosschain empty contract.
27+
* @dev The empty contract MUST stay consistent across all chains/deployments.
28+
* @dev The empty contract MUST always be deployed with the same salt.
29+
*/
30+
function deployEmptyContract(
31+
address deployer
32+
) internal returns (address) {
33+
return _deployCrosschain(deployer, type(EmptyContract).creationCode, type(EmptyContract).name);
34+
}
35+
36+
/*
37+
* @notice Deploys a crosschain `TransparentUpgradeableProxy` using CreateX.
38+
* @dev The initial admin is the deployer.
39+
* @dev The implementation MUST also be deterministic to ensure the contract can be deployed on all chains.
40+
* @dev The salt MUST be unique for each proxy deployment sharing the same implementation otherwise address collisions WILL occur.
41+
* @dev The `admin` is also assumed to be the deployer.
42+
*
43+
* @dev Example usage:
44+
* ```solidity
45+
* bytes11 salt = bytes11(uint88(0xffffffffffffffffffffff));
46+
* address emptyContract = type(EmptyContract).creationCode.deployCrosschain(deployer);
47+
* address proxy = emptyContract.deployCrosschainProxy(deployer, salt);
48+
* ITransparentUpgradeableProxy(address(proxy)).upgradeTo(address(implementation));
49+
* ITransparentUpgradeableProxy(address(proxy)).changeAdmin(address(admin));
50+
* ```
51+
*/
52+
function deployCrosschainProxy(
53+
address adminAndDeployer,
54+
address implementation,
55+
string memory name
56+
) internal returns (ITransparentUpgradeableProxy) {
57+
return ITransparentUpgradeableProxy(
58+
_deployCrosschain(adminAndDeployer, computeUpgradeableProxyInitCode(implementation, adminAndDeployer), name)
59+
);
60+
}
61+
62+
/*
63+
* @notice Deploys a crosschain contract with CreateX.
64+
*
65+
* @dev Example usage:
66+
* ```solidity
67+
* type(EmptyContract).creationCode.deployCrosschain(deployer, EMPTY_CONTRACT_SALT)
68+
* ```
69+
*/
70+
function _deployCrosschain(address deployer, bytes memory initCode, string memory name) private returns (address) {
71+
return createx.deployCreate2(computeProtectedSalt(deployer, name), initCode);
72+
}
73+
74+
/// -----------------------------------------------------------------------
75+
/// Helpers
76+
/// -----------------------------------------------------------------------
77+
78+
/*
79+
* @notice Returns an encoded CreateX salt.
80+
* @dev The deployer is the only account that can use this salt via CreateX hence the name "protected".
81+
* @dev The salt is structured as: Deployer EOA (20 bytes) | Cross-chain flag (1 byte) | Entropy (11 bytes)
82+
* @dev Example: 0xbebebebebebebebebebebebebebebebebebebebe|ff|1212121212121212121212
83+
*/
84+
function computeProtectedSalt(address deployer, string memory name) internal pure returns (bytes32) {
85+
return bytes32(
86+
bytes.concat(
87+
bytes20(deployer),
88+
bytes1(uint8(0)), // Cross-chain redeploy protection enabled (0: false, 1: true)
89+
bytes11(keccak256(bytes(name))) // salt
90+
)
91+
);
92+
}
93+
94+
/*
95+
* @notice Returns the initialization code for a transparent upgradeable proxy.
96+
* @dev The returned init code does not include metadata typically appended by the compiler.
97+
*/
98+
function computeUpgradeableProxyInitCode(
99+
address implementation,
100+
address admin
101+
) internal pure returns (bytes memory) {
102+
return abi.encodePacked(type(TransparentUpgradeableProxy).creationCode, abi.encode(implementation, admin, ""));
103+
}
104+
105+
/*
106+
* @notice Computes the deterministic address where a crosschain contract will be deployed.
107+
* @dev Uses CreateX's computeCreate2Address with a protected salt to ensure deterministic deployment.
108+
* @param deployer The address of the deployer account.
109+
* @param initCodeHash The keccak256 hash of the contract's initialization code.
110+
* @param name The name used to generate the protected salt.
111+
* @return The deterministic address where the contract will be deployed.
112+
*/
113+
function computeCrosschainAddress(
114+
address deployer,
115+
bytes32 initCodeHash,
116+
string memory name
117+
) internal view returns (address) {
118+
return createx.computeCreate2Address(
119+
keccak256(abi.encodePacked(bytes32(uint256(uint160(deployer))), computeProtectedSalt(deployer, name))),
120+
initCodeHash
121+
);
122+
}
123+
124+
/*
125+
* @notice Computes the deterministic address where a crosschain upgradeable proxy will be deployed.
126+
* @dev Computes the init code hash for a transparent upgradeable proxy and calls computeCrosschainAddress.
127+
* @param adminAndDeployer The address that will be both the admin and deployer of the proxy.
128+
* @param implementation The address of the implementation contract.
129+
* @param name The name used to generate the protected salt.
130+
* @return The deterministic address where the upgradeable proxy will be deployed.
131+
*/
132+
function computeCrosschainUpgradeableProxyAddress(
133+
address adminAndDeployer,
134+
address implementation,
135+
string memory name
136+
) internal view returns (address) {
137+
return computeCrosschainAddress(
138+
adminAndDeployer, keccak256(computeUpgradeableProxyInitCode(implementation, adminAndDeployer)), name
139+
);
140+
}
141+
}

0 commit comments

Comments
 (0)