diff --git a/contracts/contracts/ERC4337Plugin.sol b/contracts/contracts/ERC4337Plugin.sol index 7133aa6..cbeb61e 100644 --- a/contracts/contracts/ERC4337Plugin.sol +++ b/contracts/contracts/ERC4337Plugin.sol @@ -33,6 +33,8 @@ interface ISafe { function enablePlugin(address plugin, uint8 permissions) external; function setFunctionHandler(bytes4 selector, address functionHandler) external; + + function checkSignatures(bytes32 dataHash, bytes memory, bytes memory signatures) external; } struct UserOperation { @@ -60,6 +62,10 @@ contract ERC4337Plugin is ISafeProtocolFunctionHandler, BasePluginWithEventMetad ISafeProtocolManager public immutable SAFE_PROTOCOL_MANAGER; address payable public immutable ENTRY_POINT; + //return value in case of signature failure, with no time-range. + // equivalent to _packValidationData(true,0,0); + uint256 internal constant SIG_VALIDATION_FAILED = 1; + constructor( ISafeProtocolManager safeCoreProtocolManager, address payable entryPoint @@ -71,26 +77,32 @@ contract ERC4337Plugin is ISafeProtocolFunctionHandler, BasePluginWithEventMetad function validateUserOp(UserOperation calldata userOp, bytes32, uint256 missingAccountFunds) external returns (uint256 validationData) { require(msg.sender == address(PLUGIN_ADDRESS)); + address payable safeAddress = payable(userOp.sender); ISafe senderSafe = ISafe(safeAddress); + senderSafe.checkSignatures(getUserOpHash(userOp), "", userOp.signature); if (missingAccountFunds != 0) { - senderSafe.execTransactionFromModule(ENTRY_POINT, missingAccountFunds, "", 0); + SAFE_PROTOCOL_MANAGER.executeTransaction( + userOp.sender, + SafeTransaction({to: ENTRY_POINT, value: missingAccountFunds, data: ""}) + ); } return 0; } - function execTransaction(address payable to, uint256 value, bytes calldata data) external { + function execTransaction(address safe, address payable to, uint256 value, bytes calldata data) external { require(msg.sender == address(PLUGIN_ADDRESS)); address payable safeAddress = payable(msg.sender); ISafe safe = ISafe(safeAddress); - require(safe.execTransactionFromModule(to, value, data, 0), "tx failed"); + require(SAFE_PROTOCOL_MANAGER.executeTransaction(safe, SafeTransaction({to: to, value: value, data: data}))); } function handle(address safe, address sender, uint256 value, bytes calldata data) external returns (bytes memory result) { bytes4 selector = bytes4(data[0:4]); + requireFromEntryPoint(sender); if (selector == this.validateUserOp.selector) { (, result) = PLUGIN_ADDRESS.call(data); @@ -124,6 +136,26 @@ contract ERC4337Plugin is ISafeProtocolFunctionHandler, BasePluginWithEventMetad location = abi.encode(address(this)); } + function packUserOperation(UserOperation calldata userOp) internal pure returns (bytes memory ret) { + //lighter signature scheme. must match UserOp.ts#packUserOp + bytes calldata sig = userOp.signature; + // copy directly the userOp from calldata up to (but not including) the signature. + // this encoding depends on the ABI encoding of calldata, but is much lighter to copy + // than referencing each field separately. + assembly { + let ofs := userOp + let len := sub(sub(sig.offset, ofs), 32) + ret := mload(0x40) + mstore(0x40, add(ret, add(len, 32))) + mstore(ret, len) + calldatacopy(add(ret, 32), ofs, len) + } + } + + function getUserOpHash(UserOperation calldata userOp) internal pure returns (bytes32) { + return keccak256(packUserOperation(userOp)); + } + function supportsInterface(bytes4 interfaceId) external pure override(BasePlugin, IERC165) returns (bool) { return interfaceId == type(ISafeProtocolPlugin).interfaceId || diff --git a/contracts/test/ERC4337Plugin.spec.ts b/contracts/test/ERC4337Plugin.spec.ts index 0887d26..7ec69a8 100644 --- a/contracts/test/ERC4337Plugin.spec.ts +++ b/contracts/test/ERC4337Plugin.spec.ts @@ -28,26 +28,28 @@ const MNEMONIC = process.env.ERC4337_TEST_MNEMONIC; const randomAddress = "0x1234567890123456789012345678901234567890"; describe("ERC4337 Plugin", () => { - const setup = deployments.createFixture(async ({ deployments }) => { - const signers = await hre.ethers.getSigners(); + const hardhatNetworkSetup = deployments.createFixture(async ({ deployments }) => { await deployments.fixture(); + const signers = await hre.ethers.getSigners(); const manager = await ethers.getContractAt("MockContract", await getProtocolManagerAddress(hre)); - const testRegistryFactory = await ethers.getContractFactory("TestSafeProtocolRegistryUnrestricted"); - const testRegistryDeployment = await testRegistryFactory.deploy(signers[0].address); - const safeProtocolManager = await ( - await ethers.getContractFactory("SafeProtocolManager") - ).deploy(signers[0].address, testRegistryDeployment.getAddress()); - const erc4337Plugin = await ( - await ethers.getContractFactory("ERC4337Plugin") - ).deploy(safeProtocolManager.getAddress(), randomAddress); + const testRegistryDeployment = await ethers + .getContractFactory("TestSafeProtocolRegistryUnrestricted") + .then((factory) => factory.deploy(signers[0].address)); + const safeProtocolManager = await ethers + .getContractFactory("SafeProtocolManager") + .then((factory) => factory.deploy(signers[0].address, testRegistryDeployment.getAddress())); + const erc4337Plugin = await ethers + .getContractFactory("ERC4337Plugin") + .then((factory) => factory.deploy(safeProtocolManager.getAddress(), randomAddress)); + await testRegistryDeployment .addModule(erc4337Plugin.getAddress(), ModuleType.Plugin + ModuleType.FunctionHandler) .then((tx) => tx.wait(1)); - const account = await (await ethers.getContractFactory("ExecutableMockContract")).deploy(); - const proxyFactory = await (await (await getSafeProxyFactoryContractFactory()).deploy()).waitForDeployment(); - const safeSingleton = await (await (await getSafeSingletonContractFactory()).deploy()).waitForDeployment(); + const account = await ethers.getContractFactory("ExecutableMockContract").then((f) => f.deploy()); + const proxyFactory = await getSafeProxyFactoryContractFactory().then((f) => f.deploy()); + const safeSingleton = await getSafeSingletonContractFactory().then((f) => f.deploy()); const callData = erc4337Plugin.interface.encodeFunctionData("enableSafeCoreProtocolWith4337Plugin"); const safe = await deploySafe(proxyFactory, safeSingleton, [signers[0].address], 1, await erc4337Plugin.getAddress(), callData); @@ -62,14 +64,14 @@ describe("ERC4337 Plugin", () => { }); it("should be initialized correctly", async () => { - const { erc4337Plugin } = await setup(); + const { erc4337Plugin } = await hardhatNetworkSetup(); expect(await erc4337Plugin.name()).to.be.eq("ERC4337 Plugin"); expect(await erc4337Plugin.version()).to.be.eq("1.0.0"); expect(await erc4337Plugin.permissions()).to.be.eq(1); }); - it.only("can retrieve metadata for the module", async () => { - const { erc4337Plugin } = await setup(); + it("can retrieve metadata for the module", async () => { + const { erc4337Plugin } = await hardhatNetworkSetup(); expect(await loadPluginMetadata(hre, erc4337Plugin)).to.be.deep.eq({ name: "ERC4337 Plugin", version: "1.0.0", @@ -79,10 +81,18 @@ describe("ERC4337 Plugin", () => { }); }); - it("can deploy a safe with manager and ERC4337 module", async () => { - const { erc4337Plugin, manager, signers, safe } = await setup(); + it("can validate a signed user operation and send the prefund", async () => { + const { erc4337Plugin, manager, signers, safe } = await hardhatNetworkSetup(); }); + it("rejects a signed user operation if the signature is invalid", async () => {}); + + it("can execute a transaction coming from the entrypoint", async () => {}); + + it("rejects validation requests coming from an address that is not the entrypoint", async () => {}); + + it("rejects execution requests coming from an address that is not the entrypoint", async () => {}); + /** * This test verifies the ERC4337 based on gas estimation for a user operation * The user operation deploys a Safe with the ERC4337 module and a handler