diff --git a/accounts/safe7579/src/SafeERC7579.sol b/accounts/safe7579/src/SafeERC7579.sol index 52f5bae7..b5f25a2e 100644 --- a/accounts/safe7579/src/SafeERC7579.sol +++ b/accounts/safe7579/src/SafeERC7579.sol @@ -29,12 +29,14 @@ import { } from "@ERC4337/account-abstraction/contracts/core/UserOperationLib.sol"; import { _packValidationData } from "@ERC4337/account-abstraction/contracts/core/Helpers.sol"; +import "forge-std/console2.sol"; /** * @title ERC7579 Adapter for Safe accounts. * By using Safe's Fallback and Execution modules, * this contract creates full ERC7579 compliance to Safe accounts * @author zeroknots.eth | rhinestone.wtf */ + contract SafeERC7579 is ISafeOp, IERC7579Account, AccessControl, IMSA, HookManager { using UserOperationLib for PackedUserOperation; using ModeLib for ModeCode; @@ -42,6 +44,8 @@ contract SafeERC7579 is ISafeOp, IERC7579Account, AccessControl, IMSA, HookManag error Unsupported(); + event Safe7579Initialized(address indexed safe); + bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218; @@ -151,6 +155,7 @@ contract SafeERC7579 is ISafeOp, IERC7579Account, AccessControl, IMSA, HookManag // pay prefund if (missingAccountFunds != 0) { + console2.log("missingAccountFunds", missingAccountFunds); _execute({ safe: userOp.getSender(), target: entryPoint(), @@ -368,12 +373,28 @@ contract SafeERC7579 is ISafeOp, IERC7579Account, AccessControl, IMSA, HookManag return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, block.chainid, this)); } - function initializeAccount(bytes calldata data) external payable { + function initializeAccount(bytes calldata initCode) external payable { _initModuleManager(); - (address bootstrap, bytes memory bootstrapCall) = abi.decode(data, (address, bytes)); + ( + address[] memory validator, + bytes[] memory validatorInitcode, + address[] memory executors, + bytes[] memory executorsInitcode + ) = abi.decode(initCode, (address[], bytes[], address[], bytes[])); + + uint256 length = validator.length; + if (length != validatorInitcode.length) revert("Invalid input"); + for (uint256 i; i < length; i++) { + _installValidator(validator[i], validatorInitcode[i]); + } + + length = executors.length; + if (length != executorsInitcode.length) revert("Invalid input"); + for (uint256 i; i < length; i++) { + _installExecutor(executors[i], executorsInitcode[i]); + } - (bool success,) = bootstrap.delegatecall(bootstrapCall); - if (!success) revert AccountInitializationFailed(); + emit Safe7579Initialized(msg.sender); } } diff --git a/accounts/safe7579/src/utils/Launchpad.sol b/accounts/safe7579/src/utils/Launchpad.sol new file mode 100644 index 00000000..9323fa82 --- /dev/null +++ b/accounts/safe7579/src/utils/Launchpad.sol @@ -0,0 +1,8 @@ +import "../SafeERC7579.sol"; + +contract Launchpad { + function initSafe7579(address safe7579, bytes calldata safe7579InitCode) public { + ISafe(address(this)).enableModule(safe7579); + SafeERC7579(payable(safe7579)).initializeAccount(safe7579InitCode); + } +} diff --git a/accounts/safe7579/src/utils/SafeLaunchpad.sol b/accounts/safe7579/src/utils/SafeLaunchpad.sol deleted file mode 100644 index 0f14a07a..00000000 --- a/accounts/safe7579/src/utils/SafeLaunchpad.sol +++ /dev/null @@ -1,364 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity >=0.8.0 <0.9.0; - -import { IAccount } from "@ERC4337/account-abstraction/contracts/interfaces/IAccount.sol"; - -import { - PackedUserOperation, - UserOperationLib -} from "@ERC4337/account-abstraction/contracts/core/UserOperationLib.sol"; -import { _packValidationData } from "@ERC4337/account-abstraction/contracts/core/Helpers.sol"; -import { SafeStorage } from "@safe-global/safe-contracts/contracts/libraries/SafeStorage.sol"; -import { SignatureValidatorConstants } from "./SignatureValidatorConstants.sol"; - -interface IUniqueSignerFactory { - /** - * @notice Gets the unique signer address for the specified data. - * @dev The unique signer address must be unique for some given data. The signer is not - * guaranteed to be created yet. - * @param data The signer specific data. - * @return signer The signer address. - */ - function getSigner(bytes memory data) external view returns (address signer); - - /** - * @notice Create a new unique signer for the specified data. - * @dev The unique signer address must be unique for some given data. This must not revert if - * the unique owner already exists. - * @param data The signer specific data. - * @return signer The signer address. - */ - function createSigner(bytes memory data) external returns (address signer); - - /** - * @notice Verifies a signature for the specified address without deploying it. - * @dev This must be equivalent to first deploying the signer with the factory, and then - * verifying the signature - * with it directly: `factory.createSigner(signerData).isValidSignature(message, signature)` - * @param message The signed message. - * @param signature The signature bytes. - * @param signerData The signer data to verify signature for. - * @return magicValue Returns a legacy EIP-1271 magic value - * (`bytes4(keccak256(isValidSignature(bytes,bytes))`) when the signature is valid. Reverting or - * returning any other value implies an invalid signature. - */ - function isValidSignatureForSigner( - bytes32 message, - bytes calldata signature, - bytes calldata signerData - ) - external - view - returns (bytes4 magicValue); -} - -/** - * @title SafeOpLaunchpad - A contract for Safe initialization with custom unique signers that would - * violate ERC-4337 factory rules. - * @dev The is intended to be set as a Safe proxy's implementation for ERC-4337 user operation that - * deploys the account. - */ -contract SafeSignerLaunchpad is IAccount, SafeStorage, SignatureValidatorConstants { - bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = - keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); - - // keccak256("SafeSignerLaunchpad.initHash") - 1 - uint256 private constant INIT_HASH_SLOT = - 0x1d2f0b9dbb6ed3f829c9614e6c5d2ea2285238801394dc57e8500e0e306d8f80; - - /** - * @notice The keccak256 hash of the EIP-712 SafeInit struct, representing the structure of a - * ERC-4337 compatible deferred Safe initialization. - * {address} singleton - The singleton to evolve into during the setup. - * {address} signerFactory - The unique signer factory to use for creating an owner. - * {bytes} signerData - The signer data to use the owner. - * {address} setupTo - The contract to delegatecall during setup. - * {bytes} setupData - The calldata for the setup delegatecall. - * {address} fallbackHandler - The fallback handler to initialize the Safe with. - */ - bytes32 private constant SAFE_INIT_TYPEHASH = keccak256( - "SafeInit(address singleton,address signerFactory,bytes signerData,address setupTo,bytes setupData,address fallbackHandler)" - ); - - /** - * @notice The keccak256 hash of the EIP-712 SafeInitOp struct, representing the user operation - * to execute alongside initialization. - * {bytes32} userOpHash - The user operation hash being executed. - * {uint48} validAfter - A timestamp representing from when the user operation is valid. - * {uint48} validUntil - A timestamp representing until when the user operation is valid, or 0 - * to indicated "forever". - * {address} entryPoint - The address of the entry point that will execute the user operation. - */ - bytes32 private constant SAFE_INIT_OP_TYPEHASH = keccak256( - "SafeInitOp(bytes32 userOpHash,uint48 validAfter,uint48 validUntil,address entryPoint)" - ); - - address private immutable SELF; - address public immutable SUPPORTED_ENTRYPOINT; - - constructor(address entryPoint) { - require(entryPoint != address(0), "Invalid entry point"); - - SELF = address(this); - SUPPORTED_ENTRYPOINT = entryPoint; - } - - modifier onlyProxy() { - require(singleton == SELF, "Not called from proxy"); - _; - } - - modifier onlySupportedEntryPoint() { - require(msg.sender == SUPPORTED_ENTRYPOINT, "Unsupported entry point"); - _; - } - - receive() external payable { } - - function preValidationSetup( - bytes32 initHash, - address to, - bytes calldata preInit - ) - external - onlyProxy - { - _setInitHash(initHash); - if (to != address(0)) { - (bool success,) = to.delegatecall(preInit); - require(success, "Pre-initialization failed"); - } - } - - function getInitHash( - address singleton, - address signerFactory, - bytes memory signerData, - address setupTo, - bytes memory setupData, - address fallbackHandler - ) - public - view - returns (bytes32 initHash) - { - initHash = keccak256( - abi.encodePacked( - bytes1(0x19), - bytes1(0x01), - _domainSeparator(), - keccak256( - abi.encode( - SAFE_INIT_TYPEHASH, - singleton, - signerFactory, - keccak256(signerData), - setupTo, - keccak256(setupData), - fallbackHandler - ) - ) - ) - ); - } - - function getOperationHash( - bytes32 userOpHash, - uint48 validAfter, - uint48 validUntil - ) - public - view - returns (bytes32 operationHash) - { - operationHash = keccak256(_getOperationData(userOpHash, validAfter, validUntil)); - } - - function validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash, - uint256 missingAccountFunds - ) - external - override - onlyProxy - onlySupportedEntryPoint - returns (uint256 validationData) - { - address signerFactory; - bytes memory signerData; - { - require( - this.initializeThenUserOp.selector == bytes4(userOp.callData[:4]), - "invalid user operation data" - ); - - address singleton; - address setupTo; - bytes memory setupData; - address fallbackHandler; - (singleton, signerFactory, signerData, setupTo, setupData, fallbackHandler,) = abi - .decode(userOp.callData[4:], (address, address, bytes, address, bytes, address, bytes)); - bytes32 initHash = getInitHash( - singleton, signerFactory, signerData, setupTo, setupData, fallbackHandler - ); - - require(initHash == _initHash(), "invalid init hash"); - } - - validationData = _validateSignatures(userOp, userOpHash, signerFactory, signerData); - if (missingAccountFunds > 0) { - // solhint-disable-next-line no-inline-assembly - assembly ("memory-safe") { - // The `pop` is necessary here because solidity 0.5.0 - // enforces "strict" assembly blocks and "statements (elements of a block) are - // disallowed if they return something onto the stack at the end." - // This is not well documented, the quote is taken from here: - // https://github.com/ethereum/solidity/issues/1820 - // The compiler will throw an error if we keep the success value on the stack - pop(call(gas(), caller(), missingAccountFunds, 0, 0, 0, 0)) - } - } - } - - /** - * @dev Validates that the user operation is correctly signed and returns an ERC-4337 packed - * validation data - * of `validAfter || validUntil || authorizer`: - * - `authorizer`: 20-byte address, 0 for valid signature or 1 to mark signature failure (this - * module does not make use of signature aggregators). - * - `validUntil`: 6-byte timestamp value, or zero for "infinite". The user operation is valid - * only up to this time. - * - `validAfter`: 6-byte timestamp. The user operation is valid only after this time. - * @param userOp User operation struct. - * @return validationData An integer indicating the result of the validation. - */ - function _validateSignatures( - PackedUserOperation calldata userOp, - bytes32 userOpHash, - address signerFactory, - bytes memory signerData - ) - internal - view - returns (uint256 validationData) - { - uint48 validAfter; - uint48 validUntil; - bytes calldata signature; - { - bytes calldata sig = userOp.signature; - validAfter = uint48(bytes6(sig[0:6])); - validUntil = uint48(bytes6(sig[6:12])); - signature = sig[12:]; - } - - bytes memory operationData = _getOperationData(userOpHash, validAfter, validUntil); - bytes32 operationHash = keccak256(operationData); - try IUniqueSignerFactory(signerFactory).isValidSignatureForSigner( - operationHash, signature, signerData - ) returns (bytes4 magicValue) { - // The timestamps are validated by the entry point, therefore we will not check them - // again - validationData = - _packValidationData(magicValue != EIP1271_MAGIC_VALUE, validUntil, validAfter); - } catch { - validationData = _packValidationData(true, validUntil, validAfter); - } - } - - function initializeThenUserOp( - address singleton, - address signerFactory, - bytes calldata signerData, - address setupTo, - bytes calldata setupData, - address fallbackHandler, - bytes memory callData - ) - external - onlySupportedEntryPoint - { - SafeStorage.singleton = singleton; - { - address[] memory owners = new address[](1); - owners[0] = IUniqueSignerFactory(signerFactory).createSigner(signerData); - - SafeSetup(address(this)).setup( - owners, 1, setupTo, setupData, fallbackHandler, address(0), 0, payable(address(0)) - ); - } - - (bool success, bytes memory returnData) = address(this).delegatecall(callData); - if (!success) { - // solhint-disable-next-line no-inline-assembly - assembly ("memory-safe") { - revert(add(returnData, 0x20), mload(returnData)) - } - } - - _setInitHash(0); - } - - function _domainSeparator() internal view returns (bytes32) { - return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, block.chainid, SELF)); - } - - function _getOperationData( - bytes32 userOpHash, - uint48 validAfter, - uint48 validUntil - ) - internal - view - returns (bytes memory operationData) - { - operationData = abi.encodePacked( - bytes1(0x19), - bytes1(0x01), - _domainSeparator(), - keccak256( - abi.encode( - SAFE_INIT_OP_TYPEHASH, userOpHash, validAfter, validUntil, SUPPORTED_ENTRYPOINT - ) - ) - ); - } - - function _initHash() public view returns (bytes32 value) { - // solhint-disable-next-line no-inline-assembly - assembly ("memory-safe") { - value := sload(INIT_HASH_SLOT) - } - } - - function _setInitHash(bytes32 value) internal { - // solhint-disable-next-line no-inline-assembly - assembly ("memory-safe") { - sstore(INIT_HASH_SLOT, value) - } - } - - function _isContract(address account) internal view returns (bool) { - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly ("memory-safe") { - size := extcodesize(account) - } - return size > 0; - } -} - -interface SafeSetup { - function setup( - address[] calldata _owners, - uint256 _threshold, - address to, - bytes calldata data, - address fallbackHandler, - address paymentToken, - uint256 payment, - address payable paymentReceiver - ) - external; -} diff --git a/accounts/safe7579/src/utils/SignerFactory.sol b/accounts/safe7579/src/utils/SignerFactory.sol deleted file mode 100644 index cbf3769d..00000000 --- a/accounts/safe7579/src/utils/SignerFactory.sol +++ /dev/null @@ -1,18 +0,0 @@ -import "./SafeLaunchpad.sol"; - -contract SignerFactory is IUniqueSignerFactory { - function getSigner(bytes memory data) external view override returns (address signer) { } - - function createSigner(bytes memory data) external override returns (address signer) { } - - function isValidSignatureForSigner( - bytes32 message, - bytes calldata signature, - bytes calldata signerData - ) - external - view - override - returns (bytes4 magicValue) - { } -} diff --git a/accounts/safe7579/test/Launchpad.t.sol b/accounts/safe7579/test/Launchpad.t.sol index e719cc8b..a595108e 100644 --- a/accounts/safe7579/test/Launchpad.t.sol +++ b/accounts/safe7579/test/Launchpad.t.sol @@ -6,40 +6,51 @@ import "forge-std/Test.sol"; import "src/SafeERC7579.sol"; import "src/SafeERC7579.sol"; -import "src/utils/SafeLaunchpad.sol"; +import "src/utils/Launchpad.sol"; import "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol"; import "@safe-global/safe-contracts/contracts/Safe.sol"; +import "@rhinestone/modulekit/src/Mocks.sol"; +import "@rhinestone/modulekit/src/test/predeploy/EntryPoint.sol"; + +import { EntryPoint } from "@ERC4337/account-abstraction/contracts/core/EntryPoint.sol"; + +import { LibClone } from "solady/src/utils/LibClone.sol"; contract SafeLaunchPadTest is Test { - SafeSignerLaunchpad safeLaunchpad; SafeERC7579 safe7579; Safe singleton; Safe safeAccount; SafeProxyFactory safeProxyFactory; + Launchpad launchpad; - address entrypoint = address(this); + MockValidator defaultValidator; Account signer1 = makeAccount("signer1"); Account signer2 = makeAccount("signer2"); + IEntryPoint entrypoint; + function setUp() public { singleton = new Safe(); safeProxyFactory = new SafeProxyFactory(); - safeLaunchpad = new SafeSignerLaunchpad(entrypoint); - - bytes memory safeLaunchPadSetup; - // Safe.setup, - // ( - // owners, - // 2, - // address(safeLaunchpad), - // safeLaunchPadSetup, - // address(safe7579), - // address(0), - // 0, - // payable(address(0)) - // ) - // ); + launchpad = new Launchpad(); + safe7579 = new SafeERC7579(); + + entrypoint = etchEntrypoint(); + + defaultValidator = new MockValidator(); + + address[] memory validators = new address[](1); + validators[0] = address(defaultValidator); + bytes[] memory validatorsInitCode = new bytes[](1); + + bytes memory safeLaunchPadSetup = abi.encodeCall( + Launchpad.initSafe7579, + ( + address(safe7579), + abi.encode(validators, validatorsInitCode, new address[](0), new bytes[](0)) + ) + ); address[] memory owners = new address[](2); owners[0] = signer1.addr; @@ -50,7 +61,7 @@ contract SafeLaunchPadTest is Test { ( owners, 2, - address(safeLaunchpad), + address(launchpad), safeLaunchPadSetup, address(safe7579), address(0), @@ -60,31 +71,82 @@ contract SafeLaunchPadTest is Test { ); uint256 salt = 0; - SafeProxy safeProxy = - safeProxyFactory.createProxyWithNonce(address(singleton), initializer, salt); + // SafeProxy safeProxy = + // safeProxyFactory.createProxyWithNonce(address(singleton), initializer, salt); } - /** - * Genereates initcode that will be passed to safeProxyFactory - * @param safeLaunchPadSetup init code for safe launchpad setup() function - */ - function _safeProxyFactory_initcode(bytes memory safeLaunchPadSetup) - internal - returns (bytes memory initCode) - { - // initCode = abi.encodeCall( - // Safe.setup, - // ( - // owners, - // 2, - // address(safeLaunchpad), - // safeLaunchPadSetup, - // address(safe7579), - // address(0), - // 0, - // payable(address(0)) - // ) - // ); + function test_createAccount() public { + address[] memory validators = new address[](1); + validators[0] = address(defaultValidator); + bytes[] memory validatorsInitCode = new bytes[](1); + + bytes memory safeLaunchPadSetup = abi.encodeCall( + Launchpad.initSafe7579, + ( + address(safe7579), + abi.encode(validators, validatorsInitCode, new address[](0), new bytes[](0)) + ) + ); + + address[] memory owners = new address[](2); + owners[0] = signer1.addr; + owners[1] = signer2.addr; + // SETUP SAFE + bytes memory initializer = abi.encodeCall( + Safe.setup, + ( + owners, + 2, + address(launchpad), + safeLaunchPadSetup, + address(safe7579), + address(0), + 0, + payable(address(0)) + ) + ); + uint256 salt = 0; + + PackedUserOperation memory userOp = getDefaultUserOp(); + userOp.initCode = abi.encodePacked( + address(safeProxyFactory), + abi.encodeCall( + SafeProxyFactory.createProxyWithNonce, (address(singleton), initializer, salt) + ) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + entrypoint.handleOps(userOps, payable(address(0x69))); + } + + function getDefaultUserOp() internal returns (PackedUserOperation memory userOp) { + address account = address(0x8bDB7B3070D3cefA1586427ebAfd5FDD56aE96A7); + + console.log("accountPredict", _predictAddress(bytes32(0))); + vm.deal(account, 1 ether); + uint192 key = uint192(bytes24(bytes20(address(defaultValidator)))); + uint256 nonce = entrypoint.getNonce(address(account), key); + userOp = PackedUserOperation({ + sender: account, + nonce: nonce, + 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") + }); + } + + function _predictAddress(bytes32 salt) internal returns (address safeProxy) { + bytes memory deploymentData = abi.encodePacked( + safeProxyFactory.proxyCreationCode(), uint256(uint160(address(singleton))) + ); + bytes32 hash = LibClone.initCodeHash(address(singleton)); + safeProxy = LibClone.predictDeterministicAddress(hash, salt, address(safeProxyFactory)); } function test_foo() public { }