Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OPSM: Deploy Superchain #11468

Closed
wants to merge 4 commits into from
Closed
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
136 changes: 136 additions & 0 deletions packages/contracts-bedrock/scripts/DeploySuperchain.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { Script } from "forge-std/Script.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";

/// @notice Deploys the Superchain contracts that can be shared by many chains.
/// We intentionally use the terms "Input" and "Output" to clearly distinguish this script from the
/// existing ones that use terms of "Config" and "Artifacts".
contract DeploySuperchain is Script {
struct Roles {
address proxyAdminOwner;
address protocolVersionsOwner;
address guardian;
}

struct Input {
Roles roles;
bool paused;
ProtocolVersion requiredProtocolVersion;
ProtocolVersion recommendedProtocolVersion;
}

struct Output {
ProxyAdmin superchainProxyAdmin;
SuperchainConfig superchainConfigImpl;
SuperchainConfig superchainConfigProxy;
ProtocolVersions protocolVersionsImpl;
ProtocolVersions protocolVersionsProxy;
}

/// @notice This entrypoint is for end-users to deploy from an input file and write to an output file.
function run(string memory _infile) public returns (Output memory output_, string memory outfile_) {
Input memory _input = parseInputFile(_infile);
output_ = runWithoutIO(_input);
outfile_ = writeOutputFile(_infile, _input, output_);
require(false, "DeploySuperchain: run is not implemented");
}

/// @notice This entrypoint is useful for e2e testing purposes, and doesn't use any file I/O.
function runWithoutIO(Input memory _input) public returns (Output memory output_) {
Copy link
Contributor

@protolambda protolambda Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing input/output as memory, it would be nice to have it structured as:

  • a SuperchainConfig contract that has the attributes of Input (as public getters), and a method to load config from a file.
  • a SuperchainDeployment contract with setters to write the results to, and a method that writes the final output as file to disk.

And then etch in each contract to some address that is a hash of the contract name, maybe combined with the sender address (so configs/deployments of different tests don't collide).

This would then work well with the Go forge execution, where I can insert precompiles to form the Go config + capture the outputs, without IO, by overriding these input/output contracts with Go.

Any unit-tests could read the addresses from the SuperchainDeployment contract, not that different from reading from a contract struct variable.

And, we can then Semver the input/output contracts; versioned deploy configuration would be very nice to have.

And the setters/getters of a contract would then all be "type safe" from an ABI standpoint, whereas a memory/calldata struct can have a layout with two swapped address fields and it would be hard to tell apart. The ABI signature here is: runWithoutIO(((address,address,address),bool,uint256,uint256)).

But instead it can be one ABI per config variable, and adding/removing configurables and deployment outputs would be much easier to keep compatible (not multiple struct definitions, but rather just additional getters/setters).

And, even better would be to standardize around something like check() on the config input contract and the deployment output contract, where we can do some invariant checks (ensure inputs are correct, and ensure outputs are complete/consistent). Edit: Putting a check() in the DeploySuperchain contract that uses the input/output would both be easier (no need to source the inputs to check outputs), and allow the check() to run even when the config/output are overridden.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a shot at this alternate approach in #11480 (though it doesn't have check() yet), keeping both PRs in draft pending a decision

// Validate inputs.
require(_input.roles.proxyAdminOwner != address(0), "zero address: proxyAdminOwner");
require(_input.roles.protocolVersionsOwner != address(0), "zero address: protocolVersionsOwner");
require(_input.roles.guardian != address(0), "zero address: guardian");

// 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.startBroadcast(msg.sender);

output_.superchainProxyAdmin = new ProxyAdmin(msg.sender);
vm.label(address(output_.superchainProxyAdmin), "SuperchainProxyAdmin");

// Deploy implementation contracts.
output_.superchainConfigImpl = new SuperchainConfig();
output_.protocolVersionsImpl = new ProtocolVersions();

// Deploy and initialize the proxies.
output_.superchainConfigProxy = SuperchainConfig(address(new Proxy(address(output_.superchainProxyAdmin))));
vm.label(address(output_.superchainConfigProxy), "SuperchainConfigProxy");
output_.superchainProxyAdmin.upgradeAndCall(
payable(address(output_.superchainConfigProxy)),
address(output_.superchainConfigImpl),
abi.encodeCall(SuperchainConfig.initialize, (_input.roles.guardian, _input.paused))
);

output_.protocolVersionsProxy = ProtocolVersions(address(new Proxy(address(output_.superchainProxyAdmin))));
vm.label(address(output_.protocolVersionsProxy), "ProtocolVersionsProxy");
output_.superchainProxyAdmin.upgradeAndCall(
payable(address(output_.protocolVersionsProxy)),
address(output_.protocolVersionsImpl),
abi.encodeCall(
ProtocolVersions.initialize,
(_input.roles.protocolVersionsOwner, _input.requiredProtocolVersion, _input.recommendedProtocolVersion)
)
);

// Transfer ownership of the ProxyAdmin from the deployer to the specified owner.
output_.superchainProxyAdmin.transferOwnership(_input.roles.proxyAdminOwner);

vm.stopBroadcast();

// Output assertions, to make sure outputs were assigned correctly.
address[] memory addresses = new address[](5);
addresses[0] = address(output_.superchainProxyAdmin);
addresses[1] = address(output_.superchainConfigImpl);
addresses[2] = address(output_.superchainConfigProxy);
addresses[3] = address(output_.protocolVersionsImpl);
addresses[4] = address(output_.protocolVersionsProxy);

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

// 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("duplicates at: ", vm.toString(i), ",", vm.toString(j));
require(addresses[i] != addresses[j], err);
}
}
}

/// @notice This method is public for testing purposes, but there isn't a need for users to call this method
/// directly.
function parseInputFile(string memory _infile) public pure returns (Input memory input_) {
_infile;
input_;
require(false, "DeploySuperchain: parseInputFile is not implemented");
}

/// @notice Writes an output file, where the filename is based on the input filename, e.g. an input file of
/// `DeploySuperchain.in.toml` results in an output file of `DeploySuperchain.out.toml`.
function writeOutputFile(
string memory _infile,
Input memory _input,
Output memory _output
)
internal
pure
returns (string memory outfile_)
{
_infile;
_input;
_output;
outfile_;
require(false, "DeploySuperchain: writeOutputFile not implemented");
}
}
80 changes: 80 additions & 0 deletions packages/contracts-bedrock/test/DeploySuperchain.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { Test } from "forge-std/Test.sol";

import { Proxy } from "src/universal/Proxy.sol";
import { ProtocolVersion } from "src/L1/ProtocolVersions.sol";
import { DeploySuperchain } from "scripts/DeploySuperchain.s.sol";

/// @notice Deploys the Superchain contracts that can be shared by many chains.
contract DeploySuperchain_Test is Test {
DeploySuperchain deploySuperchain;

// Define a default input struct for testing.
DeploySuperchain.Input input = DeploySuperchain.Input({
roles: DeploySuperchain.Roles({
proxyAdminOwner: makeAddr("defaultProxyAdminOwner"),
protocolVersionsOwner: makeAddr("defaultProtocolVersionsOwner"),
guardian: makeAddr("defaultGuardian")
}),
paused: false,
requiredProtocolVersion: ProtocolVersion.wrap(1),
recommendedProtocolVersion: ProtocolVersion.wrap(2)
});

function setUp() public {
deploySuperchain = new DeploySuperchain();
}

function unwrap(ProtocolVersion _pv) internal pure returns (uint256) {
return ProtocolVersion.unwrap(_pv);
}

function test_runWithoutIO_succeeds(DeploySuperchain.Input memory _input) public {
vm.assume(_input.roles.proxyAdminOwner != address(0));
vm.assume(_input.roles.protocolVersionsOwner != address(0));
vm.assume(_input.roles.guardian != address(0));

DeploySuperchain.Output memory output = deploySuperchain.runWithoutIO(_input);

// Assert the inputs were properly set.
assertEq(address(output.superchainProxyAdmin.owner()), _input.roles.proxyAdminOwner, "100");
assertEq(address(output.protocolVersionsProxy.owner()), _input.roles.protocolVersionsOwner, "200");
assertEq(address(output.superchainConfigProxy.guardian()), _input.roles.guardian, "300");
assertEq(output.superchainConfigProxy.paused(), _input.paused, "400");
assertEq(unwrap(output.protocolVersionsProxy.required()), unwrap(_input.requiredProtocolVersion), "500");
assertEq(unwrap(output.protocolVersionsProxy.recommended()), unwrap(_input.recommendedProtocolVersion), "600");

// Architecture assertions.
// We prank as the zero address due to the Proxy's `proxyCallIfNotAdmin` modifier.
Proxy superchainConfigProxy = Proxy(payable(address(output.superchainConfigProxy)));
Proxy protocolVersionsProxy = Proxy(payable(address(output.protocolVersionsProxy)));
vm.startPrank(address(0));

assertEq(superchainConfigProxy.implementation(), address(output.superchainConfigImpl), "700");
assertEq(protocolVersionsProxy.implementation(), address(output.protocolVersionsImpl), "800");
assertEq(superchainConfigProxy.admin(), protocolVersionsProxy.admin(), "900");
assertEq(superchainConfigProxy.admin(), address(output.superchainProxyAdmin), "1000");
}

function test_runWithoutIOAndZeroAddressRoles_reverts() public {
// Snapshot the state so we can revert to the default `input` struct between assertions.
uint256 snapshotId = vm.snapshot();

// Assert over each role being set to the zero address.
input.roles.proxyAdminOwner = address(0);
vm.expectRevert("zero address: proxyAdminOwner");
deploySuperchain.runWithoutIO(input);

vm.revertTo(snapshotId);
input.roles.protocolVersionsOwner = address(0);
vm.expectRevert("zero address: protocolVersionsOwner");
deploySuperchain.runWithoutIO(input);

vm.revertTo(snapshotId);
input.roles.guardian = address(0);
vm.expectRevert("zero address: guardian");
deploySuperchain.runWithoutIO(input);
}
}