Add Account framework (#5657)
This commit is contained in:
5
.changeset/clean-ways-push.md
Normal file
5
.changeset/clean-ways-push.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': minor
|
||||
---
|
||||
|
||||
`AccountERC7579`: Extension of `Account` that implements support for ERC-7579 modules of type executor, validator, and fallback handler.
|
||||
5
.changeset/funny-years-yawn.md
Normal file
5
.changeset/funny-years-yawn.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': minor
|
||||
---
|
||||
|
||||
`Account`: Added a simple ERC-4337 account implementation with minimal logic to process user operations.
|
||||
5
.changeset/lazy-poets-cheer.md
Normal file
5
.changeset/lazy-poets-cheer.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': minor
|
||||
---
|
||||
|
||||
`SignerERC7702`: Implementation of `AbstractSigner` for Externally Owned Accounts (EOAs). Useful with ERC-7702.
|
||||
5
.changeset/rotten-apes-lie.md
Normal file
5
.changeset/rotten-apes-lie.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': minor
|
||||
---
|
||||
|
||||
`IERC7821`, `ERC7821`: Interface and logic for minimal batch execution. No support for additional `opData` is included.
|
||||
5
.changeset/strong-points-change.md
Normal file
5
.changeset/strong-points-change.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': minor
|
||||
---
|
||||
|
||||
`AccountERC7579Hooked`: Extension of `AccountERC7579` that implements support for ERC-7579 hook modules.
|
||||
5
.changeset/tame-bears-mix.md
Normal file
5
.changeset/tame-bears-mix.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': minor
|
||||
---
|
||||
|
||||
`AbstractSigner`, `SignerECDSA`, `SignerP256`, and `SignerRSA`: Add an abstract contract and various implementations for contracts that deal with signature verification.
|
||||
144
contracts/account/Account.sol
Normal file
144
contracts/account/Account.sol
Normal file
@ -0,0 +1,144 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {PackedUserOperation, IAccount, IEntryPoint} from "../interfaces/draft-IERC4337.sol";
|
||||
import {ERC4337Utils} from "./utils/draft-ERC4337Utils.sol";
|
||||
import {AbstractSigner} from "../utils/cryptography/AbstractSigner.sol";
|
||||
|
||||
/**
|
||||
* @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process
|
||||
* user operations.
|
||||
*
|
||||
* Developers must implement the {AbstractSigner-_rawSignatureValidation} function to define the account's validation logic.
|
||||
*
|
||||
* NOTE: This core account doesn't include any mechanism for performing arbitrary external calls. This is an essential
|
||||
* feature that all Account should have. We leave it up to the developers to implement the mechanism of their choice.
|
||||
* Common choices include ERC-6900, ERC-7579 and ERC-7821 (among others).
|
||||
*
|
||||
* IMPORTANT: Implementing a mechanism to validate signatures is a security-sensitive operation as it may allow an
|
||||
* attacker to bypass the account's security measures. Check out {SignerECDSA}, {SignerP256}, or {SignerRSA} for
|
||||
* digital signature validation implementations.
|
||||
*
|
||||
* @custom:stateless
|
||||
*/
|
||||
abstract contract Account is AbstractSigner, IAccount {
|
||||
/**
|
||||
* @dev Unauthorized call to the account.
|
||||
*/
|
||||
error AccountUnauthorized(address sender);
|
||||
|
||||
/**
|
||||
* @dev Revert if the caller is not the entry point or the account itself.
|
||||
*/
|
||||
modifier onlyEntryPointOrSelf() {
|
||||
_checkEntryPointOrSelf();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Revert if the caller is not the entry point.
|
||||
*/
|
||||
modifier onlyEntryPoint() {
|
||||
_checkEntryPoint();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Canonical entry point for the account that forwards and validates user operations.
|
||||
*/
|
||||
function entryPoint() public view virtual returns (IEntryPoint) {
|
||||
return ERC4337Utils.ENTRYPOINT_V08;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Return the account nonce for the canonical sequence.
|
||||
*/
|
||||
function getNonce() public view virtual returns (uint256) {
|
||||
return getNonce(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Return the account nonce for a given sequence (key).
|
||||
*/
|
||||
function getNonce(uint192 key) public view virtual returns (uint256) {
|
||||
return entryPoint().getNonce(address(this), key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc IAccount
|
||||
*/
|
||||
function validateUserOp(
|
||||
PackedUserOperation calldata userOp,
|
||||
bytes32 userOpHash,
|
||||
uint256 missingAccountFunds
|
||||
) public virtual onlyEntryPoint returns (uint256) {
|
||||
uint256 validationData = _validateUserOp(userOp, userOpHash);
|
||||
_payPrefund(missingAccountFunds);
|
||||
return validationData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the validationData for a given user operation. By default, this checks the signature of the
|
||||
* signable hash (produced by {_signableUserOpHash}) using the abstract signer ({AbstractSigner-_rawSignatureValidation}).
|
||||
*
|
||||
* NOTE: The userOpHash is assumed to be correct. Calling this function with a userOpHash that does not match the
|
||||
* userOp will result in undefined behavior.
|
||||
*/
|
||||
function _validateUserOp(
|
||||
PackedUserOperation calldata userOp,
|
||||
bytes32 userOpHash
|
||||
) internal virtual returns (uint256) {
|
||||
return
|
||||
_rawSignatureValidation(_signableUserOpHash(userOp, userOpHash), userOp.signature)
|
||||
? ERC4337Utils.SIG_VALIDATION_SUCCESS
|
||||
: ERC4337Utils.SIG_VALIDATION_FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Virtual function that returns the signable hash for a user operations. Since v0.8.0 of the entrypoint,
|
||||
* `userOpHash` is an EIP-712 hash that can be signed directly.
|
||||
*/
|
||||
function _signableUserOpHash(
|
||||
PackedUserOperation calldata /*userOp*/,
|
||||
bytes32 userOpHash
|
||||
) internal view virtual returns (bytes32) {
|
||||
return userOpHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Sends the missing funds for executing the user operation to the {entrypoint}.
|
||||
* The `missingAccountFunds` must be defined by the entrypoint when calling {validateUserOp}.
|
||||
*/
|
||||
function _payPrefund(uint256 missingAccountFunds) internal virtual {
|
||||
if (missingAccountFunds > 0) {
|
||||
(bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
|
||||
success; // Silence warning. The entrypoint should validate the result.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Ensures the caller is the {entrypoint}.
|
||||
*/
|
||||
function _checkEntryPoint() internal view virtual {
|
||||
address sender = msg.sender;
|
||||
if (sender != address(entryPoint())) {
|
||||
revert AccountUnauthorized(sender);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Ensures the caller is the {entrypoint} or the account itself.
|
||||
*/
|
||||
function _checkEntryPointOrSelf() internal view virtual {
|
||||
address sender = msg.sender;
|
||||
if (sender != address(this) && sender != address(entryPoint())) {
|
||||
revert AccountUnauthorized(sender);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Receive Ether.
|
||||
*/
|
||||
receive() external payable virtual {}
|
||||
}
|
||||
@ -1,9 +1,27 @@
|
||||
= Account
|
||||
|
||||
[.readme-notice]
|
||||
NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/account
|
||||
|
||||
This directory includes contracts to build accounts for ERC-4337.
|
||||
This directory includes contracts to build accounts for ERC-4337. These include:
|
||||
|
||||
* {Account}: An ERC-4337 smart account implementation that includes the core logic to process user operations.
|
||||
* {AccountERC7579}: An extension of `Account` that implements support for ERC-7579 modules.
|
||||
* {AccountERC7579Hooked}: An extension of `AccountERC7579` with support for a single hook module (type 4).
|
||||
* {ERC7821}: Minimal batch executor implementation contracts. Useful to enable easy batch execution for smart contracts.
|
||||
* {ERC4337Utils}: Utility functions for working with ERC-4337 user operations.
|
||||
* {ERC7579Utils}: Utility functions for working with ERC-7579 modules and account modularity.
|
||||
|
||||
== Core
|
||||
|
||||
{{Account}}
|
||||
|
||||
== Extensions
|
||||
|
||||
{{AccountERC7579}}
|
||||
|
||||
{{AccountERC7579Hooked}}
|
||||
|
||||
{{ERC7821}}
|
||||
|
||||
== Utilities
|
||||
|
||||
|
||||
404
contracts/account/extensions/AccountERC7579.sol
Normal file
404
contracts/account/extensions/AccountERC7579.sol
Normal file
@ -0,0 +1,404 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.27;
|
||||
|
||||
import {PackedUserOperation} from "../../interfaces/draft-IERC4337.sol";
|
||||
import {IERC1271} from "../../interfaces/IERC1271.sol";
|
||||
import {IERC7579Module, IERC7579Validator, IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig, MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_TYPE_FALLBACK} from "../../interfaces/draft-IERC7579.sol";
|
||||
import {ERC7579Utils, Mode, CallType, ExecType} from "../../account/utils/draft-ERC7579Utils.sol";
|
||||
import {EnumerableSet} from "../../utils/structs/EnumerableSet.sol";
|
||||
import {Bytes} from "../../utils/Bytes.sol";
|
||||
import {Packing} from "../../utils/Packing.sol";
|
||||
import {Address} from "../../utils/Address.sol";
|
||||
import {Calldata} from "../../utils/Calldata.sol";
|
||||
import {Account} from "../Account.sol";
|
||||
|
||||
/**
|
||||
* @dev Extension of {Account} that implements support for ERC-7579 modules.
|
||||
*
|
||||
* To comply with the ERC-1271 support requirement, this contract defers signature validation to
|
||||
* installed validator modules by calling {IERC7579Validator-isValidSignatureWithSender}.
|
||||
*
|
||||
* This contract does not implement validation logic for user operations since this functionality
|
||||
* is often delegated to self-contained validation modules. Developers must install a validator module
|
||||
* upon initialization (or any other mechanism to enable execution from the account):
|
||||
*
|
||||
* ```solidity
|
||||
* contract MyAccountERC7579 is AccountERC7579, Initializable {
|
||||
* function initializeAccount(address validator, bytes calldata validatorData) public initializer {
|
||||
* _installModule(MODULE_TYPE_VALIDATOR, validator, validatorData);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* [NOTE]
|
||||
* ====
|
||||
* * Hook support is not included. See {AccountERC7579Hooked} for a version that hooks to execution.
|
||||
* * Validator selection, when verifying either ERC-1271 signature or ERC-4337 UserOperation is implemented in
|
||||
* internal virtual functions {_extractUserOpValidator} and {_extractSignatureValidator}. Both are implemented
|
||||
* following common practices. However, this part is not standardized in ERC-7579 (or in any follow-up ERC). Some
|
||||
* accounts may want to override these internal functions.
|
||||
* * When combined with {ERC7739}, resolution ordering of {isValidSignature} may have an impact ({ERC7739} does not
|
||||
* call super). Manual resolution might be necessary.
|
||||
* * Static calls (using callType `0xfe`) are currently NOT supported.
|
||||
* ====
|
||||
*
|
||||
* WARNING: Removing all validator modules will render the account inoperable, as no user operations can be validated thereafter.
|
||||
*/
|
||||
abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig {
|
||||
using Bytes for *;
|
||||
using ERC7579Utils for *;
|
||||
using EnumerableSet for *;
|
||||
using Packing for bytes32;
|
||||
|
||||
EnumerableSet.AddressSet private _validators;
|
||||
EnumerableSet.AddressSet private _executors;
|
||||
mapping(bytes4 selector => address) private _fallbacks;
|
||||
|
||||
/// @dev The account's {fallback} was called with a selector that doesn't have an installed handler.
|
||||
error ERC7579MissingFallbackHandler(bytes4 selector);
|
||||
|
||||
/// @dev Modifier that checks if the caller is an installed module of the given type.
|
||||
modifier onlyModule(uint256 moduleTypeId, bytes calldata additionalContext) {
|
||||
_checkModule(moduleTypeId, msg.sender, additionalContext);
|
||||
_;
|
||||
}
|
||||
|
||||
/// @dev See {_fallback}.
|
||||
fallback(bytes calldata) external payable virtual returns (bytes memory) {
|
||||
return _fallback();
|
||||
}
|
||||
|
||||
/// @inheritdoc IERC7579AccountConfig
|
||||
function accountId() public view virtual returns (string memory) {
|
||||
// vendorname.accountname.semver
|
||||
return "@openzeppelin/community-contracts.AccountERC7579.v0.0.0";
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc IERC7579AccountConfig
|
||||
*
|
||||
* @dev Supported call types:
|
||||
* * Single (`0x00`): A single transaction execution.
|
||||
* * Batch (`0x01`): A batch of transactions execution.
|
||||
* * Delegate (`0xff`): A delegate call execution.
|
||||
*
|
||||
* Supported exec types:
|
||||
* * Default (`0x00`): Default execution type (revert on failure).
|
||||
* * Try (`0x01`): Try execution type (emits ERC7579TryExecuteFail on failure).
|
||||
*/
|
||||
function supportsExecutionMode(bytes32 encodedMode) public view virtual returns (bool) {
|
||||
(CallType callType, ExecType execType, , ) = Mode.wrap(encodedMode).decodeMode();
|
||||
return
|
||||
(callType == ERC7579Utils.CALLTYPE_SINGLE ||
|
||||
callType == ERC7579Utils.CALLTYPE_BATCH ||
|
||||
callType == ERC7579Utils.CALLTYPE_DELEGATECALL) &&
|
||||
(execType == ERC7579Utils.EXECTYPE_DEFAULT || execType == ERC7579Utils.EXECTYPE_TRY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc IERC7579AccountConfig
|
||||
*
|
||||
* @dev Supported module types:
|
||||
*
|
||||
* * Validator: A module used during the validation phase to determine if a transaction is valid and
|
||||
* should be executed on the account.
|
||||
* * Executor: A module that can execute transactions on behalf of the smart account via a callback.
|
||||
* * Fallback Handler: A module that can extend the fallback functionality of a smart account.
|
||||
*/
|
||||
function supportsModule(uint256 moduleTypeId) public view virtual returns (bool) {
|
||||
return
|
||||
moduleTypeId == MODULE_TYPE_VALIDATOR ||
|
||||
moduleTypeId == MODULE_TYPE_EXECUTOR ||
|
||||
moduleTypeId == MODULE_TYPE_FALLBACK;
|
||||
}
|
||||
|
||||
/// @inheritdoc IERC7579ModuleConfig
|
||||
function installModule(
|
||||
uint256 moduleTypeId,
|
||||
address module,
|
||||
bytes calldata initData
|
||||
) public virtual onlyEntryPointOrSelf {
|
||||
_installModule(moduleTypeId, module, initData);
|
||||
}
|
||||
|
||||
/// @inheritdoc IERC7579ModuleConfig
|
||||
function uninstallModule(
|
||||
uint256 moduleTypeId,
|
||||
address module,
|
||||
bytes calldata deInitData
|
||||
) public virtual onlyEntryPointOrSelf {
|
||||
_uninstallModule(moduleTypeId, module, deInitData);
|
||||
}
|
||||
|
||||
/// @inheritdoc IERC7579ModuleConfig
|
||||
function isModuleInstalled(
|
||||
uint256 moduleTypeId,
|
||||
address module,
|
||||
bytes calldata additionalContext
|
||||
) public view virtual returns (bool) {
|
||||
if (moduleTypeId == MODULE_TYPE_VALIDATOR) return _validators.contains(module);
|
||||
if (moduleTypeId == MODULE_TYPE_EXECUTOR) return _executors.contains(module);
|
||||
if (moduleTypeId == MODULE_TYPE_FALLBACK) return _fallbacks[bytes4(additionalContext[0:4])] == module;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// @inheritdoc IERC7579Execution
|
||||
function execute(bytes32 mode, bytes calldata executionCalldata) public payable virtual onlyEntryPointOrSelf {
|
||||
_execute(Mode.wrap(mode), executionCalldata);
|
||||
}
|
||||
|
||||
/// @inheritdoc IERC7579Execution
|
||||
function executeFromExecutor(
|
||||
bytes32 mode,
|
||||
bytes calldata executionCalldata
|
||||
)
|
||||
public
|
||||
payable
|
||||
virtual
|
||||
onlyModule(MODULE_TYPE_EXECUTOR, Calldata.emptyBytes())
|
||||
returns (bytes[] memory returnData)
|
||||
{
|
||||
return _execute(Mode.wrap(mode), executionCalldata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Implement ERC-1271 through IERC7579Validator modules. If module based validation fails, fallback to
|
||||
* "native" validation by the abstract signer.
|
||||
*
|
||||
* NOTE: when combined with {ERC7739}, resolution ordering may have an impact ({ERC7739} does not call super).
|
||||
* Manual resolution might be necessary.
|
||||
*/
|
||||
function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
|
||||
// check signature length is enough for extraction
|
||||
if (signature.length >= 20) {
|
||||
(address module, bytes calldata innerSignature) = _extractSignatureValidator(signature);
|
||||
// if module is not installed, skip
|
||||
if (isModuleInstalled(MODULE_TYPE_VALIDATOR, module, Calldata.emptyBytes())) {
|
||||
// try validation, skip any revert
|
||||
try IERC7579Validator(module).isValidSignatureWithSender(msg.sender, hash, innerSignature) returns (
|
||||
bytes4 magic
|
||||
) {
|
||||
return magic;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return bytes4(0xffffffff);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Validates a user operation with {_signableUserOpHash} and returns the validation data
|
||||
* if the module specified by the first 20 bytes of the nonce key is installed. Falls back to
|
||||
* {Account-_validateUserOp} otherwise.
|
||||
*
|
||||
* See {_extractUserOpValidator} for the module extraction logic.
|
||||
*/
|
||||
function _validateUserOp(
|
||||
PackedUserOperation calldata userOp,
|
||||
bytes32 userOpHash
|
||||
) internal virtual override returns (uint256) {
|
||||
address module = _extractUserOpValidator(userOp);
|
||||
return
|
||||
isModuleInstalled(MODULE_TYPE_VALIDATOR, module, Calldata.emptyBytes())
|
||||
? IERC7579Validator(module).validateUserOp(userOp, _signableUserOpHash(userOp, userOpHash))
|
||||
: super._validateUserOp(userOp, userOpHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev ERC-7579 execution logic. See {supportsExecutionMode} for supported modes.
|
||||
*
|
||||
* Reverts if the call type is not supported.
|
||||
*/
|
||||
function _execute(
|
||||
Mode mode,
|
||||
bytes calldata executionCalldata
|
||||
) internal virtual returns (bytes[] memory returnData) {
|
||||
(CallType callType, ExecType execType, , ) = mode.decodeMode();
|
||||
if (callType == ERC7579Utils.CALLTYPE_SINGLE) return executionCalldata.execSingle(execType);
|
||||
if (callType == ERC7579Utils.CALLTYPE_BATCH) return executionCalldata.execBatch(execType);
|
||||
if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) return executionCalldata.execDelegateCall(execType);
|
||||
revert ERC7579Utils.ERC7579UnsupportedCallType(callType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Installs a module of the given type with the given initialization data.
|
||||
*
|
||||
* For the fallback module type, the `initData` is expected to be the (packed) concatenation of a 4-byte
|
||||
* selector and the rest of the data to be sent to the handler when calling {IERC7579Module-onInstall}.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* * Module type must be supported. See {supportsModule}. Reverts with {ERC7579UnsupportedModuleType}.
|
||||
* * Module must be of the given type. Reverts with {ERC7579MismatchedModuleTypeId}.
|
||||
* * Module must not be already installed. Reverts with {ERC7579AlreadyInstalledModule}.
|
||||
*
|
||||
* Emits a {ModuleInstalled} event.
|
||||
*/
|
||||
function _installModule(uint256 moduleTypeId, address module, bytes memory initData) internal virtual {
|
||||
require(supportsModule(moduleTypeId), ERC7579Utils.ERC7579UnsupportedModuleType(moduleTypeId));
|
||||
require(
|
||||
IERC7579Module(module).isModuleType(moduleTypeId),
|
||||
ERC7579Utils.ERC7579MismatchedModuleTypeId(moduleTypeId, module)
|
||||
);
|
||||
|
||||
if (moduleTypeId == MODULE_TYPE_VALIDATOR) {
|
||||
require(_validators.add(module), ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module));
|
||||
} else if (moduleTypeId == MODULE_TYPE_EXECUTOR) {
|
||||
require(_executors.add(module), ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module));
|
||||
} else if (moduleTypeId == MODULE_TYPE_FALLBACK) {
|
||||
bytes4 selector;
|
||||
(selector, initData) = _decodeFallbackData(initData);
|
||||
require(
|
||||
_fallbacks[selector] == address(0),
|
||||
ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module)
|
||||
);
|
||||
_fallbacks[selector] = module;
|
||||
}
|
||||
|
||||
IERC7579Module(module).onInstall(initData);
|
||||
emit ModuleInstalled(moduleTypeId, module);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Uninstalls a module of the given type with the given de-initialization data.
|
||||
*
|
||||
* For the fallback module type, the `deInitData` is expected to be the (packed) concatenation of a 4-byte
|
||||
* selector and the rest of the data to be sent to the handler when calling {IERC7579Module-onUninstall}.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* * Module must be already installed. Reverts with {ERC7579UninstalledModule} otherwise.
|
||||
*/
|
||||
function _uninstallModule(uint256 moduleTypeId, address module, bytes memory deInitData) internal virtual {
|
||||
require(supportsModule(moduleTypeId), ERC7579Utils.ERC7579UnsupportedModuleType(moduleTypeId));
|
||||
|
||||
if (moduleTypeId == MODULE_TYPE_VALIDATOR) {
|
||||
require(_validators.remove(module), ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
|
||||
} else if (moduleTypeId == MODULE_TYPE_EXECUTOR) {
|
||||
require(_executors.remove(module), ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
|
||||
} else if (moduleTypeId == MODULE_TYPE_FALLBACK) {
|
||||
bytes4 selector;
|
||||
(selector, deInitData) = _decodeFallbackData(deInitData);
|
||||
require(
|
||||
_fallbackHandler(selector) == module && module != address(0),
|
||||
ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module)
|
||||
);
|
||||
delete _fallbacks[selector];
|
||||
}
|
||||
|
||||
IERC7579Module(module).onUninstall(deInitData);
|
||||
emit ModuleUninstalled(moduleTypeId, module);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Fallback function that delegates the call to the installed handler for the given selector.
|
||||
*
|
||||
* Reverts with {ERC7579MissingFallbackHandler} if the handler is not installed.
|
||||
*
|
||||
* Calls the handler with the original `msg.sender` appended at the end of the calldata following
|
||||
* the ERC-2771 format.
|
||||
*/
|
||||
function _fallback() internal virtual returns (bytes memory) {
|
||||
address handler = _fallbackHandler(msg.sig);
|
||||
require(handler != address(0), ERC7579MissingFallbackHandler(msg.sig));
|
||||
|
||||
// From https://eips.ethereum.org/EIPS/eip-7579#fallback[ERC-7579 specifications]:
|
||||
// - MUST utilize ERC-2771 to add the original msg.sender to the calldata sent to the fallback handler
|
||||
// - MUST use call to invoke the fallback handler
|
||||
(bool success, bytes memory returndata) = handler.call{value: msg.value}(
|
||||
abi.encodePacked(msg.data, msg.sender)
|
||||
);
|
||||
|
||||
if (success) return returndata;
|
||||
|
||||
assembly ("memory-safe") {
|
||||
revert(add(returndata, 0x20), mload(returndata))
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Returns the fallback handler for the given selector. Returns `address(0)` if not installed.
|
||||
function _fallbackHandler(bytes4 selector) internal view virtual returns (address) {
|
||||
return _fallbacks[selector];
|
||||
}
|
||||
|
||||
/// @dev Checks if the module is installed. Reverts if the module is not installed.
|
||||
function _checkModule(
|
||||
uint256 moduleTypeId,
|
||||
address module,
|
||||
bytes calldata additionalContext
|
||||
) internal view virtual {
|
||||
require(
|
||||
isModuleInstalled(moduleTypeId, module, additionalContext),
|
||||
ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Extracts the nonce validator from the user operation.
|
||||
*
|
||||
* To construct a nonce key, set nonce as follows:
|
||||
*
|
||||
* ```
|
||||
* <module address (20 bytes)> | <key (4 bytes)> | <nonce (8 bytes)>
|
||||
* ```
|
||||
* NOTE: The default behavior of this function replicates the behavior of
|
||||
* https://github.com/rhinestonewtf/safe7579/blob/bb29e8b1a66658790c4169e72608e27d220f79be/src/Safe7579.sol#L266[Safe adapter],
|
||||
* https://github.com/etherspot/etherspot-prime-contracts/blob/cfcdb48c4172cea0d66038324c0bae3288aa8caa/src/modular-etherspot-wallet/wallet/ModularEtherspotWallet.sol#L227[Etherspot's Prime Account], and
|
||||
* https://github.com/erc7579/erc7579-implementation/blob/16138d1afd4e9711f6c1425133538837bd7787b5/src/MSAAdvanced.sol#L247[ERC7579 reference implementation].
|
||||
*
|
||||
* This is not standardized in ERC-7579 (or in any follow-up ERC). Some accounts may want to override these internal functions.
|
||||
*
|
||||
* For example, https://github.com/bcnmy/nexus/blob/54f4e19baaff96081a8843672977caf712ef19f4/contracts/lib/NonceLib.sol#L17[Biconomy's Nexus]
|
||||
* uses a similar yet incompatible approach (the validator address is also part of the nonce, but not at the same location)
|
||||
*/
|
||||
function _extractUserOpValidator(PackedUserOperation calldata userOp) internal pure virtual returns (address) {
|
||||
return address(bytes32(userOp.nonce).extract_32_20(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Extracts the signature validator from the signature.
|
||||
*
|
||||
* To construct a signature, set the first 20 bytes as the module address and the remaining bytes as the
|
||||
* signature data:
|
||||
*
|
||||
* ```
|
||||
* <module address (20 bytes)> | <signature data>
|
||||
* ```
|
||||
*
|
||||
* NOTE: The default behavior of this function replicates the behavior of
|
||||
* https://github.com/rhinestonewtf/safe7579/blob/bb29e8b1a66658790c4169e72608e27d220f79be/src/Safe7579.sol#L350[Safe adapter],
|
||||
* https://github.com/bcnmy/nexus/blob/54f4e19baaff96081a8843672977caf712ef19f4/contracts/Nexus.sol#L239[Biconomy's Nexus],
|
||||
* https://github.com/etherspot/etherspot-prime-contracts/blob/cfcdb48c4172cea0d66038324c0bae3288aa8caa/src/modular-etherspot-wallet/wallet/ModularEtherspotWallet.sol#L252[Etherspot's Prime Account], and
|
||||
* https://github.com/erc7579/erc7579-implementation/blob/16138d1afd4e9711f6c1425133538837bd7787b5/src/MSAAdvanced.sol#L296[ERC7579 reference implementation].
|
||||
*
|
||||
* This is not standardized in ERC-7579 (or in any follow-up ERC). Some accounts may want to override these internal functions.
|
||||
*/
|
||||
function _extractSignatureValidator(
|
||||
bytes calldata signature
|
||||
) internal pure virtual returns (address module, bytes calldata innerSignature) {
|
||||
return (address(bytes20(signature[0:20])), signature[20:]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Extract the function selector from initData/deInitData for MODULE_TYPE_FALLBACK
|
||||
*
|
||||
* NOTE: If we had calldata here, we could use calldata slice which are cheaper to manipulate and don't require
|
||||
* actual copy. However, this would require `_installModule` to get a calldata bytes object instead of a memory
|
||||
* bytes object. This would prevent calling `_installModule` from a contract constructor and would force the use
|
||||
* of external initializers. That may change in the future, as most accounts will probably be deployed as
|
||||
* clones/proxy/ERC-7702 delegates and therefore rely on initializers anyway.
|
||||
*/
|
||||
function _decodeFallbackData(
|
||||
bytes memory data
|
||||
) internal pure virtual returns (bytes4 selector, bytes memory remaining) {
|
||||
return (bytes4(data), data.slice(4));
|
||||
}
|
||||
|
||||
/// @dev By default, only use the modules for validation of userOp and signature. Disable raw signatures.
|
||||
function _rawSignatureValidation(
|
||||
bytes32 /*hash*/,
|
||||
bytes calldata /*signature*/
|
||||
) internal view virtual override returns (bool) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
106
contracts/account/extensions/AccountERC7579Hooked.sol
Normal file
106
contracts/account/extensions/AccountERC7579Hooked.sol
Normal file
@ -0,0 +1,106 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.27;
|
||||
|
||||
import {IERC7579Hook, MODULE_TYPE_HOOK} from "../../interfaces/draft-IERC7579.sol";
|
||||
import {ERC7579Utils, Mode} from "../../account/utils/draft-ERC7579Utils.sol";
|
||||
import {AccountERC7579} from "./AccountERC7579.sol";
|
||||
|
||||
/**
|
||||
* @dev Extension of {AccountERC7579} with support for a single hook module (type 4).
|
||||
*
|
||||
* If installed, this extension will call the hook module's {IERC7579Hook-preCheck} before executing any operation
|
||||
* with {_execute} (including {execute} and {executeFromExecutor} by default) and {IERC7579Hook-postCheck} thereafter.
|
||||
*
|
||||
* NOTE: Hook modules break the check-effect-interaction pattern. In particular, the {IERC7579Hook-preCheck} hook can
|
||||
* lead to potentially dangerous reentrancy. Using the `withHook()` modifier is safe if no effect is performed
|
||||
* before the preHook or after the postHook. That is the case on all functions here, but it may not be the case if
|
||||
* functions that have this modifier are overridden. Developers should be extremely careful when implementing hook
|
||||
* modules or further overriding functions that involve hooks.
|
||||
*/
|
||||
abstract contract AccountERC7579Hooked is AccountERC7579 {
|
||||
address private _hook;
|
||||
|
||||
/// @dev A hook module is already present. This contract only supports one hook module.
|
||||
error ERC7579HookModuleAlreadyPresent(address hook);
|
||||
|
||||
/**
|
||||
* @dev Calls {IERC7579Hook-preCheck} before executing the modified function and {IERC7579Hook-postCheck}
|
||||
* thereafter.
|
||||
*/
|
||||
modifier withHook() {
|
||||
address hook_ = hook();
|
||||
bytes memory hookData;
|
||||
|
||||
// slither-disable-next-line reentrancy-no-eth
|
||||
if (hook_ != address(0)) hookData = IERC7579Hook(hook_).preCheck(msg.sender, msg.value, msg.data);
|
||||
_;
|
||||
if (hook_ != address(0)) IERC7579Hook(hook_).postCheck(hookData);
|
||||
}
|
||||
|
||||
/// @inheritdoc AccountERC7579
|
||||
function accountId() public view virtual override returns (string memory) {
|
||||
// vendorname.accountname.semver
|
||||
return "@openzeppelin/community-contracts.AccountERC7579Hooked.v0.0.0";
|
||||
}
|
||||
|
||||
/// @dev Returns the hook module address if installed, or `address(0)` otherwise.
|
||||
function hook() public view virtual returns (address) {
|
||||
return _hook;
|
||||
}
|
||||
|
||||
/// @dev Supports hook modules. See {AccountERC7579-supportsModule}
|
||||
function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) {
|
||||
return moduleTypeId == MODULE_TYPE_HOOK || super.supportsModule(moduleTypeId);
|
||||
}
|
||||
|
||||
/// @inheritdoc AccountERC7579
|
||||
function isModuleInstalled(
|
||||
uint256 moduleTypeId,
|
||||
address module,
|
||||
bytes calldata data
|
||||
) public view virtual override returns (bool) {
|
||||
return
|
||||
(moduleTypeId == MODULE_TYPE_HOOK && module == hook()) ||
|
||||
super.isModuleInstalled(moduleTypeId, module, data);
|
||||
}
|
||||
|
||||
/// @dev Installs a module with support for hook modules. See {AccountERC7579-_installModule}
|
||||
function _installModule(
|
||||
uint256 moduleTypeId,
|
||||
address module,
|
||||
bytes memory initData
|
||||
) internal virtual override withHook {
|
||||
if (moduleTypeId == MODULE_TYPE_HOOK) {
|
||||
require(_hook == address(0), ERC7579HookModuleAlreadyPresent(_hook));
|
||||
_hook = module;
|
||||
}
|
||||
super._installModule(moduleTypeId, module, initData);
|
||||
}
|
||||
|
||||
/// @dev Uninstalls a module with support for hook modules. See {AccountERC7579-_uninstallModule}
|
||||
function _uninstallModule(
|
||||
uint256 moduleTypeId,
|
||||
address module,
|
||||
bytes memory deInitData
|
||||
) internal virtual override withHook {
|
||||
if (moduleTypeId == MODULE_TYPE_HOOK) {
|
||||
require(_hook == module, ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
|
||||
_hook = address(0);
|
||||
}
|
||||
super._uninstallModule(moduleTypeId, module, deInitData);
|
||||
}
|
||||
|
||||
/// @dev Hooked version of {AccountERC7579-_execute}.
|
||||
function _execute(
|
||||
Mode mode,
|
||||
bytes calldata executionCalldata
|
||||
) internal virtual override withHook returns (bytes[] memory) {
|
||||
return super._execute(mode, executionCalldata);
|
||||
}
|
||||
|
||||
/// @dev Hooked version of {AccountERC7579-_fallback}.
|
||||
function _fallback() internal virtual override withHook returns (bytes memory) {
|
||||
return super._fallback();
|
||||
}
|
||||
}
|
||||
69
contracts/account/extensions/ERC7821.sol
Normal file
69
contracts/account/extensions/ERC7821.sol
Normal file
@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {ERC7579Utils, Mode, CallType, ExecType, ModeSelector} from "../utils/draft-ERC7579Utils.sol";
|
||||
import {IERC7821} from "../../interfaces/IERC7821.sol";
|
||||
import {Account} from "../Account.sol";
|
||||
|
||||
/**
|
||||
* @dev Minimal batch executor following ERC-7821.
|
||||
*
|
||||
* Only supports supports single batch mode (`0x01000000000000000000`). Does not support optional "opData".
|
||||
*
|
||||
* @custom:stateless
|
||||
*/
|
||||
abstract contract ERC7821 is IERC7821 {
|
||||
using ERC7579Utils for *;
|
||||
|
||||
error UnsupportedExecutionMode();
|
||||
|
||||
/**
|
||||
* @dev Executes the calls in `executionData` with no optional `opData` support.
|
||||
*
|
||||
* NOTE: Access to this function is controlled by {_erc7821AuthorizedExecutor}. Changing access permissions, for
|
||||
* example to approve calls by the ERC-4337 entrypoint, should be implemented by overriding it.
|
||||
*
|
||||
* Reverts and bubbles up error if any call fails.
|
||||
*/
|
||||
function execute(bytes32 mode, bytes calldata executionData) public payable virtual {
|
||||
if (!_erc7821AuthorizedExecutor(msg.sender, mode, executionData))
|
||||
revert Account.AccountUnauthorized(msg.sender);
|
||||
if (!supportsExecutionMode(mode)) revert 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 == ModeSelector.wrap(0x00000000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Access control mechanism for the {execute} function.
|
||||
* By default, only the contract itself is allowed to execute.
|
||||
*
|
||||
* Override this function to implement custom access control, for example to allow the
|
||||
* ERC-4337 entrypoint to execute.
|
||||
*
|
||||
* ```solidity
|
||||
* function _erc7821AuthorizedExecutor(
|
||||
* address caller,
|
||||
* bytes32 mode,
|
||||
* bytes calldata executionData
|
||||
* ) internal view virtual override returns (bool) {
|
||||
* return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function _erc7821AuthorizedExecutor(
|
||||
address caller,
|
||||
bytes32 /* mode */,
|
||||
bytes calldata /* executionData */
|
||||
) internal view virtual returns (bool) {
|
||||
return caller == address(this);
|
||||
}
|
||||
}
|
||||
43
contracts/interfaces/IERC7821.sol
Normal file
43
contracts/interfaces/IERC7821.sol
Normal file
@ -0,0 +1,43 @@
|
||||
// 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.
|
||||
*
|
||||
* For calldata compression efficiency, if a Call.to is `address(0)`,
|
||||
* it will be replaced with `address(this)`.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
@ -5,6 +5,7 @@ pragma solidity ^0.8.20;
|
||||
contract CallReceiverMock {
|
||||
event MockFunctionCalled();
|
||||
event MockFunctionCalledWithArgs(uint256 a, uint256 b);
|
||||
event MockFunctionCalledExtra(address caller, uint256 value);
|
||||
|
||||
uint256[] private _array;
|
||||
|
||||
@ -58,6 +59,10 @@ contract CallReceiverMock {
|
||||
}
|
||||
return "0x1234";
|
||||
}
|
||||
|
||||
function mockFunctionExtra() public payable {
|
||||
emit MockFunctionCalledExtra(msg.sender, msg.value);
|
||||
}
|
||||
}
|
||||
|
||||
contract CallReceiverMockTrustingForwarder is CallReceiverMock {
|
||||
|
||||
138
contracts/mocks/account/AccountMock.sol
Normal file
138
contracts/mocks/account/AccountMock.sol
Normal file
@ -0,0 +1,138 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.27;
|
||||
|
||||
import {Account} from "../../account/Account.sol";
|
||||
import {AccountERC7579} from "../../account/extensions/AccountERC7579.sol";
|
||||
import {AccountERC7579Hooked} from "../../account/extensions/AccountERC7579Hooked.sol";
|
||||
import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol";
|
||||
import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol";
|
||||
import {ERC4337Utils} from "../../account/utils/draft-ERC4337Utils.sol";
|
||||
import {ERC7739} from "../../utils/cryptography/ERC7739.sol";
|
||||
import {ERC7821} from "../../account/extensions/ERC7821.sol";
|
||||
import {MODULE_TYPE_VALIDATOR} from "../../interfaces/draft-IERC7579.sol";
|
||||
import {PackedUserOperation} from "../../interfaces/draft-IERC4337.sol";
|
||||
import {AbstractSigner} from "../../utils/cryptography/AbstractSigner.sol";
|
||||
import {SignerECDSA} from "../../utils/cryptography/SignerECDSA.sol";
|
||||
import {SignerP256} from "../../utils/cryptography/SignerP256.sol";
|
||||
import {SignerRSA} from "../../utils/cryptography/SignerRSA.sol";
|
||||
import {SignerERC7702} from "../../utils/cryptography/SignerERC7702.sol";
|
||||
|
||||
abstract contract AccountMock is Account, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
|
||||
/// Validates a user operation with a boolean signature.
|
||||
function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal pure override returns (bool) {
|
||||
return signature.length >= 32 && bytes32(signature) == hash;
|
||||
}
|
||||
|
||||
/// @inheritdoc ERC7821
|
||||
function _erc7821AuthorizedExecutor(
|
||||
address caller,
|
||||
bytes32 mode,
|
||||
bytes calldata executionData
|
||||
) internal view virtual override returns (bool) {
|
||||
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract AccountECDSAMock is Account, SignerECDSA, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
|
||||
constructor(address signerAddr) {
|
||||
_setSigner(signerAddr);
|
||||
}
|
||||
|
||||
/// @inheritdoc ERC7821
|
||||
function _erc7821AuthorizedExecutor(
|
||||
address caller,
|
||||
bytes32 mode,
|
||||
bytes calldata executionData
|
||||
) internal view virtual override returns (bool) {
|
||||
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract AccountP256Mock is Account, SignerP256, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
|
||||
constructor(bytes32 qx, bytes32 qy) {
|
||||
_setSigner(qx, qy);
|
||||
}
|
||||
|
||||
/// @inheritdoc ERC7821
|
||||
function _erc7821AuthorizedExecutor(
|
||||
address caller,
|
||||
bytes32 mode,
|
||||
bytes calldata executionData
|
||||
) internal view virtual override returns (bool) {
|
||||
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract AccountRSAMock is Account, SignerRSA, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
|
||||
constructor(bytes memory e, bytes memory n) {
|
||||
_setSigner(e, n);
|
||||
}
|
||||
|
||||
/// @inheritdoc ERC7821
|
||||
function _erc7821AuthorizedExecutor(
|
||||
address caller,
|
||||
bytes32 mode,
|
||||
bytes calldata executionData
|
||||
) internal view virtual override returns (bool) {
|
||||
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract AccountERC7702Mock is Account, SignerERC7702, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
|
||||
/// @inheritdoc ERC7821
|
||||
function _erc7821AuthorizedExecutor(
|
||||
address caller,
|
||||
bytes32 mode,
|
||||
bytes calldata executionData
|
||||
) internal view virtual override returns (bool) {
|
||||
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract AccountERC7702WithModulesMock is
|
||||
Account,
|
||||
AccountERC7579,
|
||||
SignerERC7702,
|
||||
ERC7739,
|
||||
ERC721Holder,
|
||||
ERC1155Holder
|
||||
{
|
||||
function _validateUserOp(
|
||||
PackedUserOperation calldata userOp,
|
||||
bytes32 userOpHash
|
||||
) internal virtual override(Account, AccountERC7579) returns (uint256) {
|
||||
return super._validateUserOp(userOp, userOpHash);
|
||||
}
|
||||
|
||||
/// @dev Resolve implementation of ERC-1271 by both ERC7739 and AccountERC7579 to support both schemes.
|
||||
function isValidSignature(
|
||||
bytes32 hash,
|
||||
bytes calldata signature
|
||||
) public view virtual override(ERC7739, AccountERC7579) returns (bytes4) {
|
||||
// ERC-7739 can return the fn selector (success), 0xffffffff (invalid) or 0x77390001 (detection).
|
||||
// If the return is 0xffffffff, we fallback to validation using ERC-7579 modules.
|
||||
bytes4 erc7739magic = ERC7739.isValidSignature(hash, signature);
|
||||
return erc7739magic == bytes4(0xffffffff) ? AccountERC7579.isValidSignature(hash, signature) : erc7739magic;
|
||||
}
|
||||
|
||||
/// @dev Enable signature using the ERC-7702 signer.
|
||||
function _rawSignatureValidation(
|
||||
bytes32 hash,
|
||||
bytes calldata signature
|
||||
) internal view virtual override(AbstractSigner, AccountERC7579, SignerERC7702) returns (bool) {
|
||||
return SignerERC7702._rawSignatureValidation(hash, signature);
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract AccountERC7579Mock is AccountERC7579 {
|
||||
constructor(address validator, bytes memory initData) {
|
||||
_installModule(MODULE_TYPE_VALIDATOR, validator, initData);
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract AccountERC7579HookedMock is AccountERC7579Hooked {
|
||||
constructor(address validator, bytes memory initData) {
|
||||
_installModule(MODULE_TYPE_VALIDATOR, validator, initData);
|
||||
}
|
||||
}
|
||||
115
contracts/mocks/account/modules/ERC7579Mock.sol
Normal file
115
contracts/mocks/account/modules/ERC7579Mock.sol
Normal file
@ -0,0 +1,115 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {MODULE_TYPE_HOOK, MODULE_TYPE_FALLBACK, MODULE_TYPE_VALIDATOR, IERC7579Hook, IERC7579Module, IERC7579Validator} from "../../../interfaces/draft-IERC7579.sol";
|
||||
import {SignatureChecker} from "../../../utils/cryptography/SignatureChecker.sol";
|
||||
import {PackedUserOperation} from "../../../interfaces/draft-IERC4337.sol";
|
||||
import {IERC1271} from "../../../interfaces/IERC1271.sol";
|
||||
import {ERC4337Utils} from "../../../account/utils/draft-ERC4337Utils.sol";
|
||||
|
||||
abstract contract ERC7579ModuleMock is IERC7579Module {
|
||||
uint256 private _moduleTypeId;
|
||||
|
||||
event ModuleInstalledReceived(address account, bytes data);
|
||||
event ModuleUninstalledReceived(address account, bytes data);
|
||||
|
||||
constructor(uint256 moduleTypeId) {
|
||||
_moduleTypeId = moduleTypeId;
|
||||
}
|
||||
|
||||
function onInstall(bytes calldata data) public virtual {
|
||||
emit ModuleInstalledReceived(msg.sender, data);
|
||||
}
|
||||
|
||||
function onUninstall(bytes calldata data) public virtual {
|
||||
emit ModuleUninstalledReceived(msg.sender, data);
|
||||
}
|
||||
|
||||
function isModuleType(uint256 moduleTypeId) external view returns (bool) {
|
||||
return moduleTypeId == _moduleTypeId;
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract ERC7579HookMock is ERC7579ModuleMock(MODULE_TYPE_HOOK), IERC7579Hook {
|
||||
event PreCheck(address sender, uint256 value, bytes data);
|
||||
event PostCheck(bytes hookData);
|
||||
|
||||
function preCheck(
|
||||
address msgSender,
|
||||
uint256 value,
|
||||
bytes calldata msgData
|
||||
) external returns (bytes memory hookData) {
|
||||
emit PreCheck(msgSender, value, msgData);
|
||||
return msgData;
|
||||
}
|
||||
|
||||
function postCheck(bytes calldata hookData) external {
|
||||
emit PostCheck(hookData);
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract ERC7579FallbackHandlerMock is ERC7579ModuleMock(MODULE_TYPE_FALLBACK) {
|
||||
event ERC7579FallbackHandlerMockCalled(address account, address sender, uint256 value, bytes data);
|
||||
|
||||
error ERC7579FallbackHandlerMockRevert();
|
||||
|
||||
function _msgAccount() internal view returns (address) {
|
||||
return msg.sender;
|
||||
}
|
||||
|
||||
function _msgSender() internal pure returns (address) {
|
||||
return address(bytes20(msg.data[msg.data.length - 20:]));
|
||||
}
|
||||
|
||||
function _msgData() internal pure returns (bytes calldata) {
|
||||
return msg.data[:msg.data.length - 20];
|
||||
}
|
||||
|
||||
function callPayable() public payable {
|
||||
emit ERC7579FallbackHandlerMockCalled(_msgAccount(), _msgSender(), msg.value, _msgData());
|
||||
}
|
||||
|
||||
function callView() public view returns (address, address) {
|
||||
return (_msgAccount(), _msgSender());
|
||||
}
|
||||
|
||||
function callRevert() public pure {
|
||||
revert ERC7579FallbackHandlerMockRevert();
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract ERC7579ValidatorMock is ERC7579ModuleMock(MODULE_TYPE_VALIDATOR), IERC7579Validator {
|
||||
mapping(address sender => address signer) private _associatedSigners;
|
||||
|
||||
function onInstall(bytes calldata data) public virtual override(IERC7579Module, ERC7579ModuleMock) {
|
||||
_associatedSigners[msg.sender] = address(bytes20(data[0:20]));
|
||||
super.onInstall(data);
|
||||
}
|
||||
|
||||
function onUninstall(bytes calldata data) public virtual override(IERC7579Module, ERC7579ModuleMock) {
|
||||
delete _associatedSigners[msg.sender];
|
||||
super.onUninstall(data);
|
||||
}
|
||||
|
||||
function validateUserOp(
|
||||
PackedUserOperation calldata userOp,
|
||||
bytes32 userOpHash
|
||||
) public view virtual returns (uint256) {
|
||||
return
|
||||
SignatureChecker.isValidSignatureNow(_associatedSigners[msg.sender], userOpHash, userOp.signature)
|
||||
? ERC4337Utils.SIG_VALIDATION_SUCCESS
|
||||
: ERC4337Utils.SIG_VALIDATION_FAILED;
|
||||
}
|
||||
|
||||
function isValidSignatureWithSender(
|
||||
address /*sender*/,
|
||||
bytes32 hash,
|
||||
bytes calldata signature
|
||||
) public view virtual returns (bytes4) {
|
||||
return
|
||||
SignatureChecker.isValidSignatureNow(_associatedSigners[msg.sender], hash, signature)
|
||||
? IERC1271.isValidSignature.selector
|
||||
: bytes4(0xffffffff);
|
||||
}
|
||||
}
|
||||
@ -5,24 +5,24 @@ pragma solidity ^0.8.20;
|
||||
import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
|
||||
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
|
||||
import {ERC7739} from "../../../utils/cryptography/ERC7739.sol";
|
||||
import {AbstractSigner} from "../../../utils/cryptography/AbstractSigner.sol";
|
||||
|
||||
contract ERC7739ECDSAMock is AbstractSigner, ERC7739 {
|
||||
address private _signer;
|
||||
import {SignerECDSA} from "../../../utils/cryptography/SignerECDSA.sol";
|
||||
import {SignerP256} from "../../../utils/cryptography/SignerP256.sol";
|
||||
import {SignerRSA} from "../../../utils/cryptography/SignerRSA.sol";
|
||||
|
||||
contract ERC7739ECDSAMock is ERC7739, SignerECDSA {
|
||||
constructor(address signerAddr) EIP712("ERC7739ECDSA", "1") {
|
||||
_signer = signerAddr;
|
||||
}
|
||||
|
||||
function signer() public view virtual returns (address) {
|
||||
return _signer;
|
||||
}
|
||||
|
||||
function _rawSignatureValidation(
|
||||
bytes32 hash,
|
||||
bytes calldata signature
|
||||
) internal view virtual override returns (bool) {
|
||||
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
|
||||
return signer() == recovered && err == ECDSA.RecoverError.NoError;
|
||||
_setSigner(signerAddr);
|
||||
}
|
||||
}
|
||||
|
||||
contract ERC7739P256Mock is ERC7739, SignerP256 {
|
||||
constructor(bytes32 qx, bytes32 qy) EIP712("ERC7739P256", "1") {
|
||||
_setSigner(qx, qy);
|
||||
}
|
||||
}
|
||||
|
||||
contract ERC7739RSAMock is ERC7739, SignerRSA {
|
||||
constructor(bytes memory e, bytes memory n) EIP712("ERC7739RSA", "1") {
|
||||
_setSigner(e, n);
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,10 +46,12 @@ Miscellaneous contracts and libraries containing utility functions you can use t
|
||||
* {Comparators}: A library that contains comparator functions to use with the {Heap} library.
|
||||
* {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
|
||||
* {Blockhash}: A library for accessing historical block hashes beyond the standard 256 block limit utilizing EIP-2935's historical blockhash functionality.
|
||||
* {Time}: A library that provides helpers for manipulating time-related objects, including a `Delay` type.
|
||||
* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
|
||||
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
|
||||
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
|
||||
* {Time}: A library that provides helpers for manipulating time-related objects, including a `Delay` type.
|
||||
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
|
||||
* {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@ -90,6 +92,14 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable
|
||||
|
||||
{{AbstractSigner}}
|
||||
|
||||
{{SignerECDSA}}
|
||||
|
||||
{{SignerP256}}
|
||||
|
||||
{{SignerERC7702}}
|
||||
|
||||
{{SignerRSA}}
|
||||
|
||||
== Security
|
||||
|
||||
{{ReentrancyGuard}}
|
||||
|
||||
51
contracts/utils/cryptography/SignerECDSA.sol
Normal file
51
contracts/utils/cryptography/SignerECDSA.sol
Normal file
@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {ECDSA} from "../cryptography/ECDSA.sol";
|
||||
import {AbstractSigner} from "./AbstractSigner.sol";
|
||||
|
||||
/**
|
||||
* @dev Implementation of {AbstractSigner} using xref:api:utils#ECDSA[ECDSA] signatures.
|
||||
*
|
||||
* For {Account} usage, a {_setSigner} function is provided to set the {signer} address.
|
||||
* Doing so is easier for a factory, who is likely to use initializable clones of this contract.
|
||||
*
|
||||
* Example of usage:
|
||||
*
|
||||
* ```solidity
|
||||
* contract MyAccountECDSA is Account, SignerECDSA, Initializable {
|
||||
* function initialize(address signerAddr) public initializer {
|
||||
* _setSigner(signerAddr);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
|
||||
* or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
|
||||
*/
|
||||
abstract contract SignerECDSA is AbstractSigner {
|
||||
address private _signer;
|
||||
|
||||
/**
|
||||
* @dev Sets the signer with the address of the native signer. This function should be called during construction
|
||||
* or through an initializer.
|
||||
*/
|
||||
function _setSigner(address signerAddr) internal {
|
||||
_signer = signerAddr;
|
||||
}
|
||||
|
||||
/// @dev Return the signer's address.
|
||||
function signer() public view virtual returns (address) {
|
||||
return _signer;
|
||||
}
|
||||
|
||||
/// @inheritdoc AbstractSigner
|
||||
function _rawSignatureValidation(
|
||||
bytes32 hash,
|
||||
bytes calldata signature
|
||||
) internal view virtual override returns (bool) {
|
||||
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
|
||||
return signer() == recovered && err == ECDSA.RecoverError.NoError;
|
||||
}
|
||||
}
|
||||
24
contracts/utils/cryptography/SignerERC7702.sol
Normal file
24
contracts/utils/cryptography/SignerERC7702.sol
Normal file
@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {ECDSA} from "./ECDSA.sol";
|
||||
import {AbstractSigner} from "./AbstractSigner.sol";
|
||||
|
||||
/**
|
||||
* @dev Implementation of {AbstractSigner} for implementation for an EOA. Useful for ERC-7702 accounts.
|
||||
*
|
||||
* @custom:stateless
|
||||
*/
|
||||
abstract contract SignerERC7702 is AbstractSigner {
|
||||
/**
|
||||
* @dev Validates the signature using the EOA's address (i.e. `address(this)`).
|
||||
*/
|
||||
function _rawSignatureValidation(
|
||||
bytes32 hash,
|
||||
bytes calldata signature
|
||||
) internal view virtual override returns (bool) {
|
||||
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
|
||||
return address(this) == recovered && err == ECDSA.RecoverError.NoError;
|
||||
}
|
||||
}
|
||||
59
contracts/utils/cryptography/SignerP256.sol
Normal file
59
contracts/utils/cryptography/SignerP256.sol
Normal file
@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {P256} from "./P256.sol";
|
||||
import {AbstractSigner} from "./AbstractSigner.sol";
|
||||
|
||||
/**
|
||||
* @dev Implementation of {AbstractSigner} using xref:api:utils#P256[P256] signatures.
|
||||
*
|
||||
* For {Account} usage, a {_setSigner} function is provided to set the {signer} public key.
|
||||
* Doing so is easier for a factory, who is likely to use initializable clones of this contract.
|
||||
*
|
||||
* Example of usage:
|
||||
*
|
||||
* ```solidity
|
||||
* contract MyAccountP256 is Account, SignerP256, Initializable {
|
||||
* function initialize(bytes32 qx, bytes32 qy) public initializer {
|
||||
* _setSigner(qx, qy);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
|
||||
* or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
|
||||
*/
|
||||
abstract contract SignerP256 is AbstractSigner {
|
||||
bytes32 private _qx;
|
||||
bytes32 private _qy;
|
||||
|
||||
error SignerP256InvalidPublicKey(bytes32 qx, bytes32 qy);
|
||||
|
||||
/**
|
||||
* @dev Sets the signer with a P256 public key. This function should be called during construction
|
||||
* or through an initializer.
|
||||
*/
|
||||
function _setSigner(bytes32 qx, bytes32 qy) internal {
|
||||
if (!P256.isValidPublicKey(qx, qy)) revert SignerP256InvalidPublicKey(qx, qy);
|
||||
_qx = qx;
|
||||
_qy = qy;
|
||||
}
|
||||
|
||||
/// @dev Return the signer's P256 public key.
|
||||
function signer() public view virtual returns (bytes32 qx, bytes32 qy) {
|
||||
return (_qx, _qy);
|
||||
}
|
||||
|
||||
/// @inheritdoc AbstractSigner
|
||||
function _rawSignatureValidation(
|
||||
bytes32 hash,
|
||||
bytes calldata signature
|
||||
) internal view virtual override returns (bool) {
|
||||
if (signature.length < 0x40) return false;
|
||||
bytes32 r = bytes32(signature[0x00:0x20]);
|
||||
bytes32 s = bytes32(signature[0x20:0x40]);
|
||||
(bytes32 qx, bytes32 qy) = signer();
|
||||
return P256.verify(hash, r, s, qx, qy);
|
||||
}
|
||||
}
|
||||
60
contracts/utils/cryptography/SignerRSA.sol
Normal file
60
contracts/utils/cryptography/SignerRSA.sol
Normal file
@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {RSA} from "./RSA.sol";
|
||||
import {AbstractSigner} from "./AbstractSigner.sol";
|
||||
|
||||
/**
|
||||
* @dev Implementation of {AbstractSigner} using xref:api:utils#RSA[RSA] signatures.
|
||||
*
|
||||
* For {Account} usage, a {_setSigner} function is provided to set the {signer} public key.
|
||||
* Doing so is easier for a factory, who is likely to use initializable clones of this contract.
|
||||
*
|
||||
* Example of usage:
|
||||
*
|
||||
* ```solidity
|
||||
* contract MyAccountRSA is Account, SignerRSA, Initializable {
|
||||
* function initialize(bytes memory e, bytes memory n) public initializer {
|
||||
* _setSigner(e, n);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
|
||||
* or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
|
||||
*/
|
||||
abstract contract SignerRSA is AbstractSigner {
|
||||
bytes private _e;
|
||||
bytes private _n;
|
||||
|
||||
/**
|
||||
* @dev Sets the signer with a RSA public key. This function should be called during construction
|
||||
* or through an initializer.
|
||||
*/
|
||||
function _setSigner(bytes memory e, bytes memory n) internal {
|
||||
_e = e;
|
||||
_n = n;
|
||||
}
|
||||
|
||||
/// @dev Return the signer's RSA public key.
|
||||
function signer() public view virtual returns (bytes memory e, bytes memory n) {
|
||||
return (_e, _n);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {AbstractSigner-_rawSignatureValidation}. Verifies a PKCSv1.5 signature by calling
|
||||
* xref:api:utils.adoc#RSA-pkcs1Sha256-bytes-bytes-bytes-bytes-[RSA.pkcs1Sha256].
|
||||
*
|
||||
* IMPORTANT: Following the RSASSA-PKCS1-V1_5-VERIFY procedure outlined in RFC8017 (section 8.2.2), the
|
||||
* provided `hash` is used as the `M` (message) and rehashed using SHA256 according to EMSA-PKCS1-v1_5
|
||||
* encoding as per section 9.2 (step 1) of the RFC.
|
||||
*/
|
||||
function _rawSignatureValidation(
|
||||
bytes32 hash,
|
||||
bytes calldata signature
|
||||
) internal view virtual override returns (bool) {
|
||||
(bytes memory e, bytes memory n) = signer();
|
||||
return RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n);
|
||||
}
|
||||
}
|
||||
@ -334,6 +334,36 @@ index 304d1386a..a1cd63bee 100644
|
||||
-@openzeppelin/contracts/=contracts/
|
||||
+@openzeppelin/contracts-upgradeable/=contracts/
|
||||
+@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
|
||||
diff --git a/test/account/AccountERC7702.test.js b/test/account/AccountERC7702.test.js
|
||||
index d08a52209..7a44bccfe 100644
|
||||
--- a/test/account/AccountERC7702.test.js
|
||||
+++ b/test/account/AccountERC7702.test.js
|
||||
@@ -26,8 +26,8 @@ async function fixture() {
|
||||
|
||||
// domain cannot be fetched using getDomain(mock) before the mock is deployed
|
||||
const domain = {
|
||||
- name: 'AccountERC7702Mock',
|
||||
- version: '1',
|
||||
+ name: '', // Not initialized in the context of signer
|
||||
+ version: '', // Not initialized in the context of signer
|
||||
chainId: entrypointDomain.chainId,
|
||||
verifyingContract: mock.address,
|
||||
};
|
||||
diff --git a/test/account/examples/AccountERC7702WithModulesMock.test.js b/test/account/examples/AccountERC7702WithModulesMock.test.js
|
||||
index 9ee5f9177..f6106bcc7 100644
|
||||
--- a/test/account/examples/AccountERC7702WithModulesMock.test.js
|
||||
+++ b/test/account/examples/AccountERC7702WithModulesMock.test.js
|
||||
@@ -36,8 +36,8 @@ async function fixture() {
|
||||
|
||||
// domain cannot be fetched using getDomain(mock) before the mock is deployed
|
||||
const domain = {
|
||||
- name: 'AccountERC7702WithModulesMock',
|
||||
- version: '1',
|
||||
+ name: '', // Not initialized in the context of signer
|
||||
+ version: '', // Not initialized in the context of signer
|
||||
chainId: entrypointDomain.chainId,
|
||||
verifyingContract: mock.address,
|
||||
};
|
||||
diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js
|
||||
index 2b6e7fa97..268e0d29d 100644
|
||||
--- a/test/utils/cryptography/EIP712.test.js
|
||||
|
||||
144
test/account/Account.behavior.js
Normal file
144
test/account/Account.behavior.js
Normal file
@ -0,0 +1,144 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { impersonate } = require('../helpers/account');
|
||||
const { SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILURE } = require('../helpers/erc4337');
|
||||
const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
function shouldBehaveLikeAccountCore() {
|
||||
describe('entryPoint', function () {
|
||||
it('should return the canonical entrypoint', async function () {
|
||||
await this.mock.deploy();
|
||||
await expect(this.mock.entryPoint()).to.eventually.equal(entrypoint.v08);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateUserOp', function () {
|
||||
beforeEach(async function () {
|
||||
await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') });
|
||||
await this.mock.deploy();
|
||||
this.userOp ??= {};
|
||||
});
|
||||
|
||||
it('should revert if the caller is not the canonical entrypoint', async function () {
|
||||
// empty operation (does nothing)
|
||||
const operation = await this.mock.createUserOp(this.userOp).then(op => this.signUserOp(op));
|
||||
|
||||
await expect(this.mock.connect(this.other).validateUserOp(operation.packed, operation.hash(), 0))
|
||||
.to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
|
||||
describe('when the caller is the canonical entrypoint', function () {
|
||||
beforeEach(async function () {
|
||||
this.mockFromEntrypoint = this.mock.connect(await impersonate(entrypoint.v08.target));
|
||||
});
|
||||
|
||||
it('should return SIG_VALIDATION_SUCCESS if the signature is valid', async function () {
|
||||
// empty operation (does nothing)
|
||||
const operation = await this.mock.createUserOp(this.userOp).then(op => this.signUserOp(op));
|
||||
|
||||
expect(await this.mockFromEntrypoint.validateUserOp.staticCall(operation.packed, operation.hash(), 0)).to.eq(
|
||||
SIG_VALIDATION_SUCCESS,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return SIG_VALIDATION_FAILURE if the signature is invalid', async function () {
|
||||
// empty operation (does nothing)
|
||||
const operation = await this.mock.createUserOp(this.userOp);
|
||||
operation.signature = (await this.invalidSig?.()) ?? '0x00';
|
||||
|
||||
expect(await this.mockFromEntrypoint.validateUserOp.staticCall(operation.packed, operation.hash(), 0)).to.eq(
|
||||
SIG_VALIDATION_FAILURE,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pay missing account funds for execution', async function () {
|
||||
// empty operation (does nothing)
|
||||
const operation = await this.mock.createUserOp(this.userOp).then(op => this.signUserOp(op));
|
||||
const value = 42n;
|
||||
|
||||
await expect(
|
||||
this.mockFromEntrypoint.validateUserOp(operation.packed, operation.hash(), value),
|
||||
).to.changeEtherBalances([this.mock, entrypoint.v08], [-value, value]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fallback', function () {
|
||||
it('should receive ether', async function () {
|
||||
await this.mock.deploy();
|
||||
const value = 42n;
|
||||
|
||||
await expect(this.other.sendTransaction({ to: this.mock, value })).to.changeEtherBalances(
|
||||
[this.other, this.mock],
|
||||
[-value, value],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeAccountHolder() {
|
||||
describe('onReceived', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mock.deploy();
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC1155Receiver']);
|
||||
|
||||
describe('onERC1155Received', function () {
|
||||
const ids = [1n, 2n, 3n];
|
||||
const values = [1000n, 2000n, 3000n];
|
||||
const data = '0x12345678';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ethers.deployContract('$ERC1155', ['https://somedomain.com/{id}.json']);
|
||||
await this.token.$_mintBatch(this.other, ids, values, '0x');
|
||||
});
|
||||
|
||||
it('receives ERC1155 tokens from a single ID', async function () {
|
||||
await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, ids[0], values[0], data);
|
||||
|
||||
await expect(
|
||||
this.token.balanceOfBatch(
|
||||
ids.map(() => this.mock),
|
||||
ids,
|
||||
),
|
||||
).to.eventually.deep.equal(values.map((v, i) => (i == 0 ? v : 0n)));
|
||||
});
|
||||
|
||||
it('receives ERC1155 tokens from a multiple IDs', async function () {
|
||||
await expect(
|
||||
this.token.balanceOfBatch(
|
||||
ids.map(() => this.mock),
|
||||
ids,
|
||||
),
|
||||
).to.eventually.deep.equal(ids.map(() => 0n));
|
||||
|
||||
await this.token.connect(this.other).safeBatchTransferFrom(this.other, this.mock, ids, values, data);
|
||||
await expect(
|
||||
this.token.balanceOfBatch(
|
||||
ids.map(() => this.mock),
|
||||
ids,
|
||||
),
|
||||
).to.eventually.deep.equal(values);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onERC721Received', function () {
|
||||
const tokenId = 1n;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ethers.deployContract('$ERC721', ['Some NFT', 'SNFT']);
|
||||
await this.token.$_mint(this.other, tokenId);
|
||||
});
|
||||
|
||||
it('receives an ERC721 token', async function () {
|
||||
await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, tokenId);
|
||||
|
||||
await expect(this.token.ownerOf(tokenId)).to.eventually.equal(this.mock);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder };
|
||||
48
test/account/Account.test.js
Normal file
48
test/account/Account.test.js
Normal file
@ -0,0 +1,48 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../helpers/erc4337');
|
||||
const { PackedUserOperation } = require('../helpers/eip712-types');
|
||||
const { NonNativeSigner } = require('../helpers/signers');
|
||||
|
||||
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
|
||||
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [beneficiary, other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// ERC-4337 signer
|
||||
const signer = new NonNativeSigner({ sign: hash => ({ serialized: hash }) });
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountMock', ['Account', '1']);
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
// domain cannot be fetched using getDomain(mock) before the mock is deployed
|
||||
const domain = { name: 'Account', version: '1', chainId: entrypointDomain.chainId, verifyingContract: mock.address };
|
||||
|
||||
const signUserOp = async userOp =>
|
||||
signer
|
||||
.signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
|
||||
}
|
||||
|
||||
describe('Account', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountHolder();
|
||||
shouldBehaveLikeERC1271({ erc7739: true });
|
||||
shouldBehaveLikeERC7821();
|
||||
});
|
||||
52
test/account/AccountECDSA.test.js
Normal file
52
test/account/AccountECDSA.test.js
Normal file
@ -0,0 +1,52 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../helpers/erc4337');
|
||||
const { PackedUserOperation } = require('../helpers/eip712-types');
|
||||
|
||||
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
|
||||
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [beneficiary, other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// ERC-4337 signer
|
||||
const signer = ethers.Wallet.createRandom();
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountECDSAMock', ['AccountECDSA', '1', signer]);
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
// domain cannot be fetched using getDomain(mock) before the mock is deployed
|
||||
const domain = {
|
||||
name: 'AccountECDSA',
|
||||
version: '1',
|
||||
chainId: entrypointDomain.chainId,
|
||||
verifyingContract: mock.address,
|
||||
};
|
||||
|
||||
const signUserOp = userOp =>
|
||||
signer
|
||||
.signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
|
||||
}
|
||||
|
||||
describe('AccountECDSA', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountHolder();
|
||||
shouldBehaveLikeERC1271({ erc7739: true });
|
||||
shouldBehaveLikeERC7821();
|
||||
});
|
||||
97
test/account/AccountERC7702.t.sol
Normal file
97
test/account/AccountERC7702.t.sol
Normal file
@ -0,0 +1,97 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {AccountERC7702Mock} from "@openzeppelin/contracts/mocks/account/AccountMock.sol";
|
||||
import {CallReceiverMock} from "@openzeppelin/contracts/mocks/CallReceiverMock.sol";
|
||||
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
||||
import {ERC7579Utils, Execution, Mode, ModeSelector, ModePayload} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
|
||||
import {ERC4337Utils, IEntryPointExtra} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
|
||||
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
|
||||
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
|
||||
import {ERC7821} from "@openzeppelin/contracts/account/extensions/ERC7821.sol";
|
||||
|
||||
contract AccountERC7702MockConstructor is AccountERC7702Mock {
|
||||
constructor() EIP712("MyAccount", "1") {}
|
||||
}
|
||||
|
||||
contract AccountERC7702Test is Test {
|
||||
using ERC7579Utils for *;
|
||||
using ERC4337Utils for PackedUserOperation;
|
||||
using Strings for *;
|
||||
|
||||
uint256 private constant MAX_ETH = type(uint128).max;
|
||||
|
||||
// Test accounts
|
||||
CallReceiverMock private _target;
|
||||
|
||||
// ERC-4337 signer
|
||||
uint256 private _signerPrivateKey;
|
||||
AccountERC7702MockConstructor private _signer;
|
||||
|
||||
function setUp() public {
|
||||
// Deploy target contract
|
||||
_target = new CallReceiverMock();
|
||||
|
||||
// Setup signer
|
||||
_signerPrivateKey = 0x1234;
|
||||
_signer = AccountERC7702MockConstructor(payable(vm.addr(_signerPrivateKey)));
|
||||
vm.deal(address(_signer), MAX_ETH);
|
||||
|
||||
// Sign and attach delegation
|
||||
vm.signAndAttachDelegation(address(new AccountERC7702MockConstructor()), _signerPrivateKey);
|
||||
|
||||
// Setup entrypoint
|
||||
vm.deal(address(ERC4337Utils.ENTRYPOINT_V08), MAX_ETH);
|
||||
vm.etch(address(ERC4337Utils.ENTRYPOINT_V08), vm.readFileBinary("test/bin/EntryPoint070.bytecode"));
|
||||
}
|
||||
|
||||
function testExecuteBatch(uint256 argA, uint256 argB) public {
|
||||
// Create the mode for batch execution
|
||||
Mode mode = ERC7579Utils.CALLTYPE_BATCH.encodeMode(
|
||||
ERC7579Utils.EXECTYPE_DEFAULT,
|
||||
ModeSelector.wrap(0x00000000),
|
||||
ModePayload.wrap(0x00000000)
|
||||
);
|
||||
|
||||
Execution[] memory execution = new Execution[](2);
|
||||
execution[0] = Execution({
|
||||
target: address(_target),
|
||||
value: 1 ether,
|
||||
callData: abi.encodeCall(CallReceiverMock.mockFunctionExtra, ())
|
||||
});
|
||||
execution[1] = Execution({
|
||||
target: address(_target),
|
||||
value: 0,
|
||||
callData: abi.encodeCall(CallReceiverMock.mockFunctionWithArgs, (argA, argB))
|
||||
});
|
||||
|
||||
// Pack the batch within a PackedUserOperation
|
||||
PackedUserOperation[] memory ops = new PackedUserOperation[](1);
|
||||
ops[0] = PackedUserOperation({
|
||||
sender: address(_signer),
|
||||
nonce: 0,
|
||||
initCode: bytes(""),
|
||||
callData: abi.encodeCall(ERC7821.execute, (Mode.unwrap(mode), execution.encodeBatch())),
|
||||
preVerificationGas: 100000,
|
||||
accountGasLimits: bytes32(abi.encodePacked(uint128(100000), uint128(100000))),
|
||||
gasFees: bytes32(abi.encodePacked(uint128(1000000), uint128(1000000))),
|
||||
paymasterAndData: bytes(""),
|
||||
signature: bytes("")
|
||||
});
|
||||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
|
||||
_signerPrivateKey,
|
||||
IEntryPointExtra(address(ERC4337Utils.ENTRYPOINT_V08)).getUserOpHash(ops[0])
|
||||
);
|
||||
ops[0].signature = abi.encodePacked(r, s, v);
|
||||
|
||||
// Expect the events to be emitted
|
||||
vm.expectEmit(true, true, true, true);
|
||||
emit CallReceiverMock.MockFunctionCalledExtra(address(_signer), 1 ether);
|
||||
vm.expectEmit(true, true, true, true);
|
||||
emit CallReceiverMock.MockFunctionCalledWithArgs(argA, argB);
|
||||
|
||||
// Execute the batch
|
||||
_signer.entryPoint().handleOps(ops, payable(makeAddr("beneficiary")));
|
||||
}
|
||||
}
|
||||
52
test/account/AccountERC7702.test.js
Normal file
52
test/account/AccountERC7702.test.js
Normal file
@ -0,0 +1,52 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../helpers/erc4337');
|
||||
const { PackedUserOperation } = require('../helpers/eip712-types');
|
||||
|
||||
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
|
||||
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [beneficiary, other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// ERC-4337 signer
|
||||
const signer = ethers.Wallet.createRandom(ethers.provider);
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountERC7702Mock', ['AccountERC7702Mock', '1'], { erc7702signer: signer });
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
// domain cannot be fetched using getDomain(mock) before the mock is deployed
|
||||
const domain = {
|
||||
name: 'AccountERC7702Mock',
|
||||
version: '1',
|
||||
chainId: entrypointDomain.chainId,
|
||||
verifyingContract: mock.address,
|
||||
};
|
||||
|
||||
const signUserOp = userOp =>
|
||||
signer
|
||||
.signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
|
||||
}
|
||||
|
||||
describe('AccountERC7702', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountHolder();
|
||||
shouldBehaveLikeERC1271({ erc7739: true });
|
||||
shouldBehaveLikeERC7821({ deployable: false });
|
||||
});
|
||||
58
test/account/AccountP256.test.js
Normal file
58
test/account/AccountP256.test.js
Normal file
@ -0,0 +1,58 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../helpers/erc4337');
|
||||
const { NonNativeSigner, P256SigningKey } = require('../helpers/signers');
|
||||
const { PackedUserOperation } = require('../helpers/eip712-types');
|
||||
|
||||
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
|
||||
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [beneficiary, other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// ERC-4337 signer
|
||||
const signer = new NonNativeSigner(P256SigningKey.random());
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountP256Mock', [
|
||||
'AccountP256',
|
||||
'1',
|
||||
signer.signingKey.publicKey.qx,
|
||||
signer.signingKey.publicKey.qy,
|
||||
]);
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
// domain cannot be fetched using getDomain(mock) before the mock is deployed
|
||||
const domain = {
|
||||
name: 'AccountP256',
|
||||
version: '1',
|
||||
chainId: entrypointDomain.chainId,
|
||||
verifyingContract: mock.address,
|
||||
};
|
||||
|
||||
const signUserOp = userOp =>
|
||||
signer
|
||||
.signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
|
||||
}
|
||||
|
||||
describe('AccountP256', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountHolder();
|
||||
shouldBehaveLikeERC1271({ erc7739: true });
|
||||
shouldBehaveLikeERC7821();
|
||||
});
|
||||
58
test/account/AccountRSA.test.js
Normal file
58
test/account/AccountRSA.test.js
Normal file
@ -0,0 +1,58 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../helpers/erc4337');
|
||||
const { NonNativeSigner, RSASHA256SigningKey } = require('../helpers/signers');
|
||||
const { PackedUserOperation } = require('../helpers/eip712-types');
|
||||
|
||||
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
|
||||
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [beneficiary, other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// ERC-4337 signer
|
||||
const signer = new NonNativeSigner(RSASHA256SigningKey.random());
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountRSAMock', [
|
||||
'AccountRSA',
|
||||
'1',
|
||||
signer.signingKey.publicKey.e,
|
||||
signer.signingKey.publicKey.n,
|
||||
]);
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
// domain cannot be fetched using getDomain(mock) before the mock is deployed
|
||||
const domain = {
|
||||
name: 'AccountRSA',
|
||||
version: '1',
|
||||
chainId: entrypointDomain.chainId,
|
||||
verifyingContract: mock.address,
|
||||
};
|
||||
|
||||
const signUserOp = userOp =>
|
||||
signer
|
||||
.signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
|
||||
}
|
||||
|
||||
describe('AccountRSA', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountHolder();
|
||||
shouldBehaveLikeERC1271({ erc7739: true });
|
||||
shouldBehaveLikeERC7821();
|
||||
});
|
||||
99
test/account/examples/AccountERC7702WithModulesMock.test.js
Normal file
99
test/account/examples/AccountERC7702WithModulesMock.test.js
Normal file
@ -0,0 +1,99 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../../helpers/erc4337');
|
||||
const { PackedUserOperation } = require('../../helpers/eip712-types');
|
||||
|
||||
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('../Account.behavior');
|
||||
const { shouldBehaveLikeAccountERC7579 } = require('../extensions/AccountERC7579.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../../utils/cryptography/ERC1271.behavior');
|
||||
const { shouldBehaveLikeERC7821 } = require('../extensions/ERC7821.behavior');
|
||||
|
||||
const { MODULE_TYPE_VALIDATOR } = require('../../helpers/erc7579');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [beneficiary, other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
const anotherTarget = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// Signer with EIP-7702 support + funding
|
||||
const eoa = ethers.Wallet.createRandom(ethers.provider);
|
||||
await setBalance(eoa.address, ethers.WeiPerEther);
|
||||
|
||||
// ERC-7579 validator module
|
||||
const validator = await ethers.deployContract('$ERC7579ValidatorMock');
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountERC7702WithModulesMock', ['AccountERC7702WithModulesMock', '1'], {
|
||||
erc7702signer: eoa,
|
||||
});
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
// domain cannot be fetched using getDomain(mock) before the mock is deployed
|
||||
const domain = {
|
||||
name: 'AccountERC7702WithModulesMock',
|
||||
version: '1',
|
||||
chainId: entrypointDomain.chainId,
|
||||
verifyingContract: mock.address,
|
||||
};
|
||||
|
||||
return { helper, validator, mock, domain, entrypointDomain, eoa, target, anotherTarget, beneficiary, other };
|
||||
}
|
||||
|
||||
describe('AccountERC7702WithModules: ERC-7702 account with ERC-7579 modules supports', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
describe('using ERC-7702 signer', function () {
|
||||
beforeEach(async function () {
|
||||
this.signer = this.eoa;
|
||||
this.signUserOp = userOp =>
|
||||
this.signer
|
||||
.signTypedData(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountHolder();
|
||||
shouldBehaveLikeERC7821({ deployable: false });
|
||||
shouldBehaveLikeERC1271({ erc7739: true });
|
||||
});
|
||||
|
||||
describe('using ERC-7579 validator', function () {
|
||||
beforeEach(async function () {
|
||||
// signer that adds a prefix to all signatures (except the userOp ones)
|
||||
this.signer = ethers.Wallet.createRandom();
|
||||
this.signer.signMessage = message =>
|
||||
ethers.Wallet.prototype.signMessage
|
||||
.bind(this.signer)(message)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
this.signer.signTypedData = (domain, types, values) =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(domain, types, values)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
|
||||
this.signUserOp = userOp =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
// Use the first 20 bytes from the nonce key (24 bytes) to identify the validator module
|
||||
this.userOp = { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) };
|
||||
|
||||
// Deploy (using ERC-7702) and add the validator module using EOA
|
||||
await this.mock.deploy();
|
||||
await this.mock.connect(this.eoa).installModule(MODULE_TYPE_VALIDATOR, this.validator, this.signer.address);
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountHolder();
|
||||
shouldBehaveLikeAccountERC7579();
|
||||
shouldBehaveLikeERC1271({ erc7739: false });
|
||||
});
|
||||
});
|
||||
563
test/account/extensions/AccountERC7579.behavior.js
Normal file
563
test/account/extensions/AccountERC7579.behavior.js
Normal file
@ -0,0 +1,563 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { impersonate } = require('../../helpers/account');
|
||||
const { selector } = require('../../helpers/methods');
|
||||
const { zip } = require('../../helpers/iterate');
|
||||
const {
|
||||
encodeMode,
|
||||
encodeBatch,
|
||||
encodeSingle,
|
||||
encodeDelegate,
|
||||
MODULE_TYPE_VALIDATOR,
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
MODULE_TYPE_FALLBACK,
|
||||
MODULE_TYPE_HOOK,
|
||||
CALL_TYPE_CALL,
|
||||
CALL_TYPE_BATCH,
|
||||
CALL_TYPE_DELEGATE,
|
||||
EXEC_TYPE_DEFAULT,
|
||||
EXEC_TYPE_TRY,
|
||||
} = require('../../helpers/erc7579');
|
||||
|
||||
const CALL_TYPE_INVALID = '0x42';
|
||||
const EXEC_TYPE_INVALID = '0x17';
|
||||
const MODULE_TYPE_INVALID = 999n;
|
||||
|
||||
const coder = ethers.AbiCoder.defaultAbiCoder();
|
||||
|
||||
function shouldBehaveLikeAccountERC7579({ withHooks = false } = {}) {
|
||||
describe('AccountERC7579', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mock.deploy();
|
||||
await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') });
|
||||
|
||||
this.modules = {};
|
||||
this.modules[MODULE_TYPE_VALIDATOR] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_VALIDATOR]);
|
||||
this.modules[MODULE_TYPE_EXECUTOR] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_EXECUTOR]);
|
||||
this.modules[MODULE_TYPE_FALLBACK] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_FALLBACK]);
|
||||
this.modules[MODULE_TYPE_HOOK] = await ethers.deployContract('$ERC7579HookMock');
|
||||
|
||||
this.mockFromEntrypoint = this.mock.connect(await impersonate(entrypoint.v08.target));
|
||||
this.mockFromExecutor = this.mock.connect(await impersonate(this.modules[MODULE_TYPE_EXECUTOR].target));
|
||||
});
|
||||
|
||||
describe('accountId', function () {
|
||||
it('should return the account ID', async function () {
|
||||
await expect(this.mock.accountId()).to.eventually.equal(
|
||||
withHooks
|
||||
? '@openzeppelin/community-contracts.AccountERC7579Hooked.v0.0.0'
|
||||
: '@openzeppelin/community-contracts.AccountERC7579.v0.0.0',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('supportsExecutionMode', function () {
|
||||
for (const [callType, execType] of zip(
|
||||
[CALL_TYPE_CALL, CALL_TYPE_BATCH, CALL_TYPE_DELEGATE, CALL_TYPE_INVALID],
|
||||
[EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY, EXEC_TYPE_INVALID],
|
||||
)) {
|
||||
const result = callType != CALL_TYPE_INVALID && execType != EXEC_TYPE_INVALID;
|
||||
|
||||
it(`${
|
||||
result ? 'does not support' : 'supports'
|
||||
} CALL_TYPE=${callType} and EXEC_TYPE=${execType} execution mode`, async function () {
|
||||
await expect(this.mock.supportsExecutionMode(encodeMode({ callType, execType }))).to.eventually.equal(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('supportsModule', function () {
|
||||
it('supports MODULE_TYPE_VALIDATOR module type', async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_VALIDATOR)).to.eventually.equal(true);
|
||||
});
|
||||
|
||||
it('supports MODULE_TYPE_EXECUTOR module type', async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_EXECUTOR)).to.eventually.equal(true);
|
||||
});
|
||||
|
||||
it('supports MODULE_TYPE_FALLBACK module type', async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_FALLBACK)).to.eventually.equal(true);
|
||||
});
|
||||
|
||||
it(
|
||||
withHooks ? 'supports MODULE_TYPE_HOOK module type' : 'does not support MODULE_TYPE_HOOK module type',
|
||||
async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_HOOK)).to.eventually.equal(withHooks);
|
||||
},
|
||||
);
|
||||
|
||||
it('does not support invalid module type', async function () {
|
||||
await expect(this.mock.supportsModule(MODULE_TYPE_INVALID)).to.eventually.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('module installation', function () {
|
||||
it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
|
||||
await expect(this.mock.connect(this.other).installModule(MODULE_TYPE_VALIDATOR, this.mock, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
|
||||
it('should revert if the module type is not supported', async function () {
|
||||
await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_INVALID, this.mock, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedModuleType')
|
||||
.withArgs(MODULE_TYPE_INVALID);
|
||||
});
|
||||
|
||||
it('should revert if the module is not the provided type', async function () {
|
||||
const instance = this.modules[MODULE_TYPE_EXECUTOR];
|
||||
await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_VALIDATOR, instance, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579MismatchedModuleTypeId')
|
||||
.withArgs(MODULE_TYPE_VALIDATOR, instance);
|
||||
});
|
||||
|
||||
for (const moduleTypeId of [
|
||||
MODULE_TYPE_VALIDATOR,
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
MODULE_TYPE_FALLBACK,
|
||||
withHooks && MODULE_TYPE_HOOK,
|
||||
].filter(Boolean)) {
|
||||
const prefix = moduleTypeId == MODULE_TYPE_FALLBACK ? '0x12345678' : '0x';
|
||||
const initData = ethers.hexlify(ethers.randomBytes(256));
|
||||
const fullData = ethers.concat([prefix, initData]);
|
||||
|
||||
it(`should install a module of type ${moduleTypeId}`, async function () {
|
||||
const instance = this.modules[moduleTypeId];
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(false);
|
||||
|
||||
await expect(this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData))
|
||||
.to.emit(this.mock, 'ModuleInstalled')
|
||||
.withArgs(moduleTypeId, instance)
|
||||
.to.emit(instance, 'ModuleInstalledReceived')
|
||||
.withArgs(this.mock, initData); // After decoding MODULE_TYPE_FALLBACK, it should remove the fnSig
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
|
||||
});
|
||||
|
||||
it(`does not allow to install a module of ${moduleTypeId} id twice`, async function () {
|
||||
const instance = this.modules[moduleTypeId];
|
||||
|
||||
await this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData);
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
|
||||
|
||||
await expect(this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData))
|
||||
.to.be.revertedWithCustomError(
|
||||
this.mock,
|
||||
moduleTypeId == MODULE_TYPE_HOOK ? 'ERC7579HookModuleAlreadyPresent' : 'ERC7579AlreadyInstalledModule',
|
||||
)
|
||||
.withArgs(...[moduleTypeId != MODULE_TYPE_HOOK && moduleTypeId, instance].filter(Boolean));
|
||||
});
|
||||
}
|
||||
|
||||
withHooks &&
|
||||
describe('with hook', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
|
||||
});
|
||||
|
||||
it('should call the hook of the installed module when performing an module install', async function () {
|
||||
const instance = this.modules[MODULE_TYPE_EXECUTOR];
|
||||
const initData = ethers.hexlify(ethers.randomBytes(256));
|
||||
|
||||
const precheckData = this.mock.interface.encodeFunctionData('installModule', [
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
instance.target,
|
||||
initData,
|
||||
]);
|
||||
|
||||
await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_EXECUTOR, instance, initData))
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
|
||||
.withArgs(entrypoint.v08, 0n, precheckData)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
|
||||
.withArgs(precheckData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('module uninstallation', function () {
|
||||
it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
|
||||
await expect(this.mock.connect(this.other).uninstallModule(MODULE_TYPE_VALIDATOR, this.mock, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
|
||||
it('should revert if the module type is not supported', async function () {
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_INVALID, this.mock, '0x'))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedModuleType')
|
||||
.withArgs(MODULE_TYPE_INVALID);
|
||||
});
|
||||
|
||||
for (const moduleTypeId of [
|
||||
MODULE_TYPE_VALIDATOR,
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
MODULE_TYPE_FALLBACK,
|
||||
withHooks && MODULE_TYPE_HOOK,
|
||||
].filter(Boolean)) {
|
||||
const prefix = moduleTypeId == MODULE_TYPE_FALLBACK ? '0x12345678' : '0x';
|
||||
const initData = ethers.hexlify(ethers.randomBytes(256));
|
||||
const fullData = ethers.concat([prefix, initData]);
|
||||
|
||||
it(`should uninstall a module of type ${moduleTypeId}`, async function () {
|
||||
const instance = this.modules[moduleTypeId];
|
||||
|
||||
await this.mock.$_installModule(moduleTypeId, instance, fullData);
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
|
||||
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(moduleTypeId, instance, fullData))
|
||||
.to.emit(this.mock, 'ModuleUninstalled')
|
||||
.withArgs(moduleTypeId, instance)
|
||||
.to.emit(instance, 'ModuleUninstalledReceived')
|
||||
.withArgs(this.mock, initData); // After decoding MODULE_TYPE_FALLBACK, it should remove the fnSig
|
||||
|
||||
await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(false);
|
||||
});
|
||||
|
||||
it(`should revert uninstalling a module of type ${moduleTypeId} if it was not installed`, async function () {
|
||||
const instance = this.modules[moduleTypeId];
|
||||
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(moduleTypeId, instance, fullData))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UninstalledModule')
|
||||
.withArgs(moduleTypeId, instance);
|
||||
});
|
||||
}
|
||||
|
||||
it('should revert uninstalling a module of type MODULE_TYPE_FALLBACK if a different module was installed for the provided selector', async function () {
|
||||
const instance = this.modules[MODULE_TYPE_FALLBACK];
|
||||
const anotherInstance = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_FALLBACK]);
|
||||
const initData = '0x12345678abcdef';
|
||||
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_FALLBACK, instance, initData);
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_FALLBACK, anotherInstance, initData))
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UninstalledModule')
|
||||
.withArgs(MODULE_TYPE_FALLBACK, anotherInstance);
|
||||
});
|
||||
|
||||
withHooks &&
|
||||
describe('with hook', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
|
||||
});
|
||||
|
||||
it('should call the hook of the installed module when performing a module uninstall', async function () {
|
||||
const instance = this.modules[MODULE_TYPE_EXECUTOR];
|
||||
const initData = ethers.hexlify(ethers.randomBytes(256));
|
||||
|
||||
const precheckData = this.mock.interface.encodeFunctionData('uninstallModule', [
|
||||
MODULE_TYPE_EXECUTOR,
|
||||
instance.target,
|
||||
initData,
|
||||
]);
|
||||
|
||||
await this.mock.$_installModule(MODULE_TYPE_EXECUTOR, instance, initData);
|
||||
await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_EXECUTOR, instance, initData))
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
|
||||
.withArgs(entrypoint.v08, 0n, precheckData)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
|
||||
.withArgs(precheckData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mock.$_installModule(MODULE_TYPE_EXECUTOR, this.modules[MODULE_TYPE_EXECUTOR], '0x');
|
||||
});
|
||||
|
||||
for (const [execFn, mock] of [
|
||||
['execute', 'mockFromEntrypoint'],
|
||||
['executeFromExecutor', 'mockFromExecutor'],
|
||||
]) {
|
||||
describe(`executing with ${execFn}`, function () {
|
||||
it('should revert if the call type is not supported', async function () {
|
||||
await expect(
|
||||
this[mock][execFn](encodeMode({ callType: CALL_TYPE_INVALID }), encodeSingle(this.other, 0, '0x')),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedCallType')
|
||||
.withArgs(ethers.solidityPacked(['bytes1'], [CALL_TYPE_INVALID]));
|
||||
});
|
||||
|
||||
it('should revert if the caller is not authorized / installed', async function () {
|
||||
const error = execFn == 'execute' ? 'AccountUnauthorized' : 'ERC7579UninstalledModule';
|
||||
const args = execFn == 'execute' ? [this.other] : [MODULE_TYPE_EXECUTOR, this.other];
|
||||
|
||||
await expect(
|
||||
this[mock]
|
||||
.connect(this.other)
|
||||
[execFn](encodeMode({ callType: CALL_TYPE_CALL }), encodeSingle(this.other, 0, '0x')),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.mock, error)
|
||||
.withArgs(...args);
|
||||
});
|
||||
|
||||
describe('single execution', function () {
|
||||
it('calls the target with value and args', async function () {
|
||||
const value = 0x432;
|
||||
const data = encodeSingle(
|
||||
this.target,
|
||||
value,
|
||||
this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
|
||||
);
|
||||
|
||||
const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL }), data);
|
||||
|
||||
await expect(tx).to.emit(this.target, 'MockFunctionCalledWithArgs').withArgs(42, '0x1234');
|
||||
await expect(tx).to.changeEtherBalances([this.mock, this.target], [-value, value]);
|
||||
});
|
||||
|
||||
it('reverts when target reverts in default ExecType', async function () {
|
||||
const value = 0x012;
|
||||
const data = encodeSingle(
|
||||
this.target,
|
||||
value,
|
||||
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
);
|
||||
|
||||
await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL }), data)).to.be.revertedWith(
|
||||
'CallReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
|
||||
it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
|
||||
const value = 0x012;
|
||||
const data = encodeSingle(
|
||||
this.target,
|
||||
value,
|
||||
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
);
|
||||
|
||||
await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL, execType: EXEC_TYPE_TRY }), data))
|
||||
.to.emit(this.mock, 'ERC7579TryExecuteFail')
|
||||
.withArgs(
|
||||
CALL_TYPE_CALL,
|
||||
ethers.solidityPacked(
|
||||
['bytes4', 'bytes'],
|
||||
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batch execution', function () {
|
||||
it('calls the targets with value and args', async function () {
|
||||
const value1 = 0x012;
|
||||
const value2 = 0x234;
|
||||
const data = encodeBatch(
|
||||
[this.target, value1, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234'])],
|
||||
[
|
||||
this.anotherTarget,
|
||||
value2,
|
||||
this.anotherTarget.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
|
||||
],
|
||||
);
|
||||
|
||||
const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH }), data);
|
||||
await expect(tx)
|
||||
.to.emit(this.target, 'MockFunctionCalledWithArgs')
|
||||
.to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs');
|
||||
await expect(tx).to.changeEtherBalances(
|
||||
[this.mock, this.target, this.anotherTarget],
|
||||
[-value1 - value2, value1, value2],
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when any target reverts in default ExecType', async function () {
|
||||
const value1 = 0x012;
|
||||
const value2 = 0x234;
|
||||
const data = encodeBatch(
|
||||
[this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
|
||||
[
|
||||
this.anotherTarget,
|
||||
value2,
|
||||
this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
],
|
||||
);
|
||||
|
||||
await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH }), data)).to.be.revertedWith(
|
||||
'CallReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
|
||||
it('emits ERC7579TryExecuteFail event when any target reverts in try ExecType', async function () {
|
||||
const value1 = 0x012;
|
||||
const value2 = 0x234;
|
||||
const data = encodeBatch(
|
||||
[this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
|
||||
[
|
||||
this.anotherTarget,
|
||||
value2,
|
||||
this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
],
|
||||
);
|
||||
|
||||
const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH, execType: EXEC_TYPE_TRY }), data);
|
||||
|
||||
await expect(tx)
|
||||
.to.emit(this.mock, 'ERC7579TryExecuteFail')
|
||||
.withArgs(
|
||||
CALL_TYPE_BATCH,
|
||||
ethers.solidityPacked(
|
||||
['bytes4', 'bytes'],
|
||||
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
|
||||
),
|
||||
);
|
||||
|
||||
await expect(tx).to.changeEtherBalances(
|
||||
[this.mock, this.target, this.anotherTarget],
|
||||
[-value1, value1, 0],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delegate call execution', function () {
|
||||
it('delegate calls the target', async function () {
|
||||
const slot = ethers.hexlify(ethers.randomBytes(32));
|
||||
const value = ethers.hexlify(ethers.randomBytes(32));
|
||||
const data = encodeDelegate(
|
||||
this.target,
|
||||
this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]),
|
||||
);
|
||||
|
||||
await expect(ethers.provider.getStorage(this.mock.target, slot)).to.eventually.equal(ethers.ZeroHash);
|
||||
await this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE }), data);
|
||||
await expect(ethers.provider.getStorage(this.mock.target, slot)).to.eventually.equal(value);
|
||||
});
|
||||
|
||||
it('reverts when target reverts in default ExecType', async function () {
|
||||
const data = encodeDelegate(
|
||||
this.target,
|
||||
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
);
|
||||
await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE }), data)).to.be.revertedWith(
|
||||
'CallReceiverMock: reverting',
|
||||
);
|
||||
});
|
||||
|
||||
it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
|
||||
const data = encodeDelegate(
|
||||
this.target,
|
||||
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
|
||||
);
|
||||
await expect(
|
||||
this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE, execType: EXEC_TYPE_TRY }), data),
|
||||
)
|
||||
.to.emit(this.mock, 'ERC7579TryExecuteFail')
|
||||
.withArgs(
|
||||
CALL_TYPE_CALL,
|
||||
ethers.solidityPacked(
|
||||
['bytes4', 'bytes'],
|
||||
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
withHooks &&
|
||||
describe('with hook', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
|
||||
});
|
||||
|
||||
it(`should call the hook of the installed module when executing ${execFn}`, async function () {
|
||||
const caller = execFn === 'execute' ? entrypoint.v08 : this.modules[MODULE_TYPE_EXECUTOR];
|
||||
const value = 17;
|
||||
const data = this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']);
|
||||
|
||||
const mode = encodeMode({ callType: CALL_TYPE_CALL });
|
||||
const call = encodeSingle(this.target, value, data);
|
||||
const precheckData = this[mock].interface.encodeFunctionData(execFn, [mode, call]);
|
||||
|
||||
const tx = this[mock][execFn](mode, call, { value });
|
||||
|
||||
await expect(tx)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
|
||||
.withArgs(caller, value, precheckData)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
|
||||
.withArgs(precheckData);
|
||||
await expect(tx).to.changeEtherBalances([caller, this.mock, this.target], [-value, 0n, value]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('fallback', function () {
|
||||
beforeEach(async function () {
|
||||
this.fallbackHandler = await ethers.deployContract('$ERC7579FallbackHandlerMock');
|
||||
});
|
||||
|
||||
it('reverts if there is no fallback module installed', async function () {
|
||||
const { selector } = this.fallbackHandler.callPayable.getFragment();
|
||||
|
||||
await expect(this.fallbackHandler.attach(this.mock).callPayable())
|
||||
.to.be.revertedWithCustomError(this.mock, 'ERC7579MissingFallbackHandler')
|
||||
.withArgs(selector);
|
||||
});
|
||||
|
||||
describe('with a fallback module installed', function () {
|
||||
beforeEach(async function () {
|
||||
await Promise.all(
|
||||
[
|
||||
this.fallbackHandler.callPayable.getFragment().selector,
|
||||
this.fallbackHandler.callView.getFragment().selector,
|
||||
this.fallbackHandler.callRevert.getFragment().selector,
|
||||
].map(selector =>
|
||||
this.mock.$_installModule(
|
||||
MODULE_TYPE_FALLBACK,
|
||||
this.fallbackHandler,
|
||||
coder.encode(['bytes4', 'bytes'], [selector, '0x']),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards the call to the fallback handler', async function () {
|
||||
const calldata = this.fallbackHandler.interface.encodeFunctionData('callPayable');
|
||||
const value = 17n;
|
||||
|
||||
await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callPayable({ value }))
|
||||
.to.emit(this.fallbackHandler, 'ERC7579FallbackHandlerMockCalled')
|
||||
.withArgs(this.mock, this.other, value, calldata);
|
||||
});
|
||||
|
||||
it('returns answer from the fallback handler', async function () {
|
||||
await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callView()).to.eventually.deep.equal([
|
||||
this.mock.target,
|
||||
this.other.address,
|
||||
]);
|
||||
});
|
||||
|
||||
it('bubble up reverts from the fallback handler', async function () {
|
||||
await expect(
|
||||
this.fallbackHandler.attach(this.mock).connect(this.other).callRevert(),
|
||||
).to.be.revertedWithCustomError(this.fallbackHandler, 'ERC7579FallbackHandlerMockRevert');
|
||||
});
|
||||
|
||||
withHooks &&
|
||||
describe('with hook', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
|
||||
});
|
||||
|
||||
it('should call the hook of the installed module when performing a callback', async function () {
|
||||
const precheckData = this.fallbackHandler.interface.encodeFunctionData('callPayable');
|
||||
const value = 17n;
|
||||
|
||||
// call with interface: decode returned data
|
||||
await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callPayable({ value }))
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
|
||||
.withArgs(this.other, value, precheckData)
|
||||
.to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
|
||||
.withArgs(precheckData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeAccountERC7579,
|
||||
};
|
||||
60
test/account/extensions/AccountERC7579.test.js
Normal file
60
test/account/extensions/AccountERC7579.test.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../../helpers/erc4337');
|
||||
const { PackedUserOperation } = require('../../helpers/eip712-types');
|
||||
|
||||
const { shouldBehaveLikeAccountCore } = require('../Account.behavior');
|
||||
const { shouldBehaveLikeAccountERC7579 } = require('./AccountERC7579.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../../utils/cryptography/ERC1271.behavior');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
const anotherTarget = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// ERC-7579 validator
|
||||
const validator = await ethers.deployContract('$ERC7579ValidatorMock');
|
||||
|
||||
// ERC-4337 signer
|
||||
const signer = ethers.Wallet.createRandom();
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountERC7579Mock', [
|
||||
validator,
|
||||
ethers.solidityPacked(['address'], [signer.address]),
|
||||
]);
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
return { helper, validator, mock, entrypointDomain, signer, target, anotherTarget, other };
|
||||
}
|
||||
|
||||
describe('AccountERC7579', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
|
||||
this.signer.signMessage = message =>
|
||||
ethers.Wallet.prototype.signMessage
|
||||
.bind(this.signer)(message)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
this.signer.signTypedData = (domain, types, values) =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(domain, types, values)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
this.signUserOp = userOp =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
this.userOp = { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) };
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountERC7579();
|
||||
shouldBehaveLikeERC1271();
|
||||
});
|
||||
60
test/account/extensions/AccountERC7579Hooked.test.js
Normal file
60
test/account/extensions/AccountERC7579Hooked.test.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getDomain } = require('../../helpers/eip712');
|
||||
const { ERC4337Helper } = require('../../helpers/erc4337');
|
||||
const { PackedUserOperation } = require('../../helpers/eip712-types');
|
||||
|
||||
const { shouldBehaveLikeAccountCore } = require('../Account.behavior');
|
||||
const { shouldBehaveLikeAccountERC7579 } = require('./AccountERC7579.behavior');
|
||||
const { shouldBehaveLikeERC1271 } = require('../../utils/cryptography/ERC1271.behavior');
|
||||
|
||||
async function fixture() {
|
||||
// EOAs and environment
|
||||
const [other] = await ethers.getSigners();
|
||||
const target = await ethers.deployContract('CallReceiverMock');
|
||||
const anotherTarget = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
// ERC-7579 validator
|
||||
const validator = await ethers.deployContract('$ERC7579ValidatorMock');
|
||||
|
||||
// ERC-4337 signer
|
||||
const signer = ethers.Wallet.createRandom();
|
||||
|
||||
// ERC-4337 account
|
||||
const helper = new ERC4337Helper();
|
||||
const mock = await helper.newAccount('$AccountERC7579HookedMock', [
|
||||
validator,
|
||||
ethers.solidityPacked(['address'], [signer.address]),
|
||||
]);
|
||||
|
||||
// ERC-4337 Entrypoint domain
|
||||
const entrypointDomain = await getDomain(entrypoint.v08);
|
||||
|
||||
return { helper, validator, mock, entrypointDomain, signer, target, anotherTarget, other };
|
||||
}
|
||||
|
||||
describe('AccountERC7579Hooked', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
|
||||
this.signer.signMessage = message =>
|
||||
ethers.Wallet.prototype.signMessage
|
||||
.bind(this.signer)(message)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
this.signer.signTypedData = (domain, types, values) =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(domain, types, values)
|
||||
.then(sign => ethers.concat([this.validator.target, sign]));
|
||||
this.signUserOp = userOp =>
|
||||
ethers.Wallet.prototype.signTypedData
|
||||
.bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
|
||||
.then(signature => Object.assign(userOp, { signature }));
|
||||
|
||||
this.userOp = { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) };
|
||||
});
|
||||
|
||||
shouldBehaveLikeAccountCore();
|
||||
shouldBehaveLikeAccountERC7579({ withHooks: true });
|
||||
shouldBehaveLikeERC1271();
|
||||
});
|
||||
145
test/account/extensions/ERC7821.behavior.js
Normal file
145
test/account/extensions/ERC7821.behavior.js
Normal file
@ -0,0 +1,145 @@
|
||||
const { ethers, entrypoint } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { CALL_TYPE_BATCH, encodeMode, encodeBatch } = require('../../helpers/erc7579');
|
||||
|
||||
function shouldBehaveLikeERC7821({ deployable = true } = {}) {
|
||||
describe('supports ERC-7821', function () {
|
||||
beforeEach(async function () {
|
||||
// give eth to the account (before deployment)
|
||||
await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') });
|
||||
|
||||
// account is not initially deployed
|
||||
await expect(ethers.provider.getCode(this.mock)).to.eventually.equal('0x');
|
||||
|
||||
this.encodeUserOpCalldata = (...calls) =>
|
||||
this.mock.interface.encodeFunctionData('execute', [
|
||||
encodeMode({ callType: CALL_TYPE_BATCH }),
|
||||
encodeBatch(...calls),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
|
||||
await this.mock.deploy();
|
||||
|
||||
await expect(
|
||||
this.mock.connect(this.other).execute(
|
||||
encodeMode({ callType: CALL_TYPE_BATCH }),
|
||||
encodeBatch({
|
||||
target: this.target,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
|
||||
if (deployable) {
|
||||
describe('when not deployed', function () {
|
||||
it('should be created with handleOps and increase nonce', async function () {
|
||||
const operation = await this.mock
|
||||
.createUserOp({
|
||||
callData: this.encodeUserOpCalldata({
|
||||
target: this.target,
|
||||
value: 17,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
}),
|
||||
})
|
||||
.then(op => op.addInitCode())
|
||||
.then(op => this.signUserOp(op));
|
||||
|
||||
// Can't call the account to get its nonce before it's deployed
|
||||
await expect(entrypoint.v08.getNonce(this.mock.target, 0)).to.eventually.equal(0);
|
||||
await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary))
|
||||
.to.emit(entrypoint.v08, 'AccountDeployed')
|
||||
.withArgs(operation.hash(), this.mock, this.helper.factory, ethers.ZeroAddress)
|
||||
.to.emit(this.target, 'MockFunctionCalledExtra')
|
||||
.withArgs(this.mock, 17);
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(1);
|
||||
});
|
||||
|
||||
it('should revert if the signature is invalid', async function () {
|
||||
const operation = await this.mock
|
||||
.createUserOp({
|
||||
callData: this.encodeUserOpCalldata({
|
||||
target: this.target,
|
||||
value: 17,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
}),
|
||||
})
|
||||
.then(op => op.addInitCode());
|
||||
|
||||
operation.signature = '0x00';
|
||||
|
||||
await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary)).to.be.reverted;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('when deployed', function () {
|
||||
beforeEach(async function () {
|
||||
await this.mock.deploy();
|
||||
});
|
||||
|
||||
it('should increase nonce and call target', async function () {
|
||||
const operation = await this.mock
|
||||
.createUserOp({
|
||||
callData: this.encodeUserOpCalldata({
|
||||
target: this.target,
|
||||
value: 42,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
}),
|
||||
})
|
||||
.then(op => this.signUserOp(op));
|
||||
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(0);
|
||||
await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary))
|
||||
.to.emit(this.target, 'MockFunctionCalledExtra')
|
||||
.withArgs(this.mock, 42);
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(1);
|
||||
});
|
||||
|
||||
it('should support sending eth to an EOA', async function () {
|
||||
const operation = await this.mock
|
||||
.createUserOp({ callData: this.encodeUserOpCalldata({ target: this.other, value: 42 }) })
|
||||
.then(op => this.signUserOp(op));
|
||||
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(0);
|
||||
await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary)).to.changeEtherBalance(
|
||||
this.other,
|
||||
42,
|
||||
);
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(1);
|
||||
});
|
||||
|
||||
it('should support batch execution', async function () {
|
||||
const value1 = 43374337n;
|
||||
const value2 = 69420n;
|
||||
|
||||
const operation = await this.mock
|
||||
.createUserOp({
|
||||
callData: this.encodeUserOpCalldata(
|
||||
{ target: this.other, value: value1 },
|
||||
{
|
||||
target: this.target,
|
||||
value: value2,
|
||||
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
|
||||
},
|
||||
),
|
||||
})
|
||||
.then(op => this.signUserOp(op));
|
||||
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(0);
|
||||
const tx = entrypoint.v08.handleOps([operation.packed], this.beneficiary);
|
||||
await expect(tx).to.changeEtherBalances([this.other, this.target], [value1, value2]);
|
||||
await expect(tx).to.emit(this.target, 'MockFunctionCalledExtra').withArgs(this.mock, value2);
|
||||
await expect(this.mock.getNonce()).to.eventually.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC7821,
|
||||
};
|
||||
@ -11,19 +11,8 @@ module.exports = mapValues(
|
||||
verifyingContract: 'address',
|
||||
salt: 'bytes32',
|
||||
},
|
||||
Permit: {
|
||||
owner: 'address',
|
||||
spender: 'address',
|
||||
value: 'uint256',
|
||||
nonce: 'uint256',
|
||||
deadline: 'uint256',
|
||||
},
|
||||
Ballot: {
|
||||
proposalId: 'uint256',
|
||||
support: 'uint8',
|
||||
voter: 'address',
|
||||
nonce: 'uint256',
|
||||
},
|
||||
Permit: { owner: 'address', spender: 'address', value: 'uint256', nonce: 'uint256', deadline: 'uint256' },
|
||||
Ballot: { proposalId: 'uint256', support: 'uint8', voter: 'address', nonce: 'uint256' },
|
||||
ExtendedBallot: {
|
||||
proposalId: 'uint256',
|
||||
support: 'uint8',
|
||||
@ -32,18 +21,8 @@ module.exports = mapValues(
|
||||
reason: 'string',
|
||||
params: 'bytes',
|
||||
},
|
||||
OverrideBallot: {
|
||||
proposalId: 'uint256',
|
||||
support: 'uint8',
|
||||
voter: 'address',
|
||||
nonce: 'uint256',
|
||||
reason: 'string',
|
||||
},
|
||||
Delegation: {
|
||||
delegatee: 'address',
|
||||
nonce: 'uint256',
|
||||
expiry: 'uint256',
|
||||
},
|
||||
OverrideBallot: { proposalId: 'uint256', support: 'uint8', voter: 'address', nonce: 'uint256', reason: 'string' },
|
||||
Delegation: { delegatee: 'address', nonce: 'uint256', expiry: 'uint256' },
|
||||
ForwardRequest: {
|
||||
from: 'address',
|
||||
to: 'address',
|
||||
@ -53,6 +32,29 @@ module.exports = mapValues(
|
||||
deadline: 'uint48',
|
||||
data: 'bytes',
|
||||
},
|
||||
PackedUserOperation: {
|
||||
sender: 'address',
|
||||
nonce: 'uint256',
|
||||
initCode: 'bytes',
|
||||
callData: 'bytes',
|
||||
accountGasLimits: 'bytes32',
|
||||
preVerificationGas: 'uint256',
|
||||
gasFees: 'bytes32',
|
||||
paymasterAndData: 'bytes',
|
||||
},
|
||||
UserOperationRequest: {
|
||||
sender: 'address',
|
||||
nonce: 'uint256',
|
||||
initCode: 'bytes',
|
||||
callData: 'bytes',
|
||||
accountGasLimits: 'bytes32',
|
||||
preVerificationGas: 'uint256',
|
||||
gasFees: 'bytes32',
|
||||
paymasterVerificationGasLimit: 'uint256',
|
||||
paymasterPostOpGasLimit: 'uint256',
|
||||
validAfter: 'uint48',
|
||||
validUntil: 'uint48',
|
||||
},
|
||||
},
|
||||
formatType,
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { ethers, config, entrypoint, senderCreator } = require('hardhat');
|
||||
|
||||
const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000';
|
||||
const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001';
|
||||
@ -83,6 +83,129 @@ class UserOperation {
|
||||
}
|
||||
}
|
||||
|
||||
const parseInitCode = initCode => ({
|
||||
factory: '0x' + initCode.replace(/0x/, '').slice(0, 40),
|
||||
factoryData: '0x' + initCode.replace(/0x/, '').slice(40),
|
||||
});
|
||||
|
||||
/// Global ERC-4337 environment helper.
|
||||
class ERC4337Helper {
|
||||
constructor() {
|
||||
this.factoryAsPromise = ethers.deployContract('$Create2');
|
||||
}
|
||||
|
||||
async wait() {
|
||||
this.factory = await this.factoryAsPromise;
|
||||
return this;
|
||||
}
|
||||
|
||||
async newAccount(name, extraArgs = [], params = {}) {
|
||||
const env = {
|
||||
entrypoint: params.entrypoint ?? entrypoint.v08,
|
||||
senderCreator: params.senderCreator ?? senderCreator.v08,
|
||||
};
|
||||
|
||||
const { factory } = await this.wait();
|
||||
|
||||
const accountFactory = await ethers.getContractFactory(name);
|
||||
|
||||
if (params.erc7702signer) {
|
||||
const delegate = await accountFactory.deploy(...extraArgs);
|
||||
const instance = await params.erc7702signer.getAddress().then(address => accountFactory.attach(address));
|
||||
const authorization = await params.erc7702signer.authorize({ address: delegate.target });
|
||||
return new ERC7702SmartAccount(instance, authorization, env);
|
||||
} else {
|
||||
const initCode = await accountFactory
|
||||
.getDeployTransaction(...extraArgs)
|
||||
.then(tx =>
|
||||
factory.interface.encodeFunctionData('$deploy', [0, params.salt ?? ethers.randomBytes(32), tx.data]),
|
||||
)
|
||||
.then(deployCode => ethers.concat([factory.target, deployCode]));
|
||||
|
||||
const instance = await ethers.provider
|
||||
.call({
|
||||
from: env.entrypoint,
|
||||
to: env.senderCreator,
|
||||
data: env.senderCreator.interface.encodeFunctionData('createSender', [initCode]),
|
||||
})
|
||||
.then(result => ethers.getAddress(ethers.hexlify(ethers.getBytes(result).slice(-20))))
|
||||
.then(address => accountFactory.attach(address));
|
||||
|
||||
return new SmartAccount(instance, initCode, env);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent one ERC-4337 account contract.
|
||||
class SmartAccount extends ethers.BaseContract {
|
||||
constructor(instance, initCode, env) {
|
||||
super(instance.target, instance.interface, instance.runner, instance.deployTx);
|
||||
this.address = instance.target;
|
||||
this.initCode = initCode;
|
||||
this._env = env;
|
||||
}
|
||||
|
||||
async deploy(account = this.runner) {
|
||||
const { factory: to, factoryData: data } = parseInitCode(this.initCode);
|
||||
this.deployTx = await account.sendTransaction({ to, data });
|
||||
return this;
|
||||
}
|
||||
|
||||
async createUserOp(userOp = {}) {
|
||||
userOp.sender ??= this;
|
||||
userOp.nonce ??= await this._env.entrypoint.getNonce(userOp.sender, 0);
|
||||
if (ethers.isAddressable(userOp.paymaster)) {
|
||||
userOp.paymaster = await ethers.resolveAddress(userOp.paymaster);
|
||||
userOp.paymasterVerificationGasLimit ??= 100_000n;
|
||||
userOp.paymasterPostOpGasLimit ??= 100_000n;
|
||||
}
|
||||
return new UserOperationWithContext(userOp, this._env);
|
||||
}
|
||||
}
|
||||
|
||||
class ERC7702SmartAccount extends SmartAccount {
|
||||
constructor(instance, authorization, env) {
|
||||
super(instance, undefined, env);
|
||||
this.authorization = authorization;
|
||||
}
|
||||
|
||||
async deploy() {
|
||||
// hardhat signers from @nomicfoundation/hardhat-ethers do not support type 4 txs.
|
||||
// so we rebuild it using "native" ethers
|
||||
await ethers.Wallet.fromPhrase(config.networks.hardhat.accounts.mnemonic, ethers.provider).sendTransaction({
|
||||
to: ethers.ZeroAddress,
|
||||
authorizationList: [this.authorization],
|
||||
gasLimit: 46_000n, // 21,000 base + PER_EMPTY_ACCOUNT_COST
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class UserOperationWithContext extends UserOperation {
|
||||
constructor(userOp, env) {
|
||||
super(userOp);
|
||||
this._sender = userOp.sender;
|
||||
this._env = env;
|
||||
}
|
||||
|
||||
addInitCode() {
|
||||
if (this._sender?.initCode) {
|
||||
return Object.assign(this, parseInitCode(this._sender.initCode));
|
||||
} else throw new Error('No init code available for the sender of this user operation');
|
||||
}
|
||||
|
||||
getAuthorization() {
|
||||
if (this._sender?.authorization) {
|
||||
return this._sender.authorization;
|
||||
} else throw new Error('No EIP-7702 authorization available for the sender of this user operation');
|
||||
}
|
||||
|
||||
hash() {
|
||||
return super.hash(this._env.entrypoint);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SIG_VALIDATION_SUCCESS,
|
||||
SIG_VALIDATION_FAILURE,
|
||||
@ -90,4 +213,5 @@ module.exports = {
|
||||
packInitCode,
|
||||
packPaymasterAndData,
|
||||
UserOperation,
|
||||
ERC4337Helper,
|
||||
};
|
||||
|
||||
147
test/helpers/signers.js
Normal file
147
test/helpers/signers.js
Normal file
@ -0,0 +1,147 @@
|
||||
const {
|
||||
AbstractSigner,
|
||||
Signature,
|
||||
TypedDataEncoder,
|
||||
assert,
|
||||
assertArgument,
|
||||
concat,
|
||||
dataLength,
|
||||
decodeBase64,
|
||||
getBytes,
|
||||
getBytesCopy,
|
||||
hashMessage,
|
||||
hexlify,
|
||||
sha256,
|
||||
toBeHex,
|
||||
} = require('ethers');
|
||||
const { secp256r1 } = require('@noble/curves/p256');
|
||||
const { generateKeyPairSync, privateEncrypt } = require('crypto');
|
||||
|
||||
// Lightweight version of BaseWallet
|
||||
class NonNativeSigner extends AbstractSigner {
|
||||
#signingKey;
|
||||
|
||||
constructor(privateKey, provider) {
|
||||
super(provider);
|
||||
assertArgument(
|
||||
privateKey && typeof privateKey.sign === 'function',
|
||||
'invalid private key',
|
||||
'privateKey',
|
||||
'[ REDACTED ]',
|
||||
);
|
||||
this.#signingKey = privateKey;
|
||||
}
|
||||
|
||||
get signingKey() {
|
||||
return this.#signingKey;
|
||||
}
|
||||
get privateKey() {
|
||||
return this.signingKey.privateKey;
|
||||
}
|
||||
|
||||
async getAddress() {
|
||||
throw new Error("NonNativeSigner doesn't have an address");
|
||||
}
|
||||
|
||||
connect(provider) {
|
||||
return new NonNativeSigner(this.#signingKey, provider);
|
||||
}
|
||||
|
||||
async signTransaction(/*tx: TransactionRequest*/) {
|
||||
throw new Error('NonNativeSigner cannot send transactions');
|
||||
}
|
||||
|
||||
async signMessage(message /*: string | Uint8Array*/) /*: Promise<string>*/ {
|
||||
return this.signingKey.sign(hashMessage(message)).serialized;
|
||||
}
|
||||
|
||||
async signTypedData(
|
||||
domain /*: TypedDataDomain*/,
|
||||
types /*: Record<string, Array<TypedDataField>>*/,
|
||||
value /*: Record<string, any>*/,
|
||||
) /*: Promise<string>*/ {
|
||||
// Populate any ENS names
|
||||
const populated = await TypedDataEncoder.resolveNames(domain, types, value, async name => {
|
||||
assert(this.provider != null, 'cannot resolve ENS names without a provider', 'UNSUPPORTED_OPERATION', {
|
||||
operation: 'resolveName',
|
||||
info: { name },
|
||||
});
|
||||
const address = await this.provider.resolveName(name);
|
||||
assert(address != null, 'unconfigured ENS name', 'UNCONFIGURED_NAME', { value: name });
|
||||
return address;
|
||||
});
|
||||
|
||||
return this.signingKey.sign(TypedDataEncoder.hash(populated.domain, types, populated.value)).serialized;
|
||||
}
|
||||
}
|
||||
|
||||
class P256SigningKey {
|
||||
#privateKey;
|
||||
|
||||
constructor(privateKey) {
|
||||
this.#privateKey = getBytes(privateKey);
|
||||
}
|
||||
|
||||
static random() {
|
||||
return new this(secp256r1.utils.randomPrivateKey());
|
||||
}
|
||||
|
||||
get privateKey() {
|
||||
return hexlify(this.#privateKey);
|
||||
}
|
||||
|
||||
get publicKey() {
|
||||
const publicKeyBytes = secp256r1.getPublicKey(this.#privateKey, false);
|
||||
return { qx: hexlify(publicKeyBytes.slice(0x01, 0x21)), qy: hexlify(publicKeyBytes.slice(0x21, 0x41)) };
|
||||
}
|
||||
|
||||
sign(digest /*: BytesLike*/) /*: Signature*/ {
|
||||
assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest);
|
||||
|
||||
const sig = secp256r1.sign(getBytesCopy(digest), getBytesCopy(this.#privateKey), { lowS: true });
|
||||
|
||||
return Signature.from({ r: toBeHex(sig.r, 32), s: toBeHex(sig.s, 32), v: sig.recovery ? 0x1c : 0x1b });
|
||||
}
|
||||
}
|
||||
|
||||
class RSASigningKey {
|
||||
#privateKey;
|
||||
#publicKey;
|
||||
|
||||
constructor(keyPair) {
|
||||
const jwk = keyPair.publicKey.export({ format: 'jwk' });
|
||||
this.#privateKey = keyPair.privateKey;
|
||||
this.#publicKey = { e: decodeBase64(jwk.e), n: decodeBase64(jwk.n) };
|
||||
}
|
||||
|
||||
static random(modulusLength = 2048) {
|
||||
return new this(generateKeyPairSync('rsa', { modulusLength }));
|
||||
}
|
||||
|
||||
get privateKey() {
|
||||
return hexlify(this.#privateKey);
|
||||
}
|
||||
|
||||
get publicKey() {
|
||||
return { e: hexlify(this.#publicKey.e), n: hexlify(this.#publicKey.n) };
|
||||
}
|
||||
|
||||
sign(digest /*: BytesLike*/) /*: Signature*/ {
|
||||
assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest);
|
||||
// SHA256 OID = 608648016503040201 (9 bytes) | NULL = 0500 (2 bytes) (explicit) | OCTET_STRING length (0x20) = 0420 (2 bytes)
|
||||
return {
|
||||
serialized: hexlify(
|
||||
privateEncrypt(this.#privateKey, getBytes(concat(['0x3031300d060960864801650304020105000420', digest]))),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RSASHA256SigningKey extends RSASigningKey {
|
||||
sign(digest /*: BytesLike*/) /*: Signature*/ {
|
||||
assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest);
|
||||
return super.sign(sha256(getBytes(digest)));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { NonNativeSigner, P256SigningKey, RSASigningKey, RSASHA256SigningKey };
|
||||
@ -1,5 +1,6 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { shouldBehaveLikeERC1271 } = require('./ERC1271.behavior');
|
||||
const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey } = require('../../helpers/signers');
|
||||
|
||||
describe('ERC7739', function () {
|
||||
describe('for an ECDSA signer', function () {
|
||||
@ -10,4 +11,28 @@ describe('ERC7739', function () {
|
||||
|
||||
shouldBehaveLikeERC1271({ erc7739: true });
|
||||
});
|
||||
|
||||
describe('for a P256 signer', function () {
|
||||
before(async function () {
|
||||
this.signer = new NonNativeSigner(P256SigningKey.random());
|
||||
this.mock = await ethers.deployContract('ERC7739P256Mock', [
|
||||
this.signer.signingKey.publicKey.qx,
|
||||
this.signer.signingKey.publicKey.qy,
|
||||
]);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC1271({ erc7739: true });
|
||||
});
|
||||
|
||||
describe('for an RSA signer', function () {
|
||||
before(async function () {
|
||||
this.signer = new NonNativeSigner(RSASHA256SigningKey.random());
|
||||
this.mock = await ethers.deployContract('ERC7739RSAMock', [
|
||||
this.signer.signingKey.publicKey.e,
|
||||
this.signer.signingKey.publicKey.n,
|
||||
]);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC1271({ erc7739: true });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user