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
|
= Account
|
||||||
|
|
||||||
[.readme-notice]
|
[.readme-notice]
|
||||||
NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/account
|
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
|
== 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 {
|
contract CallReceiverMock {
|
||||||
event MockFunctionCalled();
|
event MockFunctionCalled();
|
||||||
event MockFunctionCalledWithArgs(uint256 a, uint256 b);
|
event MockFunctionCalledWithArgs(uint256 a, uint256 b);
|
||||||
|
event MockFunctionCalledExtra(address caller, uint256 value);
|
||||||
|
|
||||||
uint256[] private _array;
|
uint256[] private _array;
|
||||||
|
|
||||||
@ -58,6 +59,10 @@ contract CallReceiverMock {
|
|||||||
}
|
}
|
||||||
return "0x1234";
|
return "0x1234";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockFunctionExtra() public payable {
|
||||||
|
emit MockFunctionCalledExtra(msg.sender, msg.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contract CallReceiverMockTrustingForwarder is CallReceiverMock {
|
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 {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
|
||||||
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
|
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
|
||||||
import {ERC7739} from "../../../utils/cryptography/ERC7739.sol";
|
import {ERC7739} from "../../../utils/cryptography/ERC7739.sol";
|
||||||
import {AbstractSigner} from "../../../utils/cryptography/AbstractSigner.sol";
|
import {SignerECDSA} from "../../../utils/cryptography/SignerECDSA.sol";
|
||||||
|
import {SignerP256} from "../../../utils/cryptography/SignerP256.sol";
|
||||||
contract ERC7739ECDSAMock is AbstractSigner, ERC7739 {
|
import {SignerRSA} from "../../../utils/cryptography/SignerRSA.sol";
|
||||||
address private _signer;
|
|
||||||
|
|
||||||
|
contract ERC7739ECDSAMock is ERC7739, SignerECDSA {
|
||||||
constructor(address signerAddr) EIP712("ERC7739ECDSA", "1") {
|
constructor(address signerAddr) EIP712("ERC7739ECDSA", "1") {
|
||||||
_signer = signerAddr;
|
_setSigner(signerAddr);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function signer() public view virtual returns (address) {
|
|
||||||
return _signer;
|
contract ERC7739P256Mock is ERC7739, SignerP256 {
|
||||||
}
|
constructor(bytes32 qx, bytes32 qy) EIP712("ERC7739P256", "1") {
|
||||||
|
_setSigner(qx, qy);
|
||||||
function _rawSignatureValidation(
|
}
|
||||||
bytes32 hash,
|
}
|
||||||
bytes calldata signature
|
|
||||||
) internal view virtual override returns (bool) {
|
contract ERC7739RSAMock is ERC7739, SignerRSA {
|
||||||
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
|
constructor(bytes memory e, bytes memory n) EIP712("ERC7739RSA", "1") {
|
||||||
return signer() == recovered && err == ECDSA.RecoverError.NoError;
|
_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.
|
* {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.
|
* {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.
|
* {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.
|
* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
|
||||||
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
|
* {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.
|
* {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]
|
[NOTE]
|
||||||
====
|
====
|
||||||
@ -90,6 +92,14 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable
|
|||||||
|
|
||||||
{{AbstractSigner}}
|
{{AbstractSigner}}
|
||||||
|
|
||||||
|
{{SignerECDSA}}
|
||||||
|
|
||||||
|
{{SignerP256}}
|
||||||
|
|
||||||
|
{{SignerERC7702}}
|
||||||
|
|
||||||
|
{{SignerRSA}}
|
||||||
|
|
||||||
== Security
|
== Security
|
||||||
|
|
||||||
{{ReentrancyGuard}}
|
{{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/=contracts/
|
||||||
+@openzeppelin/contracts-upgradeable/=contracts/
|
+@openzeppelin/contracts-upgradeable/=contracts/
|
||||||
+@openzeppelin/contracts/=lib/openzeppelin-contracts/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
|
diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js
|
||||||
index 2b6e7fa97..268e0d29d 100644
|
index 2b6e7fa97..268e0d29d 100644
|
||||||
--- a/test/utils/cryptography/EIP712.test.js
|
--- 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',
|
verifyingContract: 'address',
|
||||||
salt: 'bytes32',
|
salt: 'bytes32',
|
||||||
},
|
},
|
||||||
Permit: {
|
Permit: { owner: 'address', spender: 'address', value: 'uint256', nonce: 'uint256', deadline: 'uint256' },
|
||||||
owner: 'address',
|
Ballot: { proposalId: 'uint256', support: 'uint8', voter: 'address', nonce: 'uint256' },
|
||||||
spender: 'address',
|
|
||||||
value: 'uint256',
|
|
||||||
nonce: 'uint256',
|
|
||||||
deadline: 'uint256',
|
|
||||||
},
|
|
||||||
Ballot: {
|
|
||||||
proposalId: 'uint256',
|
|
||||||
support: 'uint8',
|
|
||||||
voter: 'address',
|
|
||||||
nonce: 'uint256',
|
|
||||||
},
|
|
||||||
ExtendedBallot: {
|
ExtendedBallot: {
|
||||||
proposalId: 'uint256',
|
proposalId: 'uint256',
|
||||||
support: 'uint8',
|
support: 'uint8',
|
||||||
@ -32,18 +21,8 @@ module.exports = mapValues(
|
|||||||
reason: 'string',
|
reason: 'string',
|
||||||
params: 'bytes',
|
params: 'bytes',
|
||||||
},
|
},
|
||||||
OverrideBallot: {
|
OverrideBallot: { proposalId: 'uint256', support: 'uint8', voter: 'address', nonce: 'uint256', reason: 'string' },
|
||||||
proposalId: 'uint256',
|
Delegation: { delegatee: 'address', nonce: 'uint256', expiry: 'uint256' },
|
||||||
support: 'uint8',
|
|
||||||
voter: 'address',
|
|
||||||
nonce: 'uint256',
|
|
||||||
reason: 'string',
|
|
||||||
},
|
|
||||||
Delegation: {
|
|
||||||
delegatee: 'address',
|
|
||||||
nonce: 'uint256',
|
|
||||||
expiry: 'uint256',
|
|
||||||
},
|
|
||||||
ForwardRequest: {
|
ForwardRequest: {
|
||||||
from: 'address',
|
from: 'address',
|
||||||
to: 'address',
|
to: 'address',
|
||||||
@ -53,6 +32,29 @@ module.exports = mapValues(
|
|||||||
deadline: 'uint48',
|
deadline: 'uint48',
|
||||||
data: 'bytes',
|
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,
|
formatType,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const { ethers } = require('hardhat');
|
const { ethers, config, entrypoint, senderCreator } = require('hardhat');
|
||||||
|
|
||||||
const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000';
|
const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000';
|
||||||
const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001';
|
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 = {
|
module.exports = {
|
||||||
SIG_VALIDATION_SUCCESS,
|
SIG_VALIDATION_SUCCESS,
|
||||||
SIG_VALIDATION_FAILURE,
|
SIG_VALIDATION_FAILURE,
|
||||||
@ -90,4 +213,5 @@ module.exports = {
|
|||||||
packInitCode,
|
packInitCode,
|
||||||
packPaymasterAndData,
|
packPaymasterAndData,
|
||||||
UserOperation,
|
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 { ethers } = require('hardhat');
|
||||||
const { shouldBehaveLikeERC1271 } = require('./ERC1271.behavior');
|
const { shouldBehaveLikeERC1271 } = require('./ERC1271.behavior');
|
||||||
|
const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey } = require('../../helpers/signers');
|
||||||
|
|
||||||
describe('ERC7739', function () {
|
describe('ERC7739', function () {
|
||||||
describe('for an ECDSA signer', function () {
|
describe('for an ECDSA signer', function () {
|
||||||
@ -10,4 +11,28 @@ describe('ERC7739', function () {
|
|||||||
|
|
||||||
shouldBehaveLikeERC1271({ erc7739: true });
|
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