From 0e534ffa7fd80017654f13c8f6f6cf65a6572b83 Mon Sep 17 00:00:00 2001 From: zeroknots Date: Fri, 8 Mar 2024 15:03:49 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20Tests=20passing=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/safe7579/src/SafeERC7579.sol | 5 + accounts/safe7579/src/interfaces/ISafe.sol | 12 ++ accounts/safe7579/src/utils/Boostrap.sol | 69 --------- accounts/safe7579/src/utils/Launchpad.sol | 84 +++++++++- .../src/utils/SignatureValidatorConstants.sol | 15 -- accounts/safe7579/test/Base.t.sol | 143 ++++++++---------- accounts/safe7579/test/Base.t.sol.bak | 122 +++++++++++++++ .../{Launchpad.t.sol => Launchpad.t.sol.bak} | 0 accounts/safe7579/test/SafeERC7579.t.sol | 78 +++++++--- 9 files changed, 347 insertions(+), 181 deletions(-) delete mode 100644 accounts/safe7579/src/utils/Boostrap.sol delete mode 100644 accounts/safe7579/src/utils/SignatureValidatorConstants.sol create mode 100644 accounts/safe7579/test/Base.t.sol.bak rename accounts/safe7579/test/{Launchpad.t.sol => Launchpad.t.sol.bak} (100%) diff --git a/accounts/safe7579/src/SafeERC7579.sol b/accounts/safe7579/src/SafeERC7579.sol index 89e6769c..797d7650 100644 --- a/accounts/safe7579/src/SafeERC7579.sol +++ b/accounts/safe7579/src/SafeERC7579.sol @@ -396,6 +396,11 @@ contract SafeERC7579 is ISafeOp, IERC7579Account, AccessControl, IMSA, HookManag emit Safe7579Initialized(msg.sender); } + /** + * Safe7579 is using validator selection encoding in the userop nonce. + * to make it easier for SDKs / devs to integrate, this function can be + * called to get the next nonce for a specific validator + */ function getNonce(address safe, address validator) external view returns (uint256 nonce) { uint192 key = uint192(bytes24(bytes20(address(validator)))); nonce = IEntryPoint(entryPoint()).getNonce(safe, key); diff --git a/accounts/safe7579/src/interfaces/ISafe.sol b/accounts/safe7579/src/interfaces/ISafe.sol index 25ee4efb..be60fa30 100644 --- a/accounts/safe7579/src/interfaces/ISafe.sol +++ b/accounts/safe7579/src/interfaces/ISafe.sol @@ -2,6 +2,18 @@ pragma solidity ^0.8.0; interface ISafe { + function setup( + address[] calldata _owners, + uint256 _threshold, + address to, + bytes calldata data, + address fallbackHandler, + address paymentToken, + uint256 payment, + address payable paymentReceiver + ) + external; + /** * @dev Allows a Module to execute a Safe transaction without any further confirmations. * @param to Destination address of module transaction. diff --git a/accounts/safe7579/src/utils/Boostrap.sol b/accounts/safe7579/src/utils/Boostrap.sol deleted file mode 100644 index 46981a1b..00000000 --- a/accounts/safe7579/src/utils/Boostrap.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; - -import { ModuleManager } from "../core/ModuleManager.sol"; -import { HookManager } from "../core/HookManager.sol"; - -import { IModule } from "erc7579/interfaces/IERC7579Module.sol"; - -struct BootstrapConfig { - address module; - bytes data; -} - -contract Bootstrap is ModuleManager, HookManager { - function singleInitMSA(IModule validator, bytes calldata data) external { - // init validator - _installValidator(address(validator), data); - } - - /** - * This function is intended to be called by the MSA with a delegatecall. - * Make sure that the MSA already initilazed the linked lists in the ModuleManager prior to - * calling this function - */ - function initMSA( - BootstrapConfig[] calldata _validators, - BootstrapConfig[] calldata _executors, - BootstrapConfig calldata _hook, - BootstrapConfig calldata _fallback - ) - external - { - // init validators - for (uint256 i; i < _validators.length; i++) { - _installValidator(_validators[i].module, _validators[i].data); - } - - // init executors - for (uint256 i; i < _executors.length; i++) { - if (_executors[i].module == address(0)) continue; - _installExecutor(_executors[i].module, _executors[i].data); - } - - // init hook - if (_hook.module != address(0)) { - _installHook(_hook.module, _hook.data); - } - - // init fallback - if (_fallback.module != address(0)) { - _installFallbackHandler(_fallback.module, _fallback.data); - } - } - - function _getInitMSACalldata( - BootstrapConfig[] calldata _validators, - BootstrapConfig[] calldata _executors, - BootstrapConfig calldata _hook, - BootstrapConfig calldata _fallback - ) - external - view - returns (bytes memory init) - { - init = abi.encode( - address(this), abi.encodeCall(this.initMSA, (_validators, _executors, _hook, _fallback)) - ); - } -} diff --git a/accounts/safe7579/src/utils/Launchpad.sol b/accounts/safe7579/src/utils/Launchpad.sol index 9323fa82..b272d990 100644 --- a/accounts/safe7579/src/utils/Launchpad.sol +++ b/accounts/safe7579/src/utils/Launchpad.sol @@ -1,8 +1,88 @@ -import "../SafeERC7579.sol"; +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import { ISafe, SafeERC7579 } from "../SafeERC7579.sol"; + +/** + * Helper contract that gets delegatecalled byt SafeProxy.setup() to setup safe7579 as a module + * (safe module) + * as well as initializing Safe7579 for the SafeProxy + */ +contract Safe7579Launchpad { + address immutable safe7579Singleton; + + constructor(address _safe7579Singleton) { + safe7579Singleton = _safe7579Singleton; + } -contract Launchpad { function initSafe7579(address safe7579, bytes calldata safe7579InitCode) public { ISafe(address(this)).enableModule(safe7579); SafeERC7579(payable(safe7579)).initializeAccount(safe7579InitCode); } + + function predictSafeAddress( + address singleton, + address safeProxyFactory, + bytes memory creationCode, + bytes32 salt, + bytes memory initializer + ) + external + pure + returns (address safeProxy) + { + salt = keccak256(abi.encodePacked(keccak256(initializer), salt)); + + safeProxy = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(safeProxyFactory), + salt, + keccak256( + abi.encodePacked(creationCode, uint256(uint160(address(singleton)))) + ) + ) + ) + ) + ) + ); + } + + function getInitCode( + address[] memory signers, + uint256 threshold, + address[] calldata validators, + bytes[] calldata validatorsInitCode, + address[] calldata executors, + bytes[] calldata executorsInitCode + ) + external + view + returns (bytes memory initCode) + { + bytes memory safeLaunchPadSetup = abi.encodeCall( + this.initSafe7579, + ( + address(safe7579Singleton), + abi.encode(validators, validatorsInitCode, executors, executorsInitCode) + ) + ); + // SETUP SAFE + initCode = abi.encodeCall( + ISafe.setup, + ( + signers, + threshold, + address(this), + safeLaunchPadSetup, + safe7579Singleton, + address(0), + 0, + payable(address(0)) + ) + ); + } } diff --git a/accounts/safe7579/src/utils/SignatureValidatorConstants.sol b/accounts/safe7579/src/utils/SignatureValidatorConstants.sol deleted file mode 100644 index 06d5f1d2..00000000 --- a/accounts/safe7579/src/utils/SignatureValidatorConstants.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -/* solhint-disable one-contract-per-file */ -pragma solidity >=0.7.0 <0.9.0; - -/** - * @title SignatureValidatorConstants - * @dev This contract defines the constants used for EIP-1271 signature validation. - */ -contract SignatureValidatorConstants { - // bytes4(keccak256("isValidSignature(bytes32,bytes)") - bytes4 internal constant EIP1271_MAGIC_VALUE = 0x1626ba7e; - - // bytes4(keccak256("isValidSignature(bytes,bytes)") - bytes4 internal constant LEGACY_EIP1271_MAGIC_VALUE = 0x20c13b0b; -} diff --git a/accounts/safe7579/test/Base.t.sol b/accounts/safe7579/test/Base.t.sol index 82cd0099..7e886b48 100644 --- a/accounts/safe7579/test/Base.t.sol +++ b/accounts/safe7579/test/Base.t.sol @@ -10,106 +10,95 @@ import { MockFallback } from "./mocks/MockFallback.sol"; import { MockTarget } from "@rhinestone/modulekit/src/mocks/MockTarget.sol"; import { Safe } from "@safe-global/safe-contracts/contracts/Safe.sol"; +import { SafeProxyFactory } from + "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol"; import { LibClone } from "solady/src/utils/LibClone.sol"; +import "src/utils/Launchpad.sol"; +import { Solarray } from "solarray/Solarray.sol"; import "./dependencies/EntryPoint.sol"; -contract Bootstrap is ModuleManager { - function singleInitMSA( - address validator, - bytes calldata validatorData, - address executor, - bytes calldata executorData - ) - external - { - // init validator - _installValidator(address(validator), validatorData); - _installExecutor(executor, executorData); - } -} - contract TestBaseUtil is Test { - // singletons - SafeERC7579 internal erc7579Mod; - Safe internal safeImpl; - Safe internal safe; - IEntryPoint internal entrypoint = IEntryPoint(ENTRYPOINT_ADDR); + SafeERC7579 safe7579; + Safe singleton; + Safe safe; + SafeProxyFactory safeProxyFactory; + Safe7579Launchpad launchpad; - MockValidator internal defaultValidator; - MockExecutor internal defaultExecutor; - Bootstrap internal bootstrap; + MockValidator defaultValidator; + MockExecutor defaultExecutor; - MockTarget internal target; + Account signer1 = makeAccount("signer1"); + Account signer2 = makeAccount("signer2"); - Account internal signer1; - Account internal signer2; + IEntryPoint entrypoint; + bytes userOpInitCode; function setUp() public virtual { // Set up EntryPoint - etchEntrypoint(); - - // Set up MSA and Factory - bootstrap = new Bootstrap(); - erc7579Mod = new SafeERC7579(); - safeImpl = new Safe(); - - signer1 = makeAccount("signer1"); - signer2 = makeAccount("signer2"); + entrypoint = etchEntrypoint(); + singleton = new Safe(); + safeProxyFactory = new SafeProxyFactory(); + safe7579 = new SafeERC7579(); + launchpad = new Safe7579Launchpad(address(safe7579)); // Set up Modules - defaultExecutor = new MockExecutor(); defaultValidator = new MockValidator(); + defaultExecutor = new MockExecutor(); - // Set up Target for testing - target = new MockTarget(); - - (safe,) = safeSetup(); - vm.deal(address(safe), 100 ether); - } - - function safeSetup() internal returns (Safe clone, address _defaultValidator) { - clone = Safe(payable(LibClone.clone(address(safeImpl)))); - _defaultValidator = address(defaultValidator); - - address[] memory signers = new address[](2); - signers[0] = signer1.addr; - signers[1] = signer2.addr; - - // TODO: think about launchpad impl see: passkey for safe. - clone.setup({ - _owners: signers, - _threshold: 2, - to: address(0), // optional delegatecall - data: "", - fallbackHandler: address(erc7579Mod), - paymentToken: address(0), // optional payment token - payment: 0, - paymentReceiver: payable(address(0)) // optional payment receiver - }); + bytes32 salt; - vm.startPrank(address(clone)); - clone.enableModule(address(erc7579Mod)); - erc7579Mod.initializeAccount( - abi.encode( - address(bootstrap), - abi.encodeCall( - Bootstrap.singleInitMSA, (_defaultValidator, "", address(defaultExecutor), "") - ) + bytes memory initializer = launchpad.getInitCode({ + signers: Solarray.addresses(signer1.addr, signer2.addr), + threshold: 2, + validators: Solarray.addresses(address(defaultValidator)), + validatorsInitCode: Solarray.bytess(""), + executors: Solarray.addresses(address(defaultExecutor)), + executorsInitCode: Solarray.bytess("") + }); + // computer counterfactual address for SafeProxy + safe = Safe( + payable( + launchpad.predictSafeAddress({ + singleton: address(singleton), + safeProxyFactory: address(safeProxyFactory), + creationCode: safeProxyFactory.proxyCreationCode(), + salt: salt, + initializer: initializer + }) ) ); - vm.stopPrank(); + userOpInitCode = initCode(initializer, salt); } - function getNonce(address account, address validator) internal view returns (uint256 nonce) { - uint192 key = uint192(bytes24(bytes20(address(validator)))); - nonce = entrypoint.getNonce(address(account), key); + function initCode( + bytes memory initializer, + bytes32 salt + ) + internal + view + returns (bytes memory _initCode) + { + _initCode = abi.encodePacked( + address(safeProxyFactory), + abi.encodeCall( + SafeProxyFactory.createProxyWithNonce, + (address(singleton), initializer, uint256(salt)) + ) + ); } - function getDefaultUserOp() internal pure returns (PackedUserOperation memory userOp) { + function getDefaultUserOp( + address account, + address validator + ) + internal + view + returns (PackedUserOperation memory userOp) + { userOp = PackedUserOperation({ - sender: address(0), - nonce: 0, + sender: account, + nonce: safe7579.getNonce(account, validator), initCode: "", callData: "", accountGasLimits: bytes32(abi.encodePacked(uint128(2e6), uint128(2e6))), diff --git a/accounts/safe7579/test/Base.t.sol.bak b/accounts/safe7579/test/Base.t.sol.bak new file mode 100644 index 00000000..82cd0099 --- /dev/null +++ b/accounts/safe7579/test/Base.t.sol.bak @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import { SafeERC7579 } from "src/SafeERC7579.sol"; +import { ModuleManager } from "src/core/ModuleManager.sol"; +import { MockValidator } from "./mocks/MockValidator.sol"; +import { MockExecutor } from "./mocks/MockExecutor.sol"; +import { MockFallback } from "./mocks/MockFallback.sol"; +import { MockTarget } from "@rhinestone/modulekit/src/mocks/MockTarget.sol"; + +import { Safe } from "@safe-global/safe-contracts/contracts/Safe.sol"; +import { LibClone } from "solady/src/utils/LibClone.sol"; + +import "./dependencies/EntryPoint.sol"; + +contract Bootstrap is ModuleManager { + function singleInitMSA( + address validator, + bytes calldata validatorData, + address executor, + bytes calldata executorData + ) + external + { + // init validator + _installValidator(address(validator), validatorData); + _installExecutor(executor, executorData); + } +} + +contract TestBaseUtil is Test { + // singletons + SafeERC7579 internal erc7579Mod; + Safe internal safeImpl; + Safe internal safe; + IEntryPoint internal entrypoint = IEntryPoint(ENTRYPOINT_ADDR); + + MockValidator internal defaultValidator; + MockExecutor internal defaultExecutor; + Bootstrap internal bootstrap; + + MockTarget internal target; + + Account internal signer1; + Account internal signer2; + + function setUp() public virtual { + // Set up EntryPoint + etchEntrypoint(); + + // Set up MSA and Factory + bootstrap = new Bootstrap(); + erc7579Mod = new SafeERC7579(); + safeImpl = new Safe(); + + signer1 = makeAccount("signer1"); + signer2 = makeAccount("signer2"); + + // Set up Modules + defaultExecutor = new MockExecutor(); + defaultValidator = new MockValidator(); + + // Set up Target for testing + target = new MockTarget(); + + (safe,) = safeSetup(); + vm.deal(address(safe), 100 ether); + } + + function safeSetup() internal returns (Safe clone, address _defaultValidator) { + clone = Safe(payable(LibClone.clone(address(safeImpl)))); + _defaultValidator = address(defaultValidator); + + address[] memory signers = new address[](2); + signers[0] = signer1.addr; + signers[1] = signer2.addr; + + // TODO: think about launchpad impl see: passkey for safe. + clone.setup({ + _owners: signers, + _threshold: 2, + to: address(0), // optional delegatecall + data: "", + fallbackHandler: address(erc7579Mod), + paymentToken: address(0), // optional payment token + payment: 0, + paymentReceiver: payable(address(0)) // optional payment receiver + }); + + vm.startPrank(address(clone)); + clone.enableModule(address(erc7579Mod)); + erc7579Mod.initializeAccount( + abi.encode( + address(bootstrap), + abi.encodeCall( + Bootstrap.singleInitMSA, (_defaultValidator, "", address(defaultExecutor), "") + ) + ) + ); + vm.stopPrank(); + } + + function getNonce(address account, address validator) internal view returns (uint256 nonce) { + uint192 key = uint192(bytes24(bytes20(address(validator)))); + nonce = entrypoint.getNonce(address(account), key); + } + + function getDefaultUserOp() internal pure returns (PackedUserOperation memory userOp) { + userOp = PackedUserOperation({ + sender: address(0), + nonce: 0, + initCode: "", + callData: "", + accountGasLimits: bytes32(abi.encodePacked(uint128(2e6), uint128(2e6))), + preVerificationGas: 2e6, + gasFees: bytes32(abi.encodePacked(uint128(2e6), uint128(2e6))), + paymasterAndData: bytes(""), + signature: abi.encodePacked(hex"41414141") + }); + } +} diff --git a/accounts/safe7579/test/Launchpad.t.sol b/accounts/safe7579/test/Launchpad.t.sol.bak similarity index 100% rename from accounts/safe7579/test/Launchpad.t.sol rename to accounts/safe7579/test/Launchpad.t.sol.bak diff --git a/accounts/safe7579/test/SafeERC7579.t.sol b/accounts/safe7579/test/SafeERC7579.t.sol index fe8c6baa..335f8ecf 100644 --- a/accounts/safe7579/test/SafeERC7579.t.sol +++ b/accounts/safe7579/test/SafeERC7579.t.sol @@ -6,14 +6,30 @@ import "erc7579/lib/ModeLib.sol"; import "erc7579/lib/ExecutionLib.sol"; import { TestBaseUtil, MockTarget, MockFallback } from "./Base.t.sol"; -contract MSATest is TestBaseUtil { +import "forge-std/console2.sol"; + +contract Safe7579Test is TestBaseUtil { + MockTarget target; + function setUp() public override { super.setUp(); + target = new MockTarget(); + deal(address(safe), 1 ether); + } + + modifier alreadyInitialized(bool initNow) { + if (initNow) { + test_initializeAccount(); + } + _; } - function test_execSingle() public { + function test_initializeAccount() public { + PackedUserOperation memory userOp = + getDefaultUserOp(address(safe), address(defaultValidator)); + // Create calldata for the account to execute - bytes memory setValueOnTarget = abi.encodeCall(MockTarget.set, 1337); + bytes memory setValueOnTarget = abi.encodeCall(MockTarget.set, 777); // Encode the call into the calldata for the userOp bytes memory userOpCalldata = abi.encodeCall( @@ -23,15 +39,39 @@ contract MSATest is TestBaseUtil { ExecutionLib.encodeSingle(address(target), uint256(0), setValueOnTarget) ) ); + userOp.initCode = userOpInitCode; + userOp.callData = userOpCalldata; + // Create userOps array + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; - // Get the account, initcode and nonce - uint256 nonce = getNonce(address(safe), address(defaultValidator)); + // Send the userOp to the entrypoint + entrypoint.handleOps(userOps, payable(address(0x69))); - // Create the userOp and add the data - PackedUserOperation memory userOp = getDefaultUserOp(); - userOp.sender = address(safe); - userOp.nonce = nonce; - userOp.initCode = ""; + // Assert that the value was set ie that execution was successful + assertTrue(target.value() == 777); + userOpInitCode = ""; + } + + function test_execSingle(bool withInitializedAccount) + public + alreadyInitialized(withInitializedAccount) + { + // Create calldata for the account to execute + bytes memory setValueOnTarget = abi.encodeCall(MockTarget.set, 1337); + + // Encode the call into the calldata for the userOp + bytes memory userOpCalldata = abi.encodeCall( + IERC7579Account.execute, + ( + ModeLib.encodeSimpleSingle(), + ExecutionLib.encodeSingle(address(target), uint256(0), setValueOnTarget) + ) + ); + + PackedUserOperation memory userOp = + getDefaultUserOp(address(safe), address(defaultValidator)); + userOp.initCode = userOpInitCode; userOp.callData = userOpCalldata; // Create userOps array @@ -45,7 +85,10 @@ contract MSATest is TestBaseUtil { assertTrue(target.value() == 1337); } - function test_execBatch() public { + function test_execBatch(bool withInitializedAccount) + public + alreadyInitialized(withInitializedAccount) + { // Create calldata for the account to execute bytes memory setValueOnTarget = abi.encodeCall(MockTarget.set, 1337); address target2 = address(0x420); @@ -62,14 +105,10 @@ contract MSATest is TestBaseUtil { (ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(executions)) ); - address account = address(safe); - uint256 nonce = getNonce(account, address(defaultValidator)); - // Create the userOp and add the data - PackedUserOperation memory userOp = getDefaultUserOp(); - userOp.sender = address(account); - userOp.nonce = nonce; - userOp.initCode = ""; + PackedUserOperation memory userOp = + getDefaultUserOp(address(safe), address(defaultValidator)); + userOp.initCode = userOpInitCode; userOp.callData = userOpCalldata; // Create userOps array @@ -84,6 +123,7 @@ contract MSATest is TestBaseUtil { } function test_execViaExecutor() public { + test_initializeAccount(); defaultExecutor.executeViaAccount( IERC7579Account(address(safe)), address(target), @@ -93,6 +133,7 @@ contract MSATest is TestBaseUtil { } function test_execBatchFromExecutor() public { + test_initializeAccount(); bytes memory setValueOnTarget = abi.encodeCall(MockTarget.set, 1338); Execution[] memory executions = new Execution[](2); executions[0] = Execution({ target: address(target), value: 0, callData: setValueOnTarget }); @@ -107,6 +148,7 @@ contract MSATest is TestBaseUtil { } function test_fallback() public { + test_initializeAccount(); MockFallback _fallback = new MockFallback(); vm.prank(address(safe)); IERC7579Account(address(safe)).installModule(3, address(_fallback), "");