diff --git a/contracts/account/Account.sol b/contracts/account/Account.sol index 0695d12..8a54910 100644 --- a/contracts/account/Account.sol +++ b/contracts/account/Account.sol @@ -5,9 +5,8 @@ pragma solidity ^0.8.20; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {ERC7739Signer} from "../utils/cryptography/ERC7739Signer.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol"; import {AccountCore} from "./AccountCore.sol"; +import {AccountERC7821} from "./extensions/AccountERC7821.sol"; /** * @dev Extension of {AccountCore} with recommended feature that most account abstraction implementation will want: @@ -18,10 +17,4 @@ import {AccountCore} from "./AccountCore.sol"; * NOTE: To use this contract, the {ERC7739Signer-_rawSignatureValidation} function must be * implemented using a specific signature verification algorithm. See {SignerECDSA}, {SignerP256} or {SignerRSA}. */ -abstract contract Account is AccountCore, Multicall, ERC721Holder, ERC1155Holder, ERC7739Signer { - function execute(address target, uint256 value, bytes calldata data) public virtual onlyEntryPointOrSelf { - // we cannot use `Address.functionCallWithValue` here as it would revert on EOA targets - (bool success, bytes memory returndata) = target.call{value: value}(data); - Address.verifyCallResult(success, returndata); - } -} +abstract contract Account is AccountCore, AccountERC7821, ERC721Holder, ERC1155Holder, ERC7739Signer {} diff --git a/contracts/account/AccountCore.sol b/contracts/account/AccountCore.sol index 8bab198..539fdc3 100644 --- a/contracts/account/AccountCore.sol +++ b/contracts/account/AccountCore.sol @@ -91,14 +91,7 @@ abstract contract AccountCore is AbstractSigner, EIP712, IAccount, IAccountExecu PackedUserOperation calldata userOp, bytes32 /*userOpHash*/ ) public virtual onlyEntryPointOrSelf { - // decode packed calldata - address target = address(bytes20(userOp.callData[4:24])); - uint256 value = uint256(bytes32(userOp.callData[24:56])); - bytes calldata data = userOp.callData[56:]; - - // we cannot use `Address.functionCallWithValue` here as it would revert on EOA targets - (bool success, bytes memory returndata) = target.call{value: value}(data); - Address.verifyCallResult(success, returndata); + Address.functionDelegateCall(address(this), userOp.callData[4:]); } /** diff --git a/contracts/account/extensions/AccountERC7821.sol b/contracts/account/extensions/AccountERC7821.sol new file mode 100644 index 0000000..5387fd4 --- /dev/null +++ b/contracts/account/extensions/AccountERC7821.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC7579Utils, Mode, CallType, ExecType, ModeSelector} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol"; +import {IERC7821} from "../../interfaces/IERC7821.sol"; +import {AccountCore} from "../AccountCore.sol"; + +/** + * @dev Minimal batch executor following ERC7821. Only supports basic mode (no optional "opData"). + */ +abstract contract AccountERC7821 is AccountCore, IERC7821 { + using ERC7579Utils for *; + + ModeSelector internal constant MODE_BASIC = ModeSelector.wrap(0x00000000); + ModeSelector internal constant MODE_OPDATA = ModeSelector.wrap(0x78210001); // Not supported yet + + error UnsupportedExecutionMode(); + + /// @inheritdoc IERC7821 + function execute(bytes32 mode, bytes calldata executionData) public payable virtual onlyEntryPointOrSelf { + require(supportsExecutionMode(mode), UnsupportedExecutionMode()); + executionData.execBatch(ERC7579Utils.EXECTYPE_DEFAULT); + } + + /// @inheritdoc IERC7821 + function supportsExecutionMode(bytes32 mode) public view virtual returns (bool result) { + (CallType callType, ExecType execType, ModeSelector modeSelector, ) = Mode.wrap(mode).decodeMode(); + return + callType == ERC7579Utils.CALLTYPE_BATCH && + execType == ERC7579Utils.EXECTYPE_DEFAULT && + modeSelector == MODE_BASIC; + } +} diff --git a/contracts/interfaces/IERC7821.sol b/contracts/interfaces/IERC7821.sol new file mode 100644 index 0000000..1607caa --- /dev/null +++ b/contracts/interfaces/IERC7821.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface for minimal batch executor. + */ +interface IERC7821 { + /** + * @dev Executes the calls in `executionData`. + * Reverts and bubbles up error if any call fails. + * + * `executionData` encoding: + * - If `opData` is empty, `executionData` is simply `abi.encode(calls)`. + * - Else, `executionData` is `abi.encode(calls, opData)`. + * See: https://eips.ethereum.org/EIPS/eip-7579 + * + * Supported modes: + * - `bytes32(0x01000000000000000000...)`: does not support optional `opData`. + * - `bytes32(0x01000000000078210001...)`: supports optional `opData`. + * + * Authorization checks: + * - If `opData` is empty, the implementation SHOULD require that + * `msg.sender == address(this)`. + * - If `opData` is not empty, the implementation SHOULD use the signature + * encoded in `opData` to determine if the caller can perform the execution. + * + * `opData` may be used to store additional data for authentication, + * paymaster data, gas limits, etc. + */ + function execute(bytes32 mode, bytes calldata executionData) external payable; + + /** + * @dev This function is provided for frontends to detect support. + * Only returns true for: + * - `bytes32(0x01000000000000000000...)`: does not support optional `opData`. + * - `bytes32(0x01000000000078210001...)`: supports optional `opData`. + */ + function supportsExecutionMode(bytes32 mode) external view returns (bool); +} diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index eaab8c3..d48b15b 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -4,6 +4,7 @@ const { setBalance } = require('@nomicfoundation/hardhat-network-helpers'); const { impersonate } = require('@openzeppelin/contracts/test/helpers/account'); const { SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILURE } = require('@openzeppelin/contracts/test/helpers/erc4337'); +const { CALL_TYPE_BATCH, encodeMode, encodeBatch } = require('@openzeppelin/contracts/test/helpers/erc7579'); const { shouldSupportInterfaces, } = require('@openzeppelin/contracts/test/utils/introspection/SupportsInterface.behavior'); @@ -157,24 +158,16 @@ function shouldBehaveLikeAccountExecutor({ deployable = true } = {}) { // account is not initially deployed expect(ethers.provider.getCode(this.mock)).to.eventually.equal('0x'); - this.encodeUserOpCalldata = (to, value, calldata) => - ethers.concat([ - this.mock.interface.getFunction('executeUserOp').selector, - ethers.solidityPacked( - ['address', 'uint256', 'bytes'], - [to.target ?? to.address ?? to, value ?? 0, calldata ?? '0x'], - ), + this.encodeUserOpCalldata = (target, value, data) => + this.mock.interface.encodeFunctionData('execute', [ + encodeMode({ callType: CALL_TYPE_BATCH }), + encodeBatch({ target, value, data }), ]); this.encodeUserOpCalldataBatch = (...calls) => - this.mock.interface.encodeFunctionData('multicall', [ - calls.map(({ to, value, calldata }) => - this.mock.interface.encodeFunctionData('execute', [ - to.target ?? to.address ?? to, - value ?? 0, - calldata ?? '0x', - ]), - ), + this.mock.interface.encodeFunctionData('execute', [ + encodeMode({ callType: CALL_TYPE_BATCH }), + encodeBatch(...calls), ]); }); @@ -280,11 +273,11 @@ function shouldBehaveLikeAccountExecutor({ deployable = true } = {}) { const operation = await this.mock .createUserOp({ callData: this.encodeUserOpCalldataBatch( - { to: this.other, value: value1 }, + { target: this.other, value: value1 }, { - to: this.target, + target: this.target, value: value2, - calldata: this.target.interface.encodeFunctionData('mockFunctionExtra'), + data: this.target.interface.encodeFunctionData('mockFunctionExtra'), }, ), })