Commit 0c7d8a94 authored by Maurelian's avatar Maurelian Committed by GitHub

Deploy Security Council Safe and Liveness Extensions (#10120)

* ctb: Add deploySecurityCouncilSafe

* ctb: Allow _callViaSafe to use an arbitrary safe address

* ctb: Refactor Security Council deploy

Adds console.log calls

Uses the new _callViaSafe to reduce a lot of boilerplate

* ctb: Add livenessModuleInterval to deploy config

* ctb: Add livenessModuleThresholdPercentage to deploy config

* ctb: Add LivenessModuleMinOwners to deploy config

* ctb: Add council safe config and satisfy liveness module constructor

* ctb: Make liveness deploy funcs public

* Add DeployOwnership.sol

* Use deploySafe(name) to deploy SC and Foundation

* ctb: just use addr_ on all named returns

* ctb: Fix create2 collision in deploySafe

* ctb: Add example foundation config

* ctb: Add example council config and set it up

* ctb: Remove unused imports

* ctb: Address feedback on deploy ownership

* Add override for deploySafe with custom owners and threshold

ctb: Use the deploySafe override where applicable

* ctb: cleanup keepDeployer logic

* fixup! Add override for deploySafe with custom owners and threshold

* snapshots
parent 8a548ae9
......@@ -182,6 +182,13 @@ contract Deploy is Deployer {
/// @notice Gets the address of the SafeProxyFactory and Safe singleton for use in deploying a new GnosisSafe.
function _getSafeFactory() internal returns (SafeProxyFactory safeProxyFactory_, Safe safeSingleton_) {
if (getAddress("SafeProxyFactory") != address(0)) {
// The SafeProxyFactory is already saved, we can just use it.
safeProxyFactory_ = SafeProxyFactory(getAddress("SafeProxyFactory"));
safeSingleton_ = Safe(getAddress("SafeSingleton"));
return (safeProxyFactory_, safeSingleton_);
}
// These are the standard create2 deployed contracts. First we'll check if they are deployed,
// if not we'll deploy new ones, though not at these addresses.
address safeProxyFactory = 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2;
......@@ -198,13 +205,11 @@ contract Deploy is Deployer {
}
/// @notice Make a call from the Safe contract to an arbitrary address with arbitrary data
function _callViaSafe(address _target, bytes memory _data) internal {
Safe safe = Safe(mustGetAddress("SystemOwnerSafe"));
function _callViaSafe(Safe _safe, address _target, bytes memory _data) internal {
// This is the signature format used the caller is also the signer.
bytes memory signature = abi.encodePacked(uint256(uint160(msg.sender)), bytes32(0), uint8(1));
safe.execTransaction({
_safe.execTransaction({
to: _target,
value: 0,
data: _data,
......@@ -225,7 +230,8 @@ contract Deploy is Deployer {
bytes memory data =
abi.encodeCall(ProxyAdmin.upgradeAndCall, (payable(_proxy), _implementation, _innerCallData));
_callViaSafe({ _target: proxyAdmin, _data: data });
Safe safe = Safe(mustGetAddress("SystemOwnerSafe"));
_callViaSafe({ _safe: safe, _target: proxyAdmin, _data: data });
}
/// @notice Transfer ownership of the ProxyAdmin contract to the final system owner
......@@ -272,7 +278,7 @@ contract Deploy is Deployer {
}
/// @notice Internal function containing the deploy logic.
function _run() internal {
function _run() internal virtual {
console.log("start of L1 Deploy!");
deploySafe("SystemOwnerSafe");
console.log("deployed Safe!");
......@@ -416,23 +422,44 @@ contract Deploy is Deployer {
/// @notice Deploy the Safe
function deploySafe(string memory _name) public broadcast returns (address addr_) {
console.log("Deploying Safe");
address[] memory owners = new address[](0);
addr_ = deploySafe(_name, owners, 1, true);
}
function deploySafe(
string memory _name,
address[] memory _owners,
uint256 _threshold,
bool _keepDeployer
)
public
returns (address addr_)
{
console.log("Deploying safe: %s ", _name);
(SafeProxyFactory safeProxyFactory, Safe safeSingleton) = _getSafeFactory();
address[] memory signers = new address[](1);
signers[0] = msg.sender;
address[] memory expandedOwners = new address[](_owners.length + 1);
if (_keepDeployer) {
// By always adding msg.sender first we know that the previousOwner will be SENTINEL_OWNERS, which makes it
// easier to call removeOwner later.
expandedOwners[0] = msg.sender;
for (uint256 i = 0; i < _owners.length; i++) {
expandedOwners[i + 1] = _owners[i];
}
_owners = expandedOwners;
}
bytes memory initData = abi.encodeWithSelector(
Safe.setup.selector, signers, 1, address(0), hex"", address(0), address(0), 0, address(0)
bytes memory initData = abi.encodeCall(
Safe.setup, (_owners, _threshold, address(0), hex"", address(0), address(0), 0, payable(address(0)))
);
address safe = address(safeProxyFactory.createProxyWithNonce(address(safeSingleton), initData, block.timestamp));
save(_name, address(safe));
console.log(
string.concat("New safe: ", _name, " deployed at %s\n Note that this safe is owned by the deployer key"),
address(safe)
addr_ = address(
safeProxyFactory.createProxyWithNonce(
address(safeSingleton), initData, uint256(keccak256(abi.encode(_name)))
)
);
addr_ = safe;
save(_name, addr_);
console.log("New safe: %s deployed at %s\n Note that this safe is owned by the deployer key", _name, addr_);
}
/// @notice Deploy the AddressManager
......@@ -989,8 +1016,10 @@ contract Deploy is Deployer {
address superchainConfigProxy = mustGetAddress("SuperchainConfigProxy");
uint256 proxyType = uint256(proxyAdmin.proxyType(l1StandardBridgeProxy));
Safe safe = Safe(mustGetAddress("SystemOwnerSafe"));
if (proxyType != uint256(ProxyAdmin.ProxyType.CHUGSPLASH)) {
_callViaSafe({
_safe: safe,
_target: address(proxyAdmin),
_data: abi.encodeCall(ProxyAdmin.setProxyType, (l1StandardBridgeProxy, ProxyAdmin.ProxyType.CHUGSPLASH))
});
......@@ -1066,8 +1095,10 @@ contract Deploy is Deployer {
address optimismPortalProxy = mustGetAddress("OptimismPortalProxy");
uint256 proxyType = uint256(proxyAdmin.proxyType(l1CrossDomainMessengerProxy));
Safe safe = Safe(mustGetAddress("SystemOwnerSafe"));
if (proxyType != uint256(ProxyAdmin.ProxyType.RESOLVED)) {
_callViaSafe({
_safe: safe,
_target: address(proxyAdmin),
_data: abi.encodeCall(ProxyAdmin.setProxyType, (l1CrossDomainMessengerProxy, ProxyAdmin.ProxyType.RESOLVED))
});
......@@ -1078,6 +1109,7 @@ contract Deploy is Deployer {
string memory implName = proxyAdmin.implementationName(l1CrossDomainMessenger);
if (keccak256(bytes(contractName)) != keccak256(bytes(implName))) {
_callViaSafe({
_safe: safe,
_target: address(proxyAdmin),
_data: abi.encodeCall(ProxyAdmin.setImplementationName, (l1CrossDomainMessengerProxy, contractName))
});
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { console2 as console } from "forge-std/console2.sol";
import { stdJson } from "forge-std/StdJson.sol";
import { Safe } from "safe-contracts/Safe.sol";
import { OwnerManager } from "safe-contracts/base/OwnerManager.sol";
import { GuardManager } from "safe-contracts/base/GuardManager.sol";
import { Deployer } from "scripts/Deployer.sol";
import { LivenessGuard } from "src/Safe/LivenessGuard.sol";
import { LivenessModule } from "src/Safe/LivenessModule.sol";
import { Deploy } from "./Deploy.s.sol";
/// @notice Configuration for a Safe
struct SafeConfig {
uint256 threshold;
address[] owners;
}
/// @notice Configuration for the Security Council Safe.
struct SecurityCouncilConfig {
SafeConfig safeConfig;
uint256 livenessInterval;
uint256 thresholdPercentage;
uint256 minOwners;
address fallbackOwner;
}
// The sentinel address is used to mark the start and end of the linked list of owners in the Safe.
address constant SENTINEL_OWNERS = address(0x1);
/// @title Deploy
/// @notice Script used to deploy and configure the Safe contracts which are used to manage the Superchain,
/// as the ProxyAdminOwner and other roles in the system. Note that this script is not executable in a
/// production environment as some steps depend on having a quorum of signers available. This script is meant to
/// be used as an example to guide the setup and configuration of the Safe contracts.
contract DeployOwnership is Deploy {
/// @notice Internal function containing the deploy logic.
function _run() internal override {
console.log("start of Ownership Deployment");
deployAndConfigureFoundationSafe();
deployAndConfigureSecurityCouncilSafe();
console.log("Ownership contracts completed");
}
/// @notice Returns a SafeConfig similar to that of the Foundation Safe on Mainnet.
function _getExampleFoundationConfig() internal returns (SafeConfig memory safeConfig_) {
address[] memory exampleFoundationOwners = new address[](7);
for (uint256 i; i < exampleFoundationOwners.length; i++) {
exampleFoundationOwners[i] = makeAddr(string.concat("fnd-", vm.toString(i)));
}
safeConfig_ = SafeConfig({ threshold: 5, owners: exampleFoundationOwners });
}
/// @notice Returns a SafeConfig similar to that of the Security Council Safe on Mainnet.
function _getExampleCouncilConfig() internal returns (SecurityCouncilConfig memory councilConfig_) {
address[] memory exampleCouncilOwners = new address[](13);
for (uint256 i; i < exampleCouncilOwners.length; i++) {
exampleCouncilOwners[i] = makeAddr(string.concat("sc-", vm.toString(i)));
}
SafeConfig memory safeConfig = SafeConfig({ threshold: 10, owners: exampleCouncilOwners });
councilConfig_ = SecurityCouncilConfig({
safeConfig: safeConfig,
livenessInterval: 24 weeks,
thresholdPercentage: 75,
minOwners: 8,
fallbackOwner: mustGetAddress("FoundationSafe")
});
}
/// @notice Deploys a Safe with a configuration similar to that of the Foundation Safe on Mainnet.
function deployAndConfigureFoundationSafe() public returns (address addr_) {
SafeConfig memory exampleFoundationConfig = _getExampleFoundationConfig();
addr_ = deploySafe({
_name: "FoundationSafe",
_owners: exampleFoundationConfig.owners,
_threshold: exampleFoundationConfig.threshold,
_keepDeployer: false
});
console.log("Deployed and configured the Foundation Safe!");
}
/// @notice Deploy a LivenessGuard for use on the Security Council Safe.
/// Note this function does not have the broadcast modifier.
function deployLivenessGuard() public returns (address addr_) {
Safe councilSafe = Safe(payable(mustGetAddress("SecurityCouncilSafe")));
addr_ = address(new LivenessGuard(councilSafe));
save("LivenessGuard", address(addr_));
console.log("New LivenessGuard deployed at %s", address(addr_));
}
/// @notice Deploy a LivenessModule for use on the Security Council Safe
/// Note this function does not have the broadcast modifier.
function deployLivenessModule() public returns (address addr_) {
Safe councilSafe = Safe(payable(mustGetAddress("SecurityCouncilSafe")));
address guard = mustGetAddress("LivenessGuard");
SecurityCouncilConfig memory councilConfig = _getExampleCouncilConfig();
addr_ = address(
new LivenessModule({
_safe: councilSafe,
_livenessGuard: LivenessGuard(guard),
_livenessInterval: councilConfig.livenessInterval,
_thresholdPercentage: councilConfig.thresholdPercentage,
_minOwners: councilConfig.minOwners,
_fallbackOwner: councilConfig.fallbackOwner
})
);
save("LivenessModule", address(addr_));
console.log("New LivenessModule deployed at %s", address(addr_));
}
/// @notice Deploy a Security Council with LivenessModule and LivenessGuard.
function deployAndConfigureSecurityCouncilSafe() public returns (address addr_) {
// Deploy the safe with the extra deployer key, and keep the threshold at 1 to allow for further setup.
SecurityCouncilConfig memory exampleCouncilConfig = _getExampleCouncilConfig();
Safe safe = Safe(
payable(
deploySafe({
_name: "SecurityCouncilSafe",
_owners: exampleCouncilConfig.safeConfig.owners,
_threshold: 1,
_keepDeployer: true
})
)
);
vm.startBroadcast();
address guard = deployLivenessGuard();
_callViaSafe({ _safe: safe, _target: address(safe), _data: abi.encodeCall(GuardManager.setGuard, (guard)) });
console.log("LivenessGuard setup on SecurityCouncilSafe");
// Remove the deployer address (msg.sender) which was used to setup the Security Council Safe thus far
// this call is also used to update the threshold.
// Because deploySafe() always adds msg.sender first (if keepDeployer is true), we know that the previousOwner
// will be SENTINEL_OWNERS.
_callViaSafe({
_safe: safe,
_target: address(safe),
_data: abi.encodeCall(
OwnerManager.removeOwner, (SENTINEL_OWNERS, msg.sender, exampleCouncilConfig.safeConfig.threshold)
)
});
address livenessModule = deployLivenessModule();
vm.stopBroadcast();
// Since we don't have private keys for the safe owners, we instead use 'startBroadcast' to do something
// similar to pranking as the safe. This simulates a quorum of signers executing a transation from the safe to
// call it's own 'enableModule' method.
vm.startBroadcast(address(safe));
safe.enableModule(livenessModule);
addr_ = address(safe);
console.log("Deployed and configured the Security Council Safe!");
}
}
......@@ -32,7 +32,7 @@ contract DeploymentSummary is DeploymentSummaryCode {
address internal constant superchainConfigProxyAddress = 0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809;
address internal constant systemConfigAddress = 0xB20f22D5eA3D6568d2e2AED04487b4DC15141C00;
address internal constant systemConfigProxyAddress = 0x1c23A6d89F95ef3148BCDA8E242cAb145bf9c0E4;
address internal constant systemOwnerSafeAddress = 0x2601573C28B77dea6C8B73385c25024A28a00C3F;
address internal constant systemOwnerSafeAddress = 0x7d039be7F9b5190147621b02e82B250e1D748e02;
function recreateDeployment() public {
bytes32 slot;
......@@ -73,7 +73,7 @@ contract DeploymentSummary is DeploymentSummaryCode {
value = hex"000000000000000000000000bb2180ebd78ce97360503434ed37fcf4a1df61c3";
vm.store(proxyAdminAddress, slot, value);
slot = hex"0000000000000000000000000000000000000000000000000000000000000000";
value = hex"0000000000000000000000002601573c28b77dea6c8b73385c25024a28a00c3f";
value = hex"0000000000000000000000007d039be7f9b5190147621b02e82b250e1d748e02";
vm.store(proxyAdminAddress, slot, value);
vm.etch(superchainConfigProxyAddress, superchainConfigProxyCode);
slot = hex"b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment