From df1a0e283e9ad1561c181891eb3514319d2a1f39 Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Wed, 8 Nov 2023 14:20:17 +0000 Subject: [PATCH] Added Sample 4337 Module --- .../erc4337/SafeProtocol4337Module.sol | 139 ++++++++++++++++++ .../modules/erc4337/interfaces/IERC4337.sol | 6 + .../interfaces/ISafeProtocol4337Handler.sol | 8 + package.json | 1 + .../erc4337/SafeProtocol4337Plugin.spec.ts | 84 +++++++++++ yarn.lock | 5 + 6 files changed, 243 insertions(+) create mode 100644 contracts/modules/erc4337/SafeProtocol4337Module.sol create mode 100644 contracts/modules/erc4337/interfaces/IERC4337.sol create mode 100644 contracts/modules/erc4337/interfaces/ISafeProtocol4337Handler.sol create mode 100644 test/modules/plugins/erc4337/SafeProtocol4337Plugin.spec.ts diff --git a/contracts/modules/erc4337/SafeProtocol4337Module.sol b/contracts/modules/erc4337/SafeProtocol4337Module.sol new file mode 100644 index 00000000..5a3d36bd --- /dev/null +++ b/contracts/modules/erc4337/SafeProtocol4337Module.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.18; + +import {SafeProtocolAction, SafeTransaction} from "../../DataTypes.sol"; +import {PLUGIN_PERMISSION_EXECUTE_CALL} from "../../common/Constants.sol"; +import {IAccount} from "../../interfaces/Accounts.sol"; +import {ISafeProtocolManager} from "../../interfaces/Manager.sol"; +import {IERC165, ISafeProtocolFunctionHandler, ISafeProtocolPlugin} from "../../interfaces/Modules.sol"; +import {UserOperation} from "./interfaces/IERC4337.sol"; +import {ISafeProtocol4337Handler} from "./interfaces/ISafeProtocol4337Handler.sol"; + +contract SafeProtocol4337Module is ISafeProtocolFunctionHandler, ISafeProtocolPlugin { + uint256 private constant VALIDATION_SIG_SUCCESS = 0; + uint256 private constant VALIDATION_SIG_FAILURE = 0; + + address payable public immutable entrypoint; + + constructor(address payable _entrypoint) { + require(_entrypoint != address(0), "invalid entrypoint address"); + entrypoint = _entrypoint; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return + interfaceId == type(IERC165).interfaceId || + interfaceId == type(ISafeProtocolFunctionHandler).interfaceId || + interfaceId == type(ISafeProtocolPlugin).interfaceId; + } + + /** + * @inheritdoc ISafeProtocolFunctionHandler + */ + function handle(address account, address sender, uint256 value, bytes calldata data) external override returns (bytes memory result) { + require(sender == entrypoint, "unsupported entrypoint"); + require(value == 0, "not payable"); + + ISafeProtocolManager manager = ISafeProtocolManager(msg.sender); + bytes4 selector = bytes4(data[:4]); + if (selector == ISafeProtocol4337Handler(account).validateUserOp.selector) { + (UserOperation memory userOp, bytes32 userOpHash, uint256 missingAccountFunds) = abi.decode( + data[4:], + (UserOperation, bytes32, uint256) + ); + uint256 validationData = _validateUserOp(manager, account, userOp, userOpHash, missingAccountFunds); + result = abi.encode(validationData); + } else if (selector == ISafeProtocol4337Handler(account).executeUserOp.selector) { + (address to, uint256 opValue, bytes memory opData) = abi.decode(data[4:], (address, uint256, bytes)); + _executeUserOp(manager, account, to, opValue, opData); + } + } + + /** + * Validate account operation. + * @param manager the protocol manager. + * @param account the account. + * @param userOp the operation that is about to be executed. + * @param missingAccountFunds missing funds on the account's deposit in the entrypoint. + * @return validationData packaged validation data. + */ + function _validateUserOp( + ISafeProtocolManager manager, + address account, + UserOperation memory userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) internal returns (uint256 validationData) { + require(bytes4(userOp.callData) == ISafeProtocol4337Handler(account).executeUserOp.selector, "unsupported execution"); + + if (missingAccountFunds > 0) { + SafeTransaction memory transaction; + { + transaction.actions = new SafeProtocolAction[](1); + transaction.actions[0].to = entrypoint; + transaction.actions[0].value = missingAccountFunds; + transaction.nonce = userOp.nonce; + } + manager.executeTransaction(account, transaction); + } + + try IAccount(account).checkSignatures(userOpHash, "", userOp.signature) { + validationData = VALIDATION_SIG_SUCCESS; + } catch { + validationData = VALIDATION_SIG_FAILURE; + } + } + + /** + * Executes a account operation. + * @param manager the protocol manager. + * @param account the account. + * @param to target of the operation. + * @param value value of the operation. + * @param data calldata for the operation. + */ + function _executeUserOp(ISafeProtocolManager manager, address account, address to, uint256 value, bytes memory data) internal { + SafeTransaction memory transaction; + { + transaction.actions = new SafeProtocolAction[](1); + transaction.actions[0].to = payable(to); + transaction.actions[0].value = value; + transaction.actions[0].data = data; + } + manager.executeTransaction(account, transaction); + } + + /** + * @inheritdoc ISafeProtocolPlugin + */ + function metadataProvider() + external + view + override(ISafeProtocolFunctionHandler, ISafeProtocolPlugin) + returns (uint256 providerType, bytes memory location) + {} + + /** + * @inheritdoc ISafeProtocolPlugin + */ + function name() external pure override returns (string memory) { + return "Safe Protocol ERC-4337 Plugin"; + } + + /** + * @inheritdoc ISafeProtocolPlugin + */ + function version() external pure override returns (string memory) { + return "1"; + } + + /** + * @inheritdoc ISafeProtocolPlugin + */ + function requiresPermissions() external pure override returns (uint8 permissions) { + permissions = PLUGIN_PERMISSION_EXECUTE_CALL; + } +} diff --git a/contracts/modules/erc4337/interfaces/IERC4337.sol b/contracts/modules/erc4337/interfaces/IERC4337.sol new file mode 100644 index 00000000..bad8f453 --- /dev/null +++ b/contracts/modules/erc4337/interfaces/IERC4337.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.18; + +import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; diff --git a/contracts/modules/erc4337/interfaces/ISafeProtocol4337Handler.sol b/contracts/modules/erc4337/interfaces/ISafeProtocol4337Handler.sol new file mode 100644 index 00000000..a22152ee --- /dev/null +++ b/contracts/modules/erc4337/interfaces/ISafeProtocol4337Handler.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.18; + +import {IAccount} from "./IERC4337.sol"; + +interface ISafeProtocol4337Handler is IAccount { + function executeUserOp(address to, uint256 value, bytes calldata data) external; +} diff --git a/package.json b/package.json index d8c36cea..3e001122 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "build:ts": "yarn rimraf dist && tsc -p tsconfig.prod.json" }, "devDependencies": { + "@account-abstraction/contracts": "^0.6.0", "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", "@nomicfoundation/hardhat-ethers": "^3.0.0", "@nomicfoundation/hardhat-network-helpers": "^1.0.0", diff --git a/test/modules/plugins/erc4337/SafeProtocol4337Plugin.spec.ts b/test/modules/plugins/erc4337/SafeProtocol4337Plugin.spec.ts new file mode 100644 index 00000000..a58c2497 --- /dev/null +++ b/test/modules/plugins/erc4337/SafeProtocol4337Plugin.spec.ts @@ -0,0 +1,84 @@ +import EntryPoint from "@account-abstraction/contracts/artifacts/EntryPoint.json"; +import { Signer } from "ethers"; +import { ethers, deployments } from "hardhat"; + +import { MODULE_TYPE_PLUGIN, MODULE_TYPE_FUNCTION_HANDLER, PLUGIN_PERMISSION_EXECUTE_CALL } from "../../../../src/utils/constants"; + +describe("SafeProtocol4337Module", async () => { + const setupTests = deployments.createFixture(async ({ deployments }) => { + await deployments.fixture(); + const [deployer, owner, user, bundler] = await ethers.getSigners(); + + const registry = await ethers.deployContract("SafeProtocolRegistry", [owner.address]); + const manager = await ethers.deployContract("SafeProtocolManager", [owner.address, await registry.getAddress()]); + + const entrypoint = await deployEntryPoint(deployer); + const module = await ethers.deployContract("SafeProtocol4337Module", [await entrypoint.getAddress()]); + const handler = await ethers.getContractAt("ISafeProtocol4337Handler", await module.getAddress()); + + const account = await ethers.deployContract("TestExecutor", [await manager.getAddress()]); + await account.setModule(await manager.getAddress()); + + await registry.connect(owner).addModule(await module.getAddress(), MODULE_TYPE_PLUGIN | MODULE_TYPE_FUNCTION_HANDLER); + await account.exec( + await account.getAddress(), + 0, + manager.interface.encodeFunctionData("enablePlugin", [await module.getAddress(), PLUGIN_PERMISSION_EXECUTE_CALL]), + ); + await account.exec( + await account.getAddress(), + 0, + manager.interface.encodeFunctionData("setFunctionHandler", [ + handler.validateUserOp.fragment.selector, + await handler.getAddress(), + ]), + ); + await account.exec( + await account.getAddress(), + 0, + manager.interface.encodeFunctionData("setFunctionHandler", [ + handler.executeUserOp.fragment.selector, + await handler.getAddress(), + ]), + ); + + return { account, entrypoint, handler, user, bundler }; + }); + + describe("handleOps", () => { + it("should validate and execute user operations", async () => { + const { account, entrypoint, handler, user, bundler } = await setupTests(); + + await user.sendTransaction({ + to: await account.getAddress(), + value: ethers.parseEther("1.0"), + }); + + const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData(); + await entrypoint.connect(bundler).handleOps( + [ + { + sender: await account.getAddress(), + nonce: await entrypoint.getNonce(await account.getAddress(), 0), + initCode: "0x", + callData: handler.interface.encodeFunctionData("executeUserOp", [ethers.ZeroAddress, 0, "0x"]), + callGasLimit: 100000, + verificationGasLimit: 200000, + preVerificationGas: 100000, + maxFeePerGas: maxFeePerGas ?? 0, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? 0, + paymasterAndData: "0x", + signature: "0x", + }, + ], + bundler.address, + ); + }); + }); +}); + +async function deployEntryPoint(deployer: Signer) { + const { abi, bytecode } = EntryPoint; + const contract = await new ethers.ContractFactory(abi, bytecode, deployer).deploy(); + return await ethers.getContractAt("IEntryPoint", await contract.getAddress()); +} diff --git a/yarn.lock b/yarn.lock index 998ba3a1..f1455d26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@account-abstraction/contracts@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@account-abstraction/contracts/-/contracts-0.6.0.tgz#7188a01839999226e6b2796328af338329543b76" + integrity sha512-8ooRJuR7XzohMDM4MV34I12Ci2bmxfE9+cixakRL7lA4BAwJKQ3ahvd8FbJa9kiwkUPCUNtj+/zxDQWYYalLMQ== + "@adraffy/ens-normalize@1.9.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz#60111a5d9db45b2e5cbb6231b0bb8d97e8659316"