Skip to content

Commit

Permalink
implement ERC7821
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed Dec 21, 2024
1 parent 42906dc commit 2d1f99e
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 35 deletions.
11 changes: 2 additions & 9 deletions contracts/account/Account.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 {}
9 changes: 1 addition & 8 deletions contracts/account/AccountCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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:]);
}

/**
Expand Down
34 changes: 34 additions & 0 deletions contracts/account/extensions/AccountERC7821.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
40 changes: 40 additions & 0 deletions contracts/interfaces/IERC7821.sol
Original file line number Diff line number Diff line change
@@ -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);
}
29 changes: 11 additions & 18 deletions test/account/Account.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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),
]);
});

Expand Down Expand Up @@ -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'),
},
),
})
Expand Down

0 comments on commit 2d1f99e

Please sign in to comment.