diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c280885632c..2b2d43c5528 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -66,6 +66,8 @@ jobs: run: bash scripts/upgradeable/transpile.sh - name: Run tests run: npm run test + env: + UNLIMITED: true - name: Check linearisation of the inheritance graph run: npm run test:inheritance - name: Check storage layout diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol new file mode 100644 index 00000000000..6dc112a1fd1 --- /dev/null +++ b/contracts/abstraction/account/Account.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation, IAccount, IAccountExecute, IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {ERC4337Utils} from "./../utils/ERC4337Utils.sol"; +import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol"; +import {Address} from "../../utils/Address.sol"; + +abstract contract Account is IAccount, IAccountExecute { + error AccountEntryPointRestricted(); + + /**************************************************************************************************************** + * Modifiers * + ****************************************************************************************************************/ + + modifier onlyEntryPointOrSelf() { + if (msg.sender != address(this) && msg.sender != address(entryPoint())) { + revert AccountEntryPointRestricted(); + } + _; + } + + modifier onlyEntryPoint() { + if (msg.sender != address(entryPoint())) { + revert AccountEntryPointRestricted(); + } + _; + } + + /**************************************************************************************************************** + * Hooks * + ****************************************************************************************************************/ + + /** + * @dev Return the entryPoint used by this account. + * + * Subclass should return the current entryPoint used by this account. + */ + function entryPoint() public view virtual returns (IEntryPoint); + + /** + * @dev Return weither an address (identity) is authorized to operate on this account. Depending on how the + * account is configured, this can be interpreted as either the owner of the account (if operating using a single + * owner -- default) or as an authorized signer if operating using as a multisig account. + * + * Subclass must implement this using their own access control mechanism. + */ + function _isAuthorized(address) internal view virtual returns (bool); + + /** + * @dev Recover the signer for a given signature and user operation hash. This function does not need to verify + * that the recovered signer is authorized. + * + * Subclass must implement this using their own choice of cryptography. + */ + function _recoverSigner(bytes32 userOpHash, bytes calldata signature) internal view virtual returns (address); + + /**************************************************************************************************************** + * Public interface * + ****************************************************************************************************************/ + + /** + * @dev Return the account nonce for the canonical sequence. + */ + function getNonce() public view virtual returns (uint256) { + return entryPoint().getNonce(address(this), 0); + } + + /** + * @dev Return the account nonce for a given sequence (key). + */ + function getNonce(uint192 key) public view virtual returns (uint256) { + return entryPoint().getNonce(address(this), key); + } + + /// @inheritdoc IAccount + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) public virtual onlyEntryPoint returns (uint256 validationData) { + (bool valid, , uint48 validAfter, uint48 validUntil) = _processSignature(userOpHash, userOp.signature); + _validateNonce(userOp.nonce); + _payPrefund(missingAccountFunds); + return ERC4337Utils.packValidationData(valid, validAfter, validUntil); + } + + /// @inheritdoc IAccountExecute + function executeUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/) public virtual onlyEntryPoint { + Address.functionDelegateCall(address(this), userOp.callData[4:]); + } + + /**************************************************************************************************************** + * Internal mechanisms * + ****************************************************************************************************************/ + + /** + * @dev Process the signature is valid for this message. + * @param userOpHash - Hash of the request that must be signed (includes the entrypoint and chain id) + * @param signature - The user's signature + * @return valid - Signature is valid + * @return signer - Address of the signer that produced the signature + * @return validAfter - first timestamp this operation is valid + * @return validUntil - last timestamp this operation is valid. 0 for "indefinite" + */ + function _processSignature( + bytes32 userOpHash, + bytes calldata signature + ) internal view virtual returns (bool valid, address signer, uint48 validAfter, uint48 validUntil) { + address recovered = _recoverSigner(userOpHash, signature); + return (recovered != address(0) && _isAuthorized(recovered), recovered, 0, 0); + } + + /** + * @dev Validate the nonce of the UserOperation. + * This method may validate the nonce requirement of this account. + * e.g. + * To limit the nonce to use sequenced UserOps only (no "out of order" UserOps): + * `require(nonce < type(uint64).max)` + * + * The actual nonce uniqueness is managed by the EntryPoint, and thus no other + * action is needed by the account itself. + * + * @param nonce to validate + */ + function _validateNonce(uint256 nonce) internal view virtual {} + + /** + * @dev Sends to the entrypoint (msg.sender) the missing funds for this transaction. + * SubClass MAY override this method for better funds management + * (e.g. send to the entryPoint more than the minimum required, so that in future transactions + * it will not be required to send again). + * @param missingAccountFunds - The minimum value this method should send the entrypoint. + * This value MAY be zero, in case there is enough deposit, + * or the userOp has a paymaster. + */ + function _payPrefund(uint256 missingAccountFunds) internal virtual { + if (missingAccountFunds > 0) { + (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}(""); + success; + //ignore failure (its EntryPoint's job to verify, not account.) + } + } +} diff --git a/contracts/abstraction/account/ERC7579Account.sol b/contracts/abstraction/account/ERC7579Account.sol new file mode 100644 index 00000000000..24239abcd6c --- /dev/null +++ b/contracts/abstraction/account/ERC7579Account.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Account} from "./Account.sol"; +import {Address} from "../../utils/Address.sol"; +import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; +import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; +import {IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; +import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig} from "../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, MODULE_TYPE_EXECUTOR} from "../../interfaces/IERC7579Module.sol"; +import {ERC7579Utils, Execution, Mode, CallType, ExecType} from "../utils/ERC7579Utils.sol"; + +abstract contract ERC7579Account is + IERC165, // required by erc-7579 + IERC1271, // required by erc-7579 + IERC7579Execution, // required by erc-7579 + IERC7579AccountConfig, // required by erc-7579 + IERC7579ModuleConfig, // required by erc-7579 + Account, + ERC165, + ERC721Holder, + ERC1155Holder +{ + using ERC7579Utils for *; + + IEntryPoint private immutable _entryPoint; + + event ERC7579TryExecuteUnsuccessful(uint256 batchExecutionindex, bytes result); + error ERC7579UnsupportedCallType(CallType callType); + error ERC7579UnsupportedExecType(ExecType execType); + error MismatchModuleTypeId(uint256 moduleTypeId, address module); + error UnsupportedModuleType(uint256 moduleTypeId); + error ModuleRestricted(uint256 moduleTypeId, address caller); + error ModuleAlreadyInstalled(uint256 moduleTypeId, address module); + error ModuleNotInstalled(uint256 moduleTypeId, address module); + + modifier onlyModule(uint256 moduleTypeId) { + if (!isModuleInstalled(moduleTypeId, msg.sender, msg.data)) { + revert ModuleRestricted(moduleTypeId, msg.sender); + } + _; + } + + constructor(IEntryPoint entryPoint_) { + _entryPoint = entryPoint_; + } + + receive() external payable {} + + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + /// @inheritdoc IERC165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC165, ERC1155Holder) returns (bool) { + // TODO: more? + return super.supportsInterface(interfaceId); + } + + /// @inheritdoc IERC1271 + function isValidSignature(bytes32 hash, bytes calldata signature) public view returns (bytes4 magicValue) { + (bool valid, , uint48 validAfter, uint48 validUntil) = _processSignature(hash, signature); + return + (valid && validAfter < block.timestamp && (validUntil == 0 || validUntil > block.timestamp)) + ? IERC1271.isValidSignature.selector + : bytes4(0); + } + + /**************************************************************************************************************** + * ERC-7579 Execution * + ****************************************************************************************************************/ + + /// @inheritdoc IERC7579Execution + function execute(bytes32 mode, bytes calldata executionCalldata) public virtual onlyEntryPoint { + _execute(Mode.wrap(mode), executionCalldata); + } + + /// @inheritdoc IERC7579Execution + function executeFromExecutor( + bytes32 mode, + bytes calldata executionCalldata + ) public virtual onlyModule(MODULE_TYPE_EXECUTOR) returns (bytes[] memory) { + return _execute(Mode.wrap(mode), executionCalldata); + } + + function _execute( + Mode mode, + bytes calldata executionCalldata + ) internal virtual returns (bytes[] memory returnData) { + // TODO: ModeSelector? ModePayload? + (CallType callType, ExecType execType, , ) = mode.decodeMode(); + + if (callType == ERC7579Utils.CALLTYPE_SINGLE) { + (address target, uint256 value, bytes calldata callData) = executionCalldata.decodeSingle(); + returnData = new bytes[](1); + returnData[0] = _execute(0, execType, target, value, callData); + } else if (callType == ERC7579Utils.CALLTYPE_BATCH) { + Execution[] calldata executionBatch = executionCalldata.decodeBatch(); + returnData = new bytes[](executionBatch.length); + for (uint256 i = 0; i < executionBatch.length; ++i) { + returnData[i] = _execute( + i, + execType, + executionBatch[i].target, + executionBatch[i].value, + executionBatch[i].callData + ); + } + } else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) { + (address target, bytes calldata callData) = executionCalldata.decodeDelegate(); + returnData = new bytes[](1); + returnData[0] = _executeDelegate(0, execType, target, callData); + } else { + revert ERC7579UnsupportedCallType(callType); + } + } + + function _execute( + uint256 index, + ExecType execType, + address target, + uint256 value, + bytes memory data + ) private returns (bytes memory) { + if (execType == ERC7579Utils.EXECTYPE_DEFAULT) { + (bool success, bytes memory returndata) = target.call{value: value}(data); + Address.verifyCallResult(success, returndata); + return returndata; + } else if (execType == ERC7579Utils.EXECTYPE_TRY) { + (bool success, bytes memory returndata) = target.call{value: value}(data); + if (!success) emit ERC7579TryExecuteUnsuccessful(index, returndata); + return returndata; + } else { + revert ERC7579UnsupportedExecType(execType); + } + } + + function _executeDelegate( + uint256 index, + ExecType execType, + address target, + bytes memory data + ) private returns (bytes memory) { + if (execType == ERC7579Utils.EXECTYPE_DEFAULT) { + (bool success, bytes memory returndata) = target.delegatecall(data); + Address.verifyCallResult(success, returndata); + return returndata; + } else if (execType == ERC7579Utils.EXECTYPE_TRY) { + (bool success, bytes memory returndata) = target.delegatecall(data); + if (!success) emit ERC7579TryExecuteUnsuccessful(index, returndata); + return returndata; + } else { + revert ERC7579UnsupportedExecType(execType); + } + } + + /**************************************************************************************************************** + * ERC-7579 Account and Modules * + ****************************************************************************************************************/ + + /// @inheritdoc IERC7579AccountConfig + function accountId() public view virtual returns (string memory) { + //vendorname.accountname.semver + return "@openzeppelin/contracts.erc7579account.v0-beta"; + } + + /// @inheritdoc IERC7579AccountConfig + function supportsExecutionMode(bytes32 encodedMode) public view virtual returns (bool) { + (CallType callType, , , ) = Mode.wrap(encodedMode).decodeMode(); + return + callType == ERC7579Utils.CALLTYPE_SINGLE || + callType == ERC7579Utils.CALLTYPE_BATCH || + callType == ERC7579Utils.CALLTYPE_DELEGATECALL; + } + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 /*moduleTypeId*/) public view virtual returns (bool) { + return false; + } + + /// @inheritdoc IERC7579ModuleConfig + function installModule( + uint256 moduleTypeId, + address module, + bytes calldata initData + ) public virtual onlyEntryPointOrSelf { + if (!IERC7579Module(module).isModuleType(moduleTypeId)) revert MismatchModuleTypeId(moduleTypeId, module); + _installModule(moduleTypeId, module, initData); + /// TODO: silent unreachable and re-enable this event + // emit ModuleInstalled(moduleTypeId, module); + } + + /// @inheritdoc IERC7579ModuleConfig + function uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) public virtual onlyEntryPointOrSelf { + _uninstallModule(moduleTypeId, module, deInitData); + /// TODO: silent unreachable and re-enable this event + // emit ModuleUninstalled(moduleTypeId, module); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 /*moduleTypeId*/, + address /*module*/, + bytes calldata /*additionalContext*/ + ) public view virtual returns (bool) { + return false; + } + + /**************************************************************************************************************** + * Hooks * + ****************************************************************************************************************/ + + function _installModule(uint256 moduleTypeId, address /*module*/, bytes calldata /*initData*/) internal virtual { + revert UnsupportedModuleType(moduleTypeId); + } + + function _uninstallModule( + uint256 moduleTypeId, + address /*module*/, + bytes calldata /*deInitData*/ + ) internal virtual { + revert UnsupportedModuleType(moduleTypeId); + } +} diff --git a/contracts/abstraction/account/modules/AccountEIP7702.sol b/contracts/abstraction/account/modules/AccountEIP7702.sol new file mode 100644 index 00000000000..5bf5423fb5f --- /dev/null +++ b/contracts/abstraction/account/modules/AccountEIP7702.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {AccountECDSA} from "./recovery/AccountECDSA.sol"; + +abstract contract Account7702 is AccountECDSA { + function _isAuthorized(address user) internal view virtual override returns (bool) { + return user == address(this); + } +} diff --git a/contracts/abstraction/account/modules/AccountMultisig.sol b/contracts/abstraction/account/modules/AccountMultisig.sol new file mode 100644 index 00000000000..c6c6f8f6caa --- /dev/null +++ b/contracts/abstraction/account/modules/AccountMultisig.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../../interfaces/IERC4337.sol"; +import {Math} from "./../../../utils/math/Math.sol"; +import {ERC4337Utils} from "./../../utils/ERC4337Utils.sol"; +import {Account} from "../Account.sol"; + +abstract contract AccountMultisig is Account { + function requiredSignatures() public view virtual returns (uint256); + + function _processSignature( + bytes32 userOpHash, + bytes calldata signatures + ) internal view virtual override returns (bool, address, uint48 validAfter, uint48 validUntil) { + bytes[] calldata signatureArray = _decodeBytesArray(signatures); + + if (signatureArray.length < requiredSignatures()) { + return (false, address(0), 0, 0); + } + + address lastSigner = address(0); + + for (uint256 i = 0; i < signatureArray.length; ++i) { + (bool sigValid, address sigSigner, uint48 sigValidAfter, uint48 sigValidUntil) = super._processSignature( + userOpHash, + signatureArray[i] + ); + if (sigValid && sigSigner > lastSigner) { + lastSigner = sigSigner; + validAfter = uint48(Math.ternary(validAfter > sigValidAfter, validAfter, sigValidAfter)); + validUntil = uint48( + Math.ternary(validUntil < sigValidUntil || sigValidUntil == 0, validUntil, sigValidUntil) + ); + } else { + return (false, address(0), 0, 0); + } + } + return (true, address(this), validAfter, validUntil); + } + + function _decodeBytesArray(bytes calldata input) private pure returns (bytes[] calldata output) { + assembly ("memory-safe") { + let ptr := add(input.offset, calldataload(input.offset)) + output.offset := add(ptr, 32) + output.length := calldataload(ptr) + } + } +} diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol new file mode 100644 index 00000000000..ee9cfbfdc30 --- /dev/null +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC7579AccountConfig, IERC7579ModuleConfig} from "../../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, MODULE_TYPE_EXECUTOR} from "../../../interfaces/IERC7579Module.sol"; +import {Account} from "../Account.sol"; +import {ERC7579Account} from "../ERC7579Account.sol"; +import {EnumerableSet} from "../../../utils/structs/EnumerableSet.sol"; + +abstract contract ERC7579AccountModuleExecutor is ERC7579Account { + using EnumerableSet for *; + + EnumerableSet.AddressSet private _executors; + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) { + return moduleTypeId == MODULE_TYPE_EXECUTOR || super.supportsModule(moduleTypeId); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view virtual override returns (bool) { + return + moduleTypeId == MODULE_TYPE_EXECUTOR + ? _executors.contains(module) + : super.isModuleInstalled(moduleTypeId, module, additionalContext); + } + + /// @inheritdoc ERC7579Account + function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { + if (moduleTypeId == MODULE_TYPE_EXECUTOR) { + if (!_executors.add(module)) revert ModuleAlreadyInstalled(moduleTypeId, module); + IERC7579Module(module).onInstall(initData); + } else { + super._installModule(moduleTypeId, module, initData); + } + } + + /// @inheritdoc ERC7579Account + function _uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) internal virtual override { + if (moduleTypeId == MODULE_TYPE_EXECUTOR) { + if (!_executors.remove(module)) revert ModuleNotInstalled(moduleTypeId, module); + IERC7579Module(module).onUninstall(deInitData); + } else { + super._uninstallModule(moduleTypeId, module, deInitData); + } + } + + // TODO: enumerability? +} diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol new file mode 100644 index 00000000000..85cadfbf30f --- /dev/null +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC7579AccountConfig, IERC7579ModuleConfig} from "../../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, MODULE_TYPE_FALLBACK} from "../../../interfaces/IERC7579Module.sol"; +import {ERC7579Utils, CallType} from "../../utils/ERC7579Utils.sol"; +import {ERC7579Account} from "../ERC7579Account.sol"; + +abstract contract ERC7579AccountModuleFallback is ERC7579Account { + mapping(bytes4 => address) private _fallbacks; + + error FallbackHandlerAlreadySet(bytes4 selector); + error FallbackHandlerNotSet(bytes4 selector); + error NoFallbackHandler(bytes4 selector); + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) { + return moduleTypeId == MODULE_TYPE_FALLBACK || super.supportsModule(moduleTypeId); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view virtual override returns (bool) { + return + moduleTypeId == MODULE_TYPE_FALLBACK + ? _fallbacks[bytes4(additionalContext[0:4])] == module + : super.isModuleInstalled(moduleTypeId, module, additionalContext); + } + + /// @inheritdoc ERC7579Account + function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { + if (moduleTypeId == MODULE_TYPE_FALLBACK) { + bytes4 selector = bytes4(initData[0:4]); + + if (_fallbacks[selector] != address(0)) revert FallbackHandlerAlreadySet(selector); + _fallbacks[selector] = module; + + IERC7579Module(module).onInstall(initData[4:]); + } else { + super._installModule(moduleTypeId, module, initData); + } + } + + /// @inheritdoc ERC7579Account + function _uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) internal virtual override { + if (moduleTypeId == MODULE_TYPE_FALLBACK) { + bytes4 selector = bytes4(deInitData[0:4]); + + address handler = _fallbacks[selector]; + if (handler == address(0) || handler != module) revert FallbackHandlerNotSet(selector); + delete _fallbacks[selector]; + + IERC7579Module(module).onUninstall(deInitData[4:]); + } else { + super._uninstallModule(moduleTypeId, module, deInitData); + } + } + + fallback() external payable { + address handler = _fallbacks[msg.sig]; + if (handler == address(0)) revert NoFallbackHandler(msg.sig); + + // From https://eips.ethereum.org/EIPS/eip-7579#fallback[ERC-7579 specifications]: + // - MUST utilize ERC-2771 to add the original msg.sender to the calldata sent to the fallback handler + // - MUST use call to invoke the fallback handler + (bool success, bytes memory returndata) = handler.call{value: msg.value}( + abi.encodePacked(msg.data, msg.sender) + ); + assembly ("memory-safe") { + switch success + case 0 { + revert(add(returndata, 0x20), mload(returndata)) + } + default { + return(add(returndata, 0x20), mload(returndata)) + } + } + } + + // TODO: getters? +} diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol new file mode 100644 index 00000000000..681623da977 --- /dev/null +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC7579AccountConfig, IERC7579ModuleConfig} from "../../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, IERC7579Hook, MODULE_TYPE_HOOK} from "../../../interfaces/IERC7579Module.sol"; +import {ERC7579Account} from "../ERC7579Account.sol"; + +abstract contract ERC7579AccountModuleHook is ERC7579Account { + address private _hook; + + modifier withHook() { + address hook = _hook; + if (hook == address(0)) { + _; + } else { + bytes memory hookData = IERC7579Hook(hook).preCheck(msg.sender, msg.value, msg.data); + _; + IERC7579Hook(hook).postCheck(hookData); + } + } + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) { + return moduleTypeId == MODULE_TYPE_HOOK || super.supportsModule(moduleTypeId); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view virtual override returns (bool) { + return + moduleTypeId == MODULE_TYPE_HOOK + ? module == _hook + : super.isModuleInstalled(moduleTypeId, module, additionalContext); + } + + /// @inheritdoc ERC7579Account + function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { + if (moduleTypeId == MODULE_TYPE_HOOK) { + if (_hook != address(0)) revert ModuleNotInstalled(moduleTypeId, _hook); + _hook = module; + IERC7579Module(module).onInstall(initData); + } else { + super._installModule(moduleTypeId, module, initData); + } + } + + /// @inheritdoc ERC7579Account + function _uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) internal virtual override { + if (moduleTypeId == MODULE_TYPE_HOOK) { + if (_hook != module) revert ModuleAlreadyInstalled(moduleTypeId, module); + delete _hook; + IERC7579Module(module).onUninstall(deInitData); + } else { + super._uninstallModule(moduleTypeId, module, deInitData); + } + } + + // TODO: do something with the modifier? + // TODO: getters? +} diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol new file mode 100644 index 00000000000..0333b0c5f0a --- /dev/null +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC7579AccountConfig, IERC7579ModuleConfig} from "../../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, MODULE_TYPE_VALIDATOR} from "../../../interfaces/IERC7579Module.sol"; +import {ERC7579Account} from "../ERC7579Account.sol"; +import {EnumerableSet} from "../../../utils/structs/EnumerableSet.sol"; + +abstract contract ERC7579AccountModuleValidator is ERC7579Account { + using EnumerableSet for *; + + EnumerableSet.AddressSet private _validators; + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) { + return moduleTypeId == MODULE_TYPE_VALIDATOR || super.supportsModule(moduleTypeId); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view virtual override returns (bool) { + return + moduleTypeId == MODULE_TYPE_VALIDATOR + ? _validators.contains(module) + : super.isModuleInstalled(moduleTypeId, module, additionalContext); + } + + /// @inheritdoc ERC7579Account + function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { + if (moduleTypeId == MODULE_TYPE_VALIDATOR) { + if (!_validators.add(module)) revert ModuleAlreadyInstalled(moduleTypeId, module); + IERC7579Module(module).onInstall(initData); + } else { + super._installModule(moduleTypeId, module, initData); + } + } + + /// @inheritdoc ERC7579Account + function _uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) internal virtual override { + if (moduleTypeId == MODULE_TYPE_VALIDATOR) { + if (!_validators.remove(module)) revert ModuleNotInstalled(moduleTypeId, module); + IERC7579Module(module).onUninstall(deInitData); + } else { + super._uninstallModule(moduleTypeId, module, deInitData); + } + } + + // TODO: do something with the validators? + // TODO: enumerability? +} diff --git a/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol new file mode 100644 index 00000000000..a10aa0cdff4 --- /dev/null +++ b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../../../interfaces/IERC4337.sol"; +import {AccountECDSA} from "./AccountECDSA.sol"; +import {AccountERC1271} from "./AccountERC1271.sol"; + +abstract contract AccountAllSignatures is AccountECDSA, AccountERC1271 { + enum SignatureType { + ECDSA, // secp256k1 + ERC1271 // others through erc1271 identity (support P256, RSA, ...) + } + + function _recoverSigner( + bytes32 userOpHash, + bytes calldata signature + ) internal view virtual override(AccountECDSA, AccountERC1271) returns (address) { + SignatureType sigType = SignatureType(uint8(bytes1(signature))); + + if (sigType == SignatureType.ECDSA) { + return AccountECDSA._recoverSigner(userOpHash, signature[0x01:]); + } else if (sigType == SignatureType.ERC1271) { + return AccountERC1271._recoverSigner(userOpHash, signature[0x01:]); + } else { + return address(0); + } + } +} diff --git a/contracts/abstraction/account/modules/recovery/AccountECDSA.sol b/contracts/abstraction/account/modules/recovery/AccountECDSA.sol new file mode 100644 index 00000000000..51cc214f079 --- /dev/null +++ b/contracts/abstraction/account/modules/recovery/AccountECDSA.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../../../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../../../../utils/cryptography/MessageHashUtils.sol"; +import {ECDSA} from "../../../../utils/cryptography/ECDSA.sol"; +import {Account} from "../../Account.sol"; + +abstract contract AccountECDSA is Account { + function _recoverSigner( + bytes32 userOpHash, + bytes calldata signature + ) internal view virtual override returns (address signer) { + bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); + + // This implementation support both "normal" and short signature formats: + // - If signature length is 65, process as "normal" signature (R,S,V) + // - If signature length is 64, process as https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signature] (R,SV) ECDSA signature + // This is safe because the UserOperations include a nonce (which is managed by the entrypoint) for replay protection. + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + /// @solidity memory-safe-assembly + assembly { + r := calldataload(add(signature.offset, 0x00)) + s := calldataload(add(signature.offset, 0x20)) + v := byte(0, calldataload(add(signature.offset, 0x40))) + } + (signer, , ) = ECDSA.tryRecover(msgHash, v, r, s); // return address(0) on errors + } else if (signature.length == 64) { + bytes32 r; + bytes32 vs; + /// @solidity memory-safe-assembly + assembly { + r := calldataload(add(signature.offset, 0x00)) + vs := calldataload(add(signature.offset, 0x20)) + } + (signer, , ) = ECDSA.tryRecover(msgHash, r, vs); + } else { + return address(0); + } + } +} diff --git a/contracts/abstraction/account/modules/recovery/AccountERC1271.sol b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol new file mode 100644 index 00000000000..c92f323f4aa --- /dev/null +++ b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../../../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../../../../utils/cryptography/MessageHashUtils.sol"; +import {SignatureChecker} from "../../../../utils/cryptography/SignatureChecker.sol"; +import {Account} from "../../Account.sol"; + +abstract contract AccountERC1271 is Account { + error P256InvalidSignatureLength(uint256 length); + + function _recoverSigner( + bytes32 userOpHash, + bytes calldata signature + ) internal view virtual override returns (address) { + bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); + address signer = address(bytes20(signature[0x00:0x14])); + + return SignatureChecker.isValidERC1271SignatureNow(signer, msgHash, signature[0x14:]) ? signer : address(0); + } +} diff --git a/contracts/abstraction/entrypoint/EntryPoint.sol b/contracts/abstraction/entrypoint/EntryPoint.sol new file mode 100644 index 00000000000..45a85465e99 --- /dev/null +++ b/contracts/abstraction/entrypoint/EntryPoint.sol @@ -0,0 +1,602 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IEntryPoint, IEntryPointNonces, IEntryPointStake, IAccount, IAccountExecute, IAggregator, IPaymaster, PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {IERC165} from "../../interfaces/IERC165.sol"; +import {ERC20} from "../../token/ERC20/ERC20.sol"; +import {ERC165} from "../../utils/introspection/ERC165.sol"; +import {Address} from "../../utils/Address.sol"; +import {Call} from "../../utils/Call.sol"; +import {Memory} from "../../utils/Memory.sol"; +import {NoncesWithKey} from "../../utils/NoncesWithKey.sol"; +import {ReentrancyGuard} from "../../utils/ReentrancyGuard.sol"; +import {ERC4337Utils} from "./../utils/ERC4337Utils.sol"; +import {SenderCreationHelper} from "./../utils/SenderCreationHelper.sol"; + +/* + * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. + * Only one instance required on each chain. + * + * WARNING: This contract is not properly tested. It is only present as an helper for the development of account + * contracts. The EntryPoint is a critical element or ERC-4337, and must be developped with exterm care to avoid any + * issue with gas payments/refunds in corner cases. A fully tested, production-ready, version may be available in the + * future, but for the moment this should NOT be used in production! + */ +contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, NoncesWithKey, ReentrancyGuard { + using ERC4337Utils for *; + + SenderCreationHelper private immutable _senderCreator = new SenderCreationHelper(); + + // TODO: move events to interface? + event UserOperationEvent( + bytes32 indexed userOpHash, + address indexed sender, + address indexed paymaster, + uint256 nonce, + bool success, + uint256 actualGasCost, + uint256 actualGasUsed + ); + event AccountDeployed(bytes32 indexed userOpHash, address indexed sender, address factory, address paymaster); + event UserOperationRevertReason( + bytes32 indexed userOpHash, + address indexed sender, + uint256 nonce, + bytes revertReason + ); + event PostOpRevertReason(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason); + event UserOperationPrefundTooLow(bytes32 indexed userOpHash, address indexed sender, uint256 nonce); + event BeforeExecution(); + event SignatureAggregatorChanged(address indexed aggregator); + error PostOpReverted(bytes returnData); + error SignatureValidationFailed(address aggregator); + + // compensate for innerHandleOps' emit message and deposit refund. + // allow some slack for future gas price changes. + uint256 private constant INNER_GAS_OVERHEAD = 10000; + 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; + + // ERC165: TODO + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + // || interfaceId == (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) + // || interfaceId == type(IEntryPoint).interfaceId + // || interfaceId == type(IStakeManager).interfaceId + // || interfaceId == type(INonceManager).interfaceId; + } + + /** + * @dev Simulate the deployment of an account. + * @param initCode - The init code for the smart contract account, formated according to PackedUserOperation + * specifications. + */ + function getSenderAddress(bytes calldata initCode) public returns (address) { + return _senderCreator.getSenderAddress(initCode); + } + + /**************************************************************************************************************** + * IEntryPointStake * + ****************************************************************************************************************/ + receive() external payable { + _mint(msg.sender, msg.value); + } + + /// @inheritdoc IEntryPointStake + function balanceOf(address account) public view virtual override(ERC20, IEntryPointStake) returns (uint256) { + return super.balanceOf(account); + } + + /// @inheritdoc IEntryPointStake + function depositTo(address account) public payable virtual { + _mint(account, msg.value); + } + + /// @inheritdoc IEntryPointStake + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) public virtual { + _burn(msg.sender, withdrawAmount); + Address.sendValue(withdrawAddress, withdrawAmount); + } + + /// @inheritdoc IEntryPointStake + function addStake(uint32 /*unstakeDelaySec*/) public payable virtual { + // TODO: implement + revert("Stake not Implemented yet"); + } + + /// @inheritdoc IEntryPointStake + function unlockStake() public pure virtual { + // TODO: implement and remove pure + revert("Stake not Implemented yet"); + } + + /// @inheritdoc IEntryPointStake + function withdrawStake(address payable /*withdrawAddress*/) public pure virtual { + // TODO: implement and remove pure + revert("Stake not Implemented yet"); + } + + /**************************************************************************************************************** + * IEntryPointNonces * + ****************************************************************************************************************/ + /// @inheritdoc IEntryPointNonces + function getNonce( + address owner, + uint192 key + ) public view virtual override(IEntryPointNonces, NoncesWithKey) returns (uint256) { + return super.getNonce(owner, key); + } + + /**************************************************************************************************************** + * Handle user operations * + ****************************************************************************************************************/ + /// @inheritdoc IEntryPoint + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) public nonReentrant { + ERC4337Utils.UserOpInfo[] memory userOpInfos = new ERC4337Utils.UserOpInfo[](ops.length); + + for (uint256 i = 0; i < ops.length; ++i) { + (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment(i, ops[i], userOpInfos[i]); + _validateAccountAndPaymasterValidationData(i, validationData, paymasterValidationData, address(0)); + } + + emit BeforeExecution(); + + uint256 collected = 0; + for (uint256 i = 0; i < ops.length; ++i) { + collected += _executeUserOp(i, ops[i], userOpInfos[i]); + } + + Address.sendValue(beneficiary, collected); + } + + /// @inheritdoc IEntryPoint + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) public nonReentrant { + uint256 totalOps = 0; + for (uint256 i = 0; i < opsPerAggregator.length; ++i) { + PackedUserOperation[] calldata ops = opsPerAggregator[i].userOps; + IAggregator aggregator = opsPerAggregator[i].aggregator; + + //address(1) is special marker of "signature error" + if (address(aggregator) == address(1)) { + revert("AA96 invalid aggregator"); + } else if (address(aggregator) != address(0)) { + try aggregator.validateSignatures(ops, opsPerAggregator[i].signature) {} catch { + revert SignatureValidationFailed(address(aggregator)); + } + } + totalOps += ops.length; + } + + ERC4337Utils.UserOpInfo[] memory userOpInfos = new ERC4337Utils.UserOpInfo[](totalOps); + + uint256 opIndex = 0; + for (uint256 a = 0; a < opsPerAggregator.length; ++a) { + PackedUserOperation[] calldata ops = opsPerAggregator[a].userOps; + IAggregator aggregator = opsPerAggregator[a].aggregator; + + for (uint256 i = 0; i < ops.length; ++i) { + (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment( + opIndex, + ops[i], + userOpInfos[opIndex] + ); + _validateAccountAndPaymasterValidationData( + i, + validationData, + paymasterValidationData, + address(aggregator) + ); + ++opIndex; + } + } + + emit BeforeExecution(); + + opIndex = 0; + + uint256 collected = 0; + for (uint256 a = 0; a < opsPerAggregator.length; ++a) { + PackedUserOperation[] calldata ops = opsPerAggregator[a].userOps; + IAggregator aggregator = opsPerAggregator[a].aggregator; + + emit SignatureAggregatorChanged(address(aggregator)); + for (uint256 i = 0; i < ops.length; ++i) { + collected += _executeUserOp(opIndex, ops[i], userOpInfos[opIndex]); + ++opIndex; + } + } + emit SignatureAggregatorChanged(address(0)); + + Address.sendValue(beneficiary, collected); + } + + /** + * @dev Execute a user operation. + * @param opIndex - Index into the userOpInfo array. + * @param userOp - The userOp to execute. + * @param userOpInfo - The userOpInfo filled by validatePrepayment for this userOp. + * @return collected - The total amount this userOp paid. + */ + function _executeUserOp( + uint256 opIndex, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory userOpInfo + ) internal returns (uint256 collected) { + uint256 preGas = gasleft(); + + // Allocate memory and reset the free memory pointer. Buffer for innerCall is not kept/protected + Memory.FreePtr ptr = Memory.save(); + bytes memory innerCall = abi.encodeCall( + this.innerHandleOp, + ( + userOp.callData.length >= 0x04 && bytes4(userOp.callData[0:4]) == IAccountExecute.executeUserOp.selector + ? abi.encodeCall(IAccountExecute.executeUserOp, (userOp, userOpInfo.userOpHash)) + : userOp.callData, + userOpInfo + ) + ); + Memory.load(ptr); + + bool success = Call.call(address(this), 0, innerCall); + bytes32 result = abi.decode(Call.getReturnDataFixed(0x20), (bytes32)); + + if (success) { + collected = uint256(result); + } else if (result == 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 (result == INNER_REVERT_LOW_PREFUND) { + // innerCall reverted on prefund too low. treat entire prefund as "gas cost" + uint256 actualGas = preGas - gasleft() + userOpInfo.preOpGas; + uint256 actualGasCost = userOpInfo.prefund; + emit UserOperationPrefundTooLow(userOpInfo.userOpHash, userOpInfo.sender, userOpInfo.nonce); + emit UserOperationEvent( + userOpInfo.userOpHash, + userOpInfo.sender, + userOpInfo.paymaster, + userOpInfo.nonce, + success, + actualGasCost, + actualGas + ); + collected = actualGasCost; + } else { + emit PostOpRevertReason( + userOpInfo.userOpHash, + userOpInfo.sender, + userOpInfo.nonce, + Call.getReturnData(REVERT_REASON_MAX_LEN) + ); + + uint256 actualGas = preGas - gasleft() + userOpInfo.preOpGas; + collected = _postExecution(IPaymaster.PostOpMode.postOpReverted, userOpInfo, actualGas); + } + } + + /** + * @dev 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 userOpInfo - The UserOpInfo struct. + * @return actualGasCost - the actual cost in eth this UserOperation paid for gas + */ + function innerHandleOp( + bytes memory callData, + ERC4337Utils.UserOpInfo memory userOpInfo + ) external returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + require(msg.sender == address(this), "AA92 internal call only"); + + unchecked { + // handleOps was called with gas limit too low. abort entire bundle. + if ( + (gasleft() * 63) / 64 < + userOpInfo.callGasLimit + userOpInfo.paymasterPostOpGasLimit + INNER_GAS_OVERHEAD + ) { + Call.revertWithCode(INNER_OUT_OF_GAS); + } + + IPaymaster.PostOpMode mode; + if (callData.length == 0 || Call.call(userOpInfo.sender, 0, callData, userOpInfo.callGasLimit)) { + mode = IPaymaster.PostOpMode.opSucceeded; + } else { + mode = IPaymaster.PostOpMode.opReverted; + // if we get here, that means callData.length > 0 and the Call failed + if (Call.getReturnDataSize() > 0) { + emit UserOperationRevertReason( + userOpInfo.userOpHash, + userOpInfo.sender, + userOpInfo.nonce, + Call.getReturnData(REVERT_REASON_MAX_LEN) + ); + } + } + + uint256 actualGas = preGas - gasleft() + userOpInfo.preOpGas; + return _postExecution(mode, userOpInfo, actualGas); + } + } + + /** + * @dev Create sender smart contract account if init code is provided. + * @param opIndex - The operation index. + * @param userOpInfo - The operation info. + * @param initCode - The init code for the smart contract account. + */ + function _createSenderIfNeeded( + uint256 opIndex, + ERC4337Utils.UserOpInfo memory userOpInfo, + bytes calldata initCode + ) internal { + if (initCode.length != 0) { + address sender = userOpInfo.sender; + if (sender.code.length != 0) revert FailedOp(opIndex, "AA10 sender already constructed"); + + address deployed = _senderCreator.createSender{gas: userOpInfo.verificationGasLimit}(initCode); + if (deployed == address(0)) revert FailedOp(opIndex, "AA13 initCode failed or OOG"); + else if (deployed != sender) revert FailedOp(opIndex, "AA14 initCode must return sender"); + else if (deployed.code.length == 0) revert FailedOp(opIndex, "AA15 initCode must create sender"); + + emit AccountDeployed(userOpInfo.userOpHash, sender, address(bytes20(initCode[0:20])), userOpInfo.paymaster); + } + } + + /** + * @dev Validate account and paymaster (if defined) and also make sure total validation doesn't exceed + * verificationGasLimit. + * @param opIndex - The index of this userOp into the "userOpInfos" array. + * @param userOp - The userOp to validate. + */ + function _validatePrepayment( + uint256 opIndex, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory outOpInfo + ) internal returns (uint256 validationData, uint256 paymasterValidationData) { + uint256 preGas = gasleft(); + unchecked { + outOpInfo.load(userOp); + + // Validate all numeric values in userOp are well below 128 bit, so they can safely be added + // and multiplied without causing overflow. + uint256 maxGasValues = outOpInfo.preVerificationGas | + outOpInfo.verificationGasLimit | + outOpInfo.callGasLimit | + outOpInfo.paymasterVerificationGasLimit | + outOpInfo.paymasterPostOpGasLimit | + outOpInfo.maxFeePerGas | + outOpInfo.maxPriorityFeePerGas; + + if (maxGasValues > type(uint120).max) { + revert FailedOp(opIndex, "AA94 gas values overflow"); + } + + uint256 requiredPreFund = outOpInfo.requiredPrefund(); + validationData = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund); + + if (!_tryUseNonce(outOpInfo.sender, outOpInfo.nonce)) { + revert FailedOp(opIndex, "AA25 invalid account nonce"); + } + + if (preGas - gasleft() > outOpInfo.verificationGasLimit) { + revert FailedOp(opIndex, "AA26 over verificationGasLimit"); + } + + if (outOpInfo.paymaster != address(0)) { + (outOpInfo.context, paymasterValidationData) = _validatePaymasterPrepayment( + opIndex, + userOp, + outOpInfo, + requiredPreFund + ); + } + + outOpInfo.prefund = requiredPreFund; + outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; + } + } + + /** + * @dev Validate prepayment (account part) + * - 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 userOp - The user operation. + * @param userOpInfo - The operation info. + * @param requiredPrefund - The required prefund amount. + */ + function _validateAccountPrepayment( + uint256 opIndex, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory userOpInfo, + uint256 requiredPrefund + ) internal returns (uint256 validationData) { + unchecked { + address sender = userOpInfo.sender; + address paymaster = userOpInfo.paymaster; + uint256 verificationGasLimit = userOpInfo.verificationGasLimit; + + _createSenderIfNeeded(opIndex, userOpInfo, userOp.initCode); + + uint256 missingAccountFunds = 0; + if (paymaster == address(0)) { + uint256 balance = balanceOf(sender); + if (requiredPrefund > balance) { + missingAccountFunds = requiredPrefund - balance; + } + } + + try + IAccount(sender).validateUserOp{gas: verificationGasLimit}( + userOp, + userOpInfo.userOpHash, + missingAccountFunds + ) + returns (uint256 _validationData) { + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA23 reverted", Call.getReturnData(REVERT_REASON_MAX_LEN)); + } + + if (paymaster == address(0)) { + uint256 balance = balanceOf(sender); + if (requiredPrefund > balance) { + revert FailedOp(opIndex, "AA21 didn't pay prefund"); + } else if (requiredPrefund > 0) { + _burn(sender, requiredPrefund); + } + } + } + } + + /** + * @dev Validate prepayment (paymaster part) + * - 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 userOp - The user operation. + * @param userOpInfo - The operation info. + * @param requiredPrefund - The required prefund amount. + */ + function _validatePaymasterPrepayment( + uint256 opIndex, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory userOpInfo, + uint256 requiredPrefund + ) internal returns (bytes memory context, uint256 validationData) { + unchecked { + uint256 preGas = gasleft(); + + address paymaster = userOpInfo.paymaster; + uint256 verificationGasLimit = userOpInfo.paymasterVerificationGasLimit; + + uint256 balance = balanceOf(paymaster); + if (requiredPrefund > balance) { + revert FailedOp(opIndex, "AA31 paymaster deposit too low"); + } else if (requiredPrefund > 0) { + _burn(paymaster, requiredPrefund); + } + + try + IPaymaster(paymaster).validatePaymasterUserOp{gas: verificationGasLimit}( + userOp, + userOpInfo.userOpHash, + requiredPrefund + ) + returns (bytes memory _context, uint256 _validationData) { + context = _context; + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA33 reverted", Call.getReturnData(REVERT_REASON_MAX_LEN)); + } + + if (preGas - gasleft() > verificationGasLimit) { + revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit"); + } + } + } + + /** + * @dev 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 aggregatorOutOfTimeRange) = validationData.getValidationData(); + if (aggregator != expectedAggregator) { + revert FailedOp(opIndex, "AA24 signature error"); + } else if (aggregatorOutOfTimeRange) { + 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, bool pmAggregatorOutOfTimeRange) = paymasterValidationData.getValidationData(); + if (pmAggregator != address(0)) { + revert FailedOp(opIndex, "AA34 signature error"); + } else if (pmAggregatorOutOfTimeRange) { + revert FailedOp(opIndex, "AA32 paymaster expired or not due"); + } + } + + /** + * @dev 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 userOpInfo - UserOp fields and info collected during validation. + * @param actualGas - The gas used so far by this user operation. + */ + function _postExecution( + IPaymaster.PostOpMode mode, + ERC4337Utils.UserOpInfo memory userOpInfo, + uint256 actualGas + ) private returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + unchecked { + address refundAddress = userOpInfo.paymaster; + uint256 gasPrice = userOpInfo.gasPrice(); + + if (refundAddress == address(0)) { + refundAddress = userOpInfo.sender; + } else if (userOpInfo.context.length > 0 && mode != IPaymaster.PostOpMode.postOpReverted) { + try + IPaymaster(refundAddress).postOp{gas: userOpInfo.paymasterPostOpGasLimit}( + mode, + userOpInfo.context, + actualGas * gasPrice, + gasPrice + ) + {} catch { + revert PostOpReverted(Call.getReturnData(REVERT_REASON_MAX_LEN)); + } + } + actualGas += preGas - gasleft(); + + // Calculating a penalty for unused execution gas + uint256 executionGasLimit = userOpInfo.callGasLimit + userOpInfo.paymasterPostOpGasLimit; + uint256 executionGasUsed = actualGas - userOpInfo.preOpGas; + // this check is required for the gas used within EntryPoint and not covered by explicit gas limits + if (executionGasLimit > executionGasUsed) { + actualGas += ((executionGasLimit - executionGasUsed) * PENALTY_PERCENT) / 100; + } + + actualGasCost = actualGas * gasPrice; + uint256 prefund = userOpInfo.prefund; + if (prefund < actualGasCost) { + if (mode == IPaymaster.PostOpMode.postOpReverted) { + actualGasCost = prefund; + emit UserOperationPrefundTooLow(userOpInfo.userOpHash, userOpInfo.sender, userOpInfo.nonce); + } else { + Call.revertWithCode(INNER_REVERT_LOW_PREFUND); + } + } else if (prefund > actualGasCost) { + _mint(refundAddress, prefund - actualGasCost); + } + emit UserOperationEvent( + userOpInfo.userOpHash, + userOpInfo.sender, + userOpInfo.paymaster, + userOpInfo.nonce, + mode == IPaymaster.PostOpMode.opSucceeded, + actualGasCost, + actualGas + ); + } + } +} diff --git a/contracts/abstraction/identity/IdentityP256.sol b/contracts/abstraction/identity/IdentityP256.sol new file mode 100644 index 00000000000..dd684786924 --- /dev/null +++ b/contracts/abstraction/identity/IdentityP256.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {Clones} from "../../proxy/Clones.sol"; +import {P256} from "../../utils/cryptography/P256.sol"; + +contract IdentityP256Implementation is IERC1271 { + function publicKey() public view returns (bytes memory) { + return Clones.fetchCloneArgs(address(this)); + } + + function isValidSignature(bytes32 h, bytes calldata signature) external view returns (bytes4 magicValue) { + // parse signature + if (signature.length < 0x40) return bytes4(0); + bytes32 r = bytes32(signature[0x00:0x20]); + bytes32 s = bytes32(signature[0x20:0x40]); + + // fetch and decode immutable public key for the clone + (bytes32 qx, bytes32 qy) = abi.decode(publicKey(), (bytes32, bytes32)); + + return P256.verify(h, r, s, qx, qy) ? IERC1271.isValidSignature.selector : bytes4(0); + } +} + +contract IdentityP256Factory { + address public immutable implementation = address(new IdentityP256Implementation()); + + function create(bytes memory publicKey) public returns (address instance) { + // predict the address of the instance for that key + address predicted = predict(publicKey); + // if instance does not exist ... + if (predicted.code.length == 0) { + // ... deploy it + Clones.cloneWithImmutableArgsDeterministic(implementation, publicKey, bytes32(0)); + } + return predicted; + } + + function predict(bytes memory publicKey) public view returns (address instance) { + return Clones.predictWithImmutableArgsDeterministicAddress(implementation, publicKey, bytes32(0)); + } +} diff --git a/contracts/abstraction/identity/IdentityRSA.sol b/contracts/abstraction/identity/IdentityRSA.sol new file mode 100644 index 00000000000..e8af41721f3 --- /dev/null +++ b/contracts/abstraction/identity/IdentityRSA.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {Clones} from "../../proxy/Clones.sol"; +import {RSA} from "../../utils/cryptography/RSA.sol"; + +contract IdentityRSAImplementation is IERC1271 { + function publicKey() public view returns (bytes memory e, bytes memory n) { + return abi.decode(Clones.fetchCloneArgs(address(this)), (bytes, bytes)); + } + + function isValidSignature(bytes32 h, bytes calldata signature) external view returns (bytes4 magicValue) { + // fetch immutable public key for the clone + (bytes memory e, bytes memory n) = publicKey(); + + // here we don't use pkcs1 directly, because `h` is likely not the result of a sha256 hash, but rather of a + // keccak256 one. This means RSA signers should compute the "ethereum" keccak256 hash of the data, and re-hash + // it using sha256 + return RSA.pkcs1Sha256(abi.encode(h), signature, e, n) ? IERC1271.isValidSignature.selector : bytes4(0); + } +} + +contract IdentityRSAFactory { + address public immutable implementation = address(new IdentityRSAImplementation()); + + function create(bytes calldata e, bytes calldata n) public returns (address instance) { + // predict the address of the instance for that key + address predicted = predict(e, n); + // if instance does not exist ... + if (predicted.code.length == 0) { + // ... deploy it + Clones.cloneWithImmutableArgsDeterministic(implementation, abi.encode(e, n), bytes32(0)); + } + return predicted; + } + + function predict(bytes calldata e, bytes calldata n) public view returns (address instance) { + return Clones.predictWithImmutableArgsDeterministicAddress(implementation, abi.encode(e, n), bytes32(0)); + } +} diff --git a/contracts/abstraction/mocks/AdvancedAccount.sol b/contracts/abstraction/mocks/AdvancedAccount.sol new file mode 100644 index 00000000000..9475a768104 --- /dev/null +++ b/contracts/abstraction/mocks/AdvancedAccount.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {AccessControl} from "../../access/AccessControl.sol"; +import {Account} from "../account/Account.sol"; +import {ERC7579Account} from "../account/ERC7579Account.sol"; +import {AccountMultisig} from "../account/modules/AccountMultisig.sol"; +import {AccountAllSignatures} from "../account/modules/recovery/AccountAllSignatures.sol"; + +contract AdvancedAccount is AccessControl, ERC7579Account, AccountAllSignatures, AccountMultisig { + bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); + uint256 private _requiredSignatures; + + constructor( + IEntryPoint entryPoint_, + address admin_, + address[] memory signers_, + uint256 requiredSignatures_ + ) ERC7579Account(entryPoint_) { + _grantRole(DEFAULT_ADMIN_ROLE, admin_); + for (uint256 i = 0; i < signers_.length; ++i) { + _grantRole(SIGNER_ROLE, signers_[i]); + } + _requiredSignatures = requiredSignatures_; + } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC7579Account, AccessControl) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function requiredSignatures() public view virtual override returns (uint256) { + return _requiredSignatures; + } + + function _isAuthorized(address user) internal view virtual override returns (bool) { + return hasRole(SIGNER_ROLE, user); + } + + function _processSignature( + bytes32 userOpHash, + bytes calldata signature + ) internal view virtual override(Account, AccountMultisig) returns (bool, address, uint48, uint48) { + return super._processSignature(userOpHash, signature); + } +} diff --git a/contracts/abstraction/mocks/SimpleAccount.sol b/contracts/abstraction/mocks/SimpleAccount.sol new file mode 100644 index 00000000000..9c23cd07ea3 --- /dev/null +++ b/contracts/abstraction/mocks/SimpleAccount.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {Ownable} from "../../access/Ownable.sol"; +import {ERC7579Account} from "../account/ERC7579Account.sol"; +import {AccountECDSA} from "../account/modules/recovery/AccountECDSA.sol"; +import {AccountERC1271} from "../account/modules/recovery/AccountERC1271.sol"; + +contract SimpleAccountECDSA is Ownable, ERC7579Account, AccountECDSA { + constructor(IEntryPoint entryPoint_, address owner_) ERC7579Account(entryPoint_) Ownable(owner_) {} + + function _isAuthorized(address user) internal view virtual override returns (bool) { + return user == owner(); + } +} + +contract SimpleAccountERC1271 is Ownable, ERC7579Account, AccountERC1271 { + constructor(IEntryPoint entryPoint_, address owner_) ERC7579Account(entryPoint_) Ownable(owner_) {} + + function _isAuthorized(address user) internal view virtual override returns (bool) { + return user == owner(); + } +} diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol new file mode 100644 index 00000000000..90341f6a33b --- /dev/null +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IEntryPoint, PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {Math} from "../../utils/math/Math.sol"; +import {Call} from "../../utils/Call.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(Packing.extract_32_6(bytes32(validationData), 0x00)); + validUntil = uint48(Packing.extract_32_6(bytes32(validationData), 0x06)); + aggregator = address(Packing.extract_32_20(bytes32(validationData), 0x0c)); + if (validUntil == 0) validUntil = type(uint48).max; + } + + function packValidationData( + address aggregator, + uint48 validAfter, + uint48 validUntil + ) internal pure returns (uint256) { + return + uint256(Packing.pack_12_20(Packing.pack_6_6(bytes6(validAfter), bytes6(validUntil)), bytes20(aggregator))); + } + + function packValidationData(bool sigSuccess, uint48 validUntil, uint48 validAfter) internal pure returns (uint256) { + return + uint256( + Packing.pack_12_20( + Packing.pack_6_6(bytes6(validAfter), bytes6(validUntil)), + bytes20(uint160(Math.ternary(sigSuccess, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED))) + ) + ); + } + + 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(Packing.extract_32_16(self.accountGasLimits, 0x00)); + } + + function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(Packing.extract_32_16(self.accountGasLimits, 0x10)); + } + + function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(Packing.extract_32_16(self.gasFees, 0x00)); + } + + function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(Packing.extract_32_16(self.gasFees, 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(Packing.extract_32_16(bytes32(source.accountGasLimits), 0x00)); + self.callGasLimit = uint128(Packing.extract_32_16(bytes32(source.accountGasLimits), 0x10)); + self.preVerificationGas = source.preVerificationGas; + self.maxPriorityFeePerGas = uint128(Packing.extract_32_16(bytes32(source.gasFees), 0x00)); + self.maxFeePerGas = uint128(Packing.extract_32_16(bytes32(source.gasFees), 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/abstraction/utils/SenderCreationHelper.sol b/contracts/abstraction/utils/SenderCreationHelper.sol new file mode 100644 index 00000000000..1382cbd45d5 --- /dev/null +++ b/contracts/abstraction/utils/SenderCreationHelper.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Call} from "../../utils/Call.sol"; + +/** + * @dev This is used as an helper by EntryPoint. Because creating an account requires calling an arbitrary (user + * controlled) factory with arbitrary (user controlled) data, its a call that can be used to impersonate the + * entrypoint. To avoid any potential issues, we bounce this operation through this helper. This removed the risk of + * a user using a malicious initCode to impersonate the EntryPoint. + */ +contract SenderCreationHelper { + error SenderAddressResult(address sender); + + function createSender(bytes calldata initCode) public returns (address) { + return + Call.call(address(bytes20(initCode[0:20])), 0, initCode[20:]) && Call.getReturnDataSize() >= 0x20 + ? abi.decode(Call.getReturnData(0x20), (address)) + : address(0); + } + + function createSenderAndRevert(bytes calldata initCode) public returns (address) { + revert SenderAddressResult(createSender(initCode)); + } + + function getSenderAddress(bytes calldata initCode) public returns (address sender) { + try this.createSenderAndRevert(initCode) { + return address(0); // Should not happen + } catch (bytes memory reason) { + if (reason.length != 0x24 || bytes4(reason) != SenderAddressResult.selector) { + return address(0); // Should not happen + } else { + assembly { + sender := mload(add(0x24, reason)) + } + } + } + } +} 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..8be9445530f --- /dev/null +++ b/contracts/interfaces/IERC7579Module.sol @@ -0,0 +1,88 @@ +// 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_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/mocks/CallReceiverMock.sol b/contracts/mocks/CallReceiverMock.sol index e371c7db800..a981241344c 100644 --- a/contracts/mocks/CallReceiverMock.sol +++ b/contracts/mocks/CallReceiverMock.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; contract CallReceiverMock { event MockFunctionCalled(); + event MockFunctionCalledExtra(address caller, uint256 value); event MockFunctionCalledWithArgs(uint256 a, uint256 b); uint256[] private _array; @@ -14,6 +15,10 @@ contract CallReceiverMock { return "0x1234"; } + function mockFunctionExtra() public payable { + emit MockFunctionCalledExtra(msg.sender, msg.value); + } + function mockFunctionEmptyReturn() public payable { emit MockFunctionCalled(); } diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 7f18d573fda..b5a30af8dee 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -27,7 +27,6 @@ import {MerkleProof} from "../utils/cryptography/MerkleProof.sol"; import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol"; import {P256} from "../utils/cryptography/P256.sol"; import {Panic} from "../utils/Panic.sol"; -import {Packing} from "../utils/Packing.sol"; import {RSA} from "../utils/cryptography/RSA.sol"; import {SafeCast} from "../utils/math/SafeCast.sol"; import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index d243d67f34b..661811d3fc0 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; +import {Create2} from "../utils/Create2.sol"; import {Errors} from "../utils/Errors.sol"; /** @@ -17,6 +18,8 @@ import {Errors} from "../utils/Errors.sol"; * deterministic method. */ library Clones { + error ImmutableArgsTooLarge(); + /** * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. * @@ -121,4 +124,142 @@ library Clones { ) internal view returns (address predicted) { return predictDeterministicAddress(implementation, salt, address(this)); } + + /** + * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`, with `args` + * attached to it as immutable arguments (that can be fetched using {fetchCloneArgs}). + * + * This function uses the create opcode, which should never revert. + */ + function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) { + return cloneWithImmutableArgs(implementation, args, 0); + } + + /** + * @dev Same as {xref-Clones-cloneWithImmutableArgs-address-bytes-}[cloneWithImmutableArgs], but with a `value` + * parameter to send native currency to the new contract. + * + * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory) + * to always have enough balance for new deployments. Consider exposing this function under a payable method. + */ + function cloneWithImmutableArgs( + address implementation, + bytes memory args, + uint256 value + ) internal returns (address instance) { + if (address(this).balance < value) { + revert Errors.InsufficientBalance(address(this).balance, value); + } + bytes memory bytecode = _cloneWithImmutableArgsCode(implementation, args); + assembly ("memory-safe") { + instance := create(value, add(bytecode, 0x20), mload(bytecode)) + } + if (instance == address(0)) { + revert Errors.FailedDeployment(); + } + } + + /** + * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`, with `args` + * attached to it as immutable arguments (that can be fetched using {fetchCloneArgs}). + * + * This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same + * `implementation` and `salt` multiple time will revert, since the clones cannot be deployed twice at the same + * address. + */ + function cloneWithImmutableArgsDeterministic( + address implementation, + bytes memory args, + bytes32 salt + ) internal returns (address instance) { + return cloneWithImmutableArgsDeterministic(implementation, args, salt, 0); + } + + /** + * @dev Same as {xref-Clones-cloneWithImmutableArgsDeterministic-address-bytes-bytes32-}[cloneWithImmutableArgsDeterministic], + * but with a `value` parameter to send native currency to the new contract. + * + * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory) + * to always have enough balance for new deployments. Consider exposing this function under a payable method. + */ + function cloneWithImmutableArgsDeterministic( + address implementation, + bytes memory args, + bytes32 salt, + uint256 value + ) internal returns (address instance) { + bytes memory bytecode = _cloneWithImmutableArgsCode(implementation, args); + return Create2.deploy(value, salt, bytecode); + } + + /** + * @dev Computes the address of a clone deployed using {Clones-cloneWithImmutableArgsDeterministic}. + */ + function predictWithImmutableArgsDeterministicAddress( + address implementation, + bytes memory args, + bytes32 salt, + address deployer + ) internal pure returns (address predicted) { + bytes memory bytecode = _cloneWithImmutableArgsCode(implementation, args); + return Create2.computeAddress(salt, keccak256(bytecode), deployer); + } + + /** + * @dev Computes the address of a clone deployed using {Clones-cloneWithImmutableArgsDeterministic}. + */ + function predictWithImmutableArgsDeterministicAddress( + address implementation, + bytes memory args, + bytes32 salt + ) internal view returns (address predicted) { + return predictWithImmutableArgsDeterministicAddress(implementation, args, salt, address(this)); + } + + /** + * @dev Get the immutable args attached to a clone. + * + * - If `instance` is a clone that was deployed using `clone` or `cloneDeterministic`, this + * function will return an empty array. + * - If `instance` is a clone that was deployed using `cloneWithImmutableArgs` or + * `cloneWithImmutableArgsDeterministic`, this function will return the args array used at + * creation. + * - If `instance` is NOT a clone deployed using this library, the behavior is undefined. This + * function should only be used to check addresses that are known to be clones. + */ + function fetchCloneArgs(address instance) internal view returns (bytes memory result) { + uint256 argsLength = instance.code.length - 0x2d; // revert if length is too short + assembly { + // reserve space + result := mload(0x40) + mstore(0x40, add(result, add(0x20, argsLength))) + // load + mstore(result, argsLength) + extcodecopy(instance, add(result, 0x20), 0x2d, argsLength) + } + } + + /** + * @dev Helper that prepares the initcode of the proxy with immutable args. + * + * An assembly variant of this function requires copying the `args` array, which can be efficiently done using + * `mcopy`. Unfortunatelly, that opcode is not available before cancun. A pure solidity implemenation using + * abi.encodePacked is more expensive but also more portable and easier to review. + */ + function _cloneWithImmutableArgsCode( + address implementation, + bytes memory args + ) private pure returns (bytes memory) { + uint256 initCodeLength = args.length + 0x2d; + if (initCodeLength > type(uint16).max) revert ImmutableArgsTooLarge(); + return + abi.encodePacked( + hex"3d61", + uint16(initCodeLength), + hex"80600b3d3981f3363d3d373d3d3d363d73", + implementation, + hex"5af43d82803e903d91602b57fd5bf3", + args + ); + } } diff --git a/contracts/utils/Call.sol b/contracts/utils/Call.sol new file mode 100644 index 00000000000..b3892e7266c --- /dev/null +++ b/contracts/utils/Call.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Math} from "./math/Math.sol"; + +/** + * Utility functions helpful when making different kinds of contract calls in Solidity. + */ +library Call { + function call(address to, uint256 value, bytes memory data) internal returns (bool success) { + return call(to, value, data, gasleft()); + } + + 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) internal view returns (bool success) { + return staticcall(to, data, gasleft()); + } + + 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) internal returns (bool success) { + return delegateCall(to, data, gasleft()); + } + + 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) + } + } + + function getReturnDataSize() internal pure returns (uint256 returnDataSize) { + assembly ("memory-safe") { + returnDataSize := returndatasize() + } + } + + function getReturnData(uint256 maxLen) internal pure returns (bytes memory ptr) { + return getReturnDataFixed(Math.min(maxLen, getReturnDataSize())); + } + + function getReturnDataFixed(uint256 len) internal pure returns (bytes memory ptr) { + assembly ("memory-safe") { + ptr := mload(0x40) + mstore(0x40, add(ptr, add(len, 0x20))) + mstore(ptr, len) + returndatacopy(add(ptr, 0x20), 0, len) + } + } + + function revertWithData(bytes memory returnData) internal pure { + assembly ("memory-safe") { + revert(add(returnData, 0x20), mload(returnData)) + } + } + + function revertWithCode(bytes32 code) internal pure { + assembly ("memory-safe") { + mstore(0, code) + revert(0, 0x20) + } + } +} diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol new file mode 100644 index 00000000000..d1cb7b9ddae --- /dev/null +++ b/contracts/utils/Memory.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Helper library packing and unpacking multiple values into bytes32 + */ +library Memory { + type FreePtr is bytes32; + + function save() internal pure returns (FreePtr ptr) { + assembly ("memory-safe") { + ptr := mload(0x40) + } + } + + function load(FreePtr ptr) internal pure { + assembly ("memory-safe") { + mstore(0x40, ptr) + } + } +} diff --git a/contracts/utils/NoncesWithKey.sol b/contracts/utils/NoncesWithKey.sol new file mode 100644 index 00000000000..ec4256c69fe --- /dev/null +++ b/contracts/utils/NoncesWithKey.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +contract NoncesWithKey { + /** + * @dev The nonce used for an `account` is not the expected current nonce. + */ + error InvalidAccountNonce(address account, uint256 currentNonce); + + mapping(address => mapping(uint192 => uint64)) private _nonce; + + function getNonce(address owner, uint192 key) public view virtual returns (uint256) { + return (uint256(key) << 64) | _nonce[owner][key]; + } + + function _useNonce(address owner, uint192 key) internal virtual returns (uint64) { + // TODO: use unchecked here? Do we expect 2**64 nonce ever be used for a single owner? + return _nonce[owner][key]++; + } + + function _tryUseNonce(address owner, uint256 keyNonce) internal returns (bool) { + return _tryUseNonce(owner, uint192(keyNonce >> 64), uint64(keyNonce)); + } + + function _tryUseNonce(address owner, uint192 key, uint64 nonce) internal virtual returns (bool) { + return _useNonce(owner, key) == nonce; + } + + function _useNonceOrRevert(address owner, uint256 keyNonce) internal { + _useNonceOrRevert(owner, uint192(keyNonce >> 64), uint64(keyNonce)); + } + + function _useNonceOrRevert(address owner, uint192 key, uint64 nonce) internal virtual { + uint256 current = _useNonce(owner, key); + if (nonce != current) { + revert InvalidAccountNonce(owner, current); + } + } +} diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol index a5a38b50397..61cfa42c757 100644 --- a/contracts/utils/Packing.sol +++ b/contracts/utils/Packing.sol @@ -57,6 +57,30 @@ library Packing { } } + function pack_2_8(bytes2 left, bytes8 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + result := or(left, shr(16, right)) + } + } + + function pack_2_10(bytes2 left, bytes10 right) internal pure returns (bytes12 result) { + assembly ("memory-safe") { + result := or(left, shr(16, right)) + } + } + + function pack_2_20(bytes2 left, bytes20 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(16, right)) + } + } + + function pack_2_22(bytes2 left, bytes22 right) internal pure returns (bytes24 result) { + assembly ("memory-safe") { + result := or(left, shr(16, right)) + } + } + function pack_4_2(bytes4 left, bytes2 right) internal pure returns (bytes6 result) { assembly ("memory-safe") { result := or(left, shr(32, right)) @@ -69,6 +93,12 @@ library Packing { } } + function pack_4_6(bytes4 left, bytes6 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + result := or(left, shr(32, right)) + } + } + function pack_4_8(bytes4 left, bytes8 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { result := or(left, shr(32, right)) @@ -111,12 +141,42 @@ library Packing { } } + function pack_6_4(bytes6 left, bytes4 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + result := or(left, shr(48, right)) + } + } + function pack_6_6(bytes6 left, bytes6 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { result := or(left, shr(48, right)) } } + function pack_6_10(bytes6 left, bytes10 right) internal pure returns (bytes16 result) { + assembly ("memory-safe") { + result := or(left, shr(48, right)) + } + } + + function pack_6_16(bytes6 left, bytes16 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(48, right)) + } + } + + function pack_6_22(bytes6 left, bytes22 right) internal pure returns (bytes28 result) { + assembly ("memory-safe") { + result := or(left, shr(48, right)) + } + } + + function pack_8_2(bytes8 left, bytes2 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + result := or(left, shr(64, right)) + } + } + function pack_8_4(bytes8 left, bytes4 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { result := or(left, shr(64, right)) @@ -153,6 +213,36 @@ library Packing { } } + function pack_10_2(bytes10 left, bytes2 right) internal pure returns (bytes12 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + + function pack_10_6(bytes10 left, bytes6 right) internal pure returns (bytes16 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + + function pack_10_10(bytes10 left, bytes10 right) internal pure returns (bytes20 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + + function pack_10_12(bytes10 left, bytes12 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + + function pack_10_22(bytes10 left, bytes22 right) internal pure returns (bytes32 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + function pack_12_4(bytes12 left, bytes4 right) internal pure returns (bytes16 result) { assembly ("memory-safe") { result := or(left, shr(96, right)) @@ -165,6 +255,12 @@ library Packing { } } + function pack_12_10(bytes12 left, bytes10 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(96, right)) + } + } + function pack_12_12(bytes12 left, bytes12 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { result := or(left, shr(96, right)) @@ -189,6 +285,12 @@ library Packing { } } + function pack_16_6(bytes16 left, bytes6 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(128, right)) + } + } + function pack_16_8(bytes16 left, bytes8 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { result := or(left, shr(128, right)) @@ -207,6 +309,12 @@ library Packing { } } + function pack_20_2(bytes20 left, bytes2 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(160, right)) + } + } + function pack_20_4(bytes20 left, bytes4 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { result := or(left, shr(160, right)) @@ -225,6 +333,24 @@ library Packing { } } + function pack_22_2(bytes22 left, bytes2 right) internal pure returns (bytes24 result) { + assembly ("memory-safe") { + result := or(left, shr(176, right)) + } + } + + function pack_22_6(bytes22 left, bytes6 right) internal pure returns (bytes28 result) { + assembly ("memory-safe") { + result := or(left, shr(176, right)) + } + } + + function pack_22_10(bytes22 left, bytes10 right) internal pure returns (bytes32 result) { + assembly ("memory-safe") { + result := or(left, shr(176, right)) + } + } + function pack_24_4(bytes24 left, bytes4 right) internal pure returns (bytes28 result) { assembly ("memory-safe") { result := or(left, shr(192, right)) @@ -383,6 +509,76 @@ 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") { + 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") { + 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") { + 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") { + 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") { + 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") { @@ -453,6 +649,20 @@ 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") { + 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") { @@ -523,6 +733,20 @@ 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") { + 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") { @@ -607,6 +831,20 @@ 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") { + 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") { @@ -635,6 +873,132 @@ 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") { + 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") { + 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") { + 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") { + 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") { + 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") { + 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") { + 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") { + 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") { + 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") { @@ -705,6 +1069,20 @@ 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") { + 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") { @@ -747,6 +1125,20 @@ 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") { + 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") { @@ -817,6 +1209,20 @@ 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") { + 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") { @@ -859,6 +1265,20 @@ 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") { + 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") { @@ -943,6 +1363,20 @@ 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") { + 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") { @@ -985,6 +1419,20 @@ 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") { + 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/hardhat.config.js b/hardhat.config.js index d39d3d07323..50b10e38115 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -40,6 +40,11 @@ const { argv } = require('yargs/yargs')() type: 'string', default: 'cancun', }, + unlimited: { + alias: 'allowUnlimitedContractSize', + type: 'boolean', + default: false, + }, // Extra modules coverage: { type: 'boolean', 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/abstraction/ERC7579Utils.t.sol b/test/abstraction/ERC7579Utils.t.sol new file mode 100644 index 00000000000..b5a53a5c17c --- /dev/null +++ b/test/abstraction/ERC7579Utils.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {ERC7579Utils, Execution, Mode, CallType, ExecType, ModeSelector, ModePayload} from "@openzeppelin/contracts/abstraction/utils/ERC7579Utils.sol"; + +contract ERC7579UtilsTest is Test { + using ERC7579Utils for *; + + function testEncodeDecodeMode( + CallType callType, + ExecType execType, + ModeSelector modeSelector, + ModePayload modePayload + ) public { + (CallType callType2, ExecType execType2, ModeSelector modeSelector2, ModePayload modePayload2) = ERC7579Utils + .encodeMode(callType, execType, modeSelector, modePayload) + .decodeMode(); + + assertTrue(callType == callType2); + assertTrue(execType == execType2); + assertTrue(modeSelector == modeSelector2); + assertTrue(modePayload == modePayload2); + } + + function testEncodeDecodeSingle(address target, uint256 value, bytes memory callData) public { + (address target2, uint256 value2, bytes memory callData2) = this._decodeSingle( + ERC7579Utils.encodeSingle(target, value, callData) + ); + + assertEq(target, target2); + assertEq(value, value2); + assertEq(callData, callData2); + } + + function testEncodeDecodeDelegate(address target, bytes memory callData) public { + (address target2, bytes memory callData2) = this._decodeDelegate(ERC7579Utils.encodeDelegate(target, callData)); + + assertEq(target, target2); + assertEq(callData, callData2); + } + + function testEncodeDecodeBatch(Execution[] memory executionBatch) public { + Execution[] memory executionBatch2 = this._decodeBatch(ERC7579Utils.encodeBatch(executionBatch)); + + assertEq(abi.encode(executionBatch), abi.encode(executionBatch2)); + } + + function _decodeSingle( + bytes calldata executionCalldata + ) external pure returns (address target, uint256 value, bytes calldata callData) { + return ERC7579Utils.decodeSingle(executionCalldata); + } + + function _decodeDelegate( + bytes calldata executionCalldata + ) external pure returns (address target, bytes calldata callData) { + return ERC7579Utils.decodeDelegate(executionCalldata); + } + + function _decodeBatch( + bytes calldata executionCalldata + ) external pure returns (Execution[] calldata executionBatch) { + return ERC7579Utils.decodeBatch(executionCalldata); + } +} diff --git a/test/abstraction/TODO.md b/test/abstraction/TODO.md new file mode 100644 index 00000000000..1cb0be092ae --- /dev/null +++ b/test/abstraction/TODO.md @@ -0,0 +1,5 @@ +- [ ] test ERC1271 +- [ ] test batch exec mode +- [ ] implement and test delegate exec mode ? +- [ ] implement module support +- [ ] implement ECDSA, 1271, multisig as modules ? \ No newline at end of file diff --git a/test/abstraction/accountECDSA.test.js b/test/abstraction/accountECDSA.test.js new file mode 100644 index 00000000000..a252ad4d7c8 --- /dev/null +++ b/test/abstraction/accountECDSA.test.js @@ -0,0 +1,168 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { IdentityHelper } = require('../helpers/identity'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { encodeMode, encodeSingle, encodeBatch } = require('../helpers/erc7579'); +const { encodeError } = require('../helpers/error'); + +async function fixture() { + const accounts = await ethers.getSigners(); + accounts.relayer = accounts.shift(); + accounts.beneficiary = accounts.shift(); + + // 4337 helper + const helper = new ERC4337Helper('SimpleAccountECDSA'); + const identity = new IdentityHelper(); + + // environment + const target = await ethers.deployContract('CallReceiverMock'); + + // create 4337 account controlled by ECDSA + const signer = await identity.newECDSASigner({ provider: ethers.provider }); + const sender = await helper.newAccount(signer); + + return { + accounts, + target, + helper, + entrypoint: helper.entrypoint, + factory: helper.factory, + sender, + }; +} + +describe('AccountECDSA', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('execute operation', function () { + beforeEach('fund account', async function () { + await this.accounts.relayer.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + }); + + describe('account not deployed yet', function () { + it('success: deploy and call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 17); + }); + }); + + describe('account already deployed', function () { + beforeEach(async function () { + await this.sender.deploy(this.accounts.relayer); + }); + + it('success: call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('success: call with short signature', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign()); + + // compact signature + operation.signature = ethers.Signature.from(operation.signature).compactSerialized; + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + describe('batch', function () { + it('success: batch call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('revert: batch call with one revert', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 69, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)).to.emit( + this.helper.entrypoint, + 'UserOperationRevertReason', + ); + }); + + it('success: try batch call with one revert', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01', execType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 69, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.sender, 'ERC7579TryExecuteUnsuccessful') + .withArgs(1, encodeError('CallReceiverMock: reverting')) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + }); + }); + }); +}); diff --git a/test/abstraction/accountERC1271.test.js b/test/abstraction/accountERC1271.test.js new file mode 100644 index 00000000000..e637d569a98 --- /dev/null +++ b/test/abstraction/accountERC1271.test.js @@ -0,0 +1,151 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { IdentityHelper } = require('../helpers/identity'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { encodeMode, encodeSingle, encodeBatch } = require('../helpers/erc7579'); +const { encodeError } = require('../helpers/error'); + +async function fixture() { + const accounts = await ethers.getSigners(); + accounts.relayer = accounts.shift(); + accounts.beneficiary = accounts.shift(); + + // 4337 helper + const helper = new ERC4337Helper('SimpleAccountERC1271'); + const identity = new IdentityHelper(); + + // environment + const target = await ethers.deployContract('CallReceiverMock'); + + // create 4337 account controlled by P256 + const signer = await identity.newP256Signer({ provider: ethers.provider }); + const sender = await helper.newAccount(signer); + + return { + accounts, + target, + helper, + entrypoint: helper.entrypoint, + factory: helper.factory, + signer, + sender, + }; +} + +describe('AccountERC1271', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('execute operation', function () { + beforeEach('fund account', async function () { + await this.accounts.relayer.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + }); + + describe('account not deployed yet', function () { + it('success: deploy and call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 17); + }); + }); + + describe('account already deployed', function () { + beforeEach(async function () { + await this.sender.deploy(this.accounts.relayer); + }); + + it('success: call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + describe('batch', function () { + it('success: batch call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('revert: batch call with one revert', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 69, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)).to.emit( + this.helper.entrypoint, + 'UserOperationRevertReason', + ); + }); + + it('success: try batch call with one revert', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01', execType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 69, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.sender, 'ERC7579TryExecuteUnsuccessful') + .withArgs(1, encodeError('CallReceiverMock: reverting')) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + }); + }); + }); +}); diff --git a/test/abstraction/accountMultisig.test.js b/test/abstraction/accountMultisig.test.js new file mode 100644 index 00000000000..bbe5924b366 --- /dev/null +++ b/test/abstraction/accountMultisig.test.js @@ -0,0 +1,137 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { IdentityHelper } = require('../helpers/identity'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { encodeMode, encodeSingle } = require('../helpers/erc7579'); + +async function fixture() { + const accounts = await ethers.getSigners(); + accounts.relayer = accounts.shift(); + accounts.beneficiary = accounts.shift(); + + // 4337 helper + const helper = new ERC4337Helper('AdvancedAccount', { withTypePrefix: true }); + const identity = new IdentityHelper(); + + // environment + const target = await ethers.deployContract('CallReceiverMock'); + + // create 4337 account controlled by multiple signers + const signers = await Promise.all([ + identity.newECDSASigner(), // secp256k1 + identity.newP256Signer(), // secp256r1 + identity.newP256Signer(), // secp256r1 + identity.newECDSASigner(), // secp256k1 + ]); + const sender = await helper.newAccount(accounts.relayer, [signers, 2]); // 2-of-4 + + return { + accounts, + target, + helper, + entrypoint: helper.entrypoint, + factory: helper.factory, + signers, + sender, + }; +} + +describe('AccountMultisig', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('execute operation', function () { + beforeEach('fund account', async function () { + await this.accounts.relayer.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + }); + + describe('account not deployed yet', function () { + it('success: deploy and call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign(this.signers)); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 17); + }); + }); + + describe('account already deployed', function () { + beforeEach(async function () { + await this.sender.deploy(); + }); + + it('success: 3 signers', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign(this.signers)); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('success: 2 signers', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign([this.signers[0], this.signers[2]])); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('revert: not enough signers', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign([this.signers[2]])); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, 'AA24 signature error'); + }); + + it('revert: unauthorized signer', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign([this.accounts.relayer, this.signers[2]])); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, 'AA24 signature error'); + }); + }); + }); +}); diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js new file mode 100644 index 00000000000..fab14678411 --- /dev/null +++ b/test/abstraction/entrypoint.test.js @@ -0,0 +1,183 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); + +const { ERC4337Helper } = require('../helpers/erc4337'); +const { encodeMode, encodeSingle } = require('../helpers/erc7579'); + +async function fixture() { + const accounts = await ethers.getSigners(); + accounts.user = accounts.shift(); + accounts.beneficiary = accounts.shift(); + + const target = await ethers.deployContract('CallReceiverMock'); + const helper = new ERC4337Helper(); + await helper.wait(); + const sender = await helper.newAccount(accounts.user); + + return { + accounts, + target, + helper, + entrypoint: helper.entrypoint, + factory: helper.factory, + sender, + }; +} + +describe('EntryPoint', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('deploy wallet contract', function () { + it('success: counterfactual funding', async function () { + await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + const operation = await this.sender + .createOp() + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.sender, 'OwnershipTransferred') + .withArgs(ethers.ZeroAddress, this.accounts.user) + .to.emit(this.factory, 'return$deploy') + .withArgs(this.sender) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.entrypoint, 'Transfer') + .withArgs(ethers.ZeroAddress, this.sender, anyValue) + .to.emit(this.entrypoint, 'BeforeExecution') + // BeforeExecution has no args + .to.emit(this.entrypoint, 'UserOperationEvent') + .withArgs(operation.hash, this.sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue); + + expect(await ethers.provider.getCode(this.sender)).to.not.equal('0x'); + }); + + it.skip('[TODO] success: paymaster funding', async function () { + // TODO: deploy paymaster + // TODO: fund paymaster's account in entrypoint + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + // const operation = await this.sender.createOp({ paymaster: this.accounts.user }) + // .then(op => op.addInitCode()) + // .then(op => op.sign()); + // + // await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + // .to.emit(this.sender, 'OwnershipTransferred') + // .withArgs(ethers.ZeroAddress, this.accounts.user) + // .to.emit(this.factory, 'return$deploy') + // .withArgs(this.sender) + // .to.emit(this.entrypoint, 'AccountDeployed') + // .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + // .to.emit(this.entrypoint, 'Transfer') + // .withArgs(ethers.ZeroAddress, this.sender, anyValue) + // .to.emit(this.entrypoint, 'BeforeExecution') + // // BeforeExecution has no args + // .to.emit(this.entrypoint, 'UserOperationEvent') + // .withArgs(operation.hash, this.sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue); + + expect(await ethers.provider.getCode(this.sender)).to.not.equal('0x'); + }); + + it('error: AA10 sender already constructed', async function () { + await this.sender.deploy(); + + expect(await ethers.provider.getCode(this.sender)).to.not.equal('0x'); + + const operation = await this.sender + .createOp() + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, 'AA10 sender already constructed'); + }); + + it("error: AA21 didn't pay prefund", async function () { + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + const operation = await this.sender + .createOp() + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, "AA21 didn't pay prefund"); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + }); + + it('error: AA25 invalid account nonce', async function () { + await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + const operation = await this.sender + .createOp({ nonce: 1n }) + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, 'AA25 invalid account nonce'); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + }); + }); + + describe('execute operation', function () { + beforeEach('fund account', async function () { + await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + }); + + describe('account not deployed yet', function () { + it('success: deploy and call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 17); + }); + }); + + describe('account already deployed', function () { + beforeEach(async function () { + await this.sender.deploy(); + }); + + it('success: call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + }); + }); +}); diff --git a/test/helpers/enums.js b/test/helpers/enums.js index f95767ab7e7..a6024eda22d 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -9,4 +9,5 @@ module.exports = { Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'), OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'), RevertType: Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'), + SignatureType: Enum('ECDSA', 'ERC1271'), }; diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js new file mode 100644 index 00000000000..4cc3b926964 --- /dev/null +++ b/test/helpers/erc4337.js @@ -0,0 +1,165 @@ +const { ethers } = require('hardhat'); + +const { SignatureType } = require('./enums'); + +function pack(left, right) { + return ethers.solidityPacked(['uint128', 'uint128'], [left, right]); +} + +/// Global ERC-4337 environment helper. +class ERC4337Helper { + constructor(account = 'SimpleAccountECDSA', params = {}) { + this.entrypointAsPromise = ethers.deployContract('EntryPoint'); + this.factoryAsPromise = ethers.deployContract('$Create2'); + this.accountAsPromise = ethers.getContractFactory(account); + this.chainIdAsPromise = ethers.provider.getNetwork().then(({ chainId }) => chainId); + this.params = params; + } + + async wait() { + this.entrypoint = await this.entrypointAsPromise; + this.factory = await this.factoryAsPromise; + this.account = await this.accountAsPromise; + this.chainId = await this.chainIdAsPromise; + return this; + } + + async newAccount(signer, extraArgs = [], salt = ethers.randomBytes(32)) { + await this.wait(); + const initCode = await this.account + .getDeployTransaction(this.entrypoint, signer, ...extraArgs) + .then(tx => this.factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) + .then(deployCode => ethers.concat([this.factory.target, deployCode])); + const instance = await this.entrypoint.getSenderAddress + .staticCall(initCode) + .then(address => this.account.attach(address).connect(signer)); + return new AbstractAccount(instance, initCode, this); + } +} + +/// Represent one ERC-4337 account contract. +class AbstractAccount extends ethers.BaseContract { + constructor(instance, initCode, context) { + super(instance.target, instance.interface, instance.runner, instance.deployTx); + this.address = instance.target; + this.initCode = initCode; + this.context = context; + } + + async deploy(account = this.runner) { + this.deployTx = await account.sendTransaction({ + to: '0x' + this.initCode.replace(/0x/, '').slice(0, 40), + data: '0x' + this.initCode.replace(/0x/, '').slice(40), + }); + return this; + } + + async createOp(args = {}) { + const params = Object.assign({ sender: this }, args); + // fetch nonce + if (!params.nonce) { + params.nonce = await this.context.entrypointAsPromise.then(entrypoint => entrypoint.getNonce(this, 0)); + } + // prepare paymaster and data + if (ethers.isAddressable(params.paymaster)) { + params.paymaster = await ethers.resolveAddress(params.paymaster); + params.paymasterVerificationGasLimit ??= 100_000n; + params.paymasterPostOpGasLimit ??= 100_000n; + params.paymasterAndData = ethers.solidityPacked( + ['address', 'uint128', 'uint128'], + [params.paymaster, params.paymasterVerificationGasLimit, params.paymasterPostOpGasLimit], + ); + } + return new UserOperation(params); + } +} + +/// Represent one user operation +class UserOperation { + constructor(params) { + this.sender = params.sender; + this.nonce = params.nonce; + this.initCode = params.initCode ?? '0x'; + this.callData = params.callData ?? '0x'; + this.verificationGas = params.verificationGas ?? 10_000_000n; + this.callGas = params.callGas ?? 100_000n; + this.preVerificationGas = params.preVerificationGas ?? 100_000n; + this.maxPriorityFee = params.maxPriorityFee ?? 100_000n; + this.maxFeePerGas = params.maxFeePerGas ?? 100_000n; + this.paymasterAndData = params.paymasterAndData ?? '0x'; + this.signature = params.signature ?? '0x'; + } + + get packed() { + return { + sender: this.sender, + nonce: this.nonce, + initCode: this.initCode, + callData: this.callData, + accountGasLimits: pack(this.verificationGas, this.callGas), + preVerificationGas: this.preVerificationGas, + gasFees: pack(this.maxPriorityFee, this.maxFeePerGas), + paymasterAndData: this.paymasterAndData, + signature: this.signature, + }; + } + + get hash() { + const p = this.packed; + const h = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'], + [ + p.sender.target, + p.nonce, + ethers.keccak256(p.initCode), + ethers.keccak256(p.callData), + p.accountGasLimits, + p.preVerificationGas, + p.gasFees, + ethers.keccak256(p.paymasterAndData), + ], + ), + ); + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes32', 'address', 'uint256'], + [h, this.sender.context.entrypoint.target, this.sender.context.chainId], + ), + ); + } + + addInitCode() { + this.initCode = this.sender.initCode; + return this; + } + + async sign(signer = this.sender.runner, args = {}) { + const withTypePrefix = args.withTypePrefix ?? this.sender.context.params.withTypePrefix; + + const signers = (Array.isArray(signer) ? signer : [signer]).sort( + (signer1, signer2) => signer1.address - signer2.address, + ); + const signatures = await Promise.all( + signers.map(signer => + Promise.resolve(signer.signMessage(ethers.getBytes(this.hash))).then(signature => + withTypePrefix + ? ethers.solidityPacked(['uint8', 'bytes'], [signer.type ?? SignatureType.ECDSA, signature]) + : signature, + ), + ), + ); + + this.signature = Array.isArray(signer) + ? ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]'], [signatures]) + : signatures[0]; + + return this; + } +} + +module.exports = { + ERC4337Helper, + AbstractAccount, + UserOperation, +}; diff --git a/test/helpers/erc7579.js b/test/helpers/erc7579.js new file mode 100644 index 00000000000..e2ffdfa08e9 --- /dev/null +++ b/test/helpers/erc7579.js @@ -0,0 +1,33 @@ +const { ethers } = require('hardhat'); + +const encodeMode = ({ + callType = '0x00', + execType = '0x00', + selector = '0x00000000', + payload = '0x00000000000000000000000000000000000000000000', +} = {}) => + ethers.solidityPacked( + ['bytes1', 'bytes1', 'bytes4', 'bytes4', 'bytes22'], + [callType, execType, '0x00000000', selector, payload], + ); + +const encodeSingle = (target, value = 0n, data = '0x') => + ethers.solidityPacked(['address', 'uint256', 'bytes'], [target.target ?? target.address ?? target, value, data]); + +const encodeBatch = (...entries) => + ethers.AbiCoder.defaultAbiCoder().encode( + ['(address,uint256,bytes)[]'], + [ + entries.map(entry => + Array.isArray(entry) + ? [entry[0].target ?? entry[0].address ?? entry[0], entry[1] ?? 0n, entry[2] ?? '0x'] + : [entry.target.target ?? entry.target.address ?? entry.target, entry.value ?? 0n, entry.data ?? '0x'], + ), + ], + ); + +module.exports = { + encodeMode, + encodeSingle, + encodeBatch, +}; diff --git a/test/helpers/error.js b/test/helpers/error.js new file mode 100644 index 00000000000..19af0235e73 --- /dev/null +++ b/test/helpers/error.js @@ -0,0 +1,7 @@ +const { ethers } = require('ethers'); + +const interface = ethers.Interface.from(['error Error(string)']); + +module.exports = { + encodeError: str => interface.encodeErrorResult('Error', [str]), +}; diff --git a/test/helpers/identity.js b/test/helpers/identity.js new file mode 100644 index 00000000000..cb0064675b9 --- /dev/null +++ b/test/helpers/identity.js @@ -0,0 +1,44 @@ +const { ethers } = require('hardhat'); + +const { P256Signer } = require('./p256'); +const { SignatureType } = require('./enums'); + +class IdentityHelper { + constructor() { + this.p256FactoryAsPromise = ethers.deployContract('IdentityP256Factory'); + this.rsaFactoryAsPromise = ethers.deployContract('IdentityRSAFactory'); + } + + async wait() { + this.p256Factory = await this.p256FactoryAsPromise; + this.rsaFactory = await this.rsaFactoryAsPromise; + return this; + } + + async newECDSASigner(params = {}) { + return Object.assign(ethers.Wallet.createRandom(params.provider), { type: SignatureType.ECDSA }); + } + + async newP256Signer(params = {}) { + params.withPrefixAddress ??= true; + + await this.wait(); + const signer = P256Signer.random(params); + await Promise.all([this.p256Factory.predict(signer.publicKey), this.p256Factory.create(signer.publicKey)]).then( + ([address]) => Object.assign(signer, { address, provider: params.provider }), + ); + + return signer; + } + + async newRSASigner() { + await this.wait(); + + return Promise.reject('Not implemented yet'); + } +} + +module.exports = { + SignatureType, + IdentityHelper, +}; diff --git a/test/helpers/p256.js b/test/helpers/p256.js new file mode 100644 index 00000000000..eec5f47bede --- /dev/null +++ b/test/helpers/p256.js @@ -0,0 +1,57 @@ +const { ethers } = require('hardhat'); +const { secp256r1 } = require('@noble/curves/p256'); + +const { SignatureType } = require('./enums'); + +class P256Signer { + constructor(privateKey, params = {}) { + this.privateKey = privateKey; + this.publicKey = ethers.concat( + [ + secp256r1.getPublicKey(this.privateKey, false).slice(0x01, 0x21), + secp256r1.getPublicKey(this.privateKey, false).slice(0x21, 0x41), + ].map(ethers.hexlify), + ); + this.address = ethers.getAddress(ethers.keccak256(this.publicKey).slice(-40)); + this.params = Object.assign({ withPrefixAddress: false, withRecovery: true }, params); + } + + get type() { + return SignatureType.ERC1271; + } + + static random(params = {}) { + return new P256Signer(secp256r1.utils.randomPrivateKey(), params); + } + + getAddress() { + return this.address; + } + + signMessage(message) { + let { r, s, recovery } = secp256r1.sign(ethers.hashMessage(message).replace(/0x/, ''), this.privateKey); + + // ensureLowerOrderS + if (s > secp256r1.CURVE.n / 2n) { + s = secp256r1.CURVE.n - s; + recovery = 1 - recovery; + } + + // pack signature + const elements = [ + this.params.withPrefixAddress && { type: 'address', value: this.address }, + { type: 'uint256', value: ethers.toBeHex(r) }, + { type: 'uint256', value: ethers.toBeHex(s) }, + this.params.withRecovery && { type: 'uint8', value: recovery }, + ].filter(Boolean); + + return ethers.solidityPacked( + elements.map(({ type }) => type), + elements.map(({ value }) => value), + ); + } +} + +module.exports = { + P256Signer, +}; diff --git a/test/proxy/Clones.test.js b/test/proxy/Clones.test.js index 70220fbf7a0..6ad7d55ccae 100644 --- a/test/proxy/Clones.test.js +++ b/test/proxy/Clones.test.js @@ -10,30 +10,47 @@ async function fixture() { const factory = await ethers.deployContract('$Clones'); const implementation = await ethers.deployContract('DummyImplementation'); - const newClone = async (opts = {}) => { - const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address)); - const tx = await (opts.deployValue - ? factory.$clone(implementation, ethers.Typed.uint256(opts.deployValue)) - : factory.$clone(implementation)); - if (opts.initData || opts.initValue) { - await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' }); - } - return Object.assign(clone, { deploymentTransaction: () => tx }); - }; - - const newCloneDeterministic = async (opts = {}) => { - const salt = opts.salt ?? ethers.randomBytes(32); - const clone = await factory.$cloneDeterministic - .staticCall(implementation, salt) - .then(address => implementation.attach(address)); - const tx = await (opts.deployValue - ? factory.$cloneDeterministic(implementation, salt, ethers.Typed.uint256(opts.deployValue)) - : factory.$cloneDeterministic(implementation, salt)); - if (opts.initData || opts.initValue) { - await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' }); - } - return Object.assign(clone, { deploymentTransaction: () => tx }); - }; + const newClone = + args => + async (opts = {}) => { + const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address)); + const tx = await (opts.deployValue + ? args + ? factory.$cloneWithImmutableArgs(implementation, args, ethers.Typed.uint256(opts.deployValue)) + : factory.$clone(implementation, ethers.Typed.uint256(opts.deployValue)) + : args + ? factory.$cloneWithImmutableArgs(implementation, args) + : factory.$clone(implementation)); + if (opts.initData || opts.initValue) { + await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' }); + } + return Object.assign(clone, { deploymentTransaction: () => tx }); + }; + + const newCloneDeterministic = + args => + async (opts = {}) => { + const salt = opts.salt ?? ethers.randomBytes(32); + const clone = await factory.$cloneDeterministic + .staticCall(implementation, salt) + .then(address => implementation.attach(address)); + const tx = await (opts.deployValue + ? args + ? factory.$cloneWithImmutableArgsDeterministic( + implementation, + args, + salt, + ethers.Typed.uint256(opts.deployValue), + ) + : factory.$cloneDeterministic(implementation, salt, ethers.Typed.uint256(opts.deployValue)) + : args + ? factory.$cloneWithImmutableArgsDeterministic(implementation, args, salt) + : factory.$cloneDeterministic(implementation, salt)); + if (opts.initData || opts.initValue) { + await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' }); + } + return Object.assign(clone, { deploymentTransaction: () => tx }); + }; return { deployer, factory, implementation, newClone, newCloneDeterministic }; } @@ -43,53 +60,94 @@ describe('Clones', function () { Object.assign(this, await loadFixture(fixture)); }); - describe('clone', function () { - beforeEach(async function () { - this.createClone = this.newClone; + for (const args of [undefined, '0x', '0x11223344']) { + describe(args ? `with immutable args: ${args}` : 'without immutable args', function () { + describe('clone', function () { + beforeEach(async function () { + this.createClone = this.newClone(args); + }); + + shouldBehaveLikeClone(); + + it('get immutable arguments', async function () { + const instance = await this.createClone(); + expect(await this.factory.$fetchCloneArgs(instance)).to.equal(args ?? '0x'); + }); + }); + + describe('cloneDeterministic', function () { + beforeEach(async function () { + this.createClone = this.newCloneDeterministic(undefined); + }); + + shouldBehaveLikeClone(); + + it('revert if address already used', async function () { + const salt = ethers.randomBytes(32); + + const deployClone = () => + args + ? this.factory.$cloneWithImmutableArgsDeterministic(this.implementation, args, salt) + : this.factory.$cloneDeterministic(this.implementation, salt); + + // deploy once + await expect(deployClone()).to.not.be.reverted; + + // deploy twice + await expect(deployClone()).to.be.revertedWithCustomError(this.factory, 'FailedDeployment'); + }); + + it('address prediction', async function () { + const salt = ethers.randomBytes(32); + + if (args) { + const expected = ethers.getCreate2Address( + this.factory.target, + salt, + ethers.keccak256( + ethers.concat([ + '0x3d61', + ethers.toBeHex(0x2d + ethers.getBytes(args).length, 2), + '0x80600b3d3981f3363d3d373d3d3d363d73', + this.implementation.target, + '0x5af43d82803e903d91602b57fd5bf3', + args, + ]), + ), + ); + + const predicted = await this.factory.$predictWithImmutableArgsDeterministicAddress( + this.implementation, + args, + salt, + ); + expect(predicted).to.equal(expected); + + await expect(this.factory.$cloneWithImmutableArgsDeterministic(this.implementation, args, salt)) + .to.emit(this.factory, 'return$cloneWithImmutableArgsDeterministic_address_bytes_bytes32') + .withArgs(predicted); + } else { + const expected = ethers.getCreate2Address( + this.factory.target, + salt, + ethers.keccak256( + ethers.concat([ + '0x3d602d80600a3d3981f3363d3d373d3d3d363d73', + this.implementation.target, + '0x5af43d82803e903d91602b57fd5bf3', + ]), + ), + ); + + const predicted = await this.factory.$predictDeterministicAddress(this.implementation, salt); + expect(predicted).to.equal(expected); + + await expect(this.factory.$cloneDeterministic(this.implementation, salt)) + .to.emit(this.factory, 'return$cloneDeterministic_address_bytes32') + .withArgs(predicted); + } + }); + }); }); - - shouldBehaveLikeClone(); - }); - - describe('cloneDeterministic', function () { - beforeEach(async function () { - this.createClone = this.newCloneDeterministic; - }); - - shouldBehaveLikeClone(); - - it('revert if address already used', async function () { - const salt = ethers.randomBytes(32); - - // deploy once - await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.emit( - this.factory, - 'return$cloneDeterministic_address_bytes32', - ); - - // deploy twice - await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.be.revertedWithCustomError( - this.factory, - 'FailedDeployment', - ); - }); - - it('address prediction', async function () { - const salt = ethers.randomBytes(32); - - const creationCode = ethers.concat([ - '0x3d602d80600a3d3981f3363d3d373d3d3d363d73', - this.implementation.target, - '0x5af43d82803e903d91602b57fd5bf3', - ]); - - const predicted = await this.factory.$predictDeterministicAddress(this.implementation, salt); - const expected = ethers.getCreate2Address(this.factory.target, salt, ethers.keccak256(creationCode)); - expect(predicted).to.equal(expected); - - await expect(this.factory.$cloneDeterministic(this.implementation, salt)) - .to.emit(this.factory, 'return$cloneDeterministic_address_bytes32') - .withArgs(predicted); - }); - }); + } }); 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));