diff --git a/foundry.toml b/foundry.toml index 32a7cad..8058a1c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,4 +6,7 @@ optimizer_runs = 999999 solc_version = "0.8.15" extra_output_files = ["abi"] -# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file +[lint] +lint_on_build = false + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/script/deploy/l1/SetGasLimit.sol b/script/deploy/l1/SetGasLimit.sol index f68f6fe..16be341 100644 --- a/script/deploy/l1/SetGasLimit.sol +++ b/script/deploy/l1/SetGasLimit.sol @@ -4,7 +4,8 @@ pragma solidity 0.8.15; import {Vm} from "lib/forge-std/src/Vm.sol"; import {SystemConfig} from "lib/optimism/packages/contracts-bedrock/src/L1/SystemConfig.sol"; -import {MultisigScript, IMulticall3, Simulation} from "../../universal/MultisigScript.sol"; +import {Enum} from "../../universal/IGnosisSafe.sol"; +import {MultisigScript, Simulation} from "../../universal/MultisigScript.sol"; /// @title SetGasLimit /// @@ -34,13 +35,13 @@ contract SetGasLimit is MultisigScript { assert(SystemConfig(L1_SYSTEM_CONFIG).gasLimit() == _toGasLimit()); } - function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); + function _buildCalls() internal view override returns (Call[] memory) { + Call[] memory calls = new Call[](1); - calls[0] = IMulticall3.Call3Value({ + calls[0] = Call({ + operation: Enum.Operation.Call, target: L1_SYSTEM_CONFIG, - allowFailure: false, - callData: abi.encodeCall(SystemConfig.setGasLimit, (_toGasLimit())), + data: abi.encodeCall(SystemConfig.setGasLimit, (_toGasLimit())), value: 0 }); diff --git a/script/universal/DoubleNestedMultisigBuilder.sol b/script/universal/DoubleNestedMultisigBuilder.sol deleted file mode 100644 index 5a9eac7..0000000 --- a/script/universal/DoubleNestedMultisigBuilder.sol +++ /dev/null @@ -1,38 +0,0 @@ -// 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) external { - sign({safes: _toArray(signerSafe, intermediateSafe)}); - } - - /// @custom:deprecated Use `approve(address[] memory safes, bytes memory signatures)` instead. - function approveOnBehalfOfSignerSafe(address signerSafe, address intermediateSafe, bytes memory signatures) public { - approve({safes: _toArray(signerSafe, intermediateSafe), signatures: signatures}); - } - - /// @custom:deprecated Use `approve(address[] memory safes, bytes memory signatures)` instead. - function approveOnBehalfOfIntermediateSafe(address intermediateSafe) public { - approve({safes: _toArray(intermediateSafe), signatures: ""}); - } - - /// @custom:deprecated Use `simulate(bytes memory signatures)` instead, with empty `signatures`. - function simulate() public { - simulate({signatures: ""}); - } - - /// @custom:deprecated Use `run(bytes memory signatures)` instead, with empty `signatures`. - function run() public { - run({signatures: ""}); - } - - /// @custom:deprecated Use `verify(address[] memory safes, bytes memory signatures)` instead. - function verify(address signerSafe, address intermediateSafe, bytes memory signatures) public view { - verify({safes: _toArray(signerSafe, intermediateSafe), signatures: signatures}); - } -} diff --git a/script/universal/MultisigBuilder.sol b/script/universal/MultisigBuilder.sol deleted file mode 100644 index 614cf4e..0000000 --- a/script/universal/MultisigBuilder.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import {MultisigScript} from "./MultisigScript.sol"; - -/// @title MultisigBuilder -/// @custom:deprecated Use `MultisigScript` instead. -abstract contract MultisigBuilder is MultisigScript { - /// @custom:deprecated Use `sign(address[] memory safes)` instead, with an empty array. - function sign() external { - sign({safes: new address[](0)}); - } - - /// @custom:deprecated Use `verify(address[] memory safes, bytes memory signatures)` instead, with an empty array. - function verify(bytes memory signatures) external view { - verify({safes: new address[](0), signatures: signatures}); - } -} diff --git a/script/universal/MultisigDeploy.sol b/script/universal/MultisigDeploy.sol index fe86359..5148453 100644 --- a/script/universal/MultisigDeploy.sol +++ b/script/universal/MultisigDeploy.sol @@ -133,7 +133,7 @@ contract MultisigDeployScript is Script { console.log(" Salt Nonce:", saltNonce); // Resolve owner addresses (combine direct owners + referenced safe addresses) - address[] memory resolvedOwners = _resolveOwnerAddresses({config: config, safeWallets: safes}); + address[] memory resolvedOwners = _resolveOwnerAddresses({config: config}); console.log(" Total Owners:", resolvedOwners.length); console.log(" Direct Owners:", config.owners.length); @@ -185,11 +185,7 @@ contract MultisigDeployScript is Script { } } - function _resolveOwnerAddresses(SafeWallet memory config, SafeWallet[] memory safeWallets) - internal - view - returns (address[] memory) - { + function _resolveOwnerAddresses(SafeWallet memory config) internal view returns (address[] memory) { uint256 totalOwners = config.owners.length + config.ownerRefIndices.length; address[] memory resolved = new address[](totalOwners); @@ -201,7 +197,7 @@ contract MultisigDeployScript is Script { // Add referenced safe addresses (they must already be deployed due to array order) for (uint256 i; i < config.ownerRefIndices.length; i++) { uint256 refIndex = config.ownerRefIndices[i]; - string memory refLabel = safeWallets[refIndex].label; + string memory refLabel = safes[refIndex].label; address refAddr = deployedSafes[refLabel]; require(refAddr != address(0), string(abi.encodePacked("Reference not deployed: ", refLabel))); resolved[config.owners.length + i] = refAddr; diff --git a/script/universal/MultisigScript.sol b/script/universal/MultisigScript.sol index 966e63b..519f510 100644 --- a/script/universal/MultisigScript.sol +++ b/script/universal/MultisigScript.sol @@ -3,15 +3,16 @@ pragma solidity ^0.8.15; // solhint-disable no-console import {console} from "lib/forge-std/src/console.sol"; -import {IMulticall3} from "lib/forge-std/src/interfaces/IMulticall3.sol"; import {Script} from "lib/forge-std/src/Script.sol"; +import {stdJson} from "lib/forge-std/src/StdJson.sol"; import {Vm} from "lib/forge-std/src/Vm.sol"; +import {CBMulticall} from "../../src/utils/CBMulticall.sol"; + import {IGnosisSafe, Enum} from "./IGnosisSafe.sol"; import {Signatures} from "./Signatures.sol"; -import {Simulation} from "./Simulation.sol"; import {StateDiff} from "./StateDiff.sol"; -import {CBMulticall} from "../../src/utils/CBMulticall.sol"; +import {Simulation} from "./Simulation.sol"; /// @title MultisigScript /// @notice Script builder for Forge scripts that require signatures from Safes. Supports both non-nested @@ -149,10 +150,27 @@ import {CBMulticall} from "../../src/utils/CBMulticall.sol"; /// │ │ │ │ │ │ │─────────────────────────────>│ abstract contract MultisigScript is Script { bytes32 internal constant SAFE_NONCE_SLOT = bytes32(uint256(5)); - address internal constant CB_MULTICALL = 0xA8B8CA1d6F0F5Ce63dCEA9121A01b302c5801303; - address internal multicallAddress; + /// @notice A struct representing a call to a contract. + /// + /// @param operation The operation to perform on the contract. + /// @param target The address of the contract to call. + /// @param data The data to call the contract with. + /// @param value The value to send with the call. + struct Call { + Enum.Operation operation; + address target; + bytes data; + uint256 value; + } + + /// @notice The available types of for call3 calls. + enum Call3Type { + DELEGATE_CALL, + CALL, + CALL_VALUE + } /// @dev Event emitted from a `sign()` call containing the data to sign. Used in testing. event DataToSign(bytes data); @@ -165,7 +183,7 @@ abstract contract MultisigScript is Script { 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.Call3Value[] memory); + function _buildCalls() internal view virtual returns (Call[] memory); /// @notice Follow up assertions to ensure that the script ran to completion. /// @dev Called after `sign` and `run`, but not `approve`. @@ -184,24 +202,12 @@ abstract contract MultisigScript is Script { // By default, an empty (no-op) override is returned. function _simulationOverrides() internal view virtual returns (Simulation.StateOverride[] memory overrides_) {} - /// @notice If set to true, the executed aggregate call runs through the custom `CBMulticall` contract - /// as a `DELEGATECALL` for each individual call. - /// @dev In delegatecall mode: - /// - The multisig inherits the multicall logic and executes each target in its own context - /// (e.g. for Optimism's OPCM-style flows). - /// - The `value` field of each `IMulticall3.Call3Value` returned by `_buildCalls` MUST be zero. - /// Per-call value routing is not supported; any ETH attached to the Safe transaction is shared - /// across all calls according to the delegatee's logic. - function _useDelegateCall() internal view virtual returns (bool) { - return false; - } - - constructor() { - bool useCbMulticall; - try vm.envBool("USE_CB_MULTICALL") { - useCbMulticall = vm.envBool("USE_CB_MULTICALL"); - } catch {} - multicallAddress = (useCbMulticall || _useDelegateCall()) ? CB_MULTICALL : MULTICALL3_ADDRESS; + /// @notice Controls whether the safe tx is printed as hashes or structured EIP-712 data. + /// + /// @dev Override and return `true` to print hashed data (domain + message hash) instead of + /// the typed EIP-712 JSON structure. By default, returns `false` to use EIP-712 JSON. + function _printDataHashes() internal view virtual returns (bool) { + return true; } ////////////////////////////////////////////////////////////////////////////////////// @@ -230,11 +236,11 @@ abstract contract MultisigScript is Script { originalNonces[i] = _getNonce({safe: safes[i]}); } - (bytes[] memory datas, uint256 value) = _transactionDatas({safes: safes}); + Call[] memory callsChain = _buildCallsChain({safes: safes}); vm.startMappingRecording(); (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = - _simulateForSigner({safes: safes, datas: datas, value: value}); + _simulateForSigner({safes: safes, callsChain: callsChain}); (StateDiff.MappingParent[] memory parents, string memory json) = StateDiff.collectStateDiff(StateDiff.CollectStateDiffOpts({accesses: accesses, simPayload: simPayload})); vm.stopMappingRecording(); @@ -247,10 +253,10 @@ abstract contract MultisigScript is Script { vm.store({target: safes[i], slot: SAFE_NONCE_SLOT, value: bytes32(originalNonces[i])}); } - bytes memory txData = _encodeTransactionData({safe: safes[0], data: datas[0], value: value}); + bytes memory txData = _encodeTransactionData({safe: safes[0], call: callsChain[0]}); StateDiff.recordStateDiff({json: json, parents: parents, txData: txData, targetSafe: _ownerSafe()}); - _printDataToSign({safe: safes[0], data: datas[0], value: value, txData: txData}); + _printDataToSign({safe: safes[0], call: callsChain[0]}); } /// Step 1.1 (optional) @@ -262,8 +268,9 @@ abstract contract MultisigScript is Script { /// @param signatures The signatures to verify (concatenated, 65-bytes per sig). function verify(address[] memory safes, bytes memory signatures) public view { safes = _appendOwnerSafe({safes: safes}); - (bytes[] memory datas, uint256 value) = _transactionDatas({safes: safes}); - _checkSignatures({safe: safes[0], data: datas[0], value: value, signatures: signatures}); + + Call[] memory callsChain = _buildCallsChain({safes: safes}); + _checkSignatures({safe: safes[0], call: callsChain[0], signatures: signatures}); } /// Step 2 (optional for non-nested setups) @@ -280,9 +287,11 @@ abstract contract MultisigScript is Script { /// @param signatures The signatures from step 1 (concatenated, 65-bytes per sig) function approve(address[] memory safes, bytes memory signatures) public { safes = _appendOwnerSafe({safes: safes}); - (bytes[] memory datas, uint256 value) = _transactionDatas({safes: safes}); + + Call[] memory callsChain = _buildCallsChain({safes: safes}); (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = - _executeTransaction({safe: safes[0], data: datas[0], value: value, signatures: signatures, broadcast: true}); + _executeTransaction({safe: safes[0], call: callsChain[0], signatures: signatures, broadcast: true}); + _postApprove({accesses: accesses, simPayload: simPayload}); } @@ -297,13 +306,12 @@ abstract contract MultisigScript is Script { /// @param signatures The signatures from step 1 (concatenated, 65-bytes per sig) function simulate(bytes memory signatures) public { address ownerSafe = _ownerSafe(); - (bytes[] memory datas, uint256 value) = _transactionDatas({safes: _toArray(ownerSafe)}); + Call[] memory callsChain = _buildCallsChain({safes: _toArray(ownerSafe)}); vm.store({target: ownerSafe, slot: SAFE_NONCE_SLOT, value: bytes32(_getNonce({safe: ownerSafe}))}); - (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = _executeTransaction({ - safe: ownerSafe, data: datas[0], value: value, signatures: signatures, broadcast: false - }); + (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = + _executeTransaction({safe: ownerSafe, call: callsChain[0], signatures: signatures, broadcast: false}); _postRun({accesses: accesses, simPayload: simPayload}); _postCheck({accesses: accesses, simPayload: simPayload}); @@ -318,11 +326,10 @@ abstract contract MultisigScript is Script { /// @param signatures The signatures from step 1 (concatenated, 65-bytes per sig) function run(bytes memory signatures) public { address ownerSafe = _ownerSafe(); - (bytes[] memory datas, uint256 value) = _transactionDatas({safes: _toArray(ownerSafe)}); + Call[] memory callsChain = _buildCallsChain({safes: _toArray(ownerSafe)}); - (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = _executeTransaction({ - safe: ownerSafe, data: datas[0], value: value, signatures: signatures, broadcast: true - }); + (Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = + _executeTransaction({safe: ownerSafe, call: callsChain[0], signatures: signatures, broadcast: true}); _postRun({accesses: accesses, simPayload: simPayload}); _postCheck({accesses: accesses, simPayload: simPayload}); @@ -332,6 +339,11 @@ abstract contract MultisigScript is Script { /// Internal Functions /// ////////////////////////////////////////////////////////////////////////////////////// + /// @notice Appends the owner safe to the list of safes. + /// + /// @param safes The list of safes to append the owner safe to. + /// + /// @return The list of safes with the owner safe appended. function _appendOwnerSafe(address[] memory safes) internal view returns (address[] memory) { address[] memory extendedSafes = new address[](safes.length + 1); for (uint256 i; i < safes.length; i++) { @@ -341,72 +353,209 @@ abstract contract MultisigScript is Script { return extendedSafes; } - function _transactionDatas(address[] memory safes) private view returns (bytes[] memory datas, uint256 value) { - // Build the calls and sum the values - IMulticall3.Call3Value[] memory calls = _buildCalls(); - for (uint256 i; i < calls.length; i++) { - value += calls[i].value; + /// @notice Wrapper around `_buildCalls` that checks that the script calls are valid. + /// + /// @return The list of calls. + function _buildCallsChecked() internal view returns (Call[] memory) { + Call[] memory scriptCalls = _buildCalls(); + for (uint256 i; i < scriptCalls.length; i++) { + Call memory call = scriptCalls[i]; + + require( + call.operation == Enum.Operation.Call || call.value == 0, + "MultisigScript::_buildCallsChecked: Value must be 0 for delegate calls" + ); } - // The very last call is the actual (aggregated) call to execute - datas = new bytes[](safes.length); - datas[datas.length - 1] = abi.encodeCall(IMulticall3.aggregate3Value, (calls)); + return scriptCalls; + } - if (_useDelegateCall()) { - datas[datas.length - 1] = abi.encodeCall(CBMulticall.aggregateDelegateCalls, (_toCall3Array(calls))); - } + /// @notice Build the list of safe-to-safe approval calls followed by the final script call. + /// + /// @param safes The list of safes to build the calls chain for. + /// + /// @return callsChain The calls chain for the given safes. + function _buildCallsChain(address[] memory safes) internal view returns (Call[] memory callsChain) { + // Build the script calls. + Call[] memory scriptCalls = _buildCallsChecked(); - // The first n-1 calls are the nested approval calls - uint256 valueForCallToApprove = value; - for (uint256 i = safes.length - 1; i > 0; i--) { - address targetSafe = safes[i]; - bytes memory callToApprove = datas[i]; + // Build the final script call. + Call memory aggregatedScriptCall = _buildAggregatedScriptCall({scriptCalls: scriptCalls}); - IMulticall3.Call3[] memory approvalCall = new IMulticall3.Call3[](1); - approvalCall[0] = - _generateApproveCall({safe: targetSafe, data: callToApprove, value: valueForCallToApprove}); - datas[i - 1] = abi.encodeCall(IMulticall3.aggregate3, (approvalCall)); + // The very last call is the actual call to execute + callsChain = new Call[](safes.length); + callsChain[callsChain.length - 1] = aggregatedScriptCall; + + // The first n-1 calls are the nested approval calls. We build the approvals backwards, starting from the last safe. + for (uint256 i = safes.length - 1; i > 0; i--) { + address safe = safes[i]; + Call memory callToApprove = callsChain[i]; - valueForCallToApprove = 0; + callsChain[i - 1] = _buildApproveCall({safe: safe, call: callToApprove}); } } - /// @dev Converts `IMulticall3.Call3Value` calls into `CBMulticall.Call3` calls for delegatecall mode. - /// All `value` fields must be zero; delegatecall mode does not support per-call value routing. - function _toCall3Array(IMulticall3.Call3Value[] memory calls) private pure returns (CBMulticall.Call3[] memory) { - CBMulticall.Call3[] memory dCalls = new CBMulticall.Call3[](calls.length); - for (uint256 i; i < calls.length; i++) { - // Delegatecall mode relies on the Safe's `msg.value` handling rather than per-call value routing. - // Enforce that no per-call value is specified when using delegatecall mode. - require(calls[i].value == 0, "MultisigScript: delegatecall mode does not support call value"); - dCalls[i] = CBMulticall.Call3({ - target: calls[i].target, allowFailure: calls[i].allowFailure, callData: calls[i].callData + /// @notice Builds the aggregated script call. + /// + /// @param scriptCalls The list of script calls to aggregate. + /// + /// @return The aggregated script call. + function _buildAggregatedScriptCall(Call[] memory scriptCalls) internal pure returns (Call memory) { + // When there is only one call, we return it directly as there is no need to aggregate it into a Multicall call. + if (scriptCalls.length == 1) { + return scriptCalls[0]; + } + + CBMulticall.Call3[] memory rootCalls = new CBMulticall.Call3[](scriptCalls.length); + uint256 rootCallsIndex; + + Call[] memory currentGroup = new Call[](scriptCalls.length); + currentGroup[0] = scriptCalls[0]; + uint256 currentGroupIndex; + + for (uint256 i; i < scriptCalls.length; i++) { + Call memory currentCall = scriptCalls[i]; + Call3Type currentType = _getCall3Type(currentCall); + Call3Type groupType = _getCall3Type(currentGroup[0]); + + // If the current call has the same type as the current group, add it to the current group and continue. + if (groupType == currentType) { + currentGroup[currentGroupIndex] = currentCall; + currentGroupIndex++; + continue; + } + + // Consume the current group and append the calls to the root calls. + rootCallsIndex += _aggregateCalls({ + groupType: groupType, + rootCalls: rootCalls, + rootCallsIndex: rootCallsIndex, + currentGroup: currentGroup, + currentGroupIndex: currentGroupIndex }); + + // Reset the current group (for the next group) + currentGroup[0] = currentCall; + currentGroupIndex = 1; } - return dCalls; + + // Process the final group left in the current group. + rootCallsIndex += _aggregateCalls({ + groupType: _getCall3Type(currentGroup[0]), + rootCalls: rootCalls, + rootCallsIndex: rootCallsIndex, + currentGroup: currentGroup, + currentGroupIndex: currentGroupIndex + }); + + // NOTE: When aggregating via a Multicall call, the root call is always a delegatecall to `aggregateDelegateCalls` + // as it offers the most flexibility and allows perofming any other type of call. + return Call({ + operation: Enum.Operation.DelegateCall, + target: CB_MULTICALL, + data: abi.encodeCall(CBMulticall.aggregateDelegateCalls, (rootCalls)), + value: 0 + }); } - function _generateApproveCall(address safe, bytes memory data, uint256 value) - internal - view - returns (IMulticall3.Call3 memory) - { - bytes32 hash = _getTransactionHash({safe: safe, data: data, value: value}); + /// @notice Aggregates the current group of calls into a single Multicall call. + /// + /// @param groupType The type of the current group. + /// @param rootCalls The root calls to append the calls to. + /// @param rootCallsIndex The index of the root calls to append the calls to. + /// @param currentGroup The current group of calls to consume. + /// @param currentGroupIndex The index of the current group. + /// + /// @return rootCallsCount The number of root calls appended. + function _aggregateCalls( + Call3Type groupType, + CBMulticall.Call3[] memory rootCalls, + uint256 rootCallsIndex, + Call[] memory currentGroup, + uint256 currentGroupIndex + ) internal pure returns (uint256 rootCallsCount) { + uint256 rootCallsIndexSaved = rootCallsIndex; + + // Append the call3 delegate calls directly to the root calls. + if (groupType == Call3Type.DELEGATE_CALL) { + for (uint256 j; j < currentGroupIndex; j++) { + rootCalls[rootCallsIndex] = _toDelegateCall3(currentGroup[j]); + rootCallsIndex++; + } + } + // Otherwise aggregate the calls into a single Multicall call. + else { + CBMulticall.Call3 memory rootCall; + + if (groupType == Call3Type.CALL) { + CBMulticall.Call3[] memory call3s = new CBMulticall.Call3[](currentGroupIndex); + for (uint256 j; j < currentGroupIndex; j++) { + call3s[j] = _toCall3(currentGroup[j]); + } + + rootCall = CBMulticall.Call3({ + target: CB_MULTICALL, + allowFailure: false, + callData: abi.encodeCall(CBMulticall.aggregate3, (call3s)) + }); + } else { + CBMulticall.Call3Value[] memory call3Values = new CBMulticall.Call3Value[](currentGroupIndex); + for (uint256 j; j < currentGroupIndex; j++) { + call3Values[j] = _toCall3Value(currentGroup[j]); + } + + rootCall = CBMulticall.Call3({ + target: CB_MULTICALL, + allowFailure: false, + callData: abi.encodeCall(CBMulticall.aggregate3Value, (call3Values)) + }); + } + + rootCalls[rootCallsIndex] = rootCall; + rootCallsIndex++; + } + + // Return the number of root calls appended. + return rootCallsIndex - rootCallsIndexSaved; + } + + /// @notice Builds the approve call (`approveHash`) for the given safe and call. + /// + /// @param safe The address of the safe to approve. + /// @param call The call to approve. + /// + /// @return The approve call. + function _buildApproveCall(address safe, Call memory call) internal view returns (Call memory) { + bytes32 hash = _getTransactionHash({safe: safe, call: call}); 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)) + return Call({ + operation: Enum.Operation.Call, + target: safe, + data: abi.encodeCall(IGnosisSafe(safe).approveHash, (hash)), + value: 0 }); } - function _printDataToSign(address safe, bytes memory data, uint256 value, bytes memory txData) internal { - bytes32 hash = _getTransactionHash({safe: safe, data: data, value: value}); + /// @notice Prints the data to sign for the given safe and call. + /// + /// @dev Uses `_printDataHashes()` to determine the output format: + /// - `true`: prints raw transaction data (hashes) + /// - `false` (default): prints EIP-712 JSON structure for hardware wallets + /// + /// @param safe The address of the safe to print the data to sign for. + /// @param call The call to print the data to sign for. + function _printDataToSign(address safe, Call memory call) internal { + bytes memory txData = _printDataHashes() + ? _encodeTransactionData({safe: safe, call: call}) + : _encodeEip712Json({safe: safe, call: call}); emit DataToSign({data: txData}); console.log("---\nIf submitting onchain, call Safe.approveHash on %s with the following hash:", safe); + bytes32 hash = _getTransactionHash({safe: safe, call: call}); console.logBytes32(hash); console.log("---\nData to sign:"); @@ -423,80 +572,91 @@ abstract contract MultisigScript is Script { console.log("###############################"); } - function _executeTransaction( - address safe, - bytes memory data, - uint256 value, - bytes memory signatures, - bool broadcast - ) internal returns (Vm.AccountAccess[] memory, Simulation.Payload memory) { - bytes32 hash = _getTransactionHash({safe: safe, data: data, value: value}); + /// @notice Executes the given transaction. + /// + /// @param safe The address of the safe to execute the transaction from. + /// @param call The call to execute. + /// @param signatures The signatures to use for the transaction. + /// @param broadcast Whether to broadcast the transaction. + /// + /// @return The account accesses and simulation payload. + function _executeTransaction(address safe, Call memory call, bytes memory signatures, bool broadcast) + internal + returns (Vm.AccountAccess[] memory, Simulation.Payload memory) + { + bytes32 hash = _getTransactionHash({safe: safe, call: call}); signatures = Signatures.prepareSignatures({safe: safe, hash: hash, signatures: signatures}); - bytes memory simData = _execTransactionCalldata({safe: safe, data: data, value: value, signatures: signatures}); - Simulation.logSimulationLink({to: safe, data: simData, from: msg.sender}); + Call memory simCall = _buildExecTransactionCall({safe: safe, call: call, signatures: signatures}); + Simulation.logSimulationLink({to: safe, data: simCall.data, from: msg.sender}); vm.startStateDiffRecording(); - bool success = - _execTransaction({safe: safe, data: data, value: value, signatures: signatures, broadcast: broadcast}); + bool success = _execTransaction({safe: safe, call: call, signatures: signatures, broadcast: broadcast}); Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); - require(success, "MultisigBase::_executeTransaction: Transaction failed"); - require(accesses.length > 0, "MultisigBase::_executeTransaction: No state changes"); + require(success, "MultisigScript::_executeTransaction: Transaction failed"); + require(accesses.length > 0, "MultisigScript::_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) + from: msg.sender, to: safe, data: simCall.data, stateOverrides: new Simulation.StateOverride[](0) }); return (accesses, simPayload); } - function _simulateForSigner(address[] memory safes, bytes[] memory datas, uint256 value) + /// @notice Simulates the given `callsChain` associated to the given `safes` as if initiated by `msg.sender`. + /// + /// @param safes The list of safes to simulate the transaction for. + /// @param callsChain The list of calls to simulate the transaction for. + /// + /// @return The account accesses and simulation payload. + function _simulateForSigner(address[] memory safes, Call[] memory callsChain) internal returns (Vm.AccountAccess[] memory, Simulation.Payload memory) { - IMulticall3.Call3[] memory calls = _simulateForSignerCalls({safes: safes, datas: datas, value: value}); - - bytes32 firstCallDataHash = _getTransactionHash({safe: safes[0], data: datas[0], value: value}); - - // Now define the state overrides for the simulation. + // Define the state overrides for the simulation. + bytes32 firstCallDataHash = _getTransactionHash({safe: safes[0], call: callsChain[0]}); Simulation.StateOverride[] memory overrides = _overrides({safes: safes, firstCallDataHash: firstCallDataHash}); - bytes memory txData = abi.encodeCall(IMulticall3.aggregate3, (calls)); + // Build the `execTransaction` calls chain for all the safe-to-safe approvals followed by the final script call. + Call[] memory execTransactionCalls = _buildExecTransactionCalls({safes: safes, callsChain: callsChain}); + bytes memory txData = abi.encodeCall(CBMulticall.aggregate3, (_toCall3s(execTransactionCalls))); + console.logBytes(txData); + console.log("---\nSimulation link:"); - // solhint-disable max-line-length - Simulation.logSimulationLink({to: multicallAddress, data: txData, from: msg.sender, overrides: overrides}); + Simulation.logSimulationLink({to: CB_MULTICALL, 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. + // 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: multicallAddress, data: txData, from: msg.sender, stateOverrides: overrides}); + Simulation.Payload({to: CB_MULTICALL, data: txData, from: msg.sender, stateOverrides: overrides}); Vm.AccountAccess[] memory accesses = Simulation.simulateFromSimPayload({simPayload: simPayload}); return (accesses, simPayload); } - function _simulateForSignerCalls(address[] memory safes, bytes[] memory datas, uint256 value) - private + /// @notice Wraps each of the given calls in a `execTransaction` call. + /// + /// @param safes The list of safes to execute the calls from. + /// @param callsChain The list of calls to wrap in a `execTransaction` call. + /// + /// @return execTransactionCalls The list of `execTransaction` calls. + function _buildExecTransactionCalls(address[] memory safes, Call[] memory callsChain) + internal view - returns (IMulticall3.Call3[] memory) + returns (Call[] memory execTransactionCalls) { - IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](safes.length); + require( + safes.length == callsChain.length, + "MultisigScript::_buildExecTransactionCalls: Safes and callsChain must have the same length" + ); + + execTransactionCalls = new Call[](safes.length); for (uint256 i; i < safes.length; i++) { address signer = i == 0 ? msg.sender : safes[i - 1]; - calls[i] = IMulticall3.Call3({ - target: safes[i], - allowFailure: false, - callData: _execTransactionCalldata({ - safe: safes[i], - data: datas[i], - value: value, - signatures: Signatures.genPrevalidatedSignature(signer) - }) + execTransactionCalls[i] = _buildExecTransactionCall({ + safe: safes[i], call: callsChain[i], signatures: Signatures.genPrevalidatedSignature(signer) }); } - - return calls; } // The state change simulation can set the threshold, owner address and/or nonce. @@ -563,27 +723,19 @@ abstract contract MultisigScript is Script { } } - function _checkSignatures(address safe, bytes memory data, uint256 value, bytes memory signatures) internal view { - bytes32 hash = _getTransactionHash({safe: safe, data: data, value: value}); - signatures = Signatures.prepareSignatures({safe: safe, hash: hash, signatures: signatures}); - IGnosisSafe(safe).checkSignatures({dataHash: hash, data: data, signatures: signatures}); - } - - function _getTransactionHash(address safe, bytes memory data, uint256 value) internal view returns (bytes32) { - return keccak256(_encodeTransactionData({safe: safe, data: data, value: value})); - } - - function _encodeTransactionData(address safe, bytes memory data, uint256 value) - internal - view - returns (bytes memory) - { + /// @notice Returns the result of `encodeTransactionData` function from the given safe for the given call. + /// + /// @param safe The address of the safe that will execute the transaction. + /// @param call The call to get the encoded transaction data for. + /// + /// @return The result of `encodeTransactionData` function from the given safe for the given call. + function _encodeTransactionData(address safe, Call memory call) internal view returns (bytes memory) { return IGnosisSafe(safe) .encodeTransactionData({ - to: multicallAddress, - value: value, - data: data, - operation: _getOperation(value), + to: call.target, + value: call.value, + data: call.data, + operation: call.operation, safeTxGas: 0, baseGas: 0, gasPrice: 0, @@ -593,30 +745,130 @@ abstract contract MultisigScript is Script { }); } - function _execTransactionCalldata(address safe, bytes memory data, uint256 value, bytes memory signatures) + /// @notice Encodes the transaction as EIP-712 structured JSON for hardware wallet signing. + /// + /// @param safe The address of the safe that will execute the transaction. + /// @param call The call to encode. + /// + /// @return The EIP-712 JSON structure as bytes. + function _encodeEip712Json(address safe, Call memory call) internal returns (bytes memory) { + // EIP-712 type definitions for Safe transaction + string memory types = '{"EIP712Domain":[' '{"name":"chainId","type":"uint256"},' + '{"name":"verifyingContract","type":"address"}],' '"SafeTx":[' '{"name":"to","type":"address"},' + '{"name":"value","type":"uint256"},' '{"name":"data","type":"bytes"},' + '{"name":"operation","type":"uint8"},' '{"name":"safeTxGas","type":"uint256"},' + '{"name":"baseGas","type":"uint256"},' '{"name":"gasPrice","type":"uint256"},' + '{"name":"gasToken","type":"address"},' '{"name":"refundReceiver","type":"address"},' + '{"name":"nonce","type":"uint256"}]}'; + + // Build domain object + string memory domain = stdJson.serialize("domain", "chainId", uint256(block.chainid)); + domain = stdJson.serialize("domain", "verifyingContract", safe); + + // Build message object with transaction details + string memory message = stdJson.serialize("message", "to", call.target); + message = stdJson.serialize("message", "value", call.value); + message = stdJson.serialize("message", "data", call.data); + message = stdJson.serialize("message", "operation", uint256(call.operation)); + message = stdJson.serialize("message", "safeTxGas", uint256(0)); + message = stdJson.serialize("message", "baseGas", uint256(0)); + message = stdJson.serialize("message", "gasPrice", uint256(0)); + message = stdJson.serialize("message", "gasToken", address(0)); + message = stdJson.serialize("message", "refundReceiver", address(0)); + message = stdJson.serialize("message", "nonce", _getNonce(safe)); + + // Combine into final JSON structure + string memory json = stdJson.serialize("", "primaryType", string("SafeTx")); + json = stdJson.serialize("", "types", types); + json = stdJson.serialize("", "domain", domain); + json = stdJson.serialize("", "message", message); + + return abi.encodePacked(json); + } + + /// @notice Checks the signatures for the given safe and call. + /// + /// @param safe The address of the safe to check the signatures for. + /// @param call The call to check the signatures for. + /// @param signatures The signatures to check. + function _checkSignatures(address safe, Call memory call, bytes memory signatures) internal view { + bytes32 hash = _getTransactionHash({safe: safe, call: call}); + signatures = Signatures.prepareSignatures({safe: safe, hash: hash, signatures: signatures}); + + IGnosisSafe(safe) + .checkSignatures({ + dataHash: hash, + data: _encodeTransactionData({safe: safe, call: call}), // NOTE: This field is the data preimage but not strictly required as `checkSignatures` ignores it. + signatures: signatures + }); + } + + /// @notice Gets the transaction hash for the given safe and call. + /// + /// @param safe The address of the safe that will execute the transaction. + /// @param call The call to get the transaction hash for. + /// + /// @return The transaction hash for the given safe and call. + function _getTransactionHash(address safe, Call memory call) internal view returns (bytes32) { + return keccak256(_encodeTransactionData({safe: safe, call: call})); + } + + /// @notice Wrapps the given `call` in a `execTransaction` call. + /// + /// @param safe The address of the safe to execute the transaction from. + /// @param call The call to execute. + /// @param signatures The signatures to use for the transaction. + /// + /// @return The execTransaction call. + function _buildExecTransactionCall(address safe, Call memory call, bytes memory signatures) internal - view - returns (bytes memory) + pure + returns (Call memory) { - return abi.encodeCall( - IGnosisSafe(safe).execTransaction, - (multicallAddress, value, data, _getOperation(value), 0, 0, 0, address(0), payable(address(0)), signatures) - ); + return Call({ + operation: Enum.Operation.Call, + target: safe, + data: abi.encodeCall( + IGnosisSafe(safe).execTransaction, + ( + call.target, // to + call.value, // value + call.data, // data + call.operation, // operation + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + payable(address(0)), // refundReceiver + signatures // signatures + ) + ), + value: 0 + }); } - function _execTransaction(address safe, bytes memory data, uint256 value, bytes memory signatures, bool broadcast) + /// @notice Executes the given call from the given safe. + /// + /// @param safe The address of the safe to execute the call from. + /// @param call The call to execute. + /// @param signatures The signatures to use for the transaction. + /// @param broadcast Whether to broadcast the transaction. + /// + /// @return The result of the transaction. + function _execTransaction(address safe, Call memory call, bytes memory signatures, bool broadcast) internal returns (bool) { if (broadcast) { vm.broadcast(); } + return IGnosisSafe(safe) .execTransaction({ - to: multicallAddress, - value: value, - data: data, - operation: _getOperation(value), + to: call.target, + value: call.value, + data: call.data, + operation: call.operation, safeTxGas: 0, baseGas: 0, gasPrice: 0, @@ -626,24 +878,113 @@ abstract contract MultisigScript is Script { }); } - function _toArray(address addr) internal pure returns (address[] memory) { - address[] memory array = new address[](1); - array[0] = addr; - return array; + /// @notice Gets the type for the given call. + /// + /// @param call The call to get the type for. + /// + /// @return The type for the given call. + function _getCall3Type(Call memory call) internal pure returns (Call3Type) { + if (call.operation == Enum.Operation.DelegateCall) { + return Call3Type.DELEGATE_CALL; + } + + if (call.value == 0) { + return Call3Type.CALL; + } + + return Call3Type.CALL_VALUE; } - 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; + /// @notice Converts the given call to the format expected by the `CBMulticall.aggregate3` function. + /// + /// @param call The call to convert to the format expected by the `CBMulticall.aggregate3` function. + /// + /// @return The call in the format expected by the `CBMulticall.aggregate3` function. + function _toCall3(Call memory call) internal pure returns (CBMulticall.Call3 memory) { + require(call.operation == Enum.Operation.Call, "MultisigScript::_toCall3: Operation must be Call"); + require(call.value == 0, "MultisigScript::_toCall3: Value must be 0"); + + return CBMulticall.Call3({target: call.target, allowFailure: false, callData: call.data}); + } + + /// @notice Converts the given call to the format expected by the `CBMulticall.aggregate3Value` function. + /// + /// @param call The call to convert to the format expected by the `CBMulticall.aggregate3Value` function. + /// + /// @return The call in the format expected by the `CBMulticall.aggregate3Value` function. + function _toCall3Value(Call memory call) internal pure returns (CBMulticall.Call3Value memory) { + require(call.operation == Enum.Operation.Call, "MultisigScript::_toCall3Value: Operation must be Call"); + require(call.value > 0, "MultisigScript::_toCall3Value: Value must be greater than 0"); + + return + CBMulticall.Call3Value({target: call.target, allowFailure: false, value: call.value, callData: call.data}); + } + + /// @notice Converts the given call to the format expected by the `CBMulticall.aggregateDelegateCalls` function. + /// + /// @param call The call to convert to the format expected by the `CBMulticall.aggregateDelegateCalls` function. + /// + /// @return The call in the format expected by the `CBMulticall.aggregateDelegateCalls` function. + function _toDelegateCall3(Call memory call) internal pure returns (CBMulticall.Call3 memory) { + require( + call.operation == Enum.Operation.DelegateCall, + "MultisigScript::_toDelegateCall3: Operation must be DelegateCall" + ); + require(call.value == 0, "MultisigScript::_toDelegateCall3: Value must be 0"); + + return CBMulticall.Call3({target: call.target, allowFailure: false, callData: call.data}); + } + + /// @notice Converts the given calls to the format expected by the `aggregate3` function. + /// + /// @param calls The calls to get the call3 values for. + /// + /// @return The calls in the format expected by the `aggregate3` function. + function _toCall3s(Call[] memory calls) internal pure returns (CBMulticall.Call3[] memory) { + CBMulticall.Call3[] memory call3s = new CBMulticall.Call3[](calls.length); + for (uint256 i; i < calls.length; i++) { + call3s[i] = _toCall3(calls[i]); + } + + return call3s; } - function _getOperation(uint256 value) private view returns (Enum.Operation) { - if (multicallAddress == CB_MULTICALL || value == 0) { - return Enum.Operation.DelegateCall; + /// @notice Converts the given calls to the format expected by the `aggregate3Value` function. + /// + /// @param calls The calls to get the call3 values for. + /// + /// @return The calls in the format expected by the `aggregate3` function. + function _toCall3Values(Call[] memory calls) internal pure returns (CBMulticall.Call3Value[] memory) { + CBMulticall.Call3Value[] memory call3Values = new CBMulticall.Call3Value[](calls.length); + for (uint256 i; i < calls.length; i++) { + call3Values[i] = _toCall3Value(calls[i]); } - return Enum.Operation.Call; + return call3Values; + } + + /// @notice Converts the given calls to the format expected by the `aggregateDelegateCalls` function. + /// + /// @param calls The calls to get the call3 values for. + /// + /// @return The calls in the format expected by the `aggregateDelegateCalls` function. + function _toDelegateCall3s(Call[] memory calls) internal pure returns (CBMulticall.Call3[] memory) { + CBMulticall.Call3[] memory delegateCall3s = new CBMulticall.Call3[](calls.length); + for (uint256 i; i < calls.length; i++) { + delegateCall3s[i] = _toDelegateCall3(calls[i]); + } + + return delegateCall3s; + } + + /// @notice Wraps the given address in an array of one address. + /// + /// @param addr The address to wrap. + /// + /// @return The address wrapped in an array of one address. + function _toArray(address addr) internal pure returns (address[] memory) { + address[] memory array = new address[](1); + array[0] = addr; + return array; } } diff --git a/script/universal/NestedMultisigBuilder.sol b/script/universal/NestedMultisigBuilder.sol deleted file mode 100644 index 8a88ae4..0000000 --- a/script/universal/NestedMultisigBuilder.sol +++ /dev/null @@ -1,33 +0,0 @@ -// 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) external { - sign({safes: _toArray(signerSafe)}); - } - - /// @custom:deprecated Use `approve(address[] memory safes, bytes memory signatures)` instead. - function approve(address signerSafe, bytes memory signatures) public { - approve({safes: _toArray(signerSafe), signatures: signatures}); - } - - /// @custom:deprecated Use `simulate(bytes memory signatures)` instead, with empty `signatures`. - function simulate() public { - simulate({signatures: ""}); - } - - /// @custom:deprecated Use `run(bytes memory signatures)` instead, with empty `signatures`. - function run() public { - run({signatures: ""}); - } - - /// @custom:deprecated Use `verify(address[] memory safes, bytes memory signatures)` instead. - function verify(address signerSafe, bytes memory signatures) public view { - verify({safes: _toArray(signerSafe), signatures: signatures}); - } -} diff --git a/test/universal/DoubleNestedMultisigBuilder.t.sol b/test/universal/DoubleNestedMultisigBuilder.t.sol deleted file mode 100644 index a8fdb42..0000000 --- a/test/universal/DoubleNestedMultisigBuilder.t.sol +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; -import {Test} from "forge-std/Test.sol"; -import {Vm} from "forge-std/Vm.sol"; -import {Preinstalls} from "lib/optimism/packages/contracts-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 "test/universal/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 = - // solhint-disable max-line-length - hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280c79f9c7295573dc135fa98d1fc9f5a01ae7e7caad046143376e34f9945288b7a0"; - bytes internal dataToSign2 = - hex"190132640243d7aade8c72f3d90d2dbf359e9897feba5fce1453bc8d9e7ba10d171579f9c7295573dc135fa98d1fc9f5a01ae7e7caad046143376e34f9945288b7a0"; - - 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.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); - - calls[0] = IMulticall3.Call3Value({ - target: address(counter), allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 - }); - - return calls; - } - - function _ownerSafe() internal view override returns (address) { - return safe4; - } - - function test_sign_double_nested_safe1() external { - vm.recordLogs(); - bytes memory txData = abi.encodeCall(DoubleNestedMultisigBuilder.sign, (safe1, safe3)); - vm.prank(wallet1.addr); - (bool success,) = address(this).call(txData); - vm.assertTrue(success); - 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(); - bytes memory txData = abi.encodeCall(DoubleNestedMultisigBuilder.sign, (safe2, safe3)); - vm.prank(wallet2.addr); - (bool success,) = address(this).call(txData); - vm.assertTrue(success); - 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(); - } -} diff --git a/test/universal/MultisigBuilder.t.sol b/test/universal/MultisigBuilder.t.sol deleted file mode 100644 index 252828d..0000000 --- a/test/universal/MultisigBuilder.t.sol +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; -import {Test} from "forge-std/Test.sol"; -import {Vm} from "forge-std/Vm.sol"; -import {Preinstalls} from "lib/optimism/packages/contracts-bedrock/src/libraries/Preinstalls.sol"; - -import {MultisigBuilder} from "script/universal/MultisigBuilder.sol"; -import {Simulation} from "script/universal/Simulation.sol"; -import {IGnosisSafe, Enum} from "script/universal/IGnosisSafe.sol"; -import {Signatures} from "script/universal/Signatures.sol"; - -import {Counter} from "test/universal/Counter.sol"; - -contract MultisigBuilderTest is Test, MultisigBuilder { - Vm.Wallet internal wallet1 = vm.createWallet("1"); - Vm.Wallet internal wallet2 = vm.createWallet("2"); - Vm.Wallet internal wallet3 = vm.createWallet("3"); - - address internal safe = address(1001); - Counter internal counter = new Counter(address(safe)); - - function() internal view returns (IMulticall3.Call3Value[] memory) buildCallsInternal; - - bytes internal dataToSignNoValue = - // solhint-disable-next-line max-line-length - hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280cd0722aa57d06d71497c199147817c38ae160e5b355d3fb5ccbe34c3dbadeae6d"; - - bytes internal dataToSignWithValue = - // solhint-disable-next-line max-line-length - hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280cd150dbb03d4bb38e5325a914ff3861da880437fd5856c0f7e39054e64e05aed0"; - - bytes internal dataToSign3of2 = - // solhint-disable-next-line max-line-length - hex"190132640243d7aade8c72f3d90d2dbf359e9897feba5fce1453bc8d9e7ba10d1715e6bf78f25eeee432952e1453c1b0d0bd867a1d4c4c859aa07ec7e2ef9cb87bc7"; - - function setUp() public { - vm.etch(safe, Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid)); - vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid)); - vm.deal(safe, 10 ether); - - address[] memory owners = new address[](2); - owners[0] = wallet1.addr; - owners[1] = wallet2.addr; - IGnosisSafe(safe).setup(owners, 2, 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.Call3Value[] memory) { - return buildCallsInternal(); - } - - function _ownerSafe() internal view override returns (address) { - return address(safe); - } - - function test_sign_no_value() external { - buildCallsInternal = _buildCallsNoValue; - - vm.recordLogs(); - bytes memory txData = abi.encodeCall(MultisigBuilder.sign, ()); - vm.prank(wallet1.addr); - (bool success,) = address(this).call(txData); - vm.assertTrue(success); - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSignNoValue))); - } - - function test_sign_with_value() external { - buildCallsInternal = _buildCallsWithValue; - - vm.recordLogs(); - bytes memory txData = abi.encodeCall(MultisigBuilder.sign, ()); - vm.prank(wallet1.addr); - (bool success,) = address(this).call(txData); - vm.assertTrue(success); - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSignWithValue))); - } - - function test_run() external { - buildCallsInternal = _buildCallsNoValue; - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSignNoValue)); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSignNoValue)); - bytes memory signatures = abi.encodePacked(r1, s1, v1, r2, s2, v2); - run(signatures); - } - - function test_run_with_more_signatures_than_threshold() external { - // Create a safe with 3 owners but threshold of 2 - address safe3of2 = address(1002); - vm.etch(safe3of2, Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid)); - vm.deal(safe3of2, 10 ether); - - address[] memory owners = new address[](3); - owners[0] = wallet1.addr; - owners[1] = wallet2.addr; - owners[2] = wallet3.addr; - IGnosisSafe(safe3of2).setup(owners, 2, address(0), "", address(0), address(0), 0, address(0)); - - Counter counter3of2 = new Counter(safe3of2); - bytes32 hash = keccak256(dataToSign3of2); - - // Sign with ALL 3 wallets (more than threshold of 2) - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, hash); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, hash); - (uint8 v3, bytes32 r3, bytes32 s3) = vm.sign(wallet3, hash); - - // Provide all 3 signatures (more than threshold) - bytes memory sigs = abi.encodePacked(r1, s1, v1, r2, s2, v2, r3, s3, v3); - sigs = Signatures.prepareSignatures({safe: safe3of2, hash: hash, signatures: sigs}); - - bool success = IGnosisSafe(safe3of2) - .execTransaction({ - to: address(counter3of2), - value: 0, - data: abi.encodeCall(Counter.increment, ()), - operation: Enum.Operation.Call, - safeTxGas: 0, - baseGas: 0, - gasPrice: 0, - gasToken: address(0), - refundReceiver: payable(address(0)), - signatures: sigs - }); - - assertTrue(success, "Should succeed with extra signatures"); - assertEq(counter3of2.count(), 1, "Counter should be incremented"); - } - - function _buildCallsNoValue() internal view returns (IMulticall3.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); - - calls[0] = IMulticall3.Call3Value({ - target: address(counter), allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 - }); - - return calls; - } - - function _buildCallsWithValue() internal view returns (IMulticall3.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); - - calls[0] = IMulticall3.Call3Value({ - target: address(counter), - allowFailure: false, - callData: abi.encodeCall(Counter.incrementPayable, ()), - value: 1 ether - }); - - return calls; - } -} diff --git a/test/universal/MultisigScript.t.sol b/test/universal/MultisigScript.t.sol index adc8171..11b5fba 100644 --- a/test/universal/MultisigScript.t.sol +++ b/test/universal/MultisigScript.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; import {Test} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {Preinstalls} from "lib/optimism/packages/contracts-bedrock/src/libraries/Preinstalls.sol"; @@ -9,9 +8,11 @@ import {Preinstalls} from "lib/optimism/packages/contracts-bedrock/src/libraries import {MultisigScript} from "script/universal/MultisigScript.sol"; import {Simulation} from "script/universal/Simulation.sol"; import {IGnosisSafe, Enum} from "script/universal/IGnosisSafe.sol"; -import {Signatures} from "script/universal/Signatures.sol"; import {Counter} from "test/universal/Counter.sol"; +import {LibString} from "lib/solady/src/utils/LibString.sol"; + +import {CBMulticall} from "src/utils/CBMulticall.sol"; contract MultisigScriptTest is Test, MultisigScript { Vm.Wallet internal wallet1 = vm.createWallet("1"); @@ -21,78 +22,145 @@ contract MultisigScriptTest is Test, MultisigScript { address internal safe = address(1001); Counter internal counter = new Counter(address(safe)); - function() internal view returns (IMulticall3.Call3Value[] memory) buildCallsInternal; - - bytes internal dataToSign3of2 = - // solhint-disable-next-line max-line-length - hex"190132640243d7aade8c72f3d90d2dbf359e9897feba5fce1453bc8d9e7ba10d1715e6bf78f25eeee432952e1453c1b0d0bd867a1d4c4c859aa07ec7e2ef9cb87bc7"; + /// @dev Controls whether to use hash-based or EIP-712 JSON output. True by default. + bool internal _useDataHashes = true; function setUp() public { vm.etch(safe, Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid)); - vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid)); + deployCodeTo("CBMulticall.sol", "", CB_MULTICALL); vm.deal(safe, 10 ether); - address[] memory owners = new address[](2); + // Multisig ownership tree: + // + // ┌───────┐ ┌───────┐ ┌───────┐ + // │wallet1│ │wallet2│ │wallet3│ + // └───┬───┘ └───┬───┘ └───┬───┘ + // └────────┼────────┘ + // ┌────▽────┐ + // │ safe │ (threshold: 2/3) + // └────┬────┘ + // ┌────▽────┐ + // │ counter │ + // └─────────┘ + + address[] memory owners = new address[](3); owners[0] = wallet1.addr; owners[1] = wallet2.addr; + owners[2] = wallet3.addr; IGnosisSafe(safe).setup(owners, 2, address(0), "", address(0), address(0), 0, address(0)); } + /// @inheritdoc MultisigScript + /// + /// @dev Verifies counter was incremented 6 times and received 3 ether function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override { uint256 counterValue = counter.count(); - require(counterValue == 1, "Counter value is not 1"); + assertEq(counterValue, 6, "Counter value is not 6"); + + uint256 counterBalance = address(counter).balance; + assertEq(counterBalance, 3 ether, "Counter balance is not 1 ether"); } - function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) { - return buildCallsInternal(); + /// @inheritdoc MultisigScript + /// + /// @dev Builds a mix of calls to test different operation types: + /// - 1 regular increment call + /// - 1 delegatecall with 2 increments via multicall + /// - 1 payable increment call (1 ether) + /// - 1 delegatecall with 2 payable increments via multicall (2 ether) + /// Total: 6 increments, 3 ether sent + function _buildCalls() internal view override returns (Call[] memory) { + Call memory counterIncrementCall = Call({ + operation: Enum.Operation.Call, + target: address(counter), + data: abi.encodeCall(Counter.increment, ()), + value: 0 + }); + + Call memory counterIncrementCallPayable = Call({ + operation: Enum.Operation.Call, + target: address(counter), + data: abi.encodeCall(Counter.incrementPayable, ()), + value: 1 ether + }); + + Call[] memory counterIncrementCalls = new Call[](2); + counterIncrementCalls[0] = counterIncrementCall; + counterIncrementCalls[1] = counterIncrementCall; + + Call[] memory counterIncrementCallsPayable = new Call[](2); + counterIncrementCallsPayable[0] = counterIncrementCallPayable; + counterIncrementCallsPayable[1] = counterIncrementCallPayable; + + Call[] memory calls = new Call[](4); + + calls[0] = Call({ + operation: Enum.Operation.Call, + target: address(counter), + data: abi.encodeCall(Counter.increment, ()), + value: 0 + }); + + // Use multicall to test the delegatecall use case + calls[1] = Call({ + operation: Enum.Operation.DelegateCall, + target: CB_MULTICALL, + data: abi.encodeCall(CBMulticall.aggregate3, (_toCall3s(counterIncrementCalls))), + value: 0 + }); + + calls[2] = Call({ + operation: Enum.Operation.Call, + target: address(counter), + data: abi.encodeCall(Counter.incrementPayable, ()), + value: 1 ether + }); + + calls[3] = Call({ + operation: Enum.Operation.DelegateCall, + target: CB_MULTICALL, + data: abi.encodeCall(CBMulticall.aggregate3Value, (_toCall3Values(counterIncrementCallsPayable))), + value: 0 + }); + + return calls; } + /// @inheritdoc MultisigScript function _ownerSafe() internal view override returns (address) { return address(safe); } - function _expectedTxDataForCurrentBuildCalls() internal view returns (bytes memory) { - IMulticall3.Call3Value[] memory calls = _buildCalls(); - uint256 value; - for (uint256 i; i < calls.length; i++) { - value += calls[i].value; - } - - // Non-nested case: single owner safe, last call is the aggregate call. - bytes memory data = abi.encodeCall(IMulticall3.aggregate3Value, (calls)); - return _encodeTransactionData(_ownerSafe(), data, value); + /// @inheritdoc MultisigScript + /// + /// @dev Returns `_useDataHashes` which is true by default (hash-based signing). + function _printDataHashes() internal view override returns (bool) { + return _useDataHashes; } - function test_sign_no_value() external { - buildCallsInternal = _buildCallsNoValue; - - vm.recordLogs(); - bytes memory txData = abi.encodeWithSelector(this.sign.selector, new address[](0)); - vm.prank(wallet1.addr); - (bool success,) = address(this).call(txData); - vm.assertTrue(success); - Vm.Log[] memory logs = vm.getRecordedLogs(); - bytes memory logged = abi.decode(logs[logs.length - 1].data, (bytes)); - bytes memory expected = _expectedTxDataForCurrentBuildCalls(); - assertEq(keccak256(logged), keccak256(expected)); + /// @notice Helper to compute the expected transaction data for signing + /// + /// @return The encoded transaction data that signers need to sign + function _expectedTxDataForCurrentBuildCalls() internal view returns (bytes memory) { + return _encodeTransactionData(_ownerSafe(), _buildAggregatedScriptCall({scriptCalls: _buildCalls()})); } - function test_sign_with_value() external { - buildCallsInternal = _buildCallsWithValue; - + /// @notice Tests that sign() emits the correct data to sign + function test_sign() external { vm.recordLogs(); - bytes memory txData = abi.encodeWithSelector(this.sign.selector, new address[](0)); + vm.prank(wallet1.addr); - (bool success,) = address(this).call(txData); - vm.assertTrue(success); + this.sign(new address[](0)); + Vm.Log[] memory logs = vm.getRecordedLogs(); bytes memory logged = abi.decode(logs[logs.length - 1].data, (bytes)); bytes memory expected = _expectedTxDataForCurrentBuildCalls(); + assertEq(keccak256(logged), keccak256(expected)); } + /// @notice Tests that verify() accepts valid 2-of-2 signatures function test_verify_valid_signatures() external { - buildCallsInternal = _buildCallsNoValue; // Two-of-two signatures over the encoded transaction data should verify bytes32 digest = keccak256(_expectedTxDataForCurrentBuildCalls()); (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, digest); @@ -101,8 +169,8 @@ contract MultisigScriptTest is Test, MultisigScript { verify(new address[](0), signatures); } + /// @notice Tests that verify() reverts when given an invalid signature function test_verify_reverts_with_invalid_signature() external { - buildCallsInternal = _buildCallsNoValue; // One valid, one invalid should revert bytes32 digest = keccak256(_expectedTxDataForCurrentBuildCalls()); (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, digest); @@ -113,8 +181,8 @@ contract MultisigScriptTest is Test, MultisigScript { assertTrue(ret.length > 0); } + /// @notice Tests that simulate() executes the transaction without broadcasting function test_simulate_only() external { - buildCallsInternal = _buildCallsNoValue; bytes32 digest = keccak256(_expectedTxDataForCurrentBuildCalls()); (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, digest); (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, digest); @@ -124,66 +192,40 @@ contract MultisigScriptTest is Test, MultisigScript { simulate(signatures); } + /// @notice Tests that a Safe can execute with more signatures than the threshold requires + /// + /// @dev Safe is 2/3, but we provide all 3 signatures function test_run_with_more_signatures_than_threshold() external { - // Create a safe with 3 owners but threshold of 2 - address safe3of2 = address(1002); - vm.etch(safe3of2, Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid)); - vm.deal(safe3of2, 10 ether); + // Sign with all 3 owners (threshold is 2, but we provide 3) + bytes32 digest = keccak256(_expectedTxDataForCurrentBuildCalls()); + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, digest); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, digest); + (uint8 v3, bytes32 r3, bytes32 s3) = vm.sign(wallet3, digest); - address[] memory owners = new address[](3); - owners[0] = wallet1.addr; - owners[1] = wallet2.addr; - owners[2] = wallet3.addr; - IGnosisSafe(safe3of2).setup(owners, 2, address(0), "", address(0), address(0), 0, address(0)); - - Counter counter3of2 = new Counter(safe3of2); - bytes32 hash = keccak256(dataToSign3of2); - - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, hash); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, hash); - (uint8 v3, bytes32 r3, bytes32 s3) = vm.sign(wallet3, hash); - - bytes memory sigs = abi.encodePacked(r1, s1, v1, r2, s2, v2, r3, s3, v3); - sigs = Signatures.prepareSignatures({safe: safe3of2, hash: hash, signatures: sigs}); - - bool success = IGnosisSafe(safe3of2) - .execTransaction({ - to: address(counter3of2), - value: 0, - data: abi.encodeCall(Counter.increment, ()), - operation: Enum.Operation.Call, - safeTxGas: 0, - baseGas: 0, - gasPrice: 0, - gasToken: address(0), - refundReceiver: payable(address(0)), - signatures: sigs - }); - - assertTrue(success, "Should succeed with extra signatures"); - assertEq(counter3of2.count(), 1, "Counter should be incremented"); + bytes memory signatures = abi.encodePacked(r1, s1, v1, r2, s2, v2, r3, s3, v3); + run(signatures); } - function _buildCallsNoValue() internal view returns (IMulticall3.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); - - calls[0] = IMulticall3.Call3Value({ - target: address(counter), allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 - }); + /// @notice Tests that sign() emits EIP-712 JSON formatted data + /// + /// @dev Verifies the output contains expected EIP-712 structure fields + function test_sign_eip712() external { + _useDataHashes = false; - return calls; - } + vm.recordLogs(); - function _buildCallsWithValue() internal view returns (IMulticall3.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); + vm.prank(wallet1.addr); + this.sign(new address[](0)); - calls[0] = IMulticall3.Call3Value({ - target: address(counter), - allowFailure: false, - callData: abi.encodeCall(Counter.incrementPayable, ()), - value: 1 ether - }); + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes memory logged = abi.decode(logs[logs.length - 1].data, (bytes)); - return calls; + // Verify the logged data contains EIP-712 JSON structure markers + string memory loggedStr = string(logged); + assertTrue(LibString.contains(loggedStr, "EIP712Domain"), "EIP-712 output should contain EIP712Domain"); + assertTrue(LibString.contains(loggedStr, "SafeTx"), "EIP-712 output should contain SafeTx type"); + assertTrue(LibString.contains(loggedStr, "primaryType"), "EIP-712 output should contain primaryType"); + assertTrue(LibString.contains(loggedStr, "domain"), "EIP-712 output should contain domain"); + assertTrue(LibString.contains(loggedStr, "message"), "EIP-712 output should contain message"); } } diff --git a/test/universal/MultisigScriptDelegateCall.t.sol b/test/universal/MultisigScriptDelegateCall.t.sol deleted file mode 100644 index 57c0644..0000000 --- a/test/universal/MultisigScriptDelegateCall.t.sol +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; -import {Test} from "forge-std/Test.sol"; -import {Vm} from "forge-std/Vm.sol"; -import {Preinstalls} from "lib/optimism/packages/contracts-bedrock/src/libraries/Preinstalls.sol"; - -import {MultisigScript} from "script/universal/MultisigScript.sol"; -import {Simulation} from "script/universal/Simulation.sol"; -import {IGnosisSafe, Enum} from "script/universal/IGnosisSafe.sol"; - -import {CBMulticall} from "src/utils/CBMulticall.sol"; -import {Counter} from "test/universal/Counter.sol"; - -/// @dev Variant of `MultisigScript` that always uses delegatecall via `CBMulticall`. -/// Used to assert the delegatecall path wiring and encoded data. -contract MultisigScriptDelegateCallTest is Test, MultisigScript { - Vm.Wallet internal wallet1 = vm.createWallet("1"); - Vm.Wallet internal wallet2 = vm.createWallet("2"); - - address internal safe = address(1101); - Counter internal counter = new Counter(address(safe)); - - function() internal view returns (IMulticall3.Call3Value[] memory) buildCallsInternal; - - function setUp() public { - // Deploy Safe and Multicall contracts. - vm.etch(safe, Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid)); - - // Ensure there is code at the standard Multicall3 preinstall for simulation paths - // even though this test primarily exercises the CBMulticall delegatecall wiring. - vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid)); - - // Deploy CBMulticall and map its code to the hard-coded CB_MULTICALL address - CBMulticall mc = new CBMulticall(); - vm.etch(CB_MULTICALL, address(mc).code); - - vm.deal(safe, 10 ether); - - address[] memory owners = new address[](2); - owners[0] = wallet1.addr; - owners[1] = wallet2.addr; - IGnosisSafe(safe).setup(owners, 2, address(0), "", address(0), address(0), 0, address(0)); - } - - function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override { - uint256 counterValue = counter.count(); - require(counterValue == 1, "Counter value is not 1"); - } - - function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) { - return buildCallsInternal(); - } - - function _ownerSafe() internal view override returns (address) { - return address(safe); - } - - /// @dev Force delegatecall mode for this test contract. - function _useDelegateCall() internal view override returns (bool) { - return true; - } - - function test_delegatecall_mode_rejects_non_zero_call_value() external { - buildCallsInternal = _buildCallsWithValue; - - // In delegatecall mode, per-call value is not supported and `_toCall3Array` - // should enforce this invariant. - vm.expectRevert(bytes("MultisigScript: delegatecall mode does not support call value")); - bytes memory txData = abi.encodeWithSelector(this.sign.selector, new address[](0)); - vm.prank(wallet1.addr); - // This should bubble up the revert from `_toCall3Array`. - address(this).call(txData); - } - - function _buildCallsNoValue() internal view returns (IMulticall3.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); - - calls[0] = IMulticall3.Call3Value({ - target: address(counter), allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 - }); - - return calls; - } - - function _buildCallsWithValue() internal view returns (IMulticall3.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); - - calls[0] = IMulticall3.Call3Value({ - target: address(counter), - allowFailure: false, - callData: abi.encodeCall(Counter.incrementPayable, ()), - value: 1 ether - }); - - return calls; - } - - /// @dev Expose `multicallAddress` for test assertions. - function _getMulticallAddress() internal view returns (address) { - return multicallAddress; - } - - /// @dev Expose `CB_MULTICALL` for test assertions. - function _getCbMulticallConstant() internal pure returns (address) { - return CB_MULTICALL; - } -} - diff --git a/test/universal/MultisigScriptDoubleNested.t.sol b/test/universal/MultisigScriptDoubleNested.t.sol index 3bb0145..aa0a279 100644 --- a/test/universal/MultisigScriptDoubleNested.t.sol +++ b/test/universal/MultisigScriptDoubleNested.t.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; import {Test} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {Preinstalls} from "lib/optimism/packages/contracts-bedrock/src/libraries/Preinstalls.sol"; import {MultisigScript} from "script/universal/MultisigScript.sol"; import {Simulation} from "script/universal/Simulation.sol"; -import {IGnosisSafe} from "script/universal/IGnosisSafe.sol"; +import {IGnosisSafe, Enum} from "script/universal/IGnosisSafe.sol"; import {Counter} from "test/universal/Counter.sol"; @@ -22,19 +21,25 @@ contract MultisigScriptDoubleNestedTest is Test, MultisigScript { address internal safe4 = address(1004); Counter internal counter = new Counter(address(safe4)); - bytes internal dataToSign1 = - // solhint-disable max-line-length - hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280c79f9c7295573dc135fa98d1fc9f5a01ae7e7caad046143376e34f9945288b7a0"; - bytes internal dataToSign2 = - hex"190132640243d7aade8c72f3d90d2dbf359e9897feba5fce1453bc8d9e7ba10d171579f9c7295573dc135fa98d1fc9f5a01ae7e7caad046143376e34f9945288b7a0"; - function setUp() public { bytes memory safeCode = Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid); + deployCodeTo("CBMulticall.sol", "", CB_MULTICALL); 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)); + + // Multisig ownership tree: + // + // safe4 (threshold: 1/1) + // | + // safe3 (threshold: 2/2) + // / \ + // / \ + // safe1 safe2 + // (1/1) (1/1) + // | | + // wallet1 wallet2 address[] memory owners1 = new address[](1); owners1[0] = wallet1.addr; @@ -54,100 +59,136 @@ contract MultisigScriptDoubleNestedTest is Test, MultisigScript { IGnosisSafe(safe4).setup(owners4, 1, address(0), "", address(0), address(0), 0, address(0)); } + /// @inheritdoc MultisigScript + /// + /// @dev Verifies counter was incremented once function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override { uint256 counterValue = counter.count(); require(counterValue == 1, "Counter value is not 1"); } - function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); - - calls[0] = IMulticall3.Call3Value({ - target: address(counter), allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 + /// @inheritdoc MultisigScript + function _buildCalls() internal view override returns (Call[] memory) { + Call[] memory calls = new Call[](1); + calls[0] = Call({ + target: address(counter), + operation: Enum.Operation.Call, + data: abi.encodeCall(Counter.increment, ()), + value: 0 }); return calls; } + /// @inheritdoc MultisigScript function _ownerSafe() internal view override returns (address) { return safe4; } + /// @notice Gets the safes array and data to sign for a given signer safe + /// + /// @param signerSafe The address of the signer's Safe (safe1 or safe2) + /// + /// @return safes The array of safes to pass to approve() + /// @return dataToSign The data that needs to be signed + function _getSignerData(address signerSafe) + internal + view + returns (address[] memory safes, bytes memory dataToSign) + { + safes = new address[](2); + safes[0] = signerSafe; + safes[1] = safe3; + + Call[] memory callsChain = _buildCallsChain({safes: _appendOwnerSafe(safes)}); + dataToSign = _encodeTransactionData({safe: signerSafe, call: callsChain[0]}); + } + + /// @notice Tests that sign() emits the correct data to sign for safe1 function test_sign_double_nested_safe1() external { vm.recordLogs(); - address[] memory safes = new address[](2); - safes[0] = safe1; - safes[1] = safe3; + (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe1); + vm.prank(wallet1.addr); bytes memory txData = abi.encodeWithSelector(this.sign.selector, safes); (bool success,) = address(this).call(txData); - vm.assertTrue(success); + assertTrue(success); Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign1))); + assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign))); } + /// @notice Tests that sign() emits the correct data to sign for safe2 function test_sign_double_nested_safe2() external { vm.recordLogs(); - address[] memory safes = new address[](2); - safes[0] = safe2; - safes[1] = safe3; + (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe2); + vm.prank(wallet2.addr); bytes memory txData = abi.encodeWithSelector(this.sign.selector, safes); (bool success,) = address(this).call(txData); - vm.assertTrue(success); + assertTrue(success); Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign2))); + assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign))); } + /// @notice Tests that approve() succeeds with valid signature from safe1 function test_approveInit_double_nested_safe1() external { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); - address[] memory safes = new address[](2); - safes[0] = safe1; - safes[1] = safe3; + (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe1); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign)); approve(safes, abi.encodePacked(r, s, v)); } + /// @notice Tests that approve() succeeds with valid signature from safe2 function test_approveInit_double_nested_safe2() external { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet2, keccak256(dataToSign2)); - address[] memory safes = new address[](2); - safes[0] = safe2; - safes[1] = safe3; + (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe2); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet2, keccak256(dataToSign)); approve(safes, abi.encodePacked(r, s, v)); } + /// @notice Tests that approve() fails when signature doesn't match the safe function test_approveInit_double_nested_notOwner() external { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); - address[] memory safes = new address[](2); - safes[0] = safe2; - safes[1] = safe3; - bytes memory data = abi.encodeCall(this.approve, (safes, abi.encodePacked(r, s, v))); + // Sign with wallet1 for safe1 + (, bytes memory dataToSign) = _getSignerData(safe1); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign)); + + // But try to approve for safe2 (should fail) + (address[] memory safes2,) = _getSignerData(safe2); + + bytes memory data = abi.encodeCall(this.approve, (safes2, 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")); } + /// @notice Tests the approval flow through all nested levels function test_runInit_double_nested() external { + // Prepare and sign for wallet1/safe1 + (address[] memory sA, bytes memory dataToSign1) = _getSignerData(safe1); (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); + + // Prepare and sign for wallet2/safe2 + (address[] memory sB, bytes memory dataToSign2) = _getSignerData(safe2); (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSign2)); - address[] memory sA = new address[](2); - sA[0] = safe1; - sA[1] = safe3; - address[] memory sB = new address[](2); - sB[0] = safe2; - sB[1] = safe3; + + // Approve for safe1 and safe2 approve(sA, abi.encodePacked(r1, s1, v1)); approve(sB, abi.encodePacked(r2, s2, v2)); + + // Approve for safe3 (intermediate level) address[] memory mid = new address[](1); mid[0] = safe3; approve(mid, ""); } + /// @notice Tests that intermediate approve fails when not all leaf safes have approved function test_runInit_double_nested_notApproved() external { - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); - address[] memory sA = new address[](2); - sA[0] = safe1; - sA[1] = safe3; + // Prepare and sign for wallet1/safe1 + (address[] memory sA, bytes memory dataToSign) = _getSignerData(safe1); + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign)); + + // Approve only for safe1 approve(sA, abi.encodePacked(r1, s1, v1)); + + // Try to approve for safe3 without having approved safe2 (should fail) address[] memory mid = new address[](1); mid[0] = safe3; bytes memory data = abi.encodeCall(this.approve, (mid, "")); @@ -156,22 +197,26 @@ contract MultisigScriptDoubleNestedTest is Test, MultisigScript { assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); } + /// @notice Tests the full flow: approve from all nested safes, then run function test_run_double_nested() external { + // Prepare and sign for wallet1/safe1 + (address[] memory sA, bytes memory dataToSign1) = _getSignerData(safe1); (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); + + // Prepare and sign for wallet2/safe2 + (address[] memory sB, bytes memory dataToSign2) = _getSignerData(safe2); (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSign2)); - address[] memory sA = new address[](2); - sA[0] = safe1; - sA[1] = safe3; - address[] memory sB = new address[](2); - sB[0] = safe2; - sB[1] = safe3; + + // Approve for safe1 and safe2 approve(sA, abi.encodePacked(r1, s1, v1)); approve(sB, abi.encodePacked(r2, s2, v2)); + + // Approve for safe3 (intermediate level) address[] memory mid = new address[](1); mid[0] = safe3; approve(mid, ""); + // Execute the final transaction run(""); } } - diff --git a/test/universal/MultisigScriptNested.t.sol b/test/universal/MultisigScriptNested.t.sol index 8b481e1..faff8b9 100644 --- a/test/universal/MultisigScriptNested.t.sol +++ b/test/universal/MultisigScriptNested.t.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; import {Test} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {Preinstalls} from "lib/optimism/packages/contracts-bedrock/src/libraries/Preinstalls.sol"; import {MultisigScript} from "script/universal/MultisigScript.sol"; import {Simulation} from "script/universal/Simulation.sol"; -import {IGnosisSafe} from "script/universal/IGnosisSafe.sol"; +import {IGnosisSafe, Enum} from "script/universal/IGnosisSafe.sol"; import {Counter} from "test/universal/Counter.sol"; contract MultisigScriptNestedTest is Test, MultisigScript { @@ -20,18 +19,28 @@ contract MultisigScriptNestedTest is Test, MultisigScript { address internal safe3 = address(1003); Counter internal counter = new Counter(address(safe3)); - bytes internal dataToSign1 = - // solhint-disable max-line-length - hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280c5f51d24161b7d5dfddfd10cad9118e4e37e6fde740a81d2d84dc35a401b0f74c"; - bytes internal dataToSign2 = - hex"190132640243d7aade8c72f3d90d2dbf359e9897feba5fce1453bc8d9e7ba10d17155f51d24161b7d5dfddfd10cad9118e4e37e6fde740a81d2d84dc35a401b0f74c"; - function setUp() public { bytes memory safeCode = Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid); + deployCodeTo("CBMulticall.sol", "", CB_MULTICALL); vm.etch(safe1, safeCode); vm.etch(safe2, safeCode); vm.etch(safe3, safeCode); - vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid)); + + // Multisig ownership tree: + // + // ┌───────┐ ┌───────┐ + // │wallet1│ │wallet2│ + // └───┬───┘ └───┬───┘ + // ┌───▽───┐ ┌───▽───┐ + // │ safe1 │ │ safe2 │ (threshold: 1/1 each) + // └───┬───┘ └───┬───┘ + // └────┬────┘ + // ┌────▽────┐ + // │ safe3 │ (threshold: 2/2) + // └────┬────┘ + // ┌────▽────┐ + // │ counter │ + // └─────────┘ address[] memory owners1 = new address[](1); owners1[0] = wallet1.addr; @@ -47,94 +56,137 @@ contract MultisigScriptNestedTest is Test, MultisigScript { IGnosisSafe(safe3).setup(owners3, 2, address(0), "", address(0), address(0), 0, address(0)); } + /// @inheritdoc MultisigScript + /// + /// @dev Verifies counter was incremented once function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override { uint256 counterValue = counter.count(); require(counterValue == 1, "Counter value is not 1"); } - function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); + /// @inheritdoc MultisigScript + function _buildCalls() internal view override returns (Call[] memory) { + Call[] memory calls = new Call[](1); - calls[0] = IMulticall3.Call3Value({ - target: address(counter), allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 + calls[0] = Call({ + target: address(counter), + operation: Enum.Operation.Call, + data: abi.encodeCall(Counter.increment, ()), + value: 0 }); return calls; } + /// @inheritdoc MultisigScript function _ownerSafe() internal view override returns (address) { return address(safe3); } + /// @notice Gets the safes array and data to sign for a given signer safe + /// + /// @param signerSafe The address of the signer's Safe (safe1 or safe2) + /// + /// @return safes The array of safes to pass to approve() + /// @return dataToSign The data that needs to be signed + function _getSignerData(address signerSafe) + internal + view + returns (address[] memory safes, bytes memory dataToSign) + { + safes = new address[](1); + safes[0] = signerSafe; + + Call[] memory callsChain = _buildCallsChain({safes: _appendOwnerSafe(safes)}); + dataToSign = _encodeTransactionData({safe: signerSafe, call: callsChain[0]}); + } + + /// @notice Tests that sign() emits the correct data to sign for safe1 function test_sign_safe1() external { vm.recordLogs(); - address[] memory safes = new address[](1); - safes[0] = safe1; + (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe1); + vm.prank(wallet1.addr); bytes memory txData = abi.encodeWithSelector(this.sign.selector, safes); (bool success,) = address(this).call(txData); - vm.assertTrue(success); + assertTrue(success); Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign1))); + assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign))); } + /// @notice Tests that sign() emits the correct data to sign for safe2 function test_sign_safe2() external { vm.recordLogs(); - address[] memory safes = new address[](1); - safes[0] = safe2; + (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe2); + vm.prank(wallet2.addr); bytes memory txData = abi.encodeWithSelector(this.sign.selector, safes); (bool success,) = address(this).call(txData); - vm.assertTrue(success); + assertTrue(success); Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign2))); + assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign))); } + /// @notice Tests that approve() succeeds with valid signature from safe1 function test_approve_safe1() external { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); - address[] memory safes = new address[](1); - safes[0] = safe1; + (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe1); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign)); approve(safes, abi.encodePacked(r, s, v)); } + /// @notice Tests that approve() succeeds with valid signature from safe2 function test_approve_safe2() external { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet2, keccak256(dataToSign2)); - address[] memory safes = new address[](1); - safes[0] = safe2; + (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe2); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet2, keccak256(dataToSign)); approve(safes, abi.encodePacked(r, s, v)); } + /// @notice Tests that approve() fails when signature doesn't match the safe function test_approve_notOwner() external { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); - address[] memory safes = new address[](1); - safes[0] = safe2; - bytes memory data = abi.encodeCall(this.approve, (safes, abi.encodePacked(r, s, v))); + // Sign with wallet1 for safe1 + (, bytes memory dataToSign) = _getSignerData(safe1); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign)); + + // But try to approve for safe2 (should fail) + (address[] memory safes2,) = _getSignerData(safe2); + + bytes memory data = abi.encodeCall(this.approve, (safes2, 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")); } + /// @notice Tests the full flow: approve from both safes, then run function test_run() external { + // Prepare and sign for wallet1/safe1 + (address[] memory safes1, bytes memory dataToSign1) = _getSignerData(safe1); (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); + + // Prepare and sign for wallet2/safe2 + (address[] memory safes2, bytes memory dataToSign2) = _getSignerData(safe2); (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSign2)); - address[] memory arr1 = new address[](1); - arr1[0] = safe1; - address[] memory arr2 = new address[](1); - arr2[0] = safe2; - approve(arr1, abi.encodePacked(r1, s1, v1)); - approve(arr2, abi.encodePacked(r2, s2, v2)); + + // Approve for safe1 and safe2 + approve(safes1, abi.encodePacked(r1, s1, v1)); + approve(safes2, abi.encodePacked(r2, s2, v2)); + + // Execute the final transaction run(""); } + /// @notice Tests that run() fails when not all nested safes have approved function test_run_notApproved() external { - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); - address[] memory arr1 = new address[](1); - arr1[0] = safe1; - approve(arr1, abi.encodePacked(r1, s1, v1)); + // Prepare and sign for wallet1/safe1 only + (address[] memory safes1, bytes memory dataToSign) = _getSignerData(safe1); + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign)); + + // Approve only for safe1 + approve(safes1, abi.encodePacked(r1, s1, v1)); + + // Try to run without safe2 approval (should fail) bytes memory data = abi.encodeCall(this.run, ("")); (bool success, bytes memory result) = address(this).call(data); assertFalse(success); assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); } } - diff --git a/test/universal/NestedMultisigBuilder.t.sol b/test/universal/NestedMultisigBuilder.t.sol deleted file mode 100644 index 3330285..0000000 --- a/test/universal/NestedMultisigBuilder.t.sol +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; -import {Test} from "forge-std/Test.sol"; -import {Vm} from "forge-std/Vm.sol"; -import {Preinstalls} from "lib/optimism/packages/contracts-bedrock/src/libraries/Preinstalls.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 "test/universal/Counter.sol"; - -contract NestedMultisigBuilderTest is Test, NestedMultisigBuilder { - 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); - Counter internal counter = new Counter(address(safe3)); - - bytes internal dataToSign1 = - // solhint-disable max-line-length - hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280c5f51d24161b7d5dfddfd10cad9118e4e37e6fde740a81d2d84dc35a401b0f74c"; - bytes internal dataToSign2 = - hex"190132640243d7aade8c72f3d90d2dbf359e9897feba5fce1453bc8d9e7ba10d17155f51d24161b7d5dfddfd10cad9118e4e37e6fde740a81d2d84dc35a401b0f74c"; - - 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(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)); - } - - 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.Call3Value[] memory) { - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](1); - - calls[0] = IMulticall3.Call3Value({ - target: address(counter), allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 - }); - - return calls; - } - - function _ownerSafe() internal view override returns (address) { - return address(safe3); - } - - function test_sign_safe1() external { - vm.recordLogs(); - bytes memory txData = abi.encodeCall(NestedMultisigBuilder.sign, (safe1)); - vm.prank(wallet1.addr); - (bool success,) = address(this).call(txData); - vm.assertTrue(success); - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign1))); - } - - function test_sign_safe2() external { - vm.recordLogs(); - bytes memory txData = abi.encodeCall(NestedMultisigBuilder.sign, (safe2)); - vm.prank(wallet2.addr); - (bool success,) = address(this).call(txData); - vm.assertTrue(success); - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign2))); - } - - function test_approve_safe1() external { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); - approve(safe1, abi.encodePacked(r, s, v)); - } - - function test_approve_safe2() external { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet2, keccak256(dataToSign2)); - approve(safe2, abi.encodePacked(r, s, v)); - } - - function test_approve_notOwner() external { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet1, keccak256(dataToSign1)); - bytes memory data = - abi.encodeWithSelector(bytes4(keccak256("approve(address,bytes)")), safe2, 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_run() external { - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, keccak256(dataToSign2)); - approve(safe1, abi.encodePacked(r1, s1, v1)); - approve(safe2, abi.encodePacked(r2, s2, v2)); - run(); - } - - function test_run_notApproved() external { - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSign1)); - approve(safe1, abi.encodePacked(r1, s1, v1)); - bytes memory data = abi.encodeWithSelector(bytes4(keccak256("run()"))); - (bool success, bytes memory result) = address(this).call(data); - assertFalse(success); - assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); - } -}