diff --git a/src/interfaces/IMultiSend.sol b/src/interfaces/IMultiSend.sol index 004d99b..bd7492a 100644 --- a/src/interfaces/IMultiSend.sol +++ b/src/interfaces/IMultiSend.sol @@ -2,5 +2,17 @@ pragma solidity ^0.8.12; interface IMultiSend { + /// @dev Sends multiple transactions and reverts all if one fails. + /// @param transactions Encoded transactions. Each transaction is encoded as a packed bytes of + /// operation has to be uint8(0) in this version (=> 1 byte), + /// to as a address (=> 20 bytes), + /// value as a uint256 (=> 32 bytes), + /// data length as a uint256 (=> 32 bytes), + /// data as bytes. + /// see abi.encodePacked for more information on packed encoding + /// @notice The code is for most part the same as the normal MultiSend (to keep compatibility), + /// but reverts if a transaction tries to use a delegatecall. + /// @notice This method is payable as delegatecalls keep the msg.value from the previous call + /// If the calling method (e.g. execTransaction) received ETH this would revert otherwise function multiSend(bytes memory transactions) external payable; } diff --git a/src/interfaces/IProxyAdmin.sol b/src/interfaces/IProxyAdmin.sol new file mode 100644 index 0000000..3443aee --- /dev/null +++ b/src/interfaces/IProxyAdmin.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "./ITransparentUpgradeableProxy.sol"; + +interface IProxyAdmin { + function getProxyImplementation(ITransparentUpgradeableProxy proxy) external view returns (address); + + function getProxyAdmin(ITransparentUpgradeableProxy proxy) external view returns (address); + + function changeProxyAdmin(ITransparentUpgradeableProxy proxy, address newAdmin) external; + + function upgrade(ITransparentUpgradeableProxy proxy, address implementation) external; + + function upgradeAndCall(ITransparentUpgradeableProxy proxy, address implementation, bytes memory data) + external + payable; +} diff --git a/src/interfaces/ITransparentUpgradeableProxy.sol b/src/interfaces/ITransparentUpgradeableProxy.sol new file mode 100644 index 0000000..1825d5a --- /dev/null +++ b/src/interfaces/ITransparentUpgradeableProxy.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +interface ITransparentUpgradeableProxy { + function admin() external view returns (address); + + function implementation() external view returns (address); + + function changeAdmin(address) external; + + function upgradeTo(address) external; + + function upgradeToAndCall(address, bytes memory) external payable; +} diff --git a/src/interfaces/IUpgradeableBeacon.sol b/src/interfaces/IUpgradeableBeacon.sol new file mode 100644 index 0000000..bff8cfe --- /dev/null +++ b/src/interfaces/IUpgradeableBeacon.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +interface IUpgradeableBeacon { + function implementation() external view returns (address); + + function upgradeTo(address newImplementation) external; +} diff --git a/src/templates/MultisigBuilder.sol b/src/templates/MultisigBuilder.sol index bc12615..beec8db 100644 --- a/src/templates/MultisigBuilder.sol +++ b/src/templates/MultisigBuilder.sol @@ -2,18 +2,12 @@ pragma solidity ^0.8.12; import {ZeusScript} from "../utils/ZeusScript.sol"; -import {MultisigCall, MultisigCallUtils} from "../utils/MultisigCallUtils.sol"; -import {SafeTx, EncGnosisSafe} from "../utils/SafeTxUtils.sol"; /** * @title MultisigBuilder * @dev Abstract contract for building arbitrary multisig scripts. */ abstract contract MultisigBuilder is ZeusScript { - using MultisigCallUtils for MultisigCall[]; - - string internal constant multiSendCallOnlyName = "MultiSendCallOnly"; - /** * @notice Constructs a SafeTx object for a Gnosis Safe to ingest. Emits via `ZeusMultisigExecute` */ diff --git a/src/utils/EncGnosisSafe.sol b/src/utils/EncGnosisSafe.sol deleted file mode 100644 index 31107c5..0000000 --- a/src/utils/EncGnosisSafe.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.12; - -import {ISafe} from "../interfaces/ISafe.sol"; - -library EncGnosisSafe { - enum Operation { - Call, - DelegateCall - } - - uint256 constant SAFE_TX_GAS = 0; - uint256 constant BASE_GAS = 0; - uint256 constant GAS_PRICE = 0; - address constant GAS_TOKEN = address(uint160(0)); - address payable constant REFUND_RECEIVER = payable(address(uint160(0))); - - function calldataToExecTransaction(address from, address to, bytes memory data, Operation op) - internal - pure - returns (bytes memory) - { - return encodeForExecutor(from, to, 0, data, op); - } - - function calldataToExecTransaction(address from, address to, uint256 value, bytes memory data, Operation op) - internal - pure - returns (bytes memory) - { - return encodeForExecutor(from, to, value, data, op); - } - - function encodeForExecutor(address from, address to, uint256 value, bytes memory data, Operation op) - internal - pure - returns (bytes memory) - { - bytes1 v = bytes1(uint8(1)); - bytes32 r = bytes32(uint256(uint160(from))); - bytes32 s; - bytes memory sig = abi.encodePacked(r, s, v); - - return abi.encodeCall( - ISafe.execTransaction, - (to, value, data, uint8(op), SAFE_TX_GAS, BASE_GAS, GAS_PRICE, GAS_TOKEN, REFUND_RECEIVER, sig) - ); - } -} diff --git a/src/utils/Encode.sol b/src/utils/Encode.sol new file mode 100644 index 0000000..41a9f91 --- /dev/null +++ b/src/utils/Encode.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +// import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +// import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +import "../interfaces/IMultiSend.sol"; +import "../interfaces/ISafe.sol"; +import "../interfaces/IProxyAdmin.sol"; +import "../interfaces/IUpgradeableBeacon.sol"; + +struct MultisigCall { + address to; + uint256 value; + bytes data; +} + +library Encode { + /// Used for state vars: + /// + /// uint idx; + /// mapping(uint => MultisigCall[]) map; + bytes32 internal constant IDX_SLOT = keccak256("IDX_SLOT"); + bytes32 internal constant MAP_SLOT = keccak256("MAP_SLOT"); + + /// Used to describe calls from a Gnosis Safe + enum Operation { + Call, + DelegateCall + } + + /// Constants for Safe.execTransaction inputs we don't usee + uint256 constant SAFE_TX_GAS = 0; + uint256 constant BASE_GAS = 0; + uint256 constant GAS_PRICE = 0; + address constant GAS_TOKEN = address(uint160(0)); + address payable constant REFUND_RECEIVER = payable(address(uint160(0))); + + /// Dummy types and variables to facilitate syntax, e.g: `Encode.proxyAdmin.upgrade(...)` + enum EncProxyAdmin { + A + } + enum EncUpgradeableBeacon { + A + } + enum EncGnosisSafe { + A + } + + EncProxyAdmin internal constant proxyAdmin = EncProxyAdmin.A; + EncUpgradeableBeacon internal constant upgradeableBeacon = EncUpgradeableBeacon.A; + EncGnosisSafe internal constant gnosisSafe = EncGnosisSafe.A; + + /// @dev Creates a new, clean `MultisigCall[] storage` pointer, guaranteeing + /// any previous pointers will not be overwritten. + /// Since we're in a library, we have to use assembly+slot pointers, but the + /// high level version of this function equates to: + /// + /// uint _idx = storage.idx; + /// storage.idx++; + /// MultisigCall[] storage calls = storage.map[_idx]; + /// return calls; + function newMultisigCalls() internal returns (MultisigCall[] storage) { + bytes32 _IDX_SLOT = IDX_SLOT; + bytes32 _MAP_SLOT = MAP_SLOT; + + uint256 idx; + assembly { + idx := sload(_IDX_SLOT) + sstore(_IDX_SLOT, add(1, idx)) + } + + // fn pointer indirection fools the compiler into letting us have + // an uninitialized storage pointer + function() pure returns (mapping(uint => MultisigCall[]) storage) fn; + function() pure returns (uint) fn2 = func; + assembly { + fn := fn2 + } + mapping(uint256 => MultisigCall[]) storage map = fn(); + assembly { + map.slot := _MAP_SLOT + } + + return map[idx]; + } + + function func() internal pure returns (uint256) { + return 0; + } + + /// @dev Appends a call to a list of `MultisigCalls`, returning the original storage pointer + /// to facilitate call chaining syntax, e.g: + /// + /// calls + /// .append(...) + /// .append(...) + /// .append(...); + function append(MultisigCall[] storage calls, address to, bytes memory data) + internal + returns (MultisigCall[] storage) + { + calls.push(MultisigCall({to: to, value: 0, data: data})); + + return calls; + } + + /// @dev Encodes a call to `ProxyAdmin.upgrade(proxy, impl)` + function upgrade(EncProxyAdmin, address proxy, address impl) internal pure returns (bytes memory) { + return abi.encodeCall(IProxyAdmin.upgrade, (ITransparentUpgradeableProxy(proxy), impl)); + } + + /// @dev Encodes a call to `UpgradeableBeacon.upgradeTo(newImpl)` + function upgradeTo(EncUpgradeableBeacon, address newImpl) internal pure returns (bytes memory) { + return abi.encodeCall(IUpgradeableBeacon.upgradeTo, (newImpl)); + } + + /// @dev Encodes a call to `MultiSend.multiSend(data)` + function multiSend(MultisigCall[] memory calls) internal pure returns (bytes memory) { + bytes memory packedCalls = new bytes(0); + + for (uint256 i = 0; i < calls.length; i++) { + packedCalls = abi.encodePacked( + packedCalls, + abi.encodePacked(uint8(0), calls[i].to, calls[i].value, uint256(calls[i].data.length), calls[i].data) + ); + } + + return abi.encodeCall(IMultiSend.multiSend, packedCalls); + } + + /// @dev Encodes a call to `Safe.execTransaction` + function execTransaction(EncGnosisSafe, address from, address to, Operation op, bytes memory data) + internal + pure + returns (bytes memory) + { + return _encExecTranasction({from: from, to: to, op: op, value: 0, data: data}); + } + + function _encExecTranasction(address from, address to, Operation op, uint256 value, bytes memory data) + private + pure + returns (bytes memory) + { + bytes1 v = bytes1(uint8(1)); + bytes32 r = bytes32(uint256(uint160(from))); + bytes32 s; + bytes memory sig = abi.encodePacked(r, s, v); + + return abi.encodeCall( + ISafe.execTransaction, + (to, value, data, uint8(op), SAFE_TX_GAS, BASE_GAS, GAS_PRICE, GAS_TOKEN, REFUND_RECEIVER, sig) + ); + } +} diff --git a/src/utils/MultisigCallUtils.sol b/src/utils/MultisigCallUtils.sol deleted file mode 100644 index 5607c50..0000000 --- a/src/utils/MultisigCallUtils.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.12; - -import {ISafe} from "../interfaces/ISafe.sol"; -import {IMultiSend} from "../interfaces/IMultiSend.sol"; - -struct MultisigCall { - address to; - uint256 value; - bytes data; -} - -library MultisigCallUtils { - function append(MultisigCall[] storage multisigCalls, address to, uint256 value, bytes memory data) - internal - returns (MultisigCall[] storage) - { - multisigCalls.push(MultisigCall({to: to, value: value, data: data})); - - return multisigCalls; - } - - /// @notice appends a multisig call with a value of 0 - function append(MultisigCall[] storage multisigCalls, address to, bytes memory data) - internal - returns (MultisigCall[] storage) - { - multisigCalls.push(MultisigCall({to: to, value: 0, data: data})); - - return multisigCalls; - } - - function encodeMultisendTxs(IMultiSend ms, MultisigCall[] memory txs) public pure returns (bytes memory) { - bytes memory ret = new bytes(0); - for (uint256 i = 0; i < txs.length; i++) { - ret = abi.encodePacked( - ret, abi.encodePacked(uint8(0), txs[i].to, txs[i].value, uint256(txs[i].data.length), txs[i].data) - ); - } - - return abi.encodeCall(ms.multiSend, ret); - } -} diff --git a/src/utils/SafeTxUtils.sol b/src/utils/SafeTxUtils.sol deleted file mode 100644 index 549ae71..0000000 --- a/src/utils/SafeTxUtils.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.12; - -import {ISafe, EncGnosisSafe} from "../utils/EncGnosisSafe.sol"; - -/// @notice SafeTx data struct -/// @dev based on -struct SafeTx { - address to; - uint256 value; - bytes data; - EncGnosisSafe.Operation op; -} - -library SafeTxUtils { - function encodeForExecutor(SafeTx memory t, address timelock) public pure returns (bytes memory) { - // TODO: validate the correctness of this sig - bytes1 v = bytes1(uint8(1)); - bytes32 r = bytes32(uint256(uint160(timelock))); - bytes32 s; - bytes memory sig = abi.encodePacked(r, s, v); - - bytes memory executorCalldata = abi.encodeWithSelector( - ISafe.execTransaction.selector, - t.to, - t.value, - t.data, - t.op, - 0, // safeTxGas - 0, // baseGas - 0, // gasPrice - address(uint160(0)), // gasToken - payable(address(uint160(0))), // refundReceiver - sig - ); - - return executorCalldata; - } -} diff --git a/src/utils/ZEnvHelpers.sol b/src/utils/ZEnvHelpers.sol index d2c73ea..e0132d8 100644 --- a/src/utils/ZEnvHelpers.sol +++ b/src/utils/ZEnvHelpers.sol @@ -33,7 +33,6 @@ struct State { mapping(string => uint16) updatedUInt16s; mapping(string => uint8) updatedUInt8s; mapping(string => bool) updatedBools; - //////////////////////////////////// mapping(string => Cleanliness) _dirty; string[] _modifiedKeys; diff --git a/src/utils/ZeusScript.sol b/src/utils/ZeusScript.sol index 887c6d2..60a65d3 100644 --- a/src/utils/ZeusScript.sol +++ b/src/utils/ZeusScript.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.12; import "./ZEnvHelpers.sol"; +import "./Encode.sol"; import {StringUtils} from "./StringUtils.sol"; import {Script} from "forge-std/Script.sol"; -import {EncGnosisSafe} from "./EncGnosisSafe.sol"; import {Test} from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; @@ -25,7 +25,7 @@ abstract contract ZeusScript is Script, Test { event ZeusRequireMultisig(address addr, Operation callType); event ZeusEnvironmentUpdate(string key, EnvironmentVariableType internalType, bytes value); event ZeusDeploy(string name, address addr, bool singleton); - event ZeusMultisigExecute(address to, uint256 value, bytes data, EncGnosisSafe.Operation op); + event ZeusMultisigExecute(address to, uint256 value, bytes data, Encode.Operation op); /** * Environment manipulation - update variables in the current environment's configuration ***** diff --git a/test/ZeusScript.test.sol b/test/ZeusScript.test.sol index 92ef41e..993e83f 100644 --- a/test/ZeusScript.test.sol +++ b/test/ZeusScript.test.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.12; import {Test} from "forge-std/Test.sol"; -import {ZeusScript, EncGnosisSafe} from "../src/utils/ZeusScript.sol"; +import {ZeusScript} from "../src/utils/ZeusScript.sol"; +import "../src/utils/Encode.sol"; import {EOADeployer} from "../src/templates/EOADeployer.sol"; import "../src/utils/ZEnvHelpers.sol"; import {StringUtils} from "../src/utils/StringUtils.sol"; @@ -415,10 +416,9 @@ contract ZeusScriptTest is EOADeployer { } function testZeusMultisigExecuteEvent() public { - // EncGnosisSafe.Operation is presumably the same shape as Operation vm.expectEmit(true, true, true, true); - emit ZeusMultisigExecute(address(0x123), 1, "0x1234", EncGnosisSafe.Operation.Call); - emit ZeusMultisigExecute(address(0x123), 1, "0x1234", EncGnosisSafe.Operation.Call); + emit ZeusMultisigExecute(address(0x123), 1, "0x1234", Encode.Operation.Call); + emit ZeusMultisigExecute(address(0x123), 1, "0x1234", Encode.Operation.Call); } function testZDeployedInstanceCountZero() public view { @@ -533,8 +533,8 @@ contract ZeusScriptTest is EOADeployer { function testZeusMultisigExecuteEventDelegateCall() public { // Emit with DelegateCall operation to cover that branch vm.expectEmit(true, true, true, true); - emit ZeusMultisigExecute(address(0x123), 1, "0x5678", EncGnosisSafe.Operation.DelegateCall); - emit ZeusMultisigExecute(address(0x123), 1, "0x5678", EncGnosisSafe.Operation.DelegateCall); + emit ZeusMultisigExecute(address(0x123), 1, "0x5678", Encode.Operation.DelegateCall); + emit ZeusMultisigExecute(address(0x123), 1, "0x5678", Encode.Operation.DelegateCall); } function testInvalidEnvAddress() public {