diff --git a/contracts/abstraction/utils/ERC7579Utils.sol b/contracts/abstraction/utils/ERC7579Utils.sol deleted file mode 100644 index b35e157101b..00000000000 --- a/contracts/abstraction/utils/ERC7579Utils.sol +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {Execution} from "../../interfaces/IERC7579Account.sol"; -import {Packing} from "../../utils/Packing.sol"; - -type Mode is bytes32; -type CallType is bytes1; -type ExecType is bytes1; -type ModeSelector is bytes4; -type ModePayload is bytes22; - -library ERC7579Utils { - using Packing for *; - - CallType constant CALLTYPE_SINGLE = CallType.wrap(0x00); - CallType constant CALLTYPE_BATCH = CallType.wrap(0x01); - CallType constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); - ExecType constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); - ExecType constant EXECTYPE_TRY = ExecType.wrap(0x01); - - function encodeMode( - CallType callType, - ExecType execType, - ModeSelector selector, - ModePayload payload - ) internal pure returns (Mode mode) { - return - Mode.wrap( - CallType - .unwrap(callType) - .pack_1_1(ExecType.unwrap(execType)) - .pack_2_4(bytes4(0)) - .pack_6_4(ModeSelector.unwrap(selector)) - .pack_10_22(ModePayload.unwrap(payload)) - ); - } - - function decodeMode( - Mode mode - ) internal pure returns (CallType callType, ExecType execType, ModeSelector selector, ModePayload payload) { - return ( - CallType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 0)), - ExecType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 1)), - ModeSelector.wrap(Packing.extract_32_4(Mode.unwrap(mode), 6)), - ModePayload.wrap(Packing.extract_32_22(Mode.unwrap(mode), 10)) - ); - } - - function encodeSingle( - address target, - uint256 value, - bytes memory callData - ) internal pure returns (bytes memory executionCalldata) { - return abi.encodePacked(target, value, callData); - } - - function decodeSingle( - bytes calldata executionCalldata - ) internal pure returns (address target, uint256 value, bytes calldata callData) { - target = address(bytes20(executionCalldata[0:20])); - value = uint256(bytes32(executionCalldata[20:52])); - callData = executionCalldata[52:]; - } - - function encodeDelegate( - address target, - bytes memory callData - ) internal pure returns (bytes memory executionCalldata) { - return abi.encodePacked(target, callData); - } - - function decodeDelegate( - bytes calldata executionCalldata - ) internal pure returns (address target, bytes calldata callData) { - target = address(bytes20(executionCalldata[0:20])); - callData = executionCalldata[20:]; - } - - function encodeBatch(Execution[] memory executionBatch) internal pure returns (bytes memory executionCalldata) { - return abi.encode(executionBatch); - } - - function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) { - assembly ("memory-safe") { - let ptr := add(executionCalldata.offset, calldataload(executionCalldata.offset)) - // Extract the ERC7579 Executions - executionBatch.offset := add(ptr, 32) - executionBatch.length := calldataload(ptr) - } - } -} - -// Operators -using {eqCallType as ==} for CallType global; -using {eqExecType as ==} for ExecType global; -using {eqModeSelector as ==} for ModeSelector global; -using {eqModePayload as ==} for ModePayload global; - -function eqCallType(CallType a, CallType b) pure returns (bool) { - return CallType.unwrap(a) == CallType.unwrap(b); -} - -function eqExecType(ExecType a, ExecType b) pure returns (bool) { - return ExecType.unwrap(a) == ExecType.unwrap(b); -} - -function eqModeSelector(ModeSelector a, ModeSelector b) pure returns (bool) { - return ModeSelector.unwrap(a) == ModeSelector.unwrap(b); -} - -function eqModePayload(ModePayload a, ModePayload b) pure returns (bool) { - return ModePayload.unwrap(a) == ModePayload.unwrap(b); -} diff --git a/contracts/account/AccountBase.sol b/contracts/account/AccountBase.sol new file mode 100644 index 00000000000..979ff3d2fc8 --- /dev/null +++ b/contracts/account/AccountBase.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation, IAccount, IEntryPoint, IAccountExecute} from "../interfaces/IERC4337.sol"; +import {Address} from "../utils/Address.sol"; + +/** + * @dev A simpleĀ ERC4337 account implementation. + * + * This base implementation only includes the minimal logic to process user operations. + * Developers must implement the {_validateUserOp} function to define the account's validation logic. + */ +abstract contract AccountBase is IAccount, IAccountExecute { + /** + * @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 IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032); + } + + /** + * @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; + } + + /** + * @inheritdoc IAccountExecute + */ + function executeUserOp( + PackedUserOperation calldata userOp, + bytes32 /*userOpHash*/ + ) public virtual onlyEntryPointOrSelf { + (address target, uint256 value, bytes memory data) = abi.decode(userOp.callData[4:], (address, uint256, bytes)); + Address.functionCallWithValue(target, data, value); + } + + /** + * @dev Validation logic for {validateUserOp}. + * + * IMPORTANT: Implementing a mechanism to validate user operations is a security-sensitive operation + * as it may allow an attacker to bypass the account's security measures. Check out {AccountECDSA}, + * {AccountP256}, or {AccountRSA} for digital signature validation implementations. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual returns (uint256 validationData); + + /** + * @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 { + if (msg.sender != address(entryPoint())) { + revert AccountUnauthorized(msg.sender); + } + } + + /** + * @dev Ensures the caller is the {entrypoint} or the account itself. + */ + function _checkEntryPointOrSelf() internal view virtual { + if (msg.sender != address(this) && msg.sender != address(entryPoint())) { + revert AccountUnauthorized(msg.sender); + } + } + + /** + * @dev Receive Ether. + */ + receive() external payable virtual {} +} diff --git a/contracts/account/AccountECDSA.sol b/contracts/account/AccountECDSA.sol new file mode 100644 index 00000000000..c0e7ed6531a --- /dev/null +++ b/contracts/account/AccountECDSA.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../interfaces/IERC4337.sol"; +import {AccountBase} from "./AccountBase.sol"; +import {ERC1271TypedSigner} from "../utils/cryptography/ERC1271TypedSigner.sol"; +import {ECDSA} from "../utils/cryptography/ECDSA.sol"; +import {ERC4337Utils} from "./utils/ERC4337Utils.sol"; +import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC1155HolderLean.sol"; +import {ERC165} from "../utils/introspection/ERC165.sol"; +import {IERC165} from "../utils/introspection/IERC165.sol"; + +/** + * @dev Account implementation using {ECDSA} signatures and {ERC1271TypedSigner} for replay protection. + */ +abstract contract AccountECDSA is ERC165, ERC1271TypedSigner, ERC721Holder, ERC1155HolderLean, AccountBase { + address private immutable _signer; + + /** + * @dev Initializes the account with the address of the native signer. + */ + constructor(address signerAddr) { + _signer = signerAddr; + } + + /** + * @dev Return the account's signer address. + */ + function signer() public view virtual returns (address) { + return _signer; + } + + /** + * @dev Internal version of {validateUserOp} that relies on {_isValidSignature}. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256) { + return + _isValidSignature(userOpHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + /** + * @dev Validates the signature using the account's signer.S + */ + function _validateSignature(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; + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/account/AccountEIP7702.sol b/contracts/account/AccountEIP7702.sol new file mode 100644 index 00000000000..a16a8096ce7 --- /dev/null +++ b/contracts/account/AccountEIP7702.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {AccountECDSA} from "./AccountECDSA.sol"; +import {EIP712} from "../utils/cryptography/EIP712.sol"; + +contract AccountEIP7702 is AccountECDSA { + constructor(string memory name, string memory version) AccountECDSA(address(this)) EIP712(name, version) {} +} diff --git a/contracts/account/AccountP256.sol b/contracts/account/AccountP256.sol new file mode 100644 index 00000000000..dccb48791ac --- /dev/null +++ b/contracts/account/AccountP256.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../interfaces/IERC4337.sol"; +import {AccountBase} from "./AccountBase.sol"; +import {ERC1271TypedSigner} from "../utils/cryptography/ERC1271TypedSigner.sol"; +import {P256} from "../utils/cryptography/P256.sol"; +import {ERC4337Utils} from "./utils/ERC4337Utils.sol"; +import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC1155HolderLean.sol"; +import {ERC165} from "../utils/introspection/ERC165.sol"; +import {IERC165} from "../utils/introspection/IERC165.sol"; + +/** + * @dev Account implementation using {P256} signatures and {ERC1271TypedSigner} for replay protection. + */ +abstract contract AccountP256 is ERC165, ERC1271TypedSigner, ERC721Holder, ERC1155HolderLean, AccountBase { + bytes32 private immutable _qx; + bytes32 private immutable _qy; + + /** + * @dev Initializes the account with the P256 public key. + */ + constructor(bytes32 qx, bytes32 qy) { + _qx = qx; + _qy = qy; + } + + /** + * @dev Return the account's signer P256 public key. + */ + function signer() public view virtual returns (bytes32 qx, bytes32 qy) { + return (_qx, _qy); + } + + /** + * @dev Internal version of {validateUserOp} that relies on {_isValidSignature}. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256) { + return + _isValidSignature(userOpHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + /** + * @dev Validates the signature using the account's signer. + */ + function _validateSignature(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); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/account/AccountRSA.sol b/contracts/account/AccountRSA.sol new file mode 100644 index 00000000000..f38228c7aa4 --- /dev/null +++ b/contracts/account/AccountRSA.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../interfaces/IERC4337.sol"; +import {AccountBase} from "./AccountBase.sol"; +import {ERC1271TypedSigner} from "../utils/cryptography/ERC1271TypedSigner.sol"; +import {RSA} from "../utils/cryptography/RSA.sol"; +import {ERC4337Utils} from "./utils/ERC4337Utils.sol"; +import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC1155HolderLean.sol"; +import {ERC165} from "../utils/introspection/ERC165.sol"; +import {IERC165} from "../utils/introspection/IERC165.sol"; + +/** + * @dev Account implementation using {RSA} signatures and {ERC1271TypedSigner} for replay protection. + * + * NOTE: Storing `_e` and `_n` in regular storage violate ERC-7562 validation rules if the contract + * is used as an ERC-1271 signer during the validation phase of a different account contract. + * Consider deploying this contract through a factory that sets `_e` and `_n` as immutable arguments + * (see {Clones-cloneDeterministicWithImmutableArgs}). + */ +abstract contract AccountRSA is ERC165, ERC1271TypedSigner, ERC721Holder, ERC1155HolderLean, AccountBase { + bytes private _e; + bytes private _n; + + /** + * @dev Initializes the account with the RSA public key. + */ + constructor(bytes memory e, bytes memory n) { + _e = e; + _n = n; + } + + /** + * @dev Return the account's signer RSA public key. + */ + function signer() public view virtual returns (bytes memory e, bytes memory n) { + return (_e, _n); + } + + /** + * @dev Internal version of {validateUserOp} that relies on {_isValidSignature}. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256) { + return + _isValidSignature(userOpHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + /** + * @dev Validates the signature using the account's signer. + */ + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + (bytes memory e, bytes memory n) = signer(); + return RSA.pkcs1(hash, signature, e, n); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/account/FactoryBase.sol b/contracts/account/FactoryBase.sol new file mode 100644 index 00000000000..99fa97e8f72 --- /dev/null +++ b/contracts/account/FactoryBase.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Clones} from "../proxy/Clones.sol"; +import {Address} from "../utils/Address.sol"; + +abstract contract FactoryBase { + using Clones for address; + + // Store the implementation of the account + address private immutable _impl; + + constructor(address impl) { + _impl = impl; + } + + function predictAddress(bytes32 salt) public view returns (address) { + return _impl.predictDeterministicAddress(salt, address(this)); + } + + // Create accounts on demand + function cloneAndInitialize(bytes32 salt, bytes calldata data) public returns (address) { + return _cloneAndInitialize(salt, data); + } + + function _cloneAndInitialize(bytes32 salt, bytes calldata data) internal returns (address) { + address predicted = predictAddress(salt); + if (predicted.code.length == 0) { + _impl.cloneDeterministic(salt); + Address.functionCall(predicted, data); + } + return predicted; + } +} diff --git a/contracts/account/README.adoc b/contracts/account/README.adoc new file mode 100644 index 00000000000..5e60ee9a63c --- /dev/null +++ b/contracts/account/README.adoc @@ -0,0 +1,50 @@ += Account + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/account + +This directory includes contracts to build accounts for ERC-4337. + +== Core + +{{AccountBase}} + +{{AccountERC7579}} + +{{AccountECDSA}} + +{{AccountEIP7702}} + +{{AccountP256}} + +{{AccountRSA}} + +{{FactoryBase}} + +== Extensions + +{{AccountERC7579Hooked}} + +== Modules + +=== Validators + +{{ECDSAValidator}} + +{{P256Validator}} + +{{RSAValidator}} + +{{MultiERC1271Validator}} + +{{SignatureValidator}} + +=== Executors + +{{TypedERC1271Executor}} + +== Utilities + +{{ERC4337Utils}} + +{{ERC7579Utils}} diff --git a/contracts/account/draft-AccountERC7579.sol b/contracts/account/draft-AccountERC7579.sol new file mode 100644 index 00000000000..00a7ecdedf8 --- /dev/null +++ b/contracts/account/draft-AccountERC7579.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC1271} from "../interfaces/IERC1271.sol"; +import {AccountBase} from "./AccountBase.sol"; +import {PackedUserOperation} from "../interfaces/IERC4337.sol"; +import {Address} from "../utils/Address.sol"; +import {IERC7579AccountConfig, IERC7579Execution, IERC7579ModuleConfig} from "../interfaces/IERC7579Account.sol"; +import {IERC7579Validator, IERC7579Module, MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_TYPE_FALLBACK} from "../interfaces/IERC7579Module.sol"; +import {ERC7579Utils, Mode, CallType, ExecType} from "./utils/ERC7579Utils.sol"; +import {ERC4337Utils} from "./utils/ERC4337Utils.sol"; +import {EnumerableSet} from "../utils/structs/EnumerableSet.sol"; +import {EIP712} from "../utils/cryptography/EIP712.sol"; + +abstract contract AccountERC7579 is + EIP712, + IERC1271, + AccountBase, + IERC7579ModuleConfig, + IERC7579Execution, + IERC7579AccountConfig +{ + using ERC7579Utils for *; + using EnumerableSet for *; + + EnumerableSet.AddressSet private _validators; + EnumerableSet.AddressSet private _executors; + mapping(bytes4 => address) private _fallbacks; + + modifier onlyModule(uint256 moduleTypeId) { + _checkModule(moduleTypeId, msg.sender); + _; + } + + /// @inheritdoc IERC1271 + function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual override returns (bytes4) { + address module = abi.decode(signature[0:20], (address)); + if (!_isModuleInstalled(MODULE_TYPE_VALIDATOR, module, msg.data)) return bytes4(0xffffffff); + return IERC7579Validator(module).isValidSignatureWithSender(msg.sender, hash, signature); + } + + /// @inheritdoc IERC7579AccountConfig + function accountId() public view virtual returns (string memory) { + //vendorname.accountname.semver + return "@openzeppelin/contracts.erc7579account.v0-beta"; + } + + /// @inheritdoc IERC7579AccountConfig + function supportsExecutionMode(bytes32 encodedMode) public view virtual returns (bool) { + return _supportsExecutionMode(encodedMode); + } + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual returns (bool) { + return _supportsModule(moduleTypeId); + } + + /// @inheritdoc IERC7579Execution + function execute(bytes32 mode, bytes calldata executionCalldata) public virtual onlyEntryPointOrSelf { + _execute(Mode.wrap(mode), executionCalldata); + } + + /// @inheritdoc IERC7579Execution + function executeFromExecutor( + bytes32 mode, + bytes calldata executionCalldata + ) public virtual onlyModule(MODULE_TYPE_EXECUTOR) returns (bytes[] memory) { + return _execute(Mode.wrap(mode), executionCalldata); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view virtual returns (bool) { + return _isModuleInstalled(moduleTypeId, module, additionalContext); + } + + /// @inheritdoc IERC7579ModuleConfig + function installModule( + uint256 moduleTypeId, + address module, + bytes memory initData + ) public virtual onlyEntryPointOrSelf { + _installModule(moduleTypeId, module, initData); + } + + /// @inheritdoc IERC7579ModuleConfig + function uninstallModule( + uint256 moduleTypeId, + address module, + bytes memory deInitData + ) public virtual onlyEntryPointOrSelf { + _uninstallModule(moduleTypeId, module, deInitData); + } + + function executeUserOp( + PackedUserOperation calldata userOp, + bytes32 /*userOpHash*/ + ) public virtual override onlyEntryPointOrSelf { + Address.functionDelegateCall(address(this), userOp.callData[4:]); + } + + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256 validationData) { + PackedUserOperation memory userOpCopy = userOp; + address module = abi.decode(userOp.signature[0:20], (address)); + userOpCopy.signature = userOp.signature[20:]; + return + isModuleInstalled(MODULE_TYPE_EXECUTOR, module, userOp.signature[0:0]) + ? IERC7579Validator(module).validateUserOp(userOpCopy, userOpHash) + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + function _supportsExecutionMode(bytes32 encodedMode) internal pure virtual returns (bool) { + (CallType callType, , , ) = Mode.wrap(encodedMode).decodeMode(); + return + callType == ERC7579Utils.CALLTYPE_SINGLE || + callType == ERC7579Utils.CALLTYPE_BATCH || + callType == ERC7579Utils.CALLTYPE_DELEGATECALL; + } + + function _execute( + Mode mode, + bytes calldata executionCalldata + ) internal virtual returns (bytes[] memory returnData) { + // TODO: ModeSelector? ModePayload? + (CallType callType, ExecType execType, , ) = mode.decodeMode(); + + if (callType == ERC7579Utils.CALLTYPE_SINGLE) return ERC7579Utils.execSingle(execType, executionCalldata); + if (callType == ERC7579Utils.CALLTYPE_BATCH) return ERC7579Utils.execBatch(execType, executionCalldata); + if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) + return ERC7579Utils.execDelegateCall(execType, executionCalldata); + revert ERC7579Utils.ERC7579UnsupportedCallType(callType); + } + + function _isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) internal 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; + } + + function _installModule(uint256 moduleTypeId, address module, bytes memory initData) internal virtual { + if (!_supportsModule(moduleTypeId)) revert ERC7579Utils.ERC7579UnsupportedModuleType(moduleTypeId); + if (!IERC7579Module(module).isModuleType(moduleTypeId)) + revert ERC7579Utils.ERC7579MismatchedModuleTypeId(moduleTypeId, module); + if ( + (moduleTypeId == MODULE_TYPE_VALIDATOR && !_validators.add(module)) || + (moduleTypeId == MODULE_TYPE_EXECUTOR && !_executors.add(module)) + ) revert ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module); + + if (moduleTypeId == MODULE_TYPE_FALLBACK) { + bytes4 selector; + (selector, initData) = abi.decode(initData, (bytes4, bytes)); + if (!_installFallback(module, selector)) + revert ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module); + } + + IERC7579Module(module).onInstall(initData); + emit ModuleInstalled(moduleTypeId, module); + } + + function _uninstallModule(uint256 moduleTypeId, address module, bytes memory deInitData) internal virtual { + if ( + (moduleTypeId == MODULE_TYPE_VALIDATOR && !_validators.remove(module)) || + (moduleTypeId == MODULE_TYPE_EXECUTOR && !_executors.remove(module)) + ) revert ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module); + + if (moduleTypeId == MODULE_TYPE_FALLBACK) { + bytes4 selector; + (selector, deInitData) = abi.decode(deInitData, (bytes4, bytes)); + if (!_uninstallFallback(module, selector)) + revert ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module); + } + + IERC7579Module(module).onUninstall(deInitData); + emit ModuleUninstalled(moduleTypeId, module); + } + + function _installFallback(address module, bytes4 selector) internal virtual returns (bool) { + if (_fallbacks[selector] != address(0)) return false; + _fallbacks[selector] = module; + return true; + } + + function _uninstallFallback(address module, bytes4 selector) internal virtual returns (bool) { + address handler = _fallbacks[selector]; + if (handler == address(0) || handler != module) return false; + delete _fallbacks[selector]; + return true; + } + + function _checkModule(uint256 moduleTypeId, address module) internal view virtual { + if (!_isModuleInstalled(moduleTypeId, module, msg.data)) { + revert ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module); + } + } + + function _supportsModule(uint256 moduleTypeId) internal view virtual returns (bool) { + return + moduleTypeId == MODULE_TYPE_VALIDATOR || + moduleTypeId == MODULE_TYPE_EXECUTOR || + moduleTypeId == MODULE_TYPE_FALLBACK; + } + + function _fallbackHandler(bytes4 selector) internal view virtual returns (address) { + return _fallbacks[selector]; + } + + function _fallback() internal virtual { + address handler = _fallbackHandler(msg.sig); + if (handler == address(0)) return; + + // 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) + ); + assembly ("memory-safe") { + switch success + case 0 { + revert(add(returndata, 0x20), mload(returndata)) + } + default { + return(add(returndata, 0x20), mload(returndata)) + } + } + } + + fallback() external payable virtual { + _fallback(); + } +} diff --git a/contracts/account/extensions/draft-AccountERC7579Hooked.sol b/contracts/account/extensions/draft-AccountERC7579Hooked.sol new file mode 100644 index 00000000000..66510da406f --- /dev/null +++ b/contracts/account/extensions/draft-AccountERC7579Hooked.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {IERC7579Hook, MODULE_TYPE_HOOK} from "../../interfaces/IERC7579Module.sol"; +import {IERC7579ModuleConfig} from "../../interfaces/IERC7579Account.sol"; +import {ERC7579Utils, Mode} from "../utils/ERC7579Utils.sol"; +import {AccountERC7579} from "../draft-AccountERC7579.sol"; + +abstract contract AccountERC7579Hooked is AccountERC7579 { + address private _hook; + + modifier withHook() { + address hook_ = hook(); + if (hook_ == address(0)) { + _; + } else { + bytes memory hookData = IERC7579Hook(hook_).preCheck(msg.sender, msg.value, msg.data); + _; + IERC7579Hook(hook_).postCheck(hookData); + } + } + + function hook() public view returns (address) { + return _hook; + } + + function _isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata data + ) internal view override returns (bool) { + return + moduleTypeId == MODULE_TYPE_HOOK + ? _isHookInstalled(module) + : super._isModuleInstalled(moduleTypeId, module, data); + } + + function _execute(Mode mode, bytes calldata executionCalldata) internal override withHook returns (bytes[] memory) { + return super._execute(mode, executionCalldata); + } + + function _installModule(uint256 moduleTypeId, address module, bytes memory initData) internal virtual override { + _hook = module; + super._installModule(moduleTypeId, module, initData); + } + + function _uninstallModule(uint256 moduleTypeId, address module, bytes memory deInitData) internal virtual override { + _hook = address(0); + super._uninstallModule(moduleTypeId, module, deInitData); + } + + function _supportsModule(uint256 moduleTypeId) internal view virtual override returns (bool) { + return moduleTypeId == MODULE_TYPE_HOOK || super._supportsModule(moduleTypeId); + } + + function _isHookInstalled(address module) internal view returns (bool) { + return _hook == module; + } + + function _fallback() internal virtual override withHook { + super._fallback(); + } +} diff --git a/contracts/account/modules/ECDSAValidator.sol b/contracts/account/modules/ECDSAValidator.sol new file mode 100644 index 00000000000..5d713a3ecd6 --- /dev/null +++ b/contracts/account/modules/ECDSAValidator.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ECDSA} from "../../utils/cryptography/ECDSA.sol"; +import {SignatureValidator} from "./SignatureValidator.sol"; + +/** + * @dev {SignatureValidator} for {ECDSA} signatures. + */ +abstract contract ECDSAValidator is SignatureValidator { + mapping(address sender => address signer) private _associatedSigners; + + /** + * @dev Emitted when an account is associated with an ECDSA signer. + */ + event ECDSASignerAssociated(address indexed account, address indexed signer); + /** + * @dev Emitted when an account is disassociated from an ECDSA signer. + */ + event ECDSASignerDisassociated(address indexed account); + + /** + * @dev Return the account's signer address for the given account. + */ + function signer(address account) public view virtual returns (address) { + return _associatedSigners[account]; + } + + /** + * @dev Associates an account with an ECDSA signer. + * + * The `data` is expected to be an `abi.encode(signerAddr)`. + */ + function onInstall(bytes calldata data) public virtual { + address signerAddr = abi.decode(data, (address)); + _onInstall(msg.sender, signerAddr); + } + + /** + * @dev Disassociates an account from an ECDSA signer. + */ + function onUninstall(bytes calldata) public virtual { + _onUninstall(msg.sender); + } + + /** + * @dev Internal version of {onInstall} without access control. + */ + function _onInstall(address account, address signerAddr) internal virtual { + _associatedSigners[account] = signerAddr; + emit ECDSASignerAssociated(account, signerAddr); + } + + /** + * @dev Internal version of {onUninstall} without access control. + */ + function _onUninstall(address account) internal virtual { + delete _associatedSigners[account]; + emit ECDSASignerDisassociated(account); + } + + /** + * @dev Validates the signature using the account's signer. + */ + function _validateSignatureWithSender( + address sender, + bytes32 envelopeHash, + bytes calldata signature + ) internal view virtual override returns (bool) { + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(envelopeHash, signature); + return signer(sender) == recovered && err == ECDSA.RecoverError.NoError; + } +} diff --git a/contracts/account/modules/MultiERC1271Validator.sol b/contracts/account/modules/MultiERC1271Validator.sol new file mode 100644 index 00000000000..f3d4f297b1c --- /dev/null +++ b/contracts/account/modules/MultiERC1271Validator.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {IERC7579Validator, IERC7579Module, MODULE_TYPE_VALIDATOR} from "../../interfaces/IERC7579Module.sol"; +import {EnumerableSet} from "../../utils/structs/EnumerableSet.sol"; +import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol"; +import {ERC4337Utils} from "../utils/ERC4337Utils.sol"; +import {SignatureValidator} from "./SignatureValidator.sol"; + +/** + * @dev A {SignatureValidator} module that supports multiple ERC1271 signers and a threshold. + * + * This module allows to register multiple ERC1271 signers for an account and set a threshold + * of required signatures to validate a user operation. The threshold must be less or equal to + * the number of associated signers. + * + * NOTE: Using this contract as part of the validation phase of an user operation might violate + * the ERC-7562 storage validation rules if any of the signers access storage that's not + * associated with the account contract (e.g. an upgradeable account will access the implementation slot). + */ +abstract contract MultiERC1271Validator is SignatureValidator { + using EnumerableSet for EnumerableSet.AddressSet; + using SignatureChecker for address; + + /** + * @dev Emitted when signers are added to an account. + */ + event ERC1271SignersAdded(address indexed account, address[] indexed signers); + + /** + * @dev Emitted when signers are removed from an account. + */ + event ERC1271SignersRemoved(address indexed account, address[] indexed signers); + + /** + * @dev Emitted when the threshold is changed for an account. + */ + event ThresholdChanged(address indexed account, uint256 threshold); + + /** + * @dev The `signer` already exists for the `account`. + */ + error MultiERC1271SignerAlreadyExists(address account, address signer); + + /** + * @dev The `signer` does not exist for the `account`. + */ + error MultiERC1271SignerDoesNotExist(address account, address signer); + + /** + * @dev The threshold is unreachable for the `account` given the number of `signers`. + */ + error MultiERC1271UnreachableThreshold(address account, uint256 signers, uint256 threshold); + + /** + * @dev The `account` has remaining signers that must be removed before uninstalling the module. + */ + error MultiERC1271RemainingSigners(address account, uint256 remaining); + + mapping(address => EnumerableSet.AddressSet) private _associatedSigners; + mapping(address => uint256) private _associatedThreshold; + + /** + * @dev Amount of account signatures required to approve a multisignature operation. + */ + function threshold(address account) public view virtual returns (uint256) { + return _associatedThreshold[account]; + } + + /** + * @dev Returns whether the `signer` is a signer for the `account`. + */ + function isSigner(address account, address signer) public view virtual returns (bool) { + return _associatedSigners[account].contains(signer); + } + + /** + * @dev Registers `signers` as authorized signers for the `account`. + */ + function addSigners(address[] memory signers) public virtual { + address account = msg.sender; + _addERC1271Signers(account, signers); + _validateThreshold(account); + } + + /** + * @dev Removes `signers` from the authorized signers for the `account`. + */ + function removeSigners(address[] memory signers) public virtual { + address account = msg.sender; + _removeSigners(account, signers); + _validateThreshold(account); + } + + /** + * @dev Sets the amount of signatures required to approve a multisignature operation. + */ + function setThreshold(uint256 threshold_) public virtual { + address account = msg.sender; + _setThreshold(account, threshold_); + _validateThreshold(account); + } + + /** + * @dev Installs the module with the initial `signers` and `threshold_` encoded as `data`. + * + * See {IERC7579Module-onInstall}. + */ + function onInstall(bytes memory data) public virtual { + address account = msg.sender; + (address[] memory signers, uint256 threshold_) = abi.decode(data, (address[], uint256)); + _associatedThreshold[account] = threshold_; + _addERC1271Signers(account, signers); + _validateThreshold(account); + } + + /** + * @dev Uninstalls the module and cleans up the registered signers. + * + * See {IERC7579Module-onUninstall}. + */ + function onUninstall(bytes memory data) public virtual { + address account = msg.sender; + address[] memory signers = abi.decode(data, (address[])); + _associatedThreshold[account] = 0; + _removeSigners(account, signers); + uint256 remaining = _associatedSigners[account].length(); + if (remaining != 0) revert MultiERC1271RemainingSigners(account, remaining); + } + + /** + * @dev Adds the `signers` to the `account` authorized signers. Internal version without access control. + */ + function _addERC1271Signers(address account, address[] memory signers) internal virtual { + for (uint256 i = 0; i < signers.length; i++) { + if (!_associatedSigners[account].add(signers[i])) + revert MultiERC1271SignerAlreadyExists(account, signers[i]); + } + emit ERC1271SignersAdded(account, signers); + } + + /** + * @dev Removes the `signers` from the `account` authorized signers. Internal version without access control. + */ + function _removeSigners(address account, address[] memory signers) internal virtual { + for (uint256 i = 0; i < signers.length; i++) { + if (!_associatedSigners[account].remove(signers[i])) + revert MultiERC1271SignerDoesNotExist(account, signers[i]); + } + emit ERC1271SignersRemoved(account, signers); + } + + /** + * @dev Sets the `threshold_` for the `account`. Internal version without access control. + */ + function _setThreshold(address account, uint256 threshold_) internal virtual { + _associatedThreshold[account] = threshold_; + emit ThresholdChanged(msg.sender, threshold_); + } + + /** + * @dev Validates the current threshold is reachable for the `account`. + */ + function _validateThreshold(address account) internal view virtual { + uint256 signers = _associatedSigners[account].length(); + uint256 _threshold = _associatedThreshold[account]; + if (signers < _threshold) revert MultiERC1271UnreachableThreshold(account, signers, _threshold); + } + + /** + * @dev Validates a signature for a specific sender. + * + * The `signers` MUST be registered signers for the `account`. Also, both the `signers` + * and `signatures` must be in order to validate the signatures correctly. See + * {_decodePackedSignatures}. + * + * NOTE: The `signers` list must be in ascending order to ensure no duplicates. Otherwise, + * the multisignature will be rejected. + */ + function _isValidSignatureWithSender( + address sender, + bytes32 envelopeHash, + bytes calldata signature + ) internal view virtual override returns (bool) { + (address[] calldata signers, bytes[] calldata signatures) = _decodePackedSignatures(signature); + if (signers.length != signatures.length) return false; + return _validateNSignatures(sender, envelopeHash, signers, signatures); + } + + /** + * @dev Checks if the `signatures` are valid for the provided `signers` and `hash`. + * + * Internal version of {_isValidSignatureWithSender}. + */ + function _validateNSignatures( + address account, + bytes32 hash, + address[] calldata signers, + bytes[] calldata signatures + ) private view returns (bool) { + address currentSigner = address(0); + + uint256 signersLength = signers.length; + for (uint256 i = 0; i < signersLength; i++) { + // Signers must be in order to ensure no duplicates + address signer = signers[i]; + if (currentSigner >= signer) return false; + currentSigner = signer; + + if (!_associatedSigners[account].contains(signer) || !signer.isValidSignatureNow(hash, signatures[i])) + return false; + } + + return signersLength >= _associatedThreshold[account]; + } + + /** + * @dev Splits a signature into signers and signatures. + * + * The format of the signature is `abi.encode(signers[], signatures[])`. + */ + function _decodePackedSignatures( + bytes calldata signature + ) internal pure returns (address[] calldata signers, bytes[] calldata signatures) { + assembly ("memory-safe") { + let ptr := add(signature.offset, calldataload(signature.offset)) + + let signersPtr := add(ptr, 0x20) + signers.offset := add(signersPtr, 0x20) + signers.length := calldataload(signersPtr) + + let signaturesPtr := add(signersPtr, signers.length) + signatures.offset := add(signaturesPtr, 0x20) + signatures.length := calldataload(signaturesPtr) + } + } +} diff --git a/contracts/account/modules/P256Validator.sol b/contracts/account/modules/P256Validator.sol new file mode 100644 index 00000000000..25d437378fa --- /dev/null +++ b/contracts/account/modules/P256Validator.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {P256} from "../../utils/cryptography/P256.sol"; +import {SignatureValidator} from "./SignatureValidator.sol"; + +/** + * @dev {SignatureValidator} for {P256} signatures. + */ +abstract contract P256Validator is SignatureValidator { + mapping(address sender => bytes32) private _associatedQx; + mapping(address sender => bytes32) private _associatedQy; + + /** + * @dev Emitted when an account is associated with a P256 public key. + */ + event P256SignerAssociated(address indexed account, bytes32 qx, bytes32 qy); + + /** + * @dev Emitted when an account is disassociated from a P256 public key. + */ + event P256SignerDisassociated(address indexed account); + + /** + * @dev Return the account's signer P256 public key for the given account. + */ + function signer(address account) public view virtual returns (bytes32, bytes32) { + return (_associatedQx[account], _associatedQy[account]); + } + + /** + * @dev Associates an account with a P256 public key. + * + * The `data` is expected to be an `abi.encode(qx, qy)` where `qx` and `qy` are + * the P256 public key components. + */ + function onInstall(bytes calldata data) public virtual { + (bytes32 qx, bytes32 qy) = abi.decode(data, (bytes32, bytes32)); + _onInstall(msg.sender, qx, qy); + } + + /** + * @dev Disassociates an account from a P256 public key. + */ + function onUninstall(bytes calldata) public virtual { + _onUninstall(msg.sender); + } + + /** + * @dev Internal version of {onInstall} without access control. + */ + function _onInstall(address account, bytes32 qx, bytes32 qy) internal virtual { + _associatedQx[account] = qx; + _associatedQy[account] = qy; + emit P256SignerAssociated(account, qx, qy); + } + + /** + * @dev Internal version of {onUninstall} without access control. + */ + function _onUninstall(address account) internal virtual { + delete _associatedQx[account]; + delete _associatedQy[account]; + emit P256SignerDisassociated(account); + } + + /** + * @dev Validate the P256 signature with the account's associated public key. + */ + function _validateSignatureWithSender( + address sender, + bytes32 envelopeHash, + bytes calldata signature + ) internal view virtual override returns (bool) { + if (signature.length < 0x40) return false; + + // parse signature + bytes32 r = bytes32(signature[0x00:0x20]); + bytes32 s = bytes32(signature[0x20:0x40]); + + // fetch and decode immutable public key for the clone + (bytes32 qx, bytes32 qy) = signer(sender); + return P256.verify(envelopeHash, r, s, qx, qy); + } +} diff --git a/contracts/account/modules/RSAValidator.sol b/contracts/account/modules/RSAValidator.sol new file mode 100644 index 00000000000..8bbc0068d30 --- /dev/null +++ b/contracts/account/modules/RSAValidator.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {RSA} from "../../utils/cryptography/RSA.sol"; +import {SignatureValidator} from "./SignatureValidator.sol"; + +/** + * @dev {SignatureValidator} for {RSA} signatures. + */ +abstract contract RSAValidator is SignatureValidator { + mapping(address sender => bytes) private _associatedE; + mapping(address sender => bytes) private _associatedN; + + /** + * @dev Emitted when an account is associated with an RSA public key. + */ + event RSASignerAssociated(address indexed account, bytes e, bytes n); + + /** + * @dev Emitted when an account is disassociated from an RSA public key. + */ + event RSASignerDisassociated(address indexed account); + + /** + * @dev Return the account's signer RSA public key for the given account. + */ + function signer(address account) public view virtual returns (bytes memory e, bytes memory n) { + return (_associatedE[account], _associatedN[account]); + } + + /** + * @dev Associates an account with an RSA public key. + * + * The `data` is expected to be an `abi.encode(e, n)` where `e` and `n` are + * the RSA public key components. + */ + function onInstall(bytes calldata data) public virtual { + (bytes memory e, bytes memory n) = abi.decode(data, (bytes, bytes)); + _onInstall(msg.sender, e, n); + } + + /** + * @dev Disassociates an account from an RSA public key. + */ + function onUninstall(bytes calldata) public virtual { + _onUninstall(msg.sender); + } + + /** + * @dev Internal version of {onInstall} without access control. + */ + function _onInstall(address account, bytes memory e, bytes memory n) internal virtual { + _associatedE[account] = e; + _associatedN[account] = n; + emit RSASignerAssociated(account, e, n); + } + + /** + * @dev Internal version of {onUninstall} without access control. + */ + function _onUninstall(address account) internal virtual { + delete _associatedE[account]; + delete _associatedN[account]; + emit RSASignerDisassociated(account); + } + + /** + * @dev Validate the RSA signature with the account's associated public key. + */ + function _validateSignatureWithSender( + address sender, + bytes32 envelopeHash, + bytes calldata signature + ) internal view virtual override returns (bool) { + (bytes memory e, bytes memory n) = signer(sender); + return RSA.pkcs1(envelopeHash, signature, e, n); + } +} diff --git a/contracts/account/modules/SignatureValidator.sol b/contracts/account/modules/SignatureValidator.sol new file mode 100644 index 00000000000..fb483821eee --- /dev/null +++ b/contracts/account/modules/SignatureValidator.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {IERC5267} from "../../interfaces/IERC5267.sol"; +import {PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {IERC7579Validator, IERC7579Module, MODULE_TYPE_VALIDATOR} from "../../interfaces/IERC7579Module.sol"; +import {ERC4337Utils} from "../utils/ERC4337Utils.sol"; +import {MessageEnvelopeUtils} from "../../utils/cryptography/MessageEnvelopeUtils.sol"; + +/** + * @dev ERC7579 Signature Validator module. + * + * This module provides signature validation for user operations using {MessageEnvelopeUtils} + * for replay protection when validating {IERC1271} signatures. This contract does not + * have a domain itself but it uses the sender's domain separator to validate typed data signatures. + * + * See {_validateSignatureWithSender} for the signature validation logic. + */ +abstract contract SignatureValidator is IERC7579Validator { + bytes32 private constant EIP712_TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + /// @inheritdoc IERC7579Module + function isModuleType(uint256 moduleTypeId) public pure virtual returns (bool) { + return moduleTypeId == MODULE_TYPE_VALIDATOR; + } + + /// @inheritdoc IERC7579Validator + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) public view virtual returns (uint256) { + return + _isValidSignatureWithSender(msg.sender, userOpHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + /// @inheritdoc IERC7579Validator + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata signature + ) public view virtual returns (bytes4) { + return + _isValidSignatureWithSender(sender, hash, signature) + ? IERC1271.isValidSignature.selector + : bytes4(0xffffffff); + } + + /** + * @dev Validates a signature wrapped in a typed data envelope for a specific sender. + * + * If the calculated sender's domain separator is different from the provided sender's domain separator, + * the signature validation defaults to a personal signature envelope validation workflow. + */ + function _isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata signature + ) internal view virtual returns (bool) { + (bytes32 envelopeHash, string memory name, string memory version, bytes calldata sig) = _hashUnwrappedSig( + sender, + signature + ); + if (envelopeHash != hash) + return _validateSignatureWithSender(sender, _personalEnvelopeHash(hash, name, version, sender), signature); + return _validateSignatureWithSender(sender, envelopeHash, sig); + } + + /** + * @dev Hashes the unwrapped signature contents and returns the envelope hash and the original signature. + */ + function _hashUnwrappedSig( + address sender, + bytes calldata signature + ) + internal + view + virtual + returns (bytes32 envelopeHash, string memory name, string memory version, bytes calldata originalSig) + { + bytes32 senderSeparator; + bytes32 contents; + bytes calldata contentsType; + (originalSig, senderSeparator, contents, contentsType) = MessageEnvelopeUtils.unwrapTypedDataSig(signature); + bytes32 envelopeStructHash; + (name, version, envelopeStructHash) = _typedDataEnvelopeStructHash(sender, contents, contentsType); + envelopeHash = MessageEnvelopeUtils.toTypedDataEnvelopeHash(senderSeparator, envelopeStructHash); + } + + /** + * @dev Calculates the hash of the typed data envelope struct. + */ + function _typedDataEnvelopeStructHash( + address sender, + bytes32 contents, + bytes calldata contentsType + ) private view returns (string memory name, string memory version, bytes32 envelopeStructHash) { + address verifyingContract; + bytes32 salt; + uint256[] memory extensions; + (name, version, verifyingContract, salt, extensions) = _appDomain(sender); + + envelopeStructHash = MessageEnvelopeUtils.typedDataEnvelopeStructHash( + contentsType, + contents, + name, + version, + verifyingContract, + salt, + extensions + ); + } + + function _appDomain( + address app + ) + internal + view + returns ( + string memory name, + string memory version, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + bytes memory data = abi.encodePacked(IERC5267.eip712Domain.selector); + bool success; + uint256 retSize; + + // Get domain without revert if the account doesn't have code + assembly ("memory-safe") { + success := staticcall(gas(), app, add(data, 0x20), mload(data), 0, 0) + retSize := returndatasize() + } + + if (!success || retSize < 32) return ("", "", address(0), bytes32(0), new uint256[](0)); + + // Copy the return data + assembly ("memory-safe") { + mstore(data, retSize) + returndatacopy(add(data, 0x20), 0, retSize) + } + + // Decode + (, name, version, , verifyingContract, salt, extensions) = abi.decode( + data, + (bytes1, string, string, uint256, address, bytes32, uint256[]) + ); + } + + /** + * @dev Calculates the hash of the personal signature envelope. + */ + function _personalEnvelopeHash( + bytes32 hash, + string memory name, + string memory version, + address sender + ) private view returns (bytes32) { + return + MessageEnvelopeUtils.toPersonalSignEnvelopeHash( + keccak256( + abi.encode( + EIP712_TYPE_HASH, + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + sender + ) + ), + hash + ); + } + + /** + * @dev Validates a signature for a specific sender. + */ + function _validateSignatureWithSender( + address sender, + bytes32 envelopeHash, + bytes calldata signature + ) internal view virtual returns (bool); +} diff --git a/contracts/account/modules/TypedERC1271Executor.sol b/contracts/account/modules/TypedERC1271Executor.sol new file mode 100644 index 00000000000..abf67151b8a --- /dev/null +++ b/contracts/account/modules/TypedERC1271Executor.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC7579Execution} from "../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, MODULE_TYPE_EXECUTOR} from "../../interfaces/IERC7579Module.sol"; +import {ERC7579Utils, CallType, Execution, Mode} from "../utils/ERC7579Utils.sol"; +import {EIP712} from "../../utils/cryptography/EIP712.sol"; +import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol"; +import {MessageEnvelopeUtils} from "../../utils/cryptography/MessageEnvelopeUtils.sol"; + +/** + * @dev An ERC7579 module that allows to execute an account's operations using {IERC1271} + * to validate typed execution requests. + */ +abstract contract TypedERC1271Executor is EIP712, IERC7579Module { + bytes private constant _EXECUTE_REQUEST_SINGLE_TYPENAME = + bytes("ExecuteSingle(address account,address target,uint256 value,bytes data)"); + bytes private constant _EXECUTE_REQUEST_BATCH_TYPENAME = + bytes("ExecuteBatch(address account,address[] targets,uint256[] values,bytes[] calldatas)"); + bytes private constant _EXECUTE_REQUEST_DELEGATECALL_TYPENAME = + bytes("ExecuteDelegate(address account,address target,bytes data)"); + + bytes32 private constant _EXECUTE_REQUEST_SINGLE_TYPEHASH = keccak256(_EXECUTE_REQUEST_SINGLE_TYPENAME); + bytes32 private constant _EXECUTE_REQUEST_BATCH_TYPEHASH = keccak256(_EXECUTE_REQUEST_BATCH_TYPENAME); + bytes32 private constant _EXECUTE_REQUEST_DELEGATECALL_TYPEHASH = keccak256(_EXECUTE_REQUEST_DELEGATECALL_TYPENAME); + + /** + * @dev The execution request is unauthorized. + */ + error UnauthorizedTypedExecution(CallType callType, address account, address sender); + + /** + * @dev The `account` installed this module. + */ + event ERC7579TypedExecutorInstalled(address indexed account); + + /** + * @dev The `account` uninstalled this module. + */ + event ERC7579TypedExecutorUninstalled(address indexed account); + + /// @inheritdoc IERC7579Module + function isModuleType(uint256 moduleTypeId) public pure virtual returns (bool) { + return moduleTypeId == MODULE_TYPE_EXECUTOR; + } + + /// @inheritdoc IERC7579Module + function onInstall(bytes memory) public virtual { + emit ERC7579TypedExecutorInstalled(msg.sender); + } + + /// @inheritdoc IERC7579Module + function onUninstall(bytes memory) public virtual { + emit ERC7579TypedExecutorUninstalled(msg.sender); + } + + /** + * @dev Executes an account's operation following ERC7579's execution mode with support for single, batch, and delegate calls. + */ + function execute(address account, Mode mode, bytes calldata request, bytes calldata signature) public virtual { + (CallType callType, , , ) = ERC7579Utils.decodeMode(mode); + if (callType == ERC7579Utils.CALLTYPE_SINGLE) return _single(mode, account, request, signature); + if (callType == ERC7579Utils.CALLTYPE_BATCH) return _batch(mode, account, request, signature); + if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) return _delegate(mode, account, request, signature); + revert ERC7579Utils.ERC7579UnsupportedCallType(callType); + } + + /** + * @dev Calls {AccountERC7579-executeFromExecutor} with a single call operation. + */ + function _single(Mode mode, address account, bytes calldata request, bytes calldata signature) internal virtual { + (address target, uint256 value, bytes calldata data) = ERC7579Utils.decodeSingle(request); + bytes32 requestHash = _hashTypedDataV4(_singleStructHash(account, target, value, data)); + if (!_isValidExecuteRequest(account, requestHash, signature, _EXECUTE_REQUEST_SINGLE_TYPENAME)) + revert UnauthorizedTypedExecution(ERC7579Utils.CALLTYPE_SINGLE, account, msg.sender); + IERC7579Execution(account).executeFromExecutor( + Mode.unwrap(mode), + ERC7579Utils.encodeSingle(target, value, data) + ); + } + + /** + * @dev Calls {AccountERC7579-executeFromExecutor} with batched calls. + */ + function _batch(Mode mode, address account, bytes calldata request, bytes calldata signature) internal virtual { + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = abi.decode( + request, + (address[], uint256[], bytes[]) + ); + bytes32 requestHash = _hashTypedDataV4(_batchStructHash(account, targets, values, calldatas)); + if (!_isValidExecuteRequest(account, requestHash, signature, _EXECUTE_REQUEST_BATCH_TYPENAME)) + revert UnauthorizedTypedExecution(ERC7579Utils.CALLTYPE_BATCH, account, msg.sender); + Execution[] calldata executions = ERC7579Utils.decodeBatch(request); + IERC7579Execution(account).executeFromExecutor(Mode.unwrap(mode), ERC7579Utils.encodeBatch(executions)); + } + + /** + * @dev Calls {AccountERC7579-executeFromExecutor} with a delegate call operation. + */ + function _delegate(Mode mode, address account, bytes calldata request, bytes calldata signature) internal virtual { + (address target, bytes calldata data) = ERC7579Utils.decodeDelegate(request); + bytes32 requestHash = _hashTypedDataV4(_delegateStructHash(account, target, data)); + if (!_isValidExecuteRequest(account, requestHash, signature, _EXECUTE_REQUEST_DELEGATECALL_TYPENAME)) + revert UnauthorizedTypedExecution(ERC7579Utils.CALLTYPE_DELEGATECALL, account, msg.sender); + IERC7579Execution(account).executeFromExecutor(Mode.unwrap(mode), ERC7579Utils.encodeDelegate(target, data)); + } + + /** + * @dev Checks whether the execution request was signed by the `account`. + */ + function _isValidExecuteRequest( + address account, + bytes32 requestHash, + bytes calldata signature, + bytes memory contentsType + ) internal view virtual returns (bool) { + bytes memory _signature = MessageEnvelopeUtils.wrapTypedDataSig( + signature, + _domainSeparatorV4(), + requestHash, + contentsType + ); + return SignatureChecker.isValidSignatureNow(account, requestHash, _signature); + } + + /** + * @dev Calculates the hash of a single execution request. + */ + function _singleStructHash( + address account, + address target, + uint256 value, + bytes calldata data + ) internal view virtual returns (bytes32) { + return keccak256(abi.encode(_EXECUTE_REQUEST_SINGLE_TYPEHASH, account, target, value, keccak256(data))); + } + + /** + * @dev Calculates the hash of a batched execution request. + */ + function _batchStructHash( + address account, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas + ) internal view virtual returns (bytes32) { + uint256 length = calldatas.length; + bytes32[] memory dataHashes = new bytes32[](length); + for (uint256 i = 0; i < length; i++) { + dataHashes[i] = keccak256(calldatas[i]); + } + bytes memory content = abi.encode( + _EXECUTE_REQUEST_BATCH_TYPEHASH, + account, + targets, + values, + keccak256(abi.encodePacked(dataHashes)) + ); + return keccak256(content); + } + + /** + * @dev Calculates the hash of a delegate call execution request. + */ + function _delegateStructHash( + address account, + address target, + bytes memory data + ) internal view virtual returns (bytes32) { + return keccak256(abi.encode(_EXECUTE_REQUEST_DELEGATECALL_TYPEHASH, account, target, keccak256(data))); + } +} diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/account/utils/ERC4337Utils.sol similarity index 95% rename from contracts/abstraction/utils/ERC4337Utils.sol rename to contracts/account/utils/ERC4337Utils.sol index 3d95db10815..58ef861aa01 100644 --- a/contracts/abstraction/utils/ERC4337Utils.sol +++ b/contracts/account/utils/ERC4337Utils.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.20; import {IEntryPoint, PackedUserOperation} from "../../interfaces/IERC4337.sol"; import {Math} from "../../utils/math/Math.sol"; -// import {Memory} from "../../utils/Memory.sol"; import {Packing} from "../../utils/Packing.sol"; library ERC4337Utils { @@ -62,8 +61,8 @@ library ERC4337Utils { if (validationData == 0) { return (address(0), false); } else { - (address agregator, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData); - return (agregator, block.timestamp > validUntil || block.timestamp < validAfter); + (address aggregator_, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData); + return (aggregator_, block.timestamp > validUntil || block.timestamp < validAfter); } } @@ -77,7 +76,6 @@ library ERC4337Utils { address entrypoint, uint256 chainid ) internal pure returns (bytes32) { - // Memory.FreePtr ptr = Memory.save(); bytes32 result = keccak256( abi.encode( keccak256( @@ -96,7 +94,6 @@ library ERC4337Utils { chainid ) ); - // Memory.load(ptr); return result; } diff --git a/contracts/account/utils/ERC7579Utils.sol b/contracts/account/utils/ERC7579Utils.sol new file mode 100644 index 00000000000..4f4ceb4b7d4 --- /dev/null +++ b/contracts/account/utils/ERC7579Utils.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Execution} from "../../interfaces/IERC7579Account.sol"; +import {Packing} from "../../utils/Packing.sol"; +import {Address} from "../../utils/Address.sol"; + +type Mode is bytes32; +type CallType is bytes1; +type ExecType is bytes1; +type ModeSelector is bytes4; +type ModePayload is bytes22; + +library ERC7579Utils { + using Packing for *; + + CallType constant CALLTYPE_SINGLE = CallType.wrap(0x00); + CallType constant CALLTYPE_BATCH = CallType.wrap(0x01); + CallType constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); + + ExecType constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); + ExecType constant EXECTYPE_TRY = ExecType.wrap(0x01); + + event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes result); + + error ERC7579UnsupportedCallType(CallType callType); + error ERC7579UnsupportedExecType(ExecType execType); + error ERC7579MismatchedModuleTypeId(uint256 moduleTypeId, address module); + error ERC7579UninstalledModule(uint256 moduleTypeId, address module); + error ERC7579AlreadyInstalledModule(uint256 moduleTypeId, address module); + error ERC7579UnsupportedModuleType(uint256 moduleTypeId); + + function execSingle( + ExecType execType, + bytes calldata executionCalldata + ) internal returns (bytes[] memory returnData) { + (address target, uint256 value, bytes calldata callData) = decodeSingle(executionCalldata); + returnData = new bytes[](1); + returnData[0] = _call(0, execType, target, value, callData); + } + + function execBatch( + ExecType execType, + bytes calldata executionCalldata + ) internal returns (bytes[] memory returnData) { + Execution[] calldata executionBatch = decodeBatch(executionCalldata); + returnData = new bytes[](executionBatch.length); + for (uint256 i = 0; i < executionBatch.length; ++i) { + returnData[i] = _call( + i, + execType, + executionBatch[i].target, + executionBatch[i].value, + executionBatch[i].callData + ); + } + } + + function execDelegateCall( + ExecType execType, + bytes calldata executionCalldata + ) internal returns (bytes[] memory returnData) { + (address target, bytes calldata callData) = decodeDelegate(executionCalldata); + returnData = new bytes[](1); + (bool success, bytes memory returndata) = target.delegatecall(callData); + returnData[0] = returndata; + _validateExecutionMode(0, execType, success, returndata); + } + + function _call( + uint256 index, + ExecType execType, + address target, + uint256 value, + bytes calldata data + ) private returns (bytes memory) { + (bool success, bytes memory returndata) = target.call{value: value}(data); + return _validateExecutionMode(index, execType, success, returndata); + } + + function _validateExecutionMode( + uint256 index, + ExecType execType, + bool success, + bytes memory returndata + ) private returns (bytes memory) { + if (execType == ERC7579Utils.EXECTYPE_DEFAULT) return Address.verifyCallResult(success, returndata); + if (execType == ERC7579Utils.EXECTYPE_TRY) { + if (!success) emit ERC7579TryExecuteFail(index, returndata); + return returndata; + } + revert ERC7579UnsupportedExecType(execType); + } + + function encodeMode( + CallType callType, + ExecType execType, + ModeSelector selector, + ModePayload payload + ) internal pure returns (Mode mode) { + return + Mode.wrap( + CallType + .unwrap(callType) + .pack_1_1(ExecType.unwrap(execType)) + .pack_2_4(bytes4(0)) + .pack_6_4(ModeSelector.unwrap(selector)) + .pack_10_22(ModePayload.unwrap(payload)) + ); + } + + function decodeMode( + Mode mode + ) internal pure returns (CallType callType, ExecType execType, ModeSelector selector, ModePayload payload) { + return ( + CallType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 0)), + ExecType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 1)), + ModeSelector.wrap(Packing.extract_32_4(Mode.unwrap(mode), 6)), + ModePayload.wrap(Packing.extract_32_22(Mode.unwrap(mode), 10)) + ); + } + + function encodeSingle( + address target, + uint256 value, + bytes calldata callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, value, callData); + } + + function decodeSingle( + bytes calldata executionCalldata + ) internal pure returns (address target, uint256 value, bytes calldata callData) { + target = abi.decode(executionCalldata[0:20], (address)); + value = abi.decode(executionCalldata[20:52], (uint256)); + callData = executionCalldata[52:]; + } + + function encodeDelegate( + address target, + bytes calldata callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, callData); + } + + function decodeDelegate( + bytes calldata executionCalldata + ) internal pure returns (address target, bytes calldata callData) { + target = abi.decode(executionCalldata[0:20], (address)); + callData = executionCalldata[20:]; + } + + function encodeBatch(Execution[] memory executionBatch) internal pure returns (bytes memory executionCalldata) { + return abi.encode(executionBatch); + } + + function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) { + assembly ("memory-safe") { + let ptr := add(executionCalldata.offset, calldataload(executionCalldata.offset)) + // Extract the ERC7579 Executions + executionBatch.offset := add(ptr, 32) + executionBatch.length := calldataload(ptr) + } + } +} + +// Operators +using {_eqCallTypeGlobal as ==} for CallType global; +using {_eqExecTypeGlobal as ==} for ExecType global; +using {_eqModeSelectorGlobal as ==} for ModeSelector global; +using {_eqModePayloadGlobal as ==} for ModePayload global; + +function _eqCallTypeGlobal(CallType a, CallType b) pure returns (bool) { + return CallType.unwrap(a) == CallType.unwrap(b); +} + +function _eqExecTypeGlobal(ExecType a, ExecType b) pure returns (bool) { + return ExecType.unwrap(a) == ExecType.unwrap(b); +} + +function _eqModeSelectorGlobal(ModeSelector a, ModeSelector b) pure returns (bool) { + return ModeSelector.unwrap(a) == ModeSelector.unwrap(b); +} + +function _eqModePayloadGlobal(ModePayload a, ModePayload b) pure returns (bool) { + return ModePayload.unwrap(a) == ModePayload.unwrap(b); +} diff --git a/contracts/interfaces/IERC7579Account.sol b/contracts/interfaces/IERC7579Account.sol index 0be805f5b1f..01a1950c90f 100644 --- a/contracts/interfaces/IERC7579Account.sol +++ b/contracts/interfaces/IERC7579Account.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; -// import { CallType, ExecType, ModeCode } from "../lib/ModeLib.sol"; -import {IERC165} from "./IERC165.sol"; -import {IERC1271} from "./IERC1271.sol"; +pragma solidity ^0.8.20; struct Execution { address target; diff --git a/contracts/interfaces/IERC7579Module.sol b/contracts/interfaces/IERC7579Module.sol index 1aee412c053..8be9445530f 100644 --- a/contracts/interfaces/IERC7579Module.sol +++ b/contracts/interfaces/IERC7579Module.sol @@ -5,7 +5,6 @@ import {PackedUserOperation} from "./IERC4337.sol"; uint256 constant VALIDATION_SUCCESS = 0; uint256 constant VALIDATION_FAILED = 1; -uint256 constant MODULE_TYPE_SIGNER = 0; uint256 constant MODULE_TYPE_VALIDATOR = 1; uint256 constant MODULE_TYPE_EXECUTOR = 2; uint256 constant MODULE_TYPE_FALLBACK = 3; diff --git a/contracts/mocks/CallReceiverMock.sol b/contracts/mocks/CallReceiverMock.sol index e371c7db800..7f6bfd0bbd2 100644 --- a/contracts/mocks/CallReceiverMock.sol +++ b/contracts/mocks/CallReceiverMock.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.20; contract CallReceiverMock { event MockFunctionCalled(); event MockFunctionCalledWithArgs(uint256 a, uint256 b); + event MockFunctionCalledExtra(address caller, uint256 value); uint256[] private _array; @@ -14,6 +15,10 @@ contract CallReceiverMock { return "0x1234"; } + function mockFunctionExtra() public payable { + emit MockFunctionCalledExtra(msg.sender, msg.value); + } + function mockFunctionEmptyReturn() public payable { emit MockFunctionCalled(); } diff --git a/contracts/mocks/ERC1271TypedSignerECDSA.sol b/contracts/mocks/ERC1271TypedSignerECDSA.sol new file mode 100644 index 00000000000..b7e02aba8cf --- /dev/null +++ b/contracts/mocks/ERC1271TypedSignerECDSA.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ECDSA} from "../utils/cryptography/ECDSA.sol"; +import {ERC1271TypedSigner} from "../utils/cryptography/ERC1271TypedSigner.sol"; +import {EIP712} from "../utils/cryptography/EIP712.sol"; + +contract ERC1271TypedSignerECDSA is ERC1271TypedSigner { + address private immutable _signer; + + constructor(address signerAddr) EIP712("ERC1271TypedSignerECDSA", "1") { + _signer = signerAddr; + } + + function _validateSignature(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; + } +} diff --git a/contracts/mocks/ERC1271TypedSignerP256.sol b/contracts/mocks/ERC1271TypedSignerP256.sol new file mode 100644 index 00000000000..0355babc7f8 --- /dev/null +++ b/contracts/mocks/ERC1271TypedSignerP256.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {P256} from "../utils/cryptography/P256.sol"; +import {ERC1271TypedSigner} from "../utils/cryptography/ERC1271TypedSigner.sol"; +import {EIP712} from "../utils/cryptography/EIP712.sol"; + +contract ERC1271TypedSignerP256 is ERC1271TypedSigner { + bytes32 private immutable _qx; + bytes32 private immutable _qy; + + constructor(bytes32 qx, bytes32 qy) EIP712("ERC1271TypedSignerP256", "1") { + _qx = qx; + _qy = qy; + } + + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + bytes32 r = bytes32(signature[0x00:0x20]); + bytes32 s = bytes32(signature[0x20:0x40]); + return P256.verify(hash, r, s, _qx, _qy); + } +} diff --git a/contracts/mocks/ERC1271TypedSignerRSA.sol b/contracts/mocks/ERC1271TypedSignerRSA.sol new file mode 100644 index 00000000000..c548218fe8c --- /dev/null +++ b/contracts/mocks/ERC1271TypedSignerRSA.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {RSA} from "../utils/cryptography/RSA.sol"; +import {ERC1271TypedSigner} from "../utils/cryptography/ERC1271TypedSigner.sol"; +import {EIP712} from "../utils/cryptography/EIP712.sol"; + +contract ERC1271TypedSignerRSA is ERC1271TypedSigner { + bytes private _e; + bytes private _n; + + constructor(bytes memory e, bytes memory n) EIP712("ERC1271TypedSignerRSA", "1") { + _e = e; + _n = n; + } + + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + return RSA.pkcs1(hash, signature, _e, _n); + } +} diff --git a/contracts/mocks/account/AccountBaseMock.sol b/contracts/mocks/account/AccountBaseMock.sol new file mode 100644 index 00000000000..d2b64f853ac --- /dev/null +++ b/contracts/mocks/account/AccountBaseMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {AccountBase} from "../../account/AccountBase.sol"; +import {ERC4337Utils} from "../../account/utils/ERC4337Utils.sol"; + +contract AccountBaseMock is AccountBase { + /// Validates a user operation with a boolean signature. + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 /* userOpHash */ + ) internal pure override returns (uint256 validationData) { + return + bytes1(userOp.signature[0:1]) == bytes1(0x01) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } +} diff --git a/contracts/mocks/account/modules/SignatureValidatorMock.sol b/contracts/mocks/account/modules/SignatureValidatorMock.sol new file mode 100644 index 00000000000..23cc010b6cb --- /dev/null +++ b/contracts/mocks/account/modules/SignatureValidatorMock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {SignatureValidator} from "../../../account/modules/SignatureValidator.sol"; + +contract SignatureValidatorMock is SignatureValidator { + function _validateSignatureWithSender( + address /* sender */, + bytes32 /* envelopeHash */, + bytes calldata signature + ) internal pure override returns (bool) { + return bytes1(signature[0:1]) == bytes1(0x01); + } + + function onInstall(bytes memory data) public virtual { + // do nothing + } + + function onUninstall(bytes memory data) public virtual { + // do nothing + } +} diff --git a/contracts/token/ERC1155/utils/ERC1155Holder.sol b/contracts/token/ERC1155/utils/ERC1155Holder.sol index 35be58c5238..c32da520acd 100644 --- a/contracts/token/ERC1155/utils/ERC1155Holder.sol +++ b/contracts/token/ERC1155/utils/ERC1155Holder.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {IERC165, ERC165} from "../../../utils/introspection/ERC165.sol"; -import {IERC1155Receiver} from "../IERC1155Receiver.sol"; +import {ERC1155HolderLean, IERC1155Receiver} from "./ERC1155HolderLean.sol"; /** * @dev Simple implementation of `IERC1155Receiver` that will allow a contract to hold ERC-1155 tokens. @@ -12,31 +12,11 @@ import {IERC1155Receiver} from "../IERC1155Receiver.sol"; * IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be * stuck. */ -abstract contract ERC1155Holder is ERC165, IERC1155Receiver { +abstract contract ERC1155Holder is ERC165, ERC1155HolderLean { /** * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); } - - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155Received.selector; - } - - function onERC1155BatchReceived( - address, - address, - uint256[] memory, - uint256[] memory, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155BatchReceived.selector; - } } diff --git a/contracts/token/ERC1155/utils/ERC1155HolderLean.sol b/contracts/token/ERC1155/utils/ERC1155HolderLean.sol new file mode 100644 index 00000000000..196f470efb5 --- /dev/null +++ b/contracts/token/ERC1155/utils/ERC1155HolderLean.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/utils/ERC1155Holder.sol) + +pragma solidity ^0.8.20; + +import {IERC1155Receiver} from "../IERC1155Receiver.sol"; + +/** + * @dev Version of {ERC1155Holder} that doesn't include {IERC165} detection. + */ +abstract contract ERC1155HolderLean is IERC1155Receiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } +} diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 0ef3e5387c8..7b65cd79fa0 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -14,6 +14,8 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Hashes}: Commonly used hash functions. * {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs. * {EIP712}: Contract with functions to allow processing signed typed structure data according to https://eips.ethereum.org/EIPS/eip-712[EIP-712]. + * {MessageEnvelopeUtils}: A enveloped hashing scheme that prevents replayability of smart contract signatures leveraging EIP-712 typing. + * {ERC1271TypedSigner}: A base contract to validate signatures using {MessageEnvelopeUtils}. * {ReentrancyGuard}: A modifier that can prevent reentrancy during certain functions. * {ReentrancyGuardTransient}: Variant of {ReentrancyGuard} that uses transient storage (https://eips.ethereum.org/EIPS/eip-1153[EIP-1153]). * {Pausable}: A common emergency response mechanism that can pause functionality while a remediation is pending. @@ -60,6 +62,10 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable {{EIP712}} +{{MessageEnvelopeUtils}} + +{{ERC1271TypedSigner}} + {{MessageHashUtils}} {{SignatureChecker}} diff --git a/contracts/utils/cryptography/ERC1271TypedSigner.sol b/contracts/utils/cryptography/ERC1271TypedSigner.sol new file mode 100644 index 00000000000..a3d225f891f --- /dev/null +++ b/contracts/utils/cryptography/ERC1271TypedSigner.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {EIP712} from "./EIP712.sol"; +import {MessageHashUtils} from "./MessageHashUtils.sol"; +import {MessageEnvelopeUtils} from "./MessageEnvelopeUtils.sol"; +import {ShortStrings} from "../ShortStrings.sol"; + +/** + * @dev Validates signatures wrapping the message hash in an EIP712 envelope. See {MessageEnvelopeUtils}. + * + * Linking the signature to the EIP-712 domain separator is a security measure to prevent signature replay across different + * EIP-712 domains (e.g. a single offchain owner of multiple contracts). + * + * This contract requires implementing the {_validateSignature} function, which passes the wrapped message hash, + * which may be either an typed data or a personal sign envelope. + * + * NOTE: {EIP712} uses {ShortStrings} to optimize gas costs for short strings (up to 31 characters). + * Consider that strings longer than that will use storage, which may limit the ability of the signer to + * be used within the ERC-4337 validation phase (due to ERC-7562 storage access rules). + */ +abstract contract ERC1271TypedSigner is EIP712, IERC1271 { + using MessageEnvelopeUtils for *; + + /** + * @dev Attempts validating the signature in an nested EIP-712 envelope. + * + * A nested EIP-712 envelope might be presented in 2 different ways: + * + * - As a nested EIP-712 typed data + * - As a _personal_ signature (an EIP-712 mimic of the `eth_personalSign` for a smart contract) + */ + function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4 result) { + return _isValidSignature(hash, signature) ? IERC1271.isValidSignature.selector : bytes4(0xffffffff); + } + + /** + * @dev Internal version of {isValidSignature} that returns a boolean. + */ + function _isValidSignature(bytes32 hash, bytes calldata signature) internal view virtual returns (bool) { + return + _isValidPersonalSigEnvelopeSignature(hash, signature) || + _isValidTypedDataEnvelopeSignature(hash, signature); + } + + /** + * @dev EIP-712 typed data envelope verification. + */ + function _isValidTypedDataEnvelopeSignature( + bytes32 hash, + bytes calldata signature + ) internal view virtual returns (bool) { + (bytes calldata originalSignature, bytes32 envelopeHash) = _typedDataEnvelopeHash(signature); + return hash == envelopeHash && _validateSignature(envelopeHash, originalSignature); + } + + /** + * @dev Personal signature envelope verification. + */ + function _isValidPersonalSigEnvelopeSignature( + bytes32 hash, + bytes calldata signature + ) internal view virtual returns (bool) { + return _validateSignature(_personalSigEnvelopeHash(hash), signature); + } + + /** + * @dev EIP-712 typed data envelope verification. + * + * See {MessageEnvelopeUtils-toTypedDataEnvelopeHash} for the envelope structure. + */ + function _typedDataEnvelopeHash( + bytes calldata signature + ) internal view virtual returns (bytes calldata originalSignature, bytes32 result) { + bytes32 appSeparator; + bytes32 contents; + bytes calldata contentsType; + (originalSignature, appSeparator, contents, contentsType) = signature.unwrapTypedDataSig(); + + ( + , + string memory name, + string memory version, + , + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) = eip712Domain(); + + result = MessageEnvelopeUtils.toTypedDataEnvelopeHash( + appSeparator, + MessageEnvelopeUtils.typedDataEnvelopeStructHash( + contentsType, + contents, + name, + version, + verifyingContract, + salt, + extensions + ) + ); + } + + /** + * @dev See {MessageEnvelopeUtils-toPersonalSignEnvelopeHash}. + */ + function _personalSigEnvelopeHash(bytes32 hash) internal view virtual returns (bytes32) { + return _domainSeparatorV4().toPersonalSignEnvelopeHash(hash); + } + + /** + * @dev Signature validation algorithm. + * + * WARNING: Implementing a signature validation algorithm is a security-sensitive operation as it involves + * cryptographic verification. It is important to review and test thoroughly before deployment. Consider + * using one of the signature verification libraries ({ECDSA}, {P256} or {RSA}). + */ + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual returns (bool); +} diff --git a/contracts/utils/cryptography/MessageEnvelopeUtils.sol b/contracts/utils/cryptography/MessageEnvelopeUtils.sol new file mode 100644 index 00000000000..62959c84ade --- /dev/null +++ b/contracts/utils/cryptography/MessageEnvelopeUtils.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC5267} from "../../interfaces/IERC5267.sol"; +import {MessageHashUtils} from "./MessageHashUtils.sol"; + +/** + * @dev Utilities to produce and process {EIP712} typed data signatures with an application envelope. + * + * Typed data envelopes are useful for smart contracts that validate signatures (e.g. Accounts), + * as these allow contracts to validate signatures of wrapped data structures that include their + * {EIP712-_domainSeparatorV4}. + * + * In this way, an off-chain signer can be sure that the signature is only valid for the specific + * domain. For developers, there might be 2 ways of validating smart contract messages: + * + * - As an application validating a typed data signature. See {toTypedDataEnvelopeHash}. + * - As a smart contract validating a raw message signature. See {toPersonalSignEnvelopeHash}. + * + * NOTE: A provider for a smart contract wallet would need to return this signature as the + * result of a call to `personal_sign` or `eth_signTypedData`, and this may be unsupported by + * API clients that expect a return value of 129 bytes, or specifically the `r,s,v` parameters + * of an {ECDSA} signature, as is for example specified for {EIP712}. + */ +library MessageEnvelopeUtils { + /** + * @dev An EIP-712 typed to represent "personal" signatures + * (i.e. mimic of `eth_personalSign` for smart contracts). + */ + bytes32 internal constant _PERSONAL_SIGN_ENVELOPE_TYPEHASH = keccak256("PersonalSign(bytes prefixed)"); + + /** + * @dev Error when the contents type is invalid. See {tryValidateContentsType}. + */ + error InvalidContentsType(); + + /** + * @dev Parses a nested signature into its components. See {nest}. + * + * Constructed as follows: + * + * `signature ā€– DOMAIN_SEPARATOR ā€– contents ā€– contentsType ā€– uint16(contentsType.length)` + * + * - `signature` is the original signature for the envelope including the `contents` hash + * - `DOMAIN_SEPARATOR` is the EIP-712 {EIP712-_domainSeparatorV4} of the smart contract verifying the signature + * - `contents` is the hash of the underlying data structure or message + * - `contentsType` is the EIP-712 type of the envelope (e.g. {TYPED_DATA_ENVELOPE_TYPEHASH} or {_PERSONAL_SIGN_ENVELOPE_TYPEHASH}) + */ + function unwrapTypedDataSig( + bytes calldata signature + ) + internal + pure + returns (bytes calldata originalSig, bytes32 appSeparator, bytes32 contents, bytes calldata contentsType) + { + uint256 sigLength = signature.length; + if (sigLength < 66) return (signature[0:0], 0, 0, signature[0:0]); + + uint256 contentsTypeEnd = sigLength - 2; // Last 2 bytes + uint256 contentsTypeLength = uint16(bytes2(signature[contentsTypeEnd:sigLength])); + if (contentsTypeLength > contentsTypeEnd) return (signature[0:0], 0, 0, signature[0:0]); + + uint256 contentsEnd = contentsTypeEnd - contentsTypeLength; + if (contentsEnd < 64) return (signature[0:0], 0, 0, signature[0:0]); + + uint256 appSeparatorEnd = contentsEnd - 32; + uint256 originalSigEnd = appSeparatorEnd - 32; + + originalSig = signature[0:originalSigEnd]; + appSeparator = bytes32(signature[originalSigEnd:appSeparatorEnd]); + contents = bytes32(signature[appSeparatorEnd:contentsEnd]); + contentsType = signature[contentsEnd:contentsTypeEnd]; + } + + /** + * @dev Nest a signature for a given EIP-712 type into an envelope for the domain `separator`. + * + * Counterpart of {unwrapTypedDataSig} to extract the original signature and the nested components. + */ + function wrapTypedDataSig( + bytes memory signature, + bytes32 separator, + bytes32 contents, + bytes memory contentsType + ) internal pure returns (bytes memory) { + return abi.encodePacked(signature, separator, contents, contentsType, uint16(contentsType.length)); + } + + /** + * @dev Wraps a `contents` digest into an envelope that simulates the `eth_personalSign` RPC + * method in the context of smart contracts. + * + * This envelope uses the {_PERSONAL_SIGN_ENVELOPE_TYPEHASH} type to wrap the `contents` + * hash in an EIP-712 envelope for the current domain `separator`. + * + * To produce a signature for this envelope, the signer must sign a wrapped message hash: + * + * ```solidity + * bytes32 hash = keccak256(abi.encodePacked( + * \x19\x01, + * CURRENT_DOMAIN_SEPARATOR, + * keccak256( + * abi.encode( + * keccak256("PersonalSign(bytes prefixed)"), + * keccak256(abi.encode("\x19Ethereum Signed Message:\n32",contents)) + * ) + * ) + * )); + * ``` + */ + function toPersonalSignEnvelopeHash(bytes32 separator, bytes32 contents) internal pure returns (bytes32) { + return + MessageHashUtils.toTypedDataHash( + separator, + keccak256( + abi.encode( + MessageEnvelopeUtils._PERSONAL_SIGN_ENVELOPE_TYPEHASH, + MessageHashUtils.toEthSignedMessageHash(contents) + ) + ) + ); + } + + /** + * @dev Wraps an {EIP712} typed data `contents` digest into an envelope that simulates the + * `eth_signTypedData` RPC method in the context of smart contracts. + * + * This envelope uses the {TYPED_DATA_ENVELOPE_TYPEHASH} type to nest the `contents` hash in + * an EIP-712 envelope for the current domain. + * + * To produce a signature for this envelope, the signer must sign a wrapped typed data hash: + * + * ```solidity + * bytes32 hash = keccak256( + * abi.encodePacked( + * \x19\x01, + * separator, // The domain separator of the application contract + * keccak256( + * abi.encode( + * TYPED_DATA_ENVELOPE_TYPEHASH(contentsType), // See {TYPED_DATA_ENVELOPE_TYPEHASH} + * contents, + * // See {IERC5267-eip712Domain} for the following arguments from the verifying contract's domain + * keccak256(bytes(name)), + * keccak256(bytes(version)), + * chainId, + * verifyingContract, + * salt, + * keccak256(abi.encodePacked(extensions)) + * ) + * ) + * ) + *); + * + * NOTE: The arguments should be those of the verifying application. See {EIP712-_domainSeparatorV4} + * and {IERC5267-eip712Domain} for more details of how to obtain these values. Respectively, they + * must be obtained from the verifying contract (e.g. an Account) and the application domain. + */ + function toTypedDataEnvelopeHash( + bytes32 separator, + bytes32 hashedTypedDataEnvelopeStruct + ) internal pure returns (bytes32 result) { + result = MessageHashUtils.toTypedDataHash(separator, hashedTypedDataEnvelopeStruct); + } + + /** + * @dev Computes the wrapped EIP-712 type hash for the given contents type. + * + * The `contentsTypeName` is the string name in the app's domain before the parentheses + * (e.g. Transfer in `Transfer(address to,uint256 amount)`). + * + * ```solidity + * TypedDataSign({contentsTypeName},bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions){contentsType} + * ``` + * + * Requirements: + * - `contentsType` must be a valid EIP-712 type (see {tryValidateContentsType}) + */ + // solhint-disable-next-line func-name-mixedcase + function TYPED_DATA_ENVELOPE_TYPEHASH(bytes calldata contentsType) internal pure returns (bytes32) { + (bool valid, bytes calldata contentsTypeName) = tryValidateContentsType(contentsType); + if (!valid) revert InvalidContentsType(); + return TYPED_DATA_ENVELOPE_TYPEHASH(contentsType, contentsTypeName); + } + + // solhint-disable-next-line func-name-mixedcase + function TYPED_DATA_ENVELOPE_TYPEHASH( + bytes calldata contentsType, + bytes calldata contentsTypeName + ) internal pure returns (bytes32) { + return + keccak256( + abi.encodePacked( + "TypedDataSign(", + contentsTypeName, + "bytes1 fields,", + "string name,", + "string version,", + "uint256 chainId,", + "address verifyingContract,", + "bytes32 salt,", + "uint256[] extensions", + ")", + contentsType + ) + ); + } + + /** + * @dev Try to validate the contents type is a valid EIP-712 type. + * + * A valid `contentsType` is considered invalid if it's empty or it: + * + * - Starts with a-z or ( + * - Contains any of the following bytes: , )\x00 + */ + function tryValidateContentsType( + bytes calldata contentsType + ) internal pure returns (bool valid, bytes calldata contentsTypeName) { + uint256 contentsTypeLength = contentsType.length; + if (contentsTypeLength == 0) return (false, contentsType[0:0]); // Empty + + // Does not start with a-z or ( + bytes1 high = contentsType[0]; + if ((high >= 0x61 && high <= 0x7a) || high == 0x28) return (false, contentsType[0:0]); // a-z or ( + + // Find the start of the arguments + uint256 argsStart = _indexOf(contentsType, bytes1("(")); + if (argsStart == contentsTypeLength) return (false, contentsType[0:0]); + + contentsType = contentsType[0:argsStart]; + + // Forbidden characters + for (uint256 i = 0; i < argsStart; i++) { + // Look for any of the following bytes: , )\x00 + bytes1 current = contentsType[i]; + if (current == 0x2c || current == 0x29 || current == 0x32 || current == 0x00) + return (false, contentsType[0:0]); + } + + return (true, contentsType); + } + + /** + * @dev Computes the hash of the envelope struct for the given contents. + */ + function typedDataEnvelopeStructHash( + bytes calldata contentsType, + bytes32 contents, + string memory name, + string memory version, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) internal view returns (bytes32 result) { + (, bytes calldata contentsTypeName) = tryValidateContentsType(contentsType); + result = keccak256( + abi.encode( + TYPED_DATA_ENVELOPE_TYPEHASH(contentsType, contentsTypeName), + contents, + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract, + salt, + keccak256(abi.encodePacked(extensions)) + ) + ); + } + + function _indexOf(bytes calldata buffer, bytes1 lookup) private pure returns (uint256) { + uint256 length = buffer.length; + for (uint256 i = 0; i < length; i++) { + if (buffer[i] == lookup) return i; + } + return length; + } +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol index 28c26f98e6c..b80dc4d6b27 100644 --- a/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol @@ -118,7 +118,7 @@ interface IEntryPoint is IStakeManager, INonceManager { * A custom revert error of handleOps, to report a revert by account or paymaster. * @param opIndex - Index into the array of ops to the failed one (in simulateValidation, this is always zero). * @param reason - Revert reason. see FailedOp(uint256,string), above - * @param inner - data from inner cought revert reason + * @param inner - data from inner caught revert reason * @dev note that inner is truncated to 2048 bytes */ error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 15af8b40ea3..778061db102 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -6,6 +6,8 @@ * xref:backwards-compatibility.adoc[Backwards Compatibility] * xref:access-control.adoc[Access Control] +* xref:account-abstraction.adoc[Account Abstraction] +** xref:erc7579.adoc[ERC-7579] * xref:tokens.adoc[Tokens] ** xref:erc20.adoc[ERC-20] diff --git a/docs/modules/ROOT/pages/account-abstraction.adoc b/docs/modules/ROOT/pages/account-abstraction.adoc new file mode 100644 index 00000000000..e50735ee9ba --- /dev/null +++ b/docs/modules/ROOT/pages/account-abstraction.adoc @@ -0,0 +1,284 @@ += Account Abstraction + +Unlike Externally Owned Accounts (EOAs), smart contracts may contain arbitrary verification logic based on authentication mechanisms different to Ethereum's native xref:api:utils.adoc#ECDSA[ECDSA] and have execution advantages such as batching or gas sponsorship. To leverage these properties of smart contracts, the community has widely adopted https://eips.ethereum.org/EIPS/eip-4337[ERC-4337], a standard to process user operations through an alternative mempool. + +OpenZeppelin Contracts provides multiple contracts for Account Abstraction following this standard as it enables more flexible and user-friendly interactions with applications. Account Abstraction use cases include wallets in novel contexts (e.g. embedded wallets), more granular configuration of accounts and recovery mechanisms. These capabilities can be supercharged with a modularity approach following standards such as xref:erc7579.adoc#ERC7579[ERC-7579]. + +== Smart Accounts + +OpenZeppelin provides an abstract implementation of an xref:api:account.adoc#AccountBase[`AccountBase`] that implements both interfaces and accepts native currency and common tokens (i.e. xref:erc20.adoc[ERC-20], xref:erc721.adoc[ERC-721] and xref:erc1155.adoc[ERC-1155]), as well as validating any arbitrary `UserOperation`. + +Aside from the xref:api:account.adoc#AccountBase[`AccountBase`], the library includes various specialized accounts that implement xref:api:account.adoc#AccountBase-validateUserOp-struct-PackedUserOperation-bytes32-uint256-[`_validateUserOp`]. Either with a modular approach like with xref:api:account.adoc#AccountERC7579[`AccountERC7579`] or using a digital signature verification algorithm like xref:api:utils.adoc#ECDSA[`ECDSA`], xref:api:utils.adoc#P256[`P256`] or xref:api:utils.adoc#RSA[`RSA`]. + +=== Setting up an account + +To setup an account, you can either bring your own validation logic and start with xref:api:account.adoc#AccountBase[`AccountBase`], or import any of the predefined accounts we provide and are controlled by a signing key. For example, to setup a contract controlled by a regular EVM private key, you can leverage xref:api:account.adoc#AccountECDSA[`AccountECDSA`] on its upgradeable version. + +Since smart accounts are deployed by a factory, the best practice is to create xref:api:utils.adoc#Clones[minimal clones] of initializable contracts. For this reason, the examples use the upgradeable version of the account contracts because they do not rely on a constructor and use an xref:api:utils.adoc#Initializer[initializer] instead. This way the implementation can be cloned and initialized by the factory right after. + +NOTE: To learn more about initializable contracts, check out our xref:upgradeable.adoc[upgradeability] guide + +```solidity +// contracts/MyAccountECDSAClonable.sol +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {AccountECDSAUpgradeable} from "@openzeppelin/upgradeable-contracts/account/AccountECDSAUpgradeable.sol"; + +contract MyAccountECDSAClonable is AccountECDSAUpgradeable { + constructor() { + _disableInitializers(); + } + + function initialize(address signer, string memory name, string memory version) public virtual initializer { + __AccountECDSA_init(signer, name, version); + } +} +``` + +NOTE: xref:api:account.adoc#AccountECDSA[`AccountECDSA`] initializes xref:api:utils.adoc#EIP712[`EIP712`] to generate a domain separator that prevents replayability in other accounts controlled by the same key. See xref:account-abstraction.adoc#eip712_typed_signatures[EIP-712 Typed signatures] + +Along with the regular ECDSA verification, the library also provides the xref:api:account.adoc#AccountP256[`AccountP256`], which is a widely used _elliptic curve_ verification algorithm that's present in mobile device security enclaves, FIDO keys, and corporate environments (i.e. public key infrastructures). + +```solidity +// contracts/MyAccountP256Clonable.sol +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {AccountP256Upgradeable} from "@openzeppelin/upgradeable-contracts/account/AccountP256Upgradeable.sol"; + +contract MyAccountP256Clonable is AccountP256Upgradeable { + constructor() { + _disableInitializers(); + } + + function initialize(bytes32 qx, bytes32 qy, string memory name, string memory version) public virtual initializer { + __AccountP256_init(qx, qy, name, version); + } +} +``` + +Similarly, some government and corporate public key infrastructures use RSA for signature verification. For those cases, the xref:api:account.adoc#AccountRSA[`AccountRSA`] may be a good fit. + +```solidity +// contracts/MyAccountRSAClonable.sol +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {AccountRSAUpgradeable} from "@openzeppelin/upgradeable-contracts/account/AccountRSAUpgradeable.sol"; + +contract MyAccountRSAClonable is AccountRSAUpgradeable { + constructor() { + _disableInitializers(); + } + + function initialize(bytes memory e, bytes memory n, string memory name, string memory version) public virtual initializer { + __AccountRSA_init(e, n, name, version); + } +} +``` + +=== Modules + +Modules are a way to extend functionality of an smart account. Given the variety of smart account implementations, a common approach has been to enable a system of modules to which accounts can delegate logic. As a result, the community has proposed xref:erc7579.adoc#ERC7579[ERC-7579] as a minimal generalized approach to smart account modules. + +OpenZeppelin's xref:api:account.adoc#AccountERC7579[`AccountERC7579`] is an ERC-7579 compliant implementation that works without validating signatures in-place, and instead, uses a validator module installed on the account. This module might be any of the https://erc7579.com/modules[validators developed by the community] or one of the xref:api:account.adoc#Validators[validator modules we provide]. + +To setup a modular Account, start by importing xref:api:account.adoc#AccountERC7579[`AccountERC7579`] and make sure to install a module during its initialization. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/IERC7579Module.sol"; +import {AccountERC7579Upgradeable} from "@openzeppelin/contracts-upgradeable/account/draft-AccountERC7579Upgradeable.sol"; + +contract MyModularAccountClonable is AccountERC7579Upgradeable { + constructor() { + _disableInitializers(); + } + + function initialize(string memory name, string memory version, address module, bytes memory moduleInitData) public virtual initializer { + __AccountERC7579_init(name, version); + _installModule(MODULE_TYPE_VALIDATOR, module, moduleInitData); + } +} +``` + +WARNING: An account that doesn't setup a module on deployment will be unusable if there's no other execution method enabled on the account. + +==== Using with a signing key + +A modular account can use a signer too. It just needs to override `_validateUserOp` logic to use the signer as part of the validation phase: + +```solidity +// contracts/MyModularAccountECDSAClonable.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {AccountERC7579Upgradeable} from "@openzeppelin/contracts-upgradeable/account/draft-AccountERC7579Upgradeable.sol"; +import {ERC1271TypedSigner} from "@openzeppelin/contracts/utils/cryptography/ERC1271TypedSigner.sol"; +import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/ERC4337Utils.sol"; +import {AccountECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/account/AccountECDSAUpgradeable.sol"; +import {AccountBase} from "@openzeppelin/contracts/account/AccountBase.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/IERC7579Module.sol"; + +contract MyModularAccountECDSAClonable is AccountECDSAUpgradeable, AccountERC7579Upgradeable { + function initialize( + address signer_, + string memory name, + string memory version, + address module, + bytes memory moduleInitData + ) public initializer { + __AccountECDSAUpgradeable_init(signer_); + __EIP712_init_unchained(name, version); + _installModule(MODULE_TYPE_VALIDATOR, module, moduleInitData); + } + + function isValidSignature( + bytes32 hash, + bytes calldata signature + ) public view override(AccountERC7579Upgradeable, ERC1271TypedSigner) returns (bytes4) { + // Prefer signer and fallback to ERC7579 validator + return + ERC1271TypedSigner.isValidSignature(hash, signature) == IERC1271.isValidSignature.selector + ? IERC1271.isValidSignature.selector + : AccountERC7579Upgradeable.isValidSignature(hash, signature); + } + + /// @inheritdoc AccountERC7579Upgradeable + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override(AccountERC7579Upgradeable, AccountECDSAUpgradeable) returns (uint256) { + // Prefer signer and fallback to ERC7579 validator + if (_validateSignature(userOpHash, userOp.signature)) return ERC4337Utils.SIG_VALIDATION_SUCCESS; + return super._validateUserOp(userOp, userOpHash); + } + + function executeUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) public override(AccountBase, AccountERC7579Upgradeable) { + // Prefer modular execution + AccountERC7579Upgradeable.executeUserOp(userOp, userOpHash); + } +} +``` + +== Account Factory + +The first time a user sends an user operation, the account will be created deterministically (i.e. its code and address can be predicted) using the the `initCode` field in the UserOperation. This field contains both the address of a smart contract (the factory) and the data required to call it and deploy the smart account. + +For this purpose, the xref:api:account.adoc#FactoryBase[`FactoryBase`] can be used to create a factory for any initializable account: + +```solidity +// contracts/MyFactoryAccountECDSA.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FactoryBase} from "@openzeppelin/contracts/account/FactoryBase.sol"; +import {MyAccountECDSAClonable} from "./MyAccountECDSAClonable.sol" + +contract MyFactoryAccountECDSA is FactoryBase { + constructor() FactoryBase(address(new MyAccountECDSAClonable())) {} +} +``` + +== Paymaster + +== ERC-4337 Overview + +The ERC-4337 is a detailed specification of how to implement the necessary logic to handle operations without making changes to the protocol level (i.e. the rules of the blockchain itself). This specification defines the following components: + +=== UserOperation + +An `UserOperation` is a higher-layer pseudo-transaction object that represents the intent of the account. This shares some similarities with regular EVM transactions like the concept of `gasFees` or `callData` but includes fields that enable new capabilities. + +```solidity +struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; // concatenation of factory address and factoryData (or empty) + bytes callData; + bytes32 accountGasLimits; // concatenation of verificationGas (16 bytes) and callGas (16 bytes) + uint256 preVerificationGas; + bytes32 gasFees; // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes) + bytes paymasterAndData; // concatenation of paymaster fields (or empty) + bytes signature; +} +``` + +=== Entrypoint + +Each `UserOperation` is executed through a contract known as the https://etherscan.io/address/0x0000000071727de22e5e9d8baf0edac6f37da032#code[`EntryPoint`]. This contract is a singleton deployed across multiple networks at the same address although other custom implementations may be used. + +The Entrypoint contracts is considered a trusted entity by the account. + +=== Bundlers + +The bundler is a piece of _offchain_ infrastructure that is in charge of processing an alternative mempool of user operations. Bundlers themselves call the Entrypoint contract's `handleOps` function with an array of UserOperations that are executed and included in a block. + +During the process, the bundler pays for the gas of executing the transaction and gets refunded during the execution phase of the Entrypoint contract. + +=== Account Contract + +The Account Contract is a type of smart contract implements the logic required to validate an `UserOperation` in the context of ERC-4337. Any smart contract account should conform with the `IAccount` interface to validate operations. + +```solidity +interface IAccount { + function validateUserOp(PackedUserOperation calldata, bytes32, uint256) external returns (uint256 validationData); +} +``` + +Similarly, an Account should have a way to execute these operations by either handling arbitrary calldata on its `fallback` or implementing the `IAccountExecute` interface: + +```solidity +interface IAccountExecute { + function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; +} +``` + +To build your own account, see xref:account-abstraction.adoc#smart_accounts[Smart Accounts]. + +=== Factory Contract + +The smart contract accounts are created by a Factory contract defined by the Account developer. This factory receives arbitrary bytes as `initData` and returns an `address` where the logic of the account is deployed. + +To build your own factory, see xref:account-abstraction.adoc#account_factory[Account Factory] + +=== Paymaster Contract + +A Paymaster is an optional entity that can sponsor gas fees for Accounts, or allow them to pay for those fees in ERC-20 instead of native currency. This abstracts gas away of the user experience in the same way that computational costs of cloud servers are abstracted away from end-users. + +To build your own paymaster, see xref:account-abstraction.adoc#paymaster[Paymaster]. + +== Further notes + +=== EIP712 Typed Signatures + +A common security practice to prevent user operation https://mirror.xyz/curiousapple.eth/pFqAdW2LiJ-6S4sg_u1z08k4vK6BCJ33LcyXpnNb8yU[replayability across smart contract accounts controlled by the same private key] (i.e. multiple accounts for the same signer) is to link the signature to the `address` and `chainId` of the account. This can be done by asking the user to sign the hash of the user operation along with these values. + +The problem with this approach is that the user might be prompted by the wallet provider to sign an https://x.com/howydev/status/1780353754333634738[obfuscated message], which is a phishing vector that may lead to a user losing its assets. + +To prevent this, each account using a signature verification algorithm inherits from xref:api:account#ERC1271TypedSigner[`ERC1271TypedSigner`], a utility that implements xref:api:interfaces#IERC1271[`IERC1271`] for smart contract signatures with a defensive rehashing mechanism based on a https://github.com/frangio/eip712-wrapper-for-eip1271[nested EIP-712 approach] to wrap the signature request in a context where there's clearer information for the end user. + +=== ERC-7562 Validation Rules + +To process a bundle of `UserOperations`, bundlers call xref:api:account.adoc#AccountBase-validateUserOp-struct-PackedUserOperation-bytes32-uint256-[`validateUserOp`] on each operation sender to check whether the operation can be executed. However, the bundler has no guarantee that the state of the blockchain will remain the same after the validation phase. To overcome this problem, https://eips.ethereum.org/EIPS/eip-7562[ERC-7562] proposes a set of limitations to EVM code so that bundlers (or node operators) are protected from unexpected state changes. + +These rules outline the requirements for operations to be processed by the canonical mempool. + +TIP: Although any Account that breaks such rules may still be processed by a private bundler, developers should keep in mind the centralization tradeoffs of relying on private infrastructure instead of _permissionless_ execution. + +==== A note on upgradeability + +xref:upgradeable.adoc[Upgradeable Contracts] might easily violate ERC-7562 storage access rules during the validation phase. For example, when upgradeability is present in a module (i.e. an external validator), the account will need to call the proxy and access the implementation address in storage. + +IMPORTANT: Consider this caveat when using upgradeable accounts as validators or creating upgradeable modules. For example, the transactions of an account that had installed an upgradeable ECDSA validation module will not be processed by the canonical mempool. diff --git a/docs/modules/ROOT/pages/erc7579.adoc b/docs/modules/ROOT/pages/erc7579.adoc new file mode 100644 index 00000000000..f362bcdfb2f --- /dev/null +++ b/docs/modules/ROOT/pages/erc7579.adoc @@ -0,0 +1,38 @@ += ERC-7579 + +The https://eips.ethereum.org/EIPS/eip-7579[ERC-7579] is a proposal for standardizing smart accounts and modules to ensure interoperability across implementations. The standard defines a set of interfaces for smart accounts that enable developers to delegate logic to third-party smart contracts (or modules). Modules are categorized in the following 4 types: + +- **Validators**: Modules to which the smart account delegates the validation of an `UserOperation`. +- **Executors**: Contracts that are allowed to execute calls on behalf of the smart account. +- **Fallback Handler**: A module that extends the https://docs.soliditylang.org/en/latest/contracts.html#fallback-function[fallback function] of a smart account. +- **Hooks**: Contracts that receive a call before and after the execution of a smart account. + +OpenZeppelin provides an implementation of an xref:api:account.adoc#AccountERC7579[`AccountERC7579`] that extends from our xref:api:account.adoc#AccountBase[`AccountBase`] contract and is compliant with this standard. The account includes support for single or batched calls as well as `delegatecall`. As for modules, it supports installing _Validators_, _Executors_ and _Fallback Handlers_, whereas _Hooks_ can be added as an extension with xref:api:account.adoc#AccountERC7579Hooked[`AccountERC7579Hooked`] + +== Setting up an ERC-7579 Account + +Considering accounts should be deployed by a factory, setting up a modular ERC-7579 account requires an initializer (e.g. `setUp`) function. This function should be called by the factory when the account is deployed and should install a module: + +```solidity +// contracts/MyModularAccount.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {AccountERC7579} from "@openzeppelin/contracts/account/draft-AccountERC7579.sol"; + +contract MyModularAccount is AccountERC7579 { + // Make the account initializable and install a validator module + function setUp(uint256 moduleTypeId, address module, bytes memory moduleInitData) public initializer { + _installModule(moduleTypeId, module, moduleInitData); + } +} +``` + +The account developers must make sure the validator installed is enough to operate the account. Otherwise, the account might be unusable after deployment. + +== Installing modules + +The xref:api:account.adoc#AccountERC7579[`AccountERC7579`] comes with built-in support for _Validators_, _Executors_ and _Fallback Handlers_. They can be installed right away into the account + +=== Supporting hooks diff --git a/scripts/upgradeable/transpile.sh b/scripts/upgradeable/transpile.sh index f7c848c1320..0500971ee18 100644 --- a/scripts/upgradeable/transpile.sh +++ b/scripts/upgradeable/transpile.sh @@ -31,6 +31,7 @@ fi npx @openzeppelin/upgrade-safe-transpiler -D \ -b "$build_info" \ -i contracts/proxy/utils/Initializable.sol \ + -x 'contracts/vendor/erc4337-entrypoint/**/*' \ -x 'contracts-exposed/**/*' \ -x 'contracts/proxy/**/*' \ -x '!contracts/proxy/Clones.sol' \ diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js new file mode 100644 index 00000000000..b167b7771ec --- /dev/null +++ b/test/account/Account.behavior.js @@ -0,0 +1,284 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { impersonate } = require('../helpers/account'); +const { SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILURE } = require('../helpers/erc4337'); +const { setBalance } = require('@nomicfoundation/hardhat-network-helpers'); +const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); + +function shouldBehaveLikeAnAccountBase() { + describe('entryPoint', function () { + it('should return the canonical entrypoint', async function () { + await this.smartAccount.deploy(); + expect(await this.smartAccount.entryPoint()).to.equal(this.entrypoint.target); + }); + }); + + describe('validateUserOp', function () { + beforeEach(async function () { + await setBalance(this.smartAccount.target, ethers.parseEther('1')); + await this.smartAccount.deploy(); + }); + + it('should revert if the caller is not the canonical entrypoint', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + await expect(this.smartAccount.connect(this.other).validateUserOp(operation.packed, operation.hash, 0)) + .to.be.revertedWithCustomError(this.smartAccount, 'AccountUnauthorized') + .withArgs(this.other); + }); + + describe('when the caller is the canonical entrypoint', function () { + beforeEach(async function () { + this.entrypointAsSigner = await impersonate(this.entrypoint.target); + }); + + it('should return SIG_VALIDATION_SUCCESS if the signature is valid', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + expect( + await this.smartAccount + .connect(this.entrypointAsSigner) + .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 () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount.createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }); + + operation.signature = '0x00'; + + expect( + await this.smartAccount + .connect(this.entrypointAsSigner) + .validateUserOp.staticCall(operation.packed, operation.hash, 0), + ).to.eq(SIG_VALIDATION_FAILURE); + }); + + it('should pay missing account funds for execution', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + const prevAccountBalance = await ethers.provider.getBalance(this.smartAccount.target); + const prevEntrypointBalance = await ethers.provider.getBalance(this.entrypoint.target); + const amount = ethers.parseEther('0.1'); + + const tx = await this.smartAccount + .connect(this.entrypointAsSigner) + .validateUserOp(operation.packed, operation.hash, amount); + + const receipt = await tx.wait(); + const callerFees = receipt.gasUsed * tx.gasPrice; + + expect(await ethers.provider.getBalance(this.smartAccount.target)).to.equal(prevAccountBalance - amount); + expect(await ethers.provider.getBalance(this.entrypoint.target)).to.equal( + prevEntrypointBalance + amount - callerFees, + ); + }); + }); + }); + + describe('fallback', function () { + it('should receive ether', async function () { + await this.smartAccount.deploy(); + await setBalance(this.other.address, ethers.parseEther('1')); + + const prevBalance = await ethers.provider.getBalance(this.smartAccount.target); + const amount = ethers.parseEther('0.1'); + await this.other.sendTransaction({ to: this.smartAccount.target, value: amount }); + + expect(await ethers.provider.getBalance(this.smartAccount.target)).to.equal(prevBalance + amount); + }); + }); +} + +function shouldBehaveLikeAccountHolder() { + describe('onReceived', function () { + beforeEach(async function () { + await this.smartAccount.deploy(); + }); + + shouldSupportInterfaces(['ERC1155Receiver']); + + describe('onERC1155Received', function () { + const ids = [1n, 2n, 3n]; + const values = [1000n, 2000n, 3000n]; + const data = '0x12345678'; + + beforeEach(async function () { + [this.owner] = await ethers.getSigners(); + this.token = await ethers.deployContract('$ERC1155', ['https://somedomain.com/{id}.json']); + await this.token.$_mintBatch(this.owner, ids, values, '0x'); + }); + + it('receives ERC1155 tokens from a single ID', async function () { + await this.token.connect(this.owner).safeTransferFrom(this.owner, this.smartAccount, ids[0], values[0], data); + expect(await this.token.balanceOf(this.smartAccount, ids[0])).to.equal(values[0]); + for (let i = 1; i < ids.length; i++) { + expect(await this.token.balanceOf(this.smartAccount, ids[i])).to.equal(0n); + } + }); + + it('receives ERC1155 tokens from a multiple IDs', async function () { + expect( + await this.token.balanceOfBatch( + ids.map(() => this.smartAccount), + ids, + ), + ).to.deep.equal(ids.map(() => 0n)); + await this.token.connect(this.owner).safeBatchTransferFrom(this.owner, this.smartAccount, ids, values, data); + expect( + await this.token.balanceOfBatch( + ids.map(() => this.smartAccount), + ids, + ), + ).to.deep.equal(values); + }); + }); + + describe('onERC721Received', function () { + it('receives an ERC721 token', async function () { + const name = 'Some NFT'; + const symbol = 'SNFT'; + const tokenId = 1n; + + const [owner] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ERC721', [name, symbol]); + await token.$_mint(owner, tokenId); + + await token.connect(owner).safeTransferFrom(owner, this.smartAccount, tokenId); + + expect(await token.ownerOf(tokenId)).to.equal(this.smartAccount.target); + }); + }); + }); +} + +function shouldBehaveLikeAnAccountBaseExecutor() { + describe('executeUserOp', function () { + beforeEach(async function () { + await setBalance(this.smartAccount.target, ethers.parseEther('1')); + expect(await ethers.provider.getCode(this.smartAccount.target)).to.equal('0x'); + this.entrypointAsSigner = await impersonate(this.entrypoint.target); + }); + + describe('when not deployed', function () { + it('should be created with handleOps and increase nonce', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign(this.domain, this.signer)); + + await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.smartAccount, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.smartAccount, 17); + expect(await this.smartAccount.getNonce()).to.equal(1); + }); + + it('should revert if the signature is invalid', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.addInitCode()); + + operation.signature = '0x00'; + + await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) + .to.be.reverted; + }); + }); + + describe('when deployed', function () { + beforeEach(async function () { + await this.smartAccount.deploy(); + }); + + it('should increase nonce and call target', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + expect(await this.smartAccount.getNonce()).to.equal(0); + await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.smartAccount, 42); + expect(await this.smartAccount.getNonce()).to.equal(1); + }); + }); + }); +} + +module.exports = { + shouldBehaveLikeAnAccountBase, + shouldBehaveLikeAccountHolder, + shouldBehaveLikeAnAccountBaseExecutor, +}; diff --git a/test/account/AccountBase.test.js b/test/account/AccountBase.test.js new file mode 100644 index 00000000000..3099417e68b --- /dev/null +++ b/test/account/AccountBase.test.js @@ -0,0 +1,24 @@ +const { ethers } = require('hardhat'); +const { shouldBehaveLikeAnAccountBase, shouldBehaveLikeAnAccountBaseExecutor } = require('./Account.behavior'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { BooleanSigner } = require('../helpers/signers'); + +async function fixture() { + const [beneficiary, other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new BooleanSigner(); + const helper = new ERC4337Helper('$AccountBaseMock'); + const smartAccount = await helper.newAccount(); + + return { ...helper, smartAccount, signer, target, beneficiary, other }; +} + +describe('AccountBase', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeAnAccountBase(); + shouldBehaveLikeAnAccountBaseExecutor(); +}); diff --git a/test/account/AccountECDSA.test.js b/test/account/AccountECDSA.test.js new file mode 100644 index 00000000000..f51ce4f972b --- /dev/null +++ b/test/account/AccountECDSA.test.js @@ -0,0 +1,44 @@ +const { ethers } = require('hardhat'); +const { + shouldBehaveLikeAnAccountBase, + shouldBehaveLikeAnAccountBaseExecutor, + shouldBehaveLikeAccountHolder, +} = require('./Account.behavior'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { ECDSASigner } = require('../helpers/signers'); +const { shouldBehaveLikeERC1271TypedSigner } = require('../utils/cryptography/ERC1271TypedSigner.behavior'); + +async function fixture() { + const [beneficiary, other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new ECDSASigner(); + const helper = new ERC4337Helper('$AccountECDSA'); + const smartAccount = await helper.newAccount(['AccountECDSA', '1', signer.EOA.address]); + const domain = { + name: 'AccountECDSA', + version: '1', + chainId: helper.chainId, + verifyingContract: smartAccount.address, + }; + + return { ...helper, domain, smartAccount, signer, target, beneficiary, other }; +} + +describe('AccountECDSA', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeAnAccountBase(); + shouldBehaveLikeAnAccountBaseExecutor(); + shouldBehaveLikeAccountHolder(); + + describe('ERC1271TypedSigner', function () { + beforeEach(async function () { + this.mock = await this.smartAccount.deploy(); + }); + + shouldBehaveLikeERC1271TypedSigner(); + }); +}); diff --git a/test/account/AccountEIP7702.test.js b/test/account/AccountEIP7702.test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/account/AccountERC7579.test.js b/test/account/AccountERC7579.test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/account/AccountP256.test.js b/test/account/AccountP256.test.js new file mode 100644 index 00000000000..c28831ee250 --- /dev/null +++ b/test/account/AccountP256.test.js @@ -0,0 +1,44 @@ +const { ethers } = require('hardhat'); +const { + shouldBehaveLikeAnAccountBase, + shouldBehaveLikeAnAccountBaseExecutor, + shouldBehaveLikeAccountHolder, +} = require('./Account.behavior'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { P256Signer } = require('../helpers/signers'); +const { shouldBehaveLikeERC1271TypedSigner } = require('../utils/cryptography/ERC1271TypedSigner.behavior'); + +async function fixture() { + const [beneficiary, other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new P256Signer(); + const helper = new ERC4337Helper('$AccountP256'); + const smartAccount = await helper.newAccount(['AccountP256', '1', signer.publicKey.qx, signer.publicKey.qy]); + const domain = { + name: 'AccountP256', + version: '1', + chainId: helper.chainId, + verifyingContract: smartAccount.address, + }; + + return { ...helper, domain, smartAccount, signer, target, beneficiary, other }; +} + +describe('AccountP256', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeAnAccountBase(); + shouldBehaveLikeAnAccountBaseExecutor(); + shouldBehaveLikeAccountHolder(); + + describe('ERC1271TypedSigner', function () { + beforeEach(async function () { + this.mock = await this.smartAccount.deploy(); + }); + + shouldBehaveLikeERC1271TypedSigner(); + }); +}); diff --git a/test/account/AccountRSA.test.js b/test/account/AccountRSA.test.js new file mode 100644 index 00000000000..ccc863ac81c --- /dev/null +++ b/test/account/AccountRSA.test.js @@ -0,0 +1,44 @@ +const { ethers } = require('hardhat'); +const { + shouldBehaveLikeAnAccountBase, + shouldBehaveLikeAnAccountBaseExecutor, + shouldBehaveLikeAccountHolder, +} = require('./Account.behavior'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { RSASigner } = require('../helpers/signers'); +const { shouldBehaveLikeERC1271TypedSigner } = require('../utils/cryptography/ERC1271TypedSigner.behavior'); + +async function fixture() { + const [beneficiary, other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new RSASigner(); + const helper = new ERC4337Helper('$AccountRSA'); + const smartAccount = await helper.newAccount(['AccountRSA', '1', signer.publicKey.e, signer.publicKey.n]); + const domain = { + name: 'AccountRSA', + version: '1', + chainId: helper.chainId, + verifyingContract: smartAccount.address, + }; + + return { ...helper, domain, smartAccount, signer, target, beneficiary, other }; +} + +describe('AccountRSA', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeAnAccountBase(); + shouldBehaveLikeAnAccountBaseExecutor(); + shouldBehaveLikeAccountHolder(); + + describe('ERC1271TypedSigner', function () { + beforeEach(async function () { + this.mock = await this.smartAccount.deploy(); + }); + + shouldBehaveLikeERC1271TypedSigner(); + }); +}); diff --git a/test/account/extensions/AccountERC7579Hooked.test.js b/test/account/extensions/AccountERC7579Hooked.test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/account/modules/ECDSAValidator.test.js b/test/account/modules/ECDSAValidator.test.js new file mode 100644 index 00000000000..ab6defbfffe --- /dev/null +++ b/test/account/modules/ECDSAValidator.test.js @@ -0,0 +1,41 @@ +const { ethers } = require('hardhat'); +const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); +const { shouldBehaveLikeSignatureValidator } = require('./SignatureValidator.behavior'); +const { ERC4337Helper } = require('../../helpers/erc4337'); +const { ECDSASigner } = require('../../helpers/signers'); +const { MODULE_TYPE_VALIDATOR } = require('../../helpers/erc7579'); +const { impersonate } = require('../../helpers/account'); + +async function fixture() { + const helper = new ERC4337Helper('$AccountERC7579'); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new ECDSASigner(); + const validator = await ethers.deployContract('$ECDSAValidator'); + const erc7579Account = await helper.newAccount(['AccountERC7579', '1']); + const domain = { + name: 'AccountERC7579', + version: '1', + chainId: helper.chainId, + verifyingContract: erc7579Account.address, + }; + + await setBalance(erc7579Account.target, ethers.parseEther('1')); + await erc7579Account.deploy(); + const erc7579AccountAsSigner = await impersonate(erc7579Account.target); + + await erc7579Account.$_installModule( + MODULE_TYPE_VALIDATOR, + validator.target, + ethers.AbiCoder.defaultAbiCoder().encode(['address'], [signer.EOA.address]), + ); + + return { ...helper, target, signer, validator, erc7579Account, erc7579AccountAsSigner, domain }; +} + +describe('ECDSAValidator', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeSignatureValidator(); +}); diff --git a/test/account/modules/MultiERC1271Validator.test.js b/test/account/modules/MultiERC1271Validator.test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/account/modules/P256Validator.test.js b/test/account/modules/P256Validator.test.js new file mode 100644 index 00000000000..377ffc36291 --- /dev/null +++ b/test/account/modules/P256Validator.test.js @@ -0,0 +1,41 @@ +const { ethers } = require('hardhat'); +const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); +const { shouldBehaveLikeSignatureValidator } = require('./SignatureValidator.behavior'); +const { ERC4337Helper } = require('../../helpers/erc4337'); +const { P256Signer } = require('../../helpers/signers'); +const { MODULE_TYPE_VALIDATOR } = require('../../helpers/erc7579'); +const { impersonate } = require('../../helpers/account'); + +async function fixture() { + const helper = new ERC4337Helper('$AccountERC7579'); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new P256Signer(); + const validator = await ethers.deployContract('$P256Validator'); + const erc7579Account = await helper.newAccount(['AccountERC7579', '1']); + const domain = { + name: 'AccountERC7579', + version: '1', + chainId: helper.chainId, + verifyingContract: erc7579Account.address, + }; + + await setBalance(erc7579Account.target, ethers.parseEther('1')); + await erc7579Account.deploy(); + const erc7579AccountAsSigner = await impersonate(erc7579Account.target); + + await erc7579Account.$_installModule( + MODULE_TYPE_VALIDATOR, + validator.target, + ethers.AbiCoder.defaultAbiCoder().encode(['bytes32', 'bytes32'], [signer.publicKey.qx, signer.publicKey.qy]), + ); + + return { ...helper, target, signer, validator, erc7579Account, erc7579AccountAsSigner, domain }; +} + +describe('P256Validator', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeSignatureValidator(); +}); diff --git a/test/account/modules/RSAValidator.test.js b/test/account/modules/RSAValidator.test.js new file mode 100644 index 00000000000..630837de3b9 --- /dev/null +++ b/test/account/modules/RSAValidator.test.js @@ -0,0 +1,41 @@ +const { ethers } = require('hardhat'); +const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); +const { shouldBehaveLikeSignatureValidator } = require('./SignatureValidator.behavior'); +const { ERC4337Helper } = require('../../helpers/erc4337'); +const { RSASigner } = require('../../helpers/signers'); +const { MODULE_TYPE_VALIDATOR } = require('../../helpers/erc7579'); +const { impersonate } = require('../../helpers/account'); + +async function fixture() { + const helper = new ERC4337Helper('$AccountERC7579'); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new RSASigner(); + const validator = await ethers.deployContract('$RSAValidator'); + const erc7579Account = await helper.newAccount(['AccountERC7579', '1']); + const domain = { + name: 'AccountERC7579', + version: '1', + chainId: helper.chainId, + verifyingContract: erc7579Account.address, + }; + + await setBalance(erc7579Account.target, ethers.parseEther('1')); + await erc7579Account.deploy(); + const erc7579AccountAsSigner = await impersonate(erc7579Account.target); + + await erc7579Account.$_installModule( + MODULE_TYPE_VALIDATOR, + validator.target, + ethers.AbiCoder.defaultAbiCoder().encode(['bytes', 'bytes'], [signer.publicKey.e, signer.publicKey.n]), + ); + + return { ...helper, target, signer, validator, erc7579Account, erc7579AccountAsSigner, domain }; +} + +describe('RSAValidator', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeSignatureValidator(); +}); diff --git a/test/account/modules/SignatureValidator.behavior.js b/test/account/modules/SignatureValidator.behavior.js new file mode 100644 index 00000000000..197f884d057 --- /dev/null +++ b/test/account/modules/SignatureValidator.behavior.js @@ -0,0 +1,115 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK, + encodeSingle, + encodeMode, +} = require('../../helpers/erc7579'); +const { SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILURE } = require('../../helpers/erc4337'); +const { hashTypedData, hashTypedDataEnvelopeStruct, domainSeparator, getDomain } = require('../../helpers/eip712'); + +function shouldBehaveLikeSignatureValidator() { + describe('isModuleType', function () { + it('returns true for MODULE_TYPE_VALIDATOR (1)', async function () { + expect(await this.validator.isModuleType(MODULE_TYPE_VALIDATOR)).to.be.true; + }); + + it('returns false for MODULE_TYPE_EXECUTOR (2)', async function () { + expect(await this.validator.isModuleType(MODULE_TYPE_EXECUTOR)).to.be.false; + }); + + it('returns false for MODULE_TYPE_FALLBACK (3)', async function () { + expect(await this.validator.isModuleType(MODULE_TYPE_FALLBACK)).to.be.false; + }); + + it('returns false for MODULE_TYPE_HOOK (4)', async function () { + expect(await this.validator.isModuleType(MODULE_TYPE_HOOK)).to.be.false; + }); + }); + + describe('validateUserOp', function () { + it('returns SIG_VALIDATION_SUCCESS for a valid personal signature', async function () { + const operation = await this.erc7579Account + .createOp({ + callData: this.erc7579Account.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + expect( + await this.validator + .connect(this.erc7579AccountAsSigner) + .validateUserOp.staticCall(operation.packed, operation.hash), + ).to.eq(SIG_VALIDATION_SUCCESS); + }); + + it('returns SIG_VALIDATION_FAILURE for an invalid personal signature', async function () { + const operation = await this.erc7579Account + .createOp({ + callData: this.erc7579Account.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + operation.signature = '0x00'; + + expect( + await this.validator + .connect(this.erc7579AccountAsSigner) + .validateUserOp.staticCall(operation.packed, operation.hash), + ).to.eq(SIG_VALIDATION_FAILURE); + }); + }); + + describe('isValidSignatureWithSender', function () { + const MAGIC_VALUE = '0x1626ba7e'; + + beforeEach(async function () { + this.eip712Verifier = await ethers.deployContract('$EIP712Verifier', ['EIP712Verifier', '1']); + }); + + it('returns true for a valid personal signature', async function () { + const contents = ethers.randomBytes(32); + const signature = await this.signer.signPersonal(this.domain, contents); + expect(await this.validator.isValidSignatureWithSender(this.erc7579Account, contents, signature)).to.equal( + MAGIC_VALUE, + ); + }); + + it('returns true for a valid typed data signature', async function () { + const contents = ethers.randomBytes(32); + const contentsType = 'SomeType(address foo,uint256 bar)'; + const appDomain = await getDomain(this.eip712Verifier); + expect( + await this.validator.isValidSignatureWithSender( + this.erc7579Account, + hashTypedData(appDomain, hashTypedDataEnvelopeStruct(this.domain, contents, contentsType)), + ethers.concat([ + await this.signer.signTypedDataEnvelope(this.domain, appDomain, contents, contentsType), + domainSeparator(appDomain), + contents, + ethers.toUtf8Bytes(contentsType), + ethers.toBeHex(ethers.dataLength(ethers.toUtf8Bytes(contentsType)), 2), + ]), + ), + ).to.equal(MAGIC_VALUE); + }); + + it('returns false for an invalid personal signature', async function () { + const contents = ethers.randomBytes(32); + const signature = '0x00'; + expect(await this.validator.isValidSignatureWithSender(this.erc7579Account, contents, signature)).to.not.equal( + MAGIC_VALUE, + ); + }); + }); +} + +module.exports = { shouldBehaveLikeSignatureValidator }; diff --git a/test/account/modules/SignatureValidator.test.js b/test/account/modules/SignatureValidator.test.js new file mode 100644 index 00000000000..2f7188ae8b8 --- /dev/null +++ b/test/account/modules/SignatureValidator.test.js @@ -0,0 +1,34 @@ +const { ethers } = require('hardhat'); +const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); +const { shouldBehaveLikeSignatureValidator } = require('./SignatureValidator.behavior'); +const { ERC4337Helper } = require('../../helpers/erc4337'); +const { BooleanSigner } = require('../../helpers/signers'); +const { impersonate } = require('../../helpers/account'); + +async function fixture() { + const helper = new ERC4337Helper('$AccountERC7579'); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new BooleanSigner(); + const validator = await ethers.deployContract('SignatureValidatorMock'); + const erc7579Account = await helper.newAccount(['AccountERC7579', '1']); + const domain = { + name: 'AccountERC7579', + version: '1', + chainId: helper.chainId, + verifyingContract: erc7579Account.address, + }; + + await setBalance(erc7579Account.target, ethers.parseEther('1')); + await erc7579Account.deploy(); + const erc7579AccountAsSigner = await impersonate(erc7579Account.target); + + return { ...helper, target, signer, validator, erc7579Account, erc7579AccountAsSigner, domain }; +} + +describe('SignatureValidator', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeSignatureValidator(); +}); diff --git a/test/account/modules/TypedERC1271Executor.test.js b/test/account/modules/TypedERC1271Executor.test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/account/utils/ERC4337Utils.test.js b/test/account/utils/ERC4337Utils.test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/account/utils/ERC7579Utils.t.sol b/test/account/utils/ERC7579Utils.t.sol new file mode 100644 index 00000000000..0b073f2df2c --- /dev/null +++ b/test/account/utils/ERC7579Utils.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {ERC7579Utils, Execution, Mode, CallType, ExecType, ModeSelector, ModePayload} from "@openzeppelin/contracts/account/utils/ERC7579Utils.sol"; + +contract ERC7579UtilsTest is Test { + using ERC7579Utils for *; + + function testEncodeDecodeMode( + CallType callType, + ExecType execType, + ModeSelector modeSelector, + ModePayload modePayload + ) public { + (CallType callType2, ExecType execType2, ModeSelector modeSelector2, ModePayload modePayload2) = ERC7579Utils + .encodeMode(callType, execType, modeSelector, modePayload) + .decodeMode(); + + assertTrue(callType == callType2); + assertTrue(execType == execType2); + assertTrue(modeSelector == modeSelector2); + assertTrue(modePayload == modePayload2); + } + + function testEncodeDecodeSingle(address target, uint256 value, bytes calldata callData) public { + (address target2, uint256 value2, bytes memory callData2) = this._decodeSingle( + ERC7579Utils.encodeSingle(target, value, callData) + ); + + assertEq(target, target2); + assertEq(value, value2); + assertEq(callData, callData2); + } + + function testEncodeDecodeDelegate(address target, bytes calldata callData) public { + (address target2, bytes memory callData2) = this._decodeDelegate(ERC7579Utils.encodeDelegate(target, callData)); + + assertEq(target, target2); + assertEq(callData, callData2); + } + + function testEncodeDecodeBatch(Execution[] memory executionBatch) public { + Execution[] memory executionBatch2 = this._decodeBatch(ERC7579Utils.encodeBatch(executionBatch)); + + assertEq(abi.encode(executionBatch), abi.encode(executionBatch2)); + } + + function _decodeSingle( + bytes calldata executionCalldata + ) external pure returns (address target, uint256 value, bytes calldata callData) { + return ERC7579Utils.decodeSingle(executionCalldata); + } + + function _decodeDelegate( + bytes calldata executionCalldata + ) external pure returns (address target, bytes calldata callData) { + return ERC7579Utils.decodeDelegate(executionCalldata); + } + + function _decodeBatch( + bytes calldata executionCalldata + ) external pure returns (Execution[] calldata executionBatch) { + return ERC7579Utils.decodeBatch(executionCalldata); + } +} diff --git a/test/account/utils/ERC7579Utils.test.js b/test/account/utils/ERC7579Utils.test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/helpers/eip712-types.js b/test/helpers/eip712-types.js index b2b6ccf837b..ab8a2a4be8f 100644 --- a/test/helpers/eip712-types.js +++ b/test/helpers/eip712-types.js @@ -46,6 +46,9 @@ module.exports = mapValues( deadline: 'uint48', data: 'bytes', }, + PersonalSign: { + prefixed: 'bytes', + }, }, formatType, ); diff --git a/test/helpers/eip712.js b/test/helpers/eip712.js index 3843ac02690..e8ae1ee2716 100644 --- a/test/helpers/eip712.js +++ b/test/helpers/eip712.js @@ -36,10 +36,40 @@ function hashTypedData(domain, structHash) { ); } +function hashTypedDataEnvelopeType(contentsTypeName, contentsType) { + return ethers.solidityPackedKeccak256( + ['string'], + [ + `TypedDataSign(${contentsTypeName}bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)${contentsType}`, + ], + ); +} + +function hashTypedDataEnvelopeStruct(domain, contents, contentsType, salt = ethers.ZeroHash, extensions = []) { + const [contentsTypeName] = contentsType.split('('); + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes32', 'bytes32', 'bytes32', 'bytes32', 'uint256', 'address', 'bytes32', 'bytes32'], + [ + hashTypedDataEnvelopeType(contentsTypeName, contentsType), + contents, + ethers.solidityPackedKeccak256(['string'], [domain.name]), + ethers.solidityPackedKeccak256(['string'], [domain.version]), + domain.chainId, + domain.verifyingContract, + salt, + ethers.solidityPackedKeccak256(['uint256[]'], [extensions]), + ], + ), + ); +} + module.exports = { getDomain, domainType, domainSeparator: ethers.TypedDataEncoder.hashDomain, hashTypedData, + hashTypedDataEnvelopeType, + hashTypedDataEnvelopeStruct, ...types, }; diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js new file mode 100644 index 00000000000..ef64c9acb74 --- /dev/null +++ b/test/helpers/erc4337.js @@ -0,0 +1,159 @@ +const { setCode } = require('@nomicfoundation/hardhat-network-helpers'); +const { ethers } = require('hardhat'); + +const CANONICAL_ENTRYPOINT = '0x0000000071727De22E5E9d8BAf0edAc6f37da032'; +const SIG_VALIDATION_SUCCESS = 0; +const SIG_VALIDATION_FAILURE = 1; + +function pack(left, right) { + return ethers.solidityPacked(['uint128', 'uint128'], [left, right]); +} + +/// Global ERC-4337 environment helper. +class ERC4337Helper { + constructor(account, params = {}) { + this.entrypointAsPromise = ethers.deployContract('EntryPoint'); + this.factoryAsPromise = ethers.deployContract('$Create2'); + this.accountContractAsPromise = ethers.getContractFactory(account); + this.chainIdAsPromise = ethers.provider.getNetwork().then(({ chainId }) => chainId); + this.senderCreatorAsPromise = ethers.deployContract('SenderCreator'); + this.params = params; + } + + async wait() { + const entrypoint = await this.entrypointAsPromise; + await entrypoint.getDeployedCode().then(code => setCode(CANONICAL_ENTRYPOINT, code)); + this.entrypoint = entrypoint.attach(CANONICAL_ENTRYPOINT); + this.entrypointAsPromise = Promise.resolve(this.entrypoint); + + this.factory = await this.factoryAsPromise; + this.accountContract = await this.accountContractAsPromise; + this.chainId = await this.chainIdAsPromise; + this.senderCreator = await this.senderCreatorAsPromise; + return this; + } + + async newAccount(extraArgs = [], salt = ethers.randomBytes(32)) { + await this.wait(); + const initCode = await this.accountContract + .getDeployTransaction(...extraArgs) + .then(tx => this.factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) + .then(deployCode => ethers.concat([this.factory.target, deployCode])); + const instance = await this.senderCreator.createSender + .staticCall(initCode) + .then(address => this.accountContract.attach(address)); + return new SmartAccount(instance, initCode, this); + } +} + +/// Represent one ERC-4337 account contract. +class SmartAccount extends ethers.BaseContract { + constructor(instance, initCode, context) { + super(instance.target, instance.interface, instance.runner, instance.deployTx); + this.address = instance.target; + this.initCode = initCode; + this.context = context; + } + + async deploy(account = this.runner) { + this.deployTx = await account.sendTransaction({ + to: '0x' + this.initCode.replace(/0x/, '').slice(0, 40), + data: '0x' + this.initCode.replace(/0x/, '').slice(40), + }); + return this; + } + + async createOp(args = {}) { + const params = Object.assign({ sender: this }, args); + // fetch nonce + if (!params.nonce) { + params.nonce = await this.context.entrypointAsPromise.then(entrypoint => entrypoint.getNonce(this, 0)); + } + // prepare paymaster and data + if (ethers.isAddressable(params.paymaster)) { + params.paymaster = await ethers.resolveAddress(params.paymaster); + params.paymasterVerificationGasLimit ??= 100_000n; + params.paymasterPostOpGasLimit ??= 100_000n; + params.paymasterAndData = ethers.solidityPacked( + ['address', 'uint128', 'uint128'], + [params.paymaster, params.paymasterVerificationGasLimit, params.paymasterPostOpGasLimit], + ); + } + return new UserOperation(params); + } +} + +/// Represent one user operation +class UserOperation { + constructor(params) { + this.sender = params.sender; + this.nonce = params.nonce; + this.initCode = params.initCode ?? '0x'; + this.callData = params.callData ?? '0x'; + this.verificationGas = params.verificationGas ?? 10_000_000n; + this.callGas = params.callGas ?? 100_000n; + this.preVerificationGas = params.preVerificationGas ?? 100_000n; + this.maxPriorityFee = params.maxPriorityFee ?? 100_000n; + this.maxFeePerGas = params.maxFeePerGas ?? 100_000n; + this.paymasterAndData = params.paymasterAndData ?? '0x'; + this.signature = params.signature ?? '0x'; + } + + get packed() { + return { + sender: this.sender, + nonce: this.nonce, + initCode: this.initCode, + callData: this.callData, + accountGasLimits: pack(this.verificationGas, this.callGas), + preVerificationGas: this.preVerificationGas, + gasFees: pack(this.maxPriorityFee, this.maxFeePerGas), + paymasterAndData: this.paymasterAndData, + signature: this.signature, + }; + } + + get hash() { + const p = this.packed; + const h = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'], + [ + p.sender.target, + p.nonce, + ethers.keccak256(p.initCode), + ethers.keccak256(p.callData), + p.accountGasLimits, + p.preVerificationGas, + p.gasFees, + ethers.keccak256(p.paymasterAndData), + ], + ), + ); + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes32', 'address', 'uint256'], + [h, this.sender.context.entrypoint.target, this.sender.context.chainId], + ), + ); + } + + addInitCode() { + this.initCode = this.sender.initCode; + return this; + } + + async sign(domain, signer) { + this.signature = await signer.signPersonal(domain, this.hash); + return this; + } +} + +module.exports = { + ERC4337Helper, + SmartAccount, + UserOperation, + CANONICAL_ENTRYPOINT, + SIG_VALIDATION_SUCCESS, + SIG_VALIDATION_FAILURE, +}; diff --git a/test/helpers/erc7579.js b/test/helpers/erc7579.js new file mode 100644 index 00000000000..3690959e072 --- /dev/null +++ b/test/helpers/erc7579.js @@ -0,0 +1,42 @@ +const { ethers } = require('hardhat'); + +const MODULE_TYPE_VALIDATOR = 1; +const MODULE_TYPE_EXECUTOR = 2; +const MODULE_TYPE_FALLBACK = 3; +const MODULE_TYPE_HOOK = 4; + +const encodeMode = ({ + callType = '0x00', + execType = '0x00', + selector = '0x00000000', + payload = '0x00000000000000000000000000000000000000000000', +} = {}) => + ethers.solidityPacked( + ['bytes1', 'bytes1', 'bytes4', 'bytes4', 'bytes22'], + [callType, execType, '0x00000000', selector, payload], + ); + +const encodeSingle = (target, value = 0n, data = '0x') => + ethers.solidityPacked(['address', 'uint256', 'bytes'], [target.target ?? target.address ?? target, value, data]); + +const encodeBatch = (...entries) => + ethers.AbiCoder.defaultAbiCoder().encode( + ['(address,uint256,bytes)[]'], + [ + entries.map(entry => + Array.isArray(entry) + ? [entry[0].target ?? entry[0].address ?? entry[0], entry[1] ?? 0n, entry[2] ?? '0x'] + : [entry.target.target ?? entry.target.address ?? entry.target, entry.value ?? 0n, entry.data ?? '0x'], + ), + ], + ); + +module.exports = { + encodeMode, + encodeSingle, + encodeBatch, + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK, +}; diff --git a/test/helpers/signers.js b/test/helpers/signers.js new file mode 100644 index 00000000000..1ea2fb63feb --- /dev/null +++ b/test/helpers/signers.js @@ -0,0 +1,132 @@ +const { ethers } = require('hardhat'); +const { secp256k1 } = require('@noble/curves/secp256k1'); +const { secp256r1 } = require('@noble/curves/p256'); +const { generateKeyPairSync, privateEncrypt } = require('crypto'); +const { hashTypedData, hashTypedDataEnvelopeStruct } = require('./eip712'); + +const ensureLowerOrderS = (N, { s, recovery, ...rest }) => { + if (s > N / 2n) { + s = N - s; + recovery = 1 - recovery; + } + return { s, recovery, ...rest }; +}; + +class BooleanSigner { + signPersonal() { + return '0x01'; + } + + signTypedDataEnvelope() { + return '0x01'; + } +} + +class TypedSigner { + signPersonal(domain, contents) { + return this._signRaw( + hashTypedData( + domain, + ethers.solidityPackedKeccak256( + ['bytes32', 'bytes32'], + [ + ethers.solidityPackedKeccak256(['string'], ['PersonalSign(bytes prefixed)']), + ethers.solidityPackedKeccak256(['string', 'bytes32'], ['\x19Ethereum Signed Message:\n32', contents]), + ], + ), + ), + ); + } + + signTypedDataEnvelope(localDomain, appDomain, contents, contentsType) { + return this._signRaw(hashTypedData(appDomain, hashTypedDataEnvelopeStruct(localDomain, contents, contentsType))); + } + + wrapTypedDataSig(originalSig, appSeparator, contents, contentsType) { + const contentsTypeLength = ethers.toBeHex(ethers.dataLength(contentsType), 2); + return ethers.concat([originalSig, appSeparator, contents, contentsType, contentsTypeLength]); + } +} + +class ECDSASigner extends TypedSigner { + N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; + + constructor() { + super(); + this._privateKey = secp256k1.utils.randomPrivateKey(); + this.publicKey = secp256k1.getPublicKey(this._privateKey, false); + } + + _signRaw(messageHash) { + const sig = this._ensureLowerOrderS(secp256k1.sign(messageHash.replace(/0x/, ''), this._privateKey)); + return ethers.Signature.from({ + r: sig.r, + v: sig.recovery + 27, + s: sig.s, + }).serialized; + } + + get EOA() { + return new ethers.Wallet(ethers.hexlify(this._privateKey)); + } + + _ensureLowerOrderS({ s, recovery, ...rest }) { + return ensureLowerOrderS(this.N, { s, recovery, ...rest }); + } +} + +class P256Signer extends TypedSigner { + N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n; + + constructor() { + super(); + this._privateKey = secp256r1.utils.randomPrivateKey(); + const [qx, qy] = [ + secp256r1.getPublicKey(this._privateKey, false).slice(0x01, 0x21), + secp256r1.getPublicKey(this._privateKey, false).slice(0x21, 0x41), + ].map(ethers.hexlify); + this.publicKey = { + qx, + qy, + }; + } + + _signRaw(messageHash) { + const sig = this._ensureLowerOrderS(secp256r1.sign(messageHash.replace(/0x/, ''), this._privateKey)); + return ethers.Signature.from({ + r: sig.r, + v: sig.recovery + 27, + s: sig.s, + }).serialized; + } + + _ensureLowerOrderS({ s, recovery, ...rest }) { + return ensureLowerOrderS(this.N, { s, recovery, ...rest }); + } +} + +class RSASigner extends TypedSigner { + constructor() { + super(); + const keyPair = generateKeyPairSync('rsa', { + modulusLength: 2048, + }); + const jwk = keyPair.publicKey.export({ format: 'jwk' }); + const [e, n] = [jwk.e, jwk.n].map(ethers.decodeBase64); + this._privateKey = keyPair.privateKey; + this.publicKey = { e, n }; + } + + _signRaw(messageHash) { + // SHA256 OID = 608648016503040201 (9 bytes) | NULL = 0500 (2 bytes) (explicit) | OCTET_STRING length (0x20) = 0420 (2 bytes) + const dataToSign = ethers.concat(['0x3031300d060960864801650304020105000420', messageHash]); + return '0x' + privateEncrypt(this._privateKey, ethers.getBytes(dataToSign)).toString('hex'); + } +} + +module.exports = { + BooleanSigner, + ECDSASigner, + P256Signer, + RSASigner, +}; diff --git a/test/utils/cryptography/ERC1271TypedSigner.behavior.js b/test/utils/cryptography/ERC1271TypedSigner.behavior.js new file mode 100644 index 00000000000..033cbb38f76 --- /dev/null +++ b/test/utils/cryptography/ERC1271TypedSigner.behavior.js @@ -0,0 +1,46 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { hashTypedData, domainSeparator, hashTypedDataEnvelopeStruct, getDomain } = require('../../helpers/eip712'); + +function shouldBehaveLikeERC1271TypedSigner() { + const MAGIC_VALUE = '0x1626ba7e'; + + beforeEach(async function () { + this.domain = await getDomain(this.mock); + }); + + describe('isValidSignature', function () { + it('returns true for a valid personal signature', async function () { + const contents = ethers.randomBytes(32); + const signature = await this.signer.signPersonal(this.domain, contents); + expect(await this.mock.isValidSignature(contents, signature)).to.equal(MAGIC_VALUE); + }); + + it('returns true for a valid typed data signature', async function () { + const contents = ethers.randomBytes(32); + const contentsType = 'SomeType(address foo,uint256 bar)'; + const appDomain = { + name: 'SomeApp', + version: '1', + chainId: this.domain.chainId, + verifyingContract: ethers.Wallet.createRandom().address, + }; + expect( + await this.mock.isValidSignature( + hashTypedData(appDomain, hashTypedDataEnvelopeStruct(this.domain, contents, contentsType)), + ethers.concat([ + await this.signer.signTypedDataEnvelope(this.domain, appDomain, contents, contentsType), + domainSeparator(appDomain), + contents, + ethers.toUtf8Bytes(contentsType), + ethers.toBeHex(ethers.dataLength(ethers.toUtf8Bytes(contentsType)), 2), + ]), + ), + ).to.equal(MAGIC_VALUE); + }); + }); +} + +module.exports = { + shouldBehaveLikeERC1271TypedSigner, +}; diff --git a/test/utils/cryptography/ERC1271TypedSigner.test.js b/test/utils/cryptography/ERC1271TypedSigner.test.js new file mode 100644 index 00000000000..3c32c062a4c --- /dev/null +++ b/test/utils/cryptography/ERC1271TypedSigner.test.js @@ -0,0 +1,57 @@ +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ethers } = require('hardhat'); +const { shouldBehaveLikeERC1271TypedSigner } = require('./ERC1271TypedSigner.behavior'); +const { ECDSASigner, P256Signer, RSASigner } = require('../../helpers/signers'); + +async function fixture() { + const ECDSA = new ECDSASigner(); + const ECDSAMock = await ethers.deployContract('ERC1271TypedSignerECDSA', [ECDSA.EOA.address]); + + const P256 = new P256Signer(); + const P256Mock = await ethers.deployContract('ERC1271TypedSignerP256', [P256.publicKey.qx, P256.publicKey.qy]); + + const RSA = new RSASigner(); + const RSAMock = await ethers.deployContract('ERC1271TypedSignerRSA', [RSA.publicKey.e, RSA.publicKey.n]); + + return { + ECDSA, + ECDSAMock, + P256, + P256Mock, + RSA, + RSAMock, + }; +} + +describe('ERC1271TypedSigner', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('for an ECDSA signer', function () { + beforeEach(function () { + this.signer = this.ECDSA; + this.mock = this.ECDSAMock; + }); + + shouldBehaveLikeERC1271TypedSigner(); + }); + + describe('for a P256 signer', function () { + beforeEach(function () { + this.signer = this.P256; + this.mock = this.P256Mock; + }); + + shouldBehaveLikeERC1271TypedSigner(); + }); + + describe('for an RSA signer', function () { + beforeEach(function () { + this.signer = this.RSA; + this.mock = this.RSAMock; + }); + + shouldBehaveLikeERC1271TypedSigner(); + }); +}); diff --git a/test/utils/cryptography/MessageEnvelopeUtils.test.js b/test/utils/cryptography/MessageEnvelopeUtils.test.js new file mode 100644 index 00000000000..742b34a8abf --- /dev/null +++ b/test/utils/cryptography/MessageEnvelopeUtils.test.js @@ -0,0 +1,162 @@ +const { expect } = require('chai'); +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { + hashTypedData, + domainSeparator, + hashTypedDataEnvelopeType, + hashTypedDataEnvelopeStruct, +} = require('../../helpers/eip712'); + +const fixture = async () => { + const mock = await ethers.deployContract('$MessageEnvelopeUtils'); + const domain = { + name: 'SomeDomain', + version: '1', + chainId: await ethers.provider.getNetwork().then(({ chainId }) => chainId), + verifyingContract: await mock.getAddress(), + }; + return { mock, domain }; +}; + +describe('MessageEnvelopeUtils', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('unwrapTypedDataSig', function () { + it('unwraps a typed data envelope', async function () { + const originalSig = ethers.randomBytes(65); + const appSeparator = ethers.id('SomeApp'); + const contents = ethers.id('SomeData'); + const contentsType = ethers.toUtf8Bytes('SomeType()'); + const contentsTypeLength = ethers.toBeHex(ethers.dataLength(contentsType), 2); + + const signature = ethers.concat([originalSig, appSeparator, contents, contentsType, contentsTypeLength]); + + const unwrapped = await this.mock.getFunction('$unwrapTypedDataSig')(signature); + + expect(unwrapped).to.deep.eq([ethers.hexlify(originalSig), appSeparator, contents, ethers.hexlify(contentsType)]); + }); + }); + + describe('wrapTypedDataSig', function () { + it('wraps a typed data envelope', async function () { + const originalSig = ethers.randomBytes(65); + const appSeparator = ethers.id('SomeApp'); + const contents = ethers.id('SomeData'); + const contentsType = ethers.toUtf8Bytes('SomeType()'); + const contentsTypeLength = ethers.toBeHex(ethers.dataLength(contentsType), 2); + + const expected = ethers.concat([originalSig, appSeparator, contents, contentsType, contentsTypeLength]); + + const wrapped = await this.mock.getFunction('$wrapTypedDataSig')( + originalSig, + appSeparator, + contents, + contentsType, + ); + + expect(wrapped).to.equal(expected); + }); + }); + + describe('toPersonalSignEnvelopeHash', function () { + it('should produce a personal signature EIP712 envelope', async function () { + const contents = ethers.randomBytes(32); + const personalSignStructHash = ethers.solidityPackedKeccak256( + ['bytes32', 'bytes32'], + [ + ethers.solidityPackedKeccak256(['string'], ['PersonalSign(bytes prefixed)']), + ethers.solidityPackedKeccak256(['string', 'bytes32'], ['\x19Ethereum Signed Message:\n32', contents]), + ], + ); + expect( + await this.mock.getFunction('$toPersonalSignEnvelopeHash')(domainSeparator(this.domain), contents), + ).to.equal(hashTypedData(this.domain, personalSignStructHash)); + }); + }); + + describe('toTypedDataEnvelopeHash', function () { + it('should produce a typed data EIP712 envelope', async function () { + const contents = ethers.randomBytes(32); + const contentsTypeName = 'SomeType'; + const contentsType = `${contentsTypeName}()`; + const typedDataEnvelopeStructHash = hashTypedDataEnvelopeStruct(this.domain, contents, contentsType); + const expected = hashTypedData(this.domain, typedDataEnvelopeStructHash); + expect( + await this.mock.getFunction('$toTypedDataEnvelopeHash')( + domainSeparator(this.domain), + typedDataEnvelopeStructHash, + ), + ).to.equal(expected); + }); + }); + + describe('TYPED_DATA_ENVELOPE_TYPEHASH', function () { + it('should match the hardcoded value', async function () { + const contentsTypeName = 'FooType'; + const contentsType = `${contentsTypeName}(address foo,uint256 bar)`; + expect(await this.mock.getFunction('$TYPED_DATA_ENVELOPE_TYPEHASH')(ethers.toUtf8Bytes(contentsType))).to.equal( + hashTypedDataEnvelopeType(contentsTypeName, contentsType), + ); + }); + }); + + describe('tryValidateContentsType', function () { + it('should return true for a valid type', async function () { + const contentsType = ethers.toUtf8Bytes('SomeType(address foo,uint256 bar)'); + const [valid, type] = await this.mock.getFunction('$tryValidateContentsType')(contentsType); + expect(valid).to.be.true; + expect(type).to.equal(ethers.hexlify(ethers.toUtf8Bytes('SomeType'))); + }); + + it('should return false for an empty type', async function () { + const [valid, type] = await this.mock.getFunction('$tryValidateContentsType')('0x'); + expect(valid).to.be.false; + expect(type).to.equal('0x'); + }); + + const invalidInitialCharacters = new Array('abcdefghijklmnopqrstuvwxyz('); + for (const char of invalidInitialCharacters) { + it(`should return false if starting with ${char}`, async function () { + const [valid, type] = await this.mock.getFunction('$tryValidateContentsType')( + ethers.toUtf8Bytes(`${char}SomeType()`), + ); + expect(valid).to.be.false; + expect(type).to.equal('0x'); + }); + } + + const forbidenChars = [',', ' ', ')', '\x00']; + for (const char of forbidenChars) { + it(`should return false if it has [${char}] char`, async function () { + const [valid, type] = await this.mock.getFunction('$tryValidateContentsType')( + ethers.toUtf8Bytes(`SomeType${char}`), + ); + expect(valid).to.be.false; + expect(type).to.equal('0x'); + }); + } + }); + + describe('typedDataEnvelopeStructHash', function () { + it('should match the typed data envelope struct hash', async function () { + const contents = ethers.randomBytes(32); + const contentsTypeName = 'SomeType'; + const contentsType = `${contentsTypeName}(address foo,uint256 bar)`; + const typedDataEnvelopeStructHash = hashTypedDataEnvelopeStruct(this.domain, contents, contentsType); + expect( + await this.mock.getFunction('$typedDataEnvelopeStructHash')( + ethers.toUtf8Bytes(contentsType), + contents, + this.domain.name, + this.domain.version, + this.domain.verifyingContract, + ethers.ZeroHash, + [], + ), + ).to.equal(typedDataEnvelopeStructHash); + }); + }); +}); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index c2bd1a479b2..144b2a98e69 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -97,7 +97,7 @@ function shouldSupportInterfaces(interfaces = []) { describe('ERC165', function () { beforeEach(function () { - this.contractUnderTest = this.mock || this.token; + this.contractUnderTest = this.mock || this.token || this.smartAccount; }); describe('when the interfaceId is supported', function () {