Skip to content

Commit 185fb80

Browse files
authored
OPSM: Deploy Superchain, alternate approach (ethereum-optimism#11480)
* feat: initial DeloySuperchain script * chore: scaffold file-based interfaces * test: additional assertions * chore: appease semgrep * scaffold alternate approach * incorporate feedback * refactor based on feedback * fix tests * test: more robust testing * refactor: dedupe etching of IO contracts and add getter method
1 parent 933abde commit 185fb80

File tree

2 files changed

+490
-0
lines changed

2 files changed

+490
-0
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
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

Comments
 (0)