|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity 0.8.15; |
| 3 | + |
| 4 | +import { Script } from "forge-std/Script.sol"; |
| 5 | +import { LibString } from "@solady/utils/LibString.sol"; |
| 6 | + |
| 7 | +import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; |
| 8 | +import { ProtocolVersions, ProtocolVersion } from "src/L1/ProtocolVersions.sol"; |
| 9 | +import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; |
| 10 | +import { Proxy } from "src/universal/Proxy.sol"; |
| 11 | + |
| 12 | +/** |
| 13 | + * This comment block defines the requirements and rationale for the architecture used in this forge |
| 14 | + * script, along with other scripts that are being written as new Superchain-first deploy scripts to |
| 15 | + * complement the OP Stack Manager. The script architecture is a bit different than a standard forge |
| 16 | + * deployment script. |
| 17 | + * |
| 18 | + * There are three categories of users that are expected to interact with the scripts: |
| 19 | + * 1. End users that want to run live contract deployments. |
| 20 | + * 2. Solidity developers that want to use or test these script in a standard forge test environment. |
| 21 | + * 3. Go developers that want to run the deploy scripts as part of e2e testing with other aspects of the OP Stack. |
| 22 | + * |
| 23 | + * We want each user to interact with the scripts in the way that's simplest for their use case: |
| 24 | + * 1. End users: TOML input files that define config, and TOML output files with all output data. |
| 25 | + * 2. Solidity developers: Direct calls to the script with input structs and output structs. |
| 26 | + * 3. Go developers: The forge scripts can be executed directly in Go. |
| 27 | + * |
| 28 | + * The following architecture is used to meet the requirements of each user. We use this file's |
| 29 | + * `DeploySuperchain` script as an example, but it applies to other scripts as well. |
| 30 | + * |
| 31 | + * This `DeploySuperchain.s.sol` file contains three contracts: |
| 32 | + * 1. `DeploySuperchainInput`: Responsible for parsing, storing, and exposing the input data. |
| 33 | + * 2. `DeploySuperchainOutput`: Responsible for storing and exposing the output data. |
| 34 | + * 3. `DeploySuperchain`: The core script that executes the deployment. It reads inputs from the |
| 35 | + * input contract, and writes outputs to the output contract. |
| 36 | + * |
| 37 | + * Because the core script performs calls to the input and output contracts, Go developers can |
| 38 | + * intercept calls to these addresses (analogous to how forge intercepts calls to the `Vm` address |
| 39 | + * to execute cheatcodes), to avoid the need for file I/O or hardcoding the input/output structs. |
| 40 | + * |
| 41 | + * Public getter methods on the input and output contracts allow individual fields to be accessed |
| 42 | + * in a strong, type-safe manner (as opposed to a single struct getter where the caller may |
| 43 | + * inadvertently transpose two addresses, for example). |
| 44 | + * |
| 45 | + * Each deployment step in the core deploy script is modularized into its own function that performs |
| 46 | + * the deploy and sets the output on the Output contract, allowing for easy composition and testing |
| 47 | + * of deployment steps. The output setter methods requires keying off the four-byte selector of the |
| 48 | + * each output field's getter method, ensuring that the output is set for the correct field and |
| 49 | + * minimizing the amount of boilerplate needed for each output field. |
| 50 | + * |
| 51 | + * This script doubles as a reference for documenting the pattern used and therefore contains |
| 52 | + * comments explaining the patterns used. Other scripts are not expected to have this level of |
| 53 | + * documentation. |
| 54 | + * |
| 55 | + * Additionally, we intentionally use "Input" and "Output" terminology to clearly distinguish these |
| 56 | + * scripts from the existing ones that "Config" and "Artifacts" terminology. |
| 57 | + */ |
| 58 | +contract DeploySuperchainInput { |
| 59 | + // The input struct contains all the input data required for the deployment. |
| 60 | + struct Input { |
| 61 | + Roles roles; |
| 62 | + bool paused; |
| 63 | + ProtocolVersion requiredProtocolVersion; |
| 64 | + ProtocolVersion recommendedProtocolVersion; |
| 65 | + } |
| 66 | + |
| 67 | + struct Roles { |
| 68 | + address proxyAdminOwner; |
| 69 | + address protocolVersionsOwner; |
| 70 | + address guardian; |
| 71 | + } |
| 72 | + |
| 73 | + // This flag tells us if all inputs have been set. An `input()` getter method that returns all |
| 74 | + // inputs reverts if this flag is false. This ensures the deploy script cannot proceed until all |
| 75 | + // inputs are validated and set. |
| 76 | + bool public inputSet = false; |
| 77 | + |
| 78 | + // The full input struct is kept in storage. It is not exposed because the return type would be |
| 79 | + // a tuple, but it's more convenient for the return type to be the struct itself. Therefore the |
| 80 | + // struct is exposed via the `input()` getter method. |
| 81 | + Input internal inputs; |
| 82 | + |
| 83 | + // And each field is exposed via it's own getter method. We can equivalently remove these |
| 84 | + // storage variables and add getter methods that return the input struct fields directly, but |
| 85 | + // that is more verbose with more boilerplate, especially for larger scripts with many inputs. |
| 86 | + // Unlike the `input()` getter, these getters do not revert if the input is not set. The caller |
| 87 | + // should check the `inputSet` value before calling any of these getters. |
| 88 | + address public proxyAdminOwner; |
| 89 | + address public protocolVersionsOwner; |
| 90 | + address public guardian; |
| 91 | + bool public paused; |
| 92 | + ProtocolVersion public requiredProtocolVersion; |
| 93 | + ProtocolVersion public recommendedProtocolVersion; |
| 94 | + |
| 95 | + // Load the input from a TOML file. |
| 96 | + function loadInputFile(string memory _infile) public { |
| 97 | + _infile; |
| 98 | + Input memory parsedInput; |
| 99 | + loadInput(parsedInput); |
| 100 | + require(false, "DeploySuperchainInput: loadInput is not implemented"); |
| 101 | + } |
| 102 | + |
| 103 | + // Load the input from a struct. |
| 104 | + function loadInput(Input memory _input) public { |
| 105 | + // As a defensive measure, we only allow inputs to be set once. |
| 106 | + require(!inputSet, "DeploySuperchainInput: Input already set"); |
| 107 | + |
| 108 | + // All assertions on inputs happen here. You cannot set any inputs in Solidity unless |
| 109 | + // they're all valid. For Go testing, the input and outputs |
| 110 | + require(_input.roles.proxyAdminOwner != address(0), "DeploySuperchainInput: Null proxyAdminOwner"); |
| 111 | + require(_input.roles.protocolVersionsOwner != address(0), "DeploySuperchainInput: Null protocolVersionsOwner"); |
| 112 | + require(_input.roles.guardian != address(0), "DeploySuperchainInput: Null guardian"); |
| 113 | + |
| 114 | + // We now set all values in storage. |
| 115 | + inputSet = true; |
| 116 | + inputs = _input; |
| 117 | + |
| 118 | + proxyAdminOwner = _input.roles.proxyAdminOwner; |
| 119 | + protocolVersionsOwner = _input.roles.protocolVersionsOwner; |
| 120 | + guardian = _input.roles.guardian; |
| 121 | + paused = _input.paused; |
| 122 | + requiredProtocolVersion = _input.requiredProtocolVersion; |
| 123 | + recommendedProtocolVersion = _input.recommendedProtocolVersion; |
| 124 | + } |
| 125 | + |
| 126 | + function input() public view returns (Input memory) { |
| 127 | + require(inputSet, "DeploySuperchainInput: Input not set"); |
| 128 | + return inputs; |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +contract DeploySuperchainOutput { |
| 133 | + // The output struct contains all the output data from the deployment. |
| 134 | + struct Output { |
| 135 | + ProxyAdmin superchainProxyAdmin; |
| 136 | + SuperchainConfig superchainConfigImpl; |
| 137 | + SuperchainConfig superchainConfigProxy; |
| 138 | + ProtocolVersions protocolVersionsImpl; |
| 139 | + ProtocolVersions protocolVersionsProxy; |
| 140 | + } |
| 141 | + |
| 142 | + // We use a similar pattern as the input contract to expose outputs. Because outputs are set |
| 143 | + // individually, and deployment steps are modular and composable, we do not have an equivalent |
| 144 | + // to the overall `input` and `inputSet` variables. |
| 145 | + ProxyAdmin public superchainProxyAdmin; |
| 146 | + SuperchainConfig public superchainConfigImpl; |
| 147 | + SuperchainConfig public superchainConfigProxy; |
| 148 | + ProtocolVersions public protocolVersionsImpl; |
| 149 | + ProtocolVersions public protocolVersionsProxy; |
| 150 | + |
| 151 | + // This method lets each field be set individually. The selector of an output's getter method |
| 152 | + // is used to determine which field to set. |
| 153 | + function set(bytes4 sel, address _address) public { |
| 154 | + if (sel == this.superchainProxyAdmin.selector) superchainProxyAdmin = ProxyAdmin(_address); |
| 155 | + else if (sel == this.superchainConfigImpl.selector) superchainConfigImpl = SuperchainConfig(_address); |
| 156 | + else if (sel == this.superchainConfigProxy.selector) superchainConfigProxy = SuperchainConfig(_address); |
| 157 | + else if (sel == this.protocolVersionsImpl.selector) protocolVersionsImpl = ProtocolVersions(_address); |
| 158 | + else if (sel == this.protocolVersionsProxy.selector) protocolVersionsProxy = ProtocolVersions(_address); |
| 159 | + else revert("DeploySuperchainOutput: Unknown selector"); |
| 160 | + } |
| 161 | + |
| 162 | + // Save the output to a TOML file. |
| 163 | + function writeOutputFile(string memory _outfile) public pure { |
| 164 | + _outfile; |
| 165 | + require(false, "DeploySuperchainOutput: saveOutput not implemented"); |
| 166 | + } |
| 167 | + |
| 168 | + function output() public view returns (Output memory) { |
| 169 | + return Output({ |
| 170 | + superchainProxyAdmin: superchainProxyAdmin, |
| 171 | + superchainConfigImpl: superchainConfigImpl, |
| 172 | + superchainConfigProxy: superchainConfigProxy, |
| 173 | + protocolVersionsImpl: protocolVersionsImpl, |
| 174 | + protocolVersionsProxy: protocolVersionsProxy |
| 175 | + }); |
| 176 | + } |
| 177 | + |
| 178 | + function checkOutput() public view { |
| 179 | + // Assert that all addresses are non-zero and have code. |
| 180 | + // We use LibString to avoid the need for adding cheatcodes to this contract. |
| 181 | + address[] memory addresses = new address[](5); |
| 182 | + addresses[0] = address(superchainProxyAdmin); |
| 183 | + addresses[1] = address(superchainConfigImpl); |
| 184 | + addresses[2] = address(superchainConfigProxy); |
| 185 | + addresses[3] = address(protocolVersionsImpl); |
| 186 | + addresses[4] = address(protocolVersionsProxy); |
| 187 | + |
| 188 | + for (uint256 i = 0; i < addresses.length; i++) { |
| 189 | + address who = addresses[i]; |
| 190 | + require(who != address(0), string.concat("check failed: zero address at index ", LibString.toString(i))); |
| 191 | + require( |
| 192 | + who.code.length > 0, string.concat("check failed: no code at ", LibString.toHexStringChecksummed(who)) |
| 193 | + ); |
| 194 | + } |
| 195 | + |
| 196 | + // All addresses should be unique. |
| 197 | + for (uint256 i = 0; i < addresses.length; i++) { |
| 198 | + for (uint256 j = i + 1; j < addresses.length; j++) { |
| 199 | + string memory err = |
| 200 | + string.concat("check failed: duplicates at ", LibString.toString(i), ",", LibString.toString(j)); |
| 201 | + require(addresses[i] != addresses[j], err); |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | +} |
| 206 | + |
| 207 | +// For all broadcasts in this script we explicitly specify the deployer as `msg.sender` because for |
| 208 | +// testing we deploy this script from a test contract. If we provide no argument, the foundry |
| 209 | +// default sender is be the broadcaster during test, but the broadcaster needs to be the deployer |
| 210 | +// since they are set to the initial proxy admin owner. |
| 211 | +contract DeploySuperchain is Script { |
| 212 | + // -------- Core Deployment Methods -------- |
| 213 | + |
| 214 | + // This entrypoint is for end-users to deploy from an input file and write to an output file. |
| 215 | + // In this usage, we don't need the input and output contract functionality, so we deploy them |
| 216 | + // here and abstract that architectural detail away from the end user. |
| 217 | + function run(string memory _infile) public { |
| 218 | + // End-user without file IO, so etch the IO helper contracts. |
| 219 | + (DeploySuperchainInput dsi, DeploySuperchainOutput dso) = etchIOContracts(); |
| 220 | + |
| 221 | + // Load the input file into the input contract. |
| 222 | + dsi.loadInputFile(_infile); |
| 223 | + |
| 224 | + // Run the deployment script and write outputs to the DeploySuperchainOutput contract. |
| 225 | + run(dsi, dso); |
| 226 | + |
| 227 | + // Write the output data to a file. The file |
| 228 | + string memory outfile = ""; // This will be derived from input file name, e.g. `foo.in.toml` -> `foo.out.toml` |
| 229 | + dso.writeOutputFile(outfile); |
| 230 | + require(false, "DeploySuperchain: run is not implemented"); |
| 231 | + } |
| 232 | + |
| 233 | + // This entrypoint is for use with Solidity tests, where the input and outputs are structs. |
| 234 | + function run(DeploySuperchainInput.Input memory _input) public returns (DeploySuperchainOutput.Output memory) { |
| 235 | + // Solidity without file IO, so etch the IO helper contracts. |
| 236 | + (DeploySuperchainInput dsi, DeploySuperchainOutput dso) = etchIOContracts(); |
| 237 | + |
| 238 | + // Load the input struct into the input contract. |
| 239 | + dsi.loadInput(_input); |
| 240 | + |
| 241 | + // Run the deployment script and write outputs to the DeploySuperchainOutput contract. |
| 242 | + run(dsi, dso); |
| 243 | + |
| 244 | + // Return the output struct from the output contract. |
| 245 | + return dso.output(); |
| 246 | + } |
| 247 | + |
| 248 | + // This entrypoint is useful for testing purposes, as it doesn't use any file I/O. |
| 249 | + function run(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { |
| 250 | + // Verify that the input contract has been set. |
| 251 | + require(_dsi.inputSet(), "DeploySuperchain: Input not set"); |
| 252 | + |
| 253 | + // Deploy the proxy admin, with the owner set to the deployer. |
| 254 | + deploySuperchainProxyAdmin(_dsi, _dso); |
| 255 | + |
| 256 | + // Deploy and initialize the superchain contracts. |
| 257 | + deploySuperchainImplementationContracts(_dsi, _dso); |
| 258 | + deployAndInitializeSuperchainConfig(_dsi, _dso); |
| 259 | + deployAndInitializeProtocolVersions(_dsi, _dso); |
| 260 | + |
| 261 | + // Transfer ownership of the ProxyAdmin from the deployer to the specified owner. |
| 262 | + transferProxyAdminOwnership(_dsi, _dso); |
| 263 | + |
| 264 | + // Output assertions, to make sure outputs were assigned correctly. |
| 265 | + _dso.checkOutput(); |
| 266 | + } |
| 267 | + |
| 268 | + // -------- Deployment Steps -------- |
| 269 | + |
| 270 | + function deploySuperchainProxyAdmin(DeploySuperchainInput, DeploySuperchainOutput _dso) public { |
| 271 | + // Deploy the proxy admin, with the owner set to the deployer. |
| 272 | + // We explicitly specify the deployer as `msg.sender` because for testing we deploy this script from a test |
| 273 | + // contract. If we provide no argument, the foundry default sender is be the broadcaster during test, but the |
| 274 | + // broadcaster needs to be the deployer since they are set to the initial proxy admin owner. |
| 275 | + vm.broadcast(msg.sender); |
| 276 | + ProxyAdmin superchainProxyAdmin = new ProxyAdmin(msg.sender); |
| 277 | + |
| 278 | + vm.label(address(superchainProxyAdmin), "SuperchainProxyAdmin"); |
| 279 | + _dso.set(_dso.superchainProxyAdmin.selector, address(superchainProxyAdmin)); |
| 280 | + } |
| 281 | + |
| 282 | + function deploySuperchainImplementationContracts(DeploySuperchainInput, DeploySuperchainOutput _dso) public { |
| 283 | + // Deploy implementation contracts. |
| 284 | + vm.startBroadcast(msg.sender); |
| 285 | + SuperchainConfig superchainConfigImpl = new SuperchainConfig(); |
| 286 | + ProtocolVersions protocolVersionsImpl = new ProtocolVersions(); |
| 287 | + vm.stopBroadcast(); |
| 288 | + |
| 289 | + vm.label(address(superchainConfigImpl), "SuperchainConfigImpl"); |
| 290 | + vm.label(address(protocolVersionsImpl), "ProtocolVersionsImpl"); |
| 291 | + |
| 292 | + _dso.set(_dso.superchainConfigImpl.selector, address(superchainConfigImpl)); |
| 293 | + _dso.set(_dso.protocolVersionsImpl.selector, address(protocolVersionsImpl)); |
| 294 | + } |
| 295 | + |
| 296 | + function deployAndInitializeSuperchainConfig(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { |
| 297 | + require(_dsi.inputSet(), "DeploySuperchain: Input not set"); |
| 298 | + address guardian = _dsi.guardian(); |
| 299 | + bool paused = _dsi.paused(); |
| 300 | + |
| 301 | + ProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); |
| 302 | + SuperchainConfig superchainConfigImpl = _dso.superchainConfigImpl(); |
| 303 | + assertValidContractAddress(address(superchainProxyAdmin)); |
| 304 | + assertValidContractAddress(address(superchainConfigImpl)); |
| 305 | + |
| 306 | + vm.startBroadcast(msg.sender); |
| 307 | + SuperchainConfig superchainConfigProxy = SuperchainConfig(address(new Proxy(address(superchainProxyAdmin)))); |
| 308 | + superchainProxyAdmin.upgradeAndCall( |
| 309 | + payable(address(superchainConfigProxy)), |
| 310 | + address(superchainConfigImpl), |
| 311 | + abi.encodeCall(SuperchainConfig.initialize, (guardian, paused)) |
| 312 | + ); |
| 313 | + vm.stopBroadcast(); |
| 314 | + |
| 315 | + vm.label(address(superchainConfigProxy), "SuperchainConfigProxy"); |
| 316 | + _dso.set(_dso.superchainConfigProxy.selector, address(superchainConfigProxy)); |
| 317 | + } |
| 318 | + |
| 319 | + function deployAndInitializeProtocolVersions(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { |
| 320 | + require(_dsi.inputSet(), "DeploySuperchain: Input not set"); |
| 321 | + |
| 322 | + address protocolVersionsOwner = _dsi.protocolVersionsOwner(); |
| 323 | + ProtocolVersion requiredProtocolVersion = _dsi.requiredProtocolVersion(); |
| 324 | + ProtocolVersion recommendedProtocolVersion = _dsi.recommendedProtocolVersion(); |
| 325 | + |
| 326 | + ProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); |
| 327 | + ProtocolVersions protocolVersionsImpl = _dso.protocolVersionsImpl(); |
| 328 | + assertValidContractAddress(address(superchainProxyAdmin)); |
| 329 | + assertValidContractAddress(address(protocolVersionsImpl)); |
| 330 | + |
| 331 | + vm.startBroadcast(msg.sender); |
| 332 | + ProtocolVersions protocolVersionsProxy = ProtocolVersions(address(new Proxy(address(superchainProxyAdmin)))); |
| 333 | + superchainProxyAdmin.upgradeAndCall( |
| 334 | + payable(address(protocolVersionsProxy)), |
| 335 | + address(protocolVersionsImpl), |
| 336 | + abi.encodeCall( |
| 337 | + ProtocolVersions.initialize, |
| 338 | + (protocolVersionsOwner, requiredProtocolVersion, recommendedProtocolVersion) |
| 339 | + ) |
| 340 | + ); |
| 341 | + vm.stopBroadcast(); |
| 342 | + |
| 343 | + vm.label(address(protocolVersionsProxy), "ProtocolVersionsProxy"); |
| 344 | + _dso.set(_dso.protocolVersionsProxy.selector, address(protocolVersionsProxy)); |
| 345 | + } |
| 346 | + |
| 347 | + function transferProxyAdminOwnership(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { |
| 348 | + require(_dsi.inputSet(), "DeploySuperchain: Input not set"); |
| 349 | + address proxyAdminOwner = _dsi.proxyAdminOwner(); |
| 350 | + |
| 351 | + ProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); |
| 352 | + assertValidContractAddress(address(superchainProxyAdmin)); |
| 353 | + |
| 354 | + vm.broadcast(msg.sender); |
| 355 | + superchainProxyAdmin.transferOwnership(proxyAdminOwner); |
| 356 | + } |
| 357 | + |
| 358 | + // -------- Utilities -------- |
| 359 | + |
| 360 | + // This takes a sender and an identifier and returns a deterministic address based on the two. |
| 361 | + // The resulting used to etch the input and output contracts to a deterministic address based on |
| 362 | + // those two values, where the identifier represents the input or output contract, such as |
| 363 | + // `optimism.DeploySuperchainInput` or `optimism.DeployOPChainOutput`. |
| 364 | + function toIOAddress(address _sender, string memory _identifier) internal pure returns (address) { |
| 365 | + return address(uint160(uint256(keccak256(abi.encode(_sender, _identifier))))); |
| 366 | + } |
| 367 | + |
| 368 | + function etchIOContracts() internal returns (DeploySuperchainInput dsi_, DeploySuperchainOutput dso_) { |
| 369 | + (dsi_, dso_) = getIOContracts(); |
| 370 | + vm.etch(address(dsi_), type(DeploySuperchainInput).runtimeCode); |
| 371 | + vm.etch(address(dso_), type(DeploySuperchainOutput).runtimeCode); |
| 372 | + } |
| 373 | + |
| 374 | + function getIOContracts() public view returns (DeploySuperchainInput dsi_, DeploySuperchainOutput dso_) { |
| 375 | + dsi_ = DeploySuperchainInput(toIOAddress(msg.sender, "optimism.DeploySuperchainInput")); |
| 376 | + dso_ = DeploySuperchainOutput(toIOAddress(msg.sender, "optimism.DeploySuperchainOutput")); |
| 377 | + } |
| 378 | + |
| 379 | + function assertValidContractAddress(address _address) internal view { |
| 380 | + require(_address != address(0), "DeploySuperchain: zero address"); |
| 381 | + require(_address.code.length > 0, "DeploySuperchain: no code"); |
| 382 | + } |
| 383 | +} |
0 commit comments