diff --git a/script/universal/DoubleNestedMultisigBuilder.sol b/script/universal/DoubleNestedMultisigBuilder.sol new file mode 100644 index 0000000..aa1149e --- /dev/null +++ b/script/universal/DoubleNestedMultisigBuilder.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import "./NestedMultisigBase.sol"; + +/** + * @title DoubleNestedMultisigBuilder + * @notice Modeled from Optimism's SafeBuilder, but built for double nested safes (Safes where + * the signers are other Safes with signers that are other Safes). + * + * There are three safes involved in a double nested multisig: + * 1. The top-level safe, which should be returned by `_ownerSafe()`. + * 2. One or more intermediate safes, which are signers for the top-level safe. + * 3. Signer safes, which are signers for the intermediate safes. + * There should be at least one signer safe per intermediate safe. + */ +abstract contract DoubleNestedMultisigBuilder is NestedMultisigBase { + /** + * Step 1 + * ====== + * Generate a transaction approval data to sign. This method should be called by a threshold + * of members of each of the signer safes involved in the nested multisig. Signers will pass + * their signature to a facilitator, who will execute the approval transaction for each + * signer safe (see step 2). + */ + function sign(address signerSafe, address intermediateSafe) public { + address topSafe = _ownerSafe(); + + // 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 originalTopNonce = _getNonce(topSafe); + uint256 originalIntermediateNonce = _getNonce(intermediateSafe); + uint256 originalSignerNonce = _getNonce(signerSafe); + + (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = + _simulateForSigner(intermediateSafe, topSafe, _buildCalls()); + + _postSign(accesses, simPayload); + _postCheck(accesses, simPayload); + + // Restore the original nonce. + vm.store(topSafe, SAFE_NONCE_SLOT, bytes32(originalTopNonce)); + vm.store(intermediateSafe, SAFE_NONCE_SLOT, bytes32(originalIntermediateNonce)); + vm.store(signerSafe, SAFE_NONCE_SLOT, bytes32(originalSignerNonce)); + + _printDataToSign(signerSafe, _generateIntermediateSafeApprovalCall(intermediateSafe)); + } + + /** + * Step 1.1 (optional) + * ====== + * Verify the signatures generated from step 1 are valid. + * This allow transactions to be pre-signed and stored safely before execution. + */ + function verify(address signerSafe, address intermediateSafe, bytes memory signatures) public view { + IMulticall3.Call3[] memory calls = _generateIntermediateSafeApprovalCall(intermediateSafe); + _checkSignatures(signerSafe, calls, signatures); + } + + /** + * Step 2 + * ====== + * Execute an approval transaction for a signer safe. This method should be called by a facilitator + * (non-signer), once for each of the signer safes involved in the nested multisig, + * after collecting a threshold of signatures for each signer safe (see step 1). + */ + function approveOnBehalfOfSignerSafe(address signerSafe, address intermediateSafe, bytes memory signatures) + public + { + IMulticall3.Call3[] memory calls = _generateIntermediateSafeApprovalCall(intermediateSafe); + _executeTransaction(signerSafe, calls, signatures, true); + _postApprove(signerSafe, intermediateSafe); + } + + /** + * Step 3 + * ====== + * Execute an approval transaction for an intermediate safe. This method should be called by a + * facilitator (non-signer), for each of the intermediate safes after all of their approval + * transactions have been submitted onchain by their signer safes (see step 2). + */ + function approveOnBehalfOfIntermediateSafe(address intermediateSafe) public { + IMulticall3.Call3[] memory calls = _generateTopSafeApprovalCall(); + // signatures is empty, because `_executeTransaction` internally collects all of the approvedHash addresses + bytes memory signatures; + _executeTransaction(intermediateSafe, calls, signatures, true); + _postRunInit(intermediateSafe); + } + + /** + * Step 4 + * ====== + * Execute the final transaction. This method should be called by a facilitator (non-signer), after + * all of the intermediate safe approval transactions have been submitted onchain (see step 3). + */ + function run() public { + IMulticall3.Call3[] memory calls = _buildCalls(); + + // signatures is empty, because `_executeTransaction` internally collects all of the approvedHash addresses + bytes memory signatures; + + (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = + _executeTransaction(_ownerSafe(), calls, signatures, true); + + _postRun(accesses, simPayload); + _postCheck(accesses, simPayload); + } + + /** + * @dev Follow up assertions on state and simulation after an `approve` call. + */ + function _postApprove(address signerSafe, address intermediateSafe) private view { + IMulticall3.Call3 memory topSafeApprovalCall = _generateApproveCall(_ownerSafe(), _buildCalls()); + bytes memory data = abi.encodeCall(IMulticall3.aggregate3, _toArray(topSafeApprovalCall)); + bytes32 approvedHash = _getTransactionHash(intermediateSafe, data); + + uint256 isApproved = IGnosisSafe(intermediateSafe).approvedHashes(signerSafe, approvedHash); + require(isApproved == 1, "DoubleNestedMultisigBuilder::_postApprove: Approval failed"); + } + + /** + * @dev Follow up assertions on state and simulation after an `init` call. + */ + function _postRunInit(address intermediateSafe) private view { + bytes memory data = abi.encodeCall(IMulticall3.aggregate3, _buildCalls()); + bytes32 approvedHash = _getTransactionHash(_ownerSafe(), data); + + uint256 isApproved = IGnosisSafe(_ownerSafe()).approvedHashes(intermediateSafe, approvedHash); + require(isApproved == 1, "DoubleNestedMultisigBuilder::_postRunInit: Init transaction failed"); + } + + function _generateIntermediateSafeApprovalCall(address intermediateSafe) + private + view + returns (IMulticall3.Call3[] memory) + { + IMulticall3.Call3[] memory topCalls = _generateTopSafeApprovalCall(); + IMulticall3.Call3 memory intermediateCall = _generateApproveCall(intermediateSafe, topCalls); + return _toArray(intermediateCall); + } + + function _generateTopSafeApprovalCall() private view returns (IMulticall3.Call3[] memory) { + IMulticall3.Call3[] memory dstCalls = _buildCalls(); + IMulticall3.Call3 memory topSafeApprovalCall = _generateApproveCall(_ownerSafe(), dstCalls); + return _toArray(topSafeApprovalCall); + } +} diff --git a/script/universal/NestedMultisigBase.sol b/script/universal/NestedMultisigBase.sol new file mode 100644 index 0000000..369368a --- /dev/null +++ b/script/universal/NestedMultisigBase.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import "./MultisigBase.sol"; + +abstract contract NestedMultisigBase is MultisigBase { + /** + * ----------------------------------------------------------- + * Virtual Functions + * ----------------------------------------------------------- + */ + + /** + * @notice Returns the nested safe address to execute the final transaction from + */ + function _ownerSafe() internal view virtual returns (address); + + /** + * @notice Creates the calldata for both signatures (`sign`) 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 `run` call. + */ + function _postRun(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual {} + + function _readFrom_SAFE_NONCE() internal pure override returns (bool) { + return false; + } + + function _generateApproveCall(address _safe, IMulticall3.Call3[] memory _calls) + internal + view + returns (IMulticall3.Call3 memory) + { + bytes32 hash = _getTransactionHash(_safe, _calls); + + console.log("---\nNested hash:"); + console.logBytes32(hash); + + return IMulticall3.Call3({ + target: _safe, + allowFailure: false, + callData: abi.encodeCall(IGnosisSafe(_safe).approveHash, (hash)) + }); + } + + function _simulateForSigner(address _signerSafe, address _safe, IMulticall3.Call3[] memory _calls) + internal + returns (Vm.AccountAccess[] memory, Simulation.Payload memory) + { + bytes memory data = abi.encodeCall(IMulticall3.aggregate3, (_calls)); + IMulticall3.Call3[] memory calls = _simulateForSignerCalls(_signerSafe, _safe, data); + + // Now define the state overrides for the simulation. + Simulation.StateOverride[] memory overrides = _overrides(_signerSafe, _safe); + + bytes memory txData = abi.encodeCall(IMulticall3.aggregate3, (calls)); + console.log("---\nSimulation link:"); + 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 _signerSafe, address _safe, bytes memory _data) + internal + view + returns (IMulticall3.Call3[] memory) + { + IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](2); + bytes32 hash = _getTransactionHash(_safe, _data); + + // simulate an approveHash, so that signer can verify the data they are signing + bytes memory approveHashData = abi.encodeCall( + IMulticall3.aggregate3, + ( + _toArray( + IMulticall3.Call3({ + target: _safe, + allowFailure: false, + callData: abi.encodeCall(IGnosisSafe(_safe).approveHash, (hash)) + }) + ) + ) + ); + bytes memory approveHashExec = _execTransationCalldata( + _signerSafe, approveHashData, Signatures.genPrevalidatedSignature(MULTICALL3_ADDRESS) + ); + calls[0] = IMulticall3.Call3({target: _signerSafe, allowFailure: false, callData: approveHashExec}); + + // simulate the final state changes tx, so that signer can verify the final results + bytes memory finalExec = _execTransationCalldata(_safe, _data, Signatures.genPrevalidatedSignature(_signerSafe)); + calls[1] = IMulticall3.Call3({target: _safe, allowFailure: false, callData: finalExec}); + + return calls; + } + + function _overrides(address _signerSafe, address _safe) internal view returns (Simulation.StateOverride[] memory) { + Simulation.StateOverride[] memory simOverrides = _simulationOverrides(); + Simulation.StateOverride[] memory overrides = new Simulation.StateOverride[](2 + simOverrides.length); + overrides[0] = _safeOverrides(_signerSafe, MULTICALL3_ADDRESS); + overrides[1] = _safeOverrides(_safe, address(0)); + for (uint256 i = 0; i < simOverrides.length; i++) { + overrides[i + 2] = simOverrides[i]; + } + return overrides; + } + + function _toArray(IMulticall3.Call3 memory call) internal pure returns (IMulticall3.Call3[] memory) { + IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](1); + calls[0] = call; + return calls; + } +} diff --git a/test/universal/DoubleNestedMultisigBuilder.t.sol b/test/universal/DoubleNestedMultisigBuilder.t.sol new file mode 100644 index 0000000..77e4f0a --- /dev/null +++ b/test/universal/DoubleNestedMultisigBuilder.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; +import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; +import {Preinstalls} from "@eth-optimism-bedrock/src/libraries/Preinstalls.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, DoubleNestedMultisigBuilder { + Vm.Wallet internal wallet1 = vm.createWallet("1"); + Vm.Wallet internal wallet2 = vm.createWallet("2"); + + address internal safe1 = address(1001); + address internal safe2 = address(1002); + address internal safe3 = address(1003); + address internal safe4 = address(1004); + Counter internal counter = new Counter(address(safe4)); + + bytes internal dataToSign1 = + hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280c32a807b9689901dd0dbb7352e9e6c5265e3f6a68667de4be988f03f6a88511f7"; + bytes internal dataToSign2 = + hex"190132640243d7aade8c72f3d90d2dbf359e9897feba5fce1453bc8d9e7ba10d171532a807b9689901dd0dbb7352e9e6c5265e3f6a68667de4be988f03f6a88511f7"; + + function setUp() public { + bytes memory safeCode = Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid); + vm.etch(safe1, safeCode); + vm.etch(safe2, safeCode); + vm.etch(safe3, safeCode); + vm.etch(safe4, safeCode); + vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid)); + + address[] memory owners1 = new address[](1); + owners1[0] = wallet1.addr; + IGnosisSafe(safe1).setup(owners1, 1, address(0), "", address(0), address(0), 0, address(0)); + + address[] memory owners2 = new address[](1); + owners2[0] = wallet2.addr; + IGnosisSafe(safe2).setup(owners2, 1, address(0), "", address(0), address(0), 0, address(0)); + + address[] memory owners3 = new address[](2); + owners3[0] = safe1; + owners3[1] = safe2; + IGnosisSafe(safe3).setup(owners3, 2, address(0), "", address(0), address(0), 0, address(0)); + + address[] memory owners4 = new address[](1); + owners4[0] = safe3; + IGnosisSafe(safe4).setup(owners4, 1, address(0), "", address(0), address(0), 0, address(0)); + } + + function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override { + // Check that the counter has been incremented + uint256 counterValue = counter.count(); + require(counterValue == 1, "Counter value is not 1"); + } + + function _buildCalls() internal view override returns (IMulticall3.Call3[] memory) { + IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](1); + + calls[0] = IMulticall3.Call3({ + target: address(counter), + allowFailure: false, + callData: abi.encodeCall(Counter.increment, ()) + }); + + return calls; + } + + function _ownerSafe() internal view override returns (address) { + return safe4; + } + + function test_sign_double_nested_safe1() external { + vm.recordLogs(); + sign(safe1, safe3); + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign1))); + } + + function test_sign_double_nested_safe2() external { + vm.recordLogs(); + sign(safe2, safe3); + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign2))); + } + + function test_approveInit_double_nested_safe1() external { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); + approveOnBehalfOfSignerSafe(safe1, safe3, abi.encodePacked(r, s, v)); + } + + function test_approveInit_double_nested_safe2() external { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet2, keccak256(dataToSign2)); + approveOnBehalfOfSignerSafe(safe2, safe3, abi.encodePacked(r, s, v)); + } + + function test_approveInit_double_nested_notOwner() external { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); + bytes memory data = abi.encodeCall(this.approveOnBehalfOfSignerSafe, (safe2, safe3, abi.encodePacked(r, s, v))); + (bool success, bytes memory result) = address(this).call(data); + assertFalse(success); + assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); + } + + function test_runInit_double_nested() external { + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSign2)); + approveOnBehalfOfSignerSafe(safe1, safe3, abi.encodePacked(r1, s1, v1)); + approveOnBehalfOfSignerSafe(safe2, safe3, abi.encodePacked(r2, s2, v2)); + approveOnBehalfOfIntermediateSafe(safe3); + } + + function test_runInit_double_nested_notApproved() external { + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); + approveOnBehalfOfSignerSafe(safe1, safe3, abi.encodePacked(r1, s1, v1)); + bytes memory data = abi.encodeCall(this.approveOnBehalfOfIntermediateSafe, (safe3)); + (bool success, bytes memory result) = address(this).call(data); + assertFalse(success); + assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); + } + + function test_run_double_nested() external { + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSign2)); + approveOnBehalfOfSignerSafe(safe1, safe3, abi.encodePacked(r1, s1, v1)); + approveOnBehalfOfSignerSafe(safe2, safe3, abi.encodePacked(r2, s2, v2)); + approveOnBehalfOfIntermediateSafe(safe3); + + run(); + } +}