Skip to content

Commit

Permalink
OPSM: Deploy Superchain, alternate approach (ethereum-optimism#11480)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mds1 authored Aug 20, 2024
1 parent 933abde commit 185fb80
Show file tree
Hide file tree
Showing 2 changed files with 490 additions and 0 deletions.
383 changes: 383 additions & 0 deletions packages/contracts-bedrock/scripts/DeploySuperchain.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { Script } from "forge-std/Script.sol";
import { LibString } from "@solady/utils/LibString.sol";

import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
import { ProtocolVersions, ProtocolVersion } from "src/L1/ProtocolVersions.sol";
import { ProxyAdmin } from "src/universal/ProxyAdmin.sol";
import { Proxy } from "src/universal/Proxy.sol";

/**
* This comment block defines the requirements and rationale for the architecture used in this forge
* script, along with other scripts that are being written as new Superchain-first deploy scripts to
* complement the OP Stack Manager. The script architecture is a bit different than a standard forge
* deployment script.
*
* There are three categories of users that are expected to interact with the scripts:
* 1. End users that want to run live contract deployments.
* 2. Solidity developers that want to use or test these script in a standard forge test environment.
* 3. Go developers that want to run the deploy scripts as part of e2e testing with other aspects of the OP Stack.
*
* We want each user to interact with the scripts in the way that's simplest for their use case:
* 1. End users: TOML input files that define config, and TOML output files with all output data.
* 2. Solidity developers: Direct calls to the script with input structs and output structs.
* 3. Go developers: The forge scripts can be executed directly in Go.
*
* The following architecture is used to meet the requirements of each user. We use this file's
* `DeploySuperchain` script as an example, but it applies to other scripts as well.
*
* This `DeploySuperchain.s.sol` file contains three contracts:
* 1. `DeploySuperchainInput`: Responsible for parsing, storing, and exposing the input data.
* 2. `DeploySuperchainOutput`: Responsible for storing and exposing the output data.
* 3. `DeploySuperchain`: The core script that executes the deployment. It reads inputs from the
* input contract, and writes outputs to the output contract.
*
* Because the core script performs calls to the input and output contracts, Go developers can
* intercept calls to these addresses (analogous to how forge intercepts calls to the `Vm` address
* to execute cheatcodes), to avoid the need for file I/O or hardcoding the input/output structs.
*
* Public getter methods on the input and output contracts allow individual fields to be accessed
* in a strong, type-safe manner (as opposed to a single struct getter where the caller may
* inadvertently transpose two addresses, for example).
*
* Each deployment step in the core deploy script is modularized into its own function that performs
* the deploy and sets the output on the Output contract, allowing for easy composition and testing
* of deployment steps. The output setter methods requires keying off the four-byte selector of the
* each output field's getter method, ensuring that the output is set for the correct field and
* minimizing the amount of boilerplate needed for each output field.
*
* This script doubles as a reference for documenting the pattern used and therefore contains
* comments explaining the patterns used. Other scripts are not expected to have this level of
* documentation.
*
* Additionally, we intentionally use "Input" and "Output" terminology to clearly distinguish these
* scripts from the existing ones that "Config" and "Artifacts" terminology.
*/
contract DeploySuperchainInput {
// The input struct contains all the input data required for the deployment.
struct Input {
Roles roles;
bool paused;
ProtocolVersion requiredProtocolVersion;
ProtocolVersion recommendedProtocolVersion;
}

struct Roles {
address proxyAdminOwner;
address protocolVersionsOwner;
address guardian;
}

// This flag tells us if all inputs have been set. An `input()` getter method that returns all
// inputs reverts if this flag is false. This ensures the deploy script cannot proceed until all
// inputs are validated and set.
bool public inputSet = false;

// The full input struct is kept in storage. It is not exposed because the return type would be
// a tuple, but it's more convenient for the return type to be the struct itself. Therefore the
// struct is exposed via the `input()` getter method.
Input internal inputs;

// And each field is exposed via it's own getter method. We can equivalently remove these
// storage variables and add getter methods that return the input struct fields directly, but
// that is more verbose with more boilerplate, especially for larger scripts with many inputs.
// Unlike the `input()` getter, these getters do not revert if the input is not set. The caller
// should check the `inputSet` value before calling any of these getters.
address public proxyAdminOwner;
address public protocolVersionsOwner;
address public guardian;
bool public paused;
ProtocolVersion public requiredProtocolVersion;
ProtocolVersion public recommendedProtocolVersion;

// Load the input from a TOML file.
function loadInputFile(string memory _infile) public {
_infile;
Input memory parsedInput;
loadInput(parsedInput);
require(false, "DeploySuperchainInput: loadInput is not implemented");
}

// Load the input from a struct.
function loadInput(Input memory _input) public {
// As a defensive measure, we only allow inputs to be set once.
require(!inputSet, "DeploySuperchainInput: Input already set");

// All assertions on inputs happen here. You cannot set any inputs in Solidity unless
// they're all valid. For Go testing, the input and outputs
require(_input.roles.proxyAdminOwner != address(0), "DeploySuperchainInput: Null proxyAdminOwner");
require(_input.roles.protocolVersionsOwner != address(0), "DeploySuperchainInput: Null protocolVersionsOwner");
require(_input.roles.guardian != address(0), "DeploySuperchainInput: Null guardian");

// We now set all values in storage.
inputSet = true;
inputs = _input;

proxyAdminOwner = _input.roles.proxyAdminOwner;
protocolVersionsOwner = _input.roles.protocolVersionsOwner;
guardian = _input.roles.guardian;
paused = _input.paused;
requiredProtocolVersion = _input.requiredProtocolVersion;
recommendedProtocolVersion = _input.recommendedProtocolVersion;
}

function input() public view returns (Input memory) {
require(inputSet, "DeploySuperchainInput: Input not set");
return inputs;
}
}

contract DeploySuperchainOutput {
// The output struct contains all the output data from the deployment.
struct Output {
ProxyAdmin superchainProxyAdmin;
SuperchainConfig superchainConfigImpl;
SuperchainConfig superchainConfigProxy;
ProtocolVersions protocolVersionsImpl;
ProtocolVersions protocolVersionsProxy;
}

// We use a similar pattern as the input contract to expose outputs. Because outputs are set
// individually, and deployment steps are modular and composable, we do not have an equivalent
// to the overall `input` and `inputSet` variables.
ProxyAdmin public superchainProxyAdmin;
SuperchainConfig public superchainConfigImpl;
SuperchainConfig public superchainConfigProxy;
ProtocolVersions public protocolVersionsImpl;
ProtocolVersions public protocolVersionsProxy;

// This method lets each field be set individually. The selector of an output's getter method
// is used to determine which field to set.
function set(bytes4 sel, address _address) public {
if (sel == this.superchainProxyAdmin.selector) superchainProxyAdmin = ProxyAdmin(_address);
else if (sel == this.superchainConfigImpl.selector) superchainConfigImpl = SuperchainConfig(_address);
else if (sel == this.superchainConfigProxy.selector) superchainConfigProxy = SuperchainConfig(_address);
else if (sel == this.protocolVersionsImpl.selector) protocolVersionsImpl = ProtocolVersions(_address);
else if (sel == this.protocolVersionsProxy.selector) protocolVersionsProxy = ProtocolVersions(_address);
else revert("DeploySuperchainOutput: Unknown selector");
}

// Save the output to a TOML file.
function writeOutputFile(string memory _outfile) public pure {
_outfile;
require(false, "DeploySuperchainOutput: saveOutput not implemented");
}

function output() public view returns (Output memory) {
return Output({
superchainProxyAdmin: superchainProxyAdmin,
superchainConfigImpl: superchainConfigImpl,
superchainConfigProxy: superchainConfigProxy,
protocolVersionsImpl: protocolVersionsImpl,
protocolVersionsProxy: protocolVersionsProxy
});
}

function checkOutput() public view {
// Assert that all addresses are non-zero and have code.
// We use LibString to avoid the need for adding cheatcodes to this contract.
address[] memory addresses = new address[](5);
addresses[0] = address(superchainProxyAdmin);
addresses[1] = address(superchainConfigImpl);
addresses[2] = address(superchainConfigProxy);
addresses[3] = address(protocolVersionsImpl);
addresses[4] = address(protocolVersionsProxy);

for (uint256 i = 0; i < addresses.length; i++) {
address who = addresses[i];
require(who != address(0), string.concat("check failed: zero address at index ", LibString.toString(i)));
require(
who.code.length > 0, string.concat("check failed: no code at ", LibString.toHexStringChecksummed(who))
);
}

// All addresses should be unique.
for (uint256 i = 0; i < addresses.length; i++) {
for (uint256 j = i + 1; j < addresses.length; j++) {
string memory err =
string.concat("check failed: duplicates at ", LibString.toString(i), ",", LibString.toString(j));
require(addresses[i] != addresses[j], err);
}
}
}
}

// For all broadcasts in this script we explicitly specify the deployer as `msg.sender` because for
// testing we deploy this script from a test contract. If we provide no argument, the foundry
// default sender is be the broadcaster during test, but the broadcaster needs to be the deployer
// since they are set to the initial proxy admin owner.
contract DeploySuperchain is Script {
// -------- Core Deployment Methods --------

// This entrypoint is for end-users to deploy from an input file and write to an output file.
// In this usage, we don't need the input and output contract functionality, so we deploy them
// here and abstract that architectural detail away from the end user.
function run(string memory _infile) public {
// End-user without file IO, so etch the IO helper contracts.
(DeploySuperchainInput dsi, DeploySuperchainOutput dso) = etchIOContracts();

// Load the input file into the input contract.
dsi.loadInputFile(_infile);

// Run the deployment script and write outputs to the DeploySuperchainOutput contract.
run(dsi, dso);

// Write the output data to a file. The file
string memory outfile = ""; // This will be derived from input file name, e.g. `foo.in.toml` -> `foo.out.toml`
dso.writeOutputFile(outfile);
require(false, "DeploySuperchain: run is not implemented");
}

// This entrypoint is for use with Solidity tests, where the input and outputs are structs.
function run(DeploySuperchainInput.Input memory _input) public returns (DeploySuperchainOutput.Output memory) {
// Solidity without file IO, so etch the IO helper contracts.
(DeploySuperchainInput dsi, DeploySuperchainOutput dso) = etchIOContracts();

// Load the input struct into the input contract.
dsi.loadInput(_input);

// Run the deployment script and write outputs to the DeploySuperchainOutput contract.
run(dsi, dso);

// Return the output struct from the output contract.
return dso.output();
}

// This entrypoint is useful for testing purposes, as it doesn't use any file I/O.
function run(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public {
// Verify that the input contract has been set.
require(_dsi.inputSet(), "DeploySuperchain: Input not set");

// Deploy the proxy admin, with the owner set to the deployer.
deploySuperchainProxyAdmin(_dsi, _dso);

// Deploy and initialize the superchain contracts.
deploySuperchainImplementationContracts(_dsi, _dso);
deployAndInitializeSuperchainConfig(_dsi, _dso);
deployAndInitializeProtocolVersions(_dsi, _dso);

// Transfer ownership of the ProxyAdmin from the deployer to the specified owner.
transferProxyAdminOwnership(_dsi, _dso);

// Output assertions, to make sure outputs were assigned correctly.
_dso.checkOutput();
}

// -------- Deployment Steps --------

function deploySuperchainProxyAdmin(DeploySuperchainInput, DeploySuperchainOutput _dso) public {
// Deploy the proxy admin, with the owner set to the deployer.
// We explicitly specify the deployer as `msg.sender` because for testing we deploy this script from a test
// contract. If we provide no argument, the foundry default sender is be the broadcaster during test, but the
// broadcaster needs to be the deployer since they are set to the initial proxy admin owner.
vm.broadcast(msg.sender);
ProxyAdmin superchainProxyAdmin = new ProxyAdmin(msg.sender);

vm.label(address(superchainProxyAdmin), "SuperchainProxyAdmin");
_dso.set(_dso.superchainProxyAdmin.selector, address(superchainProxyAdmin));
}

function deploySuperchainImplementationContracts(DeploySuperchainInput, DeploySuperchainOutput _dso) public {
// Deploy implementation contracts.
vm.startBroadcast(msg.sender);
SuperchainConfig superchainConfigImpl = new SuperchainConfig();
ProtocolVersions protocolVersionsImpl = new ProtocolVersions();
vm.stopBroadcast();

vm.label(address(superchainConfigImpl), "SuperchainConfigImpl");
vm.label(address(protocolVersionsImpl), "ProtocolVersionsImpl");

_dso.set(_dso.superchainConfigImpl.selector, address(superchainConfigImpl));
_dso.set(_dso.protocolVersionsImpl.selector, address(protocolVersionsImpl));
}

function deployAndInitializeSuperchainConfig(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public {
require(_dsi.inputSet(), "DeploySuperchain: Input not set");
address guardian = _dsi.guardian();
bool paused = _dsi.paused();

ProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin();
SuperchainConfig superchainConfigImpl = _dso.superchainConfigImpl();
assertValidContractAddress(address(superchainProxyAdmin));
assertValidContractAddress(address(superchainConfigImpl));

vm.startBroadcast(msg.sender);
SuperchainConfig superchainConfigProxy = SuperchainConfig(address(new Proxy(address(superchainProxyAdmin))));
superchainProxyAdmin.upgradeAndCall(
payable(address(superchainConfigProxy)),
address(superchainConfigImpl),
abi.encodeCall(SuperchainConfig.initialize, (guardian, paused))
);
vm.stopBroadcast();

vm.label(address(superchainConfigProxy), "SuperchainConfigProxy");
_dso.set(_dso.superchainConfigProxy.selector, address(superchainConfigProxy));
}

function deployAndInitializeProtocolVersions(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public {
require(_dsi.inputSet(), "DeploySuperchain: Input not set");

address protocolVersionsOwner = _dsi.protocolVersionsOwner();
ProtocolVersion requiredProtocolVersion = _dsi.requiredProtocolVersion();
ProtocolVersion recommendedProtocolVersion = _dsi.recommendedProtocolVersion();

ProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin();
ProtocolVersions protocolVersionsImpl = _dso.protocolVersionsImpl();
assertValidContractAddress(address(superchainProxyAdmin));
assertValidContractAddress(address(protocolVersionsImpl));

vm.startBroadcast(msg.sender);
ProtocolVersions protocolVersionsProxy = ProtocolVersions(address(new Proxy(address(superchainProxyAdmin))));
superchainProxyAdmin.upgradeAndCall(
payable(address(protocolVersionsProxy)),
address(protocolVersionsImpl),
abi.encodeCall(
ProtocolVersions.initialize,
(protocolVersionsOwner, requiredProtocolVersion, recommendedProtocolVersion)
)
);
vm.stopBroadcast();

vm.label(address(protocolVersionsProxy), "ProtocolVersionsProxy");
_dso.set(_dso.protocolVersionsProxy.selector, address(protocolVersionsProxy));
}

function transferProxyAdminOwnership(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public {
require(_dsi.inputSet(), "DeploySuperchain: Input not set");
address proxyAdminOwner = _dsi.proxyAdminOwner();

ProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin();
assertValidContractAddress(address(superchainProxyAdmin));

vm.broadcast(msg.sender);
superchainProxyAdmin.transferOwnership(proxyAdminOwner);
}

// -------- Utilities --------

// This takes a sender and an identifier and returns a deterministic address based on the two.
// The resulting used to etch the input and output contracts to a deterministic address based on
// those two values, where the identifier represents the input or output contract, such as
// `optimism.DeploySuperchainInput` or `optimism.DeployOPChainOutput`.
function toIOAddress(address _sender, string memory _identifier) internal pure returns (address) {
return address(uint160(uint256(keccak256(abi.encode(_sender, _identifier)))));
}

function etchIOContracts() internal returns (DeploySuperchainInput dsi_, DeploySuperchainOutput dso_) {
(dsi_, dso_) = getIOContracts();
vm.etch(address(dsi_), type(DeploySuperchainInput).runtimeCode);
vm.etch(address(dso_), type(DeploySuperchainOutput).runtimeCode);
}

function getIOContracts() public view returns (DeploySuperchainInput dsi_, DeploySuperchainOutput dso_) {
dsi_ = DeploySuperchainInput(toIOAddress(msg.sender, "optimism.DeploySuperchainInput"));
dso_ = DeploySuperchainOutput(toIOAddress(msg.sender, "optimism.DeploySuperchainOutput"));
}

function assertValidContractAddress(address _address) internal view {
require(_address != address(0), "DeploySuperchain: zero address");
require(_address.code.length > 0, "DeploySuperchain: no code");
}
}
Loading

0 comments on commit 185fb80

Please sign in to comment.