From 1804267bd012021248d84df1950f9d6b8c97a6ac Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Sep 2024 11:54:15 +0200 Subject: [PATCH 01/13] prepare account-abstraction branch --- contracts/abstraction/utils/ERC4337Utils.sol | 198 +++++ contracts/abstraction/utils/ERC7579Utils.sol | 115 +++ contracts/interfaces/IERC4337.sol | 118 +++ contracts/interfaces/IERC7579Account.sol | 114 +++ contracts/interfaces/IERC7579Module.sol | 89 ++ contracts/utils/Packing.sol | 513 +++++++++++ .../erc4337-entrypoint/core/EntryPoint.sol | 802 ++++++++++++++++++ .../erc4337-entrypoint/core/Helpers.sol | 106 +++ .../erc4337-entrypoint/core/NonceManager.sol | 43 + .../erc4337-entrypoint/core/SenderCreator.sol | 38 + .../erc4337-entrypoint/core/StakeManager.sol | 145 ++++ .../core/UserOperationLib.sol | 139 +++ .../interfaces/IAccount.sol | 39 + .../interfaces/IAccountExecute.sol | 20 + .../interfaces/IAggregator.sol | 44 + .../interfaces/IEntryPoint.sol | 223 +++++ .../interfaces/INonceManager.sol | 27 + .../interfaces/IPaymaster.sol | 63 ++ .../interfaces/IStakeManager.sol | 111 +++ .../interfaces/PackedUserOperation.sol | 28 + .../vendor/erc4337-entrypoint/utils/Exec.sol | 70 ++ scripts/generate/templates/Packing.opts.js | 2 +- test/utils/Packing.t.sol | 312 +++++++ 23 files changed, 3358 insertions(+), 1 deletion(-) create mode 100644 contracts/abstraction/utils/ERC4337Utils.sol create mode 100644 contracts/abstraction/utils/ERC7579Utils.sol create mode 100644 contracts/interfaces/IERC4337.sol create mode 100644 contracts/interfaces/IERC7579Account.sol create mode 100644 contracts/interfaces/IERC7579Module.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/Helpers.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/NonceManager.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/StakeManager.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol create mode 100644 contracts/vendor/erc4337-entrypoint/utils/Exec.sol diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol new file mode 100644 index 00000000000..3d95db10815 --- /dev/null +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: MIT + +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 { + using Packing for *; + /* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * return this value on success. + */ + uint256 internal constant SIG_VALIDATION_SUCCESS = 0; + + /* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * must return this value in case of signature failure, instead of revert. + */ + uint256 internal constant SIG_VALIDATION_FAILED = 1; + + // Validation data + function parseValidationData( + uint256 validationData + ) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) { + validAfter = uint48(bytes32(validationData).extract_32_6(0x00)); + validUntil = uint48(bytes32(validationData).extract_32_6(0x06)); + aggregator = address(bytes32(validationData).extract_32_20(0x0c)); + if (validUntil == 0) validUntil = type(uint48).max; + } + + function packValidationData( + address aggregator, + uint48 validAfter, + uint48 validUntil + ) internal pure returns (uint256) { + return uint256(bytes6(validAfter).pack_6_6(bytes6(validUntil)).pack_12_20(bytes20(aggregator))); + } + + function packValidationData(bool sigSuccess, uint48 validAfter, uint48 validUntil) internal pure returns (uint256) { + return + uint256( + bytes6(validAfter).pack_6_6(bytes6(validUntil)).pack_12_20( + bytes20(uint160(Math.ternary(sigSuccess, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED))) + ) + ); + } + + function combineValidationData(uint256 validationData1, uint256 validationData2) internal pure returns (uint256) { + (address aggregator1, uint48 validAfter1, uint48 validUntil1) = parseValidationData(validationData1); + (address aggregator2, uint48 validAfter2, uint48 validUntil2) = parseValidationData(validationData2); + + bool success = aggregator1 == address(0) && aggregator2 == address(0); + uint48 validAfter = uint48(Math.max(validAfter1, validAfter2)); + uint48 validUntil = uint48(Math.min(validUntil1, validUntil2)); + return packValidationData(success, validAfter, validUntil); + } + + function getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) { + if (validationData == 0) { + return (address(0), false); + } else { + (address agregator, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData); + return (agregator, block.timestamp > validUntil || block.timestamp < validAfter); + } + } + + // Packed user operation + function hash(PackedUserOperation calldata self) internal view returns (bytes32) { + return hash(self, address(this), block.chainid); + } + + function hash( + PackedUserOperation calldata self, + address entrypoint, + uint256 chainid + ) internal pure returns (bytes32) { + // Memory.FreePtr ptr = Memory.save(); + bytes32 result = keccak256( + abi.encode( + keccak256( + abi.encode( + self.sender, + self.nonce, + keccak256(self.initCode), + keccak256(self.callData), + self.accountGasLimits, + self.preVerificationGas, + self.gasFees, + keccak256(self.paymasterAndData) + ) + ), + entrypoint, + chainid + ) + ); + // Memory.load(ptr); + return result; + } + + function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(self.accountGasLimits.extract_32_16(0x00)); + } + + function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(self.accountGasLimits.extract_32_16(0x10)); + } + + function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(self.gasFees.extract_32_16(0x00)); + } + + function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(self.gasFees.extract_32_16(0x10)); + } + + function gasPrice(PackedUserOperation calldata self) internal view returns (uint256) { + unchecked { + // Following values are "per gas" + uint256 maxPriorityFee = maxPriorityFeePerGas(self); + uint256 maxFee = maxFeePerGas(self); + return Math.ternary(maxFee == maxPriorityFee, maxFee, Math.min(maxFee, maxPriorityFee + block.basefee)); + } + } + + function paymaster(PackedUserOperation calldata self) internal pure returns (address) { + return address(bytes20(self.paymasterAndData[0:20])); + } + + function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(bytes16(self.paymasterAndData[20:36])); + } + + function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(bytes16(self.paymasterAndData[36:52])); + } + + struct UserOpInfo { + address sender; + uint256 nonce; + uint256 verificationGasLimit; + uint256 callGasLimit; + uint256 paymasterVerificationGasLimit; + uint256 paymasterPostOpGasLimit; + uint256 preVerificationGas; + address paymaster; + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + bytes32 userOpHash; + uint256 prefund; + uint256 preOpGas; + bytes context; + } + + function load(UserOpInfo memory self, PackedUserOperation calldata source) internal view { + self.sender = source.sender; + self.nonce = source.nonce; + self.verificationGasLimit = uint128(bytes32(source.accountGasLimits).extract_32_16(0x00)); + self.callGasLimit = uint128(bytes32(source.accountGasLimits).extract_32_16(0x10)); + self.preVerificationGas = source.preVerificationGas; + self.maxPriorityFeePerGas = uint128(bytes32(source.gasFees).extract_32_16(0x00)); + self.maxFeePerGas = uint128(bytes32(source.gasFees).extract_32_16(0x10)); + + if (source.paymasterAndData.length > 0) { + require(source.paymasterAndData.length >= 52, "AA93 invalid paymasterAndData"); + self.paymaster = paymaster(source); + self.paymasterVerificationGasLimit = paymasterVerificationGasLimit(source); + self.paymasterPostOpGasLimit = paymasterPostOpGasLimit(source); + } else { + self.paymaster = address(0); + self.paymasterVerificationGasLimit = 0; + self.paymasterPostOpGasLimit = 0; + } + self.userOpHash = hash(source); + self.prefund = 0; + self.preOpGas = 0; + self.context = ""; + } + + function requiredPrefund(UserOpInfo memory self) internal pure returns (uint256) { + return + (self.verificationGasLimit + + self.callGasLimit + + self.paymasterVerificationGasLimit + + self.paymasterPostOpGasLimit + + self.preVerificationGas) * self.maxFeePerGas; + } + + function gasPrice(UserOpInfo memory self) internal view returns (uint256) { + unchecked { + uint256 maxFee = self.maxFeePerGas; + uint256 maxPriorityFee = self.maxPriorityFeePerGas; + return Math.ternary(maxFee == maxPriorityFee, maxFee, Math.min(maxFee, maxPriorityFee + block.basefee)); + } + } +} diff --git a/contracts/abstraction/utils/ERC7579Utils.sol b/contracts/abstraction/utils/ERC7579Utils.sol new file mode 100644 index 00000000000..b35e157101b --- /dev/null +++ b/contracts/abstraction/utils/ERC7579Utils.sol @@ -0,0 +1,115 @@ +// 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/interfaces/IERC4337.sol b/contracts/interfaces/IERC4337.sol new file mode 100644 index 00000000000..0f681b74aba --- /dev/null +++ b/contracts/interfaces/IERC4337.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/* +struct UserOperation { + address sender; // The account making the operation + uint256 nonce; // Anti-replay parameter (see “Semi-abstracted Nonce Support” ) + address factory; // account factory, only for new accounts + bytes factoryData; // data for account factory (only if account factory exists) + bytes callData; // The data to pass to the sender during the main execution call + uint256 callGasLimit; // The amount of gas to allocate the main execution call + uint256 verificationGasLimit; // The amount of gas to allocate for the verification step + uint256 preVerificationGas; // Extra gas to pay the bunder + uint256 maxFeePerGas; // Maximum fee per gas (similar to EIP-1559 max_fee_per_gas) + uint256 maxPriorityFeePerGas; // Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas) + address paymaster; // Address of paymaster contract, (or empty, if account pays for itself) + uint256 paymasterVerificationGasLimit; // The amount of gas to allocate for the paymaster validation code + uint256 paymasterPostOpGasLimit; // The amount of gas to allocate for the paymaster post-operation code + bytes paymasterData; // Data for paymaster (only if paymaster exists) + bytes signature; // Data passed into the account to verify authorization +} +*/ + +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; +} + +interface IAggregator { + function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external view; + + function validateUserOpSignature( + PackedUserOperation calldata userOp + ) external view returns (bytes memory sigForUserOp); + + function aggregateSignatures( + PackedUserOperation[] calldata userOps + ) external view returns (bytes memory aggregatesSignature); +} + +interface IEntryPointNonces { + function getNonce(address sender, uint192 key) external view returns (uint256 nonce); +} + +interface IEntryPointStake { + function balanceOf(address account) external view returns (uint256); + + function depositTo(address account) external payable; + + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external; + + function addStake(uint32 unstakeDelaySec) external payable; + + function unlockStake() external; + + function withdrawStake(address payable withdrawAddress) external; +} + +interface IEntryPoint is IEntryPointNonces, IEntryPointStake { + error FailedOp(uint256 opIndex, string reason); + error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); + + struct UserOpsPerAggregator { + PackedUserOperation[] userOps; + IAggregator aggregator; + bytes signature; + } + + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; + + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) external; +} + +// TODO: EntryPointSimulation + +interface IAccount { + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData); +} + +interface IAccountExecute { + function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; +} + +interface IPaymaster { + enum PostOpMode { + opSucceeded, + opReverted, + postOpReverted + } + + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external returns (bytes memory context, uint256 validationData); + + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external; +} diff --git a/contracts/interfaces/IERC7579Account.sol b/contracts/interfaces/IERC7579Account.sol new file mode 100644 index 00000000000..0be805f5b1f --- /dev/null +++ b/contracts/interfaces/IERC7579Account.sol @@ -0,0 +1,114 @@ +// 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"; + +struct Execution { + address target; + uint256 value; + bytes callData; +} + +interface IERC7579Execution { + /** + * @dev Executes a transaction on behalf of the account. + * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details + * @param executionCalldata The encoded execution call data + * + * MUST ensure adequate authorization control: e.g. onlyEntryPointOrSelf if used with ERC-4337 + * If a mode is requested that is not supported by the Account, it MUST revert + */ + function execute(bytes32 mode, bytes calldata executionCalldata) external; + + /** + * @dev Executes a transaction on behalf of the account. + * This function is intended to be called by Executor Modules + * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details + * @param executionCalldata The encoded execution call data + * + * MUST ensure adequate authorization control: i.e. onlyExecutorModule + * If a mode is requested that is not supported by the Account, it MUST revert + */ + function executeFromExecutor( + bytes32 mode, + bytes calldata executionCalldata + ) external returns (bytes[] memory returnData); +} + +interface IERC7579AccountConfig { + /** + * @dev Returns the account id of the smart account + * @return accountImplementationId the account id of the smart account + * + * MUST return a non-empty string + * The accountId SHOULD be structured like so: + * "vendorname.accountname.semver" + * The id SHOULD be unique across all smart accounts + */ + function accountId() external view returns (string memory accountImplementationId); + + /** + * @dev Function to check if the account supports a certain execution mode (see above) + * @param encodedMode the encoded mode + * + * MUST return true if the account supports the mode and false otherwise + */ + function supportsExecutionMode(bytes32 encodedMode) external view returns (bool); + + /** + * @dev Function to check if the account supports a certain module typeId + * @param moduleTypeId the module type ID according to the ERC-7579 spec + * + * MUST return true if the account supports the module type and false otherwise + */ + function supportsModule(uint256 moduleTypeId) external view returns (bool); +} + +interface IERC7579ModuleConfig { + event ModuleInstalled(uint256 moduleTypeId, address module); + event ModuleUninstalled(uint256 moduleTypeId, address module); + + /** + * @dev Installs a Module of a certain type on the smart account + * @param moduleTypeId the module type ID according to the ERC-7579 spec + * @param module the module address + * @param initData arbitrary data that may be required on the module during `onInstall` + * initialization. + * + * MUST implement authorization control + * MUST call `onInstall` on the module with the `initData` parameter if provided + * MUST emit ModuleInstalled event + * MUST revert if the module is already installed or the initialization on the module failed + */ + function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external; + + /** + * @dev Uninstalls a Module of a certain type on the smart account + * @param moduleTypeId the module type ID according the ERC-7579 spec + * @param module the module address + * @param deInitData arbitrary data that may be required on the module during `onInstall` + * initialization. + * + * MUST implement authorization control + * MUST call `onUninstall` on the module with the `deInitData` parameter if provided + * MUST emit ModuleUninstalled event + * MUST revert if the module is not installed or the deInitialization on the module failed + */ + function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external; + + /** + * @dev Returns whether a module is installed on the smart account + * @param moduleTypeId the module type ID according the ERC-7579 spec + * @param module the module address + * @param additionalContext arbitrary data that may be required to determine if the module is installed + * + * MUST return true if the module is installed and false otherwise + */ + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) external view returns (bool); +} diff --git a/contracts/interfaces/IERC7579Module.sol b/contracts/interfaces/IERC7579Module.sol new file mode 100644 index 00000000000..1aee412c053 --- /dev/null +++ b/contracts/interfaces/IERC7579Module.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +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; +uint256 constant MODULE_TYPE_HOOK = 4; + +interface IERC7579Module { + /** + * @dev This function is called by the smart account during installation of the module + * @param data arbitrary data that may be required on the module during `onInstall` initialization + * + * MUST revert on error (e.g. if module is already enabled) + */ + function onInstall(bytes calldata data) external; + + /** + * @dev This function is called by the smart account during uninstallation of the module + * @param data arbitrary data that may be required on the module during `onUninstall` de-initialization + * + * MUST revert on error + */ + function onUninstall(bytes calldata data) external; + + /** + * @dev Returns boolean value if module is a certain type + * @param moduleTypeId the module type ID according the ERC-7579 spec + * + * MUST return true if the module is of the given type and false otherwise + */ + function isModuleType(uint256 moduleTypeId) external view returns (bool); +} + +interface IERC7579Validator is IERC7579Module { + /** + * @dev Validates a UserOperation + * @param userOp the ERC-4337 PackedUserOperation + * @param userOpHash the hash of the ERC-4337 PackedUserOperation + * + * MUST validate that the signature is a valid signature of the userOpHash + * SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch + */ + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); + + /** + * @dev Validates a signature using ERC-1271 + * @param sender the address that sent the ERC-1271 request to the smart account + * @param hash the hash of the ERC-1271 request + * @param signature the signature of the ERC-1271 request + * + * MUST return the ERC-1271 `MAGIC_VALUE` if the signature is valid + * MUST NOT modify state + */ + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata signature + ) external view returns (bytes4); +} + +interface IERC7579Hook is IERC7579Module { + /** + * @dev Called by the smart account before execution + * @param msgSender the address that called the smart account + * @param value the value that was sent to the smart account + * @param msgData the data that was sent to the smart account + * + * MAY return arbitrary data in the `hookData` return value + */ + function preCheck( + address msgSender, + uint256 value, + bytes calldata msgData + ) external returns (bytes memory hookData); + + /** + * @dev Called by the smart account after execution + * @param hookData the data that was returned by the `preCheck` function + * + * MAY validate the `hookData` to validate transaction context of the `preCheck` function + */ + function postCheck(bytes calldata hookData) external; +} diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol index 8a8e3fee86f..b3cb8b3773f 100644 --- a/contracts/utils/Packing.sol +++ b/contracts/utils/Packing.sol @@ -65,6 +65,38 @@ library Packing { } } + function pack_2_8(bytes2 left, bytes8 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + left := and(left, shl(240, not(0))) + right := and(right, shl(192, not(0))) + result := or(left, shr(16, right)) + } + } + + function pack_2_10(bytes2 left, bytes10 right) internal pure returns (bytes12 result) { + assembly ("memory-safe") { + left := and(left, shl(240, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(16, right)) + } + } + + function pack_2_20(bytes2 left, bytes20 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(240, not(0))) + right := and(right, shl(96, not(0))) + result := or(left, shr(16, right)) + } + } + + function pack_2_22(bytes2 left, bytes22 right) internal pure returns (bytes24 result) { + assembly ("memory-safe") { + left := and(left, shl(240, not(0))) + right := and(right, shl(80, not(0))) + result := or(left, shr(16, right)) + } + } + function pack_4_2(bytes4 left, bytes2 right) internal pure returns (bytes6 result) { assembly ("memory-safe") { left := and(left, shl(224, not(0))) @@ -81,6 +113,14 @@ library Packing { } } + function pack_4_6(bytes4 left, bytes6 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + left := and(left, shl(224, not(0))) + right := and(right, shl(208, not(0))) + result := or(left, shr(32, right)) + } + } + function pack_4_8(bytes4 left, bytes8 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { left := and(left, shl(224, not(0))) @@ -137,6 +177,14 @@ library Packing { } } + function pack_6_4(bytes6 left, bytes4 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + left := and(left, shl(208, not(0))) + right := and(right, shl(224, not(0))) + result := or(left, shr(48, right)) + } + } + function pack_6_6(bytes6 left, bytes6 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { left := and(left, shl(208, not(0))) @@ -145,6 +193,38 @@ library Packing { } } + function pack_6_10(bytes6 left, bytes10 right) internal pure returns (bytes16 result) { + assembly ("memory-safe") { + left := and(left, shl(208, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(48, right)) + } + } + + function pack_6_16(bytes6 left, bytes16 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(208, not(0))) + right := and(right, shl(128, not(0))) + result := or(left, shr(48, right)) + } + } + + function pack_6_22(bytes6 left, bytes22 right) internal pure returns (bytes28 result) { + assembly ("memory-safe") { + left := and(left, shl(208, not(0))) + right := and(right, shl(80, not(0))) + result := or(left, shr(48, right)) + } + } + + function pack_8_2(bytes8 left, bytes2 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + left := and(left, shl(192, not(0))) + right := and(right, shl(240, not(0))) + result := or(left, shr(64, right)) + } + } + function pack_8_4(bytes8 left, bytes4 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { left := and(left, shl(192, not(0))) @@ -193,6 +273,46 @@ library Packing { } } + function pack_10_2(bytes10 left, bytes2 right) internal pure returns (bytes12 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(240, not(0))) + result := or(left, shr(80, right)) + } + } + + function pack_10_6(bytes10 left, bytes6 right) internal pure returns (bytes16 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(208, not(0))) + result := or(left, shr(80, right)) + } + } + + function pack_10_10(bytes10 left, bytes10 right) internal pure returns (bytes20 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(80, right)) + } + } + + function pack_10_12(bytes10 left, bytes12 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(160, not(0))) + result := or(left, shr(80, right)) + } + } + + function pack_10_22(bytes10 left, bytes22 right) internal pure returns (bytes32 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(80, not(0))) + result := or(left, shr(80, right)) + } + } + function pack_12_4(bytes12 left, bytes4 right) internal pure returns (bytes16 result) { assembly ("memory-safe") { left := and(left, shl(160, not(0))) @@ -209,6 +329,14 @@ library Packing { } } + function pack_12_10(bytes12 left, bytes10 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(160, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(96, right)) + } + } + function pack_12_12(bytes12 left, bytes12 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { left := and(left, shl(160, not(0))) @@ -241,6 +369,14 @@ library Packing { } } + function pack_16_6(bytes16 left, bytes6 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(128, not(0))) + right := and(right, shl(208, not(0))) + result := or(left, shr(128, right)) + } + } + function pack_16_8(bytes16 left, bytes8 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { left := and(left, shl(128, not(0))) @@ -265,6 +401,14 @@ library Packing { } } + function pack_20_2(bytes20 left, bytes2 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(96, not(0))) + right := and(right, shl(240, not(0))) + result := or(left, shr(160, right)) + } + } + function pack_20_4(bytes20 left, bytes4 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { left := and(left, shl(96, not(0))) @@ -289,6 +433,30 @@ library Packing { } } + function pack_22_2(bytes22 left, bytes2 right) internal pure returns (bytes24 result) { + assembly ("memory-safe") { + left := and(left, shl(80, not(0))) + right := and(right, shl(240, not(0))) + result := or(left, shr(176, right)) + } + } + + function pack_22_6(bytes22 left, bytes6 right) internal pure returns (bytes28 result) { + assembly ("memory-safe") { + left := and(left, shl(80, not(0))) + right := and(right, shl(208, not(0))) + result := or(left, shr(176, right)) + } + } + + function pack_22_10(bytes22 left, bytes10 right) internal pure returns (bytes32 result) { + assembly ("memory-safe") { + left := and(left, shl(80, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(176, right)) + } + } + function pack_24_4(bytes24 left, bytes4 right) internal pure returns (bytes28 result) { assembly ("memory-safe") { left := and(left, shl(64, not(0))) @@ -463,6 +631,81 @@ library Packing { } } + function extract_10_1(bytes10 self, uint8 offset) internal pure returns (bytes1 result) { + if (offset > 9) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(248, not(0))) + } + } + + function replace_10_1(bytes10 self, bytes1 value, uint8 offset) internal pure returns (bytes10 result) { + bytes1 oldValue = extract_10_1(self, offset); + assembly ("memory-safe") { + value := and(value, shl(248, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_2(bytes10 self, uint8 offset) internal pure returns (bytes2 result) { + if (offset > 8) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(240, not(0))) + } + } + + function replace_10_2(bytes10 self, bytes2 value, uint8 offset) internal pure returns (bytes10 result) { + bytes2 oldValue = extract_10_2(self, offset); + assembly ("memory-safe") { + value := and(value, shl(240, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_4(bytes10 self, uint8 offset) internal pure returns (bytes4 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(224, not(0))) + } + } + + function replace_10_4(bytes10 self, bytes4 value, uint8 offset) internal pure returns (bytes10 result) { + bytes4 oldValue = extract_10_4(self, offset); + assembly ("memory-safe") { + value := and(value, shl(224, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_6(bytes10 self, uint8 offset) internal pure returns (bytes6 result) { + if (offset > 4) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(208, not(0))) + } + } + + function replace_10_6(bytes10 self, bytes6 value, uint8 offset) internal pure returns (bytes10 result) { + bytes6 oldValue = extract_10_6(self, offset); + assembly ("memory-safe") { + value := and(value, shl(208, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_8(bytes10 self, uint8 offset) internal pure returns (bytes8 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(192, not(0))) + } + } + + function replace_10_8(bytes10 self, bytes8 value, uint8 offset) internal pure returns (bytes10 result) { + bytes8 oldValue = extract_10_8(self, offset); + assembly ("memory-safe") { + value := and(value, shl(192, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_12_1(bytes12 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 11) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -538,6 +781,21 @@ library Packing { } } + function extract_12_10(bytes12 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_12_10(bytes12 self, bytes10 value, uint8 offset) internal pure returns (bytes12 result) { + bytes10 oldValue = extract_12_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_16_1(bytes16 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 15) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -613,6 +871,21 @@ library Packing { } } + function extract_16_10(bytes16 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_16_10(bytes16 self, bytes10 value, uint8 offset) internal pure returns (bytes16 result) { + bytes10 oldValue = extract_16_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_16_12(bytes16 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 4) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -703,6 +976,21 @@ library Packing { } } + function extract_20_10(bytes20 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 10) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_20_10(bytes20 self, bytes10 value, uint8 offset) internal pure returns (bytes20 result) { + bytes10 oldValue = extract_20_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_20_12(bytes20 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 8) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -733,6 +1021,141 @@ library Packing { } } + function extract_22_1(bytes22 self, uint8 offset) internal pure returns (bytes1 result) { + if (offset > 21) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(248, not(0))) + } + } + + function replace_22_1(bytes22 self, bytes1 value, uint8 offset) internal pure returns (bytes22 result) { + bytes1 oldValue = extract_22_1(self, offset); + assembly ("memory-safe") { + value := and(value, shl(248, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_2(bytes22 self, uint8 offset) internal pure returns (bytes2 result) { + if (offset > 20) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(240, not(0))) + } + } + + function replace_22_2(bytes22 self, bytes2 value, uint8 offset) internal pure returns (bytes22 result) { + bytes2 oldValue = extract_22_2(self, offset); + assembly ("memory-safe") { + value := and(value, shl(240, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_4(bytes22 self, uint8 offset) internal pure returns (bytes4 result) { + if (offset > 18) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(224, not(0))) + } + } + + function replace_22_4(bytes22 self, bytes4 value, uint8 offset) internal pure returns (bytes22 result) { + bytes4 oldValue = extract_22_4(self, offset); + assembly ("memory-safe") { + value := and(value, shl(224, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_6(bytes22 self, uint8 offset) internal pure returns (bytes6 result) { + if (offset > 16) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(208, not(0))) + } + } + + function replace_22_6(bytes22 self, bytes6 value, uint8 offset) internal pure returns (bytes22 result) { + bytes6 oldValue = extract_22_6(self, offset); + assembly ("memory-safe") { + value := and(value, shl(208, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_8(bytes22 self, uint8 offset) internal pure returns (bytes8 result) { + if (offset > 14) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(192, not(0))) + } + } + + function replace_22_8(bytes22 self, bytes8 value, uint8 offset) internal pure returns (bytes22 result) { + bytes8 oldValue = extract_22_8(self, offset); + assembly ("memory-safe") { + value := and(value, shl(192, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_10(bytes22 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 12) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_22_10(bytes22 self, bytes10 value, uint8 offset) internal pure returns (bytes22 result) { + bytes10 oldValue = extract_22_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_12(bytes22 self, uint8 offset) internal pure returns (bytes12 result) { + if (offset > 10) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(160, not(0))) + } + } + + function replace_22_12(bytes22 self, bytes12 value, uint8 offset) internal pure returns (bytes22 result) { + bytes12 oldValue = extract_22_12(self, offset); + assembly ("memory-safe") { + value := and(value, shl(160, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_16(bytes22 self, uint8 offset) internal pure returns (bytes16 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(128, not(0))) + } + } + + function replace_22_16(bytes22 self, bytes16 value, uint8 offset) internal pure returns (bytes22 result) { + bytes16 oldValue = extract_22_16(self, offset); + assembly ("memory-safe") { + value := and(value, shl(128, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_20(bytes22 self, uint8 offset) internal pure returns (bytes20 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(96, not(0))) + } + } + + function replace_22_20(bytes22 self, bytes20 value, uint8 offset) internal pure returns (bytes22 result) { + bytes20 oldValue = extract_22_20(self, offset); + assembly ("memory-safe") { + value := and(value, shl(96, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_24_1(bytes24 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 23) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -808,6 +1231,21 @@ library Packing { } } + function extract_24_10(bytes24 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 14) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_24_10(bytes24 self, bytes10 value, uint8 offset) internal pure returns (bytes24 result) { + bytes10 oldValue = extract_24_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_24_12(bytes24 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 12) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -853,6 +1291,21 @@ library Packing { } } + function extract_24_22(bytes24 self, uint8 offset) internal pure returns (bytes22 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(80, not(0))) + } + } + + function replace_24_22(bytes24 self, bytes22 value, uint8 offset) internal pure returns (bytes24 result) { + bytes22 oldValue = extract_24_22(self, offset); + assembly ("memory-safe") { + value := and(value, shl(80, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_28_1(bytes28 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 27) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -928,6 +1381,21 @@ library Packing { } } + function extract_28_10(bytes28 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 18) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_28_10(bytes28 self, bytes10 value, uint8 offset) internal pure returns (bytes28 result) { + bytes10 oldValue = extract_28_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_28_12(bytes28 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 16) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -973,6 +1441,21 @@ library Packing { } } + function extract_28_22(bytes28 self, uint8 offset) internal pure returns (bytes22 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(80, not(0))) + } + } + + function replace_28_22(bytes28 self, bytes22 value, uint8 offset) internal pure returns (bytes28 result) { + bytes22 oldValue = extract_28_22(self, offset); + assembly ("memory-safe") { + value := and(value, shl(80, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_28_24(bytes28 self, uint8 offset) internal pure returns (bytes24 result) { if (offset > 4) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -1063,6 +1546,21 @@ library Packing { } } + function extract_32_10(bytes32 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 22) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_32_10(bytes32 self, bytes10 value, uint8 offset) internal pure returns (bytes32 result) { + bytes10 oldValue = extract_32_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_32_12(bytes32 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 20) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -1108,6 +1606,21 @@ library Packing { } } + function extract_32_22(bytes32 self, uint8 offset) internal pure returns (bytes22 result) { + if (offset > 10) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(80, not(0))) + } + } + + function replace_32_22(bytes32 self, bytes22 value, uint8 offset) internal pure returns (bytes32 result) { + bytes22 oldValue = extract_32_22(self, offset); + assembly ("memory-safe") { + value := and(value, shl(80, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_32_24(bytes32 self, uint8 offset) internal pure returns (bytes24 result) { if (offset > 8) revert OutOfRangeAccess(); assembly ("memory-safe") { diff --git a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol new file mode 100644 index 00000000000..778115b1637 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol @@ -0,0 +1,802 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ + +import "../interfaces/IAccount.sol"; +import "../interfaces/IAccountExecute.sol"; +import "../interfaces/IPaymaster.sol"; +import "../interfaces/IEntryPoint.sol"; + +import "../utils/Exec.sol"; +import "./StakeManager.sol"; +import "./SenderCreator.sol"; +import "./Helpers.sol"; +import "./NonceManager.sol"; +import "./UserOperationLib.sol"; + +// import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +// import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "../../../utils/introspection/ERC165.sol"; // OZ edit +import "../../../utils/ReentrancyGuard.sol"; // OZ edit + +/* + * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. + * Only one instance required on each chain. + */ + +/// @custom:security-contact https://bounty.ethereum.org +contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, ERC165 { + + using UserOperationLib for PackedUserOperation; + + SenderCreator private immutable _senderCreator = new SenderCreator(); + + function senderCreator() internal view virtual returns (SenderCreator) { + return _senderCreator; + } + + //compensate for innerHandleOps' emit message and deposit refund. + // allow some slack for future gas price changes. + uint256 private constant INNER_GAS_OVERHEAD = 10000; + + // Marker for inner call revert on out of gas + bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead"; + bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51"; + + uint256 private constant REVERT_REASON_MAX_LEN = 2048; + uint256 private constant PENALTY_PERCENT = 10; + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + // note: solidity "type(IEntryPoint).interfaceId" is without inherited methods but we want to check everything + return interfaceId == (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) || + interfaceId == type(IEntryPoint).interfaceId || + interfaceId == type(IStakeManager).interfaceId || + interfaceId == type(INonceManager).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * Compensate the caller's beneficiary address with the collected fees of all UserOperations. + * @param beneficiary - The address to receive the fees. + * @param amount - Amount to transfer. + */ + function _compensate(address payable beneficiary, uint256 amount) internal { + require(beneficiary != address(0), "AA90 invalid beneficiary"); + (bool success, ) = beneficiary.call{value: amount}(""); + require(success, "AA91 failed send to beneficiary"); + } + + /** + * Execute a user operation. + * @param opIndex - Index into the opInfo array. + * @param userOp - The userOp to execute. + * @param opInfo - The opInfo filled by validatePrepayment for this userOp. + * @return collected - The total amount this userOp paid. + */ + function _executeUserOp( + uint256 opIndex, + PackedUserOperation calldata userOp, + UserOpInfo memory opInfo + ) + internal + returns + (uint256 collected) { + uint256 preGas = gasleft(); + bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); + bool success; + { + uint256 saveFreePtr; + assembly ("memory-safe") { + saveFreePtr := mload(0x40) + } + bytes calldata callData = userOp.callData; + bytes memory innerCall; + bytes4 methodSig; + assembly { + let len := callData.length + if gt(len, 3) { + methodSig := calldataload(callData.offset) + } + } + if (methodSig == IAccountExecute.executeUserOp.selector) { + bytes memory executeUserOp = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)); + innerCall = abi.encodeCall(this.innerHandleOp, (executeUserOp, opInfo, context)); + } else + { + innerCall = abi.encodeCall(this.innerHandleOp, (callData, opInfo, context)); + } + assembly ("memory-safe") { + success := call(gas(), address(), 0, add(innerCall, 0x20), mload(innerCall), 0, 32) + collected := mload(0) + mstore(0x40, saveFreePtr) + } + } + if (!success) { + bytes32 innerRevertCode; + assembly ("memory-safe") { + let len := returndatasize() + if eq(32,len) { + returndatacopy(0, 0, 32) + innerRevertCode := mload(0) + } + } + if (innerRevertCode == INNER_OUT_OF_GAS) { + // handleOps was called with gas limit too low. abort entire bundle. + //can only be caused by bundler (leaving not enough gas for inner call) + revert FailedOp(opIndex, "AA95 out of gas"); + } else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) { + // innerCall reverted on prefund too low. treat entire prefund as "gas cost" + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + uint256 actualGasCost = opInfo.prefund; + emitPrefundTooLow(opInfo); + emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); + collected = actualGasCost; + } else { + emit PostOpRevertReason( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.nonce, + Exec.getReturnData(REVERT_REASON_MAX_LEN) + ); + + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + collected = _postExecution( + IPaymaster.PostOpMode.postOpReverted, + opInfo, + context, + actualGas + ); + } + } + } + + function emitUserOperationEvent(UserOpInfo memory opInfo, bool success, uint256 actualGasCost, uint256 actualGas) internal virtual { + emit UserOperationEvent( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.paymaster, + opInfo.mUserOp.nonce, + success, + actualGasCost, + actualGas + ); + } + + function emitPrefundTooLow(UserOpInfo memory opInfo) internal virtual { + emit UserOperationPrefundTooLow( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.nonce + ); + } + + /// @inheritdoc IEntryPoint + function handleOps( + PackedUserOperation[] calldata ops, + address payable beneficiary + ) public nonReentrant { + uint256 opslen = ops.length; + UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); + + unchecked { + for (uint256 i = 0; i < opslen; i++) { + UserOpInfo memory opInfo = opInfos[i]; + ( + uint256 validationData, + uint256 pmValidationData + ) = _validatePrepayment(i, ops[i], opInfo); + _validateAccountAndPaymasterValidationData( + i, + validationData, + pmValidationData, + address(0) + ); + } + + uint256 collected = 0; + emit BeforeExecution(); + + for (uint256 i = 0; i < opslen; i++) { + collected += _executeUserOp(i, ops[i], opInfos[i]); + } + + _compensate(beneficiary, collected); + } + } + + /// @inheritdoc IEntryPoint + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) public nonReentrant { + + uint256 opasLen = opsPerAggregator.length; + uint256 totalOps = 0; + for (uint256 i = 0; i < opasLen; i++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[i]; + PackedUserOperation[] calldata ops = opa.userOps; + IAggregator aggregator = opa.aggregator; + + //address(1) is special marker of "signature error" + require( + address(aggregator) != address(1), + "AA96 invalid aggregator" + ); + + if (address(aggregator) != address(0)) { + // solhint-disable-next-line no-empty-blocks + try aggregator.validateSignatures(ops, opa.signature) {} catch { + revert SignatureValidationFailed(address(aggregator)); + } + } + + totalOps += ops.length; + } + + UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); + + uint256 opIndex = 0; + for (uint256 a = 0; a < opasLen; a++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[a]; + PackedUserOperation[] calldata ops = opa.userOps; + IAggregator aggregator = opa.aggregator; + + uint256 opslen = ops.length; + for (uint256 i = 0; i < opslen; i++) { + UserOpInfo memory opInfo = opInfos[opIndex]; + ( + uint256 validationData, + uint256 paymasterValidationData + ) = _validatePrepayment(opIndex, ops[i], opInfo); + _validateAccountAndPaymasterValidationData( + i, + validationData, + paymasterValidationData, + address(aggregator) + ); + opIndex++; + } + } + + emit BeforeExecution(); + + uint256 collected = 0; + opIndex = 0; + for (uint256 a = 0; a < opasLen; a++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[a]; + emit SignatureAggregatorChanged(address(opa.aggregator)); + PackedUserOperation[] calldata ops = opa.userOps; + uint256 opslen = ops.length; + + for (uint256 i = 0; i < opslen; i++) { + collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); + opIndex++; + } + } + emit SignatureAggregatorChanged(address(0)); + + _compensate(beneficiary, collected); + } + + /** + * A memory copy of UserOp static fields only. + * Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster. + */ + struct MemoryUserOp { + address sender; + uint256 nonce; + uint256 verificationGasLimit; + uint256 callGasLimit; + uint256 paymasterVerificationGasLimit; + uint256 paymasterPostOpGasLimit; + uint256 preVerificationGas; + address paymaster; + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + } + + struct UserOpInfo { + MemoryUserOp mUserOp; + bytes32 userOpHash; + uint256 prefund; + uint256 contextOffset; + uint256 preOpGas; + } + + /** + * Inner function to handle a UserOperation. + * Must be declared "external" to open a call context, but it can only be called by handleOps. + * @param callData - The callData to execute. + * @param opInfo - The UserOpInfo struct. + * @param context - The context bytes. + * @return actualGasCost - the actual cost in eth this UserOperation paid for gas + */ + function innerHandleOp( + bytes memory callData, + UserOpInfo memory opInfo, + bytes calldata context + ) external returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + require(msg.sender == address(this), "AA92 internal call only"); + MemoryUserOp memory mUserOp = opInfo.mUserOp; + + uint256 callGasLimit = mUserOp.callGasLimit; + unchecked { + // handleOps was called with gas limit too low. abort entire bundle. + if ( + gasleft() * 63 / 64 < + callGasLimit + + mUserOp.paymasterPostOpGasLimit + + INNER_GAS_OVERHEAD + ) { + assembly ("memory-safe") { + mstore(0, INNER_OUT_OF_GAS) + revert(0, 32) + } + } + } + + IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; + if (callData.length > 0) { + bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); + if (!success) { + bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); + if (result.length > 0) { + emit UserOperationRevertReason( + opInfo.userOpHash, + mUserOp.sender, + mUserOp.nonce, + result + ); + } + mode = IPaymaster.PostOpMode.opReverted; + } + } + + unchecked { + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + return _postExecution(mode, opInfo, context, actualGas); + } + } + + /// @inheritdoc IEntryPoint + function getUserOpHash( + PackedUserOperation calldata userOp + ) public view returns (bytes32) { + return + keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); + } + + /** + * Copy general fields from userOp into the memory opInfo structure. + * @param userOp - The user operation. + * @param mUserOp - The memory user operation. + */ + function _copyUserOpToMemory( + PackedUserOperation calldata userOp, + MemoryUserOp memory mUserOp + ) internal pure { + mUserOp.sender = userOp.sender; + mUserOp.nonce = userOp.nonce; + (mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackUints(userOp.accountGasLimits); + mUserOp.preVerificationGas = userOp.preVerificationGas; + (mUserOp.maxPriorityFeePerGas, mUserOp.maxFeePerGas) = UserOperationLib.unpackUints(userOp.gasFees); + bytes calldata paymasterAndData = userOp.paymasterAndData; + if (paymasterAndData.length > 0) { + require( + paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET, + "AA93 invalid paymasterAndData" + ); + (mUserOp.paymaster, mUserOp.paymasterVerificationGasLimit, mUserOp.paymasterPostOpGasLimit) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData); + } else { + mUserOp.paymaster = address(0); + mUserOp.paymasterVerificationGasLimit = 0; + mUserOp.paymasterPostOpGasLimit = 0; + } + } + + /** + * Get the required prefunded gas fee amount for an operation. + * @param mUserOp - The user operation in memory. + */ + function _getRequiredPrefund( + MemoryUserOp memory mUserOp + ) internal pure returns (uint256 requiredPrefund) { + unchecked { + uint256 requiredGas = mUserOp.verificationGasLimit + + mUserOp.callGasLimit + + mUserOp.paymasterVerificationGasLimit + + mUserOp.paymasterPostOpGasLimit + + mUserOp.preVerificationGas; + + requiredPrefund = requiredGas * mUserOp.maxFeePerGas; + } + } + + /** + * Create sender smart contract account if init code is provided. + * @param opIndex - The operation index. + * @param opInfo - The operation info. + * @param initCode - The init code for the smart contract account. + */ + function _createSenderIfNeeded( + uint256 opIndex, + UserOpInfo memory opInfo, + bytes calldata initCode + ) internal { + if (initCode.length != 0) { + address sender = opInfo.mUserOp.sender; + if (sender.code.length != 0) + revert FailedOp(opIndex, "AA10 sender already constructed"); + address sender1 = senderCreator().createSender{ + gas: opInfo.mUserOp.verificationGasLimit + }(initCode); + if (sender1 == address(0)) + revert FailedOp(opIndex, "AA13 initCode failed or OOG"); + if (sender1 != sender) + revert FailedOp(opIndex, "AA14 initCode must return sender"); + if (sender1.code.length == 0) + revert FailedOp(opIndex, "AA15 initCode must create sender"); + address factory = address(bytes20(initCode[0:20])); + emit AccountDeployed( + opInfo.userOpHash, + sender, + factory, + opInfo.mUserOp.paymaster + ); + } + } + + /// @inheritdoc IEntryPoint + function getSenderAddress(bytes calldata initCode) public { + address sender = senderCreator().createSender(initCode); + revert SenderAddressResult(sender); + } + + /** + * Call account.validateUserOp. + * Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. + * Decrement account's deposit if needed. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPrefund - The required prefund amount. + */ + function _validateAccountPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + UserOpInfo memory opInfo, + uint256 requiredPrefund, + uint256 verificationGasLimit + ) + internal + returns ( + uint256 validationData + ) + { + unchecked { + MemoryUserOp memory mUserOp = opInfo.mUserOp; + address sender = mUserOp.sender; + _createSenderIfNeeded(opIndex, opInfo, op.initCode); + address paymaster = mUserOp.paymaster; + uint256 missingAccountFunds = 0; + if (paymaster == address(0)) { + uint256 bal = balanceOf(sender); + missingAccountFunds = bal > requiredPrefund + ? 0 + : requiredPrefund - bal; + } + try + IAccount(sender).validateUserOp{ + gas: verificationGasLimit + }(op, opInfo.userOpHash, missingAccountFunds) + returns (uint256 _validationData) { + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); + } + if (paymaster == address(0)) { + DepositInfo storage senderInfo = deposits[sender]; + uint256 deposit = senderInfo.deposit; + if (requiredPrefund > deposit) { + revert FailedOp(opIndex, "AA21 didn't pay prefund"); + } + senderInfo.deposit = deposit - requiredPrefund; + } + } + } + + /** + * In case the request has a paymaster: + * - Validate paymaster has enough deposit. + * - Call paymaster.validatePaymasterUserOp. + * - Revert with proper FailedOp in case paymaster reverts. + * - Decrement paymaster's deposit. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPreFund - The required prefund amount. + */ + function _validatePaymasterPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + UserOpInfo memory opInfo, + uint256 requiredPreFund + ) internal returns (bytes memory context, uint256 validationData) { + unchecked { + uint256 preGas = gasleft(); + MemoryUserOp memory mUserOp = opInfo.mUserOp; + address paymaster = mUserOp.paymaster; + DepositInfo storage paymasterInfo = deposits[paymaster]; + uint256 deposit = paymasterInfo.deposit; + if (deposit < requiredPreFund) { + revert FailedOp(opIndex, "AA31 paymaster deposit too low"); + } + paymasterInfo.deposit = deposit - requiredPreFund; + uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit; + try + IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}( + op, + opInfo.userOpHash, + requiredPreFund + ) + returns (bytes memory _context, uint256 _validationData) { + context = _context; + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); + } + if (preGas - gasleft() > pmVerificationGasLimit) { + revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit"); + } + } + } + + /** + * Revert if either account validationData or paymaster validationData is expired. + * @param opIndex - The operation index. + * @param validationData - The account validationData. + * @param paymasterValidationData - The paymaster validationData. + * @param expectedAggregator - The expected aggregator. + */ + function _validateAccountAndPaymasterValidationData( + uint256 opIndex, + uint256 validationData, + uint256 paymasterValidationData, + address expectedAggregator + ) internal view { + (address aggregator, bool outOfTimeRange) = _getValidationData( + validationData + ); + if (expectedAggregator != aggregator) { + revert FailedOp(opIndex, "AA24 signature error"); + } + if (outOfTimeRange) { + revert FailedOp(opIndex, "AA22 expired or not due"); + } + // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. + // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). + address pmAggregator; + (pmAggregator, outOfTimeRange) = _getValidationData( + paymasterValidationData + ); + if (pmAggregator != address(0)) { + revert FailedOp(opIndex, "AA34 signature error"); + } + if (outOfTimeRange) { + revert FailedOp(opIndex, "AA32 paymaster expired or not due"); + } + } + + /** + * Parse validationData into its components. + * @param validationData - The packed validation data (sigFailed, validAfter, validUntil). + * @return aggregator the aggregator of the validationData + * @return outOfTimeRange true if current time is outside the time range of this validationData. + */ + function _getValidationData( + uint256 validationData + ) internal view returns (address aggregator, bool outOfTimeRange) { + if (validationData == 0) { + return (address(0), false); + } + ValidationData memory data = _parseValidationData(validationData); + // solhint-disable-next-line not-rely-on-time + outOfTimeRange = block.timestamp > data.validUntil || block.timestamp < data.validAfter; + aggregator = data.aggregator; + } + + /** + * Validate account and paymaster (if defined) and + * also make sure total validation doesn't exceed verificationGasLimit. + * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) + * @param opIndex - The index of this userOp into the "opInfos" array. + * @param userOp - The userOp to validate. + */ + function _validatePrepayment( + uint256 opIndex, + PackedUserOperation calldata userOp, + UserOpInfo memory outOpInfo + ) + internal + returns (uint256 validationData, uint256 paymasterValidationData) + { + uint256 preGas = gasleft(); + MemoryUserOp memory mUserOp = outOpInfo.mUserOp; + _copyUserOpToMemory(userOp, mUserOp); + outOpInfo.userOpHash = getUserOpHash(userOp); + + // Validate all numeric values in userOp are well below 128 bit, so they can safely be added + // and multiplied without causing overflow. + uint256 verificationGasLimit = mUserOp.verificationGasLimit; + uint256 maxGasValues = mUserOp.preVerificationGas | + verificationGasLimit | + mUserOp.callGasLimit | + mUserOp.paymasterVerificationGasLimit | + mUserOp.paymasterPostOpGasLimit | + mUserOp.maxFeePerGas | + mUserOp.maxPriorityFeePerGas; + require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); + + uint256 requiredPreFund = _getRequiredPrefund(mUserOp); + validationData = _validateAccountPrepayment( + opIndex, + userOp, + outOpInfo, + requiredPreFund, + verificationGasLimit + ); + + if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) { + revert FailedOp(opIndex, "AA25 invalid account nonce"); + } + + unchecked { + if (preGas - gasleft() > verificationGasLimit) { + revert FailedOp(opIndex, "AA26 over verificationGasLimit"); + } + } + + bytes memory context; + if (mUserOp.paymaster != address(0)) { + (context, paymasterValidationData) = _validatePaymasterPrepayment( + opIndex, + userOp, + outOpInfo, + requiredPreFund + ); + } + unchecked { + outOpInfo.prefund = requiredPreFund; + outOpInfo.contextOffset = getOffsetOfMemoryBytes(context); + outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; + } + } + + /** + * Process post-operation, called just after the callData is executed. + * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. + * The excess amount is refunded to the account (or paymaster - if it was used in the request). + * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). + * @param opInfo - UserOp fields and info collected during validation. + * @param context - The context returned in validatePaymasterUserOp. + * @param actualGas - The gas used so far by this user operation. + */ + function _postExecution( + IPaymaster.PostOpMode mode, + UserOpInfo memory opInfo, + bytes memory context, + uint256 actualGas + ) private returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + unchecked { + address refundAddress; + MemoryUserOp memory mUserOp = opInfo.mUserOp; + uint256 gasPrice = getUserOpGasPrice(mUserOp); + + address paymaster = mUserOp.paymaster; + if (paymaster == address(0)) { + refundAddress = mUserOp.sender; + } else { + refundAddress = paymaster; + if (context.length > 0) { + actualGasCost = actualGas * gasPrice; + if (mode != IPaymaster.PostOpMode.postOpReverted) { + try IPaymaster(paymaster).postOp{ + gas: mUserOp.paymasterPostOpGasLimit + }(mode, context, actualGasCost, gasPrice) + // solhint-disable-next-line no-empty-blocks + {} catch { + bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN); + revert PostOpReverted(reason); + } + } + } + } + actualGas += preGas - gasleft(); + + // Calculating a penalty for unused execution gas + { + uint256 executionGasLimit = mUserOp.callGasLimit + mUserOp.paymasterPostOpGasLimit; + uint256 executionGasUsed = actualGas - opInfo.preOpGas; + // this check is required for the gas used within EntryPoint and not covered by explicit gas limits + if (executionGasLimit > executionGasUsed) { + uint256 unusedGas = executionGasLimit - executionGasUsed; + uint256 unusedGasPenalty = (unusedGas * PENALTY_PERCENT) / 100; + actualGas += unusedGasPenalty; + } + } + + actualGasCost = actualGas * gasPrice; + uint256 prefund = opInfo.prefund; + if (prefund < actualGasCost) { + if (mode == IPaymaster.PostOpMode.postOpReverted) { + actualGasCost = prefund; + emitPrefundTooLow(opInfo); + emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); + } else { + assembly ("memory-safe") { + mstore(0, INNER_REVERT_LOW_PREFUND) + revert(0, 32) + } + } + } else { + uint256 refund = prefund - actualGasCost; + _incrementDeposit(refundAddress, refund); + bool success = mode == IPaymaster.PostOpMode.opSucceeded; + emitUserOperationEvent(opInfo, success, actualGasCost, actualGas); + } + } // unchecked + } + + /** + * The gas price this UserOp agrees to pay. + * Relayer/block builder might submit the TX with higher priorityFee, but the user should not. + * @param mUserOp - The userOp to get the gas price from. + */ + function getUserOpGasPrice( + MemoryUserOp memory mUserOp + ) internal view returns (uint256) { + unchecked { + uint256 maxFeePerGas = mUserOp.maxFeePerGas; + uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas; + if (maxFeePerGas == maxPriorityFeePerGas) { + //legacy mode (for networks that don't support basefee opcode) + return maxFeePerGas; + } + return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); + } + } + + /** + * The offset of the given bytes in memory. + * @param data - The bytes to get the offset of. + */ + function getOffsetOfMemoryBytes( + bytes memory data + ) internal pure returns (uint256 offset) { + assembly { + offset := data + } + } + + /** + * The bytes in memory at the given offset. + * @param offset - The offset to get the bytes from. + */ + function getMemoryBytesFromOffset( + uint256 offset + ) internal pure returns (bytes memory data) { + assembly ("memory-safe") { + data := offset + } + } + + /// @inheritdoc IEntryPoint + function delegateAndRevert(address target, bytes calldata data) external { + (bool success, bytes memory ret) = target.delegatecall(data); + revert DelegateAndRevert(success, ret); + } +} diff --git a/contracts/vendor/erc4337-entrypoint/core/Helpers.sol b/contracts/vendor/erc4337-entrypoint/core/Helpers.sol new file mode 100644 index 00000000000..8579008613a --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/Helpers.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable no-inline-assembly */ + + + /* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * must return this value in case of signature failure, instead of revert. + */ +uint256 constant SIG_VALIDATION_FAILED = 1; + + +/* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * return this value on success. + */ +uint256 constant SIG_VALIDATION_SUCCESS = 0; + + +/** + * Returned data from validateUserOp. + * validateUserOp returns a uint256, which is created by `_packedValidationData` and + * parsed by `_parseValidationData`. + * @param aggregator - address(0) - The account validated the signature by itself. + * address(1) - The account failed to validate the signature. + * otherwise - This is an address of a signature aggregator that must + * be used to validate the signature. + * @param validAfter - This UserOp is valid only after this timestamp. + * @param validaUntil - This UserOp is valid only up to this timestamp. + */ +struct ValidationData { + address aggregator; + uint48 validAfter; + uint48 validUntil; +} + +/** + * Extract sigFailed, validAfter, validUntil. + * Also convert zero validUntil to type(uint48).max. + * @param validationData - The packed validation data. + */ +function _parseValidationData( + uint256 validationData +) pure returns (ValidationData memory data) { + address aggregator = address(uint160(validationData)); + uint48 validUntil = uint48(validationData >> 160); + if (validUntil == 0) { + validUntil = type(uint48).max; + } + uint48 validAfter = uint48(validationData >> (48 + 160)); + return ValidationData(aggregator, validAfter, validUntil); +} + +/** + * Helper to pack the return value for validateUserOp. + * @param data - The ValidationData to pack. + */ +function _packValidationData( + ValidationData memory data +) pure returns (uint256) { + return + uint160(data.aggregator) | + (uint256(data.validUntil) << 160) | + (uint256(data.validAfter) << (160 + 48)); +} + +/** + * Helper to pack the return value for validateUserOp, when not using an aggregator. + * @param sigFailed - True for signature failure, false for success. + * @param validUntil - Last timestamp this UserOperation is valid (or zero for infinite). + * @param validAfter - First timestamp this UserOperation is valid. + */ +function _packValidationData( + bool sigFailed, + uint48 validUntil, + uint48 validAfter +) pure returns (uint256) { + return + (sigFailed ? 1 : 0) | + (uint256(validUntil) << 160) | + (uint256(validAfter) << (160 + 48)); +} + +/** + * keccak function over calldata. + * @dev copy calldata into memory, do keccak and drop allocated memory. Strangely, this is more efficient than letting solidity do it. + */ + function calldataKeccak(bytes calldata data) pure returns (bytes32 ret) { + assembly ("memory-safe") { + let mem := mload(0x40) + let len := data.length + calldatacopy(mem, data.offset, len) + ret := keccak256(mem, len) + } + } + + +/** + * The minimum of two numbers. + * @param a - First number. + * @param b - Second number. + */ + function min(uint256 a, uint256 b) pure returns (uint256) { + return a < b ? a : b; + } diff --git a/contracts/vendor/erc4337-entrypoint/core/NonceManager.sol b/contracts/vendor/erc4337-entrypoint/core/NonceManager.sol new file mode 100644 index 00000000000..7bef62e99dd --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/NonceManager.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +import "../interfaces/INonceManager.sol"; + +/** + * nonce management functionality + */ +abstract contract NonceManager is INonceManager { + + /** + * The next valid sequence number for a given nonce key. + */ + mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber; + + /// @inheritdoc INonceManager + function getNonce(address sender, uint192 key) + public view override returns (uint256 nonce) { + return nonceSequenceNumber[sender][key] | (uint256(key) << 64); + } + + // allow an account to manually increment its own nonce. + // (mainly so that during construction nonce can be made non-zero, + // to "absorb" the gas cost of first nonce increment to 1st transaction (construction), + // not to 2nd transaction) + function incrementNonce(uint192 key) public override { + nonceSequenceNumber[msg.sender][key]++; + } + + /** + * validate nonce uniqueness for this account. + * called just after validateUserOp() + * @return true if the nonce was incremented successfully. + * false if the current nonce doesn't match the given one. + */ + function _validateAndUpdateNonce(address sender, uint256 nonce) internal returns (bool) { + + uint192 key = uint192(nonce >> 64); + uint64 seq = uint64(nonce); + return nonceSequenceNumber[sender][key]++ == seq; + } + +} diff --git a/contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol b/contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol new file mode 100644 index 00000000000..43ea80367ef --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/** + * Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, + * which is explicitly not the entryPoint itself. + */ +contract SenderCreator { + /** + * Call the "initCode" factory to create and return the sender account address. + * @param initCode - The initCode value from a UserOp. contains 20 bytes of factory address, + * followed by calldata. + * @return sender - The returned address of the created account, or zero address on failure. + */ + function createSender( + bytes calldata initCode + ) external returns (address sender) { + address factory = address(bytes20(initCode[0:20])); + bytes memory initCallData = initCode[20:]; + bool success; + /* solhint-disable no-inline-assembly */ + assembly ("memory-safe") { + success := call( + gas(), + factory, + 0, + add(initCallData, 0x20), + mload(initCallData), + 0, + 32 + ) + sender := mload(0) + } + if (!success) { + sender = address(0); + } + } +} diff --git a/contracts/vendor/erc4337-entrypoint/core/StakeManager.sol b/contracts/vendor/erc4337-entrypoint/core/StakeManager.sol new file mode 100644 index 00000000000..f90210b7e38 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/StakeManager.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.23; + +import "../interfaces/IStakeManager.sol"; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable not-rely-on-time */ + +/** + * Manage deposits and stakes. + * Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account). + * Stake is value locked for at least "unstakeDelay" by a paymaster. + */ +abstract contract StakeManager is IStakeManager { + /// maps paymaster to their deposits and stakes + mapping(address => DepositInfo) public deposits; + + /// @inheritdoc IStakeManager + function getDepositInfo( + address account + ) public view returns (DepositInfo memory info) { + return deposits[account]; + } + + /** + * Internal method to return just the stake info. + * @param addr - The account to query. + */ + function _getStakeInfo( + address addr + ) internal view returns (StakeInfo memory info) { + DepositInfo storage depositInfo = deposits[addr]; + info.stake = depositInfo.stake; + info.unstakeDelaySec = depositInfo.unstakeDelaySec; + } + + /// @inheritdoc IStakeManager + function balanceOf(address account) public view returns (uint256) { + return deposits[account].deposit; + } + + receive() external payable { + depositTo(msg.sender); + } + + /** + * Increments an account's deposit. + * @param account - The account to increment. + * @param amount - The amount to increment by. + * @return the updated deposit of this account + */ + function _incrementDeposit(address account, uint256 amount) internal returns (uint256) { + DepositInfo storage info = deposits[account]; + uint256 newAmount = info.deposit + amount; + info.deposit = newAmount; + return newAmount; + } + + /** + * Add to the deposit of the given account. + * @param account - The account to add to. + */ + function depositTo(address account) public virtual payable { + uint256 newDeposit = _incrementDeposit(account, msg.value); + emit Deposited(account, newDeposit); + } + + /** + * Add to the account's stake - amount and delay + * any pending unstake is first cancelled. + * @param unstakeDelaySec The new lock duration before the deposit can be withdrawn. + */ + function addStake(uint32 unstakeDelaySec) public payable { + DepositInfo storage info = deposits[msg.sender]; + require(unstakeDelaySec > 0, "must specify unstake delay"); + require( + unstakeDelaySec >= info.unstakeDelaySec, + "cannot decrease unstake time" + ); + uint256 stake = info.stake + msg.value; + require(stake > 0, "no stake specified"); + require(stake <= type(uint112).max, "stake overflow"); + deposits[msg.sender] = DepositInfo( + info.deposit, + true, + uint112(stake), + unstakeDelaySec, + 0 + ); + emit StakeLocked(msg.sender, stake, unstakeDelaySec); + } + + /** + * Attempt to unlock the stake. + * The value can be withdrawn (using withdrawStake) after the unstake delay. + */ + function unlockStake() external { + DepositInfo storage info = deposits[msg.sender]; + require(info.unstakeDelaySec != 0, "not staked"); + require(info.staked, "already unstaking"); + uint48 withdrawTime = uint48(block.timestamp) + info.unstakeDelaySec; + info.withdrawTime = withdrawTime; + info.staked = false; + emit StakeUnlocked(msg.sender, withdrawTime); + } + + /** + * Withdraw from the (unlocked) stake. + * Must first call unlockStake and wait for the unstakeDelay to pass. + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external { + DepositInfo storage info = deposits[msg.sender]; + uint256 stake = info.stake; + require(stake > 0, "No stake to withdraw"); + require(info.withdrawTime > 0, "must call unlockStake() first"); + require( + info.withdrawTime <= block.timestamp, + "Stake withdrawal is not due" + ); + info.unstakeDelaySec = 0; + info.withdrawTime = 0; + info.stake = 0; + emit StakeWithdrawn(msg.sender, withdrawAddress, stake); + (bool success,) = withdrawAddress.call{value: stake}(""); + require(success, "failed to withdraw stake"); + } + + /** + * Withdraw from the deposit. + * @param withdrawAddress - The address to send withdrawn value. + * @param withdrawAmount - The amount to withdraw. + */ + function withdrawTo( + address payable withdrawAddress, + uint256 withdrawAmount + ) external { + DepositInfo storage info = deposits[msg.sender]; + require(withdrawAmount <= info.deposit, "Withdraw amount too large"); + info.deposit = info.deposit - withdrawAmount; + emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); + (bool success,) = withdrawAddress.call{value: withdrawAmount}(""); + require(success, "failed to withdraw"); + } +} diff --git a/contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol b/contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol new file mode 100644 index 00000000000..dcf5740cc28 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable no-inline-assembly */ + +import "../interfaces/PackedUserOperation.sol"; +import {calldataKeccak, min} from "./Helpers.sol"; + +/** + * Utility functions helpful when working with UserOperation structs. + */ +library UserOperationLib { + + uint256 public constant PAYMASTER_VALIDATION_GAS_OFFSET = 20; + uint256 public constant PAYMASTER_POSTOP_GAS_OFFSET = 36; + uint256 public constant PAYMASTER_DATA_OFFSET = 52; + /** + * Get sender from user operation data. + * @param userOp - The user operation data. + */ + function getSender( + PackedUserOperation calldata userOp + ) internal pure returns (address) { + address data; + //read sender from userOp, which is first userOp member (saves 800 gas...) + assembly { + data := calldataload(userOp) + } + return address(uint160(data)); + } + + /** + * Relayer/block builder might submit the TX with higher priorityFee, + * but the user should not pay above what he signed for. + * @param userOp - The user operation data. + */ + function gasPrice( + PackedUserOperation calldata userOp + ) internal view returns (uint256) { + unchecked { + (uint256 maxPriorityFeePerGas, uint256 maxFeePerGas) = unpackUints(userOp.gasFees); + if (maxFeePerGas == maxPriorityFeePerGas) { + //legacy mode (for networks that don't support basefee opcode) + return maxFeePerGas; + } + return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); + } + } + + /** + * Pack the user operation data into bytes for hashing. + * @param userOp - The user operation data. + */ + function encode( + PackedUserOperation calldata userOp + ) internal pure returns (bytes memory ret) { + address sender = getSender(userOp); + uint256 nonce = userOp.nonce; + bytes32 hashInitCode = calldataKeccak(userOp.initCode); + bytes32 hashCallData = calldataKeccak(userOp.callData); + bytes32 accountGasLimits = userOp.accountGasLimits; + uint256 preVerificationGas = userOp.preVerificationGas; + bytes32 gasFees = userOp.gasFees; + bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData); + + return abi.encode( + sender, nonce, + hashInitCode, hashCallData, + accountGasLimits, preVerificationGas, gasFees, + hashPaymasterAndData + ); + } + + function unpackUints( + bytes32 packed + ) internal pure returns (uint256 high128, uint256 low128) { + return (uint128(bytes16(packed)), uint128(uint256(packed))); + } + + //unpack just the high 128-bits from a packed value + function unpackHigh128(bytes32 packed) internal pure returns (uint256) { + return uint256(packed) >> 128; + } + + // unpack just the low 128-bits from a packed value + function unpackLow128(bytes32 packed) internal pure returns (uint256) { + return uint128(uint256(packed)); + } + + function unpackMaxPriorityFeePerGas(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return unpackHigh128(userOp.gasFees); + } + + function unpackMaxFeePerGas(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return unpackLow128(userOp.gasFees); + } + + function unpackVerificationGasLimit(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return unpackHigh128(userOp.accountGasLimits); + } + + function unpackCallGasLimit(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return unpackLow128(userOp.accountGasLimits); + } + + function unpackPaymasterVerificationGasLimit(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_POSTOP_GAS_OFFSET])); + } + + function unpackPostOpGasLimit(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET])); + } + + function unpackPaymasterStaticFields( + bytes calldata paymasterAndData + ) internal pure returns (address paymaster, uint256 validationGasLimit, uint256 postOpGasLimit) { + return ( + address(bytes20(paymasterAndData[: PAYMASTER_VALIDATION_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_POSTOP_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET])) + ); + } + + /** + * Hash the user operation data. + * @param userOp - The user operation data. + */ + function hash( + PackedUserOperation calldata userOp + ) internal pure returns (bytes32) { + return keccak256(encode(userOp)); + } +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol new file mode 100644 index 00000000000..e3b355fbc27 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +import "./PackedUserOperation.sol"; + +interface IAccount { + /** + * Validate user's signature and nonce + * the entryPoint will make the call to the recipient only if this validation call returns successfully. + * signature failure should be reported by returning SIG_VALIDATION_FAILED (1). + * This allows making a "simulation call" without a valid signature + * Other failures (e.g. nonce mismatch, or invalid signature format) should still revert to signal failure. + * + * @dev Must validate caller is the entryPoint. + * Must validate the signature and nonce + * @param userOp - The operation that is about to be executed. + * @param userOpHash - Hash of the user's request data. can be used as the basis for signature. + * @param missingAccountFunds - Missing funds on the account's deposit in the entrypoint. + * This is the minimum amount to transfer to the sender(entryPoint) to be + * able to make the call. The excess is left as a deposit in the entrypoint + * for future calls. Can be withdrawn anytime using "entryPoint.withdrawTo()". + * In case there is a paymaster in the request (or the current deposit is high + * enough), this value will be zero. + * @return validationData - Packaged ValidationData structure. use `_packValidationData` and + * `_unpackValidationData` to encode and decode. + * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, + * otherwise, an address of an "authorizer" contract. + * <6-byte> validUntil - Last timestamp this operation is valid. 0 for "indefinite" + * <6-byte> validAfter - First timestamp this operation is valid + * If an account doesn't use time-range, it is enough to + * return SIG_VALIDATION_FAILED value (1) for signature failure. + * Note that the validation code cannot use block.timestamp (or block.number) directly. + */ + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData); +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol new file mode 100644 index 00000000000..4433c80ce3e --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +import "./PackedUserOperation.sol"; + +interface IAccountExecute { + /** + * Account may implement this execute method. + * passing this methodSig at the beginning of callData will cause the entryPoint to pass the full UserOp (and hash) + * to the account. + * The account should skip the methodSig, and use the callData (and optionally, other UserOp fields) + * + * @param userOp - The operation that was just validated. + * @param userOpHash - Hash of the user's request data. + */ + function executeUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol new file mode 100644 index 00000000000..070d8f27a2c --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +import "./PackedUserOperation.sol"; + +/** + * Aggregated Signatures validator. + */ +interface IAggregator { + /** + * Validate aggregated signature. + * Revert if the aggregated signature does not match the given list of operations. + * @param userOps - Array of UserOperations to validate the signature for. + * @param signature - The aggregated signature. + */ + function validateSignatures( + PackedUserOperation[] calldata userOps, + bytes calldata signature + ) external view; + + /** + * Validate signature of a single userOp. + * This method should be called by bundler after EntryPointSimulation.simulateValidation() returns + * the aggregator this account uses. + * First it validates the signature over the userOp. Then it returns data to be used when creating the handleOps. + * @param userOp - The userOperation received from the user. + * @return sigForUserOp - The value to put into the signature field of the userOp when calling handleOps. + * (usually empty, unless account and aggregator support some kind of "multisig". + */ + function validateUserOpSignature( + PackedUserOperation calldata userOp + ) external view returns (bytes memory sigForUserOp); + + /** + * Aggregate multiple signatures into a single value. + * This method is called off-chain to calculate the signature to pass with handleOps() + * bundler MAY use optimized custom code perform this aggregation. + * @param userOps - Array of UserOperations to collect the signatures from. + * @return aggregatedSignature - The aggregated signature. + */ + function aggregateSignatures( + PackedUserOperation[] calldata userOps + ) external view returns (bytes memory aggregatedSignature); +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol new file mode 100644 index 00000000000..28c26f98e6c --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol @@ -0,0 +1,223 @@ +/** + ** Account-Abstraction (EIP-4337) singleton EntryPoint implementation. + ** Only one instance required on each chain. + **/ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +import "./PackedUserOperation.sol"; +import "./IStakeManager.sol"; +import "./IAggregator.sol"; +import "./INonceManager.sol"; + +interface IEntryPoint is IStakeManager, INonceManager { + /*** + * An event emitted after each successful request. + * @param userOpHash - Unique identifier for the request (hash its entire content, except signature). + * @param sender - The account that generates this request. + * @param paymaster - If non-null, the paymaster that pays for this request. + * @param nonce - The nonce value from the request. + * @param success - True if the sender transaction succeeded, false if reverted. + * @param actualGasCost - Actual amount paid (by account or paymaster) for this UserOperation. + * @param actualGasUsed - Total gas used by this UserOperation (including preVerification, creation, + * validation and execution). + */ + event UserOperationEvent( + bytes32 indexed userOpHash, + address indexed sender, + address indexed paymaster, + uint256 nonce, + bool success, + uint256 actualGasCost, + uint256 actualGasUsed + ); + + /** + * Account "sender" was deployed. + * @param userOpHash - The userOp that deployed this account. UserOperationEvent will follow. + * @param sender - The account that is deployed + * @param factory - The factory used to deploy this account (in the initCode) + * @param paymaster - The paymaster used by this UserOp + */ + event AccountDeployed( + bytes32 indexed userOpHash, + address indexed sender, + address factory, + address paymaster + ); + + /** + * An event emitted if the UserOperation "callData" reverted with non-zero length. + * @param userOpHash - The request unique identifier. + * @param sender - The sender of this request. + * @param nonce - The nonce used in the request. + * @param revertReason - The return bytes from the (reverted) call to "callData". + */ + event UserOperationRevertReason( + bytes32 indexed userOpHash, + address indexed sender, + uint256 nonce, + bytes revertReason + ); + + /** + * An event emitted if the UserOperation Paymaster's "postOp" call reverted with non-zero length. + * @param userOpHash - The request unique identifier. + * @param sender - The sender of this request. + * @param nonce - The nonce used in the request. + * @param revertReason - The return bytes from the (reverted) call to "callData". + */ + event PostOpRevertReason( + bytes32 indexed userOpHash, + address indexed sender, + uint256 nonce, + bytes revertReason + ); + + /** + * UserOp consumed more than prefund. The UserOperation is reverted, and no refund is made. + * @param userOpHash - The request unique identifier. + * @param sender - The sender of this request. + * @param nonce - The nonce used in the request. + */ + event UserOperationPrefundTooLow( + bytes32 indexed userOpHash, + address indexed sender, + uint256 nonce + ); + + /** + * An event emitted by handleOps(), before starting the execution loop. + * Any event emitted before this event, is part of the validation. + */ + event BeforeExecution(); + + /** + * Signature aggregator used by the following UserOperationEvents within this bundle. + * @param aggregator - The aggregator used for the following UserOperationEvents. + */ + event SignatureAggregatorChanged(address indexed aggregator); + + /** + * A custom revert error of handleOps, to identify the offending op. + * Should be caught in off-chain handleOps simulation and not happen on-chain. + * Useful for mitigating DoS attempts against batchers or for troubleshooting of factory/account/paymaster reverts. + * NOTE: If simulateValidation passes successfully, there should be no reason for handleOps to fail on it. + * @param opIndex - Index into the array of ops to the failed one (in simulateValidation, this is always zero). + * @param reason - Revert reason. The string starts with a unique code "AAmn", + * where "m" is "1" for factory, "2" for account and "3" for paymaster issues, + * so a failure can be attributed to the correct entity. + */ + error FailedOp(uint256 opIndex, string reason); + + /** + * 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 + * @dev note that inner is truncated to 2048 bytes + */ + error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); + + error PostOpReverted(bytes returnData); + + /** + * Error case when a signature aggregator fails to verify the aggregated signature it had created. + * @param aggregator The aggregator that failed to verify the signature + */ + error SignatureValidationFailed(address aggregator); + + // Return value of getSenderAddress. + error SenderAddressResult(address sender); + + // UserOps handled, per aggregator. + struct UserOpsPerAggregator { + PackedUserOperation[] userOps; + // Aggregator address + IAggregator aggregator; + // Aggregated signature + bytes signature; + } + + /** + * Execute a batch of UserOperations. + * No signature aggregator is used. + * If any account requires an aggregator (that is, it returned an aggregator when + * performing simulateValidation), then handleAggregatedOps() must be used instead. + * @param ops - The operations to execute. + * @param beneficiary - The address to receive the fees. + */ + function handleOps( + PackedUserOperation[] calldata ops, + address payable beneficiary + ) external; + + /** + * Execute a batch of UserOperation with Aggregators + * @param opsPerAggregator - The operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts). + * @param beneficiary - The address to receive the fees. + */ + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) external; + + /** + * Generate a request Id - unique identifier for this request. + * The request ID is a hash over the content of the userOp (except the signature), the entrypoint and the chainid. + * @param userOp - The user operation to generate the request ID for. + * @return hash the hash of this UserOperation + */ + function getUserOpHash( + PackedUserOperation calldata userOp + ) external view returns (bytes32); + + /** + * Gas and return values during simulation. + * @param preOpGas - The gas used for validation (including preValidationGas) + * @param prefund - The required prefund for this operation + * @param accountValidationData - returned validationData from account. + * @param paymasterValidationData - return validationData from paymaster. + * @param paymasterContext - Returned by validatePaymasterUserOp (to be passed into postOp) + */ + struct ReturnInfo { + uint256 preOpGas; + uint256 prefund; + uint256 accountValidationData; + uint256 paymasterValidationData; + bytes paymasterContext; + } + + /** + * Returned aggregated signature info: + * The aggregator returned by the account, and its current stake. + */ + struct AggregatorStakeInfo { + address aggregator; + StakeInfo stakeInfo; + } + + /** + * Get counterfactual sender address. + * Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation. + * This method always revert, and returns the address in SenderAddressResult error + * @param initCode - The constructor code to be passed into the UserOperation. + */ + function getSenderAddress(bytes memory initCode) external; + + error DelegateAndRevert(bool success, bytes ret); + + /** + * Helper method for dry-run testing. + * @dev calling this method, the EntryPoint will make a delegatecall to the given data, and report (via revert) the result. + * The method always revert, so is only useful off-chain for dry run calls, in cases where state-override to replace + * actual EntryPoint code is less convenient. + * @param target a target contract to make a delegatecall from entrypoint + * @param data data to pass to target in a delegatecall + */ + function delegateAndRevert(address target, bytes calldata data) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol b/contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol new file mode 100644 index 00000000000..2f993f6875c --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +interface INonceManager { + + /** + * Return the next nonce for this sender. + * Within a given key, the nonce values are sequenced (starting with zero, and incremented by one on each userop) + * But UserOp with different keys can come with arbitrary order. + * + * @param sender the account address + * @param key the high 192 bit of the nonce + * @return nonce a full nonce to pass for next UserOp with this sender. + */ + function getNonce(address sender, uint192 key) + external view returns (uint256 nonce); + + /** + * Manually increment the nonce of the sender. + * This method is exposed just for completeness.. + * Account does NOT need to call it, neither during validation, nor elsewhere, + * as the EntryPoint will update the nonce regardless. + * Possible use-case is call it with various keys to "initialize" their nonces to one, so that future + * UserOperations will not pay extra for the first transaction with a given key. + */ + function incrementNonce(uint192 key) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol new file mode 100644 index 00000000000..9176a0b242a --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +import "./PackedUserOperation.sol"; + +/** + * The interface exposed by a paymaster contract, who agrees to pay the gas for user's operations. + * A paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction. + */ +interface IPaymaster { + enum PostOpMode { + // User op succeeded. + opSucceeded, + // User op reverted. Still has to pay for gas. + opReverted, + // Only used internally in the EntryPoint (cleanup after postOp reverts). Never calling paymaster with this value + postOpReverted + } + + /** + * Payment validation: check if paymaster agrees to pay. + * Must verify sender is the entryPoint. + * Revert to reject this request. + * Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted). + * The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns. + * @param userOp - The user operation. + * @param userOpHash - Hash of the user's request data. + * @param maxCost - The maximum cost of this transaction (based on maximum gas and gas price from userOp). + * @return context - Value to send to a postOp. Zero length to signify postOp is not required. + * @return validationData - Signature and time-range of this operation, encoded the same as the return + * value of validateUserOperation. + * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, + * other values are invalid for paymaster. + * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" + * <6-byte> validAfter - first timestamp this operation is valid + * Note that the validation code cannot use block.timestamp (or block.number) directly. + */ + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external returns (bytes memory context, uint256 validationData); + + /** + * Post-operation handler. + * Must verify sender is the entryPoint. + * @param mode - Enum with the following options: + * opSucceeded - User operation succeeded. + * opReverted - User op reverted. The paymaster still has to pay for gas. + * postOpReverted - never passed in a call to postOp(). + * @param context - The context value returned by validatePaymasterUserOp + * @param actualGasCost - Actual gas used so far (without this postOp call). + * @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas + * and maxPriorityFee (and basefee) + * It is not the same as tx.gasprice, which is what the bundler pays. + */ + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol new file mode 100644 index 00000000000..69083e93f5a --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.5; + +/** + * Manage deposits and stakes. + * Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account). + * Stake is value locked for at least "unstakeDelay" by the staked entity. + */ +interface IStakeManager { + event Deposited(address indexed account, uint256 totalDeposit); + + event Withdrawn( + address indexed account, + address withdrawAddress, + uint256 amount + ); + + // Emitted when stake or unstake delay are modified. + event StakeLocked( + address indexed account, + uint256 totalStaked, + uint256 unstakeDelaySec + ); + + // Emitted once a stake is scheduled for withdrawal. + event StakeUnlocked(address indexed account, uint256 withdrawTime); + + event StakeWithdrawn( + address indexed account, + address withdrawAddress, + uint256 amount + ); + + /** + * @param deposit - The entity's deposit. + * @param staked - True if this entity is staked. + * @param stake - Actual amount of ether staked for this entity. + * @param unstakeDelaySec - Minimum delay to withdraw the stake. + * @param withdrawTime - First block timestamp where 'withdrawStake' will be callable, or zero if already locked. + * @dev Sizes were chosen so that deposit fits into one cell (used during handleOp) + * and the rest fit into a 2nd cell (used during stake/unstake) + * - 112 bit allows for 10^15 eth + * - 48 bit for full timestamp + * - 32 bit allows 150 years for unstake delay + */ + struct DepositInfo { + uint256 deposit; + bool staked; + uint112 stake; + uint32 unstakeDelaySec; + uint48 withdrawTime; + } + + // API struct used by getStakeInfo and simulateValidation. + struct StakeInfo { + uint256 stake; + uint256 unstakeDelaySec; + } + + /** + * Get deposit info. + * @param account - The account to query. + * @return info - Full deposit information of given account. + */ + function getDepositInfo( + address account + ) external view returns (DepositInfo memory info); + + /** + * Get account balance. + * @param account - The account to query. + * @return - The deposit (for gas payment) of the account. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * Add to the deposit of the given account. + * @param account - The account to add to. + */ + function depositTo(address account) external payable; + + /** + * Add to the account's stake - amount and delay + * any pending unstake is first cancelled. + * @param _unstakeDelaySec - The new lock duration before the deposit can be withdrawn. + */ + function addStake(uint32 _unstakeDelaySec) external payable; + + /** + * Attempt to unlock the stake. + * The value can be withdrawn (using withdrawStake) after the unstake delay. + */ + function unlockStake() external; + + /** + * Withdraw from the (unlocked) stake. + * Must first call unlockStake and wait for the unstakeDelay to pass. + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external; + + /** + * Withdraw from the deposit. + * @param withdrawAddress - The address to send withdrawn value. + * @param withdrawAmount - The amount to withdraw. + */ + function withdrawTo( + address payable withdrawAddress, + uint256 withdrawAmount + ) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol b/contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol new file mode 100644 index 00000000000..fe20de56573 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +/** + * User Operation struct + * @param sender - The sender account of this request. + * @param nonce - Unique value the sender uses to verify it is not a replay. + * @param initCode - If set, the account contract will be created by this constructor/ + * @param callData - The method call to execute on this account. + * @param accountGasLimits - Packed gas limits for validateUserOp and gas limit passed to the callData method call. + * @param preVerificationGas - Gas not calculated by the handleOps method, but added to the gas paid. + * Covers batch overhead. + * @param gasFees - packed gas fields maxPriorityFeePerGas and maxFeePerGas - Same as EIP-1559 gas parameters. + * @param paymasterAndData - If set, this field holds the paymaster address, verification gas limit, postOp gas limit and paymaster-specific extra data + * The paymaster will pay for the transaction instead of the sender. + * @param signature - Sender-verified signature over the entire request, the EntryPoint address and the chain ID. + */ +struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; + bytes callData; + bytes32 accountGasLimits; + uint256 preVerificationGas; + bytes32 gasFees; + bytes paymasterAndData; + bytes signature; +} diff --git a/contracts/vendor/erc4337-entrypoint/utils/Exec.sol b/contracts/vendor/erc4337-entrypoint/utils/Exec.sol new file mode 100644 index 00000000000..ee8d71ac0d8 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/utils/Exec.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.23; + +// solhint-disable no-inline-assembly + +/** + * Utility functions helpful when making different kinds of contract calls in Solidity. + */ +library Exec { + + function call( + address to, + uint256 value, + bytes memory data, + uint256 txGas + ) internal returns (bool success) { + assembly ("memory-safe") { + success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) + } + } + + function staticcall( + address to, + bytes memory data, + uint256 txGas + ) internal view returns (bool success) { + assembly ("memory-safe") { + success := staticcall(txGas, to, add(data, 0x20), mload(data), 0, 0) + } + } + + function delegateCall( + address to, + bytes memory data, + uint256 txGas + ) internal returns (bool success) { + assembly ("memory-safe") { + success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) + } + } + + // get returned data from last call or calldelegate + function getReturnData(uint256 maxLen) internal pure returns (bytes memory returnData) { + assembly ("memory-safe") { + let len := returndatasize() + if gt(len, maxLen) { + len := maxLen + } + let ptr := mload(0x40) + mstore(0x40, add(ptr, add(len, 0x20))) + mstore(ptr, len) + returndatacopy(add(ptr, 0x20), 0, len) + returnData := ptr + } + } + + // revert with explicit byte array (probably reverted info from call) + function revertWithData(bytes memory returnData) internal pure { + assembly ("memory-safe") { + revert(add(returnData, 32), mload(returnData)) + } + } + + function callAndRevert(address to, bytes memory data, uint256 maxLen) internal { + bool success = call(to,0,data,gasleft()); + if (!success) { + revertWithData(getReturnData(maxLen)); + } + } +} diff --git a/scripts/generate/templates/Packing.opts.js b/scripts/generate/templates/Packing.opts.js index de9ab77ff53..893ad6297cf 100644 --- a/scripts/generate/templates/Packing.opts.js +++ b/scripts/generate/templates/Packing.opts.js @@ -1,3 +1,3 @@ module.exports = { - SIZES: [1, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32], + SIZES: [1, 2, 4, 6, 8, 10, 12, 16, 20, 22, 24, 28, 32], }; diff --git a/test/utils/Packing.t.sol b/test/utils/Packing.t.sol index 9531f1bffbb..e8adda48920 100644 --- a/test/utils/Packing.t.sol +++ b/test/utils/Packing.t.sol @@ -29,6 +29,26 @@ contract PackingTest is Test { assertEq(right, Packing.pack_2_6(left, right).extract_8_6(2)); } + function testPack(bytes2 left, bytes8 right) external { + assertEq(left, Packing.pack_2_8(left, right).extract_10_2(0)); + assertEq(right, Packing.pack_2_8(left, right).extract_10_8(2)); + } + + function testPack(bytes2 left, bytes10 right) external { + assertEq(left, Packing.pack_2_10(left, right).extract_12_2(0)); + assertEq(right, Packing.pack_2_10(left, right).extract_12_10(2)); + } + + function testPack(bytes2 left, bytes20 right) external { + assertEq(left, Packing.pack_2_20(left, right).extract_22_2(0)); + assertEq(right, Packing.pack_2_20(left, right).extract_22_20(2)); + } + + function testPack(bytes2 left, bytes22 right) external { + assertEq(left, Packing.pack_2_22(left, right).extract_24_2(0)); + assertEq(right, Packing.pack_2_22(left, right).extract_24_22(2)); + } + function testPack(bytes4 left, bytes2 right) external { assertEq(left, Packing.pack_4_2(left, right).extract_6_4(0)); assertEq(right, Packing.pack_4_2(left, right).extract_6_2(4)); @@ -39,6 +59,11 @@ contract PackingTest is Test { assertEq(right, Packing.pack_4_4(left, right).extract_8_4(4)); } + function testPack(bytes4 left, bytes6 right) external { + assertEq(left, Packing.pack_4_6(left, right).extract_10_4(0)); + assertEq(right, Packing.pack_4_6(left, right).extract_10_6(4)); + } + function testPack(bytes4 left, bytes8 right) external { assertEq(left, Packing.pack_4_8(left, right).extract_12_4(0)); assertEq(right, Packing.pack_4_8(left, right).extract_12_8(4)); @@ -74,11 +99,36 @@ contract PackingTest is Test { assertEq(right, Packing.pack_6_2(left, right).extract_8_2(6)); } + function testPack(bytes6 left, bytes4 right) external { + assertEq(left, Packing.pack_6_4(left, right).extract_10_6(0)); + assertEq(right, Packing.pack_6_4(left, right).extract_10_4(6)); + } + function testPack(bytes6 left, bytes6 right) external { assertEq(left, Packing.pack_6_6(left, right).extract_12_6(0)); assertEq(right, Packing.pack_6_6(left, right).extract_12_6(6)); } + function testPack(bytes6 left, bytes10 right) external { + assertEq(left, Packing.pack_6_10(left, right).extract_16_6(0)); + assertEq(right, Packing.pack_6_10(left, right).extract_16_10(6)); + } + + function testPack(bytes6 left, bytes16 right) external { + assertEq(left, Packing.pack_6_16(left, right).extract_22_6(0)); + assertEq(right, Packing.pack_6_16(left, right).extract_22_16(6)); + } + + function testPack(bytes6 left, bytes22 right) external { + assertEq(left, Packing.pack_6_22(left, right).extract_28_6(0)); + assertEq(right, Packing.pack_6_22(left, right).extract_28_22(6)); + } + + function testPack(bytes8 left, bytes2 right) external { + assertEq(left, Packing.pack_8_2(left, right).extract_10_8(0)); + assertEq(right, Packing.pack_8_2(left, right).extract_10_2(8)); + } + function testPack(bytes8 left, bytes4 right) external { assertEq(left, Packing.pack_8_4(left, right).extract_12_8(0)); assertEq(right, Packing.pack_8_4(left, right).extract_12_4(8)); @@ -109,6 +159,31 @@ contract PackingTest is Test { assertEq(right, Packing.pack_8_24(left, right).extract_32_24(8)); } + function testPack(bytes10 left, bytes2 right) external { + assertEq(left, Packing.pack_10_2(left, right).extract_12_10(0)); + assertEq(right, Packing.pack_10_2(left, right).extract_12_2(10)); + } + + function testPack(bytes10 left, bytes6 right) external { + assertEq(left, Packing.pack_10_6(left, right).extract_16_10(0)); + assertEq(right, Packing.pack_10_6(left, right).extract_16_6(10)); + } + + function testPack(bytes10 left, bytes10 right) external { + assertEq(left, Packing.pack_10_10(left, right).extract_20_10(0)); + assertEq(right, Packing.pack_10_10(left, right).extract_20_10(10)); + } + + function testPack(bytes10 left, bytes12 right) external { + assertEq(left, Packing.pack_10_12(left, right).extract_22_10(0)); + assertEq(right, Packing.pack_10_12(left, right).extract_22_12(10)); + } + + function testPack(bytes10 left, bytes22 right) external { + assertEq(left, Packing.pack_10_22(left, right).extract_32_10(0)); + assertEq(right, Packing.pack_10_22(left, right).extract_32_22(10)); + } + function testPack(bytes12 left, bytes4 right) external { assertEq(left, Packing.pack_12_4(left, right).extract_16_12(0)); assertEq(right, Packing.pack_12_4(left, right).extract_16_4(12)); @@ -119,6 +194,11 @@ contract PackingTest is Test { assertEq(right, Packing.pack_12_8(left, right).extract_20_8(12)); } + function testPack(bytes12 left, bytes10 right) external { + assertEq(left, Packing.pack_12_10(left, right).extract_22_12(0)); + assertEq(right, Packing.pack_12_10(left, right).extract_22_10(12)); + } + function testPack(bytes12 left, bytes12 right) external { assertEq(left, Packing.pack_12_12(left, right).extract_24_12(0)); assertEq(right, Packing.pack_12_12(left, right).extract_24_12(12)); @@ -139,6 +219,11 @@ contract PackingTest is Test { assertEq(right, Packing.pack_16_4(left, right).extract_20_4(16)); } + function testPack(bytes16 left, bytes6 right) external { + assertEq(left, Packing.pack_16_6(left, right).extract_22_16(0)); + assertEq(right, Packing.pack_16_6(left, right).extract_22_6(16)); + } + function testPack(bytes16 left, bytes8 right) external { assertEq(left, Packing.pack_16_8(left, right).extract_24_16(0)); assertEq(right, Packing.pack_16_8(left, right).extract_24_8(16)); @@ -154,6 +239,11 @@ contract PackingTest is Test { assertEq(right, Packing.pack_16_16(left, right).extract_32_16(16)); } + function testPack(bytes20 left, bytes2 right) external { + assertEq(left, Packing.pack_20_2(left, right).extract_22_20(0)); + assertEq(right, Packing.pack_20_2(left, right).extract_22_2(20)); + } + function testPack(bytes20 left, bytes4 right) external { assertEq(left, Packing.pack_20_4(left, right).extract_24_20(0)); assertEq(right, Packing.pack_20_4(left, right).extract_24_4(20)); @@ -169,6 +259,21 @@ contract PackingTest is Test { assertEq(right, Packing.pack_20_12(left, right).extract_32_12(20)); } + function testPack(bytes22 left, bytes2 right) external { + assertEq(left, Packing.pack_22_2(left, right).extract_24_22(0)); + assertEq(right, Packing.pack_22_2(left, right).extract_24_2(22)); + } + + function testPack(bytes22 left, bytes6 right) external { + assertEq(left, Packing.pack_22_6(left, right).extract_28_22(0)); + assertEq(right, Packing.pack_22_6(left, right).extract_28_6(22)); + } + + function testPack(bytes22 left, bytes10 right) external { + assertEq(left, Packing.pack_22_10(left, right).extract_32_22(0)); + assertEq(right, Packing.pack_22_10(left, right).extract_32_10(22)); + } + function testPack(bytes24 left, bytes4 right) external { assertEq(left, Packing.pack_24_4(left, right).extract_28_24(0)); assertEq(right, Packing.pack_24_4(left, right).extract_28_4(24)); @@ -274,6 +379,51 @@ contract PackingTest is Test { assertEq(container, container.replace_8_6(newValue, offset).replace_8_6(oldValue, offset)); } + function testReplace(bytes10 container, bytes1 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 9)); + + bytes1 oldValue = container.extract_10_1(offset); + + assertEq(newValue, container.replace_10_1(newValue, offset).extract_10_1(offset)); + assertEq(container, container.replace_10_1(newValue, offset).replace_10_1(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes2 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 8)); + + bytes2 oldValue = container.extract_10_2(offset); + + assertEq(newValue, container.replace_10_2(newValue, offset).extract_10_2(offset)); + assertEq(container, container.replace_10_2(newValue, offset).replace_10_2(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes4 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 6)); + + bytes4 oldValue = container.extract_10_4(offset); + + assertEq(newValue, container.replace_10_4(newValue, offset).extract_10_4(offset)); + assertEq(container, container.replace_10_4(newValue, offset).replace_10_4(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes6 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 4)); + + bytes6 oldValue = container.extract_10_6(offset); + + assertEq(newValue, container.replace_10_6(newValue, offset).extract_10_6(offset)); + assertEq(container, container.replace_10_6(newValue, offset).replace_10_6(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes8 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 2)); + + bytes8 oldValue = container.extract_10_8(offset); + + assertEq(newValue, container.replace_10_8(newValue, offset).extract_10_8(offset)); + assertEq(container, container.replace_10_8(newValue, offset).replace_10_8(oldValue, offset)); + } + function testReplace(bytes12 container, bytes1 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 11)); @@ -319,6 +469,15 @@ contract PackingTest is Test { assertEq(container, container.replace_12_8(newValue, offset).replace_12_8(oldValue, offset)); } + function testReplace(bytes12 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 2)); + + bytes10 oldValue = container.extract_12_10(offset); + + assertEq(newValue, container.replace_12_10(newValue, offset).extract_12_10(offset)); + assertEq(container, container.replace_12_10(newValue, offset).replace_12_10(oldValue, offset)); + } + function testReplace(bytes16 container, bytes1 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 15)); @@ -364,6 +523,15 @@ contract PackingTest is Test { assertEq(container, container.replace_16_8(newValue, offset).replace_16_8(oldValue, offset)); } + function testReplace(bytes16 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 6)); + + bytes10 oldValue = container.extract_16_10(offset); + + assertEq(newValue, container.replace_16_10(newValue, offset).extract_16_10(offset)); + assertEq(container, container.replace_16_10(newValue, offset).replace_16_10(oldValue, offset)); + } + function testReplace(bytes16 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 4)); @@ -418,6 +586,15 @@ contract PackingTest is Test { assertEq(container, container.replace_20_8(newValue, offset).replace_20_8(oldValue, offset)); } + function testReplace(bytes20 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 10)); + + bytes10 oldValue = container.extract_20_10(offset); + + assertEq(newValue, container.replace_20_10(newValue, offset).extract_20_10(offset)); + assertEq(container, container.replace_20_10(newValue, offset).replace_20_10(oldValue, offset)); + } + function testReplace(bytes20 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 8)); @@ -436,6 +613,87 @@ contract PackingTest is Test { assertEq(container, container.replace_20_16(newValue, offset).replace_20_16(oldValue, offset)); } + function testReplace(bytes22 container, bytes1 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 21)); + + bytes1 oldValue = container.extract_22_1(offset); + + assertEq(newValue, container.replace_22_1(newValue, offset).extract_22_1(offset)); + assertEq(container, container.replace_22_1(newValue, offset).replace_22_1(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes2 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 20)); + + bytes2 oldValue = container.extract_22_2(offset); + + assertEq(newValue, container.replace_22_2(newValue, offset).extract_22_2(offset)); + assertEq(container, container.replace_22_2(newValue, offset).replace_22_2(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes4 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 18)); + + bytes4 oldValue = container.extract_22_4(offset); + + assertEq(newValue, container.replace_22_4(newValue, offset).extract_22_4(offset)); + assertEq(container, container.replace_22_4(newValue, offset).replace_22_4(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes6 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 16)); + + bytes6 oldValue = container.extract_22_6(offset); + + assertEq(newValue, container.replace_22_6(newValue, offset).extract_22_6(offset)); + assertEq(container, container.replace_22_6(newValue, offset).replace_22_6(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes8 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 14)); + + bytes8 oldValue = container.extract_22_8(offset); + + assertEq(newValue, container.replace_22_8(newValue, offset).extract_22_8(offset)); + assertEq(container, container.replace_22_8(newValue, offset).replace_22_8(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 12)); + + bytes10 oldValue = container.extract_22_10(offset); + + assertEq(newValue, container.replace_22_10(newValue, offset).extract_22_10(offset)); + assertEq(container, container.replace_22_10(newValue, offset).replace_22_10(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes12 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 10)); + + bytes12 oldValue = container.extract_22_12(offset); + + assertEq(newValue, container.replace_22_12(newValue, offset).extract_22_12(offset)); + assertEq(container, container.replace_22_12(newValue, offset).replace_22_12(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes16 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 6)); + + bytes16 oldValue = container.extract_22_16(offset); + + assertEq(newValue, container.replace_22_16(newValue, offset).extract_22_16(offset)); + assertEq(container, container.replace_22_16(newValue, offset).replace_22_16(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes20 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 2)); + + bytes20 oldValue = container.extract_22_20(offset); + + assertEq(newValue, container.replace_22_20(newValue, offset).extract_22_20(offset)); + assertEq(container, container.replace_22_20(newValue, offset).replace_22_20(oldValue, offset)); + } + function testReplace(bytes24 container, bytes1 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 23)); @@ -481,6 +739,15 @@ contract PackingTest is Test { assertEq(container, container.replace_24_8(newValue, offset).replace_24_8(oldValue, offset)); } + function testReplace(bytes24 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 14)); + + bytes10 oldValue = container.extract_24_10(offset); + + assertEq(newValue, container.replace_24_10(newValue, offset).extract_24_10(offset)); + assertEq(container, container.replace_24_10(newValue, offset).replace_24_10(oldValue, offset)); + } + function testReplace(bytes24 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 12)); @@ -508,6 +775,15 @@ contract PackingTest is Test { assertEq(container, container.replace_24_20(newValue, offset).replace_24_20(oldValue, offset)); } + function testReplace(bytes24 container, bytes22 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 2)); + + bytes22 oldValue = container.extract_24_22(offset); + + assertEq(newValue, container.replace_24_22(newValue, offset).extract_24_22(offset)); + assertEq(container, container.replace_24_22(newValue, offset).replace_24_22(oldValue, offset)); + } + function testReplace(bytes28 container, bytes1 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 27)); @@ -553,6 +829,15 @@ contract PackingTest is Test { assertEq(container, container.replace_28_8(newValue, offset).replace_28_8(oldValue, offset)); } + function testReplace(bytes28 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 18)); + + bytes10 oldValue = container.extract_28_10(offset); + + assertEq(newValue, container.replace_28_10(newValue, offset).extract_28_10(offset)); + assertEq(container, container.replace_28_10(newValue, offset).replace_28_10(oldValue, offset)); + } + function testReplace(bytes28 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 16)); @@ -580,6 +865,15 @@ contract PackingTest is Test { assertEq(container, container.replace_28_20(newValue, offset).replace_28_20(oldValue, offset)); } + function testReplace(bytes28 container, bytes22 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 6)); + + bytes22 oldValue = container.extract_28_22(offset); + + assertEq(newValue, container.replace_28_22(newValue, offset).extract_28_22(offset)); + assertEq(container, container.replace_28_22(newValue, offset).replace_28_22(oldValue, offset)); + } + function testReplace(bytes28 container, bytes24 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 4)); @@ -634,6 +928,15 @@ contract PackingTest is Test { assertEq(container, container.replace_32_8(newValue, offset).replace_32_8(oldValue, offset)); } + function testReplace(bytes32 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 22)); + + bytes10 oldValue = container.extract_32_10(offset); + + assertEq(newValue, container.replace_32_10(newValue, offset).extract_32_10(offset)); + assertEq(container, container.replace_32_10(newValue, offset).replace_32_10(oldValue, offset)); + } + function testReplace(bytes32 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 20)); @@ -661,6 +964,15 @@ contract PackingTest is Test { assertEq(container, container.replace_32_20(newValue, offset).replace_32_20(oldValue, offset)); } + function testReplace(bytes32 container, bytes22 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 10)); + + bytes22 oldValue = container.extract_32_22(offset); + + assertEq(newValue, container.replace_32_22(newValue, offset).extract_32_22(offset)); + assertEq(container, container.replace_32_22(newValue, offset).replace_32_22(oldValue, offset)); + } + function testReplace(bytes32 container, bytes24 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 8)); From 248078d51cef1ccdbd11deec27934216cdaa1d56 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Sep 2024 12:17:52 +0200 Subject: [PATCH 02/13] skip linting on vendored contracts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f7c0f519737..a82ea47dd08 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", "lint:js": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint --ignore-path .gitignore .", "lint:js:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint --ignore-path .gitignore . --fix", - "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", - "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", + "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/!(vendor)/**/*.sol' --check && solhint '{contracts,test}/!(vendor)/**/*.sol'", + "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/!(vendor)/**/*.sol' '!contracts/vendor/**/*.sol' --write", "clean": "hardhat clean && rimraf build contracts/build", "prepack": "scripts/prepack.sh", "generate": "scripts/generate/run.js", From 1217f3e5cced6c70bb8bcb03d441d876a9c00bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 2 Oct 2024 11:58:46 -0600 Subject: [PATCH 03/13] Enable checks on account-abstraction branch --- .github/workflows/checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9d338bb642f..2f1d5aa687e 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -6,6 +6,7 @@ on: - master - next-v* - release-v* + - account-abstraction pull_request: {} workflow_dispatch: {} From 518fd94dc6d034780a273694472bbfc2908f4aa4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 10 Oct 2024 20:24:33 +0200 Subject: [PATCH 04/13] Fix account abstraction upgradeable testing (#5248) --- .github/workflows/checks.yml | 2 +- contracts/abstraction/utils/ERC4337Utils.sol | 4 ++-- contracts/abstraction/utils/ERC7579Utils.sol | 11 ++++++----- contracts/mocks/Stateless.sol | 2 ++ contracts/package.json | 3 ++- package.json | 3 ++- scripts/upgradeable/patch-apply.sh | 2 +- scripts/upgradeable/patch-save.sh | 2 +- scripts/upgradeable/transpile.sh | 10 +++++++++- scripts/upgradeable/upgradeable.excluded.patch | 17 +++++++++++++++++ slither.config.json | 2 +- 11 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 scripts/upgradeable/upgradeable.excluded.patch diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 2f1d5aa687e..6a78eda4d9f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -130,4 +130,4 @@ jobs: with: check_hidden: true check_filenames: true - skip: package-lock.json,*.pdf + skip: package-lock.json,*.pdf,vendor diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol index 3d95db10815..f3f049d14cc 100644 --- a/contracts/abstraction/utils/ERC4337Utils.sol +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -62,8 +62,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); } } diff --git a/contracts/abstraction/utils/ERC7579Utils.sol b/contracts/abstraction/utils/ERC7579Utils.sol index b35e157101b..4a014d3e3e5 100644 --- a/contracts/abstraction/utils/ERC7579Utils.sol +++ b/contracts/abstraction/utils/ERC7579Utils.sol @@ -11,14 +11,15 @@ type ExecType is bytes1; type ModeSelector is bytes4; type ModePayload is bytes22; +// slither-disable-next-line unused-state 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); + CallType internal constant CALLTYPE_SINGLE = CallType.wrap(0x00); + CallType internal constant CALLTYPE_BATCH = CallType.wrap(0x01); + CallType internal constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); + ExecType internal constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); + ExecType internal constant EXECTYPE_TRY = ExecType.wrap(0x01); function encodeMode( CallType callType, diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 846c77d98e8..e0193a91442 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -22,6 +22,8 @@ import {ERC165} from "../utils/introspection/ERC165.sol"; import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol"; import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol"; import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; +import {ERC4337Utils} from "../abstraction/utils/ERC4337Utils.sol"; +import {ERC7579Utils} from "../abstraction/utils/ERC7579Utils.sol"; import {Heap} from "../utils/structs/Heap.sol"; import {Math} from "../utils/math/Math.sol"; import {MerkleProof} from "../utils/cryptography/MerkleProof.sol"; diff --git a/contracts/package.json b/contracts/package.json index 845e8c4035d..2232d67ca52 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -5,7 +5,8 @@ "files": [ "**/*.sol", "/build/contracts/*.json", - "!/mocks/**/*" + "!/mocks/**/*", + "!/vendor/erc4337-entrypoint/**/*" ], "scripts": { "prepack": "bash ../scripts/prepack.sh", diff --git a/package.json b/package.json index a82ea47dd08..9075a50ad22 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "private": true, "files": [ "/contracts/**/*.sol", - "!/contracts/mocks/**/*" + "!/contracts/mocks/**/*", + "!/contracts/vendor/erc4337-entrypoint" ], "scripts": { "compile": "hardhat compile", diff --git a/scripts/upgradeable/patch-apply.sh b/scripts/upgradeable/patch-apply.sh index d9e17589b05..e16cf76a4c2 100755 --- a/scripts/upgradeable/patch-apply.sh +++ b/scripts/upgradeable/patch-apply.sh @@ -3,7 +3,7 @@ set -euo pipefail DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" -PATCH="$DIRNAME/upgradeable.patch" +PATCH="$DIRNAME/${1:-upgradeable.patch}" error() { echo Error: "$*" >&2 diff --git a/scripts/upgradeable/patch-save.sh b/scripts/upgradeable/patch-save.sh index 111e6f1572a..de0cec6a5ef 100755 --- a/scripts/upgradeable/patch-save.sh +++ b/scripts/upgradeable/patch-save.sh @@ -3,7 +3,7 @@ set -euo pipefail DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" -PATCH="$DIRNAME/upgradeable.patch" +PATCH="$DIRNAME/${1:-upgradeable.patch}" error() { echo Error: "$*" >&2 diff --git a/scripts/upgradeable/transpile.sh b/scripts/upgradeable/transpile.sh index f7c848c1320..d31172ddf4e 100644 --- a/scripts/upgradeable/transpile.sh +++ b/scripts/upgradeable/transpile.sh @@ -5,13 +5,16 @@ set -euo pipefail -x VERSION="$(jq -r .version contracts/package.json)" DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" +# Apply patch to contracts that are transpiled bash "$DIRNAME/patch-apply.sh" sed -i'' -e "s//$VERSION/g" "contracts/package.json" git add contracts/package.json +# Build artifacts npm run clean npm run compile +# Check artifacts are correctly built build_info=($(jq -r '.input.sources | keys | if any(test("^contracts/mocks/.*\\bunreachable\\b")) then empty else input_filename end' artifacts/build-info/*)) build_info_num=${#build_info[@]} @@ -20,10 +23,13 @@ if [ $build_info_num -ne 1 ]; then exit 1 fi +# Apply changes to the excluded contracts (these don't need to in the artifact and may prevent compilation) +git apply -3 "$DIRNAME/upgradeable.excluded.patch" + # -D: delete original and excluded files # -b: use this build info file # -i: use included Initializable -# -x: exclude proxy-related contracts with a few exceptions +# -x: exclude vendored and proxy-related contracts with a few exceptions # -p: emit public initializer # -n: use namespaces # -N: exclude from namespaces transformation @@ -38,6 +44,8 @@ npx @openzeppelin/upgrade-safe-transpiler -D \ -x '!contracts/proxy/ERC1967/ERC1967Utils.sol' \ -x '!contracts/proxy/utils/UUPSUpgradeable.sol' \ -x '!contracts/proxy/beacon/IBeacon.sol' \ + -x 'contracts/vendor/**/*' \ + -x '!contracts/vendor/compound/ICompoundTimelock.sol' \ -p 'contracts/access/manager/AccessManager.sol' \ -p 'contracts/finance/VestingWallet.sol' \ -p 'contracts/governance/TimelockController.sol' \ diff --git a/scripts/upgradeable/upgradeable.excluded.patch b/scripts/upgradeable/upgradeable.excluded.patch new file mode 100644 index 00000000000..7c49e21e123 --- /dev/null +++ b/scripts/upgradeable/upgradeable.excluded.patch @@ -0,0 +1,17 @@ +diff --git a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol +index 778115b1..44501524 100644 +--- a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol ++++ b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol +@@ -15,10 +15,8 @@ import "./Helpers.sol"; + import "./NonceManager.sol"; + import "./UserOperationLib.sol"; + +-// import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +-// import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +-import "../../../utils/introspection/ERC165.sol"; // OZ edit +-import "../../../utils/ReentrancyGuard.sol"; // OZ edit ++import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; ++import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + + /* + * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. diff --git a/slither.config.json b/slither.config.json index 069da1f3a21..fa52f4dd1dd 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,5 +1,5 @@ { "detectors_to_run": "arbitrary-send-erc20,array-by-reference,incorrect-shift,name-reused,rtlo,suicidal,uninitialized-state,uninitialized-storage,arbitrary-send-erc20-permit,controlled-array-length,controlled-delegatecall,delegatecall-loop,msg-value-loop,reentrancy-eth,unchecked-transfer,weak-prng,domain-separator-collision,erc20-interface,erc721-interface,locked-ether,mapping-deletion,shadowing-abstract,tautology,write-after-write,boolean-cst,reentrancy-no-eth,reused-constructor,tx-origin,unchecked-lowlevel,unchecked-send,variable-scope,void-cst,events-access,events-maths,incorrect-unary,boolean-equal,cyclomatic-complexity,deprecated-standards,erc20-indexed,function-init-state,pragma,unused-state,reentrancy-unlimited-gas,constable-states,immutable-states,var-read-using-this", - "filter_paths": "contracts/mocks,contracts-exposed", + "filter_paths": "contracts/mocks,contracts/vendor,contracts-exposed", "compile_force_framework": "hardhat" } From 2aa4828b68470b6a2eca4d94c90990d9f14483de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 10 Oct 2024 18:12:37 -0600 Subject: [PATCH 05/13] Add Account Abstraction utils and interfaces (#5242) Co-authored-by: Hadrien Croubois --- .changeset/hot-shrimps-wait.md | 5 + .changeset/small-seahorses-bathe.md | 5 + .changeset/weak-roses-bathe.md | 5 + .codecov.yml | 1 + contracts/abstraction/utils/ERC7579Utils.sol | 116 ------ contracts/account/README.adoc | 12 + .../utils/draft-ERC4337Utils.sol} | 110 ++---- .../account/utils/draft-ERC7579Utils.sol | 242 ++++++++++++ contracts/interfaces/IERC4337.sol | 118 ------ contracts/interfaces/IERC7579Module.sol | 89 ----- contracts/interfaces/draft-IERC4337.sol | 215 +++++++++++ ...IERC7579Account.sol => draft-IERC7579.sol} | 88 ++++- contracts/mocks/Stateless.sol | 4 +- .../mocks/account/utils/ERC7579UtilsMock.sol | 23 ++ contracts/vendor/erc4337-entrypoint/README.md | 1 + scripts/generate/templates/Packing.t.js | 4 +- test/account/utils/draft-ERC4337Utils.test.js | 207 +++++++++++ test/account/utils/draft-ERC7579Utils.test.js | 346 ++++++++++++++++++ test/helpers/erc4337.js | 82 +++++ test/helpers/erc7579.js | 58 +++ test/utils/Packing.t.sol | 268 +++++++------- 21 files changed, 1458 insertions(+), 541 deletions(-) create mode 100644 .changeset/hot-shrimps-wait.md create mode 100644 .changeset/small-seahorses-bathe.md create mode 100644 .changeset/weak-roses-bathe.md delete mode 100644 contracts/abstraction/utils/ERC7579Utils.sol create mode 100644 contracts/account/README.adoc rename contracts/{abstraction/utils/ERC4337Utils.sol => account/utils/draft-ERC4337Utils.sol} (61%) create mode 100644 contracts/account/utils/draft-ERC7579Utils.sol delete mode 100644 contracts/interfaces/IERC4337.sol delete mode 100644 contracts/interfaces/IERC7579Module.sol create mode 100644 contracts/interfaces/draft-IERC4337.sol rename contracts/interfaces/{IERC7579Account.sol => draft-IERC7579.sol} (58%) create mode 100644 contracts/mocks/account/utils/ERC7579UtilsMock.sol create mode 100644 contracts/vendor/erc4337-entrypoint/README.md create mode 100644 test/account/utils/draft-ERC4337Utils.test.js create mode 100644 test/account/utils/draft-ERC7579Utils.test.js create mode 100644 test/helpers/erc4337.js create mode 100644 test/helpers/erc7579.js diff --git a/.changeset/hot-shrimps-wait.md b/.changeset/hot-shrimps-wait.md new file mode 100644 index 00000000000..e4e96a981ad --- /dev/null +++ b/.changeset/hot-shrimps-wait.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Packing`: Add variants for packing `bytes10` and `bytes22` diff --git a/.changeset/small-seahorses-bathe.md b/.changeset/small-seahorses-bathe.md new file mode 100644 index 00000000000..7b5ec794f38 --- /dev/null +++ b/.changeset/small-seahorses-bathe.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC7579Utils`: Add a reusable library to interact with ERC-7579 modular accounts diff --git a/.changeset/weak-roses-bathe.md b/.changeset/weak-roses-bathe.md new file mode 100644 index 00000000000..416b2e746d3 --- /dev/null +++ b/.changeset/weak-roses-bathe.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC4337Utils`: Add a reusable library to manipulate user operations and interact with ERC-4337 contracts diff --git a/.codecov.yml b/.codecov.yml index 5bee9146ab9..4cec4ef7d5f 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -13,3 +13,4 @@ coverage: ignore: - "test" - "contracts/mocks" + - "contracts/vendor" diff --git a/contracts/abstraction/utils/ERC7579Utils.sol b/contracts/abstraction/utils/ERC7579Utils.sol deleted file mode 100644 index 4a014d3e3e5..00000000000 --- a/contracts/abstraction/utils/ERC7579Utils.sol +++ /dev/null @@ -1,116 +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; - -// slither-disable-next-line unused-state -library ERC7579Utils { - using Packing for *; - - CallType internal constant CALLTYPE_SINGLE = CallType.wrap(0x00); - CallType internal constant CALLTYPE_BATCH = CallType.wrap(0x01); - CallType internal constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); - ExecType internal constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); - ExecType internal 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/README.adoc b/contracts/account/README.adoc new file mode 100644 index 00000000000..d2eb9db5ee9 --- /dev/null +++ b/contracts/account/README.adoc @@ -0,0 +1,12 @@ += 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. + +== Utilities + +{{ERC4337Utils}} + +{{ERC7579Utils}} diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/account/utils/draft-ERC4337Utils.sol similarity index 61% rename from contracts/abstraction/utils/ERC4337Utils.sol rename to contracts/account/utils/draft-ERC4337Utils.sol index f3f049d14cc..a66dd0f9c4b 100644 --- a/contracts/abstraction/utils/ERC4337Utils.sol +++ b/contracts/account/utils/draft-ERC4337Utils.sol @@ -2,26 +2,25 @@ pragma solidity ^0.8.20; -import {IEntryPoint, PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {IEntryPoint, PackedUserOperation} from "../../interfaces/draft-IERC4337.sol"; import {Math} from "../../utils/math/Math.sol"; -// import {Memory} from "../../utils/Memory.sol"; import {Packing} from "../../utils/Packing.sol"; +/** + * @dev Library with common ERC-4337 utility functions. + * + * See https://eips.ethereum.org/EIPS/eip-4337[ERC-4337]. + */ library ERC4337Utils { using Packing for *; - /* - * For simulation purposes, validateUserOp (and validatePaymasterUserOp) - * return this value on success. - */ + + /// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) return this value on success. uint256 internal constant SIG_VALIDATION_SUCCESS = 0; - /* - * For simulation purposes, validateUserOp (and validatePaymasterUserOp) - * must return this value in case of signature failure, instead of revert. - */ + /// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) must return this value in case of signature failure, instead of revert. uint256 internal constant SIG_VALIDATION_FAILED = 1; - // Validation data + /// @dev Parses the validation data into its components. See {packValidationData}. function parseValidationData( uint256 validationData ) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) { @@ -31,6 +30,7 @@ library ERC4337Utils { if (validUntil == 0) validUntil = type(uint48).max; } + /// @dev Packs the validation data into a single uint256. See {parseValidationData}. function packValidationData( address aggregator, uint48 validAfter, @@ -39,15 +39,22 @@ library ERC4337Utils { return uint256(bytes6(validAfter).pack_6_6(bytes6(validUntil)).pack_12_20(bytes20(aggregator))); } + /// @dev Same as {packValidationData}, but with a boolean signature success flag. function packValidationData(bool sigSuccess, uint48 validAfter, uint48 validUntil) internal pure returns (uint256) { return - uint256( - bytes6(validAfter).pack_6_6(bytes6(validUntil)).pack_12_20( - bytes20(uint160(Math.ternary(sigSuccess, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED))) - ) + packValidationData( + address(uint160(Math.ternary(sigSuccess, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED))), + validAfter, + validUntil ); } + /** + * @dev Combines two validation data into a single one. + * + * The `aggregator` is set to {SIG_VALIDATION_SUCCESS} if both are successful, while + * the `validAfter` is the maximum and the `validUntil` is the minimum of both. + */ function combineValidationData(uint256 validationData1, uint256 validationData2) internal pure returns (uint256) { (address aggregator1, uint48 validAfter1, uint48 validUntil1) = parseValidationData(validationData1); (address aggregator2, uint48 validAfter2, uint48 validUntil2) = parseValidationData(validationData2); @@ -58,6 +65,7 @@ library ERC4337Utils { return packValidationData(success, validAfter, validUntil); } + /// @dev Returns the aggregator of the `validationData` and whether it is out of time range. function getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) { if (validationData == 0) { return (address(0), false); @@ -67,17 +75,17 @@ library ERC4337Utils { } } - // Packed user operation + /// @dev Computes the hash of a user operation with the current entrypoint and chainid. function hash(PackedUserOperation calldata self) internal view returns (bytes32) { return hash(self, address(this), block.chainid); } + /// @dev Sames as {hash}, but with a custom entrypoint and chainid. function hash( PackedUserOperation calldata self, address entrypoint, uint256 chainid ) internal pure returns (bytes32) { - // Memory.FreePtr ptr = Memory.save(); bytes32 result = keccak256( abi.encode( keccak256( @@ -96,26 +104,30 @@ library ERC4337Utils { chainid ) ); - // Memory.load(ptr); return result; } + /// @dev Returns `verificationGasLimit` from the {PackedUserOperation}. function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { return uint128(self.accountGasLimits.extract_32_16(0x00)); } + /// @dev Returns `accountGasLimits` from the {PackedUserOperation}. function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { return uint128(self.accountGasLimits.extract_32_16(0x10)); } + /// @dev Returns the first section of `gasFees` from the {PackedUserOperation}. function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { return uint128(self.gasFees.extract_32_16(0x00)); } + /// @dev Returns the second section of `gasFees` from the {PackedUserOperation}. function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { return uint128(self.gasFees.extract_32_16(0x10)); } + /// @dev Returns the total gas price for the {PackedUserOperation} (ie. `maxFeePerGas` or `maxPriorityFeePerGas + basefee`). function gasPrice(PackedUserOperation calldata self) internal view returns (uint256) { unchecked { // Following values are "per gas" @@ -125,74 +137,18 @@ library ERC4337Utils { } } + /// @dev Returns the first section of `paymasterAndData` from the {PackedUserOperation}. function paymaster(PackedUserOperation calldata self) internal pure returns (address) { return address(bytes20(self.paymasterAndData[0:20])); } + /// @dev Returns the second section of `paymasterAndData` from the {PackedUserOperation}. function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { return uint128(bytes16(self.paymasterAndData[20:36])); } + /// @dev Returns the third section of `paymasterAndData` from the {PackedUserOperation}. function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { return uint128(bytes16(self.paymasterAndData[36:52])); } - - struct UserOpInfo { - address sender; - uint256 nonce; - uint256 verificationGasLimit; - uint256 callGasLimit; - uint256 paymasterVerificationGasLimit; - uint256 paymasterPostOpGasLimit; - uint256 preVerificationGas; - address paymaster; - uint256 maxFeePerGas; - uint256 maxPriorityFeePerGas; - bytes32 userOpHash; - uint256 prefund; - uint256 preOpGas; - bytes context; - } - - function load(UserOpInfo memory self, PackedUserOperation calldata source) internal view { - self.sender = source.sender; - self.nonce = source.nonce; - self.verificationGasLimit = uint128(bytes32(source.accountGasLimits).extract_32_16(0x00)); - self.callGasLimit = uint128(bytes32(source.accountGasLimits).extract_32_16(0x10)); - self.preVerificationGas = source.preVerificationGas; - self.maxPriorityFeePerGas = uint128(bytes32(source.gasFees).extract_32_16(0x00)); - self.maxFeePerGas = uint128(bytes32(source.gasFees).extract_32_16(0x10)); - - if (source.paymasterAndData.length > 0) { - require(source.paymasterAndData.length >= 52, "AA93 invalid paymasterAndData"); - self.paymaster = paymaster(source); - self.paymasterVerificationGasLimit = paymasterVerificationGasLimit(source); - self.paymasterPostOpGasLimit = paymasterPostOpGasLimit(source); - } else { - self.paymaster = address(0); - self.paymasterVerificationGasLimit = 0; - self.paymasterPostOpGasLimit = 0; - } - self.userOpHash = hash(source); - self.prefund = 0; - self.preOpGas = 0; - self.context = ""; - } - - function requiredPrefund(UserOpInfo memory self) internal pure returns (uint256) { - return - (self.verificationGasLimit + - self.callGasLimit + - self.paymasterVerificationGasLimit + - self.paymasterPostOpGasLimit + - self.preVerificationGas) * self.maxFeePerGas; - } - - function gasPrice(UserOpInfo memory self) internal view returns (uint256) { - unchecked { - uint256 maxFee = self.maxFeePerGas; - uint256 maxPriorityFee = self.maxPriorityFeePerGas; - return Math.ternary(maxFee == maxPriorityFee, maxFee, Math.min(maxFee, maxPriorityFee + block.basefee)); - } - } } diff --git a/contracts/account/utils/draft-ERC7579Utils.sol b/contracts/account/utils/draft-ERC7579Utils.sol new file mode 100644 index 00000000000..de6bb6509ec --- /dev/null +++ b/contracts/account/utils/draft-ERC7579Utils.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Execution} from "../../interfaces/draft-IERC7579.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; + +/** + * @dev Library with common ERC-7579 utility functions. + * + * See https://eips.ethereum.org/EIPS/eip-7579[ERC-7579]. + */ +// slither-disable-next-line unused-state +library ERC7579Utils { + using Packing for *; + + /// @dev A single `call` execution. + CallType constant CALLTYPE_SINGLE = CallType.wrap(0x00); + + /// @dev A batch of `call` executions. + CallType constant CALLTYPE_BATCH = CallType.wrap(0x01); + + /// @dev A `delegatecall` execution. + CallType constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); + + /// @dev Default execution type that reverts on failure. + ExecType constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); + + /// @dev Execution type that does not revert on failure. + ExecType constant EXECTYPE_TRY = ExecType.wrap(0x01); + + /// @dev Emits when an {EXECTYPE_TRY} execution fails. + event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes result); + + /// @dev The provided {CallType} is not supported. + error ERC7579UnsupportedCallType(CallType callType); + + /// @dev The provided {ExecType} is not supported. + error ERC7579UnsupportedExecType(ExecType execType); + + /// @dev The provided module doesn't match the provided module type. + error ERC7579MismatchedModuleTypeId(uint256 moduleTypeId, address module); + + /// @dev The module is not installed. + error ERC7579UninstalledModule(uint256 moduleTypeId, address module); + + /// @dev The module is already installed. + error ERC7579AlreadyInstalledModule(uint256 moduleTypeId, address module); + + /// @dev The module type is not supported. + error ERC7579UnsupportedModuleType(uint256 moduleTypeId); + + /// @dev Executes a single call. + 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); + } + + /// @dev Executes a batch of calls. + 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 + ); + } + } + + /// @dev Executes a delegate call. + function execDelegateCall( + ExecType execType, + bytes calldata executionCalldata + ) internal returns (bytes[] memory returnData) { + (address target, bytes calldata callData) = decodeDelegate(executionCalldata); + returnData = new bytes[](1); + returnData[0] = _delegatecall(0, execType, target, callData); + } + + /// @dev Encodes the mode with the provided parameters. See {decodeMode}. + 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)) + ); + } + + /// @dev Decodes the mode into its parameters. See {encodeMode}. + 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)) + ); + } + + /// @dev Encodes a single call execution. See {decodeSingle}. + function encodeSingle( + address target, + uint256 value, + bytes calldata callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, value, callData); + } + + /// @dev Decodes a single call execution. See {encodeSingle}. + 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:]; + } + + /// @dev Encodes a delegate call execution. See {decodeDelegate}. + function encodeDelegate( + address target, + bytes calldata callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, callData); + } + + /// @dev Decodes a delegate call execution. See {encodeDelegate}. + function decodeDelegate( + bytes calldata executionCalldata + ) internal pure returns (address target, bytes calldata callData) { + target = address(bytes20(executionCalldata[0:20])); + callData = executionCalldata[20:]; + } + + /// @dev Encodes a batch of executions. See {decodeBatch}. + function encodeBatch(Execution[] memory executionBatch) internal pure returns (bytes memory executionCalldata) { + return abi.encode(executionBatch); + } + + /// @dev Decodes a batch of executions. See {encodeBatch}. + 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) + } + } + + /// @dev Executes a `call` to the target with the provided {ExecType}. + 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); + } + + /// @dev Executes a `delegatecall` to the target with the provided {ExecType}. + function _delegatecall( + uint256 index, + ExecType execType, + address target, + bytes calldata data + ) private returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return _validateExecutionMode(index, execType, success, returndata); + } + + /// @dev Validates the execution mode and returns the returndata. + function _validateExecutionMode( + uint256 index, + ExecType execType, + bool success, + bytes memory returndata + ) private returns (bytes memory) { + if (execType == ERC7579Utils.EXECTYPE_DEFAULT) { + Address.verifyCallResult(success, returndata); + } else if (execType == ERC7579Utils.EXECTYPE_TRY) { + if (!success) emit ERC7579TryExecuteFail(index, returndata); + } else { + revert ERC7579UnsupportedExecType(execType); + } + return returndata; + } +} + +// 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; + +/// @dev Compares two `CallType` values for equality. +function eqCallType(CallType a, CallType b) pure returns (bool) { + return CallType.unwrap(a) == CallType.unwrap(b); +} + +/// @dev Compares two `ExecType` values for equality. +function eqExecType(ExecType a, ExecType b) pure returns (bool) { + return ExecType.unwrap(a) == ExecType.unwrap(b); +} + +/// @dev Compares two `ModeSelector` values for equality. +function eqModeSelector(ModeSelector a, ModeSelector b) pure returns (bool) { + return ModeSelector.unwrap(a) == ModeSelector.unwrap(b); +} + +/// @dev Compares two `ModePayload` values for equality. +function eqModePayload(ModePayload a, ModePayload b) pure returns (bool) { + return ModePayload.unwrap(a) == ModePayload.unwrap(b); +} diff --git a/contracts/interfaces/IERC4337.sol b/contracts/interfaces/IERC4337.sol deleted file mode 100644 index 0f681b74aba..00000000000 --- a/contracts/interfaces/IERC4337.sol +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -/* -struct UserOperation { - address sender; // The account making the operation - uint256 nonce; // Anti-replay parameter (see “Semi-abstracted Nonce Support” ) - address factory; // account factory, only for new accounts - bytes factoryData; // data for account factory (only if account factory exists) - bytes callData; // The data to pass to the sender during the main execution call - uint256 callGasLimit; // The amount of gas to allocate the main execution call - uint256 verificationGasLimit; // The amount of gas to allocate for the verification step - uint256 preVerificationGas; // Extra gas to pay the bunder - uint256 maxFeePerGas; // Maximum fee per gas (similar to EIP-1559 max_fee_per_gas) - uint256 maxPriorityFeePerGas; // Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas) - address paymaster; // Address of paymaster contract, (or empty, if account pays for itself) - uint256 paymasterVerificationGasLimit; // The amount of gas to allocate for the paymaster validation code - uint256 paymasterPostOpGasLimit; // The amount of gas to allocate for the paymaster post-operation code - bytes paymasterData; // Data for paymaster (only if paymaster exists) - bytes signature; // Data passed into the account to verify authorization -} -*/ - -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; -} - -interface IAggregator { - function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external view; - - function validateUserOpSignature( - PackedUserOperation calldata userOp - ) external view returns (bytes memory sigForUserOp); - - function aggregateSignatures( - PackedUserOperation[] calldata userOps - ) external view returns (bytes memory aggregatesSignature); -} - -interface IEntryPointNonces { - function getNonce(address sender, uint192 key) external view returns (uint256 nonce); -} - -interface IEntryPointStake { - function balanceOf(address account) external view returns (uint256); - - function depositTo(address account) external payable; - - function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external; - - function addStake(uint32 unstakeDelaySec) external payable; - - function unlockStake() external; - - function withdrawStake(address payable withdrawAddress) external; -} - -interface IEntryPoint is IEntryPointNonces, IEntryPointStake { - error FailedOp(uint256 opIndex, string reason); - error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); - - struct UserOpsPerAggregator { - PackedUserOperation[] userOps; - IAggregator aggregator; - bytes signature; - } - - function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; - - function handleAggregatedOps( - UserOpsPerAggregator[] calldata opsPerAggregator, - address payable beneficiary - ) external; -} - -// TODO: EntryPointSimulation - -interface IAccount { - function validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash, - uint256 missingAccountFunds - ) external returns (uint256 validationData); -} - -interface IAccountExecute { - function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; -} - -interface IPaymaster { - enum PostOpMode { - opSucceeded, - opReverted, - postOpReverted - } - - function validatePaymasterUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash, - uint256 maxCost - ) external returns (bytes memory context, uint256 validationData); - - function postOp( - PostOpMode mode, - bytes calldata context, - uint256 actualGasCost, - uint256 actualUserOpFeePerGas - ) external; -} diff --git a/contracts/interfaces/IERC7579Module.sol b/contracts/interfaces/IERC7579Module.sol deleted file mode 100644 index 1aee412c053..00000000000 --- a/contracts/interfaces/IERC7579Module.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -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; -uint256 constant MODULE_TYPE_HOOK = 4; - -interface IERC7579Module { - /** - * @dev This function is called by the smart account during installation of the module - * @param data arbitrary data that may be required on the module during `onInstall` initialization - * - * MUST revert on error (e.g. if module is already enabled) - */ - function onInstall(bytes calldata data) external; - - /** - * @dev This function is called by the smart account during uninstallation of the module - * @param data arbitrary data that may be required on the module during `onUninstall` de-initialization - * - * MUST revert on error - */ - function onUninstall(bytes calldata data) external; - - /** - * @dev Returns boolean value if module is a certain type - * @param moduleTypeId the module type ID according the ERC-7579 spec - * - * MUST return true if the module is of the given type and false otherwise - */ - function isModuleType(uint256 moduleTypeId) external view returns (bool); -} - -interface IERC7579Validator is IERC7579Module { - /** - * @dev Validates a UserOperation - * @param userOp the ERC-4337 PackedUserOperation - * @param userOpHash the hash of the ERC-4337 PackedUserOperation - * - * MUST validate that the signature is a valid signature of the userOpHash - * SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch - */ - function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); - - /** - * @dev Validates a signature using ERC-1271 - * @param sender the address that sent the ERC-1271 request to the smart account - * @param hash the hash of the ERC-1271 request - * @param signature the signature of the ERC-1271 request - * - * MUST return the ERC-1271 `MAGIC_VALUE` if the signature is valid - * MUST NOT modify state - */ - function isValidSignatureWithSender( - address sender, - bytes32 hash, - bytes calldata signature - ) external view returns (bytes4); -} - -interface IERC7579Hook is IERC7579Module { - /** - * @dev Called by the smart account before execution - * @param msgSender the address that called the smart account - * @param value the value that was sent to the smart account - * @param msgData the data that was sent to the smart account - * - * MAY return arbitrary data in the `hookData` return value - */ - function preCheck( - address msgSender, - uint256 value, - bytes calldata msgData - ) external returns (bytes memory hookData); - - /** - * @dev Called by the smart account after execution - * @param hookData the data that was returned by the `preCheck` function - * - * MAY validate the `hookData` to validate transaction context of the `preCheck` function - */ - function postCheck(bytes calldata hookData) external; -} diff --git a/contracts/interfaces/draft-IERC4337.sol b/contracts/interfaces/draft-IERC4337.sol new file mode 100644 index 00000000000..9b1af56b6e5 --- /dev/null +++ b/contracts/interfaces/draft-IERC4337.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev A https://github.com/ethereum/ercs/blob/master/ERCS/erc-4337.md#useroperation[user operation] is composed of the following elements: + * - `sender` (`address`): The account making the operation + * - `nonce` (`uint256`): Anti-replay parameter (see “Semi-abstracted Nonce Support” ) + * - `factory` (`address`): account factory, only for new accounts + * - `factoryData` (`bytes`): data for account factory (only if account factory exists) + * - `callData` (`bytes`): The data to pass to the sender during the main execution call + * - `callGasLimit` (`uint256`): The amount of gas to allocate the main execution call + * - `verificationGasLimit` (`uint256`): The amount of gas to allocate for the verification step + * - `preVerificationGas` (`uint256`): Extra gas to pay the bunder + * - `maxFeePerGas` (`uint256`): Maximum fee per gas (similar to EIP-1559 max_fee_per_gas) + * - `maxPriorityFeePerGas` (`uint256`): Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas) + * - `paymaster` (`address`): Address of paymaster contract, (or empty, if account pays for itself) + * - `paymasterVerificationGasLimit` (`uint256`): The amount of gas to allocate for the paymaster validation code + * - `paymasterPostOpGasLimit` (`uint256`): The amount of gas to allocate for the paymaster post-operation code + * - `paymasterData` (`bytes`): Data for paymaster (only if paymaster exists) + * - `signature` (`bytes`): Data passed into the account to verify authorization + * + * When passed to on-chain contacts, the following packed version is used. + * - `sender` (`address`) + * - `nonce` (`uint256`) + * - `initCode` (`bytes`): concatenation of factory address and factoryData (or empty) + * - `callData` (`bytes`) + * - `accountGasLimits` (`bytes32`): concatenation of verificationGas (16 bytes) and callGas (16 bytes) + * - `preVerificationGas` (`uint256`) + * - `gasFees` (`bytes32`): concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes) + * - `paymasterAndData` (`bytes`): concatenation of paymaster fields (or empty) + * - `signature` (`bytes`) + */ +struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; // `abi.encodePacked(factory, factoryData)` + bytes callData; + bytes32 accountGasLimits; // `abi.encodePacked(verificationGasLimit, callGasLimit)` 16 bytes each + uint256 preVerificationGas; + bytes32 gasFees; // `abi.encodePacked(maxPriorityFee, maxFeePerGas)` 16 bytes each + bytes paymasterAndData; // `abi.encodePacked(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData)` + bytes signature; +} + +/** + * @dev Aggregates and validates multiple signatures for a batch of user operations. + */ +interface IAggregator { + /** + * @dev Validates the signature for a user operation. + */ + function validateUserOpSignature( + PackedUserOperation calldata userOp + ) external view returns (bytes memory sigForUserOp); + + /** + * @dev Returns an aggregated signature for a batch of user operation's signatures. + */ + function aggregateSignatures( + PackedUserOperation[] calldata userOps + ) external view returns (bytes memory aggregatesSignature); + + /** + * @dev Validates that the aggregated signature is valid for the user operations. + * + * Requirements: + * + * - The aggregated signature MUST match the given list of operations. + */ + function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external view; +} + +/** + * @dev Handle nonce management for accounts. + */ +interface IEntryPointNonces { + /** + * @dev Returns the nonce for a `sender` account and a `key`. + * + * Nonces for a certain `key` are always increasing. + */ + function getNonce(address sender, uint192 key) external view returns (uint256 nonce); +} + +/** + * @dev Handle stake management for accounts. + */ +interface IEntryPointStake { + /** + * @dev Returns the balance of the account. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Deposits `msg.value` to the account. + */ + function depositTo(address account) external payable; + + /** + * @dev Withdraws `withdrawAmount` from the account to `withdrawAddress`. + */ + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external; + + /** + * @dev Adds stake to the account with an unstake delay of `unstakeDelaySec`. + */ + function addStake(uint32 unstakeDelaySec) external payable; + + /** + * @dev Unlocks the stake of the account. + */ + function unlockStake() external; + + /** + * @dev Withdraws the stake of the account to `withdrawAddress`. + */ + function withdrawStake(address payable withdrawAddress) external; +} + +/** + * @dev Entry point for user operations. + */ +interface IEntryPoint is IEntryPointNonces, IEntryPointStake { + /** + * @dev A user operation at `opIndex` failed with `reason`. + */ + error FailedOp(uint256 opIndex, string reason); + + /** + * @dev A user operation at `opIndex` failed with `reason` and `inner` returned data. + */ + error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); + + /** + * @dev Batch of aggregated user operations per aggregator. + */ + struct UserOpsPerAggregator { + PackedUserOperation[] userOps; + IAggregator aggregator; + bytes signature; + } + + /** + * @dev Executes a batch of user operations. + */ + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; + + /** + * @dev Executes a batch of aggregated user operations per aggregator. + */ + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) external; +} + +/** + * @dev Base interface for an account. + */ +interface IAccount { + /** + * @dev Validates a user operation. + */ + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData); +} + +/** + * @dev Support for executing user operations by prepending the {executeUserOp} function selector + * to the UserOperation's `callData`. + */ +interface IAccountExecute { + /** + * @dev Executes a user operation. + */ + function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; +} + +/** + * @dev Interface for a paymaster contract that agrees to pay for the gas costs of a user operation. + * + * NOTE: A paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction. + */ +interface IPaymaster { + enum PostOpMode { + opSucceeded, + opReverted, + postOpReverted + } + + /** + * @dev Validates whether the paymaster is willing to pay for the user operation. + * + * NOTE: Bundlers will reject this method if it modifies the state, unless it's whitelisted. + */ + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external returns (bytes memory context, uint256 validationData); + + /** + * @dev Verifies the sender is the entrypoint. + */ + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external; +} diff --git a/contracts/interfaces/IERC7579Account.sol b/contracts/interfaces/draft-IERC7579.sol similarity index 58% rename from contracts/interfaces/IERC7579Account.sol rename to contracts/interfaces/draft-IERC7579.sol index 0be805f5b1f..47f1627f682 100644 --- a/contracts/interfaces/IERC7579Account.sol +++ b/contracts/interfaces/draft-IERC7579.sol @@ -1,9 +1,91 @@ // 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"; +import {PackedUserOperation} from "./draft-IERC4337.sol"; + +uint256 constant VALIDATION_SUCCESS = 0; +uint256 constant VALIDATION_FAILED = 1; +uint256 constant MODULE_TYPE_VALIDATOR = 1; +uint256 constant MODULE_TYPE_EXECUTOR = 2; +uint256 constant MODULE_TYPE_FALLBACK = 3; +uint256 constant MODULE_TYPE_HOOK = 4; + +interface IERC7579Module { + /** + * @dev This function is called by the smart account during installation of the module + * @param data arbitrary data that may be required on the module during `onInstall` initialization + * + * MUST revert on error (e.g. if module is already enabled) + */ + function onInstall(bytes calldata data) external; + + /** + * @dev This function is called by the smart account during uninstallation of the module + * @param data arbitrary data that may be required on the module during `onUninstall` de-initialization + * + * MUST revert on error + */ + function onUninstall(bytes calldata data) external; + + /** + * @dev Returns boolean value if module is a certain type + * @param moduleTypeId the module type ID according the ERC-7579 spec + * + * MUST return true if the module is of the given type and false otherwise + */ + function isModuleType(uint256 moduleTypeId) external view returns (bool); +} + +interface IERC7579Validator is IERC7579Module { + /** + * @dev Validates a UserOperation + * @param userOp the ERC-4337 PackedUserOperation + * @param userOpHash the hash of the ERC-4337 PackedUserOperation + * + * MUST validate that the signature is a valid signature of the userOpHash + * SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch + */ + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); + + /** + * @dev Validates a signature using ERC-1271 + * @param sender the address that sent the ERC-1271 request to the smart account + * @param hash the hash of the ERC-1271 request + * @param signature the signature of the ERC-1271 request + * + * MUST return the ERC-1271 `MAGIC_VALUE` if the signature is valid + * MUST NOT modify state + */ + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata signature + ) external view returns (bytes4); +} + +interface IERC7579Hook is IERC7579Module { + /** + * @dev Called by the smart account before execution + * @param msgSender the address that called the smart account + * @param value the value that was sent to the smart account + * @param msgData the data that was sent to the smart account + * + * MAY return arbitrary data in the `hookData` return value + */ + function preCheck( + address msgSender, + uint256 value, + bytes calldata msgData + ) external returns (bytes memory hookData); + + /** + * @dev Called by the smart account after execution + * @param hookData the data that was returned by the `preCheck` function + * + * MAY validate the `hookData` to validate transaction context of the `preCheck` function + */ + function postCheck(bytes calldata hookData) external; +} struct Execution { address target; diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index e0193a91442..97924bc7d50 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -22,8 +22,8 @@ import {ERC165} from "../utils/introspection/ERC165.sol"; import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol"; import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol"; import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; -import {ERC4337Utils} from "../abstraction/utils/ERC4337Utils.sol"; -import {ERC7579Utils} from "../abstraction/utils/ERC7579Utils.sol"; +import {ERC4337Utils} from "../account/utils/draft-ERC4337Utils.sol"; +import {ERC7579Utils} from "../account/utils/draft-ERC7579Utils.sol"; import {Heap} from "../utils/structs/Heap.sol"; import {Math} from "../utils/math/Math.sol"; import {MerkleProof} from "../utils/cryptography/MerkleProof.sol"; diff --git a/contracts/mocks/account/utils/ERC7579UtilsMock.sol b/contracts/mocks/account/utils/ERC7579UtilsMock.sol new file mode 100644 index 00000000000..e0a1e1a50ea --- /dev/null +++ b/contracts/mocks/account/utils/ERC7579UtilsMock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {CallType, ExecType, ModeSelector, ModePayload} from "../../../account/utils/draft-ERC7579Utils.sol"; + +contract ERC7579UtilsGlobalMock { + function eqCallTypeGlobal(CallType callType1, CallType callType2) internal pure returns (bool) { + return callType1 == callType2; + } + + function eqExecTypeGlobal(ExecType execType1, ExecType execType2) internal pure returns (bool) { + return execType1 == execType2; + } + + function eqModeSelectorGlobal(ModeSelector modeSelector1, ModeSelector modeSelector2) internal pure returns (bool) { + return modeSelector1 == modeSelector2; + } + + function eqModePayloadGlobal(ModePayload modePayload1, ModePayload modePayload2) internal pure returns (bool) { + return modePayload1 == modePayload2; + } +} diff --git a/contracts/vendor/erc4337-entrypoint/README.md b/contracts/vendor/erc4337-entrypoint/README.md new file mode 100644 index 00000000000..e0b91fe9dd6 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/README.md @@ -0,0 +1 @@ +Files in this directory are vendored from https://github.com/eth-infinitism/account-abstraction/commit/6f02f5a28a20e804d0410b4b5b570dd4b076dcf9 diff --git a/scripts/generate/templates/Packing.t.js b/scripts/generate/templates/Packing.t.js index 56e9c0cc7c4..1feec28f5a5 100644 --- a/scripts/generate/templates/Packing.t.js +++ b/scripts/generate/templates/Packing.t.js @@ -11,14 +11,14 @@ import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; `; const testPack = (left, right) => `\ -function testPack(bytes${left} left, bytes${right} right) external { +function testPack(bytes${left} left, bytes${right} right) external pure { assertEq(left, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${left}(0)); assertEq(right, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${right}(${left})); } `; const testReplace = (outer, inner) => `\ -function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external { +function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, ${outer - inner})); bytes${inner} oldValue = container.extract_${outer}_${inner}(offset); diff --git a/test/account/utils/draft-ERC4337Utils.test.js b/test/account/utils/draft-ERC4337Utils.test.js new file mode 100644 index 00000000000..c76f89845ac --- /dev/null +++ b/test/account/utils/draft-ERC4337Utils.test.js @@ -0,0 +1,207 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { + SIG_VALIDATION_SUCCESS, + SIG_VALIDATION_FAILURE, + toAuthorizer, + packValidationData, + packPaymasterData, + UserOperation, +} = require('../../helpers/erc4337'); +const { ZeroAddress } = require('ethers'); +const { MAX_UINT48 } = require('../../helpers/constants'); + +const fixture = async () => { + const [authorizer, sender, entrypoint, paymaster] = await ethers.getSigners(); + const utils = await ethers.deployContract('$ERC4337Utils'); + return { utils, authorizer, sender, entrypoint, paymaster }; +}; + +describe('ERC4337Utils', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('parseValidationData', function () { + it('parses the validation data', async function () { + const authorizer = this.authorizer.address; + const validUntil = 0x12345678n; + const validAfter = 0x9abcdef0n; + const result = await this.utils.$parseValidationData(packValidationData(validAfter, validUntil, authorizer)); + expect(result).to.deep.equal([authorizer, validAfter, validUntil]); + }); + + it('returns an type(uint48).max if until is 0', async function () { + const authorizer = this.authorizer.address; + const validAfter = 0x12345678n; + const result = await this.utils.$parseValidationData(packValidationData(validAfter, 0, authorizer)); + expect(result).to.deep.equal([authorizer, validAfter, MAX_UINT48]); + }); + }); + + describe('packValidationData', function () { + it('packs the validation data', async function () { + const authorizer = this.authorizer.address; + const validUntil = 0x12345678n; + const validAfter = 0x9abcdef0n; + const result = await this.utils.$packValidationData(ethers.Typed.address(authorizer), validAfter, validUntil); + expect(result).to.equal(packValidationData(validAfter, validUntil, authorizer)); + }); + + it('packs the validation data (bool)', async function () { + const success = false; + const validUntil = 0x12345678n; + const validAfter = 0x9abcdef0n; + const result = await this.utils.$packValidationData(ethers.Typed.bool(success), validAfter, validUntil); + expect(result).to.equal(packValidationData(validAfter, validUntil, toAuthorizer(SIG_VALIDATION_FAILURE))); + }); + }); + + describe('combineValidationData', function () { + it('combines the validation data', async function () { + const authorizer1 = ZeroAddress; + const validUntil1 = 0x12345678n; + const validAfter1 = 0x9abcdef0n; + + const authorizer2 = ZeroAddress; + const validUntil2 = 0x87654321n; + const validAfter2 = 0xabcdef90n; + + const result = await this.utils.$combineValidationData( + packValidationData(validAfter1, validUntil1, authorizer1), + packValidationData(validAfter2, validUntil2, authorizer2), + ); + expect(result).to.equal(packValidationData(validAfter2, validUntil1, toAuthorizer(SIG_VALIDATION_SUCCESS))); + }); + + // address(bytes20(keccak256('openzeppelin.erc4337.tests'))) + for (const authorizers of [ + [ZeroAddress, '0xbf023313b891fd6000544b79e353323aa94a4f29'], + ['0xbf023313b891fd6000544b79e353323aa94a4f29', ZeroAddress], + ]) { + it('returns SIG_VALIDATION_FAILURE if one of the authorizers is not address(0)', async function () { + const validUntil1 = 0x12345678n; + const validAfter1 = 0x9abcdef0n; + + const validUntil2 = 0x87654321n; + const validAfter2 = 0xabcdef90n; + + const result = await this.utils.$combineValidationData( + packValidationData(validAfter1, validUntil1, authorizers[0]), + packValidationData(validAfter2, validUntil2, authorizers[1]), + ); + expect(result).to.equal(packValidationData(validAfter2, validUntil1, toAuthorizer(SIG_VALIDATION_FAILURE))); + }); + } + }); + + describe('getValidationData', function () { + it('returns the validation data with valid validity range', async function () { + const aggregator = this.authorizer.address; + const validAfter = 0; + const validUntil = MAX_UINT48; + const result = await this.utils.$getValidationData(packValidationData(validAfter, validUntil, aggregator)); + expect(result).to.deep.equal([aggregator, false]); + }); + + it('returns the validation data with invalid validity range (expired)', async function () { + const aggregator = this.authorizer.address; + const validAfter = 0; + const validUntil = 1; + const result = await this.utils.$getValidationData(packValidationData(validAfter, validUntil, aggregator)); + expect(result).to.deep.equal([aggregator, true]); + }); + + it('returns the validation data with invalid validity range (not yet valid)', async function () { + const aggregator = this.authorizer.address; + const validAfter = MAX_UINT48; + const validUntil = MAX_UINT48; + const result = await this.utils.$getValidationData(packValidationData(validAfter, validUntil, aggregator)); + expect(result).to.deep.equal([aggregator, true]); + }); + + it('returns address(0) and false for validationData = 0', function () { + return expect(this.utils.$getValidationData(0n)).to.eventually.deep.equal([ZeroAddress, false]); + }); + }); + + describe('hash', function () { + it('returns the user operation hash', async function () { + const userOp = new UserOperation({ sender: this.sender.address, nonce: 1 }); + const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId); + const hash = await this.utils.$hash(userOp.packed); + expect(hash).to.equal(userOp.hash(this.utils.target, chainId)); + }); + + it('returns the operation hash with specified entrypoint and chainId', async function () { + const userOp = new UserOperation({ sender: this.sender.address, nonce: 1 }); + const chainId = 0xdeadbeef; + const hash = await this.utils.$hash(userOp.packed, this.entrypoint.address, chainId); + expect(hash).to.equal(userOp.hash(this.entrypoint.address, chainId)); + }); + }); + + describe('userOp values', function () { + it('returns verificationGasLimit', async function () { + const userOp = new UserOperation({ sender: this.sender.address, nonce: 1, verificationGas: 0x12345678n }); + expect(await this.utils.$verificationGasLimit(userOp.packed)).to.equal(userOp.verificationGas); + }); + + it('returns callGasLimit', async function () { + const userOp = new UserOperation({ sender: this.sender.address, nonce: 1, callGas: 0x12345678n }); + expect(await this.utils.$callGasLimit(userOp.packed)).to.equal(userOp.callGas); + }); + + it('returns maxPriorityFeePerGas', async function () { + const userOp = new UserOperation({ sender: this.sender.address, nonce: 1, maxPriorityFee: 0x12345678n }); + expect(await this.utils.$maxPriorityFeePerGas(userOp.packed)).to.equal(userOp.maxPriorityFee); + }); + + it('returns maxFeePerGas', async function () { + const userOp = new UserOperation({ sender: this.sender.address, nonce: 1, maxFeePerGas: 0x12345678n }); + expect(await this.utils.$maxFeePerGas(userOp.packed)).to.equal(userOp.maxFeePerGas); + }); + + it('returns gasPrice', async function () { + const userOp = new UserOperation({ + sender: this.sender.address, + nonce: 1, + maxPriorityFee: 0x12345678n, + maxFeePerGas: 0x87654321n, + }); + expect( + await this.utils['$gasPrice((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes))'](userOp.packed), + ).to.equal(userOp.maxPriorityFee); + }); + + describe('paymasterAndData', function () { + beforeEach(async function () { + this.verificationGasLimit = 0x12345678n; + this.postOpGasLimit = 0x87654321n; + this.paymasterAndData = packPaymasterData( + this.paymaster.address, + this.verificationGasLimit, + this.postOpGasLimit, + ); + this.userOp = new UserOperation({ + sender: this.sender.address, + nonce: 1, + paymasterAndData: this.paymasterAndData, + }); + }); + + it('returns paymaster', async function () { + expect(await this.utils.$paymaster(this.userOp.packed)).to.equal(this.paymaster.address); + }); + + it('returns verificationGasLimit', async function () { + expect(await this.utils.$paymasterVerificationGasLimit(this.userOp.packed)).to.equal(this.verificationGasLimit); + }); + + it('returns postOpGasLimit', async function () { + expect(await this.utils.$paymasterPostOpGasLimit(this.userOp.packed)).to.equal(this.postOpGasLimit); + }); + }); + }); +}); diff --git a/test/account/utils/draft-ERC7579Utils.test.js b/test/account/utils/draft-ERC7579Utils.test.js new file mode 100644 index 00000000000..9d6154f78bd --- /dev/null +++ b/test/account/utils/draft-ERC7579Utils.test.js @@ -0,0 +1,346 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); +const { + EXEC_TYPE_DEFAULT, + EXEC_TYPE_TRY, + encodeSingle, + encodeBatch, + encodeDelegate, + CALL_TYPE_CALL, + CALL_TYPE_BATCH, + encodeMode, +} = require('../../helpers/erc7579'); +const { selector } = require('../../helpers/methods'); + +const coder = ethers.AbiCoder.defaultAbiCoder(); + +const fixture = async () => { + const [sender] = await ethers.getSigners(); + const utils = await ethers.deployContract('$ERC7579Utils'); + const utilsGlobal = await ethers.deployContract('$ERC7579UtilsGlobalMock'); + const target = await ethers.deployContract('CallReceiverMock'); + const anotherTarget = await ethers.deployContract('CallReceiverMock'); + await setBalance(utils.target, ethers.parseEther('1')); + return { utils, utilsGlobal, target, anotherTarget, sender }; +}; + +describe('ERC7579Utils', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('execSingle', function () { + it('calls the target with value', async function () { + const value = 0x012; + const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction')); + await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.emit(this.target, 'MockFunctionCalled'); + expect(await ethers.provider.getBalance(this.target)).to.equal(value); + }); + + it('calls the target with value and args', async function () { + const value = 0x432; + const data = encodeSingle( + this.target, + value, + this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']), + ); + await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)) + .to.emit(this.target, 'MockFunctionCalledWithArgs') + .withArgs(42, '0x1234'); + expect(await ethers.provider.getBalance(this.target)).to.equal(value); + }); + + it('reverts when target reverts in default ExecType', async function () { + const value = 0x012; + const data = encodeSingle( + this.target, + value, + this.target.interface.encodeFunctionData('mockFunctionRevertsReason'), + ); + await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting'); + }); + + it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () { + const value = 0x012; + const data = encodeSingle( + this.target, + value, + this.target.interface.encodeFunctionData('mockFunctionRevertsReason'), + ); + + await expect(this.utils.$execSingle(EXEC_TYPE_TRY, data)) + .to.emit(this.utils, 'ERC7579TryExecuteFail') + .withArgs( + CALL_TYPE_CALL, + ethers.solidityPacked( + ['bytes4', 'bytes'], + [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])], + ), + ); + }); + + it('reverts with an invalid exec type', async function () { + const value = 0x012; + const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction')); + await expect(this.utils.$execSingle('0x03', data)) + .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType') + .withArgs('0x03'); + }); + }); + + describe('execBatch', function () { + it('calls the targets with value', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], + [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')], + ); + await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.anotherTarget, 'MockFunctionCalled'); + expect(await ethers.provider.getBalance(this.target)).to.equal(value1); + expect(await ethers.provider.getBalance(this.anotherTarget)).to.equal(value2); + }); + + it('calls the targets with value and args', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234'])], + [ + this.anotherTarget, + value2, + this.anotherTarget.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']), + ], + ); + await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data)) + .to.emit(this.target, 'MockFunctionCalledWithArgs') + .to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs'); + expect(await ethers.provider.getBalance(this.target)).to.equal(value1); + expect(await ethers.provider.getBalance(this.anotherTarget)).to.equal(value2); + }); + + it('reverts when any target reverts in default ExecType', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], + [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')], + ); + await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting'); + }); + + it('emits ERC7579TryExecuteFail event when any target reverts in try ExecType', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], + [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')], + ); + + await expect(this.utils.$execBatch(EXEC_TYPE_TRY, data)) + .to.emit(this.utils, 'ERC7579TryExecuteFail') + .withArgs( + CALL_TYPE_BATCH, + ethers.solidityPacked( + ['bytes4', 'bytes'], + [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])], + ), + ); + + // Check balances + expect(await ethers.provider.getBalance(this.target)).to.equal(value1); + expect(await ethers.provider.getBalance(this.anotherTarget)).to.equal(0); + }); + + it('reverts with an invalid exec type', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], + [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')], + ); + await expect(this.utils.$execBatch('0x03', data)) + .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType') + .withArgs('0x03'); + }); + }); + + describe('execDelegateCall', function () { + it('delegate calls the target', async function () { + const slot = ethers.hexlify(ethers.randomBytes(32)); + const value = ethers.hexlify(ethers.randomBytes(32)); + + const data = encodeDelegate( + this.target, + this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]), + ); + + expect(await ethers.provider.getStorage(this.utils.target, slot)).to.equal(ethers.ZeroHash); + await this.utils.$execDelegateCall(EXEC_TYPE_DEFAULT, data); + expect(await ethers.provider.getStorage(this.utils.target, slot)).to.equal(value); + }); + + it('reverts when target reverts in default ExecType', async function () { + const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')); + await expect(this.utils.$execDelegateCall(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith( + 'CallReceiverMock: reverting', + ); + }); + + it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () { + const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')); + await expect(this.utils.$execDelegateCall(EXEC_TYPE_TRY, data)) + .to.emit(this.utils, 'ERC7579TryExecuteFail') + .withArgs( + CALL_TYPE_CALL, + ethers.solidityPacked( + ['bytes4', 'bytes'], + [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])], + ), + ); + }); + + it('reverts with an invalid exec type', async function () { + const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunction')); + await expect(this.utils.$execDelegateCall('0x03', data)) + .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType') + .withArgs('0x03'); + }); + }); + + it('encodes Mode', async function () { + const callType = CALL_TYPE_BATCH; + const execType = EXEC_TYPE_TRY; + const selector = '0x12345678'; + const payload = ethers.toBeHex(0, 22); + + const mode = await this.utils.$encodeMode(callType, execType, selector, payload); + expect(mode).to.equal( + encodeMode({ + callType, + execType, + selector, + payload, + }), + ); + }); + + it('decodes Mode', async function () { + const mode = encodeMode({ + callType: CALL_TYPE_BATCH, + execType: EXEC_TYPE_TRY, + selector: '0x12345678', + payload: ethers.toBeHex(0, 22), + }); + + expect(await this.utils.$decodeMode(mode)).to.deep.eq([ + CALL_TYPE_BATCH, + EXEC_TYPE_TRY, + '0x12345678', + ethers.toBeHex(0, 22), + ]); + }); + + it('encodes single', async function () { + const target = this.target; + const value = 0x123; + const data = '0x12345678'; + + const encoded = await this.utils.$encodeSingle(target, value, data); + expect(encoded).to.equal(encodeSingle(target, value, data)); + }); + + it('decodes single', async function () { + const target = this.target; + const value = 0x123; + const data = '0x12345678'; + + const encoded = encodeSingle(target, value, data); + expect(await this.utils.$decodeSingle(encoded)).to.deep.eq([target.target, value, data]); + }); + + it('encodes batch', async function () { + const entries = [ + [this.target, 0x123, '0x12345678'], + [this.anotherTarget, 0x456, '0x12345678'], + ]; + + const encoded = await this.utils.$encodeBatch(entries); + expect(encoded).to.equal(encodeBatch(...entries)); + }); + + it('decodes batch', async function () { + const entries = [ + [this.target.target, 0x123, '0x12345678'], + [this.anotherTarget.target, 0x456, '0x12345678'], + ]; + + const encoded = encodeBatch(...entries); + expect(await this.utils.$decodeBatch(encoded)).to.deep.eq(entries); + }); + + it('encodes delegate', async function () { + const target = this.target; + const data = '0x12345678'; + + const encoded = await this.utils.$encodeDelegate(target, data); + expect(encoded).to.equal(encodeDelegate(target, data)); + }); + + it('decodes delegate', async function () { + const target = this.target; + const data = '0x12345678'; + + const encoded = encodeDelegate(target, data); + expect(await this.utils.$decodeDelegate(encoded)).to.deep.eq([target.target, data]); + }); + + describe('global', function () { + describe('eqCallTypeGlobal', function () { + it('returns true if both call types are equal', async function () { + const callType = CALL_TYPE_BATCH; + expect(await this.utilsGlobal.$eqCallTypeGlobal(callType, callType)).to.be.true; + }); + + it('returns false if both call types are different', async function () { + expect(await this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_CALL, CALL_TYPE_BATCH)).to.be.false; + }); + }); + + describe('eqExecTypeGlobal', function () { + it('returns true if both exec types are equal', async function () { + const execType = EXEC_TYPE_TRY; + expect(await this.utilsGlobal.$eqExecTypeGlobal(execType, execType)).to.be.true; + }); + + it('returns false if both exec types are different', async function () { + expect(await this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY)).to.be.false; + }); + }); + + describe('eqModeSelectorGlobal', function () { + it('returns true if both selectors are equal', async function () { + const selector = '0x12345678'; + expect(await this.utilsGlobal.$eqModeSelectorGlobal(selector, selector)).to.be.true; + }); + + it('returns false if both selectors are different', async function () { + expect(await this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x87654321')).to.be.false; + }); + }); + + describe('eqModePayloadGlobal', function () { + it('returns true if both payloads are equal', async function () { + const payload = ethers.toBeHex(0, 22); + expect(await this.utilsGlobal.$eqModePayloadGlobal(payload, payload)).to.be.true; + }); + + it('returns false if both payloads are different', async function () { + expect(await this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(1, 22))).to.be.false; + }); + }); + }); +}); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js new file mode 100644 index 00000000000..c529f5f64cf --- /dev/null +++ b/test/helpers/erc4337.js @@ -0,0 +1,82 @@ +const { ethers } = require('hardhat'); + +const SIG_VALIDATION_SUCCESS = 0; +const SIG_VALIDATION_FAILURE = 1; + +function toAuthorizer(sigValidatonSuccess) { + return `0x000000000000000000000000000000000000000${sigValidatonSuccess}`; +} + +function pack(left, right) { + return ethers.solidityPacked(['uint128', 'uint128'], [left, right]); +} + +function packValidationData(validAfter, validUntil, authorizer) { + return ethers.solidityPacked(['uint48', 'uint48', 'address'], [validAfter, validUntil, authorizer]); +} + +function packPaymasterData(paymaster, verificationGasLimit, postOpGasLimit) { + return ethers.solidityPacked(['address', 'uint128', 'uint128'], [paymaster, verificationGasLimit, postOpGasLimit]); +} + +/// 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, + }; + } + + hash(entrypoint, chainId) { + const p = this.packed; + const h = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'], + [ + p.sender, + 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, entrypoint, chainId]), + ); + } +} + +module.exports = { + SIG_VALIDATION_SUCCESS, + SIG_VALIDATION_FAILURE, + toAuthorizer, + packValidationData, + packPaymasterData, + UserOperation, +}; diff --git a/test/helpers/erc7579.js b/test/helpers/erc7579.js new file mode 100644 index 00000000000..6c3b4759b73 --- /dev/null +++ b/test/helpers/erc7579.js @@ -0,0 +1,58 @@ +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 EXEC_TYPE_DEFAULT = '0x00'; +const EXEC_TYPE_TRY = '0x01'; + +const CALL_TYPE_CALL = '0x00'; +const CALL_TYPE_BATCH = '0x01'; +const CALL_TYPE_DELEGATE = '0xff'; + +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'], + ), + ], + ); + +const encodeDelegate = (target, data = '0x') => + ethers.solidityPacked(['address', 'bytes'], [target.target ?? target.address ?? target, data]); + +module.exports = { + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK, + EXEC_TYPE_DEFAULT, + EXEC_TYPE_TRY, + CALL_TYPE_CALL, + CALL_TYPE_BATCH, + CALL_TYPE_DELEGATE, + encodeMode, + encodeSingle, + encodeBatch, + encodeDelegate, +}; diff --git a/test/utils/Packing.t.sol b/test/utils/Packing.t.sol index e8adda48920..40f052c80ff 100644 --- a/test/utils/Packing.t.sol +++ b/test/utils/Packing.t.sol @@ -9,287 +9,287 @@ import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; contract PackingTest is Test { using Packing for *; - function testPack(bytes1 left, bytes1 right) external { + function testPack(bytes1 left, bytes1 right) external pure { assertEq(left, Packing.pack_1_1(left, right).extract_2_1(0)); assertEq(right, Packing.pack_1_1(left, right).extract_2_1(1)); } - function testPack(bytes2 left, bytes2 right) external { + function testPack(bytes2 left, bytes2 right) external pure { assertEq(left, Packing.pack_2_2(left, right).extract_4_2(0)); assertEq(right, Packing.pack_2_2(left, right).extract_4_2(2)); } - function testPack(bytes2 left, bytes4 right) external { + function testPack(bytes2 left, bytes4 right) external pure { assertEq(left, Packing.pack_2_4(left, right).extract_6_2(0)); assertEq(right, Packing.pack_2_4(left, right).extract_6_4(2)); } - function testPack(bytes2 left, bytes6 right) external { + function testPack(bytes2 left, bytes6 right) external pure { assertEq(left, Packing.pack_2_6(left, right).extract_8_2(0)); assertEq(right, Packing.pack_2_6(left, right).extract_8_6(2)); } - function testPack(bytes2 left, bytes8 right) external { + function testPack(bytes2 left, bytes8 right) external pure { assertEq(left, Packing.pack_2_8(left, right).extract_10_2(0)); assertEq(right, Packing.pack_2_8(left, right).extract_10_8(2)); } - function testPack(bytes2 left, bytes10 right) external { + function testPack(bytes2 left, bytes10 right) external pure { assertEq(left, Packing.pack_2_10(left, right).extract_12_2(0)); assertEq(right, Packing.pack_2_10(left, right).extract_12_10(2)); } - function testPack(bytes2 left, bytes20 right) external { + function testPack(bytes2 left, bytes20 right) external pure { assertEq(left, Packing.pack_2_20(left, right).extract_22_2(0)); assertEq(right, Packing.pack_2_20(left, right).extract_22_20(2)); } - function testPack(bytes2 left, bytes22 right) external { + function testPack(bytes2 left, bytes22 right) external pure { assertEq(left, Packing.pack_2_22(left, right).extract_24_2(0)); assertEq(right, Packing.pack_2_22(left, right).extract_24_22(2)); } - function testPack(bytes4 left, bytes2 right) external { + function testPack(bytes4 left, bytes2 right) external pure { assertEq(left, Packing.pack_4_2(left, right).extract_6_4(0)); assertEq(right, Packing.pack_4_2(left, right).extract_6_2(4)); } - function testPack(bytes4 left, bytes4 right) external { + function testPack(bytes4 left, bytes4 right) external pure { assertEq(left, Packing.pack_4_4(left, right).extract_8_4(0)); assertEq(right, Packing.pack_4_4(left, right).extract_8_4(4)); } - function testPack(bytes4 left, bytes6 right) external { + function testPack(bytes4 left, bytes6 right) external pure { assertEq(left, Packing.pack_4_6(left, right).extract_10_4(0)); assertEq(right, Packing.pack_4_6(left, right).extract_10_6(4)); } - function testPack(bytes4 left, bytes8 right) external { + function testPack(bytes4 left, bytes8 right) external pure { assertEq(left, Packing.pack_4_8(left, right).extract_12_4(0)); assertEq(right, Packing.pack_4_8(left, right).extract_12_8(4)); } - function testPack(bytes4 left, bytes12 right) external { + function testPack(bytes4 left, bytes12 right) external pure { assertEq(left, Packing.pack_4_12(left, right).extract_16_4(0)); assertEq(right, Packing.pack_4_12(left, right).extract_16_12(4)); } - function testPack(bytes4 left, bytes16 right) external { + function testPack(bytes4 left, bytes16 right) external pure { assertEq(left, Packing.pack_4_16(left, right).extract_20_4(0)); assertEq(right, Packing.pack_4_16(left, right).extract_20_16(4)); } - function testPack(bytes4 left, bytes20 right) external { + function testPack(bytes4 left, bytes20 right) external pure { assertEq(left, Packing.pack_4_20(left, right).extract_24_4(0)); assertEq(right, Packing.pack_4_20(left, right).extract_24_20(4)); } - function testPack(bytes4 left, bytes24 right) external { + function testPack(bytes4 left, bytes24 right) external pure { assertEq(left, Packing.pack_4_24(left, right).extract_28_4(0)); assertEq(right, Packing.pack_4_24(left, right).extract_28_24(4)); } - function testPack(bytes4 left, bytes28 right) external { + function testPack(bytes4 left, bytes28 right) external pure { assertEq(left, Packing.pack_4_28(left, right).extract_32_4(0)); assertEq(right, Packing.pack_4_28(left, right).extract_32_28(4)); } - function testPack(bytes6 left, bytes2 right) external { + function testPack(bytes6 left, bytes2 right) external pure { assertEq(left, Packing.pack_6_2(left, right).extract_8_6(0)); assertEq(right, Packing.pack_6_2(left, right).extract_8_2(6)); } - function testPack(bytes6 left, bytes4 right) external { + function testPack(bytes6 left, bytes4 right) external pure { assertEq(left, Packing.pack_6_4(left, right).extract_10_6(0)); assertEq(right, Packing.pack_6_4(left, right).extract_10_4(6)); } - function testPack(bytes6 left, bytes6 right) external { + function testPack(bytes6 left, bytes6 right) external pure { assertEq(left, Packing.pack_6_6(left, right).extract_12_6(0)); assertEq(right, Packing.pack_6_6(left, right).extract_12_6(6)); } - function testPack(bytes6 left, bytes10 right) external { + function testPack(bytes6 left, bytes10 right) external pure { assertEq(left, Packing.pack_6_10(left, right).extract_16_6(0)); assertEq(right, Packing.pack_6_10(left, right).extract_16_10(6)); } - function testPack(bytes6 left, bytes16 right) external { + function testPack(bytes6 left, bytes16 right) external pure { assertEq(left, Packing.pack_6_16(left, right).extract_22_6(0)); assertEq(right, Packing.pack_6_16(left, right).extract_22_16(6)); } - function testPack(bytes6 left, bytes22 right) external { + function testPack(bytes6 left, bytes22 right) external pure { assertEq(left, Packing.pack_6_22(left, right).extract_28_6(0)); assertEq(right, Packing.pack_6_22(left, right).extract_28_22(6)); } - function testPack(bytes8 left, bytes2 right) external { + function testPack(bytes8 left, bytes2 right) external pure { assertEq(left, Packing.pack_8_2(left, right).extract_10_8(0)); assertEq(right, Packing.pack_8_2(left, right).extract_10_2(8)); } - function testPack(bytes8 left, bytes4 right) external { + function testPack(bytes8 left, bytes4 right) external pure { assertEq(left, Packing.pack_8_4(left, right).extract_12_8(0)); assertEq(right, Packing.pack_8_4(left, right).extract_12_4(8)); } - function testPack(bytes8 left, bytes8 right) external { + function testPack(bytes8 left, bytes8 right) external pure { assertEq(left, Packing.pack_8_8(left, right).extract_16_8(0)); assertEq(right, Packing.pack_8_8(left, right).extract_16_8(8)); } - function testPack(bytes8 left, bytes12 right) external { + function testPack(bytes8 left, bytes12 right) external pure { assertEq(left, Packing.pack_8_12(left, right).extract_20_8(0)); assertEq(right, Packing.pack_8_12(left, right).extract_20_12(8)); } - function testPack(bytes8 left, bytes16 right) external { + function testPack(bytes8 left, bytes16 right) external pure { assertEq(left, Packing.pack_8_16(left, right).extract_24_8(0)); assertEq(right, Packing.pack_8_16(left, right).extract_24_16(8)); } - function testPack(bytes8 left, bytes20 right) external { + function testPack(bytes8 left, bytes20 right) external pure { assertEq(left, Packing.pack_8_20(left, right).extract_28_8(0)); assertEq(right, Packing.pack_8_20(left, right).extract_28_20(8)); } - function testPack(bytes8 left, bytes24 right) external { + function testPack(bytes8 left, bytes24 right) external pure { assertEq(left, Packing.pack_8_24(left, right).extract_32_8(0)); assertEq(right, Packing.pack_8_24(left, right).extract_32_24(8)); } - function testPack(bytes10 left, bytes2 right) external { + function testPack(bytes10 left, bytes2 right) external pure { assertEq(left, Packing.pack_10_2(left, right).extract_12_10(0)); assertEq(right, Packing.pack_10_2(left, right).extract_12_2(10)); } - function testPack(bytes10 left, bytes6 right) external { + function testPack(bytes10 left, bytes6 right) external pure { assertEq(left, Packing.pack_10_6(left, right).extract_16_10(0)); assertEq(right, Packing.pack_10_6(left, right).extract_16_6(10)); } - function testPack(bytes10 left, bytes10 right) external { + function testPack(bytes10 left, bytes10 right) external pure { assertEq(left, Packing.pack_10_10(left, right).extract_20_10(0)); assertEq(right, Packing.pack_10_10(left, right).extract_20_10(10)); } - function testPack(bytes10 left, bytes12 right) external { + function testPack(bytes10 left, bytes12 right) external pure { assertEq(left, Packing.pack_10_12(left, right).extract_22_10(0)); assertEq(right, Packing.pack_10_12(left, right).extract_22_12(10)); } - function testPack(bytes10 left, bytes22 right) external { + function testPack(bytes10 left, bytes22 right) external pure { assertEq(left, Packing.pack_10_22(left, right).extract_32_10(0)); assertEq(right, Packing.pack_10_22(left, right).extract_32_22(10)); } - function testPack(bytes12 left, bytes4 right) external { + function testPack(bytes12 left, bytes4 right) external pure { assertEq(left, Packing.pack_12_4(left, right).extract_16_12(0)); assertEq(right, Packing.pack_12_4(left, right).extract_16_4(12)); } - function testPack(bytes12 left, bytes8 right) external { + function testPack(bytes12 left, bytes8 right) external pure { assertEq(left, Packing.pack_12_8(left, right).extract_20_12(0)); assertEq(right, Packing.pack_12_8(left, right).extract_20_8(12)); } - function testPack(bytes12 left, bytes10 right) external { + function testPack(bytes12 left, bytes10 right) external pure { assertEq(left, Packing.pack_12_10(left, right).extract_22_12(0)); assertEq(right, Packing.pack_12_10(left, right).extract_22_10(12)); } - function testPack(bytes12 left, bytes12 right) external { + function testPack(bytes12 left, bytes12 right) external pure { assertEq(left, Packing.pack_12_12(left, right).extract_24_12(0)); assertEq(right, Packing.pack_12_12(left, right).extract_24_12(12)); } - function testPack(bytes12 left, bytes16 right) external { + function testPack(bytes12 left, bytes16 right) external pure { assertEq(left, Packing.pack_12_16(left, right).extract_28_12(0)); assertEq(right, Packing.pack_12_16(left, right).extract_28_16(12)); } - function testPack(bytes12 left, bytes20 right) external { + function testPack(bytes12 left, bytes20 right) external pure { assertEq(left, Packing.pack_12_20(left, right).extract_32_12(0)); assertEq(right, Packing.pack_12_20(left, right).extract_32_20(12)); } - function testPack(bytes16 left, bytes4 right) external { + function testPack(bytes16 left, bytes4 right) external pure { assertEq(left, Packing.pack_16_4(left, right).extract_20_16(0)); assertEq(right, Packing.pack_16_4(left, right).extract_20_4(16)); } - function testPack(bytes16 left, bytes6 right) external { + function testPack(bytes16 left, bytes6 right) external pure { assertEq(left, Packing.pack_16_6(left, right).extract_22_16(0)); assertEq(right, Packing.pack_16_6(left, right).extract_22_6(16)); } - function testPack(bytes16 left, bytes8 right) external { + function testPack(bytes16 left, bytes8 right) external pure { assertEq(left, Packing.pack_16_8(left, right).extract_24_16(0)); assertEq(right, Packing.pack_16_8(left, right).extract_24_8(16)); } - function testPack(bytes16 left, bytes12 right) external { + function testPack(bytes16 left, bytes12 right) external pure { assertEq(left, Packing.pack_16_12(left, right).extract_28_16(0)); assertEq(right, Packing.pack_16_12(left, right).extract_28_12(16)); } - function testPack(bytes16 left, bytes16 right) external { + function testPack(bytes16 left, bytes16 right) external pure { assertEq(left, Packing.pack_16_16(left, right).extract_32_16(0)); assertEq(right, Packing.pack_16_16(left, right).extract_32_16(16)); } - function testPack(bytes20 left, bytes2 right) external { + function testPack(bytes20 left, bytes2 right) external pure { assertEq(left, Packing.pack_20_2(left, right).extract_22_20(0)); assertEq(right, Packing.pack_20_2(left, right).extract_22_2(20)); } - function testPack(bytes20 left, bytes4 right) external { + function testPack(bytes20 left, bytes4 right) external pure { assertEq(left, Packing.pack_20_4(left, right).extract_24_20(0)); assertEq(right, Packing.pack_20_4(left, right).extract_24_4(20)); } - function testPack(bytes20 left, bytes8 right) external { + function testPack(bytes20 left, bytes8 right) external pure { assertEq(left, Packing.pack_20_8(left, right).extract_28_20(0)); assertEq(right, Packing.pack_20_8(left, right).extract_28_8(20)); } - function testPack(bytes20 left, bytes12 right) external { + function testPack(bytes20 left, bytes12 right) external pure { assertEq(left, Packing.pack_20_12(left, right).extract_32_20(0)); assertEq(right, Packing.pack_20_12(left, right).extract_32_12(20)); } - function testPack(bytes22 left, bytes2 right) external { + function testPack(bytes22 left, bytes2 right) external pure { assertEq(left, Packing.pack_22_2(left, right).extract_24_22(0)); assertEq(right, Packing.pack_22_2(left, right).extract_24_2(22)); } - function testPack(bytes22 left, bytes6 right) external { + function testPack(bytes22 left, bytes6 right) external pure { assertEq(left, Packing.pack_22_6(left, right).extract_28_22(0)); assertEq(right, Packing.pack_22_6(left, right).extract_28_6(22)); } - function testPack(bytes22 left, bytes10 right) external { + function testPack(bytes22 left, bytes10 right) external pure { assertEq(left, Packing.pack_22_10(left, right).extract_32_22(0)); assertEq(right, Packing.pack_22_10(left, right).extract_32_10(22)); } - function testPack(bytes24 left, bytes4 right) external { + function testPack(bytes24 left, bytes4 right) external pure { assertEq(left, Packing.pack_24_4(left, right).extract_28_24(0)); assertEq(right, Packing.pack_24_4(left, right).extract_28_4(24)); } - function testPack(bytes24 left, bytes8 right) external { + function testPack(bytes24 left, bytes8 right) external pure { assertEq(left, Packing.pack_24_8(left, right).extract_32_24(0)); assertEq(right, Packing.pack_24_8(left, right).extract_32_8(24)); } - function testPack(bytes28 left, bytes4 right) external { + function testPack(bytes28 left, bytes4 right) external pure { assertEq(left, Packing.pack_28_4(left, right).extract_32_28(0)); assertEq(right, Packing.pack_28_4(left, right).extract_32_4(28)); } - function testReplace(bytes2 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes2 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 1)); bytes1 oldValue = container.extract_2_1(offset); @@ -298,7 +298,7 @@ contract PackingTest is Test { assertEq(container, container.replace_2_1(newValue, offset).replace_2_1(oldValue, offset)); } - function testReplace(bytes4 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes4 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 3)); bytes1 oldValue = container.extract_4_1(offset); @@ -307,7 +307,7 @@ contract PackingTest is Test { assertEq(container, container.replace_4_1(newValue, offset).replace_4_1(oldValue, offset)); } - function testReplace(bytes4 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes4 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes2 oldValue = container.extract_4_2(offset); @@ -316,7 +316,7 @@ contract PackingTest is Test { assertEq(container, container.replace_4_2(newValue, offset).replace_4_2(oldValue, offset)); } - function testReplace(bytes6 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes6 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 5)); bytes1 oldValue = container.extract_6_1(offset); @@ -325,7 +325,7 @@ contract PackingTest is Test { assertEq(container, container.replace_6_1(newValue, offset).replace_6_1(oldValue, offset)); } - function testReplace(bytes6 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes6 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes2 oldValue = container.extract_6_2(offset); @@ -334,7 +334,7 @@ contract PackingTest is Test { assertEq(container, container.replace_6_2(newValue, offset).replace_6_2(oldValue, offset)); } - function testReplace(bytes6 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes6 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes4 oldValue = container.extract_6_4(offset); @@ -343,7 +343,7 @@ contract PackingTest is Test { assertEq(container, container.replace_6_4(newValue, offset).replace_6_4(oldValue, offset)); } - function testReplace(bytes8 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes8 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 7)); bytes1 oldValue = container.extract_8_1(offset); @@ -352,7 +352,7 @@ contract PackingTest is Test { assertEq(container, container.replace_8_1(newValue, offset).replace_8_1(oldValue, offset)); } - function testReplace(bytes8 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes8 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 6)); bytes2 oldValue = container.extract_8_2(offset); @@ -361,7 +361,7 @@ contract PackingTest is Test { assertEq(container, container.replace_8_2(newValue, offset).replace_8_2(oldValue, offset)); } - function testReplace(bytes8 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes8 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes4 oldValue = container.extract_8_4(offset); @@ -370,7 +370,7 @@ contract PackingTest is Test { assertEq(container, container.replace_8_4(newValue, offset).replace_8_4(oldValue, offset)); } - function testReplace(bytes8 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes8 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes6 oldValue = container.extract_8_6(offset); @@ -379,7 +379,7 @@ contract PackingTest is Test { assertEq(container, container.replace_8_6(newValue, offset).replace_8_6(oldValue, offset)); } - function testReplace(bytes10 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes10 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 9)); bytes1 oldValue = container.extract_10_1(offset); @@ -388,7 +388,7 @@ contract PackingTest is Test { assertEq(container, container.replace_10_1(newValue, offset).replace_10_1(oldValue, offset)); } - function testReplace(bytes10 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes10 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes2 oldValue = container.extract_10_2(offset); @@ -397,7 +397,7 @@ contract PackingTest is Test { assertEq(container, container.replace_10_2(newValue, offset).replace_10_2(oldValue, offset)); } - function testReplace(bytes10 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes10 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 6)); bytes4 oldValue = container.extract_10_4(offset); @@ -406,7 +406,7 @@ contract PackingTest is Test { assertEq(container, container.replace_10_4(newValue, offset).replace_10_4(oldValue, offset)); } - function testReplace(bytes10 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes10 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes6 oldValue = container.extract_10_6(offset); @@ -415,7 +415,7 @@ contract PackingTest is Test { assertEq(container, container.replace_10_6(newValue, offset).replace_10_6(oldValue, offset)); } - function testReplace(bytes10 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes10 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes8 oldValue = container.extract_10_8(offset); @@ -424,7 +424,7 @@ contract PackingTest is Test { assertEq(container, container.replace_10_8(newValue, offset).replace_10_8(oldValue, offset)); } - function testReplace(bytes12 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 11)); bytes1 oldValue = container.extract_12_1(offset); @@ -433,7 +433,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_1(newValue, offset).replace_12_1(oldValue, offset)); } - function testReplace(bytes12 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 10)); bytes2 oldValue = container.extract_12_2(offset); @@ -442,7 +442,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_2(newValue, offset).replace_12_2(oldValue, offset)); } - function testReplace(bytes12 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes4 oldValue = container.extract_12_4(offset); @@ -451,7 +451,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_4(newValue, offset).replace_12_4(oldValue, offset)); } - function testReplace(bytes12 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 6)); bytes6 oldValue = container.extract_12_6(offset); @@ -460,7 +460,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_6(newValue, offset).replace_12_6(oldValue, offset)); } - function testReplace(bytes12 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes8 oldValue = container.extract_12_8(offset); @@ -469,7 +469,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_8(newValue, offset).replace_12_8(oldValue, offset)); } - function testReplace(bytes12 container, bytes10 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes10 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes10 oldValue = container.extract_12_10(offset); @@ -478,7 +478,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_10(newValue, offset).replace_12_10(oldValue, offset)); } - function testReplace(bytes16 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 15)); bytes1 oldValue = container.extract_16_1(offset); @@ -487,7 +487,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_1(newValue, offset).replace_16_1(oldValue, offset)); } - function testReplace(bytes16 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 14)); bytes2 oldValue = container.extract_16_2(offset); @@ -496,7 +496,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_2(newValue, offset).replace_16_2(oldValue, offset)); } - function testReplace(bytes16 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes4 oldValue = container.extract_16_4(offset); @@ -505,7 +505,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_4(newValue, offset).replace_16_4(oldValue, offset)); } - function testReplace(bytes16 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 10)); bytes6 oldValue = container.extract_16_6(offset); @@ -514,7 +514,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_6(newValue, offset).replace_16_6(oldValue, offset)); } - function testReplace(bytes16 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes8 oldValue = container.extract_16_8(offset); @@ -523,7 +523,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_8(newValue, offset).replace_16_8(oldValue, offset)); } - function testReplace(bytes16 container, bytes10 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes10 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 6)); bytes10 oldValue = container.extract_16_10(offset); @@ -532,7 +532,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_10(newValue, offset).replace_16_10(oldValue, offset)); } - function testReplace(bytes16 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes12 oldValue = container.extract_16_12(offset); @@ -541,7 +541,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_12(newValue, offset).replace_16_12(oldValue, offset)); } - function testReplace(bytes20 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 19)); bytes1 oldValue = container.extract_20_1(offset); @@ -550,7 +550,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_1(newValue, offset).replace_20_1(oldValue, offset)); } - function testReplace(bytes20 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 18)); bytes2 oldValue = container.extract_20_2(offset); @@ -559,7 +559,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_2(newValue, offset).replace_20_2(oldValue, offset)); } - function testReplace(bytes20 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 16)); bytes4 oldValue = container.extract_20_4(offset); @@ -568,7 +568,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_4(newValue, offset).replace_20_4(oldValue, offset)); } - function testReplace(bytes20 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 14)); bytes6 oldValue = container.extract_20_6(offset); @@ -577,7 +577,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_6(newValue, offset).replace_20_6(oldValue, offset)); } - function testReplace(bytes20 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes8 oldValue = container.extract_20_8(offset); @@ -586,7 +586,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_8(newValue, offset).replace_20_8(oldValue, offset)); } - function testReplace(bytes20 container, bytes10 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes10 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 10)); bytes10 oldValue = container.extract_20_10(offset); @@ -595,7 +595,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_10(newValue, offset).replace_20_10(oldValue, offset)); } - function testReplace(bytes20 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes12 oldValue = container.extract_20_12(offset); @@ -604,7 +604,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_12(newValue, offset).replace_20_12(oldValue, offset)); } - function testReplace(bytes20 container, bytes16 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes16 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes16 oldValue = container.extract_20_16(offset); @@ -613,7 +613,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_16(newValue, offset).replace_20_16(oldValue, offset)); } - function testReplace(bytes22 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 21)); bytes1 oldValue = container.extract_22_1(offset); @@ -622,7 +622,7 @@ contract PackingTest is Test { assertEq(container, container.replace_22_1(newValue, offset).replace_22_1(oldValue, offset)); } - function testReplace(bytes22 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 20)); bytes2 oldValue = container.extract_22_2(offset); @@ -631,7 +631,7 @@ contract PackingTest is Test { assertEq(container, container.replace_22_2(newValue, offset).replace_22_2(oldValue, offset)); } - function testReplace(bytes22 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 18)); bytes4 oldValue = container.extract_22_4(offset); @@ -640,7 +640,7 @@ contract PackingTest is Test { assertEq(container, container.replace_22_4(newValue, offset).replace_22_4(oldValue, offset)); } - function testReplace(bytes22 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 16)); bytes6 oldValue = container.extract_22_6(offset); @@ -649,7 +649,7 @@ contract PackingTest is Test { assertEq(container, container.replace_22_6(newValue, offset).replace_22_6(oldValue, offset)); } - function testReplace(bytes22 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 14)); bytes8 oldValue = container.extract_22_8(offset); @@ -658,7 +658,7 @@ contract PackingTest is Test { assertEq(container, container.replace_22_8(newValue, offset).replace_22_8(oldValue, offset)); } - function testReplace(bytes22 container, bytes10 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes10 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes10 oldValue = container.extract_22_10(offset); @@ -667,7 +667,7 @@ contract PackingTest is Test { assertEq(container, container.replace_22_10(newValue, offset).replace_22_10(oldValue, offset)); } - function testReplace(bytes22 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 10)); bytes12 oldValue = container.extract_22_12(offset); @@ -676,7 +676,7 @@ contract PackingTest is Test { assertEq(container, container.replace_22_12(newValue, offset).replace_22_12(oldValue, offset)); } - function testReplace(bytes22 container, bytes16 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes16 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 6)); bytes16 oldValue = container.extract_22_16(offset); @@ -685,7 +685,7 @@ contract PackingTest is Test { assertEq(container, container.replace_22_16(newValue, offset).replace_22_16(oldValue, offset)); } - function testReplace(bytes22 container, bytes20 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes20 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes20 oldValue = container.extract_22_20(offset); @@ -694,7 +694,7 @@ contract PackingTest is Test { assertEq(container, container.replace_22_20(newValue, offset).replace_22_20(oldValue, offset)); } - function testReplace(bytes24 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 23)); bytes1 oldValue = container.extract_24_1(offset); @@ -703,7 +703,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_1(newValue, offset).replace_24_1(oldValue, offset)); } - function testReplace(bytes24 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 22)); bytes2 oldValue = container.extract_24_2(offset); @@ -712,7 +712,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_2(newValue, offset).replace_24_2(oldValue, offset)); } - function testReplace(bytes24 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 20)); bytes4 oldValue = container.extract_24_4(offset); @@ -721,7 +721,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_4(newValue, offset).replace_24_4(oldValue, offset)); } - function testReplace(bytes24 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 18)); bytes6 oldValue = container.extract_24_6(offset); @@ -730,7 +730,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_6(newValue, offset).replace_24_6(oldValue, offset)); } - function testReplace(bytes24 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 16)); bytes8 oldValue = container.extract_24_8(offset); @@ -739,7 +739,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_8(newValue, offset).replace_24_8(oldValue, offset)); } - function testReplace(bytes24 container, bytes10 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes10 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 14)); bytes10 oldValue = container.extract_24_10(offset); @@ -748,7 +748,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_10(newValue, offset).replace_24_10(oldValue, offset)); } - function testReplace(bytes24 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes12 oldValue = container.extract_24_12(offset); @@ -757,7 +757,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_12(newValue, offset).replace_24_12(oldValue, offset)); } - function testReplace(bytes24 container, bytes16 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes16 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes16 oldValue = container.extract_24_16(offset); @@ -766,7 +766,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_16(newValue, offset).replace_24_16(oldValue, offset)); } - function testReplace(bytes24 container, bytes20 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes20 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes20 oldValue = container.extract_24_20(offset); @@ -775,7 +775,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_20(newValue, offset).replace_24_20(oldValue, offset)); } - function testReplace(bytes24 container, bytes22 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes22 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes22 oldValue = container.extract_24_22(offset); @@ -784,7 +784,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_22(newValue, offset).replace_24_22(oldValue, offset)); } - function testReplace(bytes28 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 27)); bytes1 oldValue = container.extract_28_1(offset); @@ -793,7 +793,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_1(newValue, offset).replace_28_1(oldValue, offset)); } - function testReplace(bytes28 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 26)); bytes2 oldValue = container.extract_28_2(offset); @@ -802,7 +802,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_2(newValue, offset).replace_28_2(oldValue, offset)); } - function testReplace(bytes28 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 24)); bytes4 oldValue = container.extract_28_4(offset); @@ -811,7 +811,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_4(newValue, offset).replace_28_4(oldValue, offset)); } - function testReplace(bytes28 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 22)); bytes6 oldValue = container.extract_28_6(offset); @@ -820,7 +820,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_6(newValue, offset).replace_28_6(oldValue, offset)); } - function testReplace(bytes28 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 20)); bytes8 oldValue = container.extract_28_8(offset); @@ -829,7 +829,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_8(newValue, offset).replace_28_8(oldValue, offset)); } - function testReplace(bytes28 container, bytes10 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes10 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 18)); bytes10 oldValue = container.extract_28_10(offset); @@ -838,7 +838,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_10(newValue, offset).replace_28_10(oldValue, offset)); } - function testReplace(bytes28 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 16)); bytes12 oldValue = container.extract_28_12(offset); @@ -847,7 +847,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_12(newValue, offset).replace_28_12(oldValue, offset)); } - function testReplace(bytes28 container, bytes16 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes16 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes16 oldValue = container.extract_28_16(offset); @@ -856,7 +856,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_16(newValue, offset).replace_28_16(oldValue, offset)); } - function testReplace(bytes28 container, bytes20 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes20 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes20 oldValue = container.extract_28_20(offset); @@ -865,7 +865,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_20(newValue, offset).replace_28_20(oldValue, offset)); } - function testReplace(bytes28 container, bytes22 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes22 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 6)); bytes22 oldValue = container.extract_28_22(offset); @@ -874,7 +874,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_22(newValue, offset).replace_28_22(oldValue, offset)); } - function testReplace(bytes28 container, bytes24 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes24 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes24 oldValue = container.extract_28_24(offset); @@ -883,7 +883,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_24(newValue, offset).replace_28_24(oldValue, offset)); } - function testReplace(bytes32 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 31)); bytes1 oldValue = container.extract_32_1(offset); @@ -892,7 +892,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_1(newValue, offset).replace_32_1(oldValue, offset)); } - function testReplace(bytes32 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 30)); bytes2 oldValue = container.extract_32_2(offset); @@ -901,7 +901,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_2(newValue, offset).replace_32_2(oldValue, offset)); } - function testReplace(bytes32 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 28)); bytes4 oldValue = container.extract_32_4(offset); @@ -910,7 +910,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_4(newValue, offset).replace_32_4(oldValue, offset)); } - function testReplace(bytes32 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 26)); bytes6 oldValue = container.extract_32_6(offset); @@ -919,7 +919,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_6(newValue, offset).replace_32_6(oldValue, offset)); } - function testReplace(bytes32 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 24)); bytes8 oldValue = container.extract_32_8(offset); @@ -928,7 +928,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_8(newValue, offset).replace_32_8(oldValue, offset)); } - function testReplace(bytes32 container, bytes10 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes10 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 22)); bytes10 oldValue = container.extract_32_10(offset); @@ -937,7 +937,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_10(newValue, offset).replace_32_10(oldValue, offset)); } - function testReplace(bytes32 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 20)); bytes12 oldValue = container.extract_32_12(offset); @@ -946,7 +946,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_12(newValue, offset).replace_32_12(oldValue, offset)); } - function testReplace(bytes32 container, bytes16 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes16 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 16)); bytes16 oldValue = container.extract_32_16(offset); @@ -955,7 +955,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_16(newValue, offset).replace_32_16(oldValue, offset)); } - function testReplace(bytes32 container, bytes20 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes20 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes20 oldValue = container.extract_32_20(offset); @@ -964,7 +964,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_20(newValue, offset).replace_32_20(oldValue, offset)); } - function testReplace(bytes32 container, bytes22 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes22 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 10)); bytes22 oldValue = container.extract_32_22(offset); @@ -973,7 +973,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_22(newValue, offset).replace_32_22(oldValue, offset)); } - function testReplace(bytes32 container, bytes24 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes24 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes24 oldValue = container.extract_32_24(offset); @@ -982,7 +982,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_24(newValue, offset).replace_32_24(oldValue, offset)); } - function testReplace(bytes32 container, bytes28 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes28 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes28 oldValue = container.extract_32_28(offset); From f297ccb99d49e04463d4fdcd9287d1133098457d Mon Sep 17 00:00:00 2001 From: Elias Rad <146735585+nnsW3@users.noreply.github.com> Date: Thu, 10 Oct 2024 07:23:01 +0300 Subject: [PATCH 06/13] Fix spelling issues in documentation (#5235) --- docs/modules/ROOT/pages/erc1155.adoc | 2 +- docs/modules/ROOT/pages/erc4626.adoc | 4 ++-- docs/modules/ROOT/pages/governance.adoc | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index 5bfb49accd4..7f00f3ea4b1 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -108,7 +108,7 @@ ERC1155InvalidReceiver("
") This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC-1155 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], worth multiple tens of thousands of dollars, and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error. -In order for our contract to receive ERC-1155 tokens we can inherit from the convenience contract xref:api:token/ERC1155.adoc#ERC1155Holder[`ERC1155Holder`] which handles the registering for us. Though we need to remember to implement functionality to allow tokens to be transferred out of our contract: +In order for our contract to receive ERC-1155 tokens we can inherit from the convenience contract xref:api:token/ERC1155.adoc#ERC1155Holder[`ERC1155Holder`] which handles the registering for us. However, we need to remember to implement functionality to allow tokens to be transferred out of our contract: [source,solidity] ---- diff --git a/docs/modules/ROOT/pages/erc4626.adoc b/docs/modules/ROOT/pages/erc4626.adoc index 79388c0a2e7..c219595dd5c 100644 --- a/docs/modules/ROOT/pages/erc4626.adoc +++ b/docs/modules/ROOT/pages/erc4626.adoc @@ -29,7 +29,7 @@ image::erc4626-rate-loglogext.png[More exchange rates in logarithmic scale] === The attack -When depositing tokens, the number of shares a user gets is rounded towards zero. This rounding takes away value from the user in favor of the vault (i.e. in favor of all the current share holders). This rounding is often negligible because of the amount at stake. If you deposit 1e9 shares worth of tokens, the rounding will have you lose at most 0.0000001% of your deposit. However if you deposit 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit <1 share worth of tokens, then you get 0 shares, and you basically made a donation. +When depositing tokens, the number of shares a user gets is rounded towards zero. This rounding takes away value from the user in favor of the vault (i.e. in favor of all the current shareholders). This rounding is often negligible because of the amount at stake. If you deposit 1e9 shares worth of tokens, the rounding will have you lose at most 0.0000001% of your deposit. However if you deposit 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit <1 share worth of tokens, then you get 0 shares, and you basically made a donation. For a given amount of assets, the more shares you receive the safer you are. If you want to limit your losses to at most 1%, you need to receive at least 100 shares. @@ -47,7 +47,7 @@ The idea of an inflation attack is that an attacker can donate assets to the vau image::erc4626-attack.png[Inflation attack without protection] -Figure 6 shows how an attacker can manipulate the rate of an empty vault. First the attacker must deposit a small amount of tokens (1 token) and follow up with a donation of 1e5 tokens directly to the vault to move the exchange rate "right". This puts the vault in a state where any deposit smaller than 1e5 would be completely lost to the vault. Given that the attacker is the only share holder (from their donation), the attacker would steal all the tokens deposited. +Figure 6 shows how an attacker can manipulate the rate of an empty vault. First the attacker must deposit a small amount of tokens (1 token) and follow up with a donation of 1e5 tokens directly to the vault to move the exchange rate "right". This puts the vault in a state where any deposit smaller than 1e5 would be completely lost to the vault. Given that the attacker is the only shareholder (from their donation), the attacker would steal all the tokens deposited. An attacker would typically wait for a user to do the first deposit into the vault, and would frontrun that operation with the attack described above. The risk is low, and the size of the "donation" required to manipulate the vault is equivalent to the size of the deposit that is being attacked. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 19f23d78d92..5f8e77555aa 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -74,7 +74,7 @@ votingPeriod: How long does a proposal remain open to votes. These parameters are specified in the unit defined in the token's clock. Assuming the token uses block numbers, and assuming block time of around 12 seconds, we will have set votingDelay = 1 day = 7200 blocks, and votingPeriod = 1 week = 50400 blocks. -We can optionally set a proposal threshold as well. This restricts proposal creation to accounts who have enough voting power. +We can optionally set a proposal threshold as well. This restricts proposal creation to accounts that have enough voting power. ```solidity include::api:example$governance/MyGovernor.sol[] From 0fccd1c6c20f8f135ec5873445cdbcf0e441249a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 14 Oct 2024 17:13:33 +0200 Subject: [PATCH 07/13] Add toUint, toInt and hexToUint to Strings (#5166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: cairo Co-authored-by: Ernesto García --- .changeset/eighty-hounds-promise.md | 5 + contracts/governance/Governor.sol | 82 +++----- contracts/utils/README.adoc | 2 +- contracts/utils/Strings.sol | 284 ++++++++++++++++++++++++++++ test/utils/Strings.t.sol | 27 +++ test/utils/Strings.test.js | 171 +++++++++++++++-- 6 files changed, 503 insertions(+), 68 deletions(-) create mode 100644 .changeset/eighty-hounds-promise.md create mode 100644 test/utils/Strings.t.sol diff --git a/.changeset/eighty-hounds-promise.md b/.changeset/eighty-hounds-promise.md new file mode 100644 index 00000000000..3727a6515f0 --- /dev/null +++ b/.changeset/eighty-hounds-promise.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Strings`: Add `parseUint`, `parseInt`, `parseHexUint` and `parseAddress` to parse strings into numbers and addresses. Also provide variants of these functions that parse substrings, and `tryXxx` variants that do not revert on invalid input. diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 02adffcb39b..f1851b30ee6 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -13,6 +13,7 @@ import {DoubleEndedQueue} from "../utils/structs/DoubleEndedQueue.sol"; import {Address} from "../utils/Address.sol"; import {Context} from "../utils/Context.sol"; import {Nonces} from "../utils/Nonces.sol"; +import {Strings} from "../utils/Strings.sol"; import {IGovernor, IERC6372} from "./IGovernor.sol"; /** @@ -760,67 +761,25 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 address proposer, string memory description ) internal view virtual returns (bool) { - uint256 len = bytes(description).length; - - // Length is too short to contain a valid proposer suffix - if (len < 52) { - return true; - } - - // Extract what would be the `#proposer=0x` marker beginning the suffix - bytes12 marker; - assembly ("memory-safe") { - // - Start of the string contents in memory = description + 32 - // - First character of the marker = len - 52 - // - Length of "#proposer=0x0000000000000000000000000000000000000000" = 52 - // - We read the memory word starting at the first character of the marker: - // - (description + 32) + (len - 52) = description + (len - 20) - // - Note: Solidity will ignore anything past the first 12 bytes - marker := mload(add(description, sub(len, 20))) - } - - // If the marker is not found, there is no proposer suffix to check - if (marker != bytes12("#proposer=0x")) { - return true; - } + unchecked { + uint256 length = bytes(description).length; - // Parse the 40 characters following the marker as uint160 - uint160 recovered = 0; - for (uint256 i = len - 40; i < len; ++i) { - (bool isHex, uint8 value) = _tryHexToUint(bytes(description)[i]); - // If any of the characters is not a hex digit, ignore the suffix entirely - if (!isHex) { + // Length is too short to contain a valid proposer suffix + if (length < 52) { return true; } - recovered = (recovered << 4) | value; - } - return recovered == uint160(proposer); - } + // Extract what would be the `#proposer=` marker beginning the suffix + bytes10 marker = bytes10(_unsafeReadBytesOffset(bytes(description), length - 52)); - /** - * @dev Try to parse a character from a string as a hex value. Returns `(true, value)` if the char is in - * `[0-9a-fA-F]` and `(false, 0)` otherwise. Value is guaranteed to be in the range `0 <= value < 16` - */ - function _tryHexToUint(bytes1 char) private pure returns (bool isHex, uint8 value) { - uint8 c = uint8(char); - unchecked { - // Case 0-9 - if (47 < c && c < 58) { - return (true, c - 48); - } - // Case A-F - else if (64 < c && c < 71) { - return (true, c - 55); - } - // Case a-f - else if (96 < c && c < 103) { - return (true, c - 87); - } - // Else: not a hex char - else { - return (false, 0); + // If the marker is not found, there is no proposer suffix to check + if (marker != bytes10("#proposer=")) { + return true; } + + // Check that the last 42 characters (after the marker) are a properly formatted address. + (bool success, address recovered) = Strings.tryParseAddress(description, length - 42, length); + return !success || recovered == proposer; } } @@ -849,4 +808,17 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 * @inheritdoc IGovernor */ function quorum(uint256 timepoint) public view virtual returns (uint256); + + /** + * @dev Reads a bytes32 from a bytes array without bounds checking. + * + * NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the + * assembly block as such would prevent some optimizations. + */ + function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) { + // This is not memory safe in the general case, but all calls to this private function are within bounds. + assembly ("memory-safe") { + value := mload(add(buffer, add(0x20, offset))) + } + } } diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 24b95b4e6f8..245c89c0486 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -34,7 +34,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Strings}: Common operations for strings formatting. * {ShortString}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters. * {SlotDerivation}: Methods for deriving storage slot from ERC-7201 namespaces as well as from constructions such as mapping and arrays. - * {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types. + * {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types. * {TransientSlot}: Primitives for reading from and writing to transient storage (only value types are currently supported). * {Multicall}: Abstract contract with a utility to allow batching together multiple calls in a single transaction. Useful for allowing EOAs to perform multiple operations at once. * {Context}: A utility for abstracting the sender and calldata in the current execution context. diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 5448060b70e..b72588646f7 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -4,12 +4,15 @@ pragma solidity ^0.8.20; import {Math} from "./math/Math.sol"; +import {SafeCast} from "./math/SafeCast.sol"; import {SignedMath} from "./math/SignedMath.sol"; /** * @dev String operations. */ library Strings { + using SafeCast for *; + bytes16 private constant HEX_DIGITS = "0123456789abcdef"; uint8 private constant ADDRESS_LENGTH = 20; @@ -18,6 +21,16 @@ library Strings { */ error StringsInsufficientHexLength(uint256 value, uint256 length); + /** + * @dev The string being parsed contains characters that are not in scope of the given base. + */ + error StringsInvalidChar(); + + /** + * @dev The string being parsed is not a properly formatted address. + */ + error StringsInvalidAddressFormat(); + /** * @dev Converts a `uint256` to its ASCII `string` decimal representation. */ @@ -113,4 +126,275 @@ library Strings { function equal(string memory a, string memory b) internal pure returns (bool) { return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); } + + /** + * @dev Parse a decimal string and returns the value as a `uint256`. + * + * Requirements: + * - The string must be formatted as `[0-9]*` + * - The result must fit into an `uint256` type + */ + function parseUint(string memory input) internal pure returns (uint256) { + return parseUint(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseUint} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `[0-9]*` + * - The result must fit into an `uint256` type + */ + function parseUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) { + (bool success, uint256 value) = tryParseUint(input, begin, end); + if (!success) revert StringsInvalidChar(); + return value; + } + + /** + * @dev Variant of {parseUint-string} that returns false if the parsing fails because of an invalid character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseUint(string memory input) internal pure returns (bool success, uint256 value) { + return tryParseUint(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseUint-string-uint256-uint256} that returns false if the parsing fails because of an invalid + * character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseUint( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, uint256 value) { + bytes memory buffer = bytes(input); + + uint256 result = 0; + for (uint256 i = begin; i < end; ++i) { + uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i))); + if (chr > 9) return (false, 0); + result *= 10; + result += chr; + } + return (true, result); + } + + /** + * @dev Parse a decimal string and returns the value as a `int256`. + * + * Requirements: + * - The string must be formatted as `[-+]?[0-9]*` + * - The result must fit in an `int256` type. + */ + function parseInt(string memory input) internal pure returns (int256) { + return parseInt(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseInt-string} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `[-+]?[0-9]*` + * - The result must fit in an `int256` type. + */ + function parseInt(string memory input, uint256 begin, uint256 end) internal pure returns (int256) { + (bool success, int256 value) = tryParseInt(input, begin, end); + if (!success) revert StringsInvalidChar(); + return value; + } + + /** + * @dev Variant of {parseInt-string} that returns false if the parsing fails because of an invalid character or if + * the result does not fit in a `int256`. + * + * NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`. + */ + function tryParseInt(string memory input) internal pure returns (bool success, int256 value) { + return tryParseInt(input, 0, bytes(input).length); + } + + uint256 private constant ABS_MIN_INT256 = 2 ** 255; + + /** + * @dev Variant of {parseInt-string-uint256-uint256} that returns false if the parsing fails because of an invalid + * character or if the result does not fit in a `int256`. + * + * NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`. + */ + function tryParseInt( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, int256 value) { + bytes memory buffer = bytes(input); + + // Check presence of a negative sign. + bytes1 sign = bytes1(_unsafeReadBytesOffset(buffer, begin)); + bool positiveSign = sign == bytes1("+"); + bool negativeSign = sign == bytes1("-"); + uint256 offset = (positiveSign || negativeSign).toUint(); + + (bool absSuccess, uint256 absValue) = tryParseUint(input, begin + offset, end); + + if (absSuccess && absValue < ABS_MIN_INT256) { + return (true, negativeSign ? -int256(absValue) : int256(absValue)); + } else if (absSuccess && negativeSign && absValue == ABS_MIN_INT256) { + return (true, type(int256).min); + } else return (false, 0); + } + + /** + * @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as a `uint256`. + * + * Requirements: + * - The string must be formatted as `(0x)?[0-9a-fA-F]*` + * - The result must fit in an `uint256` type. + */ + function parseHexUint(string memory input) internal pure returns (uint256) { + return parseHexUint(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseHexUint} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `(0x)?[0-9a-fA-F]*` + * - The result must fit in an `uint256` type. + */ + function parseHexUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) { + (bool success, uint256 value) = tryParseHexUint(input, begin, end); + if (!success) revert StringsInvalidChar(); + return value; + } + + /** + * @dev Variant of {parseHexUint-string} that returns false if the parsing fails because of an invalid character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseHexUint(string memory input) internal pure returns (bool success, uint256 value) { + return tryParseHexUint(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseHexUint-string-uint256-uint256} that returns false if the parsing fails because of an + * invalid character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseHexUint( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, uint256 value) { + bytes memory buffer = bytes(input); + + // skip 0x prefix if present + bool hasPrefix = bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2("0x"); + uint256 offset = hasPrefix.toUint() * 2; + + uint256 result = 0; + for (uint256 i = begin + offset; i < end; ++i) { + uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i))); + if (chr > 15) return (false, 0); + result *= 16; + unchecked { + // Multiplying by 16 is equivalent to a shift of 4 bits (with additional overflow check). + // This guaratees that adding a value < 16 will not cause an overflow, hence the unchecked. + result += chr; + } + } + return (true, result); + } + + /** + * @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as an `address`. + * + * Requirements: + * - The string must be formatted as `(0x)?[0-9a-fA-F]{40}` + */ + function parseAddress(string memory input) internal pure returns (address) { + return parseAddress(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseAddress} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `(0x)?[0-9a-fA-F]{40}` + */ + function parseAddress(string memory input, uint256 begin, uint256 end) internal pure returns (address) { + (bool success, address value) = tryParseAddress(input, begin, end); + if (!success) revert StringsInvalidAddressFormat(); + return value; + } + + /** + * @dev Variant of {parseAddress-string} that returns false if the parsing fails because the input is not a properly + * formatted address. See {parseAddress} requirements. + */ + function tryParseAddress(string memory input) internal pure returns (bool success, address value) { + return tryParseAddress(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseAddress-string-uint256-uint256} that returns false if the parsing fails because input is not a properly + * formatted address. See {parseAddress} requirements. + */ + function tryParseAddress( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, address value) { + // check that input is the correct length + bool hasPrefix = bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2("0x"); + uint256 expectedLength = 40 + hasPrefix.toUint() * 2; + + if (end - begin == expectedLength) { + // length guarantees that this does not overflow, and value is at most type(uint160).max + (bool s, uint256 v) = tryParseHexUint(input, begin, end); + return (s, address(uint160(v))); + } else { + return (false, address(0)); + } + } + + function _tryParseChr(bytes1 chr) private pure returns (uint8) { + uint8 value = uint8(chr); + + // Try to parse `chr`: + // - Case 1: [0-9] + // - Case 2: [a-f] + // - Case 3: [A-F] + // - otherwise not supported + unchecked { + if (value > 47 && value < 58) value -= 48; + else if (value > 96 && value < 103) value -= 87; + else if (value > 64 && value < 71) value -= 55; + else return type(uint8).max; + } + + return value; + } + + /** + * @dev Reads a bytes32 from a bytes array without bounds checking. + * + * NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the + * assembly block as such would prevent some optimizations. + */ + function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) { + // This is not memory safe in the general case, but all calls to this private function are within bounds. + assembly ("memory-safe") { + value := mload(add(buffer, add(0x20, offset))) + } + } } diff --git a/test/utils/Strings.t.sol b/test/utils/Strings.t.sol new file mode 100644 index 00000000000..b3eb67a5cd2 --- /dev/null +++ b/test/utils/Strings.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +contract StringsTest is Test { + using Strings for *; + + function testParse(uint256 value) external { + assertEq(value, value.toString().parseUint()); + } + + function testParseSigned(int256 value) external { + assertEq(value, value.toStringSigned().parseInt()); + } + + function testParseHex(uint256 value) external { + assertEq(value, value.toHexString().parseHexUint()); + } + + function testParseChecksumHex(address value) external { + assertEq(value, value.toChecksumHexString().parseAddress()); + } +} diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 6353fd886db..5a47d4d10de 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -1,6 +1,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); async function fixture() { const mock = await ethers.deployContract('$Strings'); @@ -38,11 +39,15 @@ describe('Strings', function () { it('converts MAX_UINT256', async function () { const value = ethers.MaxUint256; expect(await this.mock.$toString(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseUint(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseUint(value.toString(10))).to.deep.equal([true, value]); }); for (const value of values) { it(`converts ${value}`, async function () { - expect(await this.mock.$toString(value)).to.equal(value); + expect(await this.mock.$toString(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseUint(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseUint(value.toString(10))).to.deep.equal([true, value]); }); } }); @@ -51,21 +56,29 @@ describe('Strings', function () { it('converts MAX_INT256', async function () { const value = ethers.MaxInt256; expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseInt(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseInt(value.toString(10))).to.deep.equal([true, value]); }); it('converts MIN_INT256', async function () { const value = ethers.MinInt256; expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseInt(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseInt(value.toString(10))).to.deep.equal([true, value]); }); for (const value of values) { it(`convert ${value}`, async function () { - expect(await this.mock.$toStringSigned(value)).to.equal(value); + expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseInt(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseInt(value.toString(10))).to.deep.equal([true, value]); }); it(`convert negative ${value}`, async function () { const negated = -value; expect(await this.mock.$toStringSigned(negated)).to.equal(negated.toString(10)); + expect(await this.mock.$parseInt(negated.toString(10))).to.equal(negated); + expect(await this.mock.$tryParseInt(negated.toString(10))).to.deep.equal([true, negated]); }); } }); @@ -73,17 +86,36 @@ describe('Strings', function () { describe('toHexString', function () { it('converts 0', async function () { - expect(await this.mock.getFunction('$toHexString(uint256)')(0n)).to.equal('0x00'); + const value = 0n; + const string = ethers.toBeHex(value); // 0x00 + + expect(await this.mock.getFunction('$toHexString(uint256)')(value)).to.equal(string); + expect(await this.mock.$parseHexUint(string)).to.equal(value); + expect(await this.mock.$parseHexUint(string.replace(/0x/, ''))).to.equal(value); + expect(await this.mock.$tryParseHexUint(string)).to.deep.equal([true, value]); + expect(await this.mock.$tryParseHexUint(string.replace(/0x/, ''))).to.deep.equal([true, value]); }); it('converts a positive number', async function () { - expect(await this.mock.getFunction('$toHexString(uint256)')(0x4132n)).to.equal('0x4132'); + const value = 0x4132n; + const string = ethers.toBeHex(value); + + expect(await this.mock.getFunction('$toHexString(uint256)')(value)).to.equal(string); + expect(await this.mock.$parseHexUint(string)).to.equal(value); + expect(await this.mock.$parseHexUint(string.replace(/0x/, ''))).to.equal(value); + expect(await this.mock.$tryParseHexUint(string)).to.deep.equal([true, value]); + expect(await this.mock.$tryParseHexUint(string.replace(/0x/, ''))).to.deep.equal([true, value]); }); it('converts MAX_UINT256', async function () { - expect(await this.mock.getFunction('$toHexString(uint256)')(ethers.MaxUint256)).to.equal( - `0x${ethers.MaxUint256.toString(16)}`, - ); + const value = ethers.MaxUint256; + const string = ethers.toBeHex(value); + + expect(await this.mock.getFunction('$toHexString(uint256)')(value)).to.equal(string); + expect(await this.mock.$parseHexUint(string)).to.equal(value); + expect(await this.mock.$parseHexUint(string.replace(/0x/, ''))).to.equal(value); + expect(await this.mock.$tryParseHexUint(string)).to.deep.equal([true, value]); + expect(await this.mock.$tryParseHexUint(string.replace(/0x/, ''))).to.deep.equal([true, value]); }); }); @@ -97,13 +129,13 @@ describe('Strings', function () { it('converts a positive number (short)', async function () { const length = 1n; await expect(this.mock.getFunction('$toHexString(uint256,uint256)')(0x4132n, length)) - .to.be.revertedWithCustomError(this.mock, `StringsInsufficientHexLength`) + .to.be.revertedWithCustomError(this.mock, 'StringsInsufficientHexLength') .withArgs(0x4132, length); }); it('converts MAX_UINT256', async function () { expect(await this.mock.getFunction('$toHexString(uint256,uint256)')(ethers.MaxUint256, 32n)).to.equal( - `0x${ethers.MaxUint256.toString(16)}`, + ethers.toBeHex(ethers.MaxUint256), ); }); }); @@ -139,9 +171,16 @@ describe('Strings', function () { describe('toChecksumHexString', function () { for (const addr of addresses) { it(`converts ${addr}`, async function () { - expect(await this.mock.getFunction('$toChecksumHexString(address)')(addr)).to.equal( - ethers.getAddress(addr.toLowerCase()), - ); + expect(await this.mock.$toChecksumHexString(addr)).to.equal(ethers.getAddress(addr)); + }); + } + }); + + describe('parseAddress', function () { + for (const addr of addresses) { + it(`converts ${addr}`, async function () { + expect(await this.mock.$parseAddress(addr)).to.equal(ethers.getAddress(addr)); + expect(await this.mock.$tryParseAddress(addr)).to.deep.equal([true, ethers.getAddress(addr)]); }); } }); @@ -177,4 +216,112 @@ describe('Strings', function () { expect(await this.mock.$equal(str1, str2)).to.be.true; }); }); + + describe('Edge cases: invalid parsing', function () { + it('parseUint overflow', async function () { + await expect(this.mock.$parseUint((ethers.MaxUint256 + 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$tryParseUint((ethers.MaxUint256 + 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + }); + + it('parseUint invalid character', async function () { + await expect(this.mock.$parseUint('0x1')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseUint('1f')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseUint('-10')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseUint('1.0')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseUint('1 000')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + expect(await this.mock.$tryParseUint('0x1')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseUint('1f')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseUint('-10')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseUint('1.0')).deep.equal([false, 0n]); + expect(await this.mock.$tryParseUint('1 000')).deep.equal([false, 0n]); + }); + + it('parseInt overflow', async function () { + await expect(this.mock.$parseInt((ethers.MaxUint256 + 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$parseInt((-ethers.MaxUint256 - 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$tryParseInt((ethers.MaxUint256 + 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$tryParseInt((-ethers.MaxUint256 - 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$parseInt((ethers.MaxInt256 + 1n).toString(10))).to.be.revertedWithCustomError( + this.mock, + 'StringsInvalidChar', + ); + await expect(this.mock.$parseInt((ethers.MinInt256 - 1n).toString(10))).to.be.revertedWithCustomError( + this.mock, + 'StringsInvalidChar', + ); + expect(await this.mock.$tryParseInt((ethers.MaxInt256 + 1n).toString(10))).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseInt((ethers.MinInt256 - 1n).toString(10))).to.deep.equal([false, 0n]); + }); + + it('parseInt invalid character', async function () { + await expect(this.mock.$parseInt('0x1')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseInt('1f')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseInt('1.0')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseInt('1 000')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + expect(await this.mock.$tryParseInt('0x1')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseInt('1f')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseInt('1.0')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseInt('1 000')).to.deep.equal([false, 0n]); + }); + + it('parseHexUint overflow', async function () { + await expect(this.mock.$parseHexUint((ethers.MaxUint256 + 1n).toString(16))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$tryParseHexUint((ethers.MaxUint256 + 1n).toString(16))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + }); + + it('parseHexUint invalid character', async function () { + await expect(this.mock.$parseHexUint('0123456789abcdefg')).to.be.revertedWithCustomError( + this.mock, + 'StringsInvalidChar', + ); + await expect(this.mock.$parseHexUint('-1')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseHexUint('-f')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseHexUint('-0xf')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseHexUint('1.0')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseHexUint('1 000')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + expect(await this.mock.$tryParseHexUint('0123456789abcdefg')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('-1')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('-f')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('-0xf')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('1.0')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('1 000')).to.deep.equal([false, 0n]); + }); + + it('parseAddress invalid format', async function () { + for (const addr of [ + '0x736a507fB2881d6bB62dcA54673CF5295dC07833', // valid + '0x736a507fB2881d6-B62dcA54673CF5295dC07833', // invalid char + '0x0736a507fB2881d6bB62dcA54673CF5295dC07833', // tooLong + '0x36a507fB2881d6bB62dcA54673CF5295dC07833', // tooShort + '736a507fB2881d6bB62dcA54673CF5295dC07833', // missingPrefix - supported + ]) { + if (ethers.isAddress(addr)) { + expect(await this.mock.$parseAddress(addr)).to.equal(ethers.getAddress(addr)); + expect(await this.mock.$tryParseAddress(addr)).to.deep.equal([true, ethers.getAddress(addr)]); + } else { + await expect(this.mock.$parseAddress(addr)).to.be.revertedWithCustomError( + this.mock, + 'StringsInvalidAddressFormat', + ); + expect(await this.mock.$tryParseAddress(addr)).to.deep.equal([false, ethers.ZeroAddress]); + } + } + }); + }); }); From 62c0eb2c7d89048e4dce8b821b85f7b9ef9d91dc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 14 Oct 2024 22:41:08 +0200 Subject: [PATCH 08/13] Bytes library and CAIP2/CAIP10 helpers (#5252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: cairo Co-authored-by: Ernesto García Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com> --- .changeset/healthy-books-shout.md | 5 ++ .changeset/proud-planes-arrive.md | 5 ++ contracts/mocks/Stateless.sol | 3 + contracts/utils/Bytes.sol | 115 ++++++++++++++++++++++++++++++ contracts/utils/CAIP10.sol | 49 +++++++++++++ contracts/utils/CAIP2.sol | 47 ++++++++++++ contracts/utils/README.adoc | 2 + test/helpers/chains.js | 109 ++++++++++++++++++++++++++++ test/utils/Bytes.test.js | 88 +++++++++++++++++++++++ test/utils/CAIP.test.js | 53 ++++++++++++++ 10 files changed, 476 insertions(+) create mode 100644 .changeset/healthy-books-shout.md create mode 100644 .changeset/proud-planes-arrive.md create mode 100644 contracts/utils/Bytes.sol create mode 100644 contracts/utils/CAIP10.sol create mode 100644 contracts/utils/CAIP2.sol create mode 100644 test/helpers/chains.js create mode 100644 test/utils/Bytes.test.js create mode 100644 test/utils/CAIP.test.js diff --git a/.changeset/healthy-books-shout.md b/.changeset/healthy-books-shout.md new file mode 100644 index 00000000000..274e7a48868 --- /dev/null +++ b/.changeset/healthy-books-shout.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`CAIP2` and `CAIP10`: Add libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers. diff --git a/.changeset/proud-planes-arrive.md b/.changeset/proud-planes-arrive.md new file mode 100644 index 00000000000..6408976414d --- /dev/null +++ b/.changeset/proud-planes-arrive.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add a library of common operation that operate on `bytes` objects. diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 97924bc7d50..15a6ee64dd4 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -9,6 +9,9 @@ import {Arrays} from "../utils/Arrays.sol"; import {AuthorityUtils} from "../access/manager/AuthorityUtils.sol"; import {Base64} from "../utils/Base64.sol"; import {BitMaps} from "../utils/structs/BitMaps.sol"; +import {Bytes} from "../utils/Bytes.sol"; +import {CAIP2} from "../utils/CAIP2.sol"; +import {CAIP10} from "../utils/CAIP10.sol"; import {Checkpoints} from "../utils/structs/Checkpoints.sol"; import {CircularBuffer} from "../utils/structs/CircularBuffer.sol"; import {Clones} from "../proxy/Clones.sol"; diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol new file mode 100644 index 00000000000..84e5a3ed51f --- /dev/null +++ b/contracts/utils/Bytes.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Math} from "./math/Math.sol"; + +/** + * @dev Bytes operations. + */ +library Bytes { + /** + * @dev Forward search for `s` in `buffer` + * * If `s` is present in the buffer, returns the index of the first instance + * * If `s` is not present in the buffer, returns type(uint256).max + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf[Javascript's `Array.indexOf`] + */ + function indexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) { + return indexOf(buffer, s, 0); + } + + /** + * @dev Forward search for `s` in `buffer` starting at position `pos` + * * If `s` is present in the buffer (at or after `pos`), returns the index of the next instance + * * If `s` is not present in the buffer (at or after `pos`), returns type(uint256).max + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf[Javascript's `Array.indexOf`] + */ + function indexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) { + unchecked { + uint256 length = buffer.length; + for (uint256 i = pos; i < length; ++i) { + if (bytes1(_unsafeReadBytesOffset(buffer, i)) == s) { + return i; + } + } + return type(uint256).max; + } + } + + /** + * @dev Backward search for `s` in `buffer` + * * If `s` is present in the buffer, returns the index of the last instance + * * If `s` is not present in the buffer, returns type(uint256).max + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf[Javascript's `Array.lastIndexOf`] + */ + function lastIndexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) { + return lastIndexOf(buffer, s, type(uint256).max); + } + + /** + * @dev Backward search for `s` in `buffer` starting at position `pos` + * * If `s` is present in the buffer (at or before `pos`), returns the index of the previous instance + * * If `s` is not present in the buffer (at or before `pos`), returns type(uint256).max + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf[Javascript's `Array.lastIndexOf`] + */ + function lastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) { + unchecked { + uint256 length = buffer.length; + // NOTE here we cannot do `i = Math.min(pos + 1, length)` because `pos + 1` could overflow + for (uint256 i = Math.min(pos, length - 1) + 1; i > 0; --i) { + if (bytes1(_unsafeReadBytesOffset(buffer, i - 1)) == s) { + return i - 1; + } + } + return type(uint256).max; + } + } + + /** + * @dev Copies the content of `buffer`, from `start` (included) to the end of `buffer` into a new bytes object in + * memory. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(bytes memory buffer, uint256 start) internal pure returns (bytes memory) { + return slice(buffer, start, buffer.length); + } + + /** + * @dev Copies the content of `buffer`, from `start` (included) to `end` (excluded) into a new bytes object in + * memory. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(bytes memory buffer, uint256 start, uint256 end) internal pure returns (bytes memory) { + // sanitize + uint256 length = buffer.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // allocate and copy + bytes memory result = new bytes(end - start); + assembly ("memory-safe") { + mcopy(add(result, 0x20), add(buffer, add(start, 0x20)), sub(end, start)) + } + + return result; + } + + /** + * @dev Reads a bytes32 from a bytes array without bounds checking. + * + * NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the + * assembly block as such would prevent some optimizations. + */ + function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) { + // This is not memory safe in the general case, but all calls to this private function are within bounds. + assembly ("memory-safe") { + value := mload(add(buffer, add(0x20, offset))) + } + } +} diff --git a/contracts/utils/CAIP10.sol b/contracts/utils/CAIP10.sol new file mode 100644 index 00000000000..e9ed17305b6 --- /dev/null +++ b/contracts/utils/CAIP10.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {SafeCast} from "./math/SafeCast.sol"; +import {Bytes} from "./Bytes.sol"; +import {CAIP2} from "./CAIP2.sol"; +import {Strings} from "./Strings.sol"; + +/** + * @dev Helper library to format and parse CAIP-10 identifiers + * + * https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md[CAIP-10] defines account identifiers as: + * account_id: chain_id + ":" + account_address + * chain_id: [-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32} (See {CAIP2}) + * account_address: [-.%a-zA-Z0-9]{1,128} + */ +library CAIP10 { + using SafeCast for uint256; + using Strings for address; + using Bytes for bytes; + + /// @dev Return the CAIP-10 identifier for an account on the current (local) chain. + function local(address account) internal view returns (string memory) { + return format(CAIP2.local(), account.toChecksumHexString()); + } + + /** + * @dev Return the CAIP-10 identifier for a given caip2 chain and account. + * + * NOTE: This function does not verify that the inputs are properly formatted. + */ + function format(string memory caip2, string memory account) internal pure returns (string memory) { + return string.concat(caip2, ":", account); + } + + /** + * @dev Parse a CAIP-10 identifier into its components. + * + * NOTE: This function does not verify that the CAIP-10 input is properly formatted. The `caip2` return can be + * parsed using the {CAIP2} library. + */ + function parse(string memory caip10) internal pure returns (string memory caip2, string memory account) { + bytes memory buffer = bytes(caip10); + + uint256 pos = buffer.lastIndexOf(":"); + return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1))); + } +} diff --git a/contracts/utils/CAIP2.sol b/contracts/utils/CAIP2.sol new file mode 100644 index 00000000000..13a98f58a46 --- /dev/null +++ b/contracts/utils/CAIP2.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {SafeCast} from "./math/SafeCast.sol"; +import {Bytes} from "./Bytes.sol"; +import {Strings} from "./Strings.sol"; + +/** + * @dev Helper library to format and parse CAIP-2 identifiers + * + * https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md[CAIP-2] defines chain identifiers as: + * chain_id: namespace + ":" + reference + * namespace: [-a-z0-9]{3,8} + * reference: [-_a-zA-Z0-9]{1,32} + */ +library CAIP2 { + using SafeCast for uint256; + using Strings for uint256; + using Bytes for bytes; + + /// @dev Return the CAIP-2 identifier for the current (local) chain. + function local() internal view returns (string memory) { + return format("eip155", block.chainid.toString()); + } + + /** + * @dev Return the CAIP-2 identifier for a given namespace and reference. + * + * NOTE: This function does not verify that the inputs are properly formatted. + */ + function format(string memory namespace, string memory ref) internal pure returns (string memory) { + return string.concat(namespace, ":", ref); + } + + /** + * @dev Parse a CAIP-2 identifier into its components. + * + * NOTE: This function does not verify that the CAIP-2 input is properly formatted. + */ + function parse(string memory caip2) internal pure returns (string memory namespace, string memory ref) { + bytes memory buffer = bytes(caip2); + + uint256 pos = buffer.indexOf(":"); + return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1))); + } +} diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 245c89c0486..eeef84aae7c 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -31,6 +31,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type. * {Arrays}: Collection of functions that operate on https://docs.soliditylang.org/en/latest/types.html#arrays[`arrays`]. * {Base64}: On-chain base64 and base64URL encoding according to https://datatracker.ietf.org/doc/html/rfc4648[RFC-4648]. + * {Bytes}: Common operations on bytes objects. * {Strings}: Common operations for strings formatting. * {ShortString}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters. * {SlotDerivation}: Methods for deriving storage slot from ERC-7201 namespaces as well as from constructions such as mapping and arrays. @@ -41,6 +42,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Packing}: A library for packing and unpacking multiple values into bytes32 * {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes]. * {Comparators}: A library that contains comparator functions to use with with the {Heap} library. + * {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers. [NOTE] ==== diff --git a/test/helpers/chains.js b/test/helpers/chains.js new file mode 100644 index 00000000000..3711a81258e --- /dev/null +++ b/test/helpers/chains.js @@ -0,0 +1,109 @@ +// NOTE: this file defines some examples of CAIP-2 and CAIP-10 identifiers. +// The following listing does not pretend to be exhaustive or even accurate. It SHOULD NOT be used in production. + +const { ethers } = require('hardhat'); +const { mapValues } = require('./iterate'); + +// EVM (https://axelarscan.io/resources/chains?type=evm) +const ethereum = { + Ethereum: '1', + optimism: '10', + binance: '56', + Polygon: '137', + Fantom: '250', + fraxtal: '252', + filecoin: '314', + Moonbeam: '1284', + centrifuge: '2031', + kava: '2222', + mantle: '5000', + base: '8453', + immutable: '13371', + arbitrum: '42161', + celo: '42220', + Avalanche: '43114', + linea: '59144', + blast: '81457', + scroll: '534352', + aurora: '1313161554', +}; + +// Cosmos (https://axelarscan.io/resources/chains?type=cosmos) +const cosmos = { + Axelarnet: 'axelar-dojo-1', + osmosis: 'osmosis-1', + cosmoshub: 'cosmoshub-4', + juno: 'juno-1', + 'e-money': 'emoney-3', + injective: 'injective-1', + crescent: 'crescent-1', + kujira: 'kaiyo-1', + 'secret-snip': 'secret-4', + secret: 'secret-4', + sei: 'pacific-1', + stargaze: 'stargaze-1', + assetmantle: 'mantle-1', + fetch: 'fetchhub-4', + ki: 'kichain-2', + evmos: 'evmos_9001-2', + aura: 'xstaxy-1', + comdex: 'comdex-1', + persistence: 'core-1', + regen: 'regen-1', + umee: 'umee-1', + agoric: 'agoric-3', + xpla: 'dimension_37-1', + acre: 'acre_9052-1', + stride: 'stride-1', + carbon: 'carbon-1', + sommelier: 'sommelier-3', + neutron: 'neutron-1', + rebus: 'reb_1111-1', + archway: 'archway-1', + provenance: 'pio-mainnet-1', + ixo: 'ixo-5', + migaloo: 'migaloo-1', + teritori: 'teritori-1', + haqq: 'haqq_11235-1', + celestia: 'celestia', + ojo: 'agamotto', + chihuahua: 'chihuahua-1', + saga: 'ssc-1', + dymension: 'dymension_1100-1', + fxcore: 'fxcore', + c4e: 'perun-1', + bitsong: 'bitsong-2b', + nolus: 'pirin-1', + lava: 'lava-mainnet-1', + 'terra-2': 'phoenix-1', + terra: 'columbus-5', +}; + +const makeCAIP = ({ namespace, reference, account }) => ({ + namespace, + reference, + account, + caip2: `${namespace}:${reference}`, + caip10: `${namespace}:${reference}:${account}`, + toCaip10: other => `${namespace}:${reference}:${ethers.getAddress(other.target ?? other.address ?? other)}`, +}); + +module.exports = { + CHAINS: mapValues( + Object.assign( + mapValues(ethereum, reference => ({ + namespace: 'eip155', + reference, + account: ethers.Wallet.createRandom().address, + })), + mapValues(cosmos, reference => ({ + namespace: 'cosmos', + reference, + account: ethers.encodeBase58(ethers.randomBytes(32)), + })), + ), + makeCAIP, + ), + getLocalCAIP: account => + ethers.provider.getNetwork().then(({ chainId }) => makeCAIP({ namespace: 'eip155', reference: chainId, account })), +}; diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js new file mode 100644 index 00000000000..52a1ae95e77 --- /dev/null +++ b/test/utils/Bytes.test.js @@ -0,0 +1,88 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const mock = await ethers.deployContract('$Bytes'); + return { mock }; +} + +const lorem = ethers.toUtf8Bytes( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', +); +const present = lorem.at(1); +const absent = 255; + +describe('Bytes', function () { + before(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('indexOf', function () { + it('first', async function () { + expect(await this.mock.$indexOf(lorem, ethers.toBeHex(present))).to.equal(lorem.indexOf(present)); + }); + + it('from index', async function () { + for (const start in Array(lorem.length + 10).fill()) { + const index = lorem.indexOf(present, start); + const result = index === -1 ? ethers.MaxUint256 : index; + expect(await this.mock.$indexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start))).to.equal(result); + } + }); + + it('absent', async function () { + expect(await this.mock.$indexOf(lorem, ethers.toBeHex(absent))).to.equal(ethers.MaxUint256); + }); + }); + + describe('lastIndexOf', function () { + it('first', async function () { + expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(present))).to.equal(lorem.lastIndexOf(present)); + }); + + it('from index', async function () { + for (const start in Array(lorem.length + 10).fill()) { + const index = lorem.lastIndexOf(present, start); + const result = index === -1 ? ethers.MaxUint256 : index; + expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start))).to.equal( + result, + ); + } + }); + + it('absent', async function () { + expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(absent))).to.equal(ethers.MaxUint256); + }); + }); + + describe('slice', function () { + describe('slice(bytes, uint256)', function () { + for (const [descr, start] of Object.entries({ + 'start = 0': 0, + 'start within bound': 10, + 'start out of bound': 1000, + })) { + it(descr, async function () { + const result = ethers.hexlify(lorem.slice(start)); + expect(await this.mock.$slice(lorem, start)).to.equal(result); + }); + } + }); + + describe('slice(bytes, uint256, uint256)', function () { + for (const [descr, [start, end]] of Object.entries({ + 'start = 0': [0, 42], + 'start and end within bound': [17, 42], + 'end out of bound': [42, 1000], + 'start = end': [17, 17], + 'start > end': [42, 17], + })) { + it(descr, async function () { + const result = ethers.hexlify(lorem.slice(start, end)); + expect(await this.mock.$slice(lorem, start, ethers.Typed.uint256(end))).to.equal(result); + }); + } + }); + }); +}); diff --git a/test/utils/CAIP.test.js b/test/utils/CAIP.test.js new file mode 100644 index 00000000000..cd5995cade0 --- /dev/null +++ b/test/utils/CAIP.test.js @@ -0,0 +1,53 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { CHAINS, getLocalCAIP } = require('../helpers/chains'); + +async function fixture() { + const caip2 = await ethers.deployContract('$CAIP2'); + const caip10 = await ethers.deployContract('$CAIP10'); + return { caip2, caip10 }; +} + +describe('CAIP utilities', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('CAIP-2', function () { + it('local()', async function () { + const { caip2 } = await getLocalCAIP(); + expect(await this.caip2.$local()).to.equal(caip2); + }); + + for (const { namespace, reference, caip2 } of Object.values(CHAINS)) + it(`format(${namespace}, ${reference})`, async function () { + expect(await this.caip2.$format(namespace, reference)).to.equal(caip2); + }); + + for (const { namespace, reference, caip2 } of Object.values(CHAINS)) + it(`parse(${caip2})`, async function () { + expect(await this.caip2.$parse(caip2)).to.deep.equal([namespace, reference]); + }); + }); + + describe('CAIP-10', function () { + const { address: account } = ethers.Wallet.createRandom(); + + it(`local(${account})`, async function () { + const { caip10 } = await getLocalCAIP(account); + expect(await this.caip10.$local(ethers.Typed.address(account))).to.equal(caip10); + }); + + for (const { account, caip2, caip10 } of Object.values(CHAINS)) + it(`format(${caip2}, ${account})`, async function () { + expect(await this.caip10.$format(ethers.Typed.string(caip2), ethers.Typed.string(account))).to.equal(caip10); + }); + + for (const { account, caip2, caip10 } of Object.values(CHAINS)) + it(`parse(${caip10})`, async function () { + expect(await this.caip10.$parse(caip10)).to.deep.equal([caip2, account]); + }); + }); +}); From 0c6dc1b6667770a3f40efa46f124ce4f264fa1c6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 18 Oct 2024 14:22:37 +0200 Subject: [PATCH 09/13] remove vendored entrypoint --- contracts/package.json | 3 +- contracts/vendor/erc4337-entrypoint/README.md | 1 - .../erc4337-entrypoint/core/EntryPoint.sol | 802 ------------------ .../erc4337-entrypoint/core/Helpers.sol | 106 --- .../erc4337-entrypoint/core/NonceManager.sol | 43 - .../erc4337-entrypoint/core/SenderCreator.sol | 38 - .../erc4337-entrypoint/core/StakeManager.sol | 145 ---- .../core/UserOperationLib.sol | 139 --- .../interfaces/IAccount.sol | 39 - .../interfaces/IAccountExecute.sol | 20 - .../interfaces/IAggregator.sol | 44 - .../interfaces/IEntryPoint.sol | 223 ----- .../interfaces/INonceManager.sol | 27 - .../interfaces/IPaymaster.sol | 63 -- .../interfaces/IStakeManager.sol | 111 --- .../interfaces/PackedUserOperation.sol | 28 - .../vendor/erc4337-entrypoint/utils/Exec.sol | 70 -- package.json | 7 +- scripts/upgradeable/patch-apply.sh | 2 +- scripts/upgradeable/patch-save.sh | 2 +- scripts/upgradeable/transpile.sh | 10 +- .../upgradeable/upgradeable.excluded.patch | 17 - 22 files changed, 7 insertions(+), 1933 deletions(-) delete mode 100644 contracts/vendor/erc4337-entrypoint/README.md delete mode 100644 contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/core/Helpers.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/core/NonceManager.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/core/StakeManager.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/utils/Exec.sol delete mode 100644 scripts/upgradeable/upgradeable.excluded.patch diff --git a/contracts/package.json b/contracts/package.json index 6aec27c8d52..e0ed163d05a 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -5,8 +5,7 @@ "files": [ "**/*.sol", "/build/contracts/*.json", - "!/mocks/**/*", - "!/vendor/erc4337-entrypoint/**/*" + "!/mocks/**/*" ], "scripts": { "prepack": "bash ../scripts/prepack.sh", diff --git a/contracts/vendor/erc4337-entrypoint/README.md b/contracts/vendor/erc4337-entrypoint/README.md deleted file mode 100644 index e0b91fe9dd6..00000000000 --- a/contracts/vendor/erc4337-entrypoint/README.md +++ /dev/null @@ -1 +0,0 @@ -Files in this directory are vendored from https://github.com/eth-infinitism/account-abstraction/commit/6f02f5a28a20e804d0410b4b5b570dd4b076dcf9 diff --git a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol deleted file mode 100644 index 778115b1637..00000000000 --- a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol +++ /dev/null @@ -1,802 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; -/* solhint-disable avoid-low-level-calls */ -/* solhint-disable no-inline-assembly */ - -import "../interfaces/IAccount.sol"; -import "../interfaces/IAccountExecute.sol"; -import "../interfaces/IPaymaster.sol"; -import "../interfaces/IEntryPoint.sol"; - -import "../utils/Exec.sol"; -import "./StakeManager.sol"; -import "./SenderCreator.sol"; -import "./Helpers.sol"; -import "./NonceManager.sol"; -import "./UserOperationLib.sol"; - -// import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -// import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import "../../../utils/introspection/ERC165.sol"; // OZ edit -import "../../../utils/ReentrancyGuard.sol"; // OZ edit - -/* - * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. - * Only one instance required on each chain. - */ - -/// @custom:security-contact https://bounty.ethereum.org -contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, ERC165 { - - using UserOperationLib for PackedUserOperation; - - SenderCreator private immutable _senderCreator = new SenderCreator(); - - function senderCreator() internal view virtual returns (SenderCreator) { - return _senderCreator; - } - - //compensate for innerHandleOps' emit message and deposit refund. - // allow some slack for future gas price changes. - uint256 private constant INNER_GAS_OVERHEAD = 10000; - - // Marker for inner call revert on out of gas - bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead"; - bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51"; - - uint256 private constant REVERT_REASON_MAX_LEN = 2048; - uint256 private constant PENALTY_PERCENT = 10; - - /// @inheritdoc IERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - // note: solidity "type(IEntryPoint).interfaceId" is without inherited methods but we want to check everything - return interfaceId == (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) || - interfaceId == type(IEntryPoint).interfaceId || - interfaceId == type(IStakeManager).interfaceId || - interfaceId == type(INonceManager).interfaceId || - super.supportsInterface(interfaceId); - } - - /** - * Compensate the caller's beneficiary address with the collected fees of all UserOperations. - * @param beneficiary - The address to receive the fees. - * @param amount - Amount to transfer. - */ - function _compensate(address payable beneficiary, uint256 amount) internal { - require(beneficiary != address(0), "AA90 invalid beneficiary"); - (bool success, ) = beneficiary.call{value: amount}(""); - require(success, "AA91 failed send to beneficiary"); - } - - /** - * Execute a user operation. - * @param opIndex - Index into the opInfo array. - * @param userOp - The userOp to execute. - * @param opInfo - The opInfo filled by validatePrepayment for this userOp. - * @return collected - The total amount this userOp paid. - */ - function _executeUserOp( - uint256 opIndex, - PackedUserOperation calldata userOp, - UserOpInfo memory opInfo - ) - internal - returns - (uint256 collected) { - uint256 preGas = gasleft(); - bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); - bool success; - { - uint256 saveFreePtr; - assembly ("memory-safe") { - saveFreePtr := mload(0x40) - } - bytes calldata callData = userOp.callData; - bytes memory innerCall; - bytes4 methodSig; - assembly { - let len := callData.length - if gt(len, 3) { - methodSig := calldataload(callData.offset) - } - } - if (methodSig == IAccountExecute.executeUserOp.selector) { - bytes memory executeUserOp = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)); - innerCall = abi.encodeCall(this.innerHandleOp, (executeUserOp, opInfo, context)); - } else - { - innerCall = abi.encodeCall(this.innerHandleOp, (callData, opInfo, context)); - } - assembly ("memory-safe") { - success := call(gas(), address(), 0, add(innerCall, 0x20), mload(innerCall), 0, 32) - collected := mload(0) - mstore(0x40, saveFreePtr) - } - } - if (!success) { - bytes32 innerRevertCode; - assembly ("memory-safe") { - let len := returndatasize() - if eq(32,len) { - returndatacopy(0, 0, 32) - innerRevertCode := mload(0) - } - } - if (innerRevertCode == INNER_OUT_OF_GAS) { - // handleOps was called with gas limit too low. abort entire bundle. - //can only be caused by bundler (leaving not enough gas for inner call) - revert FailedOp(opIndex, "AA95 out of gas"); - } else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) { - // innerCall reverted on prefund too low. treat entire prefund as "gas cost" - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - uint256 actualGasCost = opInfo.prefund; - emitPrefundTooLow(opInfo); - emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); - collected = actualGasCost; - } else { - emit PostOpRevertReason( - opInfo.userOpHash, - opInfo.mUserOp.sender, - opInfo.mUserOp.nonce, - Exec.getReturnData(REVERT_REASON_MAX_LEN) - ); - - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - collected = _postExecution( - IPaymaster.PostOpMode.postOpReverted, - opInfo, - context, - actualGas - ); - } - } - } - - function emitUserOperationEvent(UserOpInfo memory opInfo, bool success, uint256 actualGasCost, uint256 actualGas) internal virtual { - emit UserOperationEvent( - opInfo.userOpHash, - opInfo.mUserOp.sender, - opInfo.mUserOp.paymaster, - opInfo.mUserOp.nonce, - success, - actualGasCost, - actualGas - ); - } - - function emitPrefundTooLow(UserOpInfo memory opInfo) internal virtual { - emit UserOperationPrefundTooLow( - opInfo.userOpHash, - opInfo.mUserOp.sender, - opInfo.mUserOp.nonce - ); - } - - /// @inheritdoc IEntryPoint - function handleOps( - PackedUserOperation[] calldata ops, - address payable beneficiary - ) public nonReentrant { - uint256 opslen = ops.length; - UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); - - unchecked { - for (uint256 i = 0; i < opslen; i++) { - UserOpInfo memory opInfo = opInfos[i]; - ( - uint256 validationData, - uint256 pmValidationData - ) = _validatePrepayment(i, ops[i], opInfo); - _validateAccountAndPaymasterValidationData( - i, - validationData, - pmValidationData, - address(0) - ); - } - - uint256 collected = 0; - emit BeforeExecution(); - - for (uint256 i = 0; i < opslen; i++) { - collected += _executeUserOp(i, ops[i], opInfos[i]); - } - - _compensate(beneficiary, collected); - } - } - - /// @inheritdoc IEntryPoint - function handleAggregatedOps( - UserOpsPerAggregator[] calldata opsPerAggregator, - address payable beneficiary - ) public nonReentrant { - - uint256 opasLen = opsPerAggregator.length; - uint256 totalOps = 0; - for (uint256 i = 0; i < opasLen; i++) { - UserOpsPerAggregator calldata opa = opsPerAggregator[i]; - PackedUserOperation[] calldata ops = opa.userOps; - IAggregator aggregator = opa.aggregator; - - //address(1) is special marker of "signature error" - require( - address(aggregator) != address(1), - "AA96 invalid aggregator" - ); - - if (address(aggregator) != address(0)) { - // solhint-disable-next-line no-empty-blocks - try aggregator.validateSignatures(ops, opa.signature) {} catch { - revert SignatureValidationFailed(address(aggregator)); - } - } - - totalOps += ops.length; - } - - UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); - - uint256 opIndex = 0; - for (uint256 a = 0; a < opasLen; a++) { - UserOpsPerAggregator calldata opa = opsPerAggregator[a]; - PackedUserOperation[] calldata ops = opa.userOps; - IAggregator aggregator = opa.aggregator; - - uint256 opslen = ops.length; - for (uint256 i = 0; i < opslen; i++) { - UserOpInfo memory opInfo = opInfos[opIndex]; - ( - uint256 validationData, - uint256 paymasterValidationData - ) = _validatePrepayment(opIndex, ops[i], opInfo); - _validateAccountAndPaymasterValidationData( - i, - validationData, - paymasterValidationData, - address(aggregator) - ); - opIndex++; - } - } - - emit BeforeExecution(); - - uint256 collected = 0; - opIndex = 0; - for (uint256 a = 0; a < opasLen; a++) { - UserOpsPerAggregator calldata opa = opsPerAggregator[a]; - emit SignatureAggregatorChanged(address(opa.aggregator)); - PackedUserOperation[] calldata ops = opa.userOps; - uint256 opslen = ops.length; - - for (uint256 i = 0; i < opslen; i++) { - collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); - opIndex++; - } - } - emit SignatureAggregatorChanged(address(0)); - - _compensate(beneficiary, collected); - } - - /** - * A memory copy of UserOp static fields only. - * Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster. - */ - struct MemoryUserOp { - address sender; - uint256 nonce; - uint256 verificationGasLimit; - uint256 callGasLimit; - uint256 paymasterVerificationGasLimit; - uint256 paymasterPostOpGasLimit; - uint256 preVerificationGas; - address paymaster; - uint256 maxFeePerGas; - uint256 maxPriorityFeePerGas; - } - - struct UserOpInfo { - MemoryUserOp mUserOp; - bytes32 userOpHash; - uint256 prefund; - uint256 contextOffset; - uint256 preOpGas; - } - - /** - * Inner function to handle a UserOperation. - * Must be declared "external" to open a call context, but it can only be called by handleOps. - * @param callData - The callData to execute. - * @param opInfo - The UserOpInfo struct. - * @param context - The context bytes. - * @return actualGasCost - the actual cost in eth this UserOperation paid for gas - */ - function innerHandleOp( - bytes memory callData, - UserOpInfo memory opInfo, - bytes calldata context - ) external returns (uint256 actualGasCost) { - uint256 preGas = gasleft(); - require(msg.sender == address(this), "AA92 internal call only"); - MemoryUserOp memory mUserOp = opInfo.mUserOp; - - uint256 callGasLimit = mUserOp.callGasLimit; - unchecked { - // handleOps was called with gas limit too low. abort entire bundle. - if ( - gasleft() * 63 / 64 < - callGasLimit + - mUserOp.paymasterPostOpGasLimit + - INNER_GAS_OVERHEAD - ) { - assembly ("memory-safe") { - mstore(0, INNER_OUT_OF_GAS) - revert(0, 32) - } - } - } - - IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; - if (callData.length > 0) { - bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); - if (!success) { - bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); - if (result.length > 0) { - emit UserOperationRevertReason( - opInfo.userOpHash, - mUserOp.sender, - mUserOp.nonce, - result - ); - } - mode = IPaymaster.PostOpMode.opReverted; - } - } - - unchecked { - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - return _postExecution(mode, opInfo, context, actualGas); - } - } - - /// @inheritdoc IEntryPoint - function getUserOpHash( - PackedUserOperation calldata userOp - ) public view returns (bytes32) { - return - keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); - } - - /** - * Copy general fields from userOp into the memory opInfo structure. - * @param userOp - The user operation. - * @param mUserOp - The memory user operation. - */ - function _copyUserOpToMemory( - PackedUserOperation calldata userOp, - MemoryUserOp memory mUserOp - ) internal pure { - mUserOp.sender = userOp.sender; - mUserOp.nonce = userOp.nonce; - (mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackUints(userOp.accountGasLimits); - mUserOp.preVerificationGas = userOp.preVerificationGas; - (mUserOp.maxPriorityFeePerGas, mUserOp.maxFeePerGas) = UserOperationLib.unpackUints(userOp.gasFees); - bytes calldata paymasterAndData = userOp.paymasterAndData; - if (paymasterAndData.length > 0) { - require( - paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET, - "AA93 invalid paymasterAndData" - ); - (mUserOp.paymaster, mUserOp.paymasterVerificationGasLimit, mUserOp.paymasterPostOpGasLimit) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData); - } else { - mUserOp.paymaster = address(0); - mUserOp.paymasterVerificationGasLimit = 0; - mUserOp.paymasterPostOpGasLimit = 0; - } - } - - /** - * Get the required prefunded gas fee amount for an operation. - * @param mUserOp - The user operation in memory. - */ - function _getRequiredPrefund( - MemoryUserOp memory mUserOp - ) internal pure returns (uint256 requiredPrefund) { - unchecked { - uint256 requiredGas = mUserOp.verificationGasLimit + - mUserOp.callGasLimit + - mUserOp.paymasterVerificationGasLimit + - mUserOp.paymasterPostOpGasLimit + - mUserOp.preVerificationGas; - - requiredPrefund = requiredGas * mUserOp.maxFeePerGas; - } - } - - /** - * Create sender smart contract account if init code is provided. - * @param opIndex - The operation index. - * @param opInfo - The operation info. - * @param initCode - The init code for the smart contract account. - */ - function _createSenderIfNeeded( - uint256 opIndex, - UserOpInfo memory opInfo, - bytes calldata initCode - ) internal { - if (initCode.length != 0) { - address sender = opInfo.mUserOp.sender; - if (sender.code.length != 0) - revert FailedOp(opIndex, "AA10 sender already constructed"); - address sender1 = senderCreator().createSender{ - gas: opInfo.mUserOp.verificationGasLimit - }(initCode); - if (sender1 == address(0)) - revert FailedOp(opIndex, "AA13 initCode failed or OOG"); - if (sender1 != sender) - revert FailedOp(opIndex, "AA14 initCode must return sender"); - if (sender1.code.length == 0) - revert FailedOp(opIndex, "AA15 initCode must create sender"); - address factory = address(bytes20(initCode[0:20])); - emit AccountDeployed( - opInfo.userOpHash, - sender, - factory, - opInfo.mUserOp.paymaster - ); - } - } - - /// @inheritdoc IEntryPoint - function getSenderAddress(bytes calldata initCode) public { - address sender = senderCreator().createSender(initCode); - revert SenderAddressResult(sender); - } - - /** - * Call account.validateUserOp. - * Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. - * Decrement account's deposit if needed. - * @param opIndex - The operation index. - * @param op - The user operation. - * @param opInfo - The operation info. - * @param requiredPrefund - The required prefund amount. - */ - function _validateAccountPrepayment( - uint256 opIndex, - PackedUserOperation calldata op, - UserOpInfo memory opInfo, - uint256 requiredPrefund, - uint256 verificationGasLimit - ) - internal - returns ( - uint256 validationData - ) - { - unchecked { - MemoryUserOp memory mUserOp = opInfo.mUserOp; - address sender = mUserOp.sender; - _createSenderIfNeeded(opIndex, opInfo, op.initCode); - address paymaster = mUserOp.paymaster; - uint256 missingAccountFunds = 0; - if (paymaster == address(0)) { - uint256 bal = balanceOf(sender); - missingAccountFunds = bal > requiredPrefund - ? 0 - : requiredPrefund - bal; - } - try - IAccount(sender).validateUserOp{ - gas: verificationGasLimit - }(op, opInfo.userOpHash, missingAccountFunds) - returns (uint256 _validationData) { - validationData = _validationData; - } catch { - revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); - } - if (paymaster == address(0)) { - DepositInfo storage senderInfo = deposits[sender]; - uint256 deposit = senderInfo.deposit; - if (requiredPrefund > deposit) { - revert FailedOp(opIndex, "AA21 didn't pay prefund"); - } - senderInfo.deposit = deposit - requiredPrefund; - } - } - } - - /** - * In case the request has a paymaster: - * - Validate paymaster has enough deposit. - * - Call paymaster.validatePaymasterUserOp. - * - Revert with proper FailedOp in case paymaster reverts. - * - Decrement paymaster's deposit. - * @param opIndex - The operation index. - * @param op - The user operation. - * @param opInfo - The operation info. - * @param requiredPreFund - The required prefund amount. - */ - function _validatePaymasterPrepayment( - uint256 opIndex, - PackedUserOperation calldata op, - UserOpInfo memory opInfo, - uint256 requiredPreFund - ) internal returns (bytes memory context, uint256 validationData) { - unchecked { - uint256 preGas = gasleft(); - MemoryUserOp memory mUserOp = opInfo.mUserOp; - address paymaster = mUserOp.paymaster; - DepositInfo storage paymasterInfo = deposits[paymaster]; - uint256 deposit = paymasterInfo.deposit; - if (deposit < requiredPreFund) { - revert FailedOp(opIndex, "AA31 paymaster deposit too low"); - } - paymasterInfo.deposit = deposit - requiredPreFund; - uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit; - try - IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}( - op, - opInfo.userOpHash, - requiredPreFund - ) - returns (bytes memory _context, uint256 _validationData) { - context = _context; - validationData = _validationData; - } catch { - revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); - } - if (preGas - gasleft() > pmVerificationGasLimit) { - revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit"); - } - } - } - - /** - * Revert if either account validationData or paymaster validationData is expired. - * @param opIndex - The operation index. - * @param validationData - The account validationData. - * @param paymasterValidationData - The paymaster validationData. - * @param expectedAggregator - The expected aggregator. - */ - function _validateAccountAndPaymasterValidationData( - uint256 opIndex, - uint256 validationData, - uint256 paymasterValidationData, - address expectedAggregator - ) internal view { - (address aggregator, bool outOfTimeRange) = _getValidationData( - validationData - ); - if (expectedAggregator != aggregator) { - revert FailedOp(opIndex, "AA24 signature error"); - } - if (outOfTimeRange) { - revert FailedOp(opIndex, "AA22 expired or not due"); - } - // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. - // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). - address pmAggregator; - (pmAggregator, outOfTimeRange) = _getValidationData( - paymasterValidationData - ); - if (pmAggregator != address(0)) { - revert FailedOp(opIndex, "AA34 signature error"); - } - if (outOfTimeRange) { - revert FailedOp(opIndex, "AA32 paymaster expired or not due"); - } - } - - /** - * Parse validationData into its components. - * @param validationData - The packed validation data (sigFailed, validAfter, validUntil). - * @return aggregator the aggregator of the validationData - * @return outOfTimeRange true if current time is outside the time range of this validationData. - */ - function _getValidationData( - uint256 validationData - ) internal view returns (address aggregator, bool outOfTimeRange) { - if (validationData == 0) { - return (address(0), false); - } - ValidationData memory data = _parseValidationData(validationData); - // solhint-disable-next-line not-rely-on-time - outOfTimeRange = block.timestamp > data.validUntil || block.timestamp < data.validAfter; - aggregator = data.aggregator; - } - - /** - * Validate account and paymaster (if defined) and - * also make sure total validation doesn't exceed verificationGasLimit. - * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) - * @param opIndex - The index of this userOp into the "opInfos" array. - * @param userOp - The userOp to validate. - */ - function _validatePrepayment( - uint256 opIndex, - PackedUserOperation calldata userOp, - UserOpInfo memory outOpInfo - ) - internal - returns (uint256 validationData, uint256 paymasterValidationData) - { - uint256 preGas = gasleft(); - MemoryUserOp memory mUserOp = outOpInfo.mUserOp; - _copyUserOpToMemory(userOp, mUserOp); - outOpInfo.userOpHash = getUserOpHash(userOp); - - // Validate all numeric values in userOp are well below 128 bit, so they can safely be added - // and multiplied without causing overflow. - uint256 verificationGasLimit = mUserOp.verificationGasLimit; - uint256 maxGasValues = mUserOp.preVerificationGas | - verificationGasLimit | - mUserOp.callGasLimit | - mUserOp.paymasterVerificationGasLimit | - mUserOp.paymasterPostOpGasLimit | - mUserOp.maxFeePerGas | - mUserOp.maxPriorityFeePerGas; - require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); - - uint256 requiredPreFund = _getRequiredPrefund(mUserOp); - validationData = _validateAccountPrepayment( - opIndex, - userOp, - outOpInfo, - requiredPreFund, - verificationGasLimit - ); - - if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) { - revert FailedOp(opIndex, "AA25 invalid account nonce"); - } - - unchecked { - if (preGas - gasleft() > verificationGasLimit) { - revert FailedOp(opIndex, "AA26 over verificationGasLimit"); - } - } - - bytes memory context; - if (mUserOp.paymaster != address(0)) { - (context, paymasterValidationData) = _validatePaymasterPrepayment( - opIndex, - userOp, - outOpInfo, - requiredPreFund - ); - } - unchecked { - outOpInfo.prefund = requiredPreFund; - outOpInfo.contextOffset = getOffsetOfMemoryBytes(context); - outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; - } - } - - /** - * Process post-operation, called just after the callData is executed. - * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. - * The excess amount is refunded to the account (or paymaster - if it was used in the request). - * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). - * @param opInfo - UserOp fields and info collected during validation. - * @param context - The context returned in validatePaymasterUserOp. - * @param actualGas - The gas used so far by this user operation. - */ - function _postExecution( - IPaymaster.PostOpMode mode, - UserOpInfo memory opInfo, - bytes memory context, - uint256 actualGas - ) private returns (uint256 actualGasCost) { - uint256 preGas = gasleft(); - unchecked { - address refundAddress; - MemoryUserOp memory mUserOp = opInfo.mUserOp; - uint256 gasPrice = getUserOpGasPrice(mUserOp); - - address paymaster = mUserOp.paymaster; - if (paymaster == address(0)) { - refundAddress = mUserOp.sender; - } else { - refundAddress = paymaster; - if (context.length > 0) { - actualGasCost = actualGas * gasPrice; - if (mode != IPaymaster.PostOpMode.postOpReverted) { - try IPaymaster(paymaster).postOp{ - gas: mUserOp.paymasterPostOpGasLimit - }(mode, context, actualGasCost, gasPrice) - // solhint-disable-next-line no-empty-blocks - {} catch { - bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN); - revert PostOpReverted(reason); - } - } - } - } - actualGas += preGas - gasleft(); - - // Calculating a penalty for unused execution gas - { - uint256 executionGasLimit = mUserOp.callGasLimit + mUserOp.paymasterPostOpGasLimit; - uint256 executionGasUsed = actualGas - opInfo.preOpGas; - // this check is required for the gas used within EntryPoint and not covered by explicit gas limits - if (executionGasLimit > executionGasUsed) { - uint256 unusedGas = executionGasLimit - executionGasUsed; - uint256 unusedGasPenalty = (unusedGas * PENALTY_PERCENT) / 100; - actualGas += unusedGasPenalty; - } - } - - actualGasCost = actualGas * gasPrice; - uint256 prefund = opInfo.prefund; - if (prefund < actualGasCost) { - if (mode == IPaymaster.PostOpMode.postOpReverted) { - actualGasCost = prefund; - emitPrefundTooLow(opInfo); - emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); - } else { - assembly ("memory-safe") { - mstore(0, INNER_REVERT_LOW_PREFUND) - revert(0, 32) - } - } - } else { - uint256 refund = prefund - actualGasCost; - _incrementDeposit(refundAddress, refund); - bool success = mode == IPaymaster.PostOpMode.opSucceeded; - emitUserOperationEvent(opInfo, success, actualGasCost, actualGas); - } - } // unchecked - } - - /** - * The gas price this UserOp agrees to pay. - * Relayer/block builder might submit the TX with higher priorityFee, but the user should not. - * @param mUserOp - The userOp to get the gas price from. - */ - function getUserOpGasPrice( - MemoryUserOp memory mUserOp - ) internal view returns (uint256) { - unchecked { - uint256 maxFeePerGas = mUserOp.maxFeePerGas; - uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas; - if (maxFeePerGas == maxPriorityFeePerGas) { - //legacy mode (for networks that don't support basefee opcode) - return maxFeePerGas; - } - return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); - } - } - - /** - * The offset of the given bytes in memory. - * @param data - The bytes to get the offset of. - */ - function getOffsetOfMemoryBytes( - bytes memory data - ) internal pure returns (uint256 offset) { - assembly { - offset := data - } - } - - /** - * The bytes in memory at the given offset. - * @param offset - The offset to get the bytes from. - */ - function getMemoryBytesFromOffset( - uint256 offset - ) internal pure returns (bytes memory data) { - assembly ("memory-safe") { - data := offset - } - } - - /// @inheritdoc IEntryPoint - function delegateAndRevert(address target, bytes calldata data) external { - (bool success, bytes memory ret) = target.delegatecall(data); - revert DelegateAndRevert(success, ret); - } -} diff --git a/contracts/vendor/erc4337-entrypoint/core/Helpers.sol b/contracts/vendor/erc4337-entrypoint/core/Helpers.sol deleted file mode 100644 index 8579008613a..00000000000 --- a/contracts/vendor/erc4337-entrypoint/core/Helpers.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; - -/* solhint-disable no-inline-assembly */ - - - /* - * For simulation purposes, validateUserOp (and validatePaymasterUserOp) - * must return this value in case of signature failure, instead of revert. - */ -uint256 constant SIG_VALIDATION_FAILED = 1; - - -/* - * For simulation purposes, validateUserOp (and validatePaymasterUserOp) - * return this value on success. - */ -uint256 constant SIG_VALIDATION_SUCCESS = 0; - - -/** - * Returned data from validateUserOp. - * validateUserOp returns a uint256, which is created by `_packedValidationData` and - * parsed by `_parseValidationData`. - * @param aggregator - address(0) - The account validated the signature by itself. - * address(1) - The account failed to validate the signature. - * otherwise - This is an address of a signature aggregator that must - * be used to validate the signature. - * @param validAfter - This UserOp is valid only after this timestamp. - * @param validaUntil - This UserOp is valid only up to this timestamp. - */ -struct ValidationData { - address aggregator; - uint48 validAfter; - uint48 validUntil; -} - -/** - * Extract sigFailed, validAfter, validUntil. - * Also convert zero validUntil to type(uint48).max. - * @param validationData - The packed validation data. - */ -function _parseValidationData( - uint256 validationData -) pure returns (ValidationData memory data) { - address aggregator = address(uint160(validationData)); - uint48 validUntil = uint48(validationData >> 160); - if (validUntil == 0) { - validUntil = type(uint48).max; - } - uint48 validAfter = uint48(validationData >> (48 + 160)); - return ValidationData(aggregator, validAfter, validUntil); -} - -/** - * Helper to pack the return value for validateUserOp. - * @param data - The ValidationData to pack. - */ -function _packValidationData( - ValidationData memory data -) pure returns (uint256) { - return - uint160(data.aggregator) | - (uint256(data.validUntil) << 160) | - (uint256(data.validAfter) << (160 + 48)); -} - -/** - * Helper to pack the return value for validateUserOp, when not using an aggregator. - * @param sigFailed - True for signature failure, false for success. - * @param validUntil - Last timestamp this UserOperation is valid (or zero for infinite). - * @param validAfter - First timestamp this UserOperation is valid. - */ -function _packValidationData( - bool sigFailed, - uint48 validUntil, - uint48 validAfter -) pure returns (uint256) { - return - (sigFailed ? 1 : 0) | - (uint256(validUntil) << 160) | - (uint256(validAfter) << (160 + 48)); -} - -/** - * keccak function over calldata. - * @dev copy calldata into memory, do keccak and drop allocated memory. Strangely, this is more efficient than letting solidity do it. - */ - function calldataKeccak(bytes calldata data) pure returns (bytes32 ret) { - assembly ("memory-safe") { - let mem := mload(0x40) - let len := data.length - calldatacopy(mem, data.offset, len) - ret := keccak256(mem, len) - } - } - - -/** - * The minimum of two numbers. - * @param a - First number. - * @param b - Second number. - */ - function min(uint256 a, uint256 b) pure returns (uint256) { - return a < b ? a : b; - } diff --git a/contracts/vendor/erc4337-entrypoint/core/NonceManager.sol b/contracts/vendor/erc4337-entrypoint/core/NonceManager.sol deleted file mode 100644 index 7bef62e99dd..00000000000 --- a/contracts/vendor/erc4337-entrypoint/core/NonceManager.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; - -import "../interfaces/INonceManager.sol"; - -/** - * nonce management functionality - */ -abstract contract NonceManager is INonceManager { - - /** - * The next valid sequence number for a given nonce key. - */ - mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber; - - /// @inheritdoc INonceManager - function getNonce(address sender, uint192 key) - public view override returns (uint256 nonce) { - return nonceSequenceNumber[sender][key] | (uint256(key) << 64); - } - - // allow an account to manually increment its own nonce. - // (mainly so that during construction nonce can be made non-zero, - // to "absorb" the gas cost of first nonce increment to 1st transaction (construction), - // not to 2nd transaction) - function incrementNonce(uint192 key) public override { - nonceSequenceNumber[msg.sender][key]++; - } - - /** - * validate nonce uniqueness for this account. - * called just after validateUserOp() - * @return true if the nonce was incremented successfully. - * false if the current nonce doesn't match the given one. - */ - function _validateAndUpdateNonce(address sender, uint256 nonce) internal returns (bool) { - - uint192 key = uint192(nonce >> 64); - uint64 seq = uint64(nonce); - return nonceSequenceNumber[sender][key]++ == seq; - } - -} diff --git a/contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol b/contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol deleted file mode 100644 index 43ea80367ef..00000000000 --- a/contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; - -/** - * Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, - * which is explicitly not the entryPoint itself. - */ -contract SenderCreator { - /** - * Call the "initCode" factory to create and return the sender account address. - * @param initCode - The initCode value from a UserOp. contains 20 bytes of factory address, - * followed by calldata. - * @return sender - The returned address of the created account, or zero address on failure. - */ - function createSender( - bytes calldata initCode - ) external returns (address sender) { - address factory = address(bytes20(initCode[0:20])); - bytes memory initCallData = initCode[20:]; - bool success; - /* solhint-disable no-inline-assembly */ - assembly ("memory-safe") { - success := call( - gas(), - factory, - 0, - add(initCallData, 0x20), - mload(initCallData), - 0, - 32 - ) - sender := mload(0) - } - if (!success) { - sender = address(0); - } - } -} diff --git a/contracts/vendor/erc4337-entrypoint/core/StakeManager.sol b/contracts/vendor/erc4337-entrypoint/core/StakeManager.sol deleted file mode 100644 index f90210b7e38..00000000000 --- a/contracts/vendor/erc4337-entrypoint/core/StakeManager.sol +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.23; - -import "../interfaces/IStakeManager.sol"; - -/* solhint-disable avoid-low-level-calls */ -/* solhint-disable not-rely-on-time */ - -/** - * Manage deposits and stakes. - * Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account). - * Stake is value locked for at least "unstakeDelay" by a paymaster. - */ -abstract contract StakeManager is IStakeManager { - /// maps paymaster to their deposits and stakes - mapping(address => DepositInfo) public deposits; - - /// @inheritdoc IStakeManager - function getDepositInfo( - address account - ) public view returns (DepositInfo memory info) { - return deposits[account]; - } - - /** - * Internal method to return just the stake info. - * @param addr - The account to query. - */ - function _getStakeInfo( - address addr - ) internal view returns (StakeInfo memory info) { - DepositInfo storage depositInfo = deposits[addr]; - info.stake = depositInfo.stake; - info.unstakeDelaySec = depositInfo.unstakeDelaySec; - } - - /// @inheritdoc IStakeManager - function balanceOf(address account) public view returns (uint256) { - return deposits[account].deposit; - } - - receive() external payable { - depositTo(msg.sender); - } - - /** - * Increments an account's deposit. - * @param account - The account to increment. - * @param amount - The amount to increment by. - * @return the updated deposit of this account - */ - function _incrementDeposit(address account, uint256 amount) internal returns (uint256) { - DepositInfo storage info = deposits[account]; - uint256 newAmount = info.deposit + amount; - info.deposit = newAmount; - return newAmount; - } - - /** - * Add to the deposit of the given account. - * @param account - The account to add to. - */ - function depositTo(address account) public virtual payable { - uint256 newDeposit = _incrementDeposit(account, msg.value); - emit Deposited(account, newDeposit); - } - - /** - * Add to the account's stake - amount and delay - * any pending unstake is first cancelled. - * @param unstakeDelaySec The new lock duration before the deposit can be withdrawn. - */ - function addStake(uint32 unstakeDelaySec) public payable { - DepositInfo storage info = deposits[msg.sender]; - require(unstakeDelaySec > 0, "must specify unstake delay"); - require( - unstakeDelaySec >= info.unstakeDelaySec, - "cannot decrease unstake time" - ); - uint256 stake = info.stake + msg.value; - require(stake > 0, "no stake specified"); - require(stake <= type(uint112).max, "stake overflow"); - deposits[msg.sender] = DepositInfo( - info.deposit, - true, - uint112(stake), - unstakeDelaySec, - 0 - ); - emit StakeLocked(msg.sender, stake, unstakeDelaySec); - } - - /** - * Attempt to unlock the stake. - * The value can be withdrawn (using withdrawStake) after the unstake delay. - */ - function unlockStake() external { - DepositInfo storage info = deposits[msg.sender]; - require(info.unstakeDelaySec != 0, "not staked"); - require(info.staked, "already unstaking"); - uint48 withdrawTime = uint48(block.timestamp) + info.unstakeDelaySec; - info.withdrawTime = withdrawTime; - info.staked = false; - emit StakeUnlocked(msg.sender, withdrawTime); - } - - /** - * Withdraw from the (unlocked) stake. - * Must first call unlockStake and wait for the unstakeDelay to pass. - * @param withdrawAddress - The address to send withdrawn value. - */ - function withdrawStake(address payable withdrawAddress) external { - DepositInfo storage info = deposits[msg.sender]; - uint256 stake = info.stake; - require(stake > 0, "No stake to withdraw"); - require(info.withdrawTime > 0, "must call unlockStake() first"); - require( - info.withdrawTime <= block.timestamp, - "Stake withdrawal is not due" - ); - info.unstakeDelaySec = 0; - info.withdrawTime = 0; - info.stake = 0; - emit StakeWithdrawn(msg.sender, withdrawAddress, stake); - (bool success,) = withdrawAddress.call{value: stake}(""); - require(success, "failed to withdraw stake"); - } - - /** - * Withdraw from the deposit. - * @param withdrawAddress - The address to send withdrawn value. - * @param withdrawAmount - The amount to withdraw. - */ - function withdrawTo( - address payable withdrawAddress, - uint256 withdrawAmount - ) external { - DepositInfo storage info = deposits[msg.sender]; - require(withdrawAmount <= info.deposit, "Withdraw amount too large"); - info.deposit = info.deposit - withdrawAmount; - emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); - (bool success,) = withdrawAddress.call{value: withdrawAmount}(""); - require(success, "failed to withdraw"); - } -} diff --git a/contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol b/contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol deleted file mode 100644 index dcf5740cc28..00000000000 --- a/contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; - -/* solhint-disable no-inline-assembly */ - -import "../interfaces/PackedUserOperation.sol"; -import {calldataKeccak, min} from "./Helpers.sol"; - -/** - * Utility functions helpful when working with UserOperation structs. - */ -library UserOperationLib { - - uint256 public constant PAYMASTER_VALIDATION_GAS_OFFSET = 20; - uint256 public constant PAYMASTER_POSTOP_GAS_OFFSET = 36; - uint256 public constant PAYMASTER_DATA_OFFSET = 52; - /** - * Get sender from user operation data. - * @param userOp - The user operation data. - */ - function getSender( - PackedUserOperation calldata userOp - ) internal pure returns (address) { - address data; - //read sender from userOp, which is first userOp member (saves 800 gas...) - assembly { - data := calldataload(userOp) - } - return address(uint160(data)); - } - - /** - * Relayer/block builder might submit the TX with higher priorityFee, - * but the user should not pay above what he signed for. - * @param userOp - The user operation data. - */ - function gasPrice( - PackedUserOperation calldata userOp - ) internal view returns (uint256) { - unchecked { - (uint256 maxPriorityFeePerGas, uint256 maxFeePerGas) = unpackUints(userOp.gasFees); - if (maxFeePerGas == maxPriorityFeePerGas) { - //legacy mode (for networks that don't support basefee opcode) - return maxFeePerGas; - } - return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); - } - } - - /** - * Pack the user operation data into bytes for hashing. - * @param userOp - The user operation data. - */ - function encode( - PackedUserOperation calldata userOp - ) internal pure returns (bytes memory ret) { - address sender = getSender(userOp); - uint256 nonce = userOp.nonce; - bytes32 hashInitCode = calldataKeccak(userOp.initCode); - bytes32 hashCallData = calldataKeccak(userOp.callData); - bytes32 accountGasLimits = userOp.accountGasLimits; - uint256 preVerificationGas = userOp.preVerificationGas; - bytes32 gasFees = userOp.gasFees; - bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData); - - return abi.encode( - sender, nonce, - hashInitCode, hashCallData, - accountGasLimits, preVerificationGas, gasFees, - hashPaymasterAndData - ); - } - - function unpackUints( - bytes32 packed - ) internal pure returns (uint256 high128, uint256 low128) { - return (uint128(bytes16(packed)), uint128(uint256(packed))); - } - - //unpack just the high 128-bits from a packed value - function unpackHigh128(bytes32 packed) internal pure returns (uint256) { - return uint256(packed) >> 128; - } - - // unpack just the low 128-bits from a packed value - function unpackLow128(bytes32 packed) internal pure returns (uint256) { - return uint128(uint256(packed)); - } - - function unpackMaxPriorityFeePerGas(PackedUserOperation calldata userOp) - internal pure returns (uint256) { - return unpackHigh128(userOp.gasFees); - } - - function unpackMaxFeePerGas(PackedUserOperation calldata userOp) - internal pure returns (uint256) { - return unpackLow128(userOp.gasFees); - } - - function unpackVerificationGasLimit(PackedUserOperation calldata userOp) - internal pure returns (uint256) { - return unpackHigh128(userOp.accountGasLimits); - } - - function unpackCallGasLimit(PackedUserOperation calldata userOp) - internal pure returns (uint256) { - return unpackLow128(userOp.accountGasLimits); - } - - function unpackPaymasterVerificationGasLimit(PackedUserOperation calldata userOp) - internal pure returns (uint256) { - return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_POSTOP_GAS_OFFSET])); - } - - function unpackPostOpGasLimit(PackedUserOperation calldata userOp) - internal pure returns (uint256) { - return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET])); - } - - function unpackPaymasterStaticFields( - bytes calldata paymasterAndData - ) internal pure returns (address paymaster, uint256 validationGasLimit, uint256 postOpGasLimit) { - return ( - address(bytes20(paymasterAndData[: PAYMASTER_VALIDATION_GAS_OFFSET])), - uint128(bytes16(paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_POSTOP_GAS_OFFSET])), - uint128(bytes16(paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET])) - ); - } - - /** - * Hash the user operation data. - * @param userOp - The user operation data. - */ - function hash( - PackedUserOperation calldata userOp - ) internal pure returns (bytes32) { - return keccak256(encode(userOp)); - } -} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol deleted file mode 100644 index e3b355fbc27..00000000000 --- a/contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.7.5; - -import "./PackedUserOperation.sol"; - -interface IAccount { - /** - * Validate user's signature and nonce - * the entryPoint will make the call to the recipient only if this validation call returns successfully. - * signature failure should be reported by returning SIG_VALIDATION_FAILED (1). - * This allows making a "simulation call" without a valid signature - * Other failures (e.g. nonce mismatch, or invalid signature format) should still revert to signal failure. - * - * @dev Must validate caller is the entryPoint. - * Must validate the signature and nonce - * @param userOp - The operation that is about to be executed. - * @param userOpHash - Hash of the user's request data. can be used as the basis for signature. - * @param missingAccountFunds - Missing funds on the account's deposit in the entrypoint. - * This is the minimum amount to transfer to the sender(entryPoint) to be - * able to make the call. The excess is left as a deposit in the entrypoint - * for future calls. Can be withdrawn anytime using "entryPoint.withdrawTo()". - * In case there is a paymaster in the request (or the current deposit is high - * enough), this value will be zero. - * @return validationData - Packaged ValidationData structure. use `_packValidationData` and - * `_unpackValidationData` to encode and decode. - * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, - * otherwise, an address of an "authorizer" contract. - * <6-byte> validUntil - Last timestamp this operation is valid. 0 for "indefinite" - * <6-byte> validAfter - First timestamp this operation is valid - * If an account doesn't use time-range, it is enough to - * return SIG_VALIDATION_FAILED value (1) for signature failure. - * Note that the validation code cannot use block.timestamp (or block.number) directly. - */ - function validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash, - uint256 missingAccountFunds - ) external returns (uint256 validationData); -} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol deleted file mode 100644 index 4433c80ce3e..00000000000 --- a/contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.7.5; - -import "./PackedUserOperation.sol"; - -interface IAccountExecute { - /** - * Account may implement this execute method. - * passing this methodSig at the beginning of callData will cause the entryPoint to pass the full UserOp (and hash) - * to the account. - * The account should skip the methodSig, and use the callData (and optionally, other UserOp fields) - * - * @param userOp - The operation that was just validated. - * @param userOpHash - Hash of the user's request data. - */ - function executeUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash - ) external; -} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol deleted file mode 100644 index 070d8f27a2c..00000000000 --- a/contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.7.5; - -import "./PackedUserOperation.sol"; - -/** - * Aggregated Signatures validator. - */ -interface IAggregator { - /** - * Validate aggregated signature. - * Revert if the aggregated signature does not match the given list of operations. - * @param userOps - Array of UserOperations to validate the signature for. - * @param signature - The aggregated signature. - */ - function validateSignatures( - PackedUserOperation[] calldata userOps, - bytes calldata signature - ) external view; - - /** - * Validate signature of a single userOp. - * This method should be called by bundler after EntryPointSimulation.simulateValidation() returns - * the aggregator this account uses. - * First it validates the signature over the userOp. Then it returns data to be used when creating the handleOps. - * @param userOp - The userOperation received from the user. - * @return sigForUserOp - The value to put into the signature field of the userOp when calling handleOps. - * (usually empty, unless account and aggregator support some kind of "multisig". - */ - function validateUserOpSignature( - PackedUserOperation calldata userOp - ) external view returns (bytes memory sigForUserOp); - - /** - * Aggregate multiple signatures into a single value. - * This method is called off-chain to calculate the signature to pass with handleOps() - * bundler MAY use optimized custom code perform this aggregation. - * @param userOps - Array of UserOperations to collect the signatures from. - * @return aggregatedSignature - The aggregated signature. - */ - function aggregateSignatures( - PackedUserOperation[] calldata userOps - ) external view returns (bytes memory aggregatedSignature); -} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol deleted file mode 100644 index 28c26f98e6c..00000000000 --- a/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol +++ /dev/null @@ -1,223 +0,0 @@ -/** - ** Account-Abstraction (EIP-4337) singleton EntryPoint implementation. - ** Only one instance required on each chain. - **/ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.7.5; - -/* solhint-disable avoid-low-level-calls */ -/* solhint-disable no-inline-assembly */ -/* solhint-disable reason-string */ - -import "./PackedUserOperation.sol"; -import "./IStakeManager.sol"; -import "./IAggregator.sol"; -import "./INonceManager.sol"; - -interface IEntryPoint is IStakeManager, INonceManager { - /*** - * An event emitted after each successful request. - * @param userOpHash - Unique identifier for the request (hash its entire content, except signature). - * @param sender - The account that generates this request. - * @param paymaster - If non-null, the paymaster that pays for this request. - * @param nonce - The nonce value from the request. - * @param success - True if the sender transaction succeeded, false if reverted. - * @param actualGasCost - Actual amount paid (by account or paymaster) for this UserOperation. - * @param actualGasUsed - Total gas used by this UserOperation (including preVerification, creation, - * validation and execution). - */ - event UserOperationEvent( - bytes32 indexed userOpHash, - address indexed sender, - address indexed paymaster, - uint256 nonce, - bool success, - uint256 actualGasCost, - uint256 actualGasUsed - ); - - /** - * Account "sender" was deployed. - * @param userOpHash - The userOp that deployed this account. UserOperationEvent will follow. - * @param sender - The account that is deployed - * @param factory - The factory used to deploy this account (in the initCode) - * @param paymaster - The paymaster used by this UserOp - */ - event AccountDeployed( - bytes32 indexed userOpHash, - address indexed sender, - address factory, - address paymaster - ); - - /** - * An event emitted if the UserOperation "callData" reverted with non-zero length. - * @param userOpHash - The request unique identifier. - * @param sender - The sender of this request. - * @param nonce - The nonce used in the request. - * @param revertReason - The return bytes from the (reverted) call to "callData". - */ - event UserOperationRevertReason( - bytes32 indexed userOpHash, - address indexed sender, - uint256 nonce, - bytes revertReason - ); - - /** - * An event emitted if the UserOperation Paymaster's "postOp" call reverted with non-zero length. - * @param userOpHash - The request unique identifier. - * @param sender - The sender of this request. - * @param nonce - The nonce used in the request. - * @param revertReason - The return bytes from the (reverted) call to "callData". - */ - event PostOpRevertReason( - bytes32 indexed userOpHash, - address indexed sender, - uint256 nonce, - bytes revertReason - ); - - /** - * UserOp consumed more than prefund. The UserOperation is reverted, and no refund is made. - * @param userOpHash - The request unique identifier. - * @param sender - The sender of this request. - * @param nonce - The nonce used in the request. - */ - event UserOperationPrefundTooLow( - bytes32 indexed userOpHash, - address indexed sender, - uint256 nonce - ); - - /** - * An event emitted by handleOps(), before starting the execution loop. - * Any event emitted before this event, is part of the validation. - */ - event BeforeExecution(); - - /** - * Signature aggregator used by the following UserOperationEvents within this bundle. - * @param aggregator - The aggregator used for the following UserOperationEvents. - */ - event SignatureAggregatorChanged(address indexed aggregator); - - /** - * A custom revert error of handleOps, to identify the offending op. - * Should be caught in off-chain handleOps simulation and not happen on-chain. - * Useful for mitigating DoS attempts against batchers or for troubleshooting of factory/account/paymaster reverts. - * NOTE: If simulateValidation passes successfully, there should be no reason for handleOps to fail on it. - * @param opIndex - Index into the array of ops to the failed one (in simulateValidation, this is always zero). - * @param reason - Revert reason. The string starts with a unique code "AAmn", - * where "m" is "1" for factory, "2" for account and "3" for paymaster issues, - * so a failure can be attributed to the correct entity. - */ - error FailedOp(uint256 opIndex, string reason); - - /** - * 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 - * @dev note that inner is truncated to 2048 bytes - */ - error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); - - error PostOpReverted(bytes returnData); - - /** - * Error case when a signature aggregator fails to verify the aggregated signature it had created. - * @param aggregator The aggregator that failed to verify the signature - */ - error SignatureValidationFailed(address aggregator); - - // Return value of getSenderAddress. - error SenderAddressResult(address sender); - - // UserOps handled, per aggregator. - struct UserOpsPerAggregator { - PackedUserOperation[] userOps; - // Aggregator address - IAggregator aggregator; - // Aggregated signature - bytes signature; - } - - /** - * Execute a batch of UserOperations. - * No signature aggregator is used. - * If any account requires an aggregator (that is, it returned an aggregator when - * performing simulateValidation), then handleAggregatedOps() must be used instead. - * @param ops - The operations to execute. - * @param beneficiary - The address to receive the fees. - */ - function handleOps( - PackedUserOperation[] calldata ops, - address payable beneficiary - ) external; - - /** - * Execute a batch of UserOperation with Aggregators - * @param opsPerAggregator - The operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts). - * @param beneficiary - The address to receive the fees. - */ - function handleAggregatedOps( - UserOpsPerAggregator[] calldata opsPerAggregator, - address payable beneficiary - ) external; - - /** - * Generate a request Id - unique identifier for this request. - * The request ID is a hash over the content of the userOp (except the signature), the entrypoint and the chainid. - * @param userOp - The user operation to generate the request ID for. - * @return hash the hash of this UserOperation - */ - function getUserOpHash( - PackedUserOperation calldata userOp - ) external view returns (bytes32); - - /** - * Gas and return values during simulation. - * @param preOpGas - The gas used for validation (including preValidationGas) - * @param prefund - The required prefund for this operation - * @param accountValidationData - returned validationData from account. - * @param paymasterValidationData - return validationData from paymaster. - * @param paymasterContext - Returned by validatePaymasterUserOp (to be passed into postOp) - */ - struct ReturnInfo { - uint256 preOpGas; - uint256 prefund; - uint256 accountValidationData; - uint256 paymasterValidationData; - bytes paymasterContext; - } - - /** - * Returned aggregated signature info: - * The aggregator returned by the account, and its current stake. - */ - struct AggregatorStakeInfo { - address aggregator; - StakeInfo stakeInfo; - } - - /** - * Get counterfactual sender address. - * Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation. - * This method always revert, and returns the address in SenderAddressResult error - * @param initCode - The constructor code to be passed into the UserOperation. - */ - function getSenderAddress(bytes memory initCode) external; - - error DelegateAndRevert(bool success, bytes ret); - - /** - * Helper method for dry-run testing. - * @dev calling this method, the EntryPoint will make a delegatecall to the given data, and report (via revert) the result. - * The method always revert, so is only useful off-chain for dry run calls, in cases where state-override to replace - * actual EntryPoint code is less convenient. - * @param target a target contract to make a delegatecall from entrypoint - * @param data data to pass to target in a delegatecall - */ - function delegateAndRevert(address target, bytes calldata data) external; -} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol b/contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol deleted file mode 100644 index 2f993f6875c..00000000000 --- a/contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.7.5; - -interface INonceManager { - - /** - * Return the next nonce for this sender. - * Within a given key, the nonce values are sequenced (starting with zero, and incremented by one on each userop) - * But UserOp with different keys can come with arbitrary order. - * - * @param sender the account address - * @param key the high 192 bit of the nonce - * @return nonce a full nonce to pass for next UserOp with this sender. - */ - function getNonce(address sender, uint192 key) - external view returns (uint256 nonce); - - /** - * Manually increment the nonce of the sender. - * This method is exposed just for completeness.. - * Account does NOT need to call it, neither during validation, nor elsewhere, - * as the EntryPoint will update the nonce regardless. - * Possible use-case is call it with various keys to "initialize" their nonces to one, so that future - * UserOperations will not pay extra for the first transaction with a given key. - */ - function incrementNonce(uint192 key) external; -} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol deleted file mode 100644 index 9176a0b242a..00000000000 --- a/contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.7.5; - -import "./PackedUserOperation.sol"; - -/** - * The interface exposed by a paymaster contract, who agrees to pay the gas for user's operations. - * A paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction. - */ -interface IPaymaster { - enum PostOpMode { - // User op succeeded. - opSucceeded, - // User op reverted. Still has to pay for gas. - opReverted, - // Only used internally in the EntryPoint (cleanup after postOp reverts). Never calling paymaster with this value - postOpReverted - } - - /** - * Payment validation: check if paymaster agrees to pay. - * Must verify sender is the entryPoint. - * Revert to reject this request. - * Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted). - * The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns. - * @param userOp - The user operation. - * @param userOpHash - Hash of the user's request data. - * @param maxCost - The maximum cost of this transaction (based on maximum gas and gas price from userOp). - * @return context - Value to send to a postOp. Zero length to signify postOp is not required. - * @return validationData - Signature and time-range of this operation, encoded the same as the return - * value of validateUserOperation. - * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, - * other values are invalid for paymaster. - * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" - * <6-byte> validAfter - first timestamp this operation is valid - * Note that the validation code cannot use block.timestamp (or block.number) directly. - */ - function validatePaymasterUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash, - uint256 maxCost - ) external returns (bytes memory context, uint256 validationData); - - /** - * Post-operation handler. - * Must verify sender is the entryPoint. - * @param mode - Enum with the following options: - * opSucceeded - User operation succeeded. - * opReverted - User op reverted. The paymaster still has to pay for gas. - * postOpReverted - never passed in a call to postOp(). - * @param context - The context value returned by validatePaymasterUserOp - * @param actualGasCost - Actual gas used so far (without this postOp call). - * @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas - * and maxPriorityFee (and basefee) - * It is not the same as tx.gasprice, which is what the bundler pays. - */ - function postOp( - PostOpMode mode, - bytes calldata context, - uint256 actualGasCost, - uint256 actualUserOpFeePerGas - ) external; -} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol deleted file mode 100644 index 69083e93f5a..00000000000 --- a/contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity >=0.7.5; - -/** - * Manage deposits and stakes. - * Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account). - * Stake is value locked for at least "unstakeDelay" by the staked entity. - */ -interface IStakeManager { - event Deposited(address indexed account, uint256 totalDeposit); - - event Withdrawn( - address indexed account, - address withdrawAddress, - uint256 amount - ); - - // Emitted when stake or unstake delay are modified. - event StakeLocked( - address indexed account, - uint256 totalStaked, - uint256 unstakeDelaySec - ); - - // Emitted once a stake is scheduled for withdrawal. - event StakeUnlocked(address indexed account, uint256 withdrawTime); - - event StakeWithdrawn( - address indexed account, - address withdrawAddress, - uint256 amount - ); - - /** - * @param deposit - The entity's deposit. - * @param staked - True if this entity is staked. - * @param stake - Actual amount of ether staked for this entity. - * @param unstakeDelaySec - Minimum delay to withdraw the stake. - * @param withdrawTime - First block timestamp where 'withdrawStake' will be callable, or zero if already locked. - * @dev Sizes were chosen so that deposit fits into one cell (used during handleOp) - * and the rest fit into a 2nd cell (used during stake/unstake) - * - 112 bit allows for 10^15 eth - * - 48 bit for full timestamp - * - 32 bit allows 150 years for unstake delay - */ - struct DepositInfo { - uint256 deposit; - bool staked; - uint112 stake; - uint32 unstakeDelaySec; - uint48 withdrawTime; - } - - // API struct used by getStakeInfo and simulateValidation. - struct StakeInfo { - uint256 stake; - uint256 unstakeDelaySec; - } - - /** - * Get deposit info. - * @param account - The account to query. - * @return info - Full deposit information of given account. - */ - function getDepositInfo( - address account - ) external view returns (DepositInfo memory info); - - /** - * Get account balance. - * @param account - The account to query. - * @return - The deposit (for gas payment) of the account. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * Add to the deposit of the given account. - * @param account - The account to add to. - */ - function depositTo(address account) external payable; - - /** - * Add to the account's stake - amount and delay - * any pending unstake is first cancelled. - * @param _unstakeDelaySec - The new lock duration before the deposit can be withdrawn. - */ - function addStake(uint32 _unstakeDelaySec) external payable; - - /** - * Attempt to unlock the stake. - * The value can be withdrawn (using withdrawStake) after the unstake delay. - */ - function unlockStake() external; - - /** - * Withdraw from the (unlocked) stake. - * Must first call unlockStake and wait for the unstakeDelay to pass. - * @param withdrawAddress - The address to send withdrawn value. - */ - function withdrawStake(address payable withdrawAddress) external; - - /** - * Withdraw from the deposit. - * @param withdrawAddress - The address to send withdrawn value. - * @param withdrawAmount - The amount to withdraw. - */ - function withdrawTo( - address payable withdrawAddress, - uint256 withdrawAmount - ) external; -} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol b/contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol deleted file mode 100644 index fe20de56573..00000000000 --- a/contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.7.5; - -/** - * User Operation struct - * @param sender - The sender account of this request. - * @param nonce - Unique value the sender uses to verify it is not a replay. - * @param initCode - If set, the account contract will be created by this constructor/ - * @param callData - The method call to execute on this account. - * @param accountGasLimits - Packed gas limits for validateUserOp and gas limit passed to the callData method call. - * @param preVerificationGas - Gas not calculated by the handleOps method, but added to the gas paid. - * Covers batch overhead. - * @param gasFees - packed gas fields maxPriorityFeePerGas and maxFeePerGas - Same as EIP-1559 gas parameters. - * @param paymasterAndData - If set, this field holds the paymaster address, verification gas limit, postOp gas limit and paymaster-specific extra data - * The paymaster will pay for the transaction instead of the sender. - * @param signature - Sender-verified signature over the entire request, the EntryPoint address and the chain ID. - */ -struct PackedUserOperation { - address sender; - uint256 nonce; - bytes initCode; - bytes callData; - bytes32 accountGasLimits; - uint256 preVerificationGas; - bytes32 gasFees; - bytes paymasterAndData; - bytes signature; -} diff --git a/contracts/vendor/erc4337-entrypoint/utils/Exec.sol b/contracts/vendor/erc4337-entrypoint/utils/Exec.sol deleted file mode 100644 index ee8d71ac0d8..00000000000 --- a/contracts/vendor/erc4337-entrypoint/utils/Exec.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.23; - -// solhint-disable no-inline-assembly - -/** - * Utility functions helpful when making different kinds of contract calls in Solidity. - */ -library Exec { - - function call( - address to, - uint256 value, - bytes memory data, - uint256 txGas - ) internal returns (bool success) { - assembly ("memory-safe") { - success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) - } - } - - function staticcall( - address to, - bytes memory data, - uint256 txGas - ) internal view returns (bool success) { - assembly ("memory-safe") { - success := staticcall(txGas, to, add(data, 0x20), mload(data), 0, 0) - } - } - - function delegateCall( - address to, - bytes memory data, - uint256 txGas - ) internal returns (bool success) { - assembly ("memory-safe") { - success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) - } - } - - // get returned data from last call or calldelegate - function getReturnData(uint256 maxLen) internal pure returns (bytes memory returnData) { - assembly ("memory-safe") { - let len := returndatasize() - if gt(len, maxLen) { - len := maxLen - } - let ptr := mload(0x40) - mstore(0x40, add(ptr, add(len, 0x20))) - mstore(ptr, len) - returndatacopy(add(ptr, 0x20), 0, len) - returnData := ptr - } - } - - // revert with explicit byte array (probably reverted info from call) - function revertWithData(bytes memory returnData) internal pure { - assembly ("memory-safe") { - revert(add(returnData, 32), mload(returnData)) - } - } - - function callAndRevert(address to, bytes memory data, uint256 maxLen) internal { - bool success = call(to,0,data,gasleft()); - if (!success) { - revertWithData(getReturnData(maxLen)); - } - } -} diff --git a/package.json b/package.json index 3d809897dd2..f9b88273362 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,7 @@ "private": true, "files": [ "/contracts/**/*.sol", - "!/contracts/mocks/**/*", - "!/contracts/vendor/erc4337-entrypoint" + "!/contracts/mocks/**/*" ], "scripts": { "compile": "hardhat compile", @@ -20,8 +19,8 @@ "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", "lint:js": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint --ignore-path .gitignore .", "lint:js:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint --ignore-path .gitignore . --fix", - "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/!(vendor)/**/*.sol' --check && solhint '{contracts,test}/!(vendor)/**/*.sol'", - "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/!(vendor)/**/*.sol' '!contracts/vendor/**/*.sol' --write", + "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", + "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", "clean": "hardhat clean && rimraf build contracts/build", "prepack": "scripts/prepack.sh", "generate": "scripts/generate/run.js", diff --git a/scripts/upgradeable/patch-apply.sh b/scripts/upgradeable/patch-apply.sh index e16cf76a4c2..d9e17589b05 100755 --- a/scripts/upgradeable/patch-apply.sh +++ b/scripts/upgradeable/patch-apply.sh @@ -3,7 +3,7 @@ set -euo pipefail DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" -PATCH="$DIRNAME/${1:-upgradeable.patch}" +PATCH="$DIRNAME/upgradeable.patch" error() { echo Error: "$*" >&2 diff --git a/scripts/upgradeable/patch-save.sh b/scripts/upgradeable/patch-save.sh index de0cec6a5ef..111e6f1572a 100755 --- a/scripts/upgradeable/patch-save.sh +++ b/scripts/upgradeable/patch-save.sh @@ -3,7 +3,7 @@ set -euo pipefail DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" -PATCH="$DIRNAME/${1:-upgradeable.patch}" +PATCH="$DIRNAME/upgradeable.patch" error() { echo Error: "$*" >&2 diff --git a/scripts/upgradeable/transpile.sh b/scripts/upgradeable/transpile.sh index d31172ddf4e..f7c848c1320 100644 --- a/scripts/upgradeable/transpile.sh +++ b/scripts/upgradeable/transpile.sh @@ -5,16 +5,13 @@ set -euo pipefail -x VERSION="$(jq -r .version contracts/package.json)" DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" -# Apply patch to contracts that are transpiled bash "$DIRNAME/patch-apply.sh" sed -i'' -e "s//$VERSION/g" "contracts/package.json" git add contracts/package.json -# Build artifacts npm run clean npm run compile -# Check artifacts are correctly built build_info=($(jq -r '.input.sources | keys | if any(test("^contracts/mocks/.*\\bunreachable\\b")) then empty else input_filename end' artifacts/build-info/*)) build_info_num=${#build_info[@]} @@ -23,13 +20,10 @@ if [ $build_info_num -ne 1 ]; then exit 1 fi -# Apply changes to the excluded contracts (these don't need to in the artifact and may prevent compilation) -git apply -3 "$DIRNAME/upgradeable.excluded.patch" - # -D: delete original and excluded files # -b: use this build info file # -i: use included Initializable -# -x: exclude vendored and proxy-related contracts with a few exceptions +# -x: exclude proxy-related contracts with a few exceptions # -p: emit public initializer # -n: use namespaces # -N: exclude from namespaces transformation @@ -44,8 +38,6 @@ npx @openzeppelin/upgrade-safe-transpiler -D \ -x '!contracts/proxy/ERC1967/ERC1967Utils.sol' \ -x '!contracts/proxy/utils/UUPSUpgradeable.sol' \ -x '!contracts/proxy/beacon/IBeacon.sol' \ - -x 'contracts/vendor/**/*' \ - -x '!contracts/vendor/compound/ICompoundTimelock.sol' \ -p 'contracts/access/manager/AccessManager.sol' \ -p 'contracts/finance/VestingWallet.sol' \ -p 'contracts/governance/TimelockController.sol' \ diff --git a/scripts/upgradeable/upgradeable.excluded.patch b/scripts/upgradeable/upgradeable.excluded.patch deleted file mode 100644 index 7c49e21e123..00000000000 --- a/scripts/upgradeable/upgradeable.excluded.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol -index 778115b1..44501524 100644 ---- a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol -+++ b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol -@@ -15,10 +15,8 @@ import "./Helpers.sol"; - import "./NonceManager.sol"; - import "./UserOperationLib.sol"; - --// import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; --// import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; --import "../../../utils/introspection/ERC165.sol"; // OZ edit --import "../../../utils/ReentrancyGuard.sol"; // OZ edit -+import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; - - /* - * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. From 8a0251e1e45ff75a468521d3a6eef57c3e0edb1b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 21 Oct 2024 17:53:00 +0200 Subject: [PATCH 10/13] Update checks.yml --- .github/workflows/checks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6a78eda4d9f..2b488a1dcb7 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -6,7 +6,6 @@ on: - master - next-v* - release-v* - - account-abstraction pull_request: {} workflow_dispatch: {} From d41ccdeb81fdcf0bd16459136514c959c8d91f9c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 22 Oct 2024 13:40:07 +0200 Subject: [PATCH 11/13] Update contracts/account/utils/draft-ERC4337Utils.sol --- contracts/account/utils/draft-ERC4337Utils.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/account/utils/draft-ERC4337Utils.sol b/contracts/account/utils/draft-ERC4337Utils.sol index a66dd0f9c4b..6769308bade 100644 --- a/contracts/account/utils/draft-ERC4337Utils.sol +++ b/contracts/account/utils/draft-ERC4337Utils.sol @@ -71,7 +71,7 @@ library ERC4337Utils { return (address(0), false); } else { (address aggregator_, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData); - return (aggregator_, block.timestamp > validUntil || block.timestamp < validAfter); + return (aggregator_, block.timestamp < validAfter || validUntil < block.timestamp); } } From f3aae710c2f0934b94bd517d4dea937bdbbf2b6d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 22 Oct 2024 14:14:54 +0200 Subject: [PATCH 12/13] optimize code and cleanup tests --- .../account/utils/draft-ERC4337Utils.sol | 8 +- test/account/utils/draft-ERC4337Utils.test.js | 170 +++++++++--------- test/account/utils/draft-ERC7579Utils.test.js | 106 ++++++----- test/helpers/erc4337.js | 31 +++- 4 files changed, 168 insertions(+), 147 deletions(-) diff --git a/contracts/account/utils/draft-ERC4337Utils.sol b/contracts/account/utils/draft-ERC4337Utils.sol index 6769308bade..bf559b86d6e 100644 --- a/contracts/account/utils/draft-ERC4337Utils.sol +++ b/contracts/account/utils/draft-ERC4337Utils.sol @@ -67,12 +67,8 @@ library ERC4337Utils { /// @dev Returns the aggregator of the `validationData` and whether it is out of time range. function getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) { - if (validationData == 0) { - return (address(0), false); - } else { - (address aggregator_, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData); - return (aggregator_, block.timestamp < validAfter || validUntil < block.timestamp); - } + (address aggregator_, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData); + return (aggregator_, block.timestamp < validAfter || validUntil < block.timestamp); } /// @dev Computes the hash of a user operation with the current entrypoint and chainid. diff --git a/test/account/utils/draft-ERC4337Utils.test.js b/test/account/utils/draft-ERC4337Utils.test.js index c76f89845ac..38235b01934 100644 --- a/test/account/utils/draft-ERC4337Utils.test.js +++ b/test/account/utils/draft-ERC4337Utils.test.js @@ -1,15 +1,8 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { - SIG_VALIDATION_SUCCESS, - SIG_VALIDATION_FAILURE, - toAuthorizer, - packValidationData, - packPaymasterData, - UserOperation, -} = require('../../helpers/erc4337'); -const { ZeroAddress } = require('ethers'); + +const { packValidationData, packPaymasterData, UserOperation } = require('../../helpers/erc4337'); const { MAX_UINT48 } = require('../../helpers/constants'); const fixture = async () => { @@ -25,182 +18,193 @@ describe('ERC4337Utils', function () { describe('parseValidationData', function () { it('parses the validation data', async function () { - const authorizer = this.authorizer.address; + const authorizer = this.authorizer; const validUntil = 0x12345678n; const validAfter = 0x9abcdef0n; - const result = await this.utils.$parseValidationData(packValidationData(validAfter, validUntil, authorizer)); - expect(result).to.deep.equal([authorizer, validAfter, validUntil]); + const validationData = packValidationData(validAfter, validUntil, authorizer); + + expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([ + authorizer.address, + validAfter, + validUntil, + ]); }); it('returns an type(uint48).max if until is 0', async function () { - const authorizer = this.authorizer.address; + const authorizer = this.authorizer; const validAfter = 0x12345678n; - const result = await this.utils.$parseValidationData(packValidationData(validAfter, 0, authorizer)); - expect(result).to.deep.equal([authorizer, validAfter, MAX_UINT48]); + const validationData = packValidationData(validAfter, 0, authorizer); + + expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([ + authorizer.address, + validAfter, + MAX_UINT48, + ]); }); }); describe('packValidationData', function () { it('packs the validation data', async function () { - const authorizer = this.authorizer.address; + const authorizer = this.authorizer; const validUntil = 0x12345678n; const validAfter = 0x9abcdef0n; - const result = await this.utils.$packValidationData(ethers.Typed.address(authorizer), validAfter, validUntil); - expect(result).to.equal(packValidationData(validAfter, validUntil, authorizer)); + const validationData = packValidationData(validAfter, validUntil, authorizer); + + expect( + this.utils.$packValidationData(ethers.Typed.address(authorizer), validAfter, validUntil), + ).to.eventually.equal(validationData); }); it('packs the validation data (bool)', async function () { const success = false; const validUntil = 0x12345678n; const validAfter = 0x9abcdef0n; - const result = await this.utils.$packValidationData(ethers.Typed.bool(success), validAfter, validUntil); - expect(result).to.equal(packValidationData(validAfter, validUntil, toAuthorizer(SIG_VALIDATION_FAILURE))); + const validationData = packValidationData(validAfter, validUntil, false); + + expect(this.utils.$packValidationData(ethers.Typed.bool(success), validAfter, validUntil)).to.eventually.equal( + validationData, + ); }); }); describe('combineValidationData', function () { - it('combines the validation data', async function () { - const authorizer1 = ZeroAddress; - const validUntil1 = 0x12345678n; - const validAfter1 = 0x9abcdef0n; + const validUntil1 = 0x12345678n; + const validAfter1 = 0x9abcdef0n; + const validUntil2 = 0x87654321n; + const validAfter2 = 0xabcdef90n; - const authorizer2 = ZeroAddress; - const validUntil2 = 0x87654321n; - const validAfter2 = 0xabcdef90n; + it('combines the validation data', async function () { + const validationData1 = packValidationData(validAfter1, validUntil1, ethers.ZeroAddress); + const validationData2 = packValidationData(validAfter2, validUntil2, ethers.ZeroAddress); + const expected = packValidationData(validAfter2, validUntil1, true); - const result = await this.utils.$combineValidationData( - packValidationData(validAfter1, validUntil1, authorizer1), - packValidationData(validAfter2, validUntil2, authorizer2), - ); - expect(result).to.equal(packValidationData(validAfter2, validUntil1, toAuthorizer(SIG_VALIDATION_SUCCESS))); + // check symetry + expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected); + expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected); }); - // address(bytes20(keccak256('openzeppelin.erc4337.tests'))) - for (const authorizers of [ - [ZeroAddress, '0xbf023313b891fd6000544b79e353323aa94a4f29'], - ['0xbf023313b891fd6000544b79e353323aa94a4f29', ZeroAddress], + for (const [authorizer1, authorizer2] of [ + [ethers.ZeroAddress, '0xbf023313b891fd6000544b79e353323aa94a4f29'], + ['0xbf023313b891fd6000544b79e353323aa94a4f29', ethers.ZeroAddress], ]) { it('returns SIG_VALIDATION_FAILURE if one of the authorizers is not address(0)', async function () { - const validUntil1 = 0x12345678n; - const validAfter1 = 0x9abcdef0n; - - const validUntil2 = 0x87654321n; - const validAfter2 = 0xabcdef90n; + const validationData1 = packValidationData(validAfter1, validUntil1, authorizer1); + const validationData2 = packValidationData(validAfter2, validUntil2, authorizer2); + const expected = packValidationData(validAfter2, validUntil1, false); - const result = await this.utils.$combineValidationData( - packValidationData(validAfter1, validUntil1, authorizers[0]), - packValidationData(validAfter2, validUntil2, authorizers[1]), - ); - expect(result).to.equal(packValidationData(validAfter2, validUntil1, toAuthorizer(SIG_VALIDATION_FAILURE))); + // check symetry + expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected); + expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected); }); } }); describe('getValidationData', function () { it('returns the validation data with valid validity range', async function () { - const aggregator = this.authorizer.address; + const aggregator = this.authorizer; const validAfter = 0; const validUntil = MAX_UINT48; - const result = await this.utils.$getValidationData(packValidationData(validAfter, validUntil, aggregator)); - expect(result).to.deep.equal([aggregator, false]); + const validationData = packValidationData(validAfter, validUntil, aggregator); + + expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, false]); }); it('returns the validation data with invalid validity range (expired)', async function () { - const aggregator = this.authorizer.address; + const aggregator = this.authorizer; const validAfter = 0; const validUntil = 1; - const result = await this.utils.$getValidationData(packValidationData(validAfter, validUntil, aggregator)); - expect(result).to.deep.equal([aggregator, true]); + const validationData = packValidationData(validAfter, validUntil, aggregator); + + expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]); }); it('returns the validation data with invalid validity range (not yet valid)', async function () { - const aggregator = this.authorizer.address; + const aggregator = this.authorizer; const validAfter = MAX_UINT48; const validUntil = MAX_UINT48; - const result = await this.utils.$getValidationData(packValidationData(validAfter, validUntil, aggregator)); - expect(result).to.deep.equal([aggregator, true]); + const validationData = packValidationData(validAfter, validUntil, aggregator); + + expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]); }); it('returns address(0) and false for validationData = 0', function () { - return expect(this.utils.$getValidationData(0n)).to.eventually.deep.equal([ZeroAddress, false]); + expect(this.utils.$getValidationData(0n)).to.eventually.deep.equal([ethers.ZeroAddress, false]); }); }); describe('hash', function () { it('returns the user operation hash', async function () { - const userOp = new UserOperation({ sender: this.sender.address, nonce: 1 }); + const userOp = new UserOperation({ sender: this.sender, nonce: 1 }); const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId); - const hash = await this.utils.$hash(userOp.packed); - expect(hash).to.equal(userOp.hash(this.utils.target, chainId)); + + expect(this.utils.$hash(userOp.packed)).to.eventually.equal(userOp.hash(this.utils.target, chainId)); }); it('returns the operation hash with specified entrypoint and chainId', async function () { - const userOp = new UserOperation({ sender: this.sender.address, nonce: 1 }); + const userOp = new UserOperation({ sender: this.sender, nonce: 1 }); const chainId = 0xdeadbeef; - const hash = await this.utils.$hash(userOp.packed, this.entrypoint.address, chainId); - expect(hash).to.equal(userOp.hash(this.entrypoint.address, chainId)); + + expect(this.utils.$hash(userOp.packed, this.entrypoint, chainId)).to.eventually.equal( + userOp.hash(this.entrypoint, chainId), + ); }); }); describe('userOp values', function () { it('returns verificationGasLimit', async function () { - const userOp = new UserOperation({ sender: this.sender.address, nonce: 1, verificationGas: 0x12345678n }); - expect(await this.utils.$verificationGasLimit(userOp.packed)).to.equal(userOp.verificationGas); + const userOp = new UserOperation({ sender: this.sender, nonce: 1, verificationGas: 0x12345678n }); + expect(this.utils.$verificationGasLimit(userOp.packed)).to.eventually.equal(userOp.verificationGas); }); it('returns callGasLimit', async function () { - const userOp = new UserOperation({ sender: this.sender.address, nonce: 1, callGas: 0x12345678n }); - expect(await this.utils.$callGasLimit(userOp.packed)).to.equal(userOp.callGas); + const userOp = new UserOperation({ sender: this.sender, nonce: 1, callGas: 0x12345678n }); + expect(this.utils.$callGasLimit(userOp.packed)).to.eventually.equal(userOp.callGas); }); it('returns maxPriorityFeePerGas', async function () { - const userOp = new UserOperation({ sender: this.sender.address, nonce: 1, maxPriorityFee: 0x12345678n }); - expect(await this.utils.$maxPriorityFeePerGas(userOp.packed)).to.equal(userOp.maxPriorityFee); + const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxPriorityFee: 0x12345678n }); + expect(this.utils.$maxPriorityFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee); }); it('returns maxFeePerGas', async function () { - const userOp = new UserOperation({ sender: this.sender.address, nonce: 1, maxFeePerGas: 0x12345678n }); - expect(await this.utils.$maxFeePerGas(userOp.packed)).to.equal(userOp.maxFeePerGas); + const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxFeePerGas: 0x12345678n }); + expect(this.utils.$maxFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxFeePerGas); }); it('returns gasPrice', async function () { const userOp = new UserOperation({ - sender: this.sender.address, + sender: this.sender, nonce: 1, maxPriorityFee: 0x12345678n, maxFeePerGas: 0x87654321n, }); - expect( - await this.utils['$gasPrice((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes))'](userOp.packed), - ).to.equal(userOp.maxPriorityFee); + expect(this.utils.$gasPrice(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee); }); describe('paymasterAndData', function () { beforeEach(async function () { this.verificationGasLimit = 0x12345678n; this.postOpGasLimit = 0x87654321n; - this.paymasterAndData = packPaymasterData( - this.paymaster.address, - this.verificationGasLimit, - this.postOpGasLimit, - ); + this.paymasterAndData = packPaymasterData(this.paymaster, this.verificationGasLimit, this.postOpGasLimit); this.userOp = new UserOperation({ - sender: this.sender.address, + sender: this.sender, nonce: 1, paymasterAndData: this.paymasterAndData, }); }); it('returns paymaster', async function () { - expect(await this.utils.$paymaster(this.userOp.packed)).to.equal(this.paymaster.address); + expect(this.utils.$paymaster(this.userOp.packed)).to.eventually.equal(this.paymaster); }); it('returns verificationGasLimit', async function () { - expect(await this.utils.$paymasterVerificationGasLimit(this.userOp.packed)).to.equal(this.verificationGasLimit); + expect(this.utils.$paymasterVerificationGasLimit(this.userOp.packed)).to.eventually.equal( + this.verificationGasLimit, + ); }); it('returns postOpGasLimit', async function () { - expect(await this.utils.$paymasterPostOpGasLimit(this.userOp.packed)).to.equal(this.postOpGasLimit); + expect(this.utils.$paymasterPostOpGasLimit(this.userOp.packed)).to.eventually.equal(this.postOpGasLimit); }); }); }); diff --git a/test/account/utils/draft-ERC7579Utils.test.js b/test/account/utils/draft-ERC7579Utils.test.js index 9d6154f78bd..cc3b70425dd 100644 --- a/test/account/utils/draft-ERC7579Utils.test.js +++ b/test/account/utils/draft-ERC7579Utils.test.js @@ -34,8 +34,10 @@ describe('ERC7579Utils', function () { it('calls the target with value', async function () { const value = 0x012; const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction')); + await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.emit(this.target, 'MockFunctionCalled'); - expect(await ethers.provider.getBalance(this.target)).to.equal(value); + + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value); }); it('calls the target with value and args', async function () { @@ -45,10 +47,12 @@ describe('ERC7579Utils', function () { value, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']), ); + await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)) .to.emit(this.target, 'MockFunctionCalledWithArgs') .withArgs(42, '0x1234'); - expect(await ethers.provider.getBalance(this.target)).to.equal(value); + + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value); }); it('reverts when target reverts in default ExecType', async function () { @@ -58,6 +62,7 @@ describe('ERC7579Utils', function () { value, this.target.interface.encodeFunctionData('mockFunctionRevertsReason'), ); + await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting'); }); @@ -83,6 +88,7 @@ describe('ERC7579Utils', function () { it('reverts with an invalid exec type', async function () { const value = 0x012; const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction')); + await expect(this.utils.$execSingle('0x03', data)) .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType') .withArgs('0x03'); @@ -97,11 +103,13 @@ describe('ERC7579Utils', function () { [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')], ); + await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data)) .to.emit(this.target, 'MockFunctionCalled') .to.emit(this.anotherTarget, 'MockFunctionCalled'); - expect(await ethers.provider.getBalance(this.target)).to.equal(value1); - expect(await ethers.provider.getBalance(this.anotherTarget)).to.equal(value2); + + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1); + expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2); }); it('calls the targets with value and args', async function () { @@ -115,11 +123,13 @@ describe('ERC7579Utils', function () { this.anotherTarget.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']), ], ); + await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data)) .to.emit(this.target, 'MockFunctionCalledWithArgs') .to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs'); - expect(await ethers.provider.getBalance(this.target)).to.equal(value1); - expect(await ethers.provider.getBalance(this.anotherTarget)).to.equal(value2); + + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1); + expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2); }); it('reverts when any target reverts in default ExecType', async function () { @@ -129,6 +139,7 @@ describe('ERC7579Utils', function () { [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')], ); + await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting'); }); @@ -151,8 +162,8 @@ describe('ERC7579Utils', function () { ); // Check balances - expect(await ethers.provider.getBalance(this.target)).to.equal(value1); - expect(await ethers.provider.getBalance(this.anotherTarget)).to.equal(0); + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1); + expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(0); }); it('reverts with an invalid exec type', async function () { @@ -162,6 +173,7 @@ describe('ERC7579Utils', function () { [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')], ); + await expect(this.utils.$execBatch('0x03', data)) .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType') .withArgs('0x03'); @@ -172,15 +184,14 @@ describe('ERC7579Utils', function () { it('delegate calls the target', async function () { const slot = ethers.hexlify(ethers.randomBytes(32)); const value = ethers.hexlify(ethers.randomBytes(32)); - const data = encodeDelegate( this.target, this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]), ); - expect(await ethers.provider.getStorage(this.utils.target, slot)).to.equal(ethers.ZeroHash); + expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(ethers.ZeroHash); await this.utils.$execDelegateCall(EXEC_TYPE_DEFAULT, data); - expect(await ethers.provider.getStorage(this.utils.target, slot)).to.equal(value); + expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(value); }); it('reverts when target reverts in default ExecType', async function () { @@ -217,8 +228,7 @@ describe('ERC7579Utils', function () { const selector = '0x12345678'; const payload = ethers.toBeHex(0, 22); - const mode = await this.utils.$encodeMode(callType, execType, selector, payload); - expect(mode).to.equal( + expect(this.utils.$encodeMode(callType, execType, selector, payload)).to.eventually.equal( encodeMode({ callType, execType, @@ -229,19 +239,21 @@ describe('ERC7579Utils', function () { }); it('decodes Mode', async function () { - const mode = encodeMode({ - callType: CALL_TYPE_BATCH, - execType: EXEC_TYPE_TRY, - selector: '0x12345678', - payload: ethers.toBeHex(0, 22), - }); + const callType = CALL_TYPE_BATCH; + const execType = EXEC_TYPE_TRY; + const selector = '0x12345678'; + const payload = ethers.toBeHex(0, 22); - expect(await this.utils.$decodeMode(mode)).to.deep.eq([ - CALL_TYPE_BATCH, - EXEC_TYPE_TRY, - '0x12345678', - ethers.toBeHex(0, 22), - ]); + expect( + this.utils.$decodeMode( + encodeMode({ + callType, + execType, + selector, + payload, + }), + ), + ).to.eventually.deep.equal([callType, execType, selector, payload]); }); it('encodes single', async function () { @@ -249,8 +261,7 @@ describe('ERC7579Utils', function () { const value = 0x123; const data = '0x12345678'; - const encoded = await this.utils.$encodeSingle(target, value, data); - expect(encoded).to.equal(encodeSingle(target, value, data)); + expect(this.utils.$encodeSingle(target, value, data)).to.eventually.equal(encodeSingle(target, value, data)); }); it('decodes single', async function () { @@ -258,8 +269,11 @@ describe('ERC7579Utils', function () { const value = 0x123; const data = '0x12345678'; - const encoded = encodeSingle(target, value, data); - expect(await this.utils.$decodeSingle(encoded)).to.deep.eq([target.target, value, data]); + expect(this.utils.$decodeSingle(encodeSingle(target, value, data))).to.eventually.deep.equal([ + target.target, + value, + data, + ]); }); it('encodes batch', async function () { @@ -268,8 +282,7 @@ describe('ERC7579Utils', function () { [this.anotherTarget, 0x456, '0x12345678'], ]; - const encoded = await this.utils.$encodeBatch(entries); - expect(encoded).to.equal(encodeBatch(...entries)); + expect(this.utils.$encodeBatch(entries)).to.eventually.equal(encodeBatch(...entries)); }); it('decodes batch', async function () { @@ -278,68 +291,63 @@ describe('ERC7579Utils', function () { [this.anotherTarget.target, 0x456, '0x12345678'], ]; - const encoded = encodeBatch(...entries); - expect(await this.utils.$decodeBatch(encoded)).to.deep.eq(entries); + expect(this.utils.$decodeBatch(encodeBatch(...entries))).to.eventually.deep.equal(entries); }); it('encodes delegate', async function () { const target = this.target; const data = '0x12345678'; - const encoded = await this.utils.$encodeDelegate(target, data); - expect(encoded).to.equal(encodeDelegate(target, data)); + expect(this.utils.$encodeDelegate(target, data)).to.eventually.equal(encodeDelegate(target, data)); }); it('decodes delegate', async function () { const target = this.target; const data = '0x12345678'; - const encoded = encodeDelegate(target, data); - expect(await this.utils.$decodeDelegate(encoded)).to.deep.eq([target.target, data]); + expect(this.utils.$decodeDelegate(encodeDelegate(target, data))).to.eventually.deep.equal([target.target, data]); }); describe('global', function () { describe('eqCallTypeGlobal', function () { it('returns true if both call types are equal', async function () { - const callType = CALL_TYPE_BATCH; - expect(await this.utilsGlobal.$eqCallTypeGlobal(callType, callType)).to.be.true; + expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_BATCH, CALL_TYPE_BATCH)).to.eventually.be.true; }); it('returns false if both call types are different', async function () { - expect(await this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_CALL, CALL_TYPE_BATCH)).to.be.false; + expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_CALL, CALL_TYPE_BATCH)).to.eventually.be.false; }); }); describe('eqExecTypeGlobal', function () { it('returns true if both exec types are equal', async function () { - const execType = EXEC_TYPE_TRY; - expect(await this.utilsGlobal.$eqExecTypeGlobal(execType, execType)).to.be.true; + expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_TRY, EXEC_TYPE_TRY)).to.eventually.be.true; }); it('returns false if both exec types are different', async function () { - expect(await this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY)).to.be.false; + expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY)).to.eventually.be.false; }); }); describe('eqModeSelectorGlobal', function () { it('returns true if both selectors are equal', async function () { - const selector = '0x12345678'; - expect(await this.utilsGlobal.$eqModeSelectorGlobal(selector, selector)).to.be.true; + expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x12345678')).to.eventually.be.true; }); it('returns false if both selectors are different', async function () { - expect(await this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x87654321')).to.be.false; + expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x87654321')).to.eventually.be.false; }); }); describe('eqModePayloadGlobal', function () { it('returns true if both payloads are equal', async function () { - const payload = ethers.toBeHex(0, 22); - expect(await this.utilsGlobal.$eqModePayloadGlobal(payload, payload)).to.be.true; + expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(0, 22))).to.eventually.be + .true; }); it('returns false if both payloads are different', async function () { - expect(await this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(1, 22))).to.be.false; + expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(1, 22))).to.eventually.be + .false; }); }); }); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index c529f5f64cf..5901375b174 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -1,10 +1,10 @@ const { ethers } = require('hardhat'); -const SIG_VALIDATION_SUCCESS = 0; -const SIG_VALIDATION_FAILURE = 1; +const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000'; +const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001'; -function toAuthorizer(sigValidatonSuccess) { - return `0x000000000000000000000000000000000000000${sigValidatonSuccess}`; +function getAddress(account) { + return account.target ?? account.address ?? account; } function pack(left, right) { @@ -12,17 +12,31 @@ function pack(left, right) { } function packValidationData(validAfter, validUntil, authorizer) { - return ethers.solidityPacked(['uint48', 'uint48', 'address'], [validAfter, validUntil, authorizer]); + return ethers.solidityPacked( + ['uint48', 'uint48', 'address'], + [ + validAfter, + validUntil, + typeof authorizer == 'boolean' + ? authorizer + ? SIG_VALIDATION_SUCCESS + : SIG_VALIDATION_FAILURE + : getAddress(authorizer), + ], + ); } function packPaymasterData(paymaster, verificationGasLimit, postOpGasLimit) { - return ethers.solidityPacked(['address', 'uint128', 'uint128'], [paymaster, verificationGasLimit, postOpGasLimit]); + return ethers.solidityPacked( + ['address', 'uint128', 'uint128'], + [getAddress(paymaster), verificationGasLimit, postOpGasLimit], + ); } /// Represent one user operation class UserOperation { constructor(params) { - this.sender = params.sender; + this.sender = getAddress(params.sender); this.nonce = params.nonce; this.initCode = params.initCode ?? '0x'; this.callData = params.callData ?? '0x'; @@ -67,7 +81,7 @@ class UserOperation { ), ); return ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(['bytes32', 'address', 'uint256'], [h, entrypoint, chainId]), + ethers.AbiCoder.defaultAbiCoder().encode(['bytes32', 'address', 'uint256'], [h, getAddress(entrypoint), chainId]), ); } } @@ -75,7 +89,6 @@ class UserOperation { module.exports = { SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILURE, - toAuthorizer, packValidationData, packPaymasterData, UserOperation, From 39dc80231493b2a933a46c4102018a6de3bfd825 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 22 Oct 2024 14:36:04 +0200 Subject: [PATCH 13/13] codespell --- test/account/utils/draft-ERC4337Utils.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/account/utils/draft-ERC4337Utils.test.js b/test/account/utils/draft-ERC4337Utils.test.js index 38235b01934..8374bc745c9 100644 --- a/test/account/utils/draft-ERC4337Utils.test.js +++ b/test/account/utils/draft-ERC4337Utils.test.js @@ -78,7 +78,7 @@ describe('ERC4337Utils', function () { const validationData2 = packValidationData(validAfter2, validUntil2, ethers.ZeroAddress); const expected = packValidationData(validAfter2, validUntil1, true); - // check symetry + // check symmetry expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected); expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected); }); @@ -92,7 +92,7 @@ describe('ERC4337Utils', function () { const validationData2 = packValidationData(validAfter2, validUntil2, authorizer2); const expected = packValidationData(validAfter2, validUntil1, false); - // check symetry + // check symmetry expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected); expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected); });