From 254692a8419b528ee146d935aed40288bda17f94 Mon Sep 17 00:00:00 2001 From: clabby Date: Sun, 2 Apr 2023 23:11:05 -0400 Subject: [PATCH 1/2] Add `PostSherlockL2` script --- packages/contracts-bedrock/foundry.toml | 4 +- packages/contracts-bedrock/package.json | 2 + .../scripts/PostSherlockL2.s.sol | 492 ++++++++++++++++++ 3 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 packages/contracts-bedrock/scripts/PostSherlockL2.s.sol diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index a6df10b4237a2..d8f6442f1e2ea 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -9,7 +9,9 @@ remappings = [ '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/', '@rari-capital/solmate/=node_modules/@rari-capital/solmate', 'forge-std/=node_modules/forge-std/src', - 'ds-test/=node_modules/ds-test/src' + 'ds-test/=node_modules/ds-test/src', + '@safe-global/safe-contracts=node_modules/@safe-global/safe-contracts/', + 'solady/=node_modules/solady/src' ] extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout'] bytecode_hash = 'none' diff --git a/packages/contracts-bedrock/package.json b/packages/contracts-bedrock/package.json index 400378e7d0fb1..fdb0b6318c412 100644 --- a/packages/contracts-bedrock/package.json +++ b/packages/contracts-bedrock/package.json @@ -80,6 +80,8 @@ "forge-std": "https://github.com/foundry-rs/forge-std.git#46264e9788017fc74f9f58b7efa0bc6e1df6d410", "glob": "^7.1.6", "hardhat-deploy": "^0.11.4", + "@safe-global/safe-contracts": "https://github.com/safe-global/safe-contracts.git#v1.3.0-libs.0", + "solady": "https://github.com/Vectorized/solady.git#v0.0.85", "solhint": "^3.3.7", "solhint-plugin-prettier": "^0.0.5", "ts-node": "^10.9.1", diff --git a/packages/contracts-bedrock/scripts/PostSherlockL2.s.sol b/packages/contracts-bedrock/scripts/PostSherlockL2.s.sol new file mode 100644 index 0000000000000..7a68b8f4fa9b7 --- /dev/null +++ b/packages/contracts-bedrock/scripts/PostSherlockL2.s.sol @@ -0,0 +1,492 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { console } from "forge-std/console.sol"; +import { Script } from "forge-std/Script.sol"; +import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol"; +import { GnosisSafe } from "@safe-global/safe-contracts/contracts/GnosisSafe.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; +import { Enum } from "@safe-global/safe-contracts/contracts/common/Enum.sol"; +import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol"; +import { Constants } from "../../contracts/libraries/Constants.sol"; +import { Predeploys } from "../../contracts/libraries/Predeploys.sol"; +import { SystemConfig } from "../../contracts/L1/SystemConfig.sol"; +import { ResourceMetering } from "../../contracts/L1/ResourceMetering.sol"; +import { Semver } from "../../contracts/universal/Semver.sol"; + +/** + * @title PostSherlockL2 + * @notice Upgrade script for upgrading the L2 predeploy implementations after the sherlock audit. + * Assumes that a gnosis safe is used as the privileged account and the same + * gnosis safe is the owner the proxy admin. + * This could be optimized by checking for the number of approvals up front + * and not submitting the final approval as `execTransaction` can be called when + * there are `threshold - 1` approvals. + * Uses the "approved hashes" method of interacting with the gnosis safe. Allows + * for the most simple user experience when using automation and no indexer. + * Run the command without the `--broadcast` flag and it will print a tenderly URL. + */ +contract PostSherlockL2 is Script { + /** + * @notice Interface for multicall3. + */ + IMulticall3 private constant multicall = IMulticall3(MULTICALL3_ADDRESS); + + /** + * @notice OP Mainnet chain id. + */ + uint256 constant OP_MAINNET = 10; + + /** + * @notice OP Goerli chain id. + */ + uint256 constant OP_GOERLI = 420; + + /** + * @notice Represents a set of L2 predepploy contracts. Used to represent a set of + * implementations and also a set of proxies. + */ + struct ContractSet { + address BaseFeeVault; + address GasPriceOracle; + address L1Block; + address L1FeeVault; + address L2CrossDomainMessenger; + address L2ERC721Bridge; + address L2StandardBridge; + address L2ToL1MessagePasser; + address SequencerFeeVault; + address OptimismMintableERC20Factory; + address OptimismMintableERC721Factory; + } + + /** + * @notice A mapping of chainid to a ContractSet of implementations. + */ + mapping(uint256 => ContractSet) internal implementations; + + /** + * @notice A mapping of chainid to ContractSet of proxy addresses. + */ + mapping(uint256 => ContractSet) internal proxies; + + /** + * @notice An array of approvals, used to generate the execution transaction. + */ + address[] internal approvals; + + /** + * @notice The expected versions for the contracts to be upgraded to. + */ + string constant internal BaseFeeVault_Version = "1.0.0"; + string constant internal GasPriceOracle_Version = "1.0.0"; + string constant internal L1Block_Version = "1.0.0"; + string constant internal L1FeeVault_Version = "1.0.0"; + string constant internal L2CrossDomainMessenger_Version = "1.1.0"; + string constant internal L2ERC721Bridge_Version = "1.1.0"; + string constant internal L2StandardBridge_Version = "1.1.0"; + string constant internal L2ToL1MessagePasser_Version = "1.0.0"; + string constant internal SequencerFeeVault_Version = "1.0.0"; + string constant internal OptimismMintableERC20Factory_Version = "1.1.0"; + string constant internal OptimismMintableERC721Factory_Version = "1.1.0"; + + /** + * @notice Place the contract addresses in storage so they can be used when building calldata. + */ + function setUp() external { + // TODO: Set goerli implementations once they're deployed. + implementations[OP_GOERLI] = ContractSet({ + BaseFeeVault: 0xEcBb01757B6b7799465a422aD0fC7Fd5F5179F0a, + GasPriceOracle: 0x79f09f735B2d1a42fF864C014d3bD4aA5FAA6A5E, + L1Block: 0xd5F2B9f6Ee80065b2Ce18bF1e629c5aC1C98c7F6, + L1FeeVault: 0x9bA5E286934F0A29fb2f8421f60d3eE8A853447C, + L2CrossDomainMessenger: 0xDe90fE30325588D895Ee4c2E862E703e165a01c7, + L2ERC721Bridge: 0x777adA49d40DAC02AE5b4FdC292feDf9066435A3, + L2StandardBridge: 0x3EA657c5aA0E4Bce1D8919dC7f248724d7B0987a, + L2ToL1MessagePasser: 0xEF2ec5A5465f075E010BE70966a8667c94BCe15a, + SequencerFeeVault: 0x4781674AAe242bbDf6C58b81Cf4F06F1534cd37d, + OptimismMintableERC20Factory: 0xeDF90ac13642e6445955b79CdDA321ecB136b29B, + OptimismMintableERC721Factory: 0x795F355F75f9B28AEC6cC6A887704191e630065b + }); + + proxies[OP_GOERLI] = ContractSet({ + BaseFeeVault: Predeploys.BASE_FEE_VAULT, + GasPriceOracle: Predeploys.GAS_PRICE_ORACLE, + L1Block: Predeploys.L1_BLOCK_ATTRIBUTES, + L1FeeVault: Predeploys.L1_FEE_VAULT, + L2CrossDomainMessenger: Predeploys.L2_CROSS_DOMAIN_MESSENGER, + L2ERC721Bridge: Predeploys.L2_ERC721_BRIDGE, + L2StandardBridge: Predeploys.L2_STANDARD_BRIDGE, + L2ToL1MessagePasser: Predeploys.L2_TO_L1_MESSAGE_PASSER, + SequencerFeeVault: Predeploys.SEQUENCER_FEE_WALLET, + OptimismMintableERC20Factory: Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY, + OptimismMintableERC721Factory: Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY + }); + } + + /** + * @notice The entrypoint to this script. + */ + function run(address _safe, address _proxyAdmin) external returns (bool) { + vm.startBroadcast(); + bool success = _run(_safe, _proxyAdmin); + if (success) _postCheck(); + return success; + } + + /** + * @notice The implementation of the upgrade. Split into its own function + * to allow for testability. This is subject to a race condition if + * the nonce changes by a different transaction finalizing while not + * all of the signers have used this script. + */ + function _run(address _safe, address _proxyAdmin) public returns (bool) { + // Ensure that the required contracts exist + require(address(multicall).code.length > 0, "multicall3 not deployed"); + require(_safe.code.length > 0, "no code at safe address"); + require(_proxyAdmin.code.length > 0, "no code at proxy admin address"); + + GnosisSafe safe = GnosisSafe(payable(_safe)); + uint256 nonce = safe.nonce(); + + bytes memory data = buildCalldata(_proxyAdmin); + + // Compute the safe transaction hash + bytes32 hash = safe.getTransactionHash({ + to: address(multicall), + value: 0, + data: data, + operation: Enum.Operation.DelegateCall, + safeTxGas: 0, + baseGas: 0, + gasPrice: 0, + gasToken: address(0), + refundReceiver: address(0), + _nonce: nonce + }); + + // Send a transaction to approve the hash + safe.approveHash(hash); + + logSimulationLink({ + _to: address(safe), + _from: msg.sender, + _data: abi.encodeCall(safe.approveHash, (hash)) + }); + + uint256 threshold = safe.getThreshold(); + address[] memory owners = safe.getOwners(); + + for (uint256 i; i < owners.length; i++) { + address owner = owners[i]; + uint256 approved = safe.approvedHashes(owner, hash); + if (approved == 1) { + approvals.push(owner); + } + } + + if (approvals.length >= threshold) { + bytes memory signatures = buildSignatures(); + + bool success = safe.execTransaction({ + to: address(multicall), + value: 0, + data: data, + operation: Enum.Operation.DelegateCall, + safeTxGas: 0, + baseGas: 0, + gasPrice: 0, + gasToken: address(0), + refundReceiver: payable(address(0)), + signatures: signatures + }); + + logSimulationLink({ + _to: address(safe), + _from: msg.sender, + _data: abi.encodeCall( + safe.execTransaction, + ( + address(multicall), + 0, + data, + Enum.Operation.DelegateCall, + 0, + 0, + 0, + address(0), + payable(address(0)), + signatures + ) + ) + }); + + require(success, "call not successful"); + return true; + } else { + console.log("not enough approvals"); + } + + // Reset the approvals because they are only used transiently. + assembly { + sstore(approvals.slot, 0) + } + + return false; + } + + /** + * @notice Log a tenderly simulation link. The TENDERLY_USERNAME and TENDERLY_PROJECT + * environment variables will be used if they are present. The vm is staticcall'ed + * because of a compiler issue with the higher level ABI. + */ + function logSimulationLink(address _to, bytes memory _data, address _from) public view { + (, bytes memory projData) = VM_ADDRESS.staticcall( + abi.encodeWithSignature("envOr(string,string)", "TENDERLY_PROJECT", "TENDERLY_PROJECT") + ); + string memory proj = abi.decode(projData, (string)); + + (, bytes memory userData) = VM_ADDRESS.staticcall( + abi.encodeWithSignature("envOr(string,string)", "TENDERLY_USERNAME", "TENDERLY_USERNAME") + ); + string memory username = abi.decode(userData, (string)); + + string memory str = string.concat( + "https://dashboard.tenderly.co/", + username, + "/", + proj, + "/simulator/new?network=", + vm.toString(block.chainid), + "&contractAddress=", + vm.toString(_to), + "&rawFunctionInput=", + vm.toString(_data), + "&from=", + vm.toString(_from) + ); + console.log(str); + } + + /** + * @notice Follow up assertions to ensure that the script ran to completion. + */ + function _postCheck() internal view { + ContractSet memory prox = getProxies(); + require(_versionHash(prox.BaseFeeVault) == keccak256(bytes(BaseFeeVault_Version))); + require(_versionHash(prox.GasPriceOracle) == keccak256(bytes(GasPriceOracle_Version))); + require(_versionHash(prox.L1Block) == keccak256(bytes(L1Block_Version))); + require(_versionHash(prox.L1FeeVault) == keccak256(bytes(L1FeeVault_Version))); + require(_versionHash(prox.L2CrossDomainMessenger) == keccak256(bytes(L2CrossDomainMessenger_Version))); + require(_versionHash(prox.L2ERC721Bridge) == keccak256(bytes(L2ERC721Bridge_Version))); + require(_versionHash(prox.L2StandardBridge) == keccak256(bytes(L2StandardBridge_Version))); + require(_versionHash(prox.L2ToL1MessagePasser) == keccak256(bytes(L2ToL1MessagePasser_Version))); + require(_versionHash(prox.SequencerFeeVault) == keccak256(bytes(SequencerFeeVault_Version))); + require(_versionHash(prox.OptimismMintableERC20Factory) == keccak256(bytes(OptimismMintableERC20Factory_Version))); + require(_versionHash(prox.OptimismMintableERC721Factory) == keccak256(bytes(OptimismMintableERC721Factory_Version))); + } + + /** + * @notice Helper function used to compute the hash of Semver's version string to be used in a + * comparison. + */ + function _versionHash(address _addr) internal view returns (bytes32) { + return keccak256(bytes(Semver(_addr).version())); + } + + /** + * @notice Test coverage of the logic. Should only run on goerli but other chains + * could be added. + */ + function test_script_succeeds() skipWhenNotForking external { + address safe; + address proxyAdmin; + + if (block.chainid == OP_GOERLI) { + safe = 0xE534ccA2753aCFbcDBCeB2291F596fc60495257e; + proxyAdmin = 0x4200000000000000000000000000000000000018; + } + + require(safe != address(0) && proxyAdmin != address(0)); + + address[] memory owners = GnosisSafe(payable(safe)).getOwners(); + + for (uint256 i; i < owners.length; i++) { + address owner = owners[i]; + vm.startBroadcast(owner); + bool success = _run(safe, proxyAdmin); + vm.stopBroadcast(); + + if (success) { + console.log("tx success"); + break; + } + } + + _postCheck(); + } + + /** + * @notice Builds the signatures by tightly packing them together. + * Ensures that they are sorted. + */ + function buildSignatures() internal view returns (bytes memory) { + address[] memory addrs = new address[](approvals.length); + for (uint256 i; i < approvals.length; i++) { + addrs[i] = approvals[i]; + } + + LibSort.sort(addrs); + + bytes memory signatures; + uint8 v = 1; + bytes32 s = bytes32(0); + for (uint256 i; i < addrs.length; i++) { + bytes32 r = bytes32(uint256(uint160(addrs[i]))); + signatures = bytes.concat(signatures, abi.encodePacked(r, s, v)); + } + return signatures; + } + + /** + * @notice Builds the calldata that the multisig needs to make for the upgrade to happen. + * A total of 9 calls are made to the proxy admin to upgrade the implementations + * of the predeploys. + */ + function buildCalldata(address _proxyAdmin) internal view returns (bytes memory) { + IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](11); + + ContractSet memory impl = getImplementations(); + ContractSet memory prox = getProxies(); + + // Upgrade the BaseFeeVault + calls[0] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.BaseFeeVault), impl.BaseFeeVault) + ) + }); + + // Upgrade the GasPriceOracle + calls[1] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.GasPriceOracle), impl.GasPriceOracle) + ) + }); + + // Upgrade the L1Block predeploy + calls[2] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.L1Block), impl.L1Block) + ) + }); + + // Upgrade the L1FeeVault + calls[3] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.L1FeeVault), impl.L1FeeVault) + ) + }); + + // Upgrade the L2CrossDomainMessenger + calls[4] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.L2CrossDomainMessenger), impl.L2CrossDomainMessenger) + ) + }); + + // Upgrade the L2ERC721Bridge + calls[5] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.L2ERC721Bridge), impl.L2ERC721Bridge) + ) + }); + + // Upgrade the L2StandardBridge + calls[6] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.L2StandardBridge), impl.L2StandardBridge) + ) + }); + + // Upgrade the L2ToL1MessagePasser + calls[7] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.L2ToL1MessagePasser), impl.L2ToL1MessagePasser) + ) + }); + + // Upgrade the SequencerFeeVault + calls[8] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.SequencerFeeVault), impl.SequencerFeeVault) + ) + }); + + // Upgrade the OptimismMintableERC20Factory + calls[9] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.OptimismMintableERC20Factory), impl.OptimismMintableERC20Factory) + ) + }); + + // Upgrade the OptimismMintableERC721Factory + calls[10] = IMulticall3.Call3({ + target: _proxyAdmin, + allowFailure: false, + callData: abi.encodeCall( + ProxyAdmin.upgrade, + (payable(prox.OptimismMintableERC721Factory), impl.OptimismMintableERC721Factory) + ) + }); + + return abi.encodeCall(IMulticall3.aggregate3, (calls)); + } + + /** + * @notice Returns the ContractSet that represents the implementations for a given network. + */ + function getImplementations() internal view returns (ContractSet memory) { + ContractSet memory set = implementations[block.chainid]; + require(set.BaseFeeVault != address(0), "no implementations for this network"); + return set; + } + + /** + * @notice Returns the ContractSet that represents the proxies for a given network. + */ + function getProxies() internal view returns (ContractSet memory) { + ContractSet memory set = proxies[block.chainid]; + require(set.BaseFeeVault != address(0), "no proxies for this network"); + return set; + } +} From 4a2a14865394675a1d62d836567915e416d92b34 Mon Sep 17 00:00:00 2001 From: clabby Date: Sun, 2 Apr 2023 23:43:02 -0400 Subject: [PATCH 2/2] Update lockfile --- yarn.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/yarn.lock b/yarn.lock index f684f3b83a1b2..b6b4b1c76e42a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3760,6 +3760,10 @@ "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" ethers "^5.7.2" +"@safe-global/safe-contracts@https://github.com/safe-global/safe-contracts.git#v1.3.0-libs.0": + version "1.3.0" + resolved "https://github.com/safe-global/safe-contracts.git#767ef36bba88bdbc0c9fe3708a4290cabef4c376" + "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.7.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.7.0.tgz#2af52f1bc73759b1b6a549fed598781c8c5fce72" @@ -19354,6 +19358,10 @@ socks@^2.3.3, socks@^2.6.1: ip "^1.1.5" smart-buffer "^4.1.0" +"solady@https://github.com/Vectorized/solady.git#v0.0.85": + version "0.0.85" + resolved "https://github.com/Vectorized/solady.git#507e0d84872f435d497e6d2ce10e7f484392db4f" + solc@0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/solc/-/solc-0.7.3.tgz#04646961bd867a744f63d2b4e3c0701ffdc7d78a"