From 492f4f7bcdb60341f833e8dd3d13bbfaab2f6e2e Mon Sep 17 00:00:00 2001 From: Michael de Hoog Date: Fri, 18 Apr 2025 12:04:31 +1000 Subject: [PATCH] Reintroduce nested multisig contracts for backwards compatibility --- script/deploy/l1/SetGasLimit.sol | 4 +- .../universal/DoubleNestedMultisigBuilder.sol | 54 ++ script/universal/MultisigBuilder.sol | 716 +----------------- script/universal/MultisigScript.sol | 597 +++++++++++++++ script/universal/NestedMultisigBuilder.sol | 45 ++ .../DoubleNestedMultisigBuilder.t.sol | 4 +- test/universal/NestedMultisigBuilder.t.sol | 4 +- 7 files changed, 708 insertions(+), 716 deletions(-) create mode 100644 script/universal/DoubleNestedMultisigBuilder.sol create mode 100644 script/universal/MultisigScript.sol create mode 100644 script/universal/NestedMultisigBuilder.sol diff --git a/script/deploy/l1/SetGasLimit.sol b/script/deploy/l1/SetGasLimit.sol index e7587b7..f56199d 100644 --- a/script/deploy/l1/SetGasLimit.sol +++ b/script/deploy/l1/SetGasLimit.sol @@ -2,10 +2,10 @@ pragma solidity 0.8.15; import {SystemConfig} from "@eth-optimism-bedrock/src/L1/SystemConfig.sol"; -import {MultisigBuilder, IMulticall3, IGnosisSafe, Simulation} from "../../universal/MultisigBuilder.sol"; +import {MultisigScript, IMulticall3, IGnosisSafe, Simulation} from "../../universal/MultisigScript.sol"; import {Vm} from "forge-std/Vm.sol"; -contract SetGasLimit is MultisigBuilder { +contract SetGasLimit is MultisigScript { address internal SYSTEM_CONFIG_OWNER = vm.envAddress("SYSTEM_CONFIG_OWNER"); address internal L1_SYSTEM_CONFIG = vm.envAddress("L1_SYSTEM_CONFIG_ADDRESS"); diff --git a/script/universal/DoubleNestedMultisigBuilder.sol b/script/universal/DoubleNestedMultisigBuilder.sol new file mode 100644 index 0000000..b16af66 --- /dev/null +++ b/script/universal/DoubleNestedMultisigBuilder.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {MultisigScript} from "./MultisigScript.sol"; + +/** + * @title DoubleNestedMultisigBuilder + * @custom:deprecated Use `MultisigScript` instead. + */ +abstract contract DoubleNestedMultisigBuilder is MultisigScript { + /* + * @custom:deprecated Use `sign(address[] memory _safes)` instead. + */ + function sign(address _signerSafe, address _intermediateSafe) public { + sign(_toArray(_signerSafe, _intermediateSafe)); + } + + /* + * @custom:deprecated Use `verify(address[] memory _safes, bytes memory _signatures)` instead. + */ + function verify(address _signerSafe, address _intermediateSafe, bytes memory _signatures) public view { + verify(_toArray(_signerSafe, _intermediateSafe), _signatures); + } + + /* + * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. + */ + function approveOnBehalfOfSignerSafe(address _signerSafe, address _intermediateSafe, bytes memory _signatures) + public + { + approve(_toArray(_signerSafe, _intermediateSafe), _signatures); + } + + /* + * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. + */ + function approveOnBehalfOfIntermediateSafe(address _intermediateSafe) public { + approve(_toArray(_intermediateSafe), ""); + } + + /* + * @custom:deprecated Use `simulate(bytes memory _signatures)` instead, with empty `_signatures`. + */ + function simulate() public { + simulate(""); + } + + /* + * @custom:deprecated Use `run(bytes memory _signatures)` instead, with empty `_signatures`. + */ + function run() public { + run(""); + } +} diff --git a/script/universal/MultisigBuilder.sol b/script/universal/MultisigBuilder.sol index e9a55d5..542802b 100644 --- a/script/universal/MultisigBuilder.sol +++ b/script/universal/MultisigBuilder.sol @@ -1,729 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -// solhint-disable no-console -import {console} from "forge-std/console.sol"; -import {Script} from "forge-std/Script.sol"; -import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; -import {Vm} from "forge-std/Vm.sol"; - -import {IGnosisSafe, Enum} from "./IGnosisSafe.sol"; -import {Signatures} from "./Signatures.sol"; -import {Simulation} from "./Simulation.sol"; +import {MultisigScript} from "./MultisigScript.sol"; /** * @title MultisigBuilder - * @notice Script builder for Forge scripts that require signatures from Safes. Supports both non-nested - * Safes, as well as nested Safes of arbitrary depth (Safes where the signers are other Safes). - * - * 1. Non-nested example: - * - * Setup: - * ┌───────┐┌───────┐ - * │Signer1││Signer2│ - * └┬──────┘└┬──────┘ - * ┌▽────────▽┐ - * │Multisig │ - * └┬─────────┘ - * ┌▽─────────┐ - * │ProxyAdmin│ - * └──────────┘ - * - * Sequence: - * ┌───────┐┌───────┐┌───────────┐┌───────────────┐ - * │Signer1││Signer2││Facilitator││MultisigBuilder│ - * └───┬───┘└───┬───┘└─────┬─────┘└───────┬───────┘ - * │ │ sign() │ - * │─────────────────────────────────>│ - * │ │ │ - * │──────────────────>│ │ - * │ │ sign() │ - * │ │────────────────────────>│ - * │ │ │ │ - * │ │─────────>│ │ - * │ │ │run(sig1,sig2)│ - * │ │ │─────────────>│ - * - * - * 2. Single-layer nested example: - * - * Setup: - * ┌───────┐┌───────┐┌───────┐┌───────┐ - * │Signer1││Signer2││Signer3││Signer4│ - * └┬──────┘└┬──────┘└┬──────┘└┬──────┘ - * ┌▽────────▽┐┌──────▽────────▽┐ - * │Safe1 ││Safe2 │ - * └┬─────────┘└┬───────────────┘ - * ┌▽───────────▽┐ - * │Safe3 │ - * └┬────────────┘ - * ┌▽─────────┐ - * │ProxyAdmin│ - * └──────────┘ - * - * Sequence: - * ┌───────┐┌───────┐┌───────┐┌───────┐┌───────────┐ ┌───────────────┐ - * │Signer1││Signer2││Signer3││Signer4││Facilitator│ │MultisigBuilder│ - * └───┬───┘└───┬───┘└───┬───┘└───┬───┘└─────┬─────┘ └───────┬───────┘ - * │ │ │ sign(Safe1) │ │ - * │─────────────────────────────────────────────────────────────>│ - * │ │ │ │ │ - * │────────────────────────────────────>│ │ - * │ │ │ │ sign(Safe1) │ - * │ │────────────────────────────────────────────────────>│ - * │ │ │ │ │ │ - * │ │───────────────────────────>│ │ - * │ │ │ │ │approve(Safe1,sig1|sig2)│ - * │ │ │ │ │───────────────────────>│ - * │ │ │ │ sign(Safe2) │ - * │ │ │───────────────────────────────────────────>│ - * │ │ │ │ │ - * │ │ │──────────────────>│ │ - * │ │ │ │ │ sign(Safe2) │ - * │ │ │ │──────────────────────────────────>│ - * │ │ │ │ │ │ - * │ │ │ │─────────>│ │ - * │ │ │ │ │approve(Safe2,sig3|sig4)│ - * │ │ │ │ │───────────────────────>│ - * │ │ │ │ │ run() │ - * │ │ │ │ │───────────────────────>│ - * - * - * 3. Multi-layer nested example: - * - * Setup: - * ┌───────┐┌───────┐┌───────┐┌───────┐┌───────┐┌───────┐ - * │Signer1││Signer2││Signer3││Signer4││Signer5││Signer6│ - * └┬──────┘└┬──────┘└┬──────┘└┬──────┘└┬──────┘└┬──────┘ - * ┌▽────────▽┐┌──────▽────────▽┐┌──────▽────────▽┐ - * │Safe1 ││Safe2 ││Safe3 │ - * └┬─────────┘└┬───────────────┘└┬───────────────┘ - * ┌▽───────────▽┐ │ - * │Safe4 │ │ - * └┬────────────┘ │ - * ┌▽─────────────────────────────▽┐ - * │Safe5 │ - * └┬──────────────────────────────┘ - * ┌▽─────────┐ - * │ProxyAdmin│ - * └──────────┘ - * - * Sequence: - * ┌───────┐┌───────┐┌───────┐┌───────┐┌───────┐┌───────┐┌───────────┐ ┌───────────────┐ - * │Signer1││Signer2││Signer3││Signer4││Signer5││Signer6││Facilitator│ │MultisigBuilder│ - * └───┬───┘└───┬───┘└───┬───┘└───┬───┘└───┬───┘└───┬───┘└─────┬─────┘ └───────┬───────┘ - * │ │ │ │ sign(Safe1,Safe4) │ │ - * │─────────────────────────────────────────────────────────────────────────────────────>│ - * │ │ │ │ │ │ │ - * │──────────────────────────────────────────────────────>│ │ - * │ │ │ │ │ sign(Safe1,Safe4) │ - * │ │────────────────────────────────────────────────────────────────────────────>│ - * │ │ │ │ │ │ │ │ - * │ │─────────────────────────────────────────────>│ │ - * │ │ │ │ │ │ │approve(Safe1,Safe4,sig1|sig2)│ - * │ │ │ │ │ │ │─────────────────────────────>│ - * │ │ │ │ │ sign(Safe2,Safe4) │ - * │ │ │───────────────────────────────────────────────────────────────────>│ - * │ │ │ │ │ │ │ - * │ │ │────────────────────────────────────>│ │ - * │ │ │ │ │ │ sign(Safe2,Safe4) │ - * │ │ │ │──────────────────────────────────────────────────────────>│ - * │ │ │ │ │ │ │ │ - * │ │ │ │───────────────────────────>│ │ - * │ │ │ │ │ │ │approve(Safe2,Safe4,sig3|sig4)│ - * │ │ │ │ │ │ │─────────────────────────────>│ - * │ │ │ │ │ │ │ approve(Safe4) │ - * │ │ │ │ │ │ │─────────────────────────────>│ - * │ │ │ │ │ │ sign(Safe3) │ - * │ │ │ │ │─────────────────────────────────────────────────>│ - * │ │ │ │ │ │ │ - * │ │ │ │ │──────────────────>│ │ - * │ │ │ │ │ │ │ sign(Safe3) │ - * │ │ │ │ │ │────────────────────────────────────────>│ - * │ │ │ │ │ │ │ │ - * │ │ │ │ │ │─────────>│ │ - * │ │ │ │ │ │ │ approve(Safe3,sig5|sig6) │ - * │ │ │ │ │ │ │─────────────────────────────>│ - * │ │ │ │ │ │ │ run() │ - * │ │ │ │ │ │ │─────────────────────────────>│ + * @custom:deprecated Use `MultisigScript` instead. */ -abstract contract MultisigBuilder is Script { - bytes32 internal constant SAFE_NONCE_SLOT = bytes32(uint256(5)); - - /* - * @dev Event emitted from a `sign()` call containing the data to sign. Used in testing. - */ - event DataToSign(bytes); - - /** - * ----------------------------------------------------------- - * Virtual Functions - * ----------------------------------------------------------- - */ - - /** - * @notice Returns the safe address to execute the final transaction from - */ - function _ownerSafe() internal view virtual returns (address); - - /** - * @notice Creates the calldata for signatures (`sign`), approvals (`approve`), and execution (`run`) - */ - function _buildCalls() internal view virtual returns (IMulticall3.Call3[] memory); - - /** - * @notice Follow up assertions to ensure that the script ran to completion. - * @dev Called after `sign` and `run`, but not `approve`. - */ - function _postCheck(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual; - - /** - * @notice Follow up assertions on state and simulation after a `sign` call. - */ - function _postSign(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual {} - - /** - * @notice Follow up assertions on state and simulation after a `approve` call. - */ - function _postApprove(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual {} - - /** - * @notice Follow up assertions on state and simulation after a `run` call. - */ - function _postRun(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual {} - - // Tenderly simulations can accept generic state overrides. This hook enables this functionality. - // By default, an empty (no-op) override is returned. - function _simulationOverrides() internal view virtual returns (Simulation.StateOverride[] memory overrides_) {} - - /** - * ----------------------------------------------------------- - * Public Functions - * ----------------------------------------------------------- - */ - - /** - * Step 1 - * ====== - * Generate a transaction approval data to sign. This method should be called by a threshold of - * multisig owners. - * - * For non-nested multisigs, the signatures can then be used to execute the transaction (see step 3). - * - * For nested multisigs, the signatures can be used to execute an approval transaction for each - * multisig (see step 2). - * - * @param _safes A list of nested safes (excluding the executing safe returned by `_ownerSafe`). - */ - function sign(address[] memory _safes) public { - _safes = _appendOwnerSafe(_safes); - - // Snapshot and restore Safe nonce after simulation, otherwise the data logged to sign - // would not match the actual data we need to sign, because the simulation - // would increment the nonce. - uint256[] memory originalNonces = new uint256[](_safes.length); - for (uint256 i = 0; i < _safes.length; i++) { - originalNonces[i] = _getNonce(_safes[i]); - } - - bytes[] memory datas = _transactionDatas(_safes); - (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = _simulateForSigner(_safes, datas); - - _postSign(accesses, simPayload); - _postCheck(accesses, simPayload); - - // Restore the original nonce. - for (uint256 i = 0; i < _safes.length; i++) { - vm.store(_safes[i], SAFE_NONCE_SLOT, bytes32(originalNonces[i])); - } - - _printDataToSign(_safes[0], datas[0]); - } - +abstract contract MultisigBuilder is MultisigScript { /* - * Same as `sign()` for a double layer of nesting. Provided for backwards compatibility with - * the old `DoubleNestedMultisigBuilder`. - * - * @custom:deprecated Use `sign(address[] memory _safes)` instead. - */ - function sign(address _signerSafe, address _intermediateSafe) public { - sign(_toArray(_signerSafe, _intermediateSafe)); - } - - /* - * Same as `sign()` for a single layer of nesting. Provided for backwards compatibility with - * the old `NestedMultisigBuilder`. - * - * @custom:deprecated Use `sign(address[] memory _safes)` instead. - */ - function sign(address _signerSafe) public { - sign(_toArray(_signerSafe)); - } - - /* - * Same as `sign()` for non-nested safes. Provided for backwards compatibility with - * the old `MultisigBuilder`. - * - * @custom:deprecated Use `sign(address[] memory _safes)` instead. + * @custom:deprecated Use `sign(address[] memory _safes)` instead, with an empty array. */ function sign() public { sign(new address[](0)); } - /** - * Step 1.1 (optional) - * ====== - * Verify the signatures generated from step 1 are valid. - * This allows transactions to be pre-signed and stored safely before execution. - * - * @param _safes A list of nested safes (excluding the executing safe returned by `_ownerSafe`). - * @param _signatures The signatures to verify (concatenated, 65-bytes per sig). - */ - function verify(address[] memory _safes, bytes memory _signatures) public view { - _safes = _appendOwnerSafe(_safes); - bytes[] memory datas = _transactionDatas(_safes); - _checkSignatures(_safes[0], datas[0], _signatures); - } - - /* - * Same as `verify()` for a double layer of nesting. Provided for backwards compatibility with - * the old `DoubleNestedMultisigBuilder`. - * - * @custom:deprecated Use `verify(address[] memory _safes, bytes memory _signatures)` instead. - */ - function verify(address _signerSafe, address _intermediateSafe, bytes memory _signatures) public view { - verify(_toArray(_signerSafe, _intermediateSafe), _signatures); - } - /* - * Same as `verify()` for a single layer of nesting. Provided for backwards compatibility with - * the old `NestedMultisigBuilder`. - * - * @custom:deprecated Use `verify(address[] memory _safes, bytes memory _signatures)` instead. - */ - function verify(address _signerSafe, bytes memory _signatures) public view { - verify(_toArray(_signerSafe), _signatures); - } - - /* - * Same as `verify()` for non-nested safes. Provided for backwards compatibility with - * the old `MultisigBuilder`. - * - * @custom:deprecated Use `verify(address[] memory _safes, bytes memory _signatures)` instead. + * @custom:deprecated Use `verify(address[] memory _safes, bytes memory _signatures)` instead, + * with an empty array. */ function verify(bytes memory _signatures) public view { verify(new address[](0), _signatures); } - - /** - * Step 2 (optional for non-nested setups) - * ====== - * Execute an approval transaction. This method should be called by a facilitator - * (non-signer), once for each of the multisigs involved in the nested multisig, - * after collecting a threshold of signatures for each multisig (see step 1). - * - * For multiple layers of nesting, this should be called for each layer of nesting (once - * the inner multisigs have registered their approval). The array of safes passed to - * `_safes` should get smaller by one for each layer of nesting. - * - * @param _safes A list of nested safes (excluding the executing safe returned by `_ownerSafe`). - * @param _signatures The signatures from step 1 (concatenated, 65-bytes per sig) - */ - function approve(address[] memory _safes, bytes memory _signatures) public { - _safes = _appendOwnerSafe(_safes); - bytes[] memory datas = _transactionDatas(_safes); - (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = - _executeTransaction(_safes[0], datas[0], _signatures, true); - _postApprove(accesses, simPayload); - } - - /* - * Same as `approve()` for a double layer of nesting. Provided for backwards compatibility with - * the old `DoubleNestedMultisigBuilder`. - * - * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. - */ - function approve(address _signerSafe, address _intermediateSafe, bytes memory _signatures) public { - approve(_toArray(_signerSafe, _intermediateSafe), _signatures); - } - - /* - * Same as `approve()` for a single layer of nesting. Provided for backwards compatibility with - * the old `NestedMultisigBuilder`. - * - * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. - */ - function approve(address _signerSafe, bytes memory _signatures) public { - approve(_toArray(_signerSafe), _signatures); - } - - /* - * Same as `approve()` for a double layer of nesting, with the signatures already approved. - * Provided for backwards compatibility with the old `DoubleNestedMultisigBuilder`. - * - * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. - */ - function approve(address _intermediateSafe) public { - approve(_toArray(_intermediateSafe), ""); - } - - /* - * Same as `approve()` for a double layer of nesting. Provided for backwards compatibility with - * the old `DoubleNestedMultisigBuilder`. - * - * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. - */ - function approveOnBehalfOfSignerSafe(address _signerSafe, address _intermediateSafe, bytes memory _signatures) - public - { - approve(_toArray(_signerSafe, _intermediateSafe), _signatures); - } - - /* - * Same as `approve()` for a double layer of nesting, with the signatures already approved. - * Provided for backwards compatibility with the old `DoubleNestedMultisigBuilder`. - * - * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. - */ - function approveOnBehalfOfIntermediateSafe(address _intermediateSafe) public { - approve(_toArray(_intermediateSafe), ""); - } - - /** - * Step 2.1 (optional) - * ====== - * Simulate the transaction. This method should be called by a facilitator (non-signer), after all of the - * signatures have been collected (non-nested case, see step 1), or the approval transactions have been - * submitted onchain (nested case, see step 2, in which case `_signatures` can be empty). - * - * Differs from `run` in that you can override the safe nonce for simulation purposes. - */ - function simulate(bytes memory _signatures) public { - address ownerSafe = _ownerSafe(); - bytes[] memory datas = _transactionDatas(_toArray(ownerSafe)); - - vm.store(ownerSafe, SAFE_NONCE_SLOT, bytes32(_getNonce(ownerSafe))); - - (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = - _executeTransaction(ownerSafe, datas[0], _signatures, false); - - _postRun(accesses, simPayload); - _postCheck(accesses, simPayload); - } - - /* - * Same as `simulate()` for nested setups (that have been approved in step 2). Provided for - * backwards compatibility with the old `NestedMultisigBuilder` and `DoubleNestedMultisigBuilder`. - * - * @custom:deprecated Use `simulate(bytes memory _signatures)` instead, with empty `_signatures`. - */ - function simulate() public { - simulate(""); - } - - /** - * Step 3 - * ====== - * Execute the transaction. This method should be called by a facilitator (non-signer), after all of the - * signatures have been collected (non-nested case, see step 1), or the approval transactions have been - * submitted onchain (nested case, see step 2, in which case `_signatures` can be empty). - */ - function run(bytes memory _signatures) public { - address ownerSafe = _ownerSafe(); - bytes[] memory datas = _transactionDatas(_toArray(ownerSafe)); - - (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = - _executeTransaction(ownerSafe, datas[0], _signatures, true); - - _postRun(accesses, simPayload); - _postCheck(accesses, simPayload); - } - - /* - * Same as `run()` for nested setups (that have been approved in step 2). Provided for - * backwards compatibility with the old `NestedMultisigBuilder` and `DoubleNestedMultisigBuilder`. - * - * @custom:deprecated Use `run(bytes memory _signatures)` instead, with empty `_signatures`. - */ - function run() public { - run(""); - } - - /** - * ----------------------------------------------------------- - * Internal Functions - * ----------------------------------------------------------- - */ - function _appendOwnerSafe(address[] memory _safes) internal view returns (address[] memory) { - address[] memory safes = new address[](_safes.length + 1); - for (uint256 i = 0; i < _safes.length; i++) { - safes[i] = _safes[i]; - } - safes[safes.length - 1] = _ownerSafe(); - return safes; - } - - function _transactionDatas(address[] memory _safes) private view returns (bytes[] memory) { - IMulticall3.Call3[] memory calls = _buildCalls(); - bytes[] memory datas = new bytes[](_safes.length); - for (uint256 i = _safes.length; i > 0; i--) { - if (i < _safes.length) { - // all outer safes should generate an approval for the inner safe: - calls = new IMulticall3.Call3[](1); - calls[0] = _generateApproveCall(_safes[i], datas[i]); - } - datas[i - 1] = abi.encodeCall(IMulticall3.aggregate3, (calls)); - } - return datas; - } - - function _generateApproveCall(address _safe, bytes memory _data) internal view returns (IMulticall3.Call3 memory) { - bytes32 hash = _getTransactionHash(_safe, _data); - - console.log("---\nNested hash for safe %s:", _safe); - console.logBytes32(hash); - - return IMulticall3.Call3({ - target: _safe, - allowFailure: false, - callData: abi.encodeCall(IGnosisSafe(_safe).approveHash, (hash)) - }); - } - - function _printDataToSign(address _safe, bytes memory _data) internal { - bytes memory txData = _encodeTransactionData(_safe, _data); - bytes32 hash = _getTransactionHash(_safe, _data); - - emit DataToSign(txData); - - console.log("---\nIf submitting onchain, call Safe.approveHash on %s with the following hash:", _safe); - console.logBytes32(hash); - - console.log("---\nData to sign:"); - console.log("vvvvvvvv"); - console.logBytes(txData); - console.log("^^^^^^^^\n"); - - console.log("########## IMPORTANT ##########"); - console.log( - // solhint-disable-next-line max-line-length - "Please make sure that the 'Data to sign' displayed above matches what you see in the simulation and on your hardware wallet." - ); - console.log("This is a critical step that must not be skipped."); - console.log("###############################"); - } - - function _executeTransaction(address _safe, bytes memory _data, bytes memory _signatures, bool _broadcast) - internal - returns (Vm.AccountAccess[] memory, Simulation.Payload memory) - { - bytes32 hash = _getTransactionHash(_safe, _data); - _signatures = Signatures.prepareSignatures(_safe, hash, _signatures); - - bytes memory simData = _execTransactionCalldata(_safe, _data, _signatures); - Simulation.logSimulationLink({_to: _safe, _from: msg.sender, _data: simData}); - - vm.startStateDiffRecording(); - bool success = _execTransaction(_safe, _data, _signatures, _broadcast); - Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); - require(success, "MultisigBase::_executeTransaction: Transaction failed"); - require(accesses.length > 0, "MultisigBase::_executeTransaction: No state changes"); - - // This can be used to e.g. call out to the Tenderly API and get additional - // data about the state diff before broadcasting the transaction. - Simulation.Payload memory simPayload = Simulation.Payload({ - from: msg.sender, - to: _safe, - data: simData, - stateOverrides: new Simulation.StateOverride[](0) - }); - return (accesses, simPayload); - } - - function _simulateForSigner(address[] memory _safes, bytes[] memory _datas) - internal - returns (Vm.AccountAccess[] memory, Simulation.Payload memory) - { - IMulticall3.Call3[] memory calls = _simulateForSignerCalls(_safes, _datas); - - // Now define the state overrides for the simulation. - Simulation.StateOverride[] memory overrides = _overrides(_safes); - - bytes memory txData = abi.encodeCall(IMulticall3.aggregate3, (calls)); - console.log("---\nSimulation link:"); - // solhint-disable max-line-length - Simulation.logSimulationLink({_to: MULTICALL3_ADDRESS, _data: txData, _from: msg.sender, _overrides: overrides}); - - // Forge simulation of the data logged in the link. If the simulation fails - // we revert to make it explicit that the simulation failed. - Simulation.Payload memory simPayload = - Simulation.Payload({to: MULTICALL3_ADDRESS, data: txData, from: msg.sender, stateOverrides: overrides}); - Vm.AccountAccess[] memory accesses = Simulation.simulateFromSimPayload(simPayload); - return (accesses, simPayload); - } - - function _simulateForSignerCalls(address[] memory _safes, bytes[] memory _datas) - private - pure - returns (IMulticall3.Call3[] memory) - { - IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](_safes.length); - for (uint256 i = 0; i < _safes.length; i++) { - // first simulated signer is the multicall3 contract - address signer = i == 0 ? MULTICALL3_ADDRESS : _safes[i - 1]; - calls[i] = IMulticall3.Call3({ - target: _safes[i], - allowFailure: false, - callData: _execTransactionCalldata(_safes[i], _datas[i], Signatures.genPrevalidatedSignature(signer)) - }); - } - return (calls); - } - - function _overrides(address[] memory _safes) internal view returns (Simulation.StateOverride[] memory) { - Simulation.StateOverride[] memory simOverrides = _simulationOverrides(); - Simulation.StateOverride[] memory overrides = - new Simulation.StateOverride[](_safes.length + simOverrides.length); - for (uint256 i = 0; i < _safes.length; i++) { - address owner = i == 0 ? MULTICALL3_ADDRESS : address(0); - overrides[i] = _safeOverrides(_safes[i], owner); - } - for (uint256 i = 0; i < simOverrides.length; i++) { - overrides[i + _safes.length] = simOverrides[i]; - } - return overrides; - } - - // The state change simulation can set the threshold, owner address and/or nonce. - // This allows simulation of the final transaction by overriding the threshold to 1. - // State changes reflected in the simulation as a result of these overrides will - // not be reflected in the prod execution. - function _safeOverrides(address _safe, address _owner) - internal - view - virtual - returns (Simulation.StateOverride memory) - { - uint256 _nonce = _getNonce(_safe); - if (_owner == address(0)) { - return Simulation.overrideSafeThresholdAndNonce(_safe, _nonce); - } - return Simulation.overrideSafeThresholdOwnerAndNonce(_safe, _owner, _nonce); - } - - // Get the nonce to use for the given safe, for signing and simulations. - // - // If you override it, ensure that the behavior is correct for all contexts. - // As an example, if you are pre-signing a message that needs safe.nonce+1 (before - // safe.nonce is executed), you should explicitly set the nonce value with an env var. - // Overriding this method with safe.nonce+1 will cause issues upon execution because - // the transaction hash will differ from the one signed. - // - // The process for determining a nonce override is as follows: - // 1. We look for an env var of the name SAFE_NONCE_{UPPERCASE_SAFE_ADDRESS}. For example, - // SAFE_NONCE_0X6DF4742A3C28790E63FE933F7D108FE9FCE51EA4. - // 2. If it exists, we use it as the nonce override for the safe. - // 3. If it does not exist, we do the same for the SAFE_NONCE env var. - // 4. Otherwise we fallback to the safe's current nonce (no override). - function _getNonce(address _safe) internal view virtual returns (uint256 nonce) { - uint256 safeNonce = IGnosisSafe(_safe).nonce(); - nonce = safeNonce; - - // first try SAFE_NONCE - try vm.envUint("SAFE_NONCE") { - nonce = vm.envUint("SAFE_NONCE"); - } catch {} - - // then try SAFE_NONCE_{UPPERCASE_SAFE_ADDRESS} - string memory envVarName = string.concat("SAFE_NONCE_", vm.toUppercase(vm.toString(_safe))); - try vm.envUint(envVarName) { - nonce = vm.envUint(envVarName); - } catch {} - - // print if any override - if (nonce != safeNonce) { - console.log("Overriding nonce for safe %s: %d -> %d", _safe, safeNonce, nonce); - } - } - - function _checkSignatures(address _safe, bytes memory _data, bytes memory _signatures) internal view { - bytes32 hash = _getTransactionHash(_safe, _data); - _signatures = Signatures.prepareSignatures(_safe, hash, _signatures); - IGnosisSafe(_safe).checkSignatures({dataHash: hash, data: _data, signatures: _signatures}); - } - - function _getTransactionHash(address _safe, bytes memory _data) internal view returns (bytes32) { - return keccak256(_encodeTransactionData(_safe, _data)); - } - - function _encodeTransactionData(address _safe, bytes memory _data) internal view returns (bytes memory) { - return IGnosisSafe(_safe).encodeTransactionData({ - to: MULTICALL3_ADDRESS, - value: 0, - data: _data, - operation: Enum.Operation.DelegateCall, - safeTxGas: 0, - baseGas: 0, - gasPrice: 0, - gasToken: address(0), - refundReceiver: address(0), - _nonce: _getNonce(_safe) - }); - } - - function _execTransactionCalldata(address _safe, bytes memory _data, bytes memory _signatures) - internal - pure - returns (bytes memory) - { - return abi.encodeCall( - IGnosisSafe(_safe).execTransaction, - ( - MULTICALL3_ADDRESS, - 0, - _data, - Enum.Operation.DelegateCall, - 0, - 0, - 0, - address(0), - payable(address(0)), - _signatures - ) - ); - } - - function _execTransaction(address _safe, bytes memory _data, bytes memory _signatures, bool _broadcast) - internal - returns (bool) - { - if (_broadcast) { - vm.broadcast(); - } - return IGnosisSafe(_safe).execTransaction({ - to: MULTICALL3_ADDRESS, - value: 0, - data: _data, - operation: Enum.Operation.DelegateCall, - safeTxGas: 0, - baseGas: 0, - gasPrice: 0, - gasToken: address(0), - refundReceiver: payable(address(0)), - signatures: _signatures - }); - } - - function _toArray(address _address) internal pure returns (address[] memory) { - address[] memory array = new address[](1); - array[0] = _address; - return array; - } - - function _toArray(address _address1, address _address2) internal pure returns (address[] memory) { - address[] memory array = new address[](2); - array[0] = _address1; - array[1] = _address2; - return array; - } } diff --git a/script/universal/MultisigScript.sol b/script/universal/MultisigScript.sol new file mode 100644 index 0000000..bba3224 --- /dev/null +++ b/script/universal/MultisigScript.sol @@ -0,0 +1,597 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +// solhint-disable no-console +import {console} from "forge-std/console.sol"; +import {Script} from "forge-std/Script.sol"; +import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {IGnosisSafe, Enum} from "./IGnosisSafe.sol"; +import {Signatures} from "./Signatures.sol"; +import {Simulation} from "./Simulation.sol"; + +/** + * @title MultisigBuilder + * @notice Script builder for Forge scripts that require signatures from Safes. Supports both non-nested + * Safes, as well as nested Safes of arbitrary depth (Safes where the signers are other Safes). + * + * 1. Non-nested example: + * + * Setup: + * ┌───────┐┌───────┐ + * │Signer1││Signer2│ + * └┬──────┘└┬──────┘ + * ┌▽────────▽┐ + * │Multisig │ + * └┬─────────┘ + * ┌▽─────────┐ + * │ProxyAdmin│ + * └──────────┘ + * + * Sequence: + * ┌───────┐┌───────┐┌───────────┐┌───────────────┐ + * │Signer1││Signer2││Facilitator││MultisigBuilder│ + * └───┬───┘└───┬───┘└─────┬─────┘└───────┬───────┘ + * │ │ sign() │ + * │─────────────────────────────────>│ + * │ │ │ + * │──────────────────>│ │ + * │ │ sign() │ + * │ │────────────────────────>│ + * │ │ │ │ + * │ │─────────>│ │ + * │ │ │run(sig1,sig2)│ + * │ │ │─────────────>│ + * + * + * 2. Single-layer nested example: + * + * Setup: + * ┌───────┐┌───────┐┌───────┐┌───────┐ + * │Signer1││Signer2││Signer3││Signer4│ + * └┬──────┘└┬──────┘└┬──────┘└┬──────┘ + * ┌▽────────▽┐┌──────▽────────▽┐ + * │Safe1 ││Safe2 │ + * └┬─────────┘└┬───────────────┘ + * ┌▽───────────▽┐ + * │Safe3 │ + * └┬────────────┘ + * ┌▽─────────┐ + * │ProxyAdmin│ + * └──────────┘ + * + * Sequence: + * ┌───────┐┌───────┐┌───────┐┌───────┐┌───────────┐ ┌───────────────┐ + * │Signer1││Signer2││Signer3││Signer4││Facilitator│ │MultisigBuilder│ + * └───┬───┘└───┬───┘└───┬───┘└───┬───┘└─────┬─────┘ └───────┬───────┘ + * │ │ │ sign(Safe1) │ │ + * │─────────────────────────────────────────────────────────────>│ + * │ │ │ │ │ + * │────────────────────────────────────>│ │ + * │ │ │ │ sign(Safe1) │ + * │ │────────────────────────────────────────────────────>│ + * │ │ │ │ │ │ + * │ │───────────────────────────>│ │ + * │ │ │ │ │approve(Safe1,sig1|sig2)│ + * │ │ │ │ │───────────────────────>│ + * │ │ │ │ sign(Safe2) │ + * │ │ │───────────────────────────────────────────>│ + * │ │ │ │ │ + * │ │ │──────────────────>│ │ + * │ │ │ │ │ sign(Safe2) │ + * │ │ │ │──────────────────────────────────>│ + * │ │ │ │ │ │ + * │ │ │ │─────────>│ │ + * │ │ │ │ │approve(Safe2,sig3|sig4)│ + * │ │ │ │ │───────────────────────>│ + * │ │ │ │ │ run() │ + * │ │ │ │ │───────────────────────>│ + * + * + * 3. Multi-layer nested example: + * + * Setup: + * ┌───────┐┌───────┐┌───────┐┌───────┐┌───────┐┌───────┐ + * │Signer1││Signer2││Signer3││Signer4││Signer5││Signer6│ + * └┬──────┘└┬──────┘└┬──────┘└┬──────┘└┬──────┘└┬──────┘ + * ┌▽────────▽┐┌──────▽────────▽┐┌──────▽────────▽┐ + * │Safe1 ││Safe2 ││Safe3 │ + * └┬─────────┘└┬───────────────┘└┬───────────────┘ + * ┌▽───────────▽┐ │ + * │Safe4 │ │ + * └┬────────────┘ │ + * ┌▽─────────────────────────────▽┐ + * │Safe5 │ + * └┬──────────────────────────────┘ + * ┌▽─────────┐ + * │ProxyAdmin│ + * └──────────┘ + * + * Sequence: + * ┌───────┐┌───────┐┌───────┐┌───────┐┌───────┐┌───────┐┌───────────┐ ┌───────────────┐ + * │Signer1││Signer2││Signer3││Signer4││Signer5││Signer6││Facilitator│ │MultisigBuilder│ + * └───┬───┘└───┬───┘└───┬───┘└───┬───┘└───┬───┘└───┬───┘└─────┬─────┘ └───────┬───────┘ + * │ │ │ │ sign(Safe1,Safe4) │ │ + * │─────────────────────────────────────────────────────────────────────────────────────>│ + * │ │ │ │ │ │ │ + * │──────────────────────────────────────────────────────>│ │ + * │ │ │ │ │ sign(Safe1,Safe4) │ + * │ │────────────────────────────────────────────────────────────────────────────>│ + * │ │ │ │ │ │ │ │ + * │ │─────────────────────────────────────────────>│ │ + * │ │ │ │ │ │ │approve(Safe1,Safe4,sig1|sig2)│ + * │ │ │ │ │ │ │─────────────────────────────>│ + * │ │ │ │ │ sign(Safe2,Safe4) │ + * │ │ │───────────────────────────────────────────────────────────────────>│ + * │ │ │ │ │ │ │ + * │ │ │────────────────────────────────────>│ │ + * │ │ │ │ │ │ sign(Safe2,Safe4) │ + * │ │ │ │──────────────────────────────────────────────────────────>│ + * │ │ │ │ │ │ │ │ + * │ │ │ │───────────────────────────>│ │ + * │ │ │ │ │ │ │approve(Safe2,Safe4,sig3|sig4)│ + * │ │ │ │ │ │ │─────────────────────────────>│ + * │ │ │ │ │ │ │ approve(Safe4) │ + * │ │ │ │ │ │ │─────────────────────────────>│ + * │ │ │ │ │ │ sign(Safe3) │ + * │ │ │ │ │─────────────────────────────────────────────────>│ + * │ │ │ │ │ │ │ + * │ │ │ │ │──────────────────>│ │ + * │ │ │ │ │ │ │ sign(Safe3) │ + * │ │ │ │ │ │────────────────────────────────────────>│ + * │ │ │ │ │ │ │ │ + * │ │ │ │ │ │─────────>│ │ + * │ │ │ │ │ │ │ approve(Safe3,sig5|sig6) │ + * │ │ │ │ │ │ │─────────────────────────────>│ + * │ │ │ │ │ │ │ run() │ + * │ │ │ │ │ │ │─────────────────────────────>│ + */ +abstract contract MultisigScript is Script { + bytes32 internal constant SAFE_NONCE_SLOT = bytes32(uint256(5)); + + /* + * @dev Event emitted from a `sign()` call containing the data to sign. Used in testing. + */ + event DataToSign(bytes); + + /** + * ----------------------------------------------------------- + * Virtual Functions + * ----------------------------------------------------------- + */ + + /** + * @notice Returns the safe address to execute the final transaction from + */ + function _ownerSafe() internal view virtual returns (address); + + /** + * @notice Creates the calldata for signatures (`sign`), approvals (`approve`), and execution (`run`) + */ + function _buildCalls() internal view virtual returns (IMulticall3.Call3[] memory); + + /** + * @notice Follow up assertions to ensure that the script ran to completion. + * @dev Called after `sign` and `run`, but not `approve`. + */ + function _postCheck(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual; + + /** + * @notice Follow up assertions on state and simulation after a `sign` call. + */ + function _postSign(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual {} + + /** + * @notice Follow up assertions on state and simulation after a `approve` call. + */ + function _postApprove(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual {} + + /** + * @notice Follow up assertions on state and simulation after a `run` call. + */ + function _postRun(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual {} + + // Tenderly simulations can accept generic state overrides. This hook enables this functionality. + // By default, an empty (no-op) override is returned. + function _simulationOverrides() internal view virtual returns (Simulation.StateOverride[] memory overrides_) {} + + /** + * ----------------------------------------------------------- + * Public Functions + * ----------------------------------------------------------- + */ + + /** + * Step 1 + * ====== + * Generate a transaction approval data to sign. This method should be called by a threshold of + * multisig owners. + * + * For non-nested multisigs, the signatures can then be used to execute the transaction (see step 3). + * + * For nested multisigs, the signatures can be used to execute an approval transaction for each + * multisig (see step 2). + * + * @param _safes A list of nested safes (excluding the executing safe returned by `_ownerSafe`). + */ + function sign(address[] memory _safes) public { + _safes = _appendOwnerSafe(_safes); + + // Snapshot and restore Safe nonce after simulation, otherwise the data logged to sign + // would not match the actual data we need to sign, because the simulation + // would increment the nonce. + uint256[] memory originalNonces = new uint256[](_safes.length); + for (uint256 i = 0; i < _safes.length; i++) { + originalNonces[i] = _getNonce(_safes[i]); + } + + bytes[] memory datas = _transactionDatas(_safes); + (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = _simulateForSigner(_safes, datas); + + _postSign(accesses, simPayload); + _postCheck(accesses, simPayload); + + // Restore the original nonce. + for (uint256 i = 0; i < _safes.length; i++) { + vm.store(_safes[i], SAFE_NONCE_SLOT, bytes32(originalNonces[i])); + } + + _printDataToSign(_safes[0], datas[0]); + } + + /** + * Step 1.1 (optional) + * ====== + * Verify the signatures generated from step 1 are valid. + * This allows transactions to be pre-signed and stored safely before execution. + * + * @param _safes A list of nested safes (excluding the executing safe returned by `_ownerSafe`). + * @param _signatures The signatures to verify (concatenated, 65-bytes per sig). + */ + function verify(address[] memory _safes, bytes memory _signatures) public view { + _safes = _appendOwnerSafe(_safes); + bytes[] memory datas = _transactionDatas(_safes); + _checkSignatures(_safes[0], datas[0], _signatures); + } + + /** + * Step 2 (optional for non-nested setups) + * ====== + * Execute an approval transaction. This method should be called by a facilitator + * (non-signer), once for each of the multisigs involved in the nested multisig, + * after collecting a threshold of signatures for each multisig (see step 1). + * + * For multiple layers of nesting, this should be called for each layer of nesting (once + * the inner multisigs have registered their approval). The array of safes passed to + * `_safes` should get smaller by one for each layer of nesting. + * + * @param _safes A list of nested safes (excluding the executing safe returned by `_ownerSafe`). + * @param _signatures The signatures from step 1 (concatenated, 65-bytes per sig) + */ + function approve(address[] memory _safes, bytes memory _signatures) public { + _safes = _appendOwnerSafe(_safes); + bytes[] memory datas = _transactionDatas(_safes); + (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = + _executeTransaction(_safes[0], datas[0], _signatures, true); + _postApprove(accesses, simPayload); + } + + /** + * Step 2.1 (optional) + * ====== + * Simulate the transaction. This method should be called by a facilitator (non-signer), after all of the + * signatures have been collected (non-nested case, see step 1), or the approval transactions have been + * submitted onchain (nested case, see step 2, in which case `_signatures` can be empty). + * + * Differs from `run` in that you can override the safe nonce for simulation purposes. + */ + function simulate(bytes memory _signatures) public { + address ownerSafe = _ownerSafe(); + bytes[] memory datas = _transactionDatas(_toArray(ownerSafe)); + + vm.store(ownerSafe, SAFE_NONCE_SLOT, bytes32(_getNonce(ownerSafe))); + + (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = + _executeTransaction(ownerSafe, datas[0], _signatures, false); + + _postRun(accesses, simPayload); + _postCheck(accesses, simPayload); + } + + /** + * Step 3 + * ====== + * Execute the transaction. This method should be called by a facilitator (non-signer), after all of the + * signatures have been collected (non-nested case, see step 1), or the approval transactions have been + * submitted onchain (nested case, see step 2, in which case `_signatures` can be empty). + */ + function run(bytes memory _signatures) public { + address ownerSafe = _ownerSafe(); + bytes[] memory datas = _transactionDatas(_toArray(ownerSafe)); + + (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = + _executeTransaction(ownerSafe, datas[0], _signatures, true); + + _postRun(accesses, simPayload); + _postCheck(accesses, simPayload); + } + + /** + * ----------------------------------------------------------- + * Internal Functions + * ----------------------------------------------------------- + */ + function _appendOwnerSafe(address[] memory _safes) internal view returns (address[] memory) { + address[] memory safes = new address[](_safes.length + 1); + for (uint256 i = 0; i < _safes.length; i++) { + safes[i] = _safes[i]; + } + safes[safes.length - 1] = _ownerSafe(); + return safes; + } + + function _transactionDatas(address[] memory _safes) private view returns (bytes[] memory) { + IMulticall3.Call3[] memory calls = _buildCalls(); + bytes[] memory datas = new bytes[](_safes.length); + for (uint256 i = _safes.length; i > 0; i--) { + if (i < _safes.length) { + // all outer safes should generate an approval for the inner safe: + calls = new IMulticall3.Call3[](1); + calls[0] = _generateApproveCall(_safes[i], datas[i]); + } + datas[i - 1] = abi.encodeCall(IMulticall3.aggregate3, (calls)); + } + return datas; + } + + function _generateApproveCall(address _safe, bytes memory _data) internal view returns (IMulticall3.Call3 memory) { + bytes32 hash = _getTransactionHash(_safe, _data); + + console.log("---\nNested hash for safe %s:", _safe); + console.logBytes32(hash); + + return IMulticall3.Call3({ + target: _safe, + allowFailure: false, + callData: abi.encodeCall(IGnosisSafe(_safe).approveHash, (hash)) + }); + } + + function _printDataToSign(address _safe, bytes memory _data) internal { + bytes memory txData = _encodeTransactionData(_safe, _data); + bytes32 hash = _getTransactionHash(_safe, _data); + + emit DataToSign(txData); + + console.log("---\nIf submitting onchain, call Safe.approveHash on %s with the following hash:", _safe); + console.logBytes32(hash); + + console.log("---\nData to sign:"); + console.log("vvvvvvvv"); + console.logBytes(txData); + console.log("^^^^^^^^\n"); + + console.log("########## IMPORTANT ##########"); + console.log( + // solhint-disable-next-line max-line-length + "Please make sure that the 'Data to sign' displayed above matches what you see in the simulation and on your hardware wallet." + ); + console.log("This is a critical step that must not be skipped."); + console.log("###############################"); + } + + function _executeTransaction(address _safe, bytes memory _data, bytes memory _signatures, bool _broadcast) + internal + returns (Vm.AccountAccess[] memory, Simulation.Payload memory) + { + bytes32 hash = _getTransactionHash(_safe, _data); + _signatures = Signatures.prepareSignatures(_safe, hash, _signatures); + + bytes memory simData = _execTransactionCalldata(_safe, _data, _signatures); + Simulation.logSimulationLink({_to: _safe, _from: msg.sender, _data: simData}); + + vm.startStateDiffRecording(); + bool success = _execTransaction(_safe, _data, _signatures, _broadcast); + Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); + require(success, "MultisigBase::_executeTransaction: Transaction failed"); + require(accesses.length > 0, "MultisigBase::_executeTransaction: No state changes"); + + // This can be used to e.g. call out to the Tenderly API and get additional + // data about the state diff before broadcasting the transaction. + Simulation.Payload memory simPayload = Simulation.Payload({ + from: msg.sender, + to: _safe, + data: simData, + stateOverrides: new Simulation.StateOverride[](0) + }); + return (accesses, simPayload); + } + + function _simulateForSigner(address[] memory _safes, bytes[] memory _datas) + internal + returns (Vm.AccountAccess[] memory, Simulation.Payload memory) + { + IMulticall3.Call3[] memory calls = _simulateForSignerCalls(_safes, _datas); + + // Now define the state overrides for the simulation. + Simulation.StateOverride[] memory overrides = _overrides(_safes); + + bytes memory txData = abi.encodeCall(IMulticall3.aggregate3, (calls)); + console.log("---\nSimulation link:"); + // solhint-disable max-line-length + Simulation.logSimulationLink({_to: MULTICALL3_ADDRESS, _data: txData, _from: msg.sender, _overrides: overrides}); + + // Forge simulation of the data logged in the link. If the simulation fails + // we revert to make it explicit that the simulation failed. + Simulation.Payload memory simPayload = + Simulation.Payload({to: MULTICALL3_ADDRESS, data: txData, from: msg.sender, stateOverrides: overrides}); + Vm.AccountAccess[] memory accesses = Simulation.simulateFromSimPayload(simPayload); + return (accesses, simPayload); + } + + function _simulateForSignerCalls(address[] memory _safes, bytes[] memory _datas) + private + pure + returns (IMulticall3.Call3[] memory) + { + IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](_safes.length); + for (uint256 i = 0; i < _safes.length; i++) { + // first simulated signer is the multicall3 contract + address signer = i == 0 ? MULTICALL3_ADDRESS : _safes[i - 1]; + calls[i] = IMulticall3.Call3({ + target: _safes[i], + allowFailure: false, + callData: _execTransactionCalldata(_safes[i], _datas[i], Signatures.genPrevalidatedSignature(signer)) + }); + } + return (calls); + } + + function _overrides(address[] memory _safes) internal view returns (Simulation.StateOverride[] memory) { + Simulation.StateOverride[] memory simOverrides = _simulationOverrides(); + Simulation.StateOverride[] memory overrides = + new Simulation.StateOverride[](_safes.length + simOverrides.length); + for (uint256 i = 0; i < _safes.length; i++) { + address owner = i == 0 ? MULTICALL3_ADDRESS : address(0); + overrides[i] = _safeOverrides(_safes[i], owner); + } + for (uint256 i = 0; i < simOverrides.length; i++) { + overrides[i + _safes.length] = simOverrides[i]; + } + return overrides; + } + + // The state change simulation can set the threshold, owner address and/or nonce. + // This allows simulation of the final transaction by overriding the threshold to 1. + // State changes reflected in the simulation as a result of these overrides will + // not be reflected in the prod execution. + function _safeOverrides(address _safe, address _owner) + internal + view + virtual + returns (Simulation.StateOverride memory) + { + uint256 _nonce = _getNonce(_safe); + if (_owner == address(0)) { + return Simulation.overrideSafeThresholdAndNonce(_safe, _nonce); + } + return Simulation.overrideSafeThresholdOwnerAndNonce(_safe, _owner, _nonce); + } + + // Get the nonce to use for the given safe, for signing and simulations. + // + // If you override it, ensure that the behavior is correct for all contexts. + // As an example, if you are pre-signing a message that needs safe.nonce+1 (before + // safe.nonce is executed), you should explicitly set the nonce value with an env var. + // Overriding this method with safe.nonce+1 will cause issues upon execution because + // the transaction hash will differ from the one signed. + // + // The process for determining a nonce override is as follows: + // 1. We look for an env var of the name SAFE_NONCE_{UPPERCASE_SAFE_ADDRESS}. For example, + // SAFE_NONCE_0X6DF4742A3C28790E63FE933F7D108FE9FCE51EA4. + // 2. If it exists, we use it as the nonce override for the safe. + // 3. If it does not exist, we do the same for the SAFE_NONCE env var. + // 4. Otherwise we fallback to the safe's current nonce (no override). + function _getNonce(address _safe) internal view virtual returns (uint256 nonce) { + uint256 safeNonce = IGnosisSafe(_safe).nonce(); + nonce = safeNonce; + + // first try SAFE_NONCE + try vm.envUint("SAFE_NONCE") { + nonce = vm.envUint("SAFE_NONCE"); + } catch {} + + // then try SAFE_NONCE_{UPPERCASE_SAFE_ADDRESS} + string memory envVarName = string.concat("SAFE_NONCE_", vm.toUppercase(vm.toString(_safe))); + try vm.envUint(envVarName) { + nonce = vm.envUint(envVarName); + } catch {} + + // print if any override + if (nonce != safeNonce) { + console.log("Overriding nonce for safe %s: %d -> %d", _safe, safeNonce, nonce); + } + } + + function _checkSignatures(address _safe, bytes memory _data, bytes memory _signatures) internal view { + bytes32 hash = _getTransactionHash(_safe, _data); + _signatures = Signatures.prepareSignatures(_safe, hash, _signatures); + IGnosisSafe(_safe).checkSignatures({dataHash: hash, data: _data, signatures: _signatures}); + } + + function _getTransactionHash(address _safe, bytes memory _data) internal view returns (bytes32) { + return keccak256(_encodeTransactionData(_safe, _data)); + } + + function _encodeTransactionData(address _safe, bytes memory _data) internal view returns (bytes memory) { + return IGnosisSafe(_safe).encodeTransactionData({ + to: MULTICALL3_ADDRESS, + value: 0, + data: _data, + operation: Enum.Operation.DelegateCall, + safeTxGas: 0, + baseGas: 0, + gasPrice: 0, + gasToken: address(0), + refundReceiver: address(0), + _nonce: _getNonce(_safe) + }); + } + + function _execTransactionCalldata(address _safe, bytes memory _data, bytes memory _signatures) + internal + pure + returns (bytes memory) + { + return abi.encodeCall( + IGnosisSafe(_safe).execTransaction, + ( + MULTICALL3_ADDRESS, + 0, + _data, + Enum.Operation.DelegateCall, + 0, + 0, + 0, + address(0), + payable(address(0)), + _signatures + ) + ); + } + + function _execTransaction(address _safe, bytes memory _data, bytes memory _signatures, bool _broadcast) + internal + returns (bool) + { + if (_broadcast) { + vm.broadcast(); + } + return IGnosisSafe(_safe).execTransaction({ + to: MULTICALL3_ADDRESS, + value: 0, + data: _data, + operation: Enum.Operation.DelegateCall, + safeTxGas: 0, + baseGas: 0, + gasPrice: 0, + gasToken: address(0), + refundReceiver: payable(address(0)), + signatures: _signatures + }); + } + + function _toArray(address _address) internal pure returns (address[] memory) { + address[] memory array = new address[](1); + array[0] = _address; + return array; + } + + function _toArray(address _address1, address _address2) internal pure returns (address[] memory) { + address[] memory array = new address[](2); + array[0] = _address1; + array[1] = _address2; + return array; + } +} diff --git a/script/universal/NestedMultisigBuilder.sol b/script/universal/NestedMultisigBuilder.sol new file mode 100644 index 0000000..b9eaac9 --- /dev/null +++ b/script/universal/NestedMultisigBuilder.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {MultisigScript} from "./MultisigScript.sol"; + +/** + * @title NestedMultisigBuilder + * @custom:deprecated Use `MultisigScript` instead. + */ +abstract contract NestedMultisigBuilder is MultisigScript { + /* + * @custom:deprecated Use `sign(address[] memory _safes)` instead. + */ + function sign(address _signerSafe) public { + sign(_toArray(_signerSafe)); + } + + /* + * @custom:deprecated Use `verify(address[] memory _safes, bytes memory _signatures)` instead. + */ + function verify(address _signerSafe, bytes memory _signatures) public view { + verify(_toArray(_signerSafe), _signatures); + } + + /* + * @custom:deprecated Use `approve(address[] memory _safes, bytes memory _signatures)` instead. + */ + function approve(address _signerSafe, bytes memory _signatures) public { + approve(_toArray(_signerSafe), _signatures); + } + + /* + * @custom:deprecated Use `simulate(bytes memory _signatures)` instead, with empty `_signatures`. + */ + function simulate() public { + simulate(""); + } + + /* + * @custom:deprecated Use `run(bytes memory _signatures)` instead, with empty `_signatures`. + */ + function run() public { + run(""); + } +} diff --git a/test/universal/DoubleNestedMultisigBuilder.t.sol b/test/universal/DoubleNestedMultisigBuilder.t.sol index e3bd8d4..5756d0a 100644 --- a/test/universal/DoubleNestedMultisigBuilder.t.sol +++ b/test/universal/DoubleNestedMultisigBuilder.t.sol @@ -7,12 +7,12 @@ import {Vm} from "forge-std/Vm.sol"; import {Preinstalls} from "@eth-optimism-bedrock/src/libraries/Preinstalls.sol"; -import {MultisigBuilder} from "../../script/universal/MultisigBuilder.sol"; +import {DoubleNestedMultisigBuilder} from "../../script/universal/DoubleNestedMultisigBuilder.sol"; import {Simulation} from "../../script/universal/Simulation.sol"; import {IGnosisSafe} from "../../script/universal/IGnosisSafe.sol"; import {Counter} from "./Counter.sol"; -contract DoubleNestedMultisigBuilderTest is Test, MultisigBuilder { +contract DoubleNestedMultisigBuilderTest is Test, DoubleNestedMultisigBuilder { Vm.Wallet internal wallet1 = vm.createWallet("1"); Vm.Wallet internal wallet2 = vm.createWallet("2"); diff --git a/test/universal/NestedMultisigBuilder.t.sol b/test/universal/NestedMultisigBuilder.t.sol index 7ed03df..5d57439 100644 --- a/test/universal/NestedMultisigBuilder.t.sol +++ b/test/universal/NestedMultisigBuilder.t.sol @@ -7,12 +7,12 @@ import {Vm} from "forge-std/Vm.sol"; import {Preinstalls} from "@eth-optimism-bedrock/src/libraries/Preinstalls.sol"; -import {MultisigBuilder} from "../../script/universal/MultisigBuilder.sol"; +import {NestedMultisigBuilder} from "../../script/universal/NestedMultisigBuilder.sol"; import {Simulation} from "../../script/universal/Simulation.sol"; import {IGnosisSafe} from "../../script/universal/IGnosisSafe.sol"; import {Counter} from "./Counter.sol"; -contract NestedMultisigBuilderTest is Test, MultisigBuilder { +contract NestedMultisigBuilderTest is Test, NestedMultisigBuilder { Vm.Wallet internal wallet1 = vm.createWallet("1"); Vm.Wallet internal wallet2 = vm.createWallet("2");