From daf2cb60faf3b66469881895d8b0f101ebf188bb Mon Sep 17 00:00:00 2001 From: Ha DANG Date: Tue, 2 Dec 2025 15:02:56 +0700 Subject: [PATCH 01/22] feat: implement ERC-7579 modular smart account (Phase 2) (#159) --- .gitmodules | 3 + foundry.lock | 20 + foundry.toml | 7 + lib/erc7579-implementation | 1 + remappings.txt | 2 + src/modular/AuraAccount.sol | 624 +++++++++++++++++++++++++++ src/modular/AuraAccountFactory.sol | 130 ++++++ test/modular/AuraAccount.t.sol | 314 ++++++++++++++ test/modular/mocks/MockExecutor.sol | 66 +++ test/modular/mocks/MockHook.sol | 85 ++++ test/modular/mocks/MockTarget.sol | 62 +++ test/modular/mocks/MockValidator.sol | 60 +++ 12 files changed, 1374 insertions(+) create mode 100644 foundry.lock create mode 160000 lib/erc7579-implementation create mode 100644 src/modular/AuraAccount.sol create mode 100644 src/modular/AuraAccountFactory.sol create mode 100644 test/modular/AuraAccount.t.sol create mode 100644 test/modular/mocks/MockExecutor.sol create mode 100644 test/modular/mocks/MockHook.sol create mode 100644 test/modular/mocks/MockTarget.sol create mode 100644 test/modular/mocks/MockValidator.sol diff --git a/.gitmodules b/.gitmodules index cc22e83..6330ff1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady +[submodule "lib/erc7579-implementation"] + path = lib/erc7579-implementation + url = https://github.com/erc7579/erc7579-implementation diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..4e72928 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,20 @@ +{ + "lib/account-abstraction": { + "rev": "7af70c8993a6f42973f520ae0752386a5032abe7" + }, + "lib/erc7579-implementation": { + "tag": { + "name": "v0.0.2", + "rev": "0f321b8544cea7ad3be1b5669c7998329636ef84" + } + }, + "lib/forge-std": { + "rev": "8e40513d678f392f398620b3ef2b418648b33e89" + }, + "lib/openzeppelin-contracts": { + "rev": "932fddf69a699a9a80fd2396fd1a2ab91cdda123" + }, + "lib/solady": { + "rev": "836c169fe357b3c23ad5d5755a9b4fbbfad7a99b" + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index a9d54da..e0af161 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,13 @@ evm_version = "cancun" optimizer = true optimizer_runs = 1000000 via_ir = true +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/", + "@account-abstraction/=lib/account-abstraction/contracts/", + "forge-std/=lib/forge-std/src/", + "@erc7579/=lib/erc7579-implementation/src/", + "solady/=lib/solady/src/" +] [rpc_endpoints] sepolia = "${SEPOLIA_RPC_URL}" diff --git a/lib/erc7579-implementation b/lib/erc7579-implementation new file mode 160000 index 0000000..0f321b8 --- /dev/null +++ b/lib/erc7579-implementation @@ -0,0 +1 @@ +Subproject commit 0f321b8544cea7ad3be1b5669c7998329636ef84 diff --git a/remappings.txt b/remappings.txt index 3cdc147..a887a99 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,6 @@ @openzeppelin/=lib/openzeppelin-contracts/ @account-abstraction/=lib/account-abstraction/contracts/ forge-std/=lib/forge-std/src/ +@erc7579/=lib/erc7579-implementation/src/ +solady/=lib/solady/src/ diff --git a/src/modular/AuraAccount.sol b/src/modular/AuraAccount.sol new file mode 100644 index 0000000..8cd38c1 --- /dev/null +++ b/src/modular/AuraAccount.sol @@ -0,0 +1,624 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IAccount} from "@account-abstraction/interfaces/IAccount.sol"; +import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; +import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; +import {Initializable} from "solady/utils/Initializable.sol"; + +// ERC-7579 interfaces and libraries from battle-tested reference implementation +import {IERC7579Account, Execution} from "@erc7579/interfaces/IERC7579Account.sol"; +import { + IModule, + IValidator, + IExecutor, + IHook, + IFallback, + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK +} from "@erc7579/interfaces/IERC7579Module.sol"; +import { + ModeCode, + ModeLib, + CallType, + ExecType, + CALLTYPE_SINGLE, + CALLTYPE_BATCH, + CALLTYPE_STATIC, + CALLTYPE_DELEGATECALL, + EXECTYPE_DEFAULT, + EXECTYPE_TRY +} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +// ERC-4337 validation return values +uint256 constant VALIDATION_SUCCESS = 0; +uint256 constant VALIDATION_FAILED = 1; + +/** + * @title AuraAccount + * @notice ERC-7579 Smart Account for EthAura + * @dev Implements ERC-4337 Account Abstraction and ERC-7579 Modular Architecture + * Signature verification is delegated to validator modules (e.g., P256MFAValidatorModule) + */ +contract AuraAccount is IAccount, IERC7579Account, Initializable { + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @notice ERC-4337 EntryPoint v0.7 + IEntryPoint public constant ENTRYPOINT = IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032); + + /// @notice ERC-1271 magic value + bytes4 internal constant ERC1271_MAGIC_VALUE = 0x1626ba7e; + + /// @notice Account implementation ID + string public constant ACCOUNT_ID = "ethaura.aura.0.1.0"; + + /// @notice Sentinel address for linked list + address internal constant SENTINEL = address(0x1); + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice Installed validators (linked list) + mapping(address => address) internal _validators; + uint256 internal _validatorCount; + + /// @notice Installed executors + mapping(address => bool) internal _executors; + + /// @notice Fallback handlers (selector => handler) + mapping(bytes4 => address) internal _fallbackHandlers; + + /// @notice Global hook (single hook, can be MultiHook wrapper) + address internal _globalHook; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error OnlyEntryPoint(); + error OnlyEntryPointOrSelf(); + error OnlyExecutorModule(); + error InvalidModule(address module); + error ModuleAlreadyInstalled(address module); + error ModuleNotInstalled(address module); + error UnsupportedModuleType(uint256 moduleTypeId); + error UnsupportedExecutionMode(ModeCode mode); + error ExecutionFailed(); + error InvalidValidator(); + error NoValidatorInstalled(); + + /*////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////*/ + + modifier onlyEntryPoint() { + if (msg.sender != address(ENTRYPOINT)) revert OnlyEntryPoint(); + _; + } + + modifier onlyEntryPointOrSelf() { + if (msg.sender != address(ENTRYPOINT) && msg.sender != address(this)) { + revert OnlyEntryPointOrSelf(); + } + _; + } + + modifier onlyExecutorModule() { + if (!_executors[msg.sender]) revert OnlyExecutorModule(); + _; + } + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initialize the account with a default validator + * @param defaultValidator The default validator module address + * @param validatorData Initialization data for the validator + * @param hook The global hook address (optional, address(0) for none) + * @param hookData Initialization data for the hook + */ + function initialize(address defaultValidator, bytes calldata validatorData, address hook, bytes calldata hookData) + external + initializer + { + // Initialize validator linked list + _validators[SENTINEL] = SENTINEL; + + // Install default validator + if (defaultValidator != address(0)) { + _installValidator(defaultValidator, validatorData); + } + + // Install hook if provided + if (hook != address(0)) { + _installHook(hook, hookData); + } + } + + /*////////////////////////////////////////////////////////////// + RECEIVE ETH + //////////////////////////////////////////////////////////////*/ + + receive() external payable {} + + /*////////////////////////////////////////////////////////////// + ERC-4337 + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Validate a user operation (ERC-4337) + * @param userOp The user operation + * @param userOpHash The hash of the user operation + * @param missingAccountFunds Funds to prefund + * @return validationData Packed validation data + */ + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) + external + onlyEntryPoint + returns (uint256 validationData) + { + // Get the active validator (first in linked list for now) + address validator = _validators[SENTINEL]; + if (validator == SENTINEL || validator == address(0)) { + revert NoValidatorInstalled(); + } + + // Delegate validation to the validator module + validationData = IValidator(validator).validateUserOp(userOp, userOpHash); + + // Pay prefund + if (missingAccountFunds > 0) { + (bool success,) = payable(msg.sender).call{value: missingAccountFunds}(""); + (success); // Silence unused variable warning + } + } + + /*////////////////////////////////////////////////////////////// + EXECUTION + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IERC7579Account + function execute(ModeCode mode, bytes calldata executionCalldata) external payable onlyEntryPointOrSelf { + _executeWithHook(mode, executionCalldata); + } + + /// @inheritdoc IERC7579Account + function executeFromExecutor(ModeCode mode, bytes calldata executionCalldata) + external + payable + onlyExecutorModule + returns (bytes[] memory returnData) + { + returnData = _executeWithHook(mode, executionCalldata); + } + + /** + * @notice Execute with hook pre/post checks + */ + function _executeWithHook(ModeCode mode, bytes calldata executionCalldata) + internal + returns (bytes[] memory returnData) + { + // Pre-check hook + bytes memory hookData; + if (_globalHook != address(0)) { + hookData = IHook(_globalHook).preCheck(msg.sender, msg.value, executionCalldata); + } + + // Execute + returnData = _execute(mode, executionCalldata); + + // Post-check hook + if (_globalHook != address(0)) { + IHook(_globalHook).postCheck(hookData); + } + } + + /** + * @notice Internal execution logic + */ + function _execute(ModeCode mode, bytes calldata executionCalldata) internal returns (bytes[] memory returnData) { + (CallType callType, ExecType execType,,) = ModeLib.decode(mode); + + if (callType == CALLTYPE_SINGLE) { + returnData = _executeSingle(executionCalldata, execType); + } else if (callType == CALLTYPE_BATCH) { + returnData = _executeBatch(executionCalldata, execType); + } else if (callType == CALLTYPE_STATIC) { + returnData = _executeStatic(executionCalldata); + } else if (callType == CALLTYPE_DELEGATECALL) { + returnData = _executeDelegatecall(executionCalldata, execType); + } else { + revert UnsupportedExecutionMode(mode); + } + } + + /** + * @notice Execute a single call + */ + function _executeSingle(bytes calldata executionCalldata, ExecType execType) + internal + returns (bytes[] memory returnData) + { + (address target, uint256 value, bytes calldata data) = ExecutionLib.decodeSingle(executionCalldata); + + returnData = new bytes[](1); + + bool success; + (success, returnData[0]) = target.call{value: value}(data); + + if (execType == EXECTYPE_DEFAULT && !success) { + revert ExecutionFailed(); + } + } + + /** + * @notice Execute a batch of calls + */ + function _executeBatch(bytes calldata executionCalldata, ExecType execType) + internal + returns (bytes[] memory returnData) + { + Execution[] calldata executions = ExecutionLib.decodeBatch(executionCalldata); + returnData = new bytes[](executions.length); + + for (uint256 i = 0; i < executions.length; i++) { + bool success; + (success, returnData[i]) = executions[i].target.call{value: executions[i].value}(executions[i].callData); + + if (execType == EXECTYPE_DEFAULT && !success) { + revert ExecutionFailed(); + } + } + } + + /** + * @notice Execute a static call (view only) + */ + function _executeStatic(bytes calldata executionCalldata) internal view returns (bytes[] memory returnData) { + (address target,, bytes calldata data) = ExecutionLib.decodeSingle(executionCalldata); + + returnData = new bytes[](1); + bool success; + (success, returnData[0]) = target.staticcall(data); + + if (!success) { + revert ExecutionFailed(); + } + } + + /** + * @notice Execute a delegate call (DANGEROUS - use with care) + */ + function _executeDelegatecall(bytes calldata executionCalldata, ExecType execType) + internal + returns (bytes[] memory returnData) + { + (address target,, bytes calldata data) = ExecutionLib.decodeSingle(executionCalldata); + + // TODO: Add whitelist check for delegatecall targets + + returnData = new bytes[](1); + bool success; + (success, returnData[0]) = target.delegatecall(data); + + if (execType == EXECTYPE_DEFAULT && !success) { + revert ExecutionFailed(); + } + } + + /*////////////////////////////////////////////////////////////// + MODULE MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IERC7579Account + function installModule(uint256 moduleTypeId, address module, bytes calldata initData) + external + payable + onlyEntryPointOrSelf + { + if (module == address(0)) revert InvalidModule(module); + + if (moduleTypeId == MODULE_TYPE_VALIDATOR) { + _installValidator(module, initData); + } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) { + _installExecutor(module, initData); + } else if (moduleTypeId == MODULE_TYPE_FALLBACK) { + _installFallback(module, initData); + } else if (moduleTypeId == MODULE_TYPE_HOOK) { + _installHook(module, initData); + } else { + revert UnsupportedModuleType(moduleTypeId); + } + + emit ModuleInstalled(moduleTypeId, module); + } + + /// @inheritdoc IERC7579Account + function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) + external + payable + onlyEntryPointOrSelf + { + if (module == address(0)) revert InvalidModule(module); + + if (moduleTypeId == MODULE_TYPE_VALIDATOR) { + _uninstallValidator(module, deInitData); + } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) { + _uninstallExecutor(module, deInitData); + } else if (moduleTypeId == MODULE_TYPE_FALLBACK) { + _uninstallFallback(module, deInitData); + } else if (moduleTypeId == MODULE_TYPE_HOOK) { + _uninstallHook(module, deInitData); + } else { + revert UnsupportedModuleType(moduleTypeId); + } + + emit ModuleUninstalled(moduleTypeId, module); + } + + /*////////////////////////////////////////////////////////////// + VALIDATOR MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function _installValidator(address validator, bytes calldata data) internal { + if (_validators[validator] != address(0)) { + revert ModuleAlreadyInstalled(validator); + } + + // Add to linked list (insert at head) + _validators[validator] = _validators[SENTINEL]; + _validators[SENTINEL] = validator; + _validatorCount++; + + // Call onInstall + IValidator(validator).onInstall(data); + } + + function _uninstallValidator(address validator, bytes calldata data) internal { + if (_validators[validator] == address(0)) { + revert ModuleNotInstalled(validator); + } + + // Find previous in linked list + address prev = SENTINEL; + address current = _validators[SENTINEL]; + while (current != SENTINEL && current != validator) { + prev = current; + current = _validators[current]; + } + + if (current != validator) { + revert ModuleNotInstalled(validator); + } + + // Remove from linked list + _validators[prev] = _validators[validator]; + _validators[validator] = address(0); + _validatorCount--; + + // Call onUninstall + IValidator(validator).onUninstall(data); + } + + /*////////////////////////////////////////////////////////////// + EXECUTOR MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function _installExecutor(address executor, bytes calldata data) internal { + if (_executors[executor]) { + revert ModuleAlreadyInstalled(executor); + } + + _executors[executor] = true; + IExecutor(executor).onInstall(data); + } + + function _uninstallExecutor(address executor, bytes calldata data) internal { + if (!_executors[executor]) { + revert ModuleNotInstalled(executor); + } + + _executors[executor] = false; + IExecutor(executor).onUninstall(data); + } + + /*////////////////////////////////////////////////////////////// + FALLBACK MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function _installFallback(address handler, bytes calldata data) internal { + // Decode selector from data + bytes4 selector = bytes4(data[:4]); + bytes calldata initData = data[4:]; + + if (_fallbackHandlers[selector] != address(0)) { + revert ModuleAlreadyInstalled(handler); + } + + _fallbackHandlers[selector] = handler; + IFallback(handler).onInstall(initData); + } + + function _uninstallFallback(address handler, bytes calldata data) internal { + bytes4 selector = bytes4(data[:4]); + bytes calldata deInitData = data[4:]; + + if (_fallbackHandlers[selector] != handler) { + revert ModuleNotInstalled(handler); + } + + _fallbackHandlers[selector] = address(0); + IFallback(handler).onUninstall(deInitData); + } + + /*////////////////////////////////////////////////////////////// + HOOK MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function _installHook(address hook, bytes calldata data) internal { + if (_globalHook != address(0)) { + revert ModuleAlreadyInstalled(hook); + } + + _globalHook = hook; + IHook(hook).onInstall(data); + } + + function _uninstallHook(address hook, bytes calldata data) internal { + if (_globalHook != hook) { + revert ModuleNotInstalled(hook); + } + + _globalHook = address(0); + IHook(hook).onUninstall(data); + } + + /*////////////////////////////////////////////////////////////// + ERC-1271 + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IERC7579Account + function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { + // Get the active validator + address validator = _validators[SENTINEL]; + if (validator == SENTINEL || validator == address(0)) { + return bytes4(0xffffffff); + } + + // Delegate to validator + return IValidator(validator).isValidSignatureWithSender(msg.sender, hash, signature); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNT CONFIG + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IERC7579Account + function supportsExecutionMode(ModeCode mode) external pure returns (bool) { + (CallType callType, ExecType execType,,) = ModeLib.decode(mode); + + // Support single, batch, static calls + if (callType == CALLTYPE_SINGLE || callType == CALLTYPE_BATCH || callType == CALLTYPE_STATIC) { + // Support default and try exec types + return execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY; + } + + // Delegatecall is optional and dangerous + if (callType == CALLTYPE_DELEGATECALL) { + return execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY; + } + + return false; + } + + /// @inheritdoc IERC7579Account + function supportsModule(uint256 moduleTypeId) external pure returns (bool) { + return moduleTypeId == MODULE_TYPE_VALIDATOR || moduleTypeId == MODULE_TYPE_EXECUTOR + || moduleTypeId == MODULE_TYPE_FALLBACK || moduleTypeId == MODULE_TYPE_HOOK; + } + + /// @inheritdoc IERC7579Account + function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext) + external + view + returns (bool) + { + (additionalContext); // Silence unused variable warning + + if (moduleTypeId == MODULE_TYPE_VALIDATOR) { + return _validators[module] != address(0); + } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) { + return _executors[module]; + } else if (moduleTypeId == MODULE_TYPE_FALLBACK) { + // Check if module is handler for any selector + // For specific selector check, use additionalContext + if (additionalContext.length >= 4) { + bytes4 selector = bytes4(additionalContext[:4]); + return _fallbackHandlers[selector] == module; + } + return false; + } else if (moduleTypeId == MODULE_TYPE_HOOK) { + return _globalHook == module; + } + + return false; + } + + /// @inheritdoc IERC7579Account + function accountId() external pure returns (string memory) { + return ACCOUNT_ID; + } + + /*////////////////////////////////////////////////////////////// + FALLBACK + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Fallback function to route unknown selectors to fallback handlers + */ + fallback() external payable { + address handler = _fallbackHandlers[msg.sig]; + if (handler == address(0)) { + revert("No fallback handler"); + } + + // Forward call to handler + assembly { + // Copy calldata + calldatacopy(0, 0, calldatasize()) + + // Delegatecall to handler + let result := delegatecall(gas(), handler, 0, calldatasize(), 0, 0) + + // Copy return data + returndatacopy(0, 0, returndatasize()) + + // Return or revert + switch result + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Get the number of installed validators + */ + function getValidatorCount() external view returns (uint256) { + return _validatorCount; + } + + /** + * @notice Get the global hook address + */ + function getGlobalHook() external view returns (address) { + return _globalHook; + } + + /** + * @notice Get the fallback handler for a selector + */ + function getFallbackHandler(bytes4 selector) external view returns (address) { + return _fallbackHandlers[selector]; + } +} diff --git a/src/modular/AuraAccountFactory.sol b/src/modular/AuraAccountFactory.sol new file mode 100644 index 0000000..171e12a --- /dev/null +++ b/src/modular/AuraAccountFactory.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ERC1967Factory} from "solady/utils/ERC1967Factory.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; +import {AuraAccount} from "./AuraAccount.sol"; + +/** + * @title AuraAccountFactory + * @notice Factory for deploying AuraAccount instances + * @dev Uses Solady's canonical ERC1967Factory for hyper-optimized proxy deployment + * Account address is based on owner + implementation + salt, NOT validator config + * This allows users to receive funds first, then decide on validator later + */ +contract AuraAccountFactory { + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @notice The account implementation address + address public immutable accountImplementation; + + /// @notice Solady's canonical ERC1967Factory for deploying proxies + /// @dev Uses the canonical address: 0x0000000000006396FF2a80c067f99B3d2Ab4Df24 + ERC1967Factory public immutable PROXY_FACTORY; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event AccountCreated(address indexed account, address indexed owner, uint256 salt); + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error InvalidValidator(); + error AccountAlreadyDeployed(); + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor() { + // Deploy the implementation + accountImplementation = address(new AuraAccount()); + // Use Solady's canonical ERC1967Factory (saves deployment gas) + PROXY_FACTORY = ERC1967Factory(ERC1967FactoryConstants.ADDRESS); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNT CREATION + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Create a new modular account + * @param owner The owner address (e.g., Web3Auth address) + * @param defaultValidator The default validator module address + * @param validatorData Initialization data for the validator + * @param hook The global hook address (optional, address(0) for none) + * @param hookData Initialization data for the hook + * @param salt Salt for CREATE2 deterministic deployment + * @return account The deployed account address + */ + function createAccount( + address owner, + address defaultValidator, + bytes calldata validatorData, + address hook, + bytes calldata hookData, + uint256 salt + ) external returns (address account) { + if (defaultValidator == address(0)) revert InvalidValidator(); + + address addr = getAddress(owner, salt); + + // If account already exists, return it + uint256 codeSize = addr.code.length; + if (codeSize > 0) { + return addr; + } + + // Compute salt based on owner + implementation + salt (NOT validator config) + // This allows users to receive funds first, then decide on validator later + bytes32 finalSalt = _computeSalt(owner, salt); + + // Deploy proxy using Solady's canonical ERC1967Factory + // Admin is set to address(0) since we don't need upgradeability + account = PROXY_FACTORY.deployDeterministicAndCall( + accountImplementation, + address(0), // No admin - proxies are not upgradeable + finalSalt, + abi.encodeCall(AuraAccount.initialize, (defaultValidator, validatorData, hook, hookData)) + ); + + emit AccountCreated(account, owner, salt); + } + + /** + * @notice Get the counterfactual address of an account + * @param owner The owner address + * @param salt Salt for CREATE2 + * @return The predicted account address + * @dev Address is independent of validator choice - based only on owner + implementation + salt + */ + function getAddress(address owner, uint256 salt) public view returns (address) { + bytes32 finalSalt = _computeSalt(owner, salt); + return PROXY_FACTORY.predictDeterministicAddress(finalSalt); + } + + /** + * @notice Compute the salt for CREATE2 deployment + * @param owner The owner address + * @param salt User-provided salt + * @return The computed salt for CREATE2 + * @dev Solady's factory requires salt to start with caller address or zero address + * We use zero address prefix to allow anyone to deploy on behalf of users + * Salt format: [20 bytes: zero address][12 bytes: hash of owner+implementation+salt] + * Including implementation ensures different contract versions get different addresses + */ + function _computeSalt(address owner, uint256 salt) internal view returns (bytes32) { + // Combine owner, implementation address, and salt to create unique hash + // Including implementation ensures different contract versions get different addresses + bytes32 combinedSalt = keccak256(abi.encodePacked(owner, accountImplementation, salt)); + // Keep only the last 96 bits (12 bytes) of the hash + // The first 160 bits (20 bytes) will be zero, satisfying Solady's requirement + return bytes32(uint256(combinedSalt) & ((1 << 96) - 1)); + } +} + diff --git a/test/modular/AuraAccount.t.sol b/test/modular/AuraAccount.t.sol new file mode 100644 index 0000000..9d41b52 --- /dev/null +++ b/test/modular/AuraAccount.t.sol @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; + +// Use the battle-tested ERC-7579 library +import {IERC7579Account, Execution} from "@erc7579/interfaces/IERC7579Account.sol"; +import { + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK +} from "@erc7579/interfaces/IERC7579Module.sol"; +import {ModeLib, ModeCode} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +import {MockValidator} from "./mocks/MockValidator.sol"; +import {MockExecutor} from "./mocks/MockExecutor.sol"; +import {MockHook} from "./mocks/MockHook.sol"; +import {MockTarget} from "./mocks/MockTarget.sol"; + +contract AuraAccountTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + MockExecutor public executor; + MockHook public hook; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + address owner = address(0x1234); + + function setUp() public { + // Deploy canonical ERC1967Factory if not already deployed + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + // Deploy factory + factory = new AuraAccountFactory(); + + // Deploy mock modules + validator = new MockValidator(); + executor = new MockExecutor(); + hook = new MockHook(); + target = new MockTarget(); + + // Create account with validator + address accountAddr = factory.createAccount( + owner, + address(validator), + abi.encode(true), // shouldValidate = true + address(0), // no hook + "", + 0 // salt + ); + account = AuraAccount(payable(accountAddr)); + + // Fund account + vm.deal(address(account), 10 ether); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_Initialize() public view { + assertEq(account.accountId(), "ethaura.aura.0.1.0"); + assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); + assertEq(account.getValidatorCount(), 1); + } + + function test_InitializeWithHook() public { + address accountAddr = factory.createAccount( + owner, + address(validator), + abi.encode(true), + address(hook), + "", + 1 // different salt + ); + AuraAccount accountWithHook = AuraAccount(payable(accountAddr)); + + assertTrue(accountWithHook.isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + assertEq(accountWithHook.getGlobalHook(), address(hook)); + } + + function test_RevertInitializeTwice() public { + vm.expectRevert(); + account.initialize(address(validator), "", address(0), ""); + } + + /*////////////////////////////////////////////////////////////// + EXECUTION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ExecuteSingle() public { + // Prepare execution + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + // Execute from EntryPoint + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + + assertEq(target.value(), 42); + assertEq(target.lastCaller(), address(account)); + } + + function test_ExecuteSingleWithValue() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 1 ether, callData); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + + assertEq(target.value(), 100); + assertEq(address(target).balance, 1 ether); + } + + function test_ExecuteBatch() public { + // Prepare batch + Execution[] memory executions = new Execution[](3); + executions[0] = + Execution({target: address(target), value: 0, callData: abi.encodeCall(MockTarget.setValue, (10))}); + executions[1] = + Execution({target: address(target), value: 0, callData: abi.encodeCall(MockTarget.increment, ())}); + executions[2] = + Execution({target: address(target), value: 0, callData: abi.encodeCall(MockTarget.increment, ())}); + + bytes memory executionData = ExecutionLib.encodeBatch(executions); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleBatch(), executionData); + + assertEq(target.value(), 12); // 10 + 1 + 1 + assertEq(target.callCount(), 3); + } + + function test_RevertExecuteNotEntryPoint() public { + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, ""); + + vm.expectRevert(AuraAccount.OnlyEntryPointOrSelf.selector); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + } + + function test_ExecuteFromSelf() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (99)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + vm.prank(address(account)); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + + assertEq(target.value(), 99); + } + + /*////////////////////////////////////////////////////////////// + EXECUTOR MODULE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_InstallExecutor() public { + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), ""); + + assertTrue(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(executor), "")); + assertTrue(executor.isInitialized(address(account))); + } + + function test_ExecuteFromExecutor() public { + // Install executor + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), ""); + + // Execute through executor + executor.executeSingle(address(account), address(target), 0, abi.encodeCall(MockTarget.setValue, (77))); + + assertEq(target.value(), 77); + } + + function test_RevertExecuteFromNonExecutor() public { + vm.expectRevert(AuraAccount.OnlyExecutorModule.selector); + account.executeFromExecutor(ModeLib.encodeSimpleSingle(), ""); + } + + function test_UninstallExecutor() public { + // Install first + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), ""); + + // Uninstall + vm.prank(ENTRYPOINT); + account.uninstallModule(MODULE_TYPE_EXECUTOR, address(executor), ""); + + assertFalse(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(executor), "")); + } + + /*////////////////////////////////////////////////////////////// + HOOK TESTS + //////////////////////////////////////////////////////////////*/ + + function test_InstallHook() public { + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_HOOK, address(hook), ""); + + assertTrue(account.isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + assertEq(account.getGlobalHook(), address(hook)); + } + + function test_HookCalledOnExecute() public { + // Install hook + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_HOOK, address(hook), ""); + + // Execute + bytes memory callData = abi.encodeCall(MockTarget.setValue, (50)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + + assertEq(hook.preCheckCount(), 1); + assertEq(hook.postCheckCount(), 1); + assertEq(hook.lastSender(), ENTRYPOINT); + } + + function test_RevertOnHookPreCheckFail() public { + // Install hook + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_HOOK, address(hook), ""); + + // Set hook to revert + hook.setShouldRevertPreCheck(true); + + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, ""); + + vm.prank(ENTRYPOINT); + vm.expectRevert("MockHook: preCheck reverted"); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + } + + /*////////////////////////////////////////////////////////////// + MODULE CONFIG TESTS + //////////////////////////////////////////////////////////////*/ + + function test_SupportsExecutionMode() public view { + assertTrue(account.supportsExecutionMode(ModeLib.encodeSimpleSingle())); + assertTrue(account.supportsExecutionMode(ModeLib.encodeSimpleBatch())); + } + + function test_SupportsModuleType() public view { + assertTrue(account.supportsModule(MODULE_TYPE_VALIDATOR)); + assertTrue(account.supportsModule(MODULE_TYPE_EXECUTOR)); + assertTrue(account.supportsModule(MODULE_TYPE_FALLBACK)); + assertTrue(account.supportsModule(MODULE_TYPE_HOOK)); + assertFalse(account.supportsModule(99)); + } + + function test_RevertInstallInvalidModule() public { + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.InvalidModule.selector, address(0))); + account.installModule(MODULE_TYPE_EXECUTOR, address(0), ""); + } + + function test_RevertInstallDuplicateModule() public { + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), ""); + + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.ModuleAlreadyInstalled.selector, address(executor))); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), ""); + } + + function test_RevertUninstallNotInstalled() public { + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.ModuleNotInstalled.selector, address(executor))); + account.uninstallModule(MODULE_TYPE_EXECUTOR, address(executor), ""); + } + + /*////////////////////////////////////////////////////////////// + ERC-1271 TESTS + //////////////////////////////////////////////////////////////*/ + + function test_IsValidSignature() public view { + bytes32 hash = keccak256("test"); + bytes4 result = account.isValidSignature(hash, ""); + assertEq(result, bytes4(0x1626ba7e)); + } + + function test_IsValidSignatureInvalid() public { + validator.setValidation(address(account), false); + + bytes32 hash = keccak256("test"); + bytes4 result = account.isValidSignature(hash, ""); + assertEq(result, bytes4(0xffffffff)); + } + + /*////////////////////////////////////////////////////////////// + RECEIVE ETH TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ReceiveEth() public { + uint256 balanceBefore = address(account).balance; + + (bool success,) = address(account).call{value: 1 ether}(""); + assertTrue(success); + + assertEq(address(account).balance, balanceBefore + 1 ether); + } +} + diff --git a/test/modular/mocks/MockExecutor.sol b/test/modular/mocks/MockExecutor.sol new file mode 100644 index 0000000..9b8a997 --- /dev/null +++ b/test/modular/mocks/MockExecutor.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IExecutor, MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IERC7579Account, Execution} from "@erc7579/interfaces/IERC7579Account.sol"; +import {ModeLib} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +/** + * @title MockExecutor + * @notice Mock executor module for testing + */ +contract MockExecutor is IExecutor { + mapping(address => bool) private _initialized; + + function onInstall(bytes calldata) external override { + if (_initialized[msg.sender]) revert AlreadyInitialized(msg.sender); + _initialized[msg.sender] = true; + } + + function onUninstall(bytes calldata) external override { + if (!_initialized[msg.sender]) revert NotInitialized(msg.sender); + _initialized[msg.sender] = false; + } + + function isModuleType(uint256 moduleTypeId) external pure override returns (bool) { + return moduleTypeId == MODULE_TYPE_EXECUTOR; + } + + function isInitialized(address smartAccount) external view override returns (bool) { + return _initialized[smartAccount]; + } + + /** + * @notice Execute a single call through the account + */ + function executeSingle(address account, address target, uint256 value, bytes calldata data) + external + returns (bytes[] memory) + { + return IERC7579Account(account) + .executeFromExecutor(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(target, value, data)); + } + + /** + * @notice Execute a batch of calls through the account + */ + function executeBatch( + address account, + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata datas + ) external returns (bytes[] memory) { + require(targets.length == values.length && values.length == datas.length, "Length mismatch"); + + Execution[] memory executions = new Execution[](targets.length); + for (uint256 i = 0; i < targets.length; i++) { + executions[i] = Execution({target: targets[i], value: values[i], callData: datas[i]}); + } + + return + IERC7579Account(account) + .executeFromExecutor(ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(executions)); + } +} + diff --git a/test/modular/mocks/MockHook.sol b/test/modular/mocks/MockHook.sol new file mode 100644 index 0000000..b8873d3 --- /dev/null +++ b/test/modular/mocks/MockHook.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IHook, MODULE_TYPE_HOOK} from "@erc7579/interfaces/IERC7579Module.sol"; + +/** + * @title MockHook + * @notice Mock hook module for testing + */ +contract MockHook is IHook { + mapping(address => bool) private _initialized; + + // Track hook calls for testing + uint256 public preCheckCount; + uint256 public postCheckCount; + address public lastSender; + uint256 public lastValue; + bytes public lastData; + bool public shouldRevertPreCheck; + bool public shouldRevertPostCheck; + + function onInstall(bytes calldata) external override { + if (_initialized[msg.sender]) revert AlreadyInitialized(msg.sender); + _initialized[msg.sender] = true; + } + + function onUninstall(bytes calldata) external override { + if (!_initialized[msg.sender]) revert NotInitialized(msg.sender); + _initialized[msg.sender] = false; + } + + function isModuleType(uint256 moduleTypeId) external pure override returns (bool) { + return moduleTypeId == MODULE_TYPE_HOOK; + } + + function isInitialized(address smartAccount) external view override returns (bool) { + return _initialized[smartAccount]; + } + + function preCheck(address msgSender, uint256 msgValue, bytes calldata msgData) + external + override + returns (bytes memory hookData) + { + require(!shouldRevertPreCheck, "MockHook: preCheck reverted"); + + preCheckCount++; + lastSender = msgSender; + lastValue = msgValue; + lastData = msgData; + + // Return some data to pass to postCheck + return abi.encode(msgSender, msgValue, block.timestamp); + } + + function postCheck(bytes calldata hookData) external override { + require(!shouldRevertPostCheck, "MockHook: postCheck reverted"); + + postCheckCount++; + + // Decode and verify hookData + (address sender,,) = abi.decode(hookData, (address, uint256, uint256)); + require(sender == lastSender, "MockHook: hookData mismatch"); + } + + // Test helpers + function setShouldRevertPreCheck(bool shouldRevert) external { + shouldRevertPreCheck = shouldRevert; + } + + function setShouldRevertPostCheck(bool shouldRevert) external { + shouldRevertPostCheck = shouldRevert; + } + + function reset() external { + preCheckCount = 0; + postCheckCount = 0; + lastSender = address(0); + lastValue = 0; + lastData = ""; + shouldRevertPreCheck = false; + shouldRevertPostCheck = false; + } +} + diff --git a/test/modular/mocks/MockTarget.sol b/test/modular/mocks/MockTarget.sol new file mode 100644 index 0000000..154929e --- /dev/null +++ b/test/modular/mocks/MockTarget.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** + * @title MockTarget + * @notice Mock target contract for testing execution + */ +contract MockTarget { + uint256 public value; + address public lastCaller; + uint256 public callCount; + bool public shouldRevert; + + event Called(address indexed caller, uint256 value, bytes data); + event ValueSet(uint256 oldValue, uint256 newValue); + + function setValue(uint256 newValue) external payable { + require(!shouldRevert, "MockTarget: reverted"); + + emit ValueSet(value, newValue); + value = newValue; + lastCaller = msg.sender; + callCount++; + } + + function getValue() external view returns (uint256) { + return value; + } + + function increment() external { + require(!shouldRevert, "MockTarget: reverted"); + + value++; + lastCaller = msg.sender; + callCount++; + } + + function echo(bytes calldata data) external payable returns (bytes memory) { + require(!shouldRevert, "MockTarget: reverted"); + + emit Called(msg.sender, msg.value, data); + lastCaller = msg.sender; + callCount++; + return data; + } + + function setShouldRevert(bool _shouldRevert) external { + shouldRevert = _shouldRevert; + } + + function reset() external { + value = 0; + lastCaller = address(0); + callCount = 0; + shouldRevert = false; + } + + receive() external payable { + callCount++; + } +} + diff --git a/test/modular/mocks/MockValidator.sol b/test/modular/mocks/MockValidator.sol new file mode 100644 index 0000000..b15aa08 --- /dev/null +++ b/test/modular/mocks/MockValidator.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IValidator, MODULE_TYPE_VALIDATOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; + +// ERC-4337 validation return values +uint256 constant VALIDATION_SUCCESS = 0; +uint256 constant VALIDATION_FAILED = 1; + +/** + * @title MockValidator + * @notice Mock validator module for testing + */ +contract MockValidator is IValidator { + mapping(address => bool) private _initialized; + mapping(address => bool) public shouldValidate; + + bytes4 internal constant ERC1271_MAGIC_VALUE = 0x1626ba7e; + + function onInstall(bytes calldata data) external override { + if (_initialized[msg.sender]) revert AlreadyInitialized(msg.sender); + _initialized[msg.sender] = true; + + // Decode initial validation setting + if (data.length > 0) { + shouldValidate[msg.sender] = abi.decode(data, (bool)); + } else { + shouldValidate[msg.sender] = true; + } + } + + function onUninstall(bytes calldata) external override { + if (!_initialized[msg.sender]) revert NotInitialized(msg.sender); + _initialized[msg.sender] = false; + shouldValidate[msg.sender] = false; + } + + function isModuleType(uint256 moduleTypeId) external pure override returns (bool) { + return moduleTypeId == MODULE_TYPE_VALIDATOR; + } + + function isInitialized(address smartAccount) external view override returns (bool) { + return _initialized[smartAccount]; + } + + function validateUserOp(PackedUserOperation calldata, bytes32) external view override returns (uint256) { + return shouldValidate[msg.sender] ? VALIDATION_SUCCESS : VALIDATION_FAILED; + } + + function isValidSignatureWithSender(address, bytes32, bytes calldata) external view override returns (bytes4) { + return shouldValidate[msg.sender] ? ERC1271_MAGIC_VALUE : bytes4(0xffffffff); + } + + // Test helper to change validation behavior + function setValidation(address account, bool validate) external { + shouldValidate[account] = validate; + } +} + From df61cedb437ee9897dc08fcd69aa14e54d60a4e6 Mon Sep 17 00:00:00 2001 From: Ha DANG Date: Tue, 2 Dec 2025 16:34:51 +0700 Subject: [PATCH 02/22] feat: implement ERC-7579 core modules (Phase 3) (#160) --- .../modules/P256MFAValidatorModule.sol | 416 +++++++++++++++ src/modular/modules/SocialRecoveryModule.sol | 483 ++++++++++++++++++ test/modular/P256MFAValidatorModule.t.sol | 172 +++++++ test/modular/SocialRecoveryModule.t.sol | 243 +++++++++ 4 files changed, 1314 insertions(+) create mode 100644 src/modular/modules/P256MFAValidatorModule.sol create mode 100644 src/modular/modules/SocialRecoveryModule.sol create mode 100644 test/modular/P256MFAValidatorModule.t.sol create mode 100644 test/modular/SocialRecoveryModule.t.sol diff --git a/src/modular/modules/P256MFAValidatorModule.sol b/src/modular/modules/P256MFAValidatorModule.sol new file mode 100644 index 0000000..01a4201 --- /dev/null +++ b/src/modular/modules/P256MFAValidatorModule.sol @@ -0,0 +1,416 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; +import { + IModule, + IValidator, + MODULE_TYPE_VALIDATOR, + VALIDATION_SUCCESS, + VALIDATION_FAILED +} from "@erc7579/interfaces/IERC7579Module.sol"; +import {WebAuthn} from "solady/utils/WebAuthn.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; + +/** + * @title P256MFAValidatorModule + * @notice ERC-7579 Validator Module with Owner + Passkey MFA + * @dev Validates owner ECDSA signature with optional passkey as additional factor (MFA) + * - mfaEnabled = false: Owner ECDSA signature only + * - mfaEnabled = true: Owner ECDSA signature + Passkey signature (MFA) + */ +contract P256MFAValidatorModule is IValidator { + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @notice ERC-1271 magic value + bytes4 internal constant ERC1271_MAGIC_VALUE = 0x1626ba7e; + + /*////////////////////////////////////////////////////////////// + ERC-7201 STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @custom:storage-location erc7201:ethaura.storage.P256MFAValidatorModule + struct P256MFAValidatorStorage { + // Per-account passkey storage + mapping(address account => mapping(bytes32 passkeyId => PasskeyInfo)) passkeys; + mapping(address account => bytes32[]) passkeyIds; + mapping(address account => uint256) passkeyCount; + // MFA settings + mapping(address account => address) owners; + mapping(address account => bool) mfaEnabled; + } + + struct PasskeyInfo { + bytes32 qx; + bytes32 qy; + uint256 addedAt; + bool active; + bytes32 deviceId; + } + + // keccak256(abi.encode(uint256(keccak256("ethaura.storage.P256MFAValidatorModule")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant STORAGE_LOCATION = + 0x8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d600; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event OwnerSet(address indexed account, address indexed owner); + event MFAEnabled(address indexed account); + event MFADisabled(address indexed account); + event PasskeyAdded(address indexed account, bytes32 indexed passkeyId, bytes32 deviceId); + event PasskeyRemoved(address indexed account, bytes32 indexed passkeyId); + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error OnlyAccount(); + error OnlyAccountOrSelf(); + error InvalidOwner(); + error InvalidPasskey(); + error PasskeyAlreadyExists(); + error PasskeyDoesNotExist(); + error PasskeyNotActive(); + error CannotRemoveLastPasskey(); + error MFARequiresPasskey(); + + /*////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////*/ + + modifier onlyAccount() { + // msg.sender is the account calling this module + _; + } + + /*////////////////////////////////////////////////////////////// + STORAGE ACCESS + //////////////////////////////////////////////////////////////*/ + + function _getStorage() internal pure returns (P256MFAValidatorStorage storage $) { + bytes32 location = STORAGE_LOCATION; + assembly { + $.slot := location + } + } + + /*////////////////////////////////////////////////////////////// + IModule INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IModule + function onInstall(bytes calldata data) external override { + P256MFAValidatorStorage storage $ = _getStorage(); + + // Decode: owner, qx, qy, deviceId, enableMFA + (address owner, bytes32 qx, bytes32 qy, bytes32 deviceId, bool shouldEnableMFA) = + abi.decode(data, (address, bytes32, bytes32, bytes32, bool)); + + if (owner == address(0)) revert InvalidOwner(); + + // Set owner + $.owners[msg.sender] = owner; + emit OwnerSet(msg.sender, owner); + + // Add passkey if provided + if (qx != bytes32(0) && qy != bytes32(0)) { + _addPasskeyInternal(msg.sender, qx, qy, deviceId); + } + + // Enable MFA if requested (requires passkey) + if (shouldEnableMFA) { + if ($.passkeyCount[msg.sender] == 0) revert MFARequiresPasskey(); + $.mfaEnabled[msg.sender] = true; + emit MFAEnabled(msg.sender); + } + } + + /// @inheritdoc IModule + function onUninstall(bytes calldata) external override { + P256MFAValidatorStorage storage $ = _getStorage(); + + // Clear owner + delete $.owners[msg.sender]; + + // Clear MFA + delete $.mfaEnabled[msg.sender]; + + // Clear all passkeys + bytes32[] storage ids = $.passkeyIds[msg.sender]; + for (uint256 i = 0; i < ids.length; i++) { + delete $.passkeys[msg.sender][ids[i]]; + } + delete $.passkeyIds[msg.sender]; + delete $.passkeyCount[msg.sender]; + } + + /// @inheritdoc IModule + function isModuleType(uint256 moduleTypeId) external pure override returns (bool) { + return moduleTypeId == MODULE_TYPE_VALIDATOR; + } + + /// @inheritdoc IModule + function isInitialized(address account) external view override returns (bool) { + return _getStorage().owners[account] != address(0); + } + + /*////////////////////////////////////////////////////////////// + IValidator INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IValidator + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) + external + view + override + returns (uint256) + { + P256MFAValidatorStorage storage $ = _getStorage(); + address account = msg.sender; + address owner = $.owners[account]; + bool mfaEnabled = $.mfaEnabled[account]; + + bytes calldata sig = userOp.signature; + + // Owner-only mode: no MFA required + if (!mfaEnabled) { + if (sig.length != 65) return VALIDATION_FAILED; + return _verifyOwnerSignature(userOpHash, sig, owner) ? VALIDATION_SUCCESS : VALIDATION_FAILED; + } + + // MFA mode: requires WebAuthn signature + passkeyId + owner signature + // Minimum: authDataLen(2) + authData(37) + clientData(20) + challengeIdx(2) + + // typeIdx(2) + r(32) + s(32) + passkeyId(32) + ownerSig(65) = 224 bytes + if (sig.length < 224) return VALIDATION_FAILED; + + // Extract owner signature (last 65 bytes) + bytes calldata ownerSig = sig[sig.length - 65:]; + + // Extract passkeyId (32 bytes before owner signature) + bytes32 passkeyId = bytes32(sig[sig.length - 97:sig.length - 65]); + + // Extract WebAuthn compact signature (everything except passkeyId and owner signature) + bytes calldata webAuthnSig = sig[:sig.length - 97]; + + // Verify passkey signature + PasskeyInfo storage passkeyInfo = $.passkeys[account][passkeyId]; + if (!passkeyInfo.active || passkeyInfo.qx == bytes32(0)) return VALIDATION_FAILED; + + // Decode and verify WebAuthn signature + WebAuthn.WebAuthnAuth memory auth = WebAuthn.tryDecodeAuthCompactCalldata(webAuthnSig); + bytes memory challenge = abi.encodePacked(userOpHash); + + bool webAuthnValid = WebAuthn.verify( + challenge, + true, // requireUserVerification + auth, + passkeyInfo.qx, + passkeyInfo.qy + ); + + if (!webAuthnValid) return VALIDATION_FAILED; + + // Verify owner signature + if (!_verifyOwnerSignature(userOpHash, ownerSig, owner)) return VALIDATION_FAILED; + + return VALIDATION_SUCCESS; + } + + /// @inheritdoc IValidator + function isValidSignatureWithSender(address, bytes32 hash, bytes calldata signature) + external + view + override + returns (bytes4) + { + P256MFAValidatorStorage storage $ = _getStorage(); + address account = msg.sender; + address owner = $.owners[account]; + bool mfaEnabled = $.mfaEnabled[account]; + + // Owner-only mode + if (!mfaEnabled) { + if (signature.length != 65) return bytes4(0xffffffff); + return _verifyOwnerSignature(hash, signature, owner) ? ERC1271_MAGIC_VALUE : bytes4(0xffffffff); + } + + // MFA mode: same format as validateUserOp + if (signature.length < 224) return bytes4(0xffffffff); + + bytes calldata ownerSig = signature[signature.length - 65:]; + bytes32 passkeyId = bytes32(signature[signature.length - 97:signature.length - 65]); + bytes calldata webAuthnSig = signature[:signature.length - 97]; + + PasskeyInfo storage passkeyInfo = $.passkeys[account][passkeyId]; + if (!passkeyInfo.active || passkeyInfo.qx == bytes32(0)) return bytes4(0xffffffff); + + WebAuthn.WebAuthnAuth memory auth = WebAuthn.tryDecodeAuthCompactCalldata(webAuthnSig); + bytes memory challenge = abi.encodePacked(hash); + + bool webAuthnValid = WebAuthn.verify( + challenge, + true, + auth, + passkeyInfo.qx, + passkeyInfo.qy + ); + + if (!webAuthnValid) return bytes4(0xffffffff); + if (!_verifyOwnerSignature(hash, ownerSig, owner)) return bytes4(0xffffffff); + + return ERC1271_MAGIC_VALUE; + } + + /*////////////////////////////////////////////////////////////// + SIGNATURE VERIFICATION + //////////////////////////////////////////////////////////////*/ + + function _verifyOwnerSignature(bytes32 hash, bytes calldata signature, address owner) + internal + view + returns (bool) + { + address recovered = ECDSA.recover(hash, signature); + return recovered == owner; + } + + /*////////////////////////////////////////////////////////////// + PASSKEY MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Add a new passkey to the account + * @param qx Public key x-coordinate + * @param qy Public key y-coordinate + * @param deviceId Device identifier + */ + function addPasskey(bytes32 qx, bytes32 qy, bytes32 deviceId) external { + _addPasskeyInternal(msg.sender, qx, qy, deviceId); + } + + /** + * @notice Remove a passkey from the account + * @param passkeyId The passkey ID to remove + */ + function removePasskey(bytes32 passkeyId) external { + P256MFAValidatorStorage storage $ = _getStorage(); + address account = msg.sender; + + PasskeyInfo storage info = $.passkeys[account][passkeyId]; + if (!info.active) revert PasskeyDoesNotExist(); + + // Cannot remove last passkey if MFA is enabled + if ($.mfaEnabled[account] && $.passkeyCount[account] == 1) { + revert CannotRemoveLastPasskey(); + } + + // Deactivate passkey + info.active = false; + $.passkeyCount[account]--; + + // Remove from passkeyIds array + bytes32[] storage ids = $.passkeyIds[account]; + for (uint256 i = 0; i < ids.length; i++) { + if (ids[i] == passkeyId) { + ids[i] = ids[ids.length - 1]; + ids.pop(); + break; + } + } + + emit PasskeyRemoved(account, passkeyId); + } + + function _addPasskeyInternal(address account, bytes32 qx, bytes32 qy, bytes32 deviceId) internal { + if (qx == bytes32(0) || qy == bytes32(0)) revert InvalidPasskey(); + + P256MFAValidatorStorage storage $ = _getStorage(); + bytes32 passkeyId = keccak256(abi.encodePacked(qx, qy)); + + if ($.passkeys[account][passkeyId].active) revert PasskeyAlreadyExists(); + + $.passkeys[account][passkeyId] = PasskeyInfo({ + qx: qx, + qy: qy, + addedAt: block.timestamp, + active: true, + deviceId: deviceId + }); + + $.passkeyIds[account].push(passkeyId); + $.passkeyCount[account]++; + + emit PasskeyAdded(account, passkeyId, deviceId); + } + + /*////////////////////////////////////////////////////////////// + MFA MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Enable MFA for the account + */ + function enableMFA() external { + P256MFAValidatorStorage storage $ = _getStorage(); + if ($.passkeyCount[msg.sender] == 0) revert MFARequiresPasskey(); + $.mfaEnabled[msg.sender] = true; + emit MFAEnabled(msg.sender); + } + + /** + * @notice Disable MFA for the account + */ + function disableMFA() external { + P256MFAValidatorStorage storage $ = _getStorage(); + $.mfaEnabled[msg.sender] = false; + emit MFADisabled(msg.sender); + } + + /*////////////////////////////////////////////////////////////// + OWNER MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Set a new owner for the account + * @param newOwner The new owner address + */ + function setOwner(address newOwner) external { + if (newOwner == address(0)) revert InvalidOwner(); + P256MFAValidatorStorage storage $ = _getStorage(); + $.owners[msg.sender] = newOwner; + emit OwnerSet(msg.sender, newOwner); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function getOwner(address account) external view returns (address) { + return _getStorage().owners[account]; + } + + function isMFAEnabled(address account) external view returns (bool) { + return _getStorage().mfaEnabled[account]; + } + + function getPasskeyCount(address account) external view returns (uint256) { + return _getStorage().passkeyCount[account]; + } + + function getPasskey(address account, bytes32 passkeyId) external view returns (PasskeyInfo memory) { + return _getStorage().passkeys[account][passkeyId]; + } + + function isPasskeyActive(address account, bytes32 passkeyId) external view returns (bool) { + return _getStorage().passkeys[account][passkeyId].active; + } + + function getPasskeyIds(address account) external view returns (bytes32[] memory) { + return _getStorage().passkeyIds[account]; + } +} + diff --git a/src/modular/modules/SocialRecoveryModule.sol b/src/modular/modules/SocialRecoveryModule.sol new file mode 100644 index 0000000..523e32b --- /dev/null +++ b/src/modular/modules/SocialRecoveryModule.sol @@ -0,0 +1,483 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { + IModule, + IExecutor, + MODULE_TYPE_EXECUTOR +} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IERC7579Account} from "@erc7579/interfaces/IERC7579Account.sol"; +import { + ModeLib, + ModeCode, + CALLTYPE_SINGLE, + EXECTYPE_DEFAULT, + MODE_DEFAULT, + ModePayload +} from "@erc7579/lib/ModeLib.sol"; +import {P256MFAValidatorModule} from "./P256MFAValidatorModule.sol"; + +/** + * @title SocialRecoveryModule + * @notice ERC-7579 Executor Module for guardian-based social recovery + * @dev Implements threshold-based guardian recovery with timelock + * - Guardians can initiate and approve recovery requests + * - Threshold of guardians must approve before recovery can execute + * - Timelock period after threshold is met (e.g., 24 hours) + * - Account owner can cancel recovery during timelock + */ +contract SocialRecoveryModule is IExecutor { + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Default timelock period (24 hours) + uint256 public constant DEFAULT_TIMELOCK = 24 hours; + + /*////////////////////////////////////////////////////////////// + ERC-7201 STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @custom:storage-location erc7201:ethaura.storage.SocialRecoveryModule + struct SocialRecoveryStorage { + // Guardian management + mapping(address account => mapping(address guardian => bool)) isGuardian; + mapping(address account => address[]) guardianList; + // Recovery configuration + mapping(address account => RecoveryConfig) config; + // Recovery requests + mapping(address account => uint256) recoveryNonce; + mapping(address account => mapping(uint256 nonce => RecoveryRequest)) requests; + // Approvals (nested mapping in struct not allowed, so separate) + mapping(address account => mapping(uint256 nonce => mapping(address => bool))) approvals; + } + + struct RecoveryConfig { + uint256 threshold; // e.g., 2 for "2 of 3 guardians" + uint256 timelockPeriod; // e.g., 24 hours + } + + struct RecoveryRequest { + bytes32 newPasskeyQx; + bytes32 newPasskeyQy; + address newOwner; + uint256 approvalCount; + uint256 initiatedAt; + uint256 executeAfter; // Set when threshold met + bool thresholdMet; + bool executed; + bool cancelled; + } + + // keccak256(abi.encode(uint256(keccak256("ethaura.storage.SocialRecoveryModule")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant STORAGE_LOCATION = + 0x9a1e5f7d8c2b3a4e6f0d1c2b3a4e5f6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a00; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event GuardianAdded(address indexed account, address indexed guardian); + event GuardianRemoved(address indexed account, address indexed guardian); + event RecoveryConfigUpdated(address indexed account, uint256 threshold, uint256 timelockPeriod); + event RecoveryInitiated( + address indexed account, + uint256 indexed nonce, + address indexed initiator, + bytes32 newPasskeyQx, + bytes32 newPasskeyQy, + address newOwner + ); + event RecoveryApproved(address indexed account, uint256 indexed nonce, address indexed guardian); + event RecoveryThresholdMet(address indexed account, uint256 indexed nonce, uint256 executeAfter); + event RecoveryExecuted(address indexed account, uint256 indexed nonce); + event RecoveryCancelled(address indexed account, uint256 indexed nonce); + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error NotGuardian(); + error AlreadyGuardian(); + error GuardianNotFound(); + error InvalidThreshold(); + error RecoveryNotFound(); + error RecoveryAlreadyExecuted(); + error RecoveryAlreadyCancelled(); + error RecoveryAlreadyApproved(); + error RecoveryNotReady(); + error ThresholdNotMet(); + error TimelockNotPassed(); + error InvalidRecoveryParams(); + error OnlyAccountOwner(); + + /*////////////////////////////////////////////////////////////// + STORAGE ACCESS + //////////////////////////////////////////////////////////////*/ + + function _getStorage() internal pure returns (SocialRecoveryStorage storage $) { + bytes32 location = STORAGE_LOCATION; + assembly { + $.slot := location + } + } + + /*////////////////////////////////////////////////////////////// + IModule INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IModule + function onInstall(bytes calldata data) external override { + SocialRecoveryStorage storage $ = _getStorage(); + + // Decode: threshold, timelockPeriod, guardians[] + (uint256 threshold, uint256 timelockPeriod, address[] memory guardians) = + abi.decode(data, (uint256, uint256, address[])); + + // Set config + $.config[msg.sender] = RecoveryConfig({ + threshold: threshold > 0 ? threshold : 1, + timelockPeriod: timelockPeriod > 0 ? timelockPeriod : DEFAULT_TIMELOCK + }); + + // Add guardians + for (uint256 i = 0; i < guardians.length; i++) { + if (!$.isGuardian[msg.sender][guardians[i]]) { + $.isGuardian[msg.sender][guardians[i]] = true; + $.guardianList[msg.sender].push(guardians[i]); + emit GuardianAdded(msg.sender, guardians[i]); + } + } + + emit RecoveryConfigUpdated(msg.sender, $.config[msg.sender].threshold, timelockPeriod); + } + + /// @inheritdoc IModule + function onUninstall(bytes calldata) external override { + SocialRecoveryStorage storage $ = _getStorage(); + + // Clear guardians + address[] storage guardians = $.guardianList[msg.sender]; + for (uint256 i = 0; i < guardians.length; i++) { + $.isGuardian[msg.sender][guardians[i]] = false; + } + delete $.guardianList[msg.sender]; + delete $.config[msg.sender]; + } + + /// @inheritdoc IModule + function isModuleType(uint256 moduleTypeId) external pure override returns (bool) { + return moduleTypeId == MODULE_TYPE_EXECUTOR; + } + + /// @inheritdoc IModule + function isInitialized(address account) external view override returns (bool) { + return _getStorage().config[account].threshold > 0; + } + + /*////////////////////////////////////////////////////////////// + GUARDIAN MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Add a guardian to the account + * @param guardian Address of the guardian to add + */ + function addGuardian(address guardian) external { + SocialRecoveryStorage storage $ = _getStorage(); + if ($.isGuardian[msg.sender][guardian]) revert AlreadyGuardian(); + + $.isGuardian[msg.sender][guardian] = true; + $.guardianList[msg.sender].push(guardian); + + emit GuardianAdded(msg.sender, guardian); + } + + /** + * @notice Remove a guardian from the account + * @param guardian Address of the guardian to remove + */ + function removeGuardian(address guardian) external { + SocialRecoveryStorage storage $ = _getStorage(); + if (!$.isGuardian[msg.sender][guardian]) revert GuardianNotFound(); + + $.isGuardian[msg.sender][guardian] = false; + + // Remove from list + address[] storage guardians = $.guardianList[msg.sender]; + for (uint256 i = 0; i < guardians.length; i++) { + if (guardians[i] == guardian) { + guardians[i] = guardians[guardians.length - 1]; + guardians.pop(); + break; + } + } + + emit GuardianRemoved(msg.sender, guardian); + } + + /** + * @notice Update recovery configuration + * @param threshold Number of guardians required for recovery + * @param timelockPeriod Time to wait after threshold is met + */ + function setRecoveryConfig(uint256 threshold, uint256 timelockPeriod) external { + SocialRecoveryStorage storage $ = _getStorage(); + + if (threshold == 0) revert InvalidThreshold(); + if (threshold > $.guardianList[msg.sender].length) revert InvalidThreshold(); + + $.config[msg.sender] = RecoveryConfig({ + threshold: threshold, + timelockPeriod: timelockPeriod + }); + + emit RecoveryConfigUpdated(msg.sender, threshold, timelockPeriod); + } + + /*////////////////////////////////////////////////////////////// + RECOVERY FLOW + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initiate a recovery request + * @param account The account to recover + * @param newQx New passkey X coordinate + * @param newQy New passkey Y coordinate + * @param newOwner New owner address + */ + function initiateRecovery( + address account, + bytes32 newQx, + bytes32 newQy, + address newOwner + ) external { + SocialRecoveryStorage storage $ = _getStorage(); + + if (!$.isGuardian[account][msg.sender]) revert NotGuardian(); + if (newQx == bytes32(0) && newQy == bytes32(0) && newOwner == address(0)) { + revert InvalidRecoveryParams(); + } + + uint256 nonce = $.recoveryNonce[account]++; + + $.requests[account][nonce] = RecoveryRequest({ + newPasskeyQx: newQx, + newPasskeyQy: newQy, + newOwner: newOwner, + approvalCount: 1, + initiatedAt: block.timestamp, + executeAfter: 0, + thresholdMet: false, + executed: false, + cancelled: false + }); + + $.approvals[account][nonce][msg.sender] = true; + + emit RecoveryInitiated(account, nonce, msg.sender, newQx, newQy, newOwner); + emit RecoveryApproved(account, nonce, msg.sender); + + // Check if threshold already met (threshold = 1) + _checkThreshold(account, nonce); + } + + /** + * @notice Approve an existing recovery request + * @param account The account being recovered + * @param nonce The recovery request nonce + */ + function approveRecovery(address account, uint256 nonce) external { + SocialRecoveryStorage storage $ = _getStorage(); + + if (!$.isGuardian[account][msg.sender]) revert NotGuardian(); + + RecoveryRequest storage request = $.requests[account][nonce]; + if (request.initiatedAt == 0) revert RecoveryNotFound(); + if (request.executed) revert RecoveryAlreadyExecuted(); + if (request.cancelled) revert RecoveryAlreadyCancelled(); + if ($.approvals[account][nonce][msg.sender]) revert RecoveryAlreadyApproved(); + + $.approvals[account][nonce][msg.sender] = true; + request.approvalCount++; + + emit RecoveryApproved(account, nonce, msg.sender); + + _checkThreshold(account, nonce); + } + + function _checkThreshold(address account, uint256 nonce) internal { + SocialRecoveryStorage storage $ = _getStorage(); + RecoveryRequest storage request = $.requests[account][nonce]; + RecoveryConfig storage config = $.config[account]; + + if (!request.thresholdMet && request.approvalCount >= config.threshold) { + request.thresholdMet = true; + request.executeAfter = block.timestamp + config.timelockPeriod; + emit RecoveryThresholdMet(account, nonce, request.executeAfter); + } + } + + /** + * @notice Execute a recovery after timelock has passed + * @param account The account being recovered + * @param nonce The recovery request nonce + * @param validatorModule The P256MFAValidatorModule to update + */ + function executeRecovery( + address account, + uint256 nonce, + address validatorModule + ) external { + SocialRecoveryStorage storage $ = _getStorage(); + RecoveryRequest storage request = $.requests[account][nonce]; + + if (request.initiatedAt == 0) revert RecoveryNotFound(); + if (request.executed) revert RecoveryAlreadyExecuted(); + if (request.cancelled) revert RecoveryAlreadyCancelled(); + if (!request.thresholdMet) revert ThresholdNotMet(); + if (block.timestamp < request.executeAfter) revert TimelockNotPassed(); + + request.executed = true; + + // Build calldata to update the validator module + bytes memory updateCalldata; + + // Update passkey if provided + if (request.newPasskeyQx != bytes32(0) || request.newPasskeyQy != bytes32(0)) { + // Call addPasskey on the validator module + updateCalldata = abi.encodeWithSelector( + P256MFAValidatorModule.addPasskey.selector, + request.newPasskeyQx, + request.newPasskeyQy, + "recovery-passkey" + ); + + // Execute via the account + IERC7579Account(account).executeFromExecutor( + _encodeExecutionMode(), + abi.encodePacked(validatorModule, uint256(0), updateCalldata) + ); + } + + // Update owner if provided + if (request.newOwner != address(0)) { + updateCalldata = abi.encodeWithSelector( + P256MFAValidatorModule.setOwner.selector, + request.newOwner + ); + + IERC7579Account(account).executeFromExecutor( + _encodeExecutionMode(), + abi.encodePacked(validatorModule, uint256(0), updateCalldata) + ); + } + + emit RecoveryExecuted(account, nonce); + } + + /** + * @notice Cancel a recovery request (only account can call) + * @param nonce The recovery request nonce to cancel + */ + function cancelRecovery(uint256 nonce) external { + SocialRecoveryStorage storage $ = _getStorage(); + RecoveryRequest storage request = $.requests[msg.sender][nonce]; + + if (request.initiatedAt == 0) revert RecoveryNotFound(); + if (request.executed) revert RecoveryAlreadyExecuted(); + if (request.cancelled) revert RecoveryAlreadyCancelled(); + + request.cancelled = true; + + emit RecoveryCancelled(msg.sender, nonce); + } + + /** + * @notice Encode execution mode for single call + * @dev ModeCode: 0x00 for single call, no delegatecall + */ + function _encodeExecutionMode() internal pure returns (ModeCode) { + return ModeLib.encode( + CALLTYPE_SINGLE, + EXECTYPE_DEFAULT, + MODE_DEFAULT, + ModePayload.wrap(bytes22(0)) + ); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Check if an address is a guardian for an account + */ + function isGuardian(address account, address guardian) external view returns (bool) { + return _getStorage().isGuardian[account][guardian]; + } + + /** + * @notice Get all guardians for an account + */ + function getGuardians(address account) external view returns (address[] memory) { + return _getStorage().guardianList[account]; + } + + /** + * @notice Get guardian count for an account + */ + function getGuardianCount(address account) external view returns (uint256) { + return _getStorage().guardianList[account].length; + } + + /** + * @notice Get recovery configuration for an account + */ + function getRecoveryConfig(address account) external view returns (uint256 threshold, uint256 timelockPeriod) { + RecoveryConfig storage config = _getStorage().config[account]; + return (config.threshold, config.timelockPeriod); + } + + /** + * @notice Get current recovery nonce for an account + */ + function getRecoveryNonce(address account) external view returns (uint256) { + return _getStorage().recoveryNonce[account]; + } + + /** + * @notice Get recovery request details + */ + function getRecoveryRequest(address account, uint256 nonce) external view returns ( + bytes32 newPasskeyQx, + bytes32 newPasskeyQy, + address newOwner, + uint256 approvalCount, + uint256 initiatedAt, + uint256 executeAfter, + bool thresholdMet, + bool executed, + bool cancelled + ) { + RecoveryRequest storage request = _getStorage().requests[account][nonce]; + return ( + request.newPasskeyQx, + request.newPasskeyQy, + request.newOwner, + request.approvalCount, + request.initiatedAt, + request.executeAfter, + request.thresholdMet, + request.executed, + request.cancelled + ); + } + + /** + * @notice Check if a guardian has approved a recovery request + */ + function hasApproved(address account, uint256 nonce, address guardian) external view returns (bool) { + return _getStorage().approvals[account][nonce][guardian]; + } +} diff --git a/test/modular/P256MFAValidatorModule.t.sol b/test/modular/P256MFAValidatorModule.t.sol new file mode 100644 index 0000000..98696ec --- /dev/null +++ b/test/modular/P256MFAValidatorModule.t.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; +import {P256MFAValidatorModule} from "../../src/modular/modules/P256MFAValidatorModule.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; +import {MODULE_TYPE_VALIDATOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; + +contract P256MFAValidatorModuleTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + P256MFAValidatorModule public validator; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + address owner = address(0x1234); + uint256 ownerPrivateKey = 0x1234; + + // Test passkey coordinates (from existing tests) + bytes32 constant QX = 0x65a2fa44daad46eab0278703edb6c4dcf5e30b8a9aec09fdc71a56f52aa392e4; + bytes32 constant QY = 0x4a7a9e4604aa36898209997288e902ac544a555e4b5e0a9efef2b59233f3f437; + + function setUp() public { + // Deploy canonical ERC1967Factory if not already deployed + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + // Deploy factory and validator module + factory = new AuraAccountFactory(); + validator = new P256MFAValidatorModule(); + + // Create account with P256MFAValidatorModule + // Init data: owner, qx, qy, deviceId, enableMFA + bytes memory initData = abi.encode(owner, QX, QY, bytes32("Test Device"), true); + + address accountAddr = factory.createAccount( + owner, + address(validator), + initData, + address(0), // no hook + "", + 0 // salt + ); + account = AuraAccount(payable(accountAddr)); + + // Fund account + vm.deal(address(account), 10 ether); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_Initialize() public view { + assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); + assertTrue(validator.isInitialized(address(account))); + } + + function test_OwnerIsSet() public view { + assertEq(validator.getOwner(address(account)), owner); + } + + function test_MFAIsEnabled() public view { + assertTrue(validator.isMFAEnabled(address(account))); + } + + function test_PasskeyIsAdded() public view { + assertEq(validator.getPasskeyCount(address(account)), 1); + } + + /*////////////////////////////////////////////////////////////// + PASSKEY MANAGEMENT TESTS + //////////////////////////////////////////////////////////////*/ + + function test_AddPasskey() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + + vm.prank(address(account)); + validator.addPasskey(newQx, newQy, "New Passkey"); + + assertEq(validator.getPasskeyCount(address(account)), 2); + } + + function test_AddPasskeyFromDifferentAddressDoesNotAffectAccount() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + + // When owner calls addPasskey, it adds to owner's storage, not account's + vm.prank(owner); + validator.addPasskey(newQx, newQy, "Test"); + + // Account's passkey count should still be 1 (unchanged) + assertEq(validator.getPasskeyCount(address(account)), 1); + // Owner's passkey count should be 1 + assertEq(validator.getPasskeyCount(owner), 1); + } + + function test_RemovePasskey() public { + // First add a second passkey + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + + vm.startPrank(address(account)); + validator.addPasskey(newQx, newQy, "New Passkey"); + assertEq(validator.getPasskeyCount(address(account)), 2); + + // Get the passkey ID + bytes32 passkeyId = keccak256(abi.encodePacked(newQx, newQy)); + + // Remove the passkey + validator.removePasskey(passkeyId); + assertEq(validator.getPasskeyCount(address(account)), 1); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + MFA MANAGEMENT TESTS + //////////////////////////////////////////////////////////////*/ + + function test_DisableMFA() public { + assertTrue(validator.isMFAEnabled(address(account))); + + vm.prank(address(account)); + validator.disableMFA(); + + assertFalse(validator.isMFAEnabled(address(account))); + } + + function test_EnableMFA() public { + // First disable + vm.prank(address(account)); + validator.disableMFA(); + assertFalse(validator.isMFAEnabled(address(account))); + + // Then enable + vm.prank(address(account)); + validator.enableMFA(); + assertTrue(validator.isMFAEnabled(address(account))); + } + + /*////////////////////////////////////////////////////////////// + OWNER MANAGEMENT TESTS + //////////////////////////////////////////////////////////////*/ + + function test_SetOwner() public { + address newOwner = address(0x5678); + + vm.prank(address(account)); + validator.setOwner(newOwner); + + assertEq(validator.getOwner(address(account)), newOwner); + } + + function test_SetOwnerFromDifferentAddressDoesNotAffectAccount() public { + address newOwner = address(0x5678); + + // When owner calls setOwner, it sets owner for owner's storage, not account's + vm.prank(owner); + validator.setOwner(newOwner); + + // Account's owner should still be the original owner + assertEq(validator.getOwner(address(account)), owner); + // Owner's owner should be newOwner + assertEq(validator.getOwner(owner), newOwner); + } +} + diff --git a/test/modular/SocialRecoveryModule.t.sol b/test/modular/SocialRecoveryModule.t.sol new file mode 100644 index 0000000..4ef62b3 --- /dev/null +++ b/test/modular/SocialRecoveryModule.t.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; +import {P256MFAValidatorModule} from "../../src/modular/modules/P256MFAValidatorModule.sol"; +import {SocialRecoveryModule} from "../../src/modular/modules/SocialRecoveryModule.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; +import {MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; + +contract SocialRecoveryModuleTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + P256MFAValidatorModule public validator; + SocialRecoveryModule public recovery; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + address owner = address(0x1234); + address guardian1 = address(0x1111); + address guardian2 = address(0x2222); + address guardian3 = address(0x3333); + + // Test passkey coordinates + bytes32 constant QX = 0x65a2fa44daad46eab0278703edb6c4dcf5e30b8a9aec09fdc71a56f52aa392e4; + bytes32 constant QY = 0x4a7a9e4604aa36898209997288e902ac544a555e4b5e0a9efef2b59233f3f437; + + function setUp() public { + // Deploy canonical ERC1967Factory if not already deployed + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + // Deploy factory and modules + factory = new AuraAccountFactory(); + validator = new P256MFAValidatorModule(); + recovery = new SocialRecoveryModule(); + + // Create account with P256MFAValidatorModule + bytes memory validatorData = abi.encode(owner, QX, QY, bytes32("Test Device"), true); + + address accountAddr = factory.createAccount( + owner, + address(validator), + validatorData, + address(0), // no hook + "", + 0 // salt + ); + account = AuraAccount(payable(accountAddr)); + + // Install SocialRecoveryModule as executor + address[] memory guardians = new address[](2); + guardians[0] = guardian1; + guardians[1] = guardian2; + + bytes memory recoveryData = abi.encode( + uint256(2), // threshold: 2 of 2 + uint256(24 hours), // timelock + guardians + ); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), recoveryData); + + // Fund account + vm.deal(address(account), 10 ether); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_Initialize() public view { + assertTrue(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(recovery), "")); + assertTrue(recovery.isInitialized(address(account))); + } + + function test_GuardiansAreSet() public view { + assertTrue(recovery.isGuardian(address(account), guardian1)); + assertTrue(recovery.isGuardian(address(account), guardian2)); + assertFalse(recovery.isGuardian(address(account), guardian3)); + assertEq(recovery.getGuardianCount(address(account)), 2); + } + + function test_RecoveryConfigIsSet() public view { + (uint256 threshold, uint256 timelockPeriod) = recovery.getRecoveryConfig(address(account)); + assertEq(threshold, 2); + assertEq(timelockPeriod, 24 hours); + } + + /*////////////////////////////////////////////////////////////// + GUARDIAN MANAGEMENT TESTS + //////////////////////////////////////////////////////////////*/ + + function test_AddGuardian() public { + vm.prank(address(account)); + recovery.addGuardian(guardian3); + + assertTrue(recovery.isGuardian(address(account), guardian3)); + assertEq(recovery.getGuardianCount(address(account)), 3); + } + + function test_RemoveGuardian() public { + vm.prank(address(account)); + recovery.removeGuardian(guardian2); + + assertFalse(recovery.isGuardian(address(account), guardian2)); + assertEq(recovery.getGuardianCount(address(account)), 1); + } + + function test_SetRecoveryConfig() public { + vm.prank(address(account)); + recovery.setRecoveryConfig(1, 48 hours); + + (uint256 threshold, uint256 timelockPeriod) = recovery.getRecoveryConfig(address(account)); + assertEq(threshold, 1); + assertEq(timelockPeriod, 48 hours); + } + + /*////////////////////////////////////////////////////////////// + RECOVERY FLOW TESTS + //////////////////////////////////////////////////////////////*/ + + function test_InitiateRecovery() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + ( + bytes32 storedQx, + bytes32 storedQy, + address storedOwner, + uint256 approvalCount, + uint256 initiatedAt, + , + bool thresholdMet, + bool executed, + bool cancelled + ) = recovery.getRecoveryRequest(address(account), 0); + + assertEq(storedQx, newQx); + assertEq(storedQy, newQy); + assertEq(storedOwner, newOwner); + assertEq(approvalCount, 1); + assertGt(initiatedAt, 0); + assertFalse(thresholdMet); + assertFalse(executed); + assertFalse(cancelled); + } + + function test_ApproveRecoveryAndThresholdMet() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + // Guardian1 initiates + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + // Guardian2 approves + vm.prank(guardian2); + recovery.approveRecovery(address(account), 0); + + ( + ,,, + uint256 approvalCount, + , + uint256 executeAfter, + bool thresholdMet, + , + ) = recovery.getRecoveryRequest(address(account), 0); + + assertEq(approvalCount, 2); + assertTrue(thresholdMet); + assertGt(executeAfter, block.timestamp); + } + + function test_CancelRecovery() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + // Guardian1 initiates + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + // Account cancels + vm.prank(address(account)); + recovery.cancelRecovery(0); + + ( + ,,,,,,,, + bool cancelled + ) = recovery.getRecoveryRequest(address(account), 0); + + assertTrue(cancelled); + } + + function test_RevertNonGuardianInitiate() public { + vm.prank(address(0xdead)); + vm.expectRevert(SocialRecoveryModule.NotGuardian.selector); + recovery.initiateRecovery(address(account), bytes32(0), bytes32(0), address(0x9999)); + } + + function test_RevertExecuteBeforeTimelock() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + // Guardian1 initiates + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + // Guardian2 approves (threshold met) + vm.prank(guardian2); + recovery.approveRecovery(address(account), 0); + + // Try to execute before timelock + vm.expectRevert(SocialRecoveryModule.TimelockNotPassed.selector); + recovery.executeRecovery(address(account), 0, address(validator)); + } + + function test_RevertDoubleApproval() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + // Guardian1 initiates + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + // Guardian1 tries to approve again + vm.prank(guardian1); + vm.expectRevert(SocialRecoveryModule.RecoveryAlreadyApproved.selector); + recovery.approveRecovery(address(account), 0); + } +} + From f77475c90ff22458c57a8ce918862f1233eeec64 Mon Sep 17 00:00:00 2001 From: Ha DANG Date: Tue, 2 Dec 2025 16:47:40 +0700 Subject: [PATCH 03/22] feat: implement ERC-7579 remaining modules (Phase 4) (#161) --- .../modules/executors/HookManagerModule.sol | 186 ++++++ .../LargeTransactionExecutorModule.sol | 253 ++++++++ .../{ => executors}/SocialRecoveryModule.sol | 99 ++-- .../fallback/ERC1155ReceiverModule.sol | 68 +++ .../modules/fallback/ERC721ReceiverModule.sol | 42 ++ .../hooks/LargeTransactionGuardHook.sol | 116 ++++ src/modular/modules/hooks/MultiHook.sol | 218 +++++++ .../P256MFAValidatorModule.sol | 40 +- test/modular/ModuleTests.t.sol | 554 ++++++++++++++++++ test/modular/P256MFAValidatorModule.t.sol | 6 +- test/modular/SocialRecoveryModule.t.sol | 28 +- 11 files changed, 1500 insertions(+), 110 deletions(-) create mode 100644 src/modular/modules/executors/HookManagerModule.sol create mode 100644 src/modular/modules/executors/LargeTransactionExecutorModule.sol rename src/modular/modules/{ => executors}/SocialRecoveryModule.sol (88%) create mode 100644 src/modular/modules/fallback/ERC1155ReceiverModule.sol create mode 100644 src/modular/modules/fallback/ERC721ReceiverModule.sol create mode 100644 src/modular/modules/hooks/LargeTransactionGuardHook.sol create mode 100644 src/modular/modules/hooks/MultiHook.sol rename src/modular/modules/{ => validators}/P256MFAValidatorModule.sol (95%) create mode 100644 test/modular/ModuleTests.t.sol diff --git a/src/modular/modules/executors/HookManagerModule.sol b/src/modular/modules/executors/HookManagerModule.sol new file mode 100644 index 0000000..e316da2 --- /dev/null +++ b/src/modular/modules/executors/HookManagerModule.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IExecutor, MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IHook} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IERC7579Account} from "@erc7579/interfaces/IERC7579Account.sol"; +import { + ModeLib, + ModeCode, + CALLTYPE_SINGLE, + EXECTYPE_DEFAULT, + MODE_DEFAULT, + ModePayload +} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +/// @title HookManagerModule +/// @notice Provides clean API for users to install/uninstall hooks into MultiHook +/// @dev Executor module that manages hook installation via MultiHook +contract HookManagerModule is IExecutor { + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @dev ERC-7201 storage slot + bytes32 private constant HOOK_MANAGER_STORAGE_SLOT = + keccak256(abi.encode(uint256(keccak256("ethaura.storage.HookManagerModule")) - 1)) & ~bytes32(uint256(0xff)); + + struct EmergencyUninstall { + address hook; + uint256 proposedAt; + } + + struct HookManagerStorage { + mapping(address account => address) multiHook; // MultiHook address + mapping(address account => EmergencyUninstall) emergencyUninstalls; + } + + uint256 public constant EMERGENCY_TIMELOCK = 24 hours; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error NotAccount(); + error InvalidHook(); + error NoEmergencyUninstallProposed(); + error EmergencyTimelockNotPassed(); + error MultiHookNotSet(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event HookInstalled(address indexed account, address indexed hook); + event HookUninstalled(address indexed account, address indexed hook); + event EmergencyUninstallProposed(address indexed account, address indexed hook, uint256 executeAfter); + event EmergencyUninstallExecuted(address indexed account, address indexed hook); + + /*////////////////////////////////////////////////////////////// + STORAGE ACCESS + //////////////////////////////////////////////////////////////*/ + + function _getStorage() internal pure returns (HookManagerStorage storage $) { + bytes32 slot = HOOK_MANAGER_STORAGE_SLOT; + assembly { + $.slot := slot + } + } + + /*////////////////////////////////////////////////////////////// + MODULE INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @notice Initialize the hook manager with MultiHook address + /// @param data ABI-encoded MultiHook address + function onInstall(bytes calldata data) external override { + address multiHook = abi.decode(data, (address)); + _getStorage().multiHook[msg.sender] = multiHook; + } + + /// @notice Uninstall the hook manager + function onUninstall(bytes calldata) external override { + HookManagerStorage storage $ = _getStorage(); + delete $.multiHook[msg.sender]; + delete $.emergencyUninstalls[msg.sender]; + } + + function isModuleType(uint256 typeID) external pure override returns (bool) { + return typeID == MODULE_TYPE_EXECUTOR; + } + + function isInitialized(address account) external view override returns (bool) { + return _getStorage().multiHook[account] != address(0); + } + + /*////////////////////////////////////////////////////////////// + HOOK MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /// @notice Install a hook into the MultiHook chain + /// @param hook The hook contract address + /// @param initData Initialization data for the hook + function installHook(address hook, bytes calldata initData) external { + if (hook == address(0)) revert InvalidHook(); + + HookManagerStorage storage $ = _getStorage(); + address multiHook = $.multiHook[msg.sender]; + if (multiHook == address(0)) revert MultiHookNotSet(); + + // Initialize the hook (called from this module, so msg.sender in hook is this module) + IHook(hook).onInstall(initData); + + // Add to MultiHook via account's executeFromExecutor + // This ensures msg.sender in MultiHook is the account + _executeOnAccount(msg.sender, multiHook, 0, abi.encodeCall(IMultiHook.addHook, (hook))); + + emit HookInstalled(msg.sender, hook); + } + + /// @notice Uninstall a hook from the MultiHook chain + /// @param hook The hook contract address + function uninstallHook(address hook) external { + HookManagerStorage storage $ = _getStorage(); + address multiHook = $.multiHook[msg.sender]; + if (multiHook == address(0)) revert MultiHookNotSet(); + + // Remove from MultiHook via account's executeFromExecutor + _executeOnAccount(msg.sender, multiHook, 0, abi.encodeCall(IMultiHook.removeHook, (hook))); + + // Uninstall the hook + IHook(hook).onUninstall(""); + + emit HookUninstalled(msg.sender, hook); + } + + /// @dev Execute a call on the account via executeFromExecutor + function _executeOnAccount(address account, address target, uint256 value, bytes memory data) internal { + ModeCode mode = ModeLib.encode(CALLTYPE_SINGLE, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(bytes22(0))); + + bytes memory executionData = ExecutionLib.encodeSingle(target, value, data); + IERC7579Account(account).executeFromExecutor(mode, executionData); + } + + /*////////////////////////////////////////////////////////////// + EMERGENCY UNINSTALL + //////////////////////////////////////////////////////////////*/ + + /// @notice Propose emergency hook uninstall (if hook blocks all txs) + /// @dev Uses timelock to prevent abuse + function proposeEmergencyUninstall(address hook) external { + HookManagerStorage storage $ = _getStorage(); + $.emergencyUninstalls[msg.sender] = EmergencyUninstall({hook: hook, proposedAt: block.timestamp}); + + emit EmergencyUninstallProposed(msg.sender, hook, block.timestamp + EMERGENCY_TIMELOCK); + } + + /// @notice Execute emergency hook uninstall after timelock + function executeEmergencyUninstall() external { + HookManagerStorage storage $ = _getStorage(); + EmergencyUninstall storage emergency = $.emergencyUninstalls[msg.sender]; + + if (emergency.proposedAt == 0) revert NoEmergencyUninstallProposed(); + if (block.timestamp < emergency.proposedAt + EMERGENCY_TIMELOCK) { + revert EmergencyTimelockNotPassed(); + } + + address hook = emergency.hook; + address multiHook = $.multiHook[msg.sender]; + + // Remove from MultiHook via account's executeFromExecutor + if (multiHook != address(0)) { + _executeOnAccount(msg.sender, multiHook, 0, abi.encodeCall(IMultiHook.removeHook, (hook))); + } + + delete $.emergencyUninstalls[msg.sender]; + emit EmergencyUninstallExecuted(msg.sender, hook); + } +} + +/// @dev Interface for MultiHook +interface IMultiHook { + function addHook(address hook) external; + function removeHook(address hook) external; +} + diff --git a/src/modular/modules/executors/LargeTransactionExecutorModule.sol b/src/modular/modules/executors/LargeTransactionExecutorModule.sol new file mode 100644 index 0000000..418a0d9 --- /dev/null +++ b/src/modular/modules/executors/LargeTransactionExecutorModule.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IExecutor, MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IERC7579Account} from "@erc7579/interfaces/IERC7579Account.sol"; +import { + ModeLib, + ModeCode, + CALLTYPE_SINGLE, + EXECTYPE_DEFAULT, + MODE_DEFAULT, + ModePayload +} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +/// @title LargeTransactionExecutorModule +/// @notice Enforces timelock for high-value transactions +/// @dev Installed at account initialization, disabled by default (threshold = type(uint256).max) +contract LargeTransactionExecutorModule is IExecutor { + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @dev ERC-7201 storage slot + bytes32 private constant EXECUTOR_STORAGE_SLOT = keccak256( + abi.encode(uint256(keccak256("ethaura.storage.LargeTransactionExecutorModule")) - 1) + ) & ~bytes32(uint256(0xff)); + + struct PendingTx { + address target; + uint256 value; + bytes data; + uint256 proposedAt; + bool executed; + bool cancelled; + } + + struct ExecutorStorage { + mapping(address account => uint256) threshold; + mapping(address account => uint256) timelockPeriod; + mapping(address account => mapping(bytes32 txHash => PendingTx)) pendingTxs; + mapping(address account => bytes32[]) pendingTxHashes; + } + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error NotAccount(); + error TimelockNotPassed(); + error TransactionAlreadyExecuted(); + error TransactionWasCancelled(); + error TransactionNotFound(); + error InvalidThreshold(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event TransactionProposed( + address indexed account, bytes32 indexed txHash, address target, uint256 value, uint256 executeAfter + ); + event TransactionExecuted(address indexed account, bytes32 indexed txHash); + event TransactionCancelled(address indexed account, bytes32 indexed txHash); + event ThresholdSet(address indexed account, uint256 threshold); + event TimelockPeriodSet(address indexed account, uint256 period); + + /*////////////////////////////////////////////////////////////// + STORAGE ACCESS + //////////////////////////////////////////////////////////////*/ + + function _getStorage() internal pure returns (ExecutorStorage storage $) { + bytes32 slot = EXECUTOR_STORAGE_SLOT; + assembly { + $.slot := slot + } + } + + /*////////////////////////////////////////////////////////////// + MODULE INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @notice Initialize the executor + /// @param data ABI-encoded (threshold, timelockPeriod) + function onInstall(bytes calldata data) external override { + (uint256 threshold, uint256 timelockPeriod) = abi.decode(data, (uint256, uint256)); + ExecutorStorage storage $ = _getStorage(); + $.threshold[msg.sender] = threshold; + $.timelockPeriod[msg.sender] = timelockPeriod; + + emit ThresholdSet(msg.sender, threshold); + emit TimelockPeriodSet(msg.sender, timelockPeriod); + } + + /// @notice Uninstall the executor + function onUninstall(bytes calldata) external override { + ExecutorStorage storage $ = _getStorage(); + delete $.threshold[msg.sender]; + delete $.timelockPeriod[msg.sender]; + // Note: pending txs are left for reference, but won't be executable + } + + function isModuleType(uint256 typeID) external pure override returns (bool) { + return typeID == MODULE_TYPE_EXECUTOR; + } + + function isInitialized(address account) external view override returns (bool) { + return _getStorage().timelockPeriod[account] > 0; + } + + /*////////////////////////////////////////////////////////////// + EXECUTOR FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Execute a large transaction (propose on first call, execute after timelock) + function execute(address target, uint256 value, bytes calldata data) external { + _onlyAccount(); + + bytes32 txHash = keccak256(abi.encode(msg.sender, target, value, data)); + ExecutorStorage storage $ = _getStorage(); + PendingTx storage pending = $.pendingTxs[msg.sender][txHash]; + + if (pending.proposedAt == 0) { + // First call - propose transaction + pending.target = target; + pending.value = value; + pending.data = data; + pending.proposedAt = block.timestamp; + $.pendingTxHashes[msg.sender].push(txHash); + + uint256 executeAfter = block.timestamp + $.timelockPeriod[msg.sender]; + emit TransactionProposed(msg.sender, txHash, target, value, executeAfter); + return; // Don't execute yet + } + + // Second call - check and execute + if (pending.executed) revert TransactionAlreadyExecuted(); + if (pending.cancelled) revert TransactionWasCancelled(); + + uint256 executeAfter = pending.proposedAt + $.timelockPeriod[msg.sender]; + if (block.timestamp < executeAfter) revert TimelockNotPassed(); + + pending.executed = true; + emit TransactionExecuted(msg.sender, txHash); + + // Execute via account + _executeOnAccount(msg.sender, target, value, data); + } + + /// @notice Cancel a pending transaction + function cancel(bytes32 txHash) external { + _onlyAccount(); + + ExecutorStorage storage $ = _getStorage(); + PendingTx storage pending = $.pendingTxs[msg.sender][txHash]; + + if (pending.proposedAt == 0) revert TransactionNotFound(); + if (pending.executed) revert TransactionAlreadyExecuted(); + + pending.cancelled = true; + emit TransactionCancelled(msg.sender, txHash); + } + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /// @notice Set the threshold for large transactions + function setThreshold(uint256 threshold) external { + _onlyAccount(); + _getStorage().threshold[msg.sender] = threshold; + emit ThresholdSet(msg.sender, threshold); + } + + /// @notice Set the timelock period + function setTimelockPeriod(uint256 period) external { + _onlyAccount(); + _getStorage().timelockPeriod[msg.sender] = period; + emit TimelockPeriodSet(msg.sender, period); + } + + /// @notice Disable large transaction protection (set threshold to max) + function disable() external { + _onlyAccount(); + _getStorage().threshold[msg.sender] = type(uint256).max; + emit ThresholdSet(msg.sender, type(uint256).max); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Get threshold for an account + function getThreshold(address account) external view returns (uint256) { + uint256 threshold = _getStorage().threshold[account]; + return threshold == 0 ? type(uint256).max : threshold; + } + + /// @notice Get timelock period for an account + function getTimelockPeriod(address account) external view returns (uint256) { + return _getStorage().timelockPeriod[account]; + } + + /// @notice Get pending transaction details + function getPendingTx(address account, bytes32 txHash) + external + view + returns ( + address target, + uint256 value, + bytes memory data, + uint256 proposedAt, + uint256 executeAfter, + bool executed, + bool cancelled + ) + { + ExecutorStorage storage $ = _getStorage(); + PendingTx storage pending = $.pendingTxs[account][txHash]; + + return ( + pending.target, + pending.value, + pending.data, + pending.proposedAt, + pending.proposedAt + $.timelockPeriod[account], + pending.executed, + pending.cancelled + ); + } + + /// @notice Get all pending transaction hashes for an account + function getPendingTxHashes(address account) external view returns (bytes32[] memory) { + return _getStorage().pendingTxHashes[account]; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function _onlyAccount() internal view { + // In ERC-7579, the account calls the executor + // msg.sender is the account + } + + function _executeOnAccount(address account, address target, uint256 value, bytes memory data) internal { + ModeCode mode = ModeLib.encode(CALLTYPE_SINGLE, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(bytes22(0))); + + bytes memory executionData = ExecutionLib.encodeSingle(target, value, data); + IERC7579Account(account).executeFromExecutor(mode, executionData); + } +} + diff --git a/src/modular/modules/SocialRecoveryModule.sol b/src/modular/modules/executors/SocialRecoveryModule.sol similarity index 88% rename from src/modular/modules/SocialRecoveryModule.sol rename to src/modular/modules/executors/SocialRecoveryModule.sol index 523e32b..ac9f627 100644 --- a/src/modular/modules/SocialRecoveryModule.sol +++ b/src/modular/modules/executors/SocialRecoveryModule.sol @@ -1,11 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import { - IModule, - IExecutor, - MODULE_TYPE_EXECUTOR -} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IModule, IExecutor, MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; import {IERC7579Account} from "@erc7579/interfaces/IERC7579Account.sol"; import { ModeLib, @@ -15,7 +11,7 @@ import { MODE_DEFAULT, ModePayload } from "@erc7579/lib/ModeLib.sol"; -import {P256MFAValidatorModule} from "./P256MFAValidatorModule.sol"; +import {P256MFAValidatorModule} from "../validators/P256MFAValidatorModule.sol"; /** * @title SocialRecoveryModule @@ -53,8 +49,8 @@ contract SocialRecoveryModule is IExecutor { } struct RecoveryConfig { - uint256 threshold; // e.g., 2 for "2 of 3 guardians" - uint256 timelockPeriod; // e.g., 24 hours + uint256 threshold; // e.g., 2 for "2 of 3 guardians" + uint256 timelockPeriod; // e.g., 24 hours } struct RecoveryRequest { @@ -63,15 +59,14 @@ contract SocialRecoveryModule is IExecutor { address newOwner; uint256 approvalCount; uint256 initiatedAt; - uint256 executeAfter; // Set when threshold met + uint256 executeAfter; // Set when threshold met bool thresholdMet; bool executed; bool cancelled; } // keccak256(abi.encode(uint256(keccak256("ethaura.storage.SocialRecoveryModule")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant STORAGE_LOCATION = - 0x9a1e5f7d8c2b3a4e6f0d1c2b3a4e5f6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a00; + bytes32 private constant STORAGE_LOCATION = 0x9a1e5f7d8c2b3a4e6f0d1c2b3a4e5f6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a00; /*////////////////////////////////////////////////////////////// EVENTS @@ -129,17 +124,17 @@ contract SocialRecoveryModule is IExecutor { /// @inheritdoc IModule function onInstall(bytes calldata data) external override { SocialRecoveryStorage storage $ = _getStorage(); - + // Decode: threshold, timelockPeriod, guardians[] - (uint256 threshold, uint256 timelockPeriod, address[] memory guardians) = + (uint256 threshold, uint256 timelockPeriod, address[] memory guardians) = abi.decode(data, (uint256, uint256, address[])); - + // Set config $.config[msg.sender] = RecoveryConfig({ threshold: threshold > 0 ? threshold : 1, timelockPeriod: timelockPeriod > 0 ? timelockPeriod : DEFAULT_TIMELOCK }); - + // Add guardians for (uint256 i = 0; i < guardians.length; i++) { if (!$.isGuardian[msg.sender][guardians[i]]) { @@ -148,7 +143,7 @@ contract SocialRecoveryModule is IExecutor { emit GuardianAdded(msg.sender, guardians[i]); } } - + emit RecoveryConfigUpdated(msg.sender, $.config[msg.sender].threshold, timelockPeriod); } @@ -227,10 +222,7 @@ contract SocialRecoveryModule is IExecutor { if (threshold == 0) revert InvalidThreshold(); if (threshold > $.guardianList[msg.sender].length) revert InvalidThreshold(); - $.config[msg.sender] = RecoveryConfig({ - threshold: threshold, - timelockPeriod: timelockPeriod - }); + $.config[msg.sender] = RecoveryConfig({threshold: threshold, timelockPeriod: timelockPeriod}); emit RecoveryConfigUpdated(msg.sender, threshold, timelockPeriod); } @@ -246,12 +238,7 @@ contract SocialRecoveryModule is IExecutor { * @param newQy New passkey Y coordinate * @param newOwner New owner address */ - function initiateRecovery( - address account, - bytes32 newQx, - bytes32 newQy, - address newOwner - ) external { + function initiateRecovery(address account, bytes32 newQx, bytes32 newQy, address newOwner) external { SocialRecoveryStorage storage $ = _getStorage(); if (!$.isGuardian[account][msg.sender]) revert NotGuardian(); @@ -324,11 +311,7 @@ contract SocialRecoveryModule is IExecutor { * @param nonce The recovery request nonce * @param validatorModule The P256MFAValidatorModule to update */ - function executeRecovery( - address account, - uint256 nonce, - address validatorModule - ) external { + function executeRecovery(address account, uint256 nonce, address validatorModule) external { SocialRecoveryStorage storage $ = _getStorage(); RecoveryRequest storage request = $.requests[account][nonce]; @@ -354,23 +337,20 @@ contract SocialRecoveryModule is IExecutor { ); // Execute via the account - IERC7579Account(account).executeFromExecutor( - _encodeExecutionMode(), - abi.encodePacked(validatorModule, uint256(0), updateCalldata) - ); + IERC7579Account(account) + .executeFromExecutor( + _encodeExecutionMode(), abi.encodePacked(validatorModule, uint256(0), updateCalldata) + ); } // Update owner if provided if (request.newOwner != address(0)) { - updateCalldata = abi.encodeWithSelector( - P256MFAValidatorModule.setOwner.selector, - request.newOwner - ); + updateCalldata = abi.encodeWithSelector(P256MFAValidatorModule.setOwner.selector, request.newOwner); - IERC7579Account(account).executeFromExecutor( - _encodeExecutionMode(), - abi.encodePacked(validatorModule, uint256(0), updateCalldata) - ); + IERC7579Account(account) + .executeFromExecutor( + _encodeExecutionMode(), abi.encodePacked(validatorModule, uint256(0), updateCalldata) + ); } emit RecoveryExecuted(account, nonce); @@ -398,12 +378,7 @@ contract SocialRecoveryModule is IExecutor { * @dev ModeCode: 0x00 for single call, no delegatecall */ function _encodeExecutionMode() internal pure returns (ModeCode) { - return ModeLib.encode( - CALLTYPE_SINGLE, - EXECTYPE_DEFAULT, - MODE_DEFAULT, - ModePayload.wrap(bytes22(0)) - ); + return ModeLib.encode(CALLTYPE_SINGLE, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(bytes22(0))); } /*////////////////////////////////////////////////////////////// @@ -449,17 +424,21 @@ contract SocialRecoveryModule is IExecutor { /** * @notice Get recovery request details */ - function getRecoveryRequest(address account, uint256 nonce) external view returns ( - bytes32 newPasskeyQx, - bytes32 newPasskeyQy, - address newOwner, - uint256 approvalCount, - uint256 initiatedAt, - uint256 executeAfter, - bool thresholdMet, - bool executed, - bool cancelled - ) { + function getRecoveryRequest(address account, uint256 nonce) + external + view + returns ( + bytes32 newPasskeyQx, + bytes32 newPasskeyQy, + address newOwner, + uint256 approvalCount, + uint256 initiatedAt, + uint256 executeAfter, + bool thresholdMet, + bool executed, + bool cancelled + ) + { RecoveryRequest storage request = _getStorage().requests[account][nonce]; return ( request.newPasskeyQx, diff --git a/src/modular/modules/fallback/ERC1155ReceiverModule.sol b/src/modular/modules/fallback/ERC1155ReceiverModule.sol new file mode 100644 index 0000000..051022c --- /dev/null +++ b/src/modular/modules/fallback/ERC1155ReceiverModule.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {MODULE_TYPE_FALLBACK} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + +/// @title ERC1155ReceiverModule +/// @notice Fallback module to allow account to receive multi-tokens (ERC1155) +/// @dev Installed as a fallback handler for onERC1155Received and onERC1155BatchReceived selectors +contract ERC1155ReceiverModule is IERC1155Receiver { + /*////////////////////////////////////////////////////////////// + MODULE INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @notice Initialize the module (no-op for this simple module) + function onInstall(bytes calldata) external {} + + /// @notice Uninstall the module (no-op for this simple module) + function onUninstall(bytes calldata) external {} + + function isModuleType(uint256 typeID) external pure returns (bool) { + return typeID == MODULE_TYPE_FALLBACK; + } + + function isInitialized(address) external pure returns (bool) { + return true; // Always initialized - stateless module + } + + /*////////////////////////////////////////////////////////////// + ERC1155 RECEIVER + //////////////////////////////////////////////////////////////*/ + + /// @notice Handle the receipt of a single ERC1155 token type + /// @dev The ERC1155 smart contract calls this function on the recipient + /// after a `safeTransferFrom`. This function MUST return the function selector, + /// otherwise the caller will revert the transaction. + /// @return bytes4 `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + pure + override + returns (bytes4) + { + return IERC1155Receiver.onERC1155Received.selector; + } + + /// @notice Handle the receipt of multiple ERC1155 token types + /// @dev The ERC1155 smart contract calls this function on the recipient + /// after a `safeBatchTransferFrom`. This function MUST return the function selector, + /// otherwise the caller will revert the transaction. + /// @return bytes4 `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + pure + override + returns (bytes4) + { + return IERC1155Receiver.onERC1155BatchReceived.selector; + } + + /// @notice Query if a contract implements an interface + /// @param interfaceId The interface identifier + /// @return True if the contract implements `interfaceId` + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId; + } +} + diff --git a/src/modular/modules/fallback/ERC721ReceiverModule.sol b/src/modular/modules/fallback/ERC721ReceiverModule.sol new file mode 100644 index 0000000..ecb1255 --- /dev/null +++ b/src/modular/modules/fallback/ERC721ReceiverModule.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {MODULE_TYPE_FALLBACK} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +/// @title ERC721ReceiverModule +/// @notice Fallback module to allow account to receive NFTs via safeTransferFrom +/// @dev Installed as a fallback handler for onERC721Received selector +contract ERC721ReceiverModule is IERC721Receiver { + /*////////////////////////////////////////////////////////////// + MODULE INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @notice Initialize the module (no-op for this simple module) + function onInstall(bytes calldata) external {} + + /// @notice Uninstall the module (no-op for this simple module) + function onUninstall(bytes calldata) external {} + + function isModuleType(uint256 typeID) external pure returns (bool) { + return typeID == MODULE_TYPE_FALLBACK; + } + + function isInitialized(address) external pure returns (bool) { + return true; // Always initialized - stateless module + } + + /*////////////////////////////////////////////////////////////// + ERC721 RECEIVER + //////////////////////////////////////////////////////////////*/ + + /// @notice Handle the receipt of an NFT + /// @dev The ERC721 smart contract calls this function on the recipient + /// after a `safeTransfer`. This function MUST return the function selector, + /// otherwise the caller will revert the transaction. + /// @return bytes4 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } +} + diff --git a/src/modular/modules/hooks/LargeTransactionGuardHook.sol b/src/modular/modules/hooks/LargeTransactionGuardHook.sol new file mode 100644 index 0000000..77e17a7 --- /dev/null +++ b/src/modular/modules/hooks/LargeTransactionGuardHook.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IHook, MODULE_TYPE_HOOK} from "@erc7579/interfaces/IERC7579Module.sol"; + +/// @title LargeTransactionGuardHook +/// @notice Hook that enforces large transactions go through LargeTransactionExecutorModule +/// @dev Installed at account initialization, disabled by default (threshold = type(uint256).max) +/// Reads threshold from LargeTransactionExecutorModule - no separate configuration needed +contract LargeTransactionGuardHook is IHook { + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @dev ERC-7201 storage slot + bytes32 private constant GUARD_HOOK_STORAGE_SLOT = keccak256( + abi.encode(uint256(keccak256("ethaura.storage.LargeTransactionGuardHook")) - 1) + ) & ~bytes32(uint256(0xff)); + + struct GuardHookStorage { + mapping(address account => address) executor; // LargeTransactionExecutorModule address + } + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error LargeTransactionMustUseExecutor(); + + /*////////////////////////////////////////////////////////////// + STORAGE ACCESS + //////////////////////////////////////////////////////////////*/ + + function _getStorage() internal pure returns (GuardHookStorage storage $) { + bytes32 slot = GUARD_HOOK_STORAGE_SLOT; + assembly { + $.slot := slot + } + } + + /*////////////////////////////////////////////////////////////// + MODULE INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @notice Initialize the hook with executor address + /// @param data ABI-encoded executor address + function onInstall(bytes calldata data) external override { + address executor = abi.decode(data, (address)); + _getStorage().executor[msg.sender] = executor; + } + + /// @notice Uninstall the hook + function onUninstall(bytes calldata) external override { + delete _getStorage().executor[msg.sender]; + } + + function isModuleType(uint256 typeID) external pure override returns (bool) { + return typeID == MODULE_TYPE_HOOK; + } + + function isInitialized(address account) external view override returns (bool) { + return _getStorage().executor[account] != address(0); + } + + /*////////////////////////////////////////////////////////////// + HOOK INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @notice Pre-execution check - enforces large txs go through executor + /// @dev If threshold is type(uint256).max (disabled) or value <= threshold, allow + /// Otherwise, only allow if caller is the LargeTransactionExecutorModule + function preCheck(address msgSender, uint256 value, bytes calldata) external view override returns (bytes memory) { + GuardHookStorage storage $ = _getStorage(); + address executor = $.executor[msg.sender]; + + if (executor == address(0)) { + // Not initialized - allow all (fail-open for uninitialized) + return ""; + } + + // Get threshold from executor + uint256 threshold = ILargeTransactionExecutorModule(executor).getThreshold(msg.sender); + + // If disabled (max value) or small tx, allow + if (threshold == type(uint256).max || value <= threshold) { + return ""; + } + + // Large tx must come from LargeTransactionExecutorModule + if (msgSender != executor) { + revert LargeTransactionMustUseExecutor(); + } + + return ""; + } + + /// @notice Post-execution check - no-op for this hook + function postCheck(bytes calldata) external pure override { + // No post-check needed + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Get the executor address for an account + function getExecutor(address account) external view returns (address) { + return _getStorage().executor[account]; + } +} + +/// @dev Interface for LargeTransactionExecutorModule (minimal for threshold reading) +interface ILargeTransactionExecutorModule { + function getThreshold(address account) external view returns (uint256); +} + diff --git a/src/modular/modules/hooks/MultiHook.sol b/src/modular/modules/hooks/MultiHook.sol new file mode 100644 index 0000000..e2fb317 --- /dev/null +++ b/src/modular/modules/hooks/MultiHook.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IHook, MODULE_TYPE_HOOK} from "@erc7579/interfaces/IERC7579Module.sol"; + +/// @title MultiHook +/// @notice Wrapper that chains multiple hooks together for ERC-7579 accounts +/// @dev Installed as the global hook, then individual hooks are added/removed +contract MultiHook is IHook { + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @dev ERC-7201 storage slot + bytes32 private constant MULTI_HOOK_STORAGE_SLOT = + keccak256(abi.encode(uint256(keccak256("ethaura.storage.MultiHook")) - 1)) & ~bytes32(uint256(0xff)); + + struct MultiHookStorage { + mapping(address account => address[]) hooks; + mapping(address account => mapping(address hook => bool)) isHook; + mapping(address account => address) manager; // HookManagerModule address + } + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error HookAlreadyInstalled(address hook); + error HookNotInstalled(address hook); + error NotManager(); + error NotAccount(); + error InvalidHook(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event HookAdded(address indexed account, address indexed hook); + event HookRemoved(address indexed account, address indexed hook); + event ManagerSet(address indexed account, address indexed manager); + + /*////////////////////////////////////////////////////////////// + STORAGE ACCESS + //////////////////////////////////////////////////////////////*/ + + function _getStorage() internal pure returns (MultiHookStorage storage $) { + bytes32 slot = MULTI_HOOK_STORAGE_SLOT; + assembly { + $.slot := slot + } + } + + /*////////////////////////////////////////////////////////////// + MODULE INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @notice Initialize the MultiHook for an account + /// @param data ABI-encoded manager address (optional) + function onInstall(bytes calldata data) external override { + if (data.length >= 32) { + address manager = abi.decode(data, (address)); + if (manager != address(0)) { + _getStorage().manager[msg.sender] = manager; + emit ManagerSet(msg.sender, manager); + } + } + } + + /// @notice Uninstall the MultiHook - removes all hooks + function onUninstall(bytes calldata) external override { + MultiHookStorage storage $ = _getStorage(); + address[] storage hooks = $.hooks[msg.sender]; + + // Remove all hooks + for (uint256 i = 0; i < hooks.length; i++) { + $.isHook[msg.sender][hooks[i]] = false; + } + delete $.hooks[msg.sender]; + delete $.manager[msg.sender]; + } + + function isModuleType(uint256 typeID) external pure override returns (bool) { + return typeID == MODULE_TYPE_HOOK; + } + + function isInitialized(address account) external view override returns (bool) { + return true; // MultiHook is always "initialized" - it works with empty hooks + } + + /*////////////////////////////////////////////////////////////// + HOOK INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @notice Pre-execution check - calls all registered hooks + function preCheck(address msgSender, uint256 value, bytes calldata data) external override returns (bytes memory) { + MultiHookStorage storage $ = _getStorage(); + address[] storage hooks = $.hooks[msg.sender]; + + // Collect context from all hooks + // Store both hooks and contexts so postCheck uses the same hooks + address[] memory hooksCopy = new address[](hooks.length); + bytes[] memory contexts = new bytes[](hooks.length); + for (uint256 i = 0; i < hooks.length; i++) { + hooksCopy[i] = hooks[i]; + contexts[i] = IHook(hooks[i]).preCheck(msgSender, value, data); + } + + return abi.encode(hooksCopy, contexts); + } + + /// @notice Post-execution check - calls all registered hooks + function postCheck(bytes calldata preCheckData) external override { + // Decode the hooks and contexts from preCheck + // This ensures we call postCheck on the same hooks that were called in preCheck + (address[] memory hooks, bytes[] memory contexts) = abi.decode(preCheckData, (address[], bytes[])); + + for (uint256 i = 0; i < hooks.length; i++) { + IHook(hooks[i]).postCheck(contexts[i]); + } + } + + /*////////////////////////////////////////////////////////////// + HOOK MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /// @notice Add a hook to the chain + /// @dev Can only be called by the account itself + function addHook(address hook) external { + if (hook == address(0)) revert InvalidHook(); + + MultiHookStorage storage $ = _getStorage(); + // msg.sender is the account (called directly or via executeFromExecutor) + address account = msg.sender; + + if ($.isHook[account][hook]) revert HookAlreadyInstalled(hook); + + $.hooks[account].push(hook); + $.isHook[account][hook] = true; + + emit HookAdded(account, hook); + } + + /// @notice Remove a hook from the chain + function removeHook(address hook) external { + MultiHookStorage storage $ = _getStorage(); + // msg.sender is the account (called directly or via executeFromExecutor) + address account = msg.sender; + + if (!$.isHook[account][hook]) revert HookNotInstalled(hook); + + // Find and remove hook + address[] storage hooks = $.hooks[account]; + for (uint256 i = 0; i < hooks.length; i++) { + if (hooks[i] == hook) { + hooks[i] = hooks[hooks.length - 1]; + hooks.pop(); + break; + } + } + $.isHook[account][hook] = false; + + emit HookRemoved(account, hook); + } + + /// @notice Set the manager (HookManagerModule) for an account + function setManager(address manager) external { + MultiHookStorage storage $ = _getStorage(); + // Only account can set its manager + $.manager[msg.sender] = manager; + emit ManagerSet(msg.sender, manager); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Get all hooks for an account + function getHooks(address account) external view returns (address[] memory) { + return _getStorage().hooks[account]; + } + + /// @notice Check if a hook is installed for an account + function isHookInstalled(address account, address hook) external view returns (bool) { + return _getStorage().isHook[account][hook]; + } + + /// @notice Get the manager for an account + function getManager(address account) external view returns (address) { + return _getStorage().manager[account]; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function _checkAuthorized() internal view { + MultiHookStorage storage $ = _getStorage(); + address account = _getAccount(); + address manager = $.manager[account]; + + // Allow account itself or its manager + if (msg.sender != account && msg.sender != manager) { + revert NotManager(); + } + } + + function _getAccount() internal view returns (address) { + MultiHookStorage storage $ = _getStorage(); + address manager = $.manager[msg.sender]; + + // If caller has a manager set, caller is the account + // If caller is the manager, we need to find the account + // For simplicity, when manager calls, msg.sender context should be the account + // This is handled by executeFromExecutor pattern + return msg.sender; + } +} + diff --git a/src/modular/modules/P256MFAValidatorModule.sol b/src/modular/modules/validators/P256MFAValidatorModule.sol similarity index 95% rename from src/modular/modules/P256MFAValidatorModule.sol rename to src/modular/modules/validators/P256MFAValidatorModule.sol index 01a4201..9604a5d 100644 --- a/src/modular/modules/P256MFAValidatorModule.sol +++ b/src/modular/modules/validators/P256MFAValidatorModule.sol @@ -51,8 +51,7 @@ contract P256MFAValidatorModule is IValidator { } // keccak256(abi.encode(uint256(keccak256("ethaura.storage.P256MFAValidatorModule")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant STORAGE_LOCATION = - 0x8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d600; + bytes32 private constant STORAGE_LOCATION = 0x8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d600; /*////////////////////////////////////////////////////////////// EVENTS @@ -109,18 +108,18 @@ contract P256MFAValidatorModule is IValidator { // Decode: owner, qx, qy, deviceId, enableMFA (address owner, bytes32 qx, bytes32 qy, bytes32 deviceId, bool shouldEnableMFA) = abi.decode(data, (address, bytes32, bytes32, bytes32, bool)); - + if (owner == address(0)) revert InvalidOwner(); - + // Set owner $.owners[msg.sender] = owner; emit OwnerSet(msg.sender, owner); - + // Add passkey if provided if (qx != bytes32(0) && qy != bytes32(0)) { _addPasskeyInternal(msg.sender, qx, qy, deviceId); } - + // Enable MFA if requested (requires passkey) if (shouldEnableMFA) { if ($.passkeyCount[msg.sender] == 0) revert MFARequiresPasskey(); @@ -132,13 +131,13 @@ contract P256MFAValidatorModule is IValidator { /// @inheritdoc IModule function onUninstall(bytes calldata) external override { P256MFAValidatorStorage storage $ = _getStorage(); - + // Clear owner delete $.owners[msg.sender]; - + // Clear MFA delete $.mfaEnabled[msg.sender]; - + // Clear all passkeys bytes32[] storage ids = $.passkeyIds[msg.sender]; for (uint256 i = 0; i < ids.length; i++) { @@ -251,13 +250,7 @@ contract P256MFAValidatorModule is IValidator { WebAuthn.WebAuthnAuth memory auth = WebAuthn.tryDecodeAuthCompactCalldata(webAuthnSig); bytes memory challenge = abi.encodePacked(hash); - bool webAuthnValid = WebAuthn.verify( - challenge, - true, - auth, - passkeyInfo.qx, - passkeyInfo.qy - ); + bool webAuthnValid = WebAuthn.verify(challenge, true, auth, passkeyInfo.qx, passkeyInfo.qy); if (!webAuthnValid) return bytes4(0xffffffff); if (!_verifyOwnerSignature(hash, ownerSig, owner)) return bytes4(0xffffffff); @@ -269,11 +262,7 @@ contract P256MFAValidatorModule is IValidator { SIGNATURE VERIFICATION //////////////////////////////////////////////////////////////*/ - function _verifyOwnerSignature(bytes32 hash, bytes calldata signature, address owner) - internal - view - returns (bool) - { + function _verifyOwnerSignature(bytes32 hash, bytes calldata signature, address owner) internal view returns (bool) { address recovered = ECDSA.recover(hash, signature); return recovered == owner; } @@ -333,13 +322,8 @@ contract P256MFAValidatorModule is IValidator { if ($.passkeys[account][passkeyId].active) revert PasskeyAlreadyExists(); - $.passkeys[account][passkeyId] = PasskeyInfo({ - qx: qx, - qy: qy, - addedAt: block.timestamp, - active: true, - deviceId: deviceId - }); + $.passkeys[account][passkeyId] = + PasskeyInfo({qx: qx, qy: qy, addedAt: block.timestamp, active: true, deviceId: deviceId}); $.passkeyIds[account].push(passkeyId); $.passkeyCount[account]++; diff --git a/test/modular/ModuleTests.t.sol b/test/modular/ModuleTests.t.sol new file mode 100644 index 0000000..4e18a53 --- /dev/null +++ b/test/modular/ModuleTests.t.sol @@ -0,0 +1,554 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; + +import {IERC7579Account, Execution} from "@erc7579/interfaces/IERC7579Account.sol"; +import { + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK +} from "@erc7579/interfaces/IERC7579Module.sol"; +import {ModeLib, ModeCode} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +import {MockValidator} from "./mocks/MockValidator.sol"; +import {MockHook} from "./mocks/MockHook.sol"; +import {MockTarget} from "./mocks/MockTarget.sol"; + +import {MultiHook} from "../../src/modular/modules/hooks/MultiHook.sol"; +import {LargeTransactionGuardHook} from "../../src/modular/modules/hooks/LargeTransactionGuardHook.sol"; +import {LargeTransactionExecutorModule} from "../../src/modular/modules/executors/LargeTransactionExecutorModule.sol"; +import {HookManagerModule} from "../../src/modular/modules/executors/HookManagerModule.sol"; +import {ERC721ReceiverModule} from "../../src/modular/modules/fallback/ERC721ReceiverModule.sol"; +import {ERC1155ReceiverModule} from "../../src/modular/modules/fallback/ERC1155ReceiverModule.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + +/*////////////////////////////////////////////////////////////// + MULTI HOOK TESTS +//////////////////////////////////////////////////////////////*/ + +contract MultiHookTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + MultiHook public multiHook; + MockHook public hook1; + MockHook public hook2; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address owner = address(0x1234); + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + factory = new AuraAccountFactory(); + validator = new MockValidator(); + multiHook = new MultiHook(); + hook1 = new MockHook(); + hook2 = new MockHook(); + target = new MockTarget(); + + // Create account with MultiHook as global hook + address accountAddr = + factory.createAccount(owner, address(validator), abi.encode(true), address(multiHook), "", 0); + account = AuraAccount(payable(accountAddr)); + vm.deal(address(account), 10 ether); + } + + function test_MultiHookInstalled() public view { + assertTrue(account.isModuleInstalled(MODULE_TYPE_HOOK, address(multiHook), "")); + assertEq(account.getGlobalHook(), address(multiHook)); + } + + function test_AddHook() public { + vm.prank(address(account)); + multiHook.addHook(address(hook1)); + + address[] memory hooks = multiHook.getHooks(address(account)); + assertEq(hooks.length, 1); + assertEq(hooks[0], address(hook1)); + assertTrue(multiHook.isHookInstalled(address(account), address(hook1))); + } + + function test_AddMultipleHooks() public { + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + multiHook.addHook(address(hook2)); + vm.stopPrank(); + + address[] memory hooks = multiHook.getHooks(address(account)); + assertEq(hooks.length, 2); + } + + function test_RemoveHook() public { + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + multiHook.removeHook(address(hook1)); + vm.stopPrank(); + + address[] memory hooks = multiHook.getHooks(address(account)); + assertEq(hooks.length, 0); + assertFalse(multiHook.isHookInstalled(address(account), address(hook1))); + } + + function test_RevertAddDuplicateHook() public { + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + + vm.expectRevert(abi.encodeWithSelector(MultiHook.HookAlreadyInstalled.selector, address(hook1))); + multiHook.addHook(address(hook1)); + vm.stopPrank(); + } + + function test_RevertRemoveNonExistentHook() public { + vm.prank(address(account)); + vm.expectRevert(abi.encodeWithSelector(MultiHook.HookNotInstalled.selector, address(hook1))); + multiHook.removeHook(address(hook1)); + } + + function test_AddHookFromDifferentAccount() public { + // When a different address calls addHook, it adds the hook for that address (not our account) + address otherAccount = address(0x9999); + vm.prank(otherAccount); + multiHook.addHook(address(hook1)); + + // Hook should be installed for otherAccount, not our account + assertTrue(multiHook.isHookInstalled(otherAccount, address(hook1))); + assertFalse(multiHook.isHookInstalled(address(account), address(hook1))); + } + + function test_HooksCalledOnExecute() public { + // Add hooks + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + multiHook.addHook(address(hook2)); + vm.stopPrank(); + + // Execute transaction + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + + // Both hooks should have been called + assertEq(hook1.preCheckCount(), 1); + assertEq(hook1.postCheckCount(), 1); + assertEq(hook2.preCheckCount(), 1); + assertEq(hook2.postCheckCount(), 1); + } + + function test_SetManager() public { + address manager = address(0x5678); + + vm.prank(address(account)); + multiHook.setManager(manager); + + assertEq(multiHook.getManager(address(account)), manager); + } +} + +/*////////////////////////////////////////////////////////////// + LARGE TRANSACTION EXECUTOR TESTS +//////////////////////////////////////////////////////////////*/ + +contract LargeTransactionExecutorModuleTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + LargeTransactionExecutorModule public executor; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address owner = address(0x1234); + + uint256 constant THRESHOLD = 1 ether; + uint256 constant TIMELOCK = 1 hours; + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + factory = new AuraAccountFactory(); + validator = new MockValidator(); + executor = new LargeTransactionExecutorModule(); + target = new MockTarget(); + + address accountAddr = factory.createAccount(owner, address(validator), abi.encode(true), address(0), "", 0); + account = AuraAccount(payable(accountAddr)); + vm.deal(address(account), 10 ether); + + // Install executor + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), abi.encode(THRESHOLD, TIMELOCK)); + } + + function test_ExecutorInstalled() public view { + assertTrue(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(executor), "")); + assertEq(executor.getThreshold(address(account)), THRESHOLD); + assertEq(executor.getTimelockPeriod(address(account)), TIMELOCK); + } + + function test_ProposeTransaction() public { + vm.prank(address(account)); + executor.execute(address(target), 2 ether, abi.encodeCall(MockTarget.setValue, (100))); + + bytes32 txHash = keccak256( + abi.encode(address(account), address(target), 2 ether, abi.encodeCall(MockTarget.setValue, (100))) + ); + + (address txTarget, uint256 value,, uint256 proposedAt, uint256 executeAfter, bool executed, bool cancelled) = + executor.getPendingTx(address(account), txHash); + + assertEq(txTarget, address(target)); + assertEq(value, 2 ether); + assertEq(proposedAt, block.timestamp); + assertEq(executeAfter, block.timestamp + TIMELOCK); + assertFalse(executed); + assertFalse(cancelled); + } + + function test_ExecuteAfterTimelock() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + + // Propose + vm.prank(address(account)); + executor.execute(address(target), 0, callData); + + // Warp past timelock + vm.warp(block.timestamp + TIMELOCK + 1); + + // Execute + vm.prank(address(account)); + executor.execute(address(target), 0, callData); + + assertEq(target.value(), 100); + } + + function test_RevertExecuteBeforeTimelock() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + + // Propose + vm.prank(address(account)); + executor.execute(address(target), 0, callData); + + // Try to execute before timelock + vm.prank(address(account)); + vm.expectRevert(LargeTransactionExecutorModule.TimelockNotPassed.selector); + executor.execute(address(target), 0, callData); + } + + function test_CancelTransaction() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + + // Propose + vm.prank(address(account)); + executor.execute(address(target), 0, callData); + + bytes32 txHash = keccak256(abi.encode(address(account), address(target), 0, callData)); + + // Cancel + vm.prank(address(account)); + executor.cancel(txHash); + + (,,,,,, bool cancelled) = executor.getPendingTx(address(account), txHash); + assertTrue(cancelled); + } + + function test_RevertExecuteCancelledTransaction() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + + // Propose + vm.prank(address(account)); + executor.execute(address(target), 0, callData); + + bytes32 txHash = keccak256(abi.encode(address(account), address(target), 0, callData)); + + // Cancel + vm.prank(address(account)); + executor.cancel(txHash); + + // Warp past timelock + vm.warp(block.timestamp + TIMELOCK + 1); + + // Try to execute cancelled tx + vm.prank(address(account)); + vm.expectRevert(LargeTransactionExecutorModule.TransactionWasCancelled.selector); + executor.execute(address(target), 0, callData); + } + + function test_SetThreshold() public { + vm.prank(address(account)); + executor.setThreshold(5 ether); + + assertEq(executor.getThreshold(address(account)), 5 ether); + } + + function test_Disable() public { + vm.prank(address(account)); + executor.disable(); + + assertEq(executor.getThreshold(address(account)), type(uint256).max); + } +} + +/*////////////////////////////////////////////////////////////// + LARGE TRANSACTION GUARD HOOK TESTS +//////////////////////////////////////////////////////////////*/ + +contract LargeTransactionGuardHookTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + LargeTransactionExecutorModule public executor; + LargeTransactionGuardHook public guardHook; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address owner = address(0x1234); + + uint256 constant THRESHOLD = 1 ether; + uint256 constant TIMELOCK = 1 hours; + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + factory = new AuraAccountFactory(); + validator = new MockValidator(); + executor = new LargeTransactionExecutorModule(); + guardHook = new LargeTransactionGuardHook(); + target = new MockTarget(); + + // Create account with guard hook + address accountAddr = factory.createAccount( + owner, address(validator), abi.encode(true), address(guardHook), abi.encode(address(executor)), 0 + ); + account = AuraAccount(payable(accountAddr)); + vm.deal(address(account), 10 ether); + + // Install executor + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), abi.encode(THRESHOLD, TIMELOCK)); + } + + function test_GuardHookInstalled() public view { + assertTrue(account.isModuleInstalled(MODULE_TYPE_HOOK, address(guardHook), "")); + assertEq(guardHook.getExecutor(address(account)), address(executor)); + } + + function test_AllowSmallTransaction() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0.5 ether, callData); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + + assertEq(target.value(), 42); + } + + function test_RevertLargeTransactionWithoutExecutor() public { + // Note: The guard hook checks msg.value of the execute call, not the value in executionData + // So we need to send value with the execute call itself + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + // Fund the EntryPoint to send value + vm.deal(ENTRYPOINT, 10 ether); + + vm.prank(ENTRYPOINT); + vm.expectRevert(LargeTransactionGuardHook.LargeTransactionMustUseExecutor.selector); + account.execute{value: 2 ether}(ModeLib.encodeSimpleSingle(), executionData); + } + + function test_AllowDisabledThreshold() public { + // Disable threshold + vm.prank(address(account)); + executor.disable(); + + // Large tx should now be allowed + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 5 ether, callData); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + + assertEq(target.value(), 42); + } +} + +/*////////////////////////////////////////////////////////////// + HOOK MANAGER MODULE TESTS +//////////////////////////////////////////////////////////////*/ + +contract HookManagerModuleTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + MultiHook public multiHook; + HookManagerModule public hookManager; + MockHook public hook1; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address owner = address(0x1234); + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + factory = new AuraAccountFactory(); + validator = new MockValidator(); + multiHook = new MultiHook(); + hookManager = new HookManagerModule(); + hook1 = new MockHook(); + + // Create account with MultiHook + address accountAddr = factory.createAccount( + owner, address(validator), abi.encode(true), address(multiHook), abi.encode(address(hookManager)), 0 + ); + account = AuraAccount(payable(accountAddr)); + vm.deal(address(account), 10 ether); + + // Install hook manager + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(hookManager), abi.encode(address(multiHook))); + + // Set hook manager as MultiHook's manager + vm.prank(address(account)); + multiHook.setManager(address(hookManager)); + } + + function test_HookManagerInstalled() public view { + assertTrue(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(hookManager), "")); + } + + function test_InstallHookViaManager() public { + vm.prank(address(account)); + hookManager.installHook(address(hook1), ""); + + assertTrue(multiHook.isHookInstalled(address(account), address(hook1))); + } + + function test_UninstallHookViaManager() public { + vm.startPrank(address(account)); + hookManager.installHook(address(hook1), ""); + hookManager.uninstallHook(address(hook1)); + vm.stopPrank(); + + assertFalse(multiHook.isHookInstalled(address(account), address(hook1))); + } + + function test_ProposeEmergencyUninstall() public { + vm.startPrank(address(account)); + hookManager.installHook(address(hook1), ""); + hookManager.proposeEmergencyUninstall(address(hook1)); + vm.stopPrank(); + } + + function test_ExecuteEmergencyUninstall() public { + vm.startPrank(address(account)); + hookManager.installHook(address(hook1), ""); + hookManager.proposeEmergencyUninstall(address(hook1)); + vm.stopPrank(); + + // Warp past emergency timelock + vm.warp(block.timestamp + 24 hours + 1); + + vm.prank(address(account)); + hookManager.executeEmergencyUninstall(); + + assertFalse(multiHook.isHookInstalled(address(account), address(hook1))); + } + + function test_RevertEmergencyUninstallBeforeTimelock() public { + vm.startPrank(address(account)); + hookManager.installHook(address(hook1), ""); + hookManager.proposeEmergencyUninstall(address(hook1)); + + vm.expectRevert(HookManagerModule.EmergencyTimelockNotPassed.selector); + hookManager.executeEmergencyUninstall(); + vm.stopPrank(); + } +} + +/*////////////////////////////////////////////////////////////// + ERC721 RECEIVER MODULE TESTS +//////////////////////////////////////////////////////////////*/ + +contract ERC721ReceiverModuleTest is Test { + ERC721ReceiverModule public receiver; + + function setUp() public { + receiver = new ERC721ReceiverModule(); + } + + function test_IsModuleType() public view { + assertTrue(receiver.isModuleType(MODULE_TYPE_FALLBACK)); + assertFalse(receiver.isModuleType(MODULE_TYPE_VALIDATOR)); + } + + function test_IsInitialized() public view { + assertTrue(receiver.isInitialized(address(this))); + } + + function test_OnERC721Received() public view { + bytes4 result = receiver.onERC721Received(address(this), address(this), 1, ""); + assertEq(result, bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))); + } +} + +/*////////////////////////////////////////////////////////////// + ERC1155 RECEIVER MODULE TESTS +//////////////////////////////////////////////////////////////*/ + +contract ERC1155ReceiverModuleTest is Test { + ERC1155ReceiverModule public receiver; + + function setUp() public { + receiver = new ERC1155ReceiverModule(); + } + + function test_IsModuleType() public view { + assertTrue(receiver.isModuleType(MODULE_TYPE_FALLBACK)); + assertFalse(receiver.isModuleType(MODULE_TYPE_VALIDATOR)); + } + + function test_IsInitialized() public view { + assertTrue(receiver.isInitialized(address(this))); + } + + function test_OnERC1155Received() public view { + bytes4 result = receiver.onERC1155Received(address(this), address(this), 1, 100, ""); + assertEq(result, bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))); + } + + function test_OnERC1155BatchReceived() public view { + uint256[] memory ids = new uint256[](2); + ids[0] = 1; + ids[1] = 2; + uint256[] memory amounts = new uint256[](2); + amounts[0] = 100; + amounts[1] = 200; + + bytes4 result = receiver.onERC1155BatchReceived(address(this), address(this), ids, amounts, ""); + assertEq(result, bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))); + } + + function test_SupportsInterface() public view { + // IERC1155Receiver interface ID + bytes4 interfaceId = type(IERC1155Receiver).interfaceId; + assertTrue(receiver.supportsInterface(interfaceId)); + } +} + diff --git a/test/modular/P256MFAValidatorModule.t.sol b/test/modular/P256MFAValidatorModule.t.sol index 98696ec..3644c53 100644 --- a/test/modular/P256MFAValidatorModule.t.sol +++ b/test/modular/P256MFAValidatorModule.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.23; import {Test} from "forge-std/Test.sol"; import {AuraAccount} from "../../src/modular/AuraAccount.sol"; import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; -import {P256MFAValidatorModule} from "../../src/modular/modules/P256MFAValidatorModule.sol"; +import {P256MFAValidatorModule} from "../../src/modular/modules/validators/P256MFAValidatorModule.sol"; import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; import {MODULE_TYPE_VALIDATOR} from "@erc7579/interfaces/IERC7579Module.sol"; import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; @@ -36,7 +36,7 @@ contract P256MFAValidatorModuleTest is Test { // Create account with P256MFAValidatorModule // Init data: owner, qx, qy, deviceId, enableMFA bytes memory initData = abi.encode(owner, QX, QY, bytes32("Test Device"), true); - + address accountAddr = factory.createAccount( owner, address(validator), @@ -111,7 +111,7 @@ contract P256MFAValidatorModuleTest is Test { // Get the passkey ID bytes32 passkeyId = keccak256(abi.encodePacked(newQx, newQy)); - + // Remove the passkey validator.removePasskey(passkeyId); assertEq(validator.getPasskeyCount(address(account)), 1); diff --git a/test/modular/SocialRecoveryModule.t.sol b/test/modular/SocialRecoveryModule.t.sol index 4ef62b3..476b6ef 100644 --- a/test/modular/SocialRecoveryModule.t.sol +++ b/test/modular/SocialRecoveryModule.t.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.23; import {Test} from "forge-std/Test.sol"; import {AuraAccount} from "../../src/modular/AuraAccount.sol"; import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; -import {P256MFAValidatorModule} from "../../src/modular/modules/P256MFAValidatorModule.sol"; -import {SocialRecoveryModule} from "../../src/modular/modules/SocialRecoveryModule.sol"; +import {P256MFAValidatorModule} from "../../src/modular/modules/validators/P256MFAValidatorModule.sol"; +import {SocialRecoveryModule} from "../../src/modular/modules/executors/SocialRecoveryModule.sol"; import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; import {MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; @@ -39,7 +39,7 @@ contract SocialRecoveryModuleTest is Test { // Create account with P256MFAValidatorModule bytes memory validatorData = abi.encode(owner, QX, QY, bytes32("Test Device"), true); - + address accountAddr = factory.createAccount( owner, address(validator), @@ -54,9 +54,9 @@ contract SocialRecoveryModuleTest is Test { address[] memory guardians = new address[](2); guardians[0] = guardian1; guardians[1] = guardian2; - + bytes memory recoveryData = abi.encode( - uint256(2), // threshold: 2 of 2 + uint256(2), // threshold: 2 of 2 uint256(24 hours), // timelock guardians ); @@ -136,8 +136,7 @@ contract SocialRecoveryModuleTest is Test { bytes32 storedQy, address storedOwner, uint256 approvalCount, - uint256 initiatedAt, - , + uint256 initiatedAt,, bool thresholdMet, bool executed, bool cancelled @@ -166,14 +165,8 @@ contract SocialRecoveryModuleTest is Test { vm.prank(guardian2); recovery.approveRecovery(address(account), 0); - ( - ,,, - uint256 approvalCount, - , - uint256 executeAfter, - bool thresholdMet, - , - ) = recovery.getRecoveryRequest(address(account), 0); + (,,, uint256 approvalCount,, uint256 executeAfter, bool thresholdMet,,) = + recovery.getRecoveryRequest(address(account), 0); assertEq(approvalCount, 2); assertTrue(thresholdMet); @@ -193,10 +186,7 @@ contract SocialRecoveryModuleTest is Test { vm.prank(address(account)); recovery.cancelRecovery(0); - ( - ,,,,,,,, - bool cancelled - ) = recovery.getRecoveryRequest(address(account), 0); + (,,,,,,,, bool cancelled) = recovery.getRecoveryRequest(address(account), 0); assertTrue(cancelled); } From 8fd2b6c9fd6cd96e977c7ad5ef7eb363149dfe56 Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 17:03:22 +0700 Subject: [PATCH 04/22] feat: add SessionKeyValidatorModule for session key management Implements ERC-7579 Validator module (Type 1) for session keys: - Session key permission structure with time bounds (validAfter/validUntil) - Target and selector restrictions for fine-grained access control - Per-transaction and total spending limits with tracking - ECDSA signature validation for session key signatures - Session key management (create, revoke, query) - ERC-7201 namespaced storage for collision resistance Tests: - 6 tests covering session key lifecycle - All 87 modular tests passing --- .../validators/SessionKeyValidatorModule.sol | 459 ++++++++++++++++++ test/modular/SessionKeyValidatorModule.t.sol | 177 +++++++ 2 files changed, 636 insertions(+) create mode 100644 src/modular/modules/validators/SessionKeyValidatorModule.sol create mode 100644 test/modular/SessionKeyValidatorModule.t.sol diff --git a/src/modular/modules/validators/SessionKeyValidatorModule.sol b/src/modular/modules/validators/SessionKeyValidatorModule.sol new file mode 100644 index 0000000..5bb54ef --- /dev/null +++ b/src/modular/modules/validators/SessionKeyValidatorModule.sol @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; +import { + IModule, + IValidator, + MODULE_TYPE_VALIDATOR, + VALIDATION_SUCCESS, + VALIDATION_FAILED +} from "@erc7579/interfaces/IERC7579Module.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; + +/** + * @title SessionKeyValidatorModule + * @notice ERC-7579 Validator Module for Session Keys with permissions and spending limits + * @dev Enables gasless, signature-less transactions for dApps (gaming, trading bots, etc.) + * - Session keys are EOAs with limited permissions + * - Supports time-bounded sessions (validAfter, validUntil) + * - Supports target/selector whitelisting + * - Supports per-tx and total spending limits + */ +contract SessionKeyValidatorModule is IValidator { + using ECDSA for bytes32; + + /*////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Session key permission structure + struct SessionKeyPermission { + address sessionKey; // EOA that can sign + uint48 validAfter; // Start timestamp + uint48 validUntil; // Expiry timestamp + address[] allowedTargets; // Contracts it can call (empty = any) + bytes4[] allowedSelectors; // Functions it can call (empty = any) + uint256 spendLimitPerTx; // Max ETH per transaction (0 = unlimited) + uint256 spendLimitTotal; // Max ETH total (0 = unlimited) + } + + /// @notice Internal storage for a session key + struct SessionKeyData { + bool active; + uint48 validAfter; + uint48 validUntil; + uint256 spendLimitPerTx; + uint256 spendLimitTotal; + uint256 spentTotal; + // Stored separately for gas efficiency + } + + /*////////////////////////////////////////////////////////////// + ERC-7201 STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @custom:storage-location erc7201:ethaura.storage.SessionKeyValidatorModule + struct SessionKeyValidatorStorage { + // Per-account session key storage + mapping(address account => mapping(address sessionKey => SessionKeyData)) sessionKeys; + mapping(address account => address[]) sessionKeyList; + mapping(address account => uint256) sessionKeyCount; + // Target/selector permissions stored separately + mapping(address account => mapping(address sessionKey => address[])) allowedTargets; + mapping(address account => mapping(address sessionKey => bytes4[])) allowedSelectors; + // Account owner (can manage session keys) + mapping(address account => address) owners; + } + + // keccak256(abi.encode(uint256(keccak256("ethaura.storage.SessionKeyValidatorModule")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant STORAGE_LOCATION = 0x96ac80b66f1fc01632bfcc8f443eba4e1a9afd53fde13b3cf2ab8d3626e18300; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event OwnerSet(address indexed account, address indexed owner); + event SessionKeyCreated( + address indexed account, + address indexed sessionKey, + uint48 validAfter, + uint48 validUntil, + uint256 spendLimitPerTx, + uint256 spendLimitTotal + ); + event SessionKeyRevoked(address indexed account, address indexed sessionKey); + event SessionKeySpent(address indexed account, address indexed sessionKey, uint256 amount); + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error OnlyOwner(); + error InvalidOwner(); + error InvalidSessionKey(); + error SessionKeyAlreadyExists(); + error SessionKeyDoesNotExist(); + error SessionKeyExpired(); + error SessionKeyNotYetValid(); + error SessionKeyNotActive(); + error TargetNotAllowed(); + error SelectorNotAllowed(); + error SpendLimitPerTxExceeded(); + error SpendLimitTotalExceeded(); + error InvalidSignature(); + error InvalidTimeRange(); + + /*////////////////////////////////////////////////////////////// + STORAGE ACCESS + //////////////////////////////////////////////////////////////*/ + + function _getStorage() internal pure returns (SessionKeyValidatorStorage storage $) { + bytes32 location = STORAGE_LOCATION; + assembly { + $.slot := location + } + } + + /*////////////////////////////////////////////////////////////// + IModule INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IModule + function onInstall(bytes calldata data) external override { + SessionKeyValidatorStorage storage $ = _getStorage(); + + // Decode: owner + address owner = abi.decode(data, (address)); + if (owner == address(0)) revert InvalidOwner(); + + $.owners[msg.sender] = owner; + emit OwnerSet(msg.sender, owner); + } + + /// @inheritdoc IModule + function onUninstall(bytes calldata) external override { + SessionKeyValidatorStorage storage $ = _getStorage(); + + // Revoke all session keys + address[] storage keys = $.sessionKeyList[msg.sender]; + for (uint256 i = 0; i < keys.length; i++) { + address sessionKey = keys[i]; + delete $.sessionKeys[msg.sender][sessionKey]; + delete $.allowedTargets[msg.sender][sessionKey]; + delete $.allowedSelectors[msg.sender][sessionKey]; + } + delete $.sessionKeyList[msg.sender]; + delete $.sessionKeyCount[msg.sender]; + delete $.owners[msg.sender]; + } + + /// @inheritdoc IModule + function isModuleType(uint256 moduleTypeId) external pure override returns (bool) { + return moduleTypeId == MODULE_TYPE_VALIDATOR; + } + + /// @inheritdoc IModule + function isInitialized(address account) external view override returns (bool) { + return _getStorage().owners[account] != address(0); + } + + /*////////////////////////////////////////////////////////////// + IValidator INTERFACE + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IValidator + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) + external + override + returns (uint256) + { + SessionKeyValidatorStorage storage $ = _getStorage(); + address account = msg.sender; + + // Signature format: sessionKey (20 bytes) + ECDSA signature (65 bytes) + bytes calldata sig = userOp.signature; + if (sig.length != 85) return VALIDATION_FAILED; + + address sessionKey = address(bytes20(sig[:20])); + bytes calldata ecdsaSig = sig[20:85]; + + // Check session key exists and is active + SessionKeyData storage keyData = $.sessionKeys[account][sessionKey]; + if (!keyData.active) return VALIDATION_FAILED; + + // Check time bounds + if (block.timestamp < keyData.validAfter) return VALIDATION_FAILED; + if (block.timestamp > keyData.validUntil) return VALIDATION_FAILED; + + // Verify ECDSA signature + address recovered = userOpHash.toEthSignedMessageHash().recover(ecdsaSig); + if (recovered != sessionKey) return VALIDATION_FAILED; + + // Validate target and selector permissions + if (!_validatePermissions(account, sessionKey, userOp)) return VALIDATION_FAILED; + + // Validate and update spending limits + uint256 txValue = _extractValue(userOp); + if (!_validateAndUpdateSpending(account, sessionKey, txValue)) return VALIDATION_FAILED; + + return VALIDATION_SUCCESS; + } + + /// @inheritdoc IValidator + function isValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature) + external + view + override + returns (bytes4) + { + SessionKeyValidatorStorage storage $ = _getStorage(); + + // Signature format: sessionKey (20 bytes) + ECDSA signature (65 bytes) + if (signature.length != 85) return bytes4(0xffffffff); + + address sessionKey = address(bytes20(signature[:20])); + bytes calldata ecdsaSig = signature[20:85]; + + SessionKeyData storage keyData = $.sessionKeys[sender][sessionKey]; + if (!keyData.active) return bytes4(0xffffffff); + if (block.timestamp < keyData.validAfter) return bytes4(0xffffffff); + if (block.timestamp > keyData.validUntil) return bytes4(0xffffffff); + + address recovered = hash.toEthSignedMessageHash().recover(ecdsaSig); + if (recovered != sessionKey) return bytes4(0xffffffff); + + return bytes4(0x1626ba7e); // ERC1271_MAGIC_VALUE + } + + /*////////////////////////////////////////////////////////////// + SESSION KEY MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /// @notice Create a new session key with permissions + /// @param permission The session key permission structure + function createSessionKey(SessionKeyPermission calldata permission) external { + SessionKeyValidatorStorage storage $ = _getStorage(); + address account = msg.sender; + + if (permission.sessionKey == address(0)) revert InvalidSessionKey(); + if (permission.validUntil <= permission.validAfter) revert InvalidTimeRange(); + if ($.sessionKeys[account][permission.sessionKey].active) revert SessionKeyAlreadyExists(); + + // Store session key data + $.sessionKeys[account][permission.sessionKey] = SessionKeyData({ + active: true, + validAfter: permission.validAfter, + validUntil: permission.validUntil, + spendLimitPerTx: permission.spendLimitPerTx, + spendLimitTotal: permission.spendLimitTotal, + spentTotal: 0 + }); + + // Store allowed targets and selectors + if (permission.allowedTargets.length > 0) { + $.allowedTargets[account][permission.sessionKey] = permission.allowedTargets; + } + if (permission.allowedSelectors.length > 0) { + $.allowedSelectors[account][permission.sessionKey] = permission.allowedSelectors; + } + + // Add to list + $.sessionKeyList[account].push(permission.sessionKey); + $.sessionKeyCount[account]++; + + emit SessionKeyCreated( + account, + permission.sessionKey, + permission.validAfter, + permission.validUntil, + permission.spendLimitPerTx, + permission.spendLimitTotal + ); + } + + /// @notice Revoke a session key + /// @param sessionKey The session key address to revoke + function revokeSessionKey(address sessionKey) external { + SessionKeyValidatorStorage storage $ = _getStorage(); + address account = msg.sender; + + if (!$.sessionKeys[account][sessionKey].active) revert SessionKeyDoesNotExist(); + + // Deactivate (don't delete to preserve spent tracking) + $.sessionKeys[account][sessionKey].active = false; + + // Remove from list + address[] storage keys = $.sessionKeyList[account]; + for (uint256 i = 0; i < keys.length; i++) { + if (keys[i] == sessionKey) { + keys[i] = keys[keys.length - 1]; + keys.pop(); + break; + } + } + $.sessionKeyCount[account]--; + + // Clear permissions + delete $.allowedTargets[account][sessionKey]; + delete $.allowedSelectors[account][sessionKey]; + + emit SessionKeyRevoked(account, sessionKey); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Get owner of an account + function getOwner(address account) external view returns (address) { + return _getStorage().owners[account]; + } + + /// @notice Get session key data + function getSessionKey(address account, address sessionKey) + external + view + returns ( + bool active, + uint48 validAfter, + uint48 validUntil, + uint256 spendLimitPerTx, + uint256 spendLimitTotal, + uint256 spentTotal + ) + { + SessionKeyData storage data = _getStorage().sessionKeys[account][sessionKey]; + return + (data.active, data.validAfter, data.validUntil, data.spendLimitPerTx, data.spendLimitTotal, data.spentTotal); + } + + /// @notice Get session key count for an account + function getSessionKeyCount(address account) external view returns (uint256) { + return _getStorage().sessionKeyCount[account]; + } + + /// @notice Get all session keys for an account + function getSessionKeys(address account) external view returns (address[] memory) { + return _getStorage().sessionKeyList[account]; + } + + /// @notice Get allowed targets for a session key + function getAllowedTargets(address account, address sessionKey) external view returns (address[] memory) { + return _getStorage().allowedTargets[account][sessionKey]; + } + + /// @notice Get allowed selectors for a session key + function getAllowedSelectors(address account, address sessionKey) external view returns (bytes4[] memory) { + return _getStorage().allowedSelectors[account][sessionKey]; + } + + /// @notice Check if a session key is valid + function isSessionKeyValid(address account, address sessionKey) external view returns (bool) { + SessionKeyData storage data = _getStorage().sessionKeys[account][sessionKey]; + return data.active && block.timestamp >= data.validAfter && block.timestamp <= data.validUntil; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Validate target and selector permissions + function _validatePermissions(address account, address sessionKey, PackedUserOperation calldata userOp) + internal + view + returns (bool) + { + SessionKeyValidatorStorage storage $ = _getStorage(); + + // Extract target and selector from callData + // callData format: execute(mode, executionCalldata) where executionCalldata = (target, value, data) + if (userOp.callData.length < 4) return true; // No call to validate + + bytes4 executeSelector = bytes4(userOp.callData[:4]); + + // Only validate for execute calls + if (executeSelector != bytes4(keccak256("execute(bytes32,bytes)"))) return true; + + // Decode the execution calldata to get target + // Skip first 4 bytes (selector) + 32 bytes (mode) + 32 bytes (offset) + 32 bytes (length) + if (userOp.callData.length < 100) return true; + + // The executionCalldata contains: target (20 bytes) + value (32 bytes) + data + bytes calldata execData = userOp.callData[100:]; + if (execData.length < 52) return true; + + address target = address(bytes20(execData[:20])); + bytes4 selector = bytes4(0); + if (execData.length >= 56) { + // data starts at offset 52, selector is first 4 bytes + selector = bytes4(execData[52:56]); + } + + // Check allowed targets (empty = any target allowed) + address[] storage targets = $.allowedTargets[account][sessionKey]; + if (targets.length > 0) { + bool targetAllowed = false; + for (uint256 i = 0; i < targets.length; i++) { + if (targets[i] == target) { + targetAllowed = true; + break; + } + } + if (!targetAllowed) return false; + } + + // Check allowed selectors (empty = any selector allowed) + bytes4[] storage selectors = $.allowedSelectors[account][sessionKey]; + if (selectors.length > 0 && selector != bytes4(0)) { + bool selectorAllowed = false; + for (uint256 i = 0; i < selectors.length; i++) { + if (selectors[i] == selector) { + selectorAllowed = true; + break; + } + } + if (!selectorAllowed) return false; + } + + return true; + } + + /// @dev Validate and update spending limits + function _validateAndUpdateSpending(address account, address sessionKey, uint256 value) internal returns (bool) { + if (value == 0) return true; + + SessionKeyValidatorStorage storage $ = _getStorage(); + SessionKeyData storage keyData = $.sessionKeys[account][sessionKey]; + + // Check per-tx limit (0 = unlimited) + if (keyData.spendLimitPerTx > 0 && value > keyData.spendLimitPerTx) { + return false; + } + + // Check total limit (0 = unlimited) + if (keyData.spendLimitTotal > 0 && keyData.spentTotal + value > keyData.spendLimitTotal) { + return false; + } + + // Update spent amount + keyData.spentTotal += value; + emit SessionKeySpent(account, sessionKey, value); + + return true; + } + + /// @dev Extract value from UserOperation + function _extractValue(PackedUserOperation calldata userOp) internal pure returns (uint256) { + // callData format: execute(mode, executionCalldata) where executionCalldata = (target, value, data) + if (userOp.callData.length < 132) return 0; + + // Skip first 4 bytes (selector) + 32 bytes (mode) + 32 bytes (offset) + 32 bytes (length) + 20 bytes (target) + // Value is at offset 120 (20 bytes after target in executionCalldata) + bytes calldata execData = userOp.callData[100:]; + if (execData.length < 52) return 0; + + // Value is 32 bytes starting at offset 20 (after target address) + return uint256(bytes32(execData[20:52])); + } +} diff --git a/test/modular/SessionKeyValidatorModule.t.sol b/test/modular/SessionKeyValidatorModule.t.sol new file mode 100644 index 0000000..0b35b73 --- /dev/null +++ b/test/modular/SessionKeyValidatorModule.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; +import {SessionKeyValidatorModule} from "../../src/modular/modules/validators/SessionKeyValidatorModule.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; +import {MODULE_TYPE_VALIDATOR} from "@erc7579/interfaces/IERC7579Module.sol"; + +contract SessionKeyValidatorModuleTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + SessionKeyValidatorModule public validator; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + address owner = address(0x1234); + address sessionKey; + uint256 sessionKeyPrivateKey; + + function setUp() public { + // Deploy canonical ERC1967Factory if not already deployed + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + // Deploy factory + factory = new AuraAccountFactory(); + + // Deploy validator module + validator = new SessionKeyValidatorModule(); + + // Setup session key + sessionKeyPrivateKey = 0xBEEF; + sessionKey = vm.addr(sessionKeyPrivateKey); + + // Deploy account with validator + bytes memory validatorData = abi.encode(owner); + uint256 salt = 1; + account = AuraAccount( + payable(factory.createAccount( + owner, + address(validator), + validatorData, + address(0), // no hook + "", // no hook data + salt + )) + ); + vm.deal(address(account), 10 ether); + } + + function test_Initialize() public view { + assertTrue(validator.isInitialized(address(account))); + assertEq(validator.getOwner(address(account)), owner); + } + + function test_IsModuleType() public view { + assertTrue(validator.isModuleType(MODULE_TYPE_VALIDATOR)); + assertFalse(validator.isModuleType(2)); // Executor + } + + function test_CreateSessionKey() public { + // Create session key with permissions + SessionKeyValidatorModule.SessionKeyPermission memory permission = SessionKeyValidatorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 hours), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 1 ether, + spendLimitTotal: 5 ether + }); + + vm.prank(address(account)); + validator.createSessionKey(permission); + + ( + bool active, + uint48 validAfter, + uint48 validUntil, + uint256 limitPerTx, + uint256 limitTotal, + uint256 spentTotal + ) = validator.getSessionKey(address(account), sessionKey); + + assertTrue(active); + assertEq(validAfter, uint48(block.timestamp)); + assertEq(validUntil, uint48(block.timestamp + 1 hours)); + assertEq(limitPerTx, 1 ether); + assertEq(limitTotal, 5 ether); + assertEq(spentTotal, 0); + } + + function test_RevokeSessionKey() public { + // First create a session key + SessionKeyValidatorModule.SessionKeyPermission memory permission = SessionKeyValidatorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 hours), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 0, + spendLimitTotal: 0 + }); + + vm.prank(address(account)); + validator.createSessionKey(permission); + assertTrue(validator.isSessionKeyValid(address(account), sessionKey)); + + // Revoke it + vm.prank(address(account)); + validator.revokeSessionKey(sessionKey); + + (bool active,,,,,) = validator.getSessionKey(address(account), sessionKey); + assertFalse(active); + } + + function test_GetSessionKeys() public { + // Create multiple session keys + address sessionKey2 = address(0x5678); + + SessionKeyValidatorModule.SessionKeyPermission memory permission1 = + SessionKeyValidatorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 hours), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 0, + spendLimitTotal: 0 + }); + + SessionKeyValidatorModule.SessionKeyPermission memory permission2 = + SessionKeyValidatorModule.SessionKeyPermission({ + sessionKey: sessionKey2, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 2 hours), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 0, + spendLimitTotal: 0 + }); + + vm.startPrank(address(account)); + validator.createSessionKey(permission1); + validator.createSessionKey(permission2); + vm.stopPrank(); + + address[] memory keys = validator.getSessionKeys(address(account)); + assertEq(keys.length, 2); + assertEq(validator.getSessionKeyCount(address(account)), 2); + } + + function test_SessionKeyExpired() public { + SessionKeyValidatorModule.SessionKeyPermission memory permission = SessionKeyValidatorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 hours), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 0, + spendLimitTotal: 0 + }); + + vm.prank(address(account)); + validator.createSessionKey(permission); + + assertTrue(validator.isSessionKeyValid(address(account), sessionKey)); + + // Warp past expiry + vm.warp(block.timestamp + 2 hours); + assertFalse(validator.isSessionKeyValid(address(account), sessionKey)); + } +} + From d1792620c5985c4b310c5a55d77f7122d0cb21ad Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 17:31:55 +0700 Subject: [PATCH 05/22] docs: add SessionKeyValidatorModule, remove PasskeyManagerModule - Added SessionKeyValidatorModule documentation with storage, interface, use cases, and validation flow - Removed PasskeyManagerModule (passkey management is now built into P256MFAValidatorModule) - Updated P256MFAValidatorModule interface to show passkey management is called by account directly - Updated module hierarchy diagram --- docs/erc7579-module-architecture.md | 90 ++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/docs/erc7579-module-architecture.md b/docs/erc7579-module-architecture.md index dba84a6..8fedfe0 100644 --- a/docs/erc7579-module-architecture.md +++ b/docs/erc7579-module-architecture.md @@ -10,10 +10,10 @@ This document describes the modular architecture for migrating P256Account to ER P256ModularAccount (Core Account) ├── Validators (Type 1) │ ├── P256MFAValidatorModule - Owner (mandatory) + Passkey (when MFA enabled) +│ ├── SessionKeyValidatorModule - Session keys with time bounds, target restrictions, spending limits │ └── PQMFAValidatorModule - Dilithium + MFA (future: post-quantum) │ ├── Executors (Type 2) -│ ├── PasskeyManagerModule - Add/remove passkeys │ ├── SocialRecoveryModule - Guardian management + recovery with threshold + timelock │ ├── HookManagerModule - Install/uninstall user hooks │ └── LargeTransactionExecutorModule - Timelock for high-value txs (built-in, disabled by default) @@ -120,17 +120,17 @@ mapping(address account => bool) mfaEnabled; // MFA toggle **Interface:** ```solidity interface IP256MFAValidatorModule is IValidator { - // Passkey management (called by PasskeyManagerModule) - function addPasskey(address account, bytes32 qx, bytes32 qy, bytes32 deviceId) external; - function removePasskey(address account, bytes32 passkeyId) external; + // Passkey management (called by account) + function addPasskey(bytes32 qx, bytes32 qy, bytes32 deviceId) external; + function removePasskey(bytes32 passkeyId) external; // MFA management - function enableMFA(address account) external; - function disableMFA(address account) external; + function enableMFA() external; + function disableMFA() external; function isMFAEnabled(address account) external view returns (bool); - // Owner management - function setOwner(address account, address newOwner) external; + // Owner management (called by SocialRecoveryModule during recovery) + function setOwner(address newOwner) external; function getOwner(address account) external view returns (address); // View functions @@ -140,20 +140,82 @@ interface IP256MFAValidatorModule is IValidator { } ``` -## Executor Modules +### 2. SessionKeyValidatorModule (Type 1) + +**Purpose:** Validate session key signatures with granular permissions for gasless/automated transactions + +**Storage (per account):** +```solidity +struct SessionKeyPermission { + address sessionKey; // EOA that can sign + uint48 validAfter; // Start timestamp + uint48 validUntil; // Expiry timestamp + address[] allowedTargets; // Contracts it can call (empty = any) + bytes4[] allowedSelectors; // Functions it can call (empty = any) + uint256 spendLimitPerTx; // Max ETH per transaction (0 = unlimited) + uint256 spendLimitTotal; // Max ETH total (0 = unlimited) +} -### 2. PasskeyManagerModule (Type 2) +struct SessionKeyData { + bool active; + uint48 validAfter; + uint48 validUntil; + uint256 spendLimitPerTx; + uint256 spendLimitTotal; + uint256 spentTotal; +} -**Purpose:** Manage passkeys on P256ValidatorModule +mapping(address account => mapping(address sessionKey => SessionKeyData)) sessionKeys; +mapping(address account => address[]) sessionKeyList; +mapping(address account => mapping(address sessionKey => mapping(address target => bool))) allowedTargets; +mapping(address account => mapping(address sessionKey => mapping(bytes4 selector => bool))) allowedSelectors; +``` **Interface:** ```solidity -interface IPasskeyManagerModule is IExecutor { - function addPasskey(bytes32 qx, bytes32 qy, bytes32 deviceId) external; - function removePasskey(bytes32 passkeyId) external; +interface ISessionKeyValidatorModule is IValidator { + // Session key management (called by account) + function createSessionKey(SessionKeyPermission calldata permission) external; + function revokeSessionKey(address sessionKey) external; + + // View functions + function getSessionKey(address account, address sessionKey) + external view returns (bool active, uint48 validAfter, uint48 validUntil, + uint256 limitPerTx, uint256 limitTotal, uint256 spentTotal); + function getSessionKeys(address account) external view returns (address[] memory); + function getSessionKeyCount(address account) external view returns (uint256); + function isSessionKeyValid(address account, address sessionKey) external view returns (bool); + function isTargetAllowed(address account, address sessionKey, address target) external view returns (bool); + function isSelectorAllowed(address account, address sessionKey, bytes4 selector) external view returns (bool); } ``` +**Use Cases:** +- **Gaming:** Allow game backend to submit moves without user signing each one +- **Trading bots:** Automated trading within spending limits +- **Subscriptions:** Recurring payments to specific addresses +- **Batch operations:** DeFi automation with selector restrictions + +**Signature Format:** +``` +┌────────────────────┬─────────────────────────┐ +│ Session Key (20B) │ ECDSA Signature (65B) │ +└────────────────────┴─────────────────────────┘ +Total: 85 bytes +``` + +**Validation Flow:** +``` +1. Extract sessionKey address from signature prefix +2. Verify session key is active and within time bounds +3. Verify ECDSA signature from session key +4. Check target and selector permissions +5. Check and update spending limits +6. Return validation success +``` + +## Executor Modules + ### 3. SocialRecoveryModule (Type 2) **Purpose:** Guardian management + social recovery with **threshold** and **timelock** From e2096ee3109785057b5296196b3e28fcb09805d4 Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 18:09:52 +0700 Subject: [PATCH 06/22] feat: implement signature-based validator selection for AuraAccount - Updated AuraAccount.validateUserOp() to extract validator address from first 20 bytes of signature instead of using first validator in linked list - Updated AuraAccount.isValidSignature() (ERC-1271) to use same signature-based validator selection pattern - Updated P256MFAValidatorModule to skip first 20 bytes (validator prefix) in both validateUserOp() and isValidSignatureWithSender() - Updated SessionKeyValidatorModule signature format to 105 bytes: [validator(20B)][sessionKey(20B)][ecdsaSig(65B)] - Added comprehensive documentation in erc7579-module-architecture.md explaining signature-based validator selection and security benefits - Updated test_IsValidSignature() to use new signature format Security benefits over nonce-based selection: - Validator address is cryptographically bound to signature - Cannot be swapped by attacker after signing - Simpler frontend implementation (no nonce manipulation) - Critical installation check prevents malicious validator injection --- docs/erc7579-module-architecture.md | 39 ++++++++++++++++--- src/modular/AuraAccount.sol | 37 +++++++++++++----- .../validators/P256MFAValidatorModule.sol | 23 +++++++---- .../validators/SessionKeyValidatorModule.sol | 25 +++++++----- test/modular/AuraAccount.t.sol | 8 +++- 5 files changed, 100 insertions(+), 32 deletions(-) diff --git a/docs/erc7579-module-architecture.md b/docs/erc7579-module-architecture.md index 8fedfe0..ea1ef9a 100644 --- a/docs/erc7579-module-architecture.md +++ b/docs/erc7579-module-architecture.md @@ -67,7 +67,7 @@ Long-term: FullPQValidatorModule (Dilithium only, when ECDSA deprecated) - May require off-chain verification with on-chain proof - EIP-7212 equivalent for Dilithium precompile would help -## Core Account: P256ModularAccount +## Core Account: AuraAccount ### Responsibilities - ERC-4337 validateUserOp delegation to validators @@ -75,9 +75,41 @@ Long-term: FullPQValidatorModule (Dilithium only, when ECDSA deprecated) - Module installation/uninstallation - ERC-1271 signature validation forwarding +### Signature-Based Validator Selection + +AuraAccount uses **signature-based validator selection** instead of nonce-based selection. The validator address is encoded in the first 20 bytes of the signature: + +``` +┌──────────────────────┬─────────────────────────┐ +│ Validator Addr (20B) │ Actual Signature (var) │ +└──────────────────────┴─────────────────────────┘ +``` + +**Security Benefits:** +- Validator is cryptographically bound to the signature +- No frontend nonce encoding complexity +- Prevents validator injection attacks (must be installed) +- Prevents downgrade attacks (validator must verify its own signature format) + +**Validation Flow:** +``` +1. Extract validator address from signature[0:20] +2. Verify validator is installed (CRITICAL security check) +3. Pass full userOp to validator's validateUserOp() +4. Validator extracts its own signature format from signature[20:] +5. Return validation result +``` + +**Example Signatures:** + +| Validator | Signature Format | +|-----------|------------------| +| P256MFAValidatorModule | `[validator(20B)][ownerSig(65B)][passkeySig(~70B)]` | +| SessionKeyValidatorModule | `[validator(20B)][sessionKey(20B)][ecdsaSig(65B)]` | + ### Key Interfaces ```solidity -interface IP256ModularAccount is IERC7579Account { +interface IAuraAccount is IERC7579Account { // Account initialization function initialize( address defaultValidator, @@ -85,9 +117,6 @@ interface IP256ModularAccount is IERC7579Account { address hook, bytes calldata hookData ) external; - - // Validator selection (encoded in userOp.nonce) - function getActiveValidator() external view returns (address); } ``` diff --git a/src/modular/AuraAccount.sol b/src/modular/AuraAccount.sol index 8cd38c1..a3f1d42 100644 --- a/src/modular/AuraAccount.sol +++ b/src/modular/AuraAccount.sol @@ -163,6 +163,11 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { /** * @notice Validate a user operation (ERC-4337) + * @dev Uses signature-based validator selection: + * - First 20 bytes of signature = validator address + * - Remaining bytes = actual signature for the validator + * This approach prevents validator injection attacks since the validator + * must be installed and the signature format is validator-specific. * @param userOp The user operation * @param userOpHash The hash of the user operation * @param missingAccountFunds Funds to prefund @@ -173,13 +178,19 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { onlyEntryPoint returns (uint256 validationData) { - // Get the active validator (first in linked list for now) - address validator = _validators[SENTINEL]; - if (validator == SENTINEL || validator == address(0)) { - revert NoValidatorInstalled(); + // Extract validator address from signature prefix (first 20 bytes) + if (userOp.signature.length < 20) { + revert InvalidValidator(); } + address validator = address(bytes20(userOp.signature[0:20])); - // Delegate validation to the validator module + // CRITICAL: Verify validator is installed to prevent malicious validator injection + if (_validators[validator] == address(0)) { + revert InvalidValidator(); + } + + // Delegate validation to the selected validator module + // The validator is responsible for extracting its own signature format from signature[20:] validationData = IValidator(validator).validateUserOp(userOp, userOpHash); // Pay prefund @@ -495,14 +506,22 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { //////////////////////////////////////////////////////////////*/ /// @inheritdoc IERC7579Account + /// @dev Uses signature-based validator selection (same as validateUserOp): + /// - First 20 bytes = validator address + /// - Remaining bytes = actual signature for the validator function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { - // Get the active validator - address validator = _validators[SENTINEL]; - if (validator == SENTINEL || validator == address(0)) { + // Extract validator from signature prefix + if (signature.length < 20) { + return bytes4(0xffffffff); + } + address validator = address(bytes20(signature[0:20])); + + // Verify validator is installed + if (_validators[validator] == address(0)) { return bytes4(0xffffffff); } - // Delegate to validator + // Delegate to validator (validator extracts its signature format from signature[20:]) return IValidator(validator).isValidSignatureWithSender(msg.sender, hash, signature); } diff --git a/src/modular/modules/validators/P256MFAValidatorModule.sol b/src/modular/modules/validators/P256MFAValidatorModule.sol index 9604a5d..51d51d9 100644 --- a/src/modular/modules/validators/P256MFAValidatorModule.sol +++ b/src/modular/modules/validators/P256MFAValidatorModule.sol @@ -173,7 +173,10 @@ contract P256MFAValidatorModule is IValidator { address owner = $.owners[account]; bool mfaEnabled = $.mfaEnabled[account]; - bytes calldata sig = userOp.signature; + // Signature format: [validator(20B)][actualSignature] + // Skip first 20 bytes (validator address) - already validated by AuraAccount + if (userOp.signature.length < 20) return VALIDATION_FAILED; + bytes calldata sig = userOp.signature[20:]; // Owner-only mode: no MFA required if (!mfaEnabled) { @@ -220,12 +223,18 @@ contract P256MFAValidatorModule is IValidator { } /// @inheritdoc IValidator + /// @dev Signature format: [validator(20B)][actualSignature] + /// Skip first 20 bytes (validator address) - already validated by AuraAccount function isValidSignatureWithSender(address, bytes32 hash, bytes calldata signature) external view override returns (bytes4) { + // Skip first 20 bytes (validator address) + if (signature.length < 20) return bytes4(0xffffffff); + bytes calldata sig = signature[20:]; + P256MFAValidatorStorage storage $ = _getStorage(); address account = msg.sender; address owner = $.owners[account]; @@ -233,16 +242,16 @@ contract P256MFAValidatorModule is IValidator { // Owner-only mode if (!mfaEnabled) { - if (signature.length != 65) return bytes4(0xffffffff); - return _verifyOwnerSignature(hash, signature, owner) ? ERC1271_MAGIC_VALUE : bytes4(0xffffffff); + if (sig.length != 65) return bytes4(0xffffffff); + return _verifyOwnerSignature(hash, sig, owner) ? ERC1271_MAGIC_VALUE : bytes4(0xffffffff); } // MFA mode: same format as validateUserOp - if (signature.length < 224) return bytes4(0xffffffff); + if (sig.length < 224) return bytes4(0xffffffff); - bytes calldata ownerSig = signature[signature.length - 65:]; - bytes32 passkeyId = bytes32(signature[signature.length - 97:signature.length - 65]); - bytes calldata webAuthnSig = signature[:signature.length - 97]; + bytes calldata ownerSig = sig[sig.length - 65:]; + bytes32 passkeyId = bytes32(sig[sig.length - 97:sig.length - 65]); + bytes calldata webAuthnSig = sig[:sig.length - 97]; PasskeyInfo storage passkeyInfo = $.passkeys[account][passkeyId]; if (!passkeyInfo.active || passkeyInfo.qx == bytes32(0)) return bytes4(0xffffffff); diff --git a/src/modular/modules/validators/SessionKeyValidatorModule.sol b/src/modular/modules/validators/SessionKeyValidatorModule.sol index 5bb54ef..a958c98 100644 --- a/src/modular/modules/validators/SessionKeyValidatorModule.sol +++ b/src/modular/modules/validators/SessionKeyValidatorModule.sol @@ -163,6 +163,10 @@ contract SessionKeyValidatorModule is IValidator { //////////////////////////////////////////////////////////////*/ /// @inheritdoc IValidator + /// @dev Signature format: [validator(20B)][sessionKey(20B)][ecdsaSig(65B)] + /// - First 20 bytes: validator address (already validated by AuraAccount) + /// - Next 20 bytes: session key address + /// - Last 65 bytes: ECDSA signature from session key function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external override @@ -171,12 +175,13 @@ contract SessionKeyValidatorModule is IValidator { SessionKeyValidatorStorage storage $ = _getStorage(); address account = msg.sender; - // Signature format: sessionKey (20 bytes) + ECDSA signature (65 bytes) - bytes calldata sig = userOp.signature; - if (sig.length != 85) return VALIDATION_FAILED; + // Signature format: [validator(20B)][sessionKey(20B)][ecdsaSig(65B)] = 105 bytes + bytes calldata fullSig = userOp.signature; + if (fullSig.length != 105) return VALIDATION_FAILED; - address sessionKey = address(bytes20(sig[:20])); - bytes calldata ecdsaSig = sig[20:85]; + // Skip first 20 bytes (validator address) + address sessionKey = address(bytes20(fullSig[20:40])); + bytes calldata ecdsaSig = fullSig[40:105]; // Check session key exists and is active SessionKeyData storage keyData = $.sessionKeys[account][sessionKey]; @@ -201,6 +206,7 @@ contract SessionKeyValidatorModule is IValidator { } /// @inheritdoc IValidator + /// @dev Signature format: [validator(20B)][sessionKey(20B)][ecdsaSig(65B)] = 105 bytes function isValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature) external view @@ -209,11 +215,12 @@ contract SessionKeyValidatorModule is IValidator { { SessionKeyValidatorStorage storage $ = _getStorage(); - // Signature format: sessionKey (20 bytes) + ECDSA signature (65 bytes) - if (signature.length != 85) return bytes4(0xffffffff); + // Signature format: [validator(20B)][sessionKey(20B)][ecdsaSig(65B)] = 105 bytes + if (signature.length != 105) return bytes4(0xffffffff); - address sessionKey = address(bytes20(signature[:20])); - bytes calldata ecdsaSig = signature[20:85]; + // Skip first 20 bytes (validator address) + address sessionKey = address(bytes20(signature[20:40])); + bytes calldata ecdsaSig = signature[40:105]; SessionKeyData storage keyData = $.sessionKeys[sender][sessionKey]; if (!keyData.active) return bytes4(0xffffffff); diff --git a/test/modular/AuraAccount.t.sol b/test/modular/AuraAccount.t.sol index 9d41b52..0c6bdff 100644 --- a/test/modular/AuraAccount.t.sol +++ b/test/modular/AuraAccount.t.sol @@ -286,7 +286,9 @@ contract AuraAccountTest is Test { function test_IsValidSignature() public view { bytes32 hash = keccak256("test"); - bytes4 result = account.isValidSignature(hash, ""); + // Signature format: [validator(20B)][actualSignature] + bytes memory signature = abi.encodePacked(address(validator)); + bytes4 result = account.isValidSignature(hash, signature); assertEq(result, bytes4(0x1626ba7e)); } @@ -294,7 +296,9 @@ contract AuraAccountTest is Test { validator.setValidation(address(account), false); bytes32 hash = keccak256("test"); - bytes4 result = account.isValidSignature(hash, ""); + // Signature format: [validator(20B)][actualSignature] + bytes memory signature = abi.encodePacked(address(validator)); + bytes4 result = account.isValidSignature(hash, signature); assertEq(result, bytes4(0xffffffff)); } From 6800870d32a39f07a4652f4e716240a4318cd96e Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 18:17:07 +0700 Subject: [PATCH 07/22] security: prevent uninstalling last validator (account lock protection) - Added CannotRemoveLastValidator error - Added check in _uninstallValidator() to prevent removing the last validator - Without this protection, users could permanently lock their account by accidentally uninstalling all validators - Added test_RevertUninstallLastValidator() to verify the protection - Added test_UninstallValidatorWithMultiple() to verify normal uninstall works --- src/modular/AuraAccount.sol | 6 ++++++ test/modular/AuraAccount.t.sol | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/modular/AuraAccount.sol b/src/modular/AuraAccount.sol index a3f1d42..53a587e 100644 --- a/src/modular/AuraAccount.sol +++ b/src/modular/AuraAccount.sol @@ -92,6 +92,7 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { error ExecutionFailed(); error InvalidValidator(); error NoValidatorInstalled(); + error CannotRemoveLastValidator(); /*////////////////////////////////////////////////////////////// MODIFIERS @@ -407,6 +408,11 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { revert ModuleNotInstalled(validator); } + // CRITICAL: Prevent uninstalling the last validator (would permanently lock account) + if (_validatorCount == 1) { + revert CannotRemoveLastValidator(); + } + // Find previous in linked list address prev = SENTINEL; address current = _validators[SENTINEL]; diff --git a/test/modular/AuraAccount.t.sol b/test/modular/AuraAccount.t.sol index 0c6bdff..815c0bb 100644 --- a/test/modular/AuraAccount.t.sol +++ b/test/modular/AuraAccount.t.sol @@ -280,6 +280,36 @@ contract AuraAccountTest is Test { account.uninstallModule(MODULE_TYPE_EXECUTOR, address(executor), ""); } + function test_RevertUninstallLastValidator() public { + // Account has only one validator installed (from setUp) + assertEq(account.getValidatorCount(), 1); + + // Trying to uninstall the last validator should revert + vm.prank(ENTRYPOINT); + vm.expectRevert(AuraAccount.CannotRemoveLastValidator.selector); + account.uninstallModule(MODULE_TYPE_VALIDATOR, address(validator), ""); + + // Validator should still be installed + assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); + } + + function test_UninstallValidatorWithMultiple() public { + // Install a second validator + MockValidator validator2 = new MockValidator(); + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_VALIDATOR, address(validator2), abi.encode(true)); + assertEq(account.getValidatorCount(), 2); + + // Now we can uninstall one validator (not the last) + vm.prank(ENTRYPOINT); + account.uninstallModule(MODULE_TYPE_VALIDATOR, address(validator2), ""); + + // Should have one validator remaining + assertEq(account.getValidatorCount(), 1); + assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); + assertFalse(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator2), "")); + } + /*////////////////////////////////////////////////////////////// ERC-1271 TESTS //////////////////////////////////////////////////////////////*/ From 7f8ac9f507a3b656f178fd551cce85fd57be169d Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 18:26:13 +0700 Subject: [PATCH 08/22] security: prevent uninstalling SENTINEL (linked list marker) - Added check in uninstallModule() to reject SENTINEL address (0x1) - SENTINEL is a linked list implementation detail, not a real module - Uninstalling SENTINEL would corrupt the validator linked list - Added test_RevertUninstallSentinel() to verify the protection --- src/modular/AuraAccount.sol | 3 ++- test/modular/AuraAccount.t.sol | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/modular/AuraAccount.sol b/src/modular/AuraAccount.sol index 53a587e..0437644 100644 --- a/src/modular/AuraAccount.sol +++ b/src/modular/AuraAccount.sol @@ -368,7 +368,8 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { payable onlyEntryPointOrSelf { - if (module == address(0)) revert InvalidModule(module); + // Prevent uninstalling address(0) or SENTINEL (linked list marker) + if (module == address(0) || module == SENTINEL) revert InvalidModule(module); if (moduleTypeId == MODULE_TYPE_VALIDATOR) { _uninstallValidator(module, deInitData); diff --git a/test/modular/AuraAccount.t.sol b/test/modular/AuraAccount.t.sol index 815c0bb..ebf96df 100644 --- a/test/modular/AuraAccount.t.sol +++ b/test/modular/AuraAccount.t.sol @@ -310,6 +310,16 @@ contract AuraAccountTest is Test { assertFalse(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator2), "")); } + function test_RevertUninstallSentinel() public { + // SENTINEL = address(0x1) is the linked list marker + // Attempting to uninstall it should revert to prevent list corruption + address SENTINEL = address(0x1); + + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.InvalidModule.selector, SENTINEL)); + account.uninstallModule(MODULE_TYPE_VALIDATOR, SENTINEL, ""); + } + /*////////////////////////////////////////////////////////////// ERC-1271 TESTS //////////////////////////////////////////////////////////////*/ From 823365bf2342a1c850ca3055a7906db4bd8df0c9 Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 18:39:13 +0700 Subject: [PATCH 09/22] refactor: convert SessionKeyValidatorModule to SessionKeyExecutorModule BREAKING CHANGE: SessionKey is now an Executor module (Type 2) instead of Validator (Type 1) Why this change: - Session keys have temporal validity (expire after validUntil) - If SessionKey was a Validator and user removed P256MFAValidator, the account would be permanently locked after session keys expire - As an Executor, P256MFAValidator remains the only validator, ensuring the 'last validator' protection works correctly - Cleaner architecture: session keys are 'delegated execution' not 'alternative authentication' Changes: - Deleted src/modular/modules/validators/SessionKeyValidatorModule.sol - Created src/modular/modules/executors/SessionKeyExecutorModule.sol - Added nonce-based replay protection (since we can't rely on EntryPoint nonces) - Added chainId to signature for cross-chain replay protection - Updated docs/erc7579-module-architecture.md to reflect the change --- docs/erc7579-module-architecture.md | 60 ++- .../SessionKeyExecutorModule.sol} | 351 ++++++++---------- 2 files changed, 206 insertions(+), 205 deletions(-) rename src/modular/modules/{validators/SessionKeyValidatorModule.sol => executors/SessionKeyExecutorModule.sol} (52%) diff --git a/docs/erc7579-module-architecture.md b/docs/erc7579-module-architecture.md index ea1ef9a..c569c88 100644 --- a/docs/erc7579-module-architecture.md +++ b/docs/erc7579-module-architecture.md @@ -10,12 +10,12 @@ This document describes the modular architecture for migrating P256Account to ER P256ModularAccount (Core Account) ├── Validators (Type 1) │ ├── P256MFAValidatorModule - Owner (mandatory) + Passkey (when MFA enabled) -│ ├── SessionKeyValidatorModule - Session keys with time bounds, target restrictions, spending limits │ └── PQMFAValidatorModule - Dilithium + MFA (future: post-quantum) │ ├── Executors (Type 2) │ ├── SocialRecoveryModule - Guardian management + recovery with threshold + timelock │ ├── HookManagerModule - Install/uninstall user hooks +│ ├── SessionKeyExecutorModule - Session keys with time bounds, target restrictions, spending limits │ └── LargeTransactionExecutorModule - Timelock for high-value txs (built-in, disabled by default) │ ├── Fallback (Type 3) @@ -28,6 +28,12 @@ P256ModularAccount (Core Account) └── User-installed hooks (WhitelistHook, SpendingLimitHook, etc.) ``` +**Why SessionKeyExecutorModule is an Executor (not Validator):** +- Session keys have temporal validity (expire after `validUntil`) +- If SessionKey was a Validator and user removed P256MFAValidator, the account would be permanently locked after session keys expire +- As an Executor, P256MFAValidator remains the only validator, ensuring the "last validator" protection works correctly +- Cleaner architecture: session keys are "delegated execution" not "alternative authentication" + **Key Design Decisions:** - No separate ECDSAValidatorModule - recovery handled by SocialRecoveryModule (Executor) - MultiHook allows users to install 3rd party hooks for custom execution checks @@ -105,7 +111,8 @@ AuraAccount uses **signature-based validator selection** instead of nonce-based | Validator | Signature Format | |-----------|------------------| | P256MFAValidatorModule | `[validator(20B)][ownerSig(65B)][passkeySig(~70B)]` | -| SessionKeyValidatorModule | `[validator(20B)][sessionKey(20B)][ecdsaSig(65B)]` | + +Note: SessionKeyExecutorModule is an Executor (not Validator), so it doesn't use the signature-based validator selection. Instead, it validates session key signatures internally when `executeWithSessionKey()` is called. ### Key Interfaces ```solidity @@ -169,9 +176,17 @@ interface IP256MFAValidatorModule is IValidator { } ``` -### 2. SessionKeyValidatorModule (Type 1) +## Executor Modules + +### 2. SessionKeyExecutorModule (Type 2) + +**Purpose:** Execute transactions on behalf of the account using session keys with granular permissions for gasless/automated transactions -**Purpose:** Validate session key signatures with granular permissions for gasless/automated transactions +**Why Executor (not Validator):** +- Session keys have temporal validity (expire after `validUntil`) +- If SessionKey was a Validator and user removed P256MFAValidator, the account would be permanently locked after session keys expire +- As an Executor, P256MFAValidator remains the only validator, ensuring the "last validator" protection works correctly +- Cleaner architecture: session keys are "delegated execution" not "alternative authentication" **Storage (per account):** ```solidity @@ -198,15 +213,27 @@ mapping(address account => mapping(address sessionKey => SessionKeyData)) sessio mapping(address account => address[]) sessionKeyList; mapping(address account => mapping(address sessionKey => mapping(address target => bool))) allowedTargets; mapping(address account => mapping(address sessionKey => mapping(bytes4 selector => bool))) allowedSelectors; +mapping(address account => mapping(address sessionKey => uint256)) nonces; // Replay protection ``` **Interface:** ```solidity -interface ISessionKeyValidatorModule is IValidator { +interface ISessionKeyExecutorModule is IExecutor { // Session key management (called by account) function createSessionKey(SessionKeyPermission calldata permission) external; function revokeSessionKey(address sessionKey) external; + // Execution (called by anyone with valid session key signature) + function executeWithSessionKey( + address account, + address sessionKey, + address target, + uint256 value, + bytes calldata data, + uint256 nonce, + bytes calldata signature + ) external returns (bytes memory); + // View functions function getSessionKey(address account, address sessionKey) external view returns (bool active, uint48 validAfter, uint48 validUntil, @@ -216,6 +243,7 @@ interface ISessionKeyValidatorModule is IValidator { function isSessionKeyValid(address account, address sessionKey) external view returns (bool); function isTargetAllowed(address account, address sessionKey, address target) external view returns (bool); function isSelectorAllowed(address account, address sessionKey, bytes4 selector) external view returns (bool); + function getNonce(address account, address sessionKey) external view returns (uint256); } ``` @@ -227,24 +255,22 @@ interface ISessionKeyValidatorModule is IValidator { **Signature Format:** ``` -┌────────────────────┬─────────────────────────┐ -│ Session Key (20B) │ ECDSA Signature (65B) │ -└────────────────────┴─────────────────────────┘ -Total: 85 bytes +Message: keccak256(account, target, value, keccak256(data), nonce, chainId) +Signature: ECDSA signature from session key (65 bytes) ``` -**Validation Flow:** +**Execution Flow:** ``` -1. Extract sessionKey address from signature prefix +1. Anyone calls executeWithSessionKey() with session key signature 2. Verify session key is active and within time bounds -3. Verify ECDSA signature from session key -4. Check target and selector permissions -5. Check and update spending limits -6. Return validation success +3. Verify nonce matches expected value (replay protection) +4. Verify ECDSA signature from session key over (account, target, value, data, nonce, chainId) +5. Check target and selector permissions +6. Check and update spending limits +7. Increment nonce +8. Call account.executeFromExecutor() to execute the transaction ``` -## Executor Modules - ### 3. SocialRecoveryModule (Type 2) **Purpose:** Guardian management + social recovery with **threshold** and **timelock** diff --git a/src/modular/modules/validators/SessionKeyValidatorModule.sol b/src/modular/modules/executors/SessionKeyExecutorModule.sol similarity index 52% rename from src/modular/modules/validators/SessionKeyValidatorModule.sol rename to src/modular/modules/executors/SessionKeyExecutorModule.sol index a958c98..ddf75c0 100644 --- a/src/modular/modules/validators/SessionKeyValidatorModule.sol +++ b/src/modular/modules/executors/SessionKeyExecutorModule.sol @@ -1,26 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; -import { - IModule, - IValidator, - MODULE_TYPE_VALIDATOR, - VALIDATION_SUCCESS, - VALIDATION_FAILED -} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IModule, IExecutor, MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {IERC7579Account} from "@erc7579/interfaces/IERC7579Account.sol"; +import {ModeLib, ModeCode} from "@erc7579/lib/ModeLib.sol"; import {ECDSA} from "solady/utils/ECDSA.sol"; /** - * @title SessionKeyValidatorModule - * @notice ERC-7579 Validator Module for Session Keys with permissions and spending limits - * @dev Enables gasless, signature-less transactions for dApps (gaming, trading bots, etc.) + * @title SessionKeyExecutorModule + * @notice ERC-7579 Executor Module for Session Keys with permissions and spending limits + * @dev Enables delegated execution for dApps (gaming, trading bots, etc.) * - Session keys are EOAs with limited permissions * - Supports time-bounded sessions (validAfter, validUntil) * - Supports target/selector whitelisting * - Supports per-tx and total spending limits + * - Execution via executeFromExecutor (not validateUserOp) */ -contract SessionKeyValidatorModule is IValidator { +contract SessionKeyExecutorModule is IExecutor { using ECDSA for bytes32; /*////////////////////////////////////////////////////////////// @@ -29,13 +25,13 @@ contract SessionKeyValidatorModule is IValidator { /// @notice Session key permission structure struct SessionKeyPermission { - address sessionKey; // EOA that can sign - uint48 validAfter; // Start timestamp - uint48 validUntil; // Expiry timestamp - address[] allowedTargets; // Contracts it can call (empty = any) - bytes4[] allowedSelectors; // Functions it can call (empty = any) - uint256 spendLimitPerTx; // Max ETH per transaction (0 = unlimited) - uint256 spendLimitTotal; // Max ETH total (0 = unlimited) + address sessionKey; // EOA that can sign + uint48 validAfter; // Start timestamp + uint48 validUntil; // Expiry timestamp + address[] allowedTargets; // Contracts it can call (empty = any) + bytes4[] allowedSelectors; // Functions it can call (empty = any) + uint256 spendLimitPerTx; // Max ETH per transaction (0 = unlimited) + uint256 spendLimitTotal; // Max ETH total (0 = unlimited) } /// @notice Internal storage for a session key @@ -46,15 +42,15 @@ contract SessionKeyValidatorModule is IValidator { uint256 spendLimitPerTx; uint256 spendLimitTotal; uint256 spentTotal; - // Stored separately for gas efficiency + uint256 nonce; // Replay protection } /*////////////////////////////////////////////////////////////// ERC-7201 STORAGE //////////////////////////////////////////////////////////////*/ - /// @custom:storage-location erc7201:ethaura.storage.SessionKeyValidatorModule - struct SessionKeyValidatorStorage { + /// @custom:storage-location erc7201:ethaura.storage.SessionKeyExecutorModule + struct SessionKeyExecutorStorage { // Per-account session key storage mapping(address account => mapping(address sessionKey => SessionKeyData)) sessionKeys; mapping(address account => address[]) sessionKeyList; @@ -62,18 +58,15 @@ contract SessionKeyValidatorModule is IValidator { // Target/selector permissions stored separately mapping(address account => mapping(address sessionKey => address[])) allowedTargets; mapping(address account => mapping(address sessionKey => bytes4[])) allowedSelectors; - // Account owner (can manage session keys) - mapping(address account => address) owners; } - // keccak256(abi.encode(uint256(keccak256("ethaura.storage.SessionKeyValidatorModule")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant STORAGE_LOCATION = 0x96ac80b66f1fc01632bfcc8f443eba4e1a9afd53fde13b3cf2ab8d3626e18300; + // keccak256(abi.encode(uint256(keccak256("ethaura.storage.SessionKeyExecutorModule")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant STORAGE_LOCATION = 0x7f8c9a3e5d1b2c4a6e8f0d2c4b6a8e0f2d4c6b8a0e2f4d6c8b0a2e4f6d8c0b00; /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - event OwnerSet(address indexed account, address indexed owner); event SessionKeyCreated( address indexed account, address indexed sessionKey, @@ -84,13 +77,18 @@ contract SessionKeyValidatorModule is IValidator { ); event SessionKeyRevoked(address indexed account, address indexed sessionKey); event SessionKeySpent(address indexed account, address indexed sessionKey, uint256 amount); + event SessionKeyExecuted( + address indexed account, + address indexed sessionKey, + address target, + uint256 value, + bytes4 selector + ); /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ - error OnlyOwner(); - error InvalidOwner(); error InvalidSessionKey(); error SessionKeyAlreadyExists(); error SessionKeyDoesNotExist(); @@ -103,12 +101,14 @@ contract SessionKeyValidatorModule is IValidator { error SpendLimitTotalExceeded(); error InvalidSignature(); error InvalidTimeRange(); + error InvalidNonce(); + error SelfCallNotAllowed(); /*////////////////////////////////////////////////////////////// STORAGE ACCESS //////////////////////////////////////////////////////////////*/ - function _getStorage() internal pure returns (SessionKeyValidatorStorage storage $) { + function _getStorage() internal pure returns (SessionKeyExecutorStorage storage $) { bytes32 location = STORAGE_LOCATION; assembly { $.slot := location @@ -120,20 +120,13 @@ contract SessionKeyValidatorModule is IValidator { //////////////////////////////////////////////////////////////*/ /// @inheritdoc IModule - function onInstall(bytes calldata data) external override { - SessionKeyValidatorStorage storage $ = _getStorage(); - - // Decode: owner - address owner = abi.decode(data, (address)); - if (owner == address(0)) revert InvalidOwner(); - - $.owners[msg.sender] = owner; - emit OwnerSet(msg.sender, owner); + function onInstall(bytes calldata) external override { + // No initialization needed - session keys are created separately } /// @inheritdoc IModule function onUninstall(bytes calldata) external override { - SessionKeyValidatorStorage storage $ = _getStorage(); + SessionKeyExecutorStorage storage $ = _getStorage(); // Revoke all session keys address[] storage keys = $.sessionKeyList[msg.sender]; @@ -145,102 +138,116 @@ contract SessionKeyValidatorModule is IValidator { } delete $.sessionKeyList[msg.sender]; delete $.sessionKeyCount[msg.sender]; - delete $.owners[msg.sender]; } /// @inheritdoc IModule function isModuleType(uint256 moduleTypeId) external pure override returns (bool) { - return moduleTypeId == MODULE_TYPE_VALIDATOR; + return moduleTypeId == MODULE_TYPE_EXECUTOR; } /// @inheritdoc IModule - function isInitialized(address account) external view override returns (bool) { - return _getStorage().owners[account] != address(0); + function isInitialized(address) external pure override returns (bool) { + // Module is initialized if installed (checked by account) + return true; } /*////////////////////////////////////////////////////////////// - IValidator INTERFACE + SESSION KEY EXECUTION //////////////////////////////////////////////////////////////*/ - /// @inheritdoc IValidator - /// @dev Signature format: [validator(20B)][sessionKey(20B)][ecdsaSig(65B)] - /// - First 20 bytes: validator address (already validated by AuraAccount) - /// - Next 20 bytes: session key address - /// - Last 65 bytes: ECDSA signature from session key - function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) - external - override - returns (uint256) - { - SessionKeyValidatorStorage storage $ = _getStorage(); - address account = msg.sender; - - // Signature format: [validator(20B)][sessionKey(20B)][ecdsaSig(65B)] = 105 bytes - bytes calldata fullSig = userOp.signature; - if (fullSig.length != 105) return VALIDATION_FAILED; - - // Skip first 20 bytes (validator address) - address sessionKey = address(bytes20(fullSig[20:40])); - bytes calldata ecdsaSig = fullSig[40:105]; + /** + * @notice Execute a transaction using a session key + * @dev Anyone can call this (relayer pattern), but signature must be valid + * @param account The account to execute from + * @param sessionKey The session key that signed the request + * @param target The target contract to call + * @param value The ETH value to send + * @param data The calldata to send + * @param nonce The session key nonce (for replay protection) + * @param signature The ECDSA signature from the session key + */ + function executeWithSessionKey( + address account, + address sessionKey, + address target, + uint256 value, + bytes calldata data, + uint256 nonce, + bytes calldata signature + ) external returns (bytes memory) { + SessionKeyExecutorStorage storage $ = _getStorage(); + + // Prevent self-calls (could be used to bypass permissions) + if (target == account) revert SelfCallNotAllowed(); // Check session key exists and is active SessionKeyData storage keyData = $.sessionKeys[account][sessionKey]; - if (!keyData.active) return VALIDATION_FAILED; + if (!keyData.active) revert SessionKeyNotActive(); // Check time bounds - if (block.timestamp < keyData.validAfter) return VALIDATION_FAILED; - if (block.timestamp > keyData.validUntil) return VALIDATION_FAILED; + if (block.timestamp < keyData.validAfter) revert SessionKeyNotYetValid(); + if (block.timestamp > keyData.validUntil) revert SessionKeyExpired(); + + // Check nonce + if (nonce != keyData.nonce) revert InvalidNonce(); + + // Verify signature + bytes32 messageHash = keccak256( + abi.encodePacked( + account, + target, + value, + keccak256(data), + nonce, + block.chainid + ) + ); + address recovered = messageHash.toEthSignedMessageHash().recover(signature); + if (recovered != sessionKey) revert InvalidSignature(); - // Verify ECDSA signature - address recovered = userOpHash.toEthSignedMessageHash().recover(ecdsaSig); - if (recovered != sessionKey) return VALIDATION_FAILED; + // Validate target permissions + _validateTarget(account, sessionKey, target); - // Validate target and selector permissions - if (!_validatePermissions(account, sessionKey, userOp)) return VALIDATION_FAILED; + // Validate selector permissions + if (data.length >= 4) { + _validateSelector(account, sessionKey, bytes4(data[:4])); + } // Validate and update spending limits - uint256 txValue = _extractValue(userOp); - if (!_validateAndUpdateSpending(account, sessionKey, txValue)) return VALIDATION_FAILED; + _validateAndUpdateSpending(account, sessionKey, value); - return VALIDATION_SUCCESS; - } - - /// @inheritdoc IValidator - /// @dev Signature format: [validator(20B)][sessionKey(20B)][ecdsaSig(65B)] = 105 bytes - function isValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature) - external - view - override - returns (bytes4) - { - SessionKeyValidatorStorage storage $ = _getStorage(); - - // Signature format: [validator(20B)][sessionKey(20B)][ecdsaSig(65B)] = 105 bytes - if (signature.length != 105) return bytes4(0xffffffff); + // Increment nonce + ++keyData.nonce; - // Skip first 20 bytes (validator address) - address sessionKey = address(bytes20(signature[20:40])); - bytes calldata ecdsaSig = signature[40:105]; - - SessionKeyData storage keyData = $.sessionKeys[sender][sessionKey]; - if (!keyData.active) return bytes4(0xffffffff); - if (block.timestamp < keyData.validAfter) return bytes4(0xffffffff); - if (block.timestamp > keyData.validUntil) return bytes4(0xffffffff); + // Execute via the account + bytes[] memory results = IERC7579Account(account).executeFromExecutor( + _encodeExecutionMode(), + abi.encodePacked(target, value, data) + ); - address recovered = hash.toEthSignedMessageHash().recover(ecdsaSig); - if (recovered != sessionKey) return bytes4(0xffffffff); + emit SessionKeyExecuted( + account, + sessionKey, + target, + value, + data.length >= 4 ? bytes4(data[:4]) : bytes4(0) + ); - return bytes4(0x1626ba7e); // ERC1271_MAGIC_VALUE + // Return first result (single execution) + return results.length > 0 ? results[0] : bytes(""); } /*////////////////////////////////////////////////////////////// SESSION KEY MANAGEMENT //////////////////////////////////////////////////////////////*/ - /// @notice Create a new session key with permissions - /// @param permission The session key permission structure + /** + * @notice Create a new session key with permissions + * @dev Only the account can call this (via execute) + * @param permission The session key permission structure + */ function createSessionKey(SessionKeyPermission calldata permission) external { - SessionKeyValidatorStorage storage $ = _getStorage(); + SessionKeyExecutorStorage storage $ = _getStorage(); address account = msg.sender; if (permission.sessionKey == address(0)) revert InvalidSessionKey(); @@ -254,7 +261,8 @@ contract SessionKeyValidatorModule is IValidator { validUntil: permission.validUntil, spendLimitPerTx: permission.spendLimitPerTx, spendLimitTotal: permission.spendLimitTotal, - spentTotal: 0 + spentTotal: 0, + nonce: 0 }); // Store allowed targets and selectors @@ -279,10 +287,13 @@ contract SessionKeyValidatorModule is IValidator { ); } - /// @notice Revoke a session key - /// @param sessionKey The session key address to revoke + /** + * @notice Revoke a session key + * @dev Only the account can call this (via execute) + * @param sessionKey The session key address to revoke + */ function revokeSessionKey(address sessionKey) external { - SessionKeyValidatorStorage storage $ = _getStorage(); + SessionKeyExecutorStorage storage $ = _getStorage(); address account = msg.sender; if (!$.sessionKeys[account][sessionKey].active) revert SessionKeyDoesNotExist(); @@ -312,11 +323,6 @@ contract SessionKeyValidatorModule is IValidator { VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @notice Get owner of an account - function getOwner(address account) external view returns (address) { - return _getStorage().owners[account]; - } - /// @notice Get session key data function getSessionKey(address account, address sessionKey) external @@ -327,12 +333,20 @@ contract SessionKeyValidatorModule is IValidator { uint48 validUntil, uint256 spendLimitPerTx, uint256 spendLimitTotal, - uint256 spentTotal + uint256 spentTotal, + uint256 nonce ) { SessionKeyData storage data = _getStorage().sessionKeys[account][sessionKey]; - return - (data.active, data.validAfter, data.validUntil, data.spendLimitPerTx, data.spendLimitTotal, data.spentTotal); + return ( + data.active, + data.validAfter, + data.validUntil, + data.spendLimitPerTx, + data.spendLimitTotal, + data.spentTotal, + data.nonce + ); } /// @notice Get session key count for an account @@ -361,106 +375,67 @@ contract SessionKeyValidatorModule is IValidator { return data.active && block.timestamp >= data.validAfter && block.timestamp <= data.validUntil; } + /// @notice Get the current nonce for a session key + function getSessionKeyNonce(address account, address sessionKey) external view returns (uint256) { + return _getStorage().sessionKeys[account][sessionKey].nonce; + } + /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @dev Validate target and selector permissions - function _validatePermissions(address account, address sessionKey, PackedUserOperation calldata userOp) - internal - view - returns (bool) - { - SessionKeyValidatorStorage storage $ = _getStorage(); - - // Extract target and selector from callData - // callData format: execute(mode, executionCalldata) where executionCalldata = (target, value, data) - if (userOp.callData.length < 4) return true; // No call to validate - - bytes4 executeSelector = bytes4(userOp.callData[:4]); - - // Only validate for execute calls - if (executeSelector != bytes4(keccak256("execute(bytes32,bytes)"))) return true; - - // Decode the execution calldata to get target - // Skip first 4 bytes (selector) + 32 bytes (mode) + 32 bytes (offset) + 32 bytes (length) - if (userOp.callData.length < 100) return true; - - // The executionCalldata contains: target (20 bytes) + value (32 bytes) + data - bytes calldata execData = userOp.callData[100:]; - if (execData.length < 52) return true; + /// @dev Validate target permissions + function _validateTarget(address account, address sessionKey, address target) internal view { + SessionKeyExecutorStorage storage $ = _getStorage(); + address[] storage targets = $.allowedTargets[account][sessionKey]; - address target = address(bytes20(execData[:20])); - bytes4 selector = bytes4(0); - if (execData.length >= 56) { - // data starts at offset 52, selector is first 4 bytes - selector = bytes4(execData[52:56]); - } + // Empty = any target allowed + if (targets.length == 0) return; - // Check allowed targets (empty = any target allowed) - address[] storage targets = $.allowedTargets[account][sessionKey]; - if (targets.length > 0) { - bool targetAllowed = false; - for (uint256 i = 0; i < targets.length; i++) { - if (targets[i] == target) { - targetAllowed = true; - break; - } - } - if (!targetAllowed) return false; + for (uint256 i = 0; i < targets.length; i++) { + if (targets[i] == target) return; } + revert TargetNotAllowed(); + } - // Check allowed selectors (empty = any selector allowed) + /// @dev Validate selector permissions + function _validateSelector(address account, address sessionKey, bytes4 selector) internal view { + SessionKeyExecutorStorage storage $ = _getStorage(); bytes4[] storage selectors = $.allowedSelectors[account][sessionKey]; - if (selectors.length > 0 && selector != bytes4(0)) { - bool selectorAllowed = false; - for (uint256 i = 0; i < selectors.length; i++) { - if (selectors[i] == selector) { - selectorAllowed = true; - break; - } - } - if (!selectorAllowed) return false; - } - return true; + // Empty = any selector allowed + if (selectors.length == 0) return; + + for (uint256 i = 0; i < selectors.length; i++) { + if (selectors[i] == selector) return; + } + revert SelectorNotAllowed(); } /// @dev Validate and update spending limits - function _validateAndUpdateSpending(address account, address sessionKey, uint256 value) internal returns (bool) { - if (value == 0) return true; + function _validateAndUpdateSpending(address account, address sessionKey, uint256 value) internal { + if (value == 0) return; - SessionKeyValidatorStorage storage $ = _getStorage(); + SessionKeyExecutorStorage storage $ = _getStorage(); SessionKeyData storage keyData = $.sessionKeys[account][sessionKey]; // Check per-tx limit (0 = unlimited) if (keyData.spendLimitPerTx > 0 && value > keyData.spendLimitPerTx) { - return false; + revert SpendLimitPerTxExceeded(); } // Check total limit (0 = unlimited) if (keyData.spendLimitTotal > 0 && keyData.spentTotal + value > keyData.spendLimitTotal) { - return false; + revert SpendLimitTotalExceeded(); } // Update spent amount keyData.spentTotal += value; emit SessionKeySpent(account, sessionKey, value); - - return true; } - /// @dev Extract value from UserOperation - function _extractValue(PackedUserOperation calldata userOp) internal pure returns (uint256) { - // callData format: execute(mode, executionCalldata) where executionCalldata = (target, value, data) - if (userOp.callData.length < 132) return 0; - - // Skip first 4 bytes (selector) + 32 bytes (mode) + 32 bytes (offset) + 32 bytes (length) + 20 bytes (target) - // Value is at offset 120 (20 bytes after target in executionCalldata) - bytes calldata execData = userOp.callData[100:]; - if (execData.length < 52) return 0; - - // Value is 32 bytes starting at offset 20 (after target address) - return uint256(bytes32(execData[20:52])); + /// @dev Encode execution mode for single call + function _encodeExecutionMode() internal pure returns (ModeCode) { + return ModeLib.encodeSimpleSingle(); } } From d66b2f0bae87b55a56f96bd3a748888a1ce97b13 Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 22:19:46 +0700 Subject: [PATCH 10/22] refactor: implement single validator model with mandatory P256MFAValidator - Changed AuraAccount from linked list to single validator storage - Factory now requires P256MFAValidator address in constructor - createAccount() no longer takes defaultValidator parameter - installModule(VALIDATOR) atomically replaces existing validator - uninstallModule(VALIDATOR) reverts with CannotUninstallValidator() - Simplified isValidSignature() - no signature prefix needed - Updated all test files for new API - All 83 modular tests pass --- src/modular/AuraAccount.sol | 131 ++++---------- src/modular/AuraAccountFactory.sol | 33 ++-- test/modular/AuraAccount.t.sol | 58 +++--- test/modular/ModuleTests.t.sol | 23 ++- test/modular/P256MFAValidatorModule.t.sol | 9 +- test/modular/SessionKeyValidatorModule.t.sol | 177 ------------------- test/modular/SocialRecoveryModule.t.sol | 9 +- 7 files changed, 98 insertions(+), 342 deletions(-) delete mode 100644 test/modular/SessionKeyValidatorModule.t.sol diff --git a/src/modular/AuraAccount.sol b/src/modular/AuraAccount.sol index 0437644..7b9d845 100644 --- a/src/modular/AuraAccount.sol +++ b/src/modular/AuraAccount.sol @@ -57,16 +57,12 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { /// @notice Account implementation ID string public constant ACCOUNT_ID = "ethaura.aura.0.1.0"; - /// @notice Sentinel address for linked list - address internal constant SENTINEL = address(0x1); - /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ - /// @notice Installed validators (linked list) - mapping(address => address) internal _validators; - uint256 internal _validatorCount; + /// @notice The single installed validator (P256MFAValidator by default, can be upgraded to PQMFAValidator) + address internal _validator; /// @notice Installed executors mapping(address => bool) internal _executors; @@ -91,8 +87,7 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { error UnsupportedExecutionMode(ModeCode mode); error ExecutionFailed(); error InvalidValidator(); - error NoValidatorInstalled(); - error CannotRemoveLastValidator(); + error CannotUninstallValidator(); /*////////////////////////////////////////////////////////////// MODIFIERS @@ -138,13 +133,12 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { external initializer { - // Initialize validator linked list - _validators[SENTINEL] = SENTINEL; + // Validator is required - factory enforces P256MFAValidator as default + if (defaultValidator == address(0)) revert InvalidValidator(); - // Install default validator - if (defaultValidator != address(0)) { - _installValidator(defaultValidator, validatorData); - } + // Install the single validator + _validator = defaultValidator; + IValidator(defaultValidator).onInstall(validatorData); // Install hook if provided if (hook != address(0)) { @@ -164,11 +158,8 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { /** * @notice Validate a user operation (ERC-4337) - * @dev Uses signature-based validator selection: - * - First 20 bytes of signature = validator address - * - Remaining bytes = actual signature for the validator - * This approach prevents validator injection attacks since the validator - * must be installed and the signature format is validator-specific. + * @dev Single validator model - delegates directly to the installed validator. + * Signature format is validator-specific (no prefix needed). * @param userOp The user operation * @param userOpHash The hash of the user operation * @param missingAccountFunds Funds to prefund @@ -179,20 +170,8 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { onlyEntryPoint returns (uint256 validationData) { - // Extract validator address from signature prefix (first 20 bytes) - if (userOp.signature.length < 20) { - revert InvalidValidator(); - } - address validator = address(bytes20(userOp.signature[0:20])); - - // CRITICAL: Verify validator is installed to prevent malicious validator injection - if (_validators[validator] == address(0)) { - revert InvalidValidator(); - } - - // Delegate validation to the selected validator module - // The validator is responsible for extracting its own signature format from signature[20:] - validationData = IValidator(validator).validateUserOp(userOp, userOpHash); + // Delegate validation to the single installed validator + validationData = IValidator(_validator).validateUserOp(userOp, userOpHash); // Pay prefund if (missingAccountFunds > 0) { @@ -368,11 +347,11 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { payable onlyEntryPointOrSelf { - // Prevent uninstalling address(0) or SENTINEL (linked list marker) - if (module == address(0) || module == SENTINEL) revert InvalidModule(module); + if (module == address(0)) revert InvalidModule(module); if (moduleTypeId == MODULE_TYPE_VALIDATOR) { - _uninstallValidator(module, deInitData); + // Single validator model: cannot uninstall, must use installModule to replace + revert CannotUninstallValidator(); } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) { _uninstallExecutor(module, deInitData); } else if (moduleTypeId == MODULE_TYPE_FALLBACK) { @@ -390,49 +369,28 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { VALIDATOR MANAGEMENT //////////////////////////////////////////////////////////////*/ + /** + * @notice Install (or replace) the validator + * @dev Single validator model: installing a new validator atomically replaces the existing one. + * This is the only way to switch validators (e.g., from P256MFA to PQMFAValidator). + */ function _installValidator(address validator, bytes calldata data) internal { - if (_validators[validator] != address(0)) { - revert ModuleAlreadyInstalled(validator); - } - - // Add to linked list (insert at head) - _validators[validator] = _validators[SENTINEL]; - _validators[SENTINEL] = validator; - _validatorCount++; - - // Call onInstall - IValidator(validator).onInstall(data); - } - - function _uninstallValidator(address validator, bytes calldata data) internal { - if (_validators[validator] == address(0)) { - revert ModuleNotInstalled(validator); - } - - // CRITICAL: Prevent uninstalling the last validator (would permanently lock account) - if (_validatorCount == 1) { - revert CannotRemoveLastValidator(); - } + address oldValidator = _validator; - // Find previous in linked list - address prev = SENTINEL; - address current = _validators[SENTINEL]; - while (current != SENTINEL && current != validator) { - prev = current; - current = _validators[current]; + // If same validator, just reinitialize + if (oldValidator == validator) { + IValidator(validator).onInstall(data); + return; } - if (current != validator) { - revert ModuleNotInstalled(validator); + // Uninstall old validator first (if any) + if (oldValidator != address(0)) { + IValidator(oldValidator).onUninstall(""); } - // Remove from linked list - _validators[prev] = _validators[validator]; - _validators[validator] = address(0); - _validatorCount--; - - // Call onUninstall - IValidator(validator).onUninstall(data); + // Install new validator + _validator = validator; + IValidator(validator).onInstall(data); } /*////////////////////////////////////////////////////////////// @@ -513,23 +471,10 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { //////////////////////////////////////////////////////////////*/ /// @inheritdoc IERC7579Account - /// @dev Uses signature-based validator selection (same as validateUserOp): - /// - First 20 bytes = validator address - /// - Remaining bytes = actual signature for the validator + /// @dev Single validator model - delegates directly to the installed validator function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { - // Extract validator from signature prefix - if (signature.length < 20) { - return bytes4(0xffffffff); - } - address validator = address(bytes20(signature[0:20])); - - // Verify validator is installed - if (_validators[validator] == address(0)) { - return bytes4(0xffffffff); - } - - // Delegate to validator (validator extracts its signature format from signature[20:]) - return IValidator(validator).isValidSignatureWithSender(msg.sender, hash, signature); + // Delegate to the single installed validator + return IValidator(_validator).isValidSignatureWithSender(msg.sender, hash, signature); } /*////////////////////////////////////////////////////////////// @@ -569,7 +514,7 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { (additionalContext); // Silence unused variable warning if (moduleTypeId == MODULE_TYPE_VALIDATOR) { - return _validators[module] != address(0); + return _validator == module; } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) { return _executors[module]; } else if (moduleTypeId == MODULE_TYPE_FALLBACK) { @@ -628,10 +573,10 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { //////////////////////////////////////////////////////////////*/ /** - * @notice Get the number of installed validators + * @notice Get the installed validator address */ - function getValidatorCount() external view returns (uint256) { - return _validatorCount; + function getValidator() external view returns (address) { + return _validator; } /** diff --git a/src/modular/AuraAccountFactory.sol b/src/modular/AuraAccountFactory.sol index 171e12a..fd6e4cc 100644 --- a/src/modular/AuraAccountFactory.sol +++ b/src/modular/AuraAccountFactory.sol @@ -9,8 +9,8 @@ import {AuraAccount} from "./AuraAccount.sol"; * @title AuraAccountFactory * @notice Factory for deploying AuraAccount instances * @dev Uses Solady's canonical ERC1967Factory for hyper-optimized proxy deployment - * Account address is based on owner + implementation + salt, NOT validator config - * This allows users to receive funds first, then decide on validator later + * All accounts are initialized with P256MFAValidatorModule as the mandatory default validator. + * Users can upgrade to PQMFAValidatorModule later for post-quantum security. */ contract AuraAccountFactory { /*////////////////////////////////////////////////////////////// @@ -20,6 +20,10 @@ contract AuraAccountFactory { /// @notice The account implementation address address public immutable accountImplementation; + /// @notice The mandatory P256MFAValidator module address + /// @dev All accounts use this as the default validator at creation + address public immutable p256MFAValidator; + /// @notice Solady's canonical ERC1967Factory for deploying proxies /// @dev Uses the canonical address: 0x0000000000006396FF2a80c067f99B3d2Ab4Df24 ERC1967Factory public immutable PROXY_FACTORY; @@ -41,9 +45,17 @@ contract AuraAccountFactory { CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor() { + /** + * @notice Deploy the factory with the mandatory P256MFAValidator address + * @param _p256MFAValidator The P256MFAValidatorModule address (mandatory for all accounts) + */ + constructor(address _p256MFAValidator) { + if (_p256MFAValidator == address(0)) revert InvalidValidator(); + // Deploy the implementation accountImplementation = address(new AuraAccount()); + // Set the mandatory validator + p256MFAValidator = _p256MFAValidator; // Use Solady's canonical ERC1967Factory (saves deployment gas) PROXY_FACTORY = ERC1967Factory(ERC1967FactoryConstants.ADDRESS); } @@ -53,10 +65,9 @@ contract AuraAccountFactory { //////////////////////////////////////////////////////////////*/ /** - * @notice Create a new modular account + * @notice Create a new modular account with P256MFAValidator as mandatory default * @param owner The owner address (e.g., Web3Auth address) - * @param defaultValidator The default validator module address - * @param validatorData Initialization data for the validator + * @param validatorData Initialization data for the P256MFAValidator * @param hook The global hook address (optional, address(0) for none) * @param hookData Initialization data for the hook * @param salt Salt for CREATE2 deterministic deployment @@ -64,14 +75,11 @@ contract AuraAccountFactory { */ function createAccount( address owner, - address defaultValidator, bytes calldata validatorData, address hook, bytes calldata hookData, uint256 salt ) external returns (address account) { - if (defaultValidator == address(0)) revert InvalidValidator(); - address addr = getAddress(owner, salt); // If account already exists, return it @@ -80,17 +88,16 @@ contract AuraAccountFactory { return addr; } - // Compute salt based on owner + implementation + salt (NOT validator config) - // This allows users to receive funds first, then decide on validator later + // Compute salt based on owner + implementation + salt bytes32 finalSalt = _computeSalt(owner, salt); // Deploy proxy using Solady's canonical ERC1967Factory - // Admin is set to address(0) since we don't need upgradeability + // Always initialize with P256MFAValidator as the mandatory default account = PROXY_FACTORY.deployDeterministicAndCall( accountImplementation, address(0), // No admin - proxies are not upgradeable finalSalt, - abi.encodeCall(AuraAccount.initialize, (defaultValidator, validatorData, hook, hookData)) + abi.encodeCall(AuraAccount.initialize, (p256MFAValidator, validatorData, hook, hookData)) ); emit AccountCreated(account, owner, salt); diff --git a/test/modular/AuraAccount.t.sol b/test/modular/AuraAccount.t.sol index ebf96df..109550a 100644 --- a/test/modular/AuraAccount.t.sol +++ b/test/modular/AuraAccount.t.sol @@ -40,19 +40,18 @@ contract AuraAccountTest is Test { vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); } - // Deploy factory - factory = new AuraAccountFactory(); - // Deploy mock modules validator = new MockValidator(); executor = new MockExecutor(); hook = new MockHook(); target = new MockTarget(); - // Create account with validator + // Deploy factory with the mandatory P256MFAValidator (using mock for tests) + factory = new AuraAccountFactory(address(validator)); + + // Create account (factory uses the mandatory P256MFAValidator) address accountAddr = factory.createAccount( owner, - address(validator), abi.encode(true), // shouldValidate = true address(0), // no hook "", @@ -71,13 +70,12 @@ contract AuraAccountTest is Test { function test_Initialize() public view { assertEq(account.accountId(), "ethaura.aura.0.1.0"); assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); - assertEq(account.getValidatorCount(), 1); + assertEq(account.getValidator(), address(validator)); } function test_InitializeWithHook() public { address accountAddr = factory.createAccount( owner, - address(validator), abi.encode(true), address(hook), "", @@ -280,44 +278,28 @@ contract AuraAccountTest is Test { account.uninstallModule(MODULE_TYPE_EXECUTOR, address(executor), ""); } - function test_RevertUninstallLastValidator() public { - // Account has only one validator installed (from setUp) - assertEq(account.getValidatorCount(), 1); - - // Trying to uninstall the last validator should revert + function test_RevertUninstallValidator() public { + // Single validator model: cannot uninstall validator, must use install to replace vm.prank(ENTRYPOINT); - vm.expectRevert(AuraAccount.CannotRemoveLastValidator.selector); + vm.expectRevert(AuraAccount.CannotUninstallValidator.selector); account.uninstallModule(MODULE_TYPE_VALIDATOR, address(validator), ""); // Validator should still be installed assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); } - function test_UninstallValidatorWithMultiple() public { - // Install a second validator + function test_ReplaceValidator() public { + // Single validator model: installing a new validator replaces the old one MockValidator validator2 = new MockValidator(); - vm.prank(ENTRYPOINT); - account.installModule(MODULE_TYPE_VALIDATOR, address(validator2), abi.encode(true)); - assertEq(account.getValidatorCount(), 2); - // Now we can uninstall one validator (not the last) + // Install new validator (this replaces the old one) vm.prank(ENTRYPOINT); - account.uninstallModule(MODULE_TYPE_VALIDATOR, address(validator2), ""); - - // Should have one validator remaining - assertEq(account.getValidatorCount(), 1); - assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); - assertFalse(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator2), "")); - } - - function test_RevertUninstallSentinel() public { - // SENTINEL = address(0x1) is the linked list marker - // Attempting to uninstall it should revert to prevent list corruption - address SENTINEL = address(0x1); + account.installModule(MODULE_TYPE_VALIDATOR, address(validator2), abi.encode(true)); - vm.prank(ENTRYPOINT); - vm.expectRevert(abi.encodeWithSelector(AuraAccount.InvalidModule.selector, SENTINEL)); - account.uninstallModule(MODULE_TYPE_VALIDATOR, SENTINEL, ""); + // New validator is now installed, old one is not + assertEq(account.getValidator(), address(validator2)); + assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator2), "")); + assertFalse(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); } /*////////////////////////////////////////////////////////////// @@ -326,8 +308,8 @@ contract AuraAccountTest is Test { function test_IsValidSignature() public view { bytes32 hash = keccak256("test"); - // Signature format: [validator(20B)][actualSignature] - bytes memory signature = abi.encodePacked(address(validator)); + // Single validator model: signature is directly passed to the validator + bytes memory signature = ""; bytes4 result = account.isValidSignature(hash, signature); assertEq(result, bytes4(0x1626ba7e)); } @@ -336,8 +318,8 @@ contract AuraAccountTest is Test { validator.setValidation(address(account), false); bytes32 hash = keccak256("test"); - // Signature format: [validator(20B)][actualSignature] - bytes memory signature = abi.encodePacked(address(validator)); + // Single validator model: signature is directly passed to the validator + bytes memory signature = ""; bytes4 result = account.isValidSignature(hash, signature); assertEq(result, bytes4(0xffffffff)); } diff --git a/test/modular/ModuleTests.t.sol b/test/modular/ModuleTests.t.sol index 4e18a53..25dc2cc 100644 --- a/test/modular/ModuleTests.t.sol +++ b/test/modular/ModuleTests.t.sol @@ -49,16 +49,15 @@ contract MultiHookTest is Test { vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); } - factory = new AuraAccountFactory(); validator = new MockValidator(); + factory = new AuraAccountFactory(address(validator)); multiHook = new MultiHook(); hook1 = new MockHook(); hook2 = new MockHook(); target = new MockTarget(); // Create account with MultiHook as global hook - address accountAddr = - factory.createAccount(owner, address(validator), abi.encode(true), address(multiHook), "", 0); + address accountAddr = factory.createAccount(owner, abi.encode(true), address(multiHook), "", 0); account = AuraAccount(payable(accountAddr)); vm.deal(address(account), 10 ether); } @@ -178,12 +177,12 @@ contract LargeTransactionExecutorModuleTest is Test { vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); } - factory = new AuraAccountFactory(); validator = new MockValidator(); + factory = new AuraAccountFactory(address(validator)); executor = new LargeTransactionExecutorModule(); target = new MockTarget(); - address accountAddr = factory.createAccount(owner, address(validator), abi.encode(true), address(0), "", 0); + address accountAddr = factory.createAccount(owner, abi.encode(true), address(0), "", 0); account = AuraAccount(payable(accountAddr)); vm.deal(address(account), 10 ether); @@ -324,16 +323,15 @@ contract LargeTransactionGuardHookTest is Test { vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); } - factory = new AuraAccountFactory(); validator = new MockValidator(); + factory = new AuraAccountFactory(address(validator)); executor = new LargeTransactionExecutorModule(); guardHook = new LargeTransactionGuardHook(); target = new MockTarget(); // Create account with guard hook - address accountAddr = factory.createAccount( - owner, address(validator), abi.encode(true), address(guardHook), abi.encode(address(executor)), 0 - ); + address accountAddr = + factory.createAccount(owner, abi.encode(true), address(guardHook), abi.encode(address(executor)), 0); account = AuraAccount(payable(accountAddr)); vm.deal(address(account), 10 ether); @@ -407,16 +405,15 @@ contract HookManagerModuleTest is Test { vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); } - factory = new AuraAccountFactory(); validator = new MockValidator(); + factory = new AuraAccountFactory(address(validator)); multiHook = new MultiHook(); hookManager = new HookManagerModule(); hook1 = new MockHook(); // Create account with MultiHook - address accountAddr = factory.createAccount( - owner, address(validator), abi.encode(true), address(multiHook), abi.encode(address(hookManager)), 0 - ); + address accountAddr = + factory.createAccount(owner, abi.encode(true), address(multiHook), abi.encode(address(hookManager)), 0); account = AuraAccount(payable(accountAddr)); vm.deal(address(account), 10 ether); diff --git a/test/modular/P256MFAValidatorModule.t.sol b/test/modular/P256MFAValidatorModule.t.sol index 3644c53..3ca4931 100644 --- a/test/modular/P256MFAValidatorModule.t.sol +++ b/test/modular/P256MFAValidatorModule.t.sol @@ -29,17 +29,18 @@ contract P256MFAValidatorModuleTest is Test { vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); } - // Deploy factory and validator module - factory = new AuraAccountFactory(); + // Deploy validator module validator = new P256MFAValidatorModule(); - // Create account with P256MFAValidatorModule + // Deploy factory with mandatory P256MFAValidator + factory = new AuraAccountFactory(address(validator)); + + // Create account (factory uses P256MFAValidatorModule as mandatory default) // Init data: owner, qx, qy, deviceId, enableMFA bytes memory initData = abi.encode(owner, QX, QY, bytes32("Test Device"), true); address accountAddr = factory.createAccount( owner, - address(validator), initData, address(0), // no hook "", diff --git a/test/modular/SessionKeyValidatorModule.t.sol b/test/modular/SessionKeyValidatorModule.t.sol deleted file mode 100644 index 0b35b73..0000000 --- a/test/modular/SessionKeyValidatorModule.t.sol +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Test} from "forge-std/Test.sol"; -import {AuraAccount} from "../../src/modular/AuraAccount.sol"; -import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; -import {SessionKeyValidatorModule} from "../../src/modular/modules/validators/SessionKeyValidatorModule.sol"; -import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; -import {MODULE_TYPE_VALIDATOR} from "@erc7579/interfaces/IERC7579Module.sol"; - -contract SessionKeyValidatorModuleTest is Test { - AuraAccountFactory public factory; - AuraAccount public account; - SessionKeyValidatorModule public validator; - - address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - - address owner = address(0x1234); - address sessionKey; - uint256 sessionKeyPrivateKey; - - function setUp() public { - // Deploy canonical ERC1967Factory if not already deployed - if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { - vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); - } - - // Deploy factory - factory = new AuraAccountFactory(); - - // Deploy validator module - validator = new SessionKeyValidatorModule(); - - // Setup session key - sessionKeyPrivateKey = 0xBEEF; - sessionKey = vm.addr(sessionKeyPrivateKey); - - // Deploy account with validator - bytes memory validatorData = abi.encode(owner); - uint256 salt = 1; - account = AuraAccount( - payable(factory.createAccount( - owner, - address(validator), - validatorData, - address(0), // no hook - "", // no hook data - salt - )) - ); - vm.deal(address(account), 10 ether); - } - - function test_Initialize() public view { - assertTrue(validator.isInitialized(address(account))); - assertEq(validator.getOwner(address(account)), owner); - } - - function test_IsModuleType() public view { - assertTrue(validator.isModuleType(MODULE_TYPE_VALIDATOR)); - assertFalse(validator.isModuleType(2)); // Executor - } - - function test_CreateSessionKey() public { - // Create session key with permissions - SessionKeyValidatorModule.SessionKeyPermission memory permission = SessionKeyValidatorModule.SessionKeyPermission({ - sessionKey: sessionKey, - validAfter: uint48(block.timestamp), - validUntil: uint48(block.timestamp + 1 hours), - allowedTargets: new address[](0), - allowedSelectors: new bytes4[](0), - spendLimitPerTx: 1 ether, - spendLimitTotal: 5 ether - }); - - vm.prank(address(account)); - validator.createSessionKey(permission); - - ( - bool active, - uint48 validAfter, - uint48 validUntil, - uint256 limitPerTx, - uint256 limitTotal, - uint256 spentTotal - ) = validator.getSessionKey(address(account), sessionKey); - - assertTrue(active); - assertEq(validAfter, uint48(block.timestamp)); - assertEq(validUntil, uint48(block.timestamp + 1 hours)); - assertEq(limitPerTx, 1 ether); - assertEq(limitTotal, 5 ether); - assertEq(spentTotal, 0); - } - - function test_RevokeSessionKey() public { - // First create a session key - SessionKeyValidatorModule.SessionKeyPermission memory permission = SessionKeyValidatorModule.SessionKeyPermission({ - sessionKey: sessionKey, - validAfter: uint48(block.timestamp), - validUntil: uint48(block.timestamp + 1 hours), - allowedTargets: new address[](0), - allowedSelectors: new bytes4[](0), - spendLimitPerTx: 0, - spendLimitTotal: 0 - }); - - vm.prank(address(account)); - validator.createSessionKey(permission); - assertTrue(validator.isSessionKeyValid(address(account), sessionKey)); - - // Revoke it - vm.prank(address(account)); - validator.revokeSessionKey(sessionKey); - - (bool active,,,,,) = validator.getSessionKey(address(account), sessionKey); - assertFalse(active); - } - - function test_GetSessionKeys() public { - // Create multiple session keys - address sessionKey2 = address(0x5678); - - SessionKeyValidatorModule.SessionKeyPermission memory permission1 = - SessionKeyValidatorModule.SessionKeyPermission({ - sessionKey: sessionKey, - validAfter: uint48(block.timestamp), - validUntil: uint48(block.timestamp + 1 hours), - allowedTargets: new address[](0), - allowedSelectors: new bytes4[](0), - spendLimitPerTx: 0, - spendLimitTotal: 0 - }); - - SessionKeyValidatorModule.SessionKeyPermission memory permission2 = - SessionKeyValidatorModule.SessionKeyPermission({ - sessionKey: sessionKey2, - validAfter: uint48(block.timestamp), - validUntil: uint48(block.timestamp + 2 hours), - allowedTargets: new address[](0), - allowedSelectors: new bytes4[](0), - spendLimitPerTx: 0, - spendLimitTotal: 0 - }); - - vm.startPrank(address(account)); - validator.createSessionKey(permission1); - validator.createSessionKey(permission2); - vm.stopPrank(); - - address[] memory keys = validator.getSessionKeys(address(account)); - assertEq(keys.length, 2); - assertEq(validator.getSessionKeyCount(address(account)), 2); - } - - function test_SessionKeyExpired() public { - SessionKeyValidatorModule.SessionKeyPermission memory permission = SessionKeyValidatorModule.SessionKeyPermission({ - sessionKey: sessionKey, - validAfter: uint48(block.timestamp), - validUntil: uint48(block.timestamp + 1 hours), - allowedTargets: new address[](0), - allowedSelectors: new bytes4[](0), - spendLimitPerTx: 0, - spendLimitTotal: 0 - }); - - vm.prank(address(account)); - validator.createSessionKey(permission); - - assertTrue(validator.isSessionKeyValid(address(account), sessionKey)); - - // Warp past expiry - vm.warp(block.timestamp + 2 hours); - assertFalse(validator.isSessionKeyValid(address(account), sessionKey)); - } -} - diff --git a/test/modular/SocialRecoveryModule.t.sol b/test/modular/SocialRecoveryModule.t.sol index 476b6ef..0fa563d 100644 --- a/test/modular/SocialRecoveryModule.t.sol +++ b/test/modular/SocialRecoveryModule.t.sol @@ -32,17 +32,18 @@ contract SocialRecoveryModuleTest is Test { vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); } - // Deploy factory and modules - factory = new AuraAccountFactory(); + // Deploy modules validator = new P256MFAValidatorModule(); recovery = new SocialRecoveryModule(); - // Create account with P256MFAValidatorModule + // Deploy factory with mandatory P256MFAValidator + factory = new AuraAccountFactory(address(validator)); + + // Create account (factory uses P256MFAValidatorModule as mandatory default) bytes memory validatorData = abi.encode(owner, QX, QY, bytes32("Test Device"), true); address accountAddr = factory.createAccount( owner, - address(validator), validatorData, address(0), // no hook "", From 675639ca0859751e16011b02b293db006d4618d5 Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 22:28:39 +0700 Subject: [PATCH 11/22] refactor: rename p256MFAValidator to validator for generic usage - AuraAccountFactory: p256MFAValidator -> validator - AuraAccount.initialize: defaultValidator -> validator - More flexible design allows any validator type at deployment --- src/modular/AuraAccount.sol | 14 +++++++------- src/modular/AuraAccountFactory.sol | 30 +++++++++++++++--------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/modular/AuraAccount.sol b/src/modular/AuraAccount.sol index 7b9d845..28d4ecc 100644 --- a/src/modular/AuraAccount.sol +++ b/src/modular/AuraAccount.sol @@ -123,22 +123,22 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { //////////////////////////////////////////////////////////////*/ /** - * @notice Initialize the account with a default validator - * @param defaultValidator The default validator module address + * @notice Initialize the account with a validator + * @param validator The validator module address * @param validatorData Initialization data for the validator * @param hook The global hook address (optional, address(0) for none) * @param hookData Initialization data for the hook */ - function initialize(address defaultValidator, bytes calldata validatorData, address hook, bytes calldata hookData) + function initialize(address validator, bytes calldata validatorData, address hook, bytes calldata hookData) external initializer { - // Validator is required - factory enforces P256MFAValidator as default - if (defaultValidator == address(0)) revert InvalidValidator(); + // Validator is required + if (validator == address(0)) revert InvalidValidator(); // Install the single validator - _validator = defaultValidator; - IValidator(defaultValidator).onInstall(validatorData); + _validator = validator; + IValidator(validator).onInstall(validatorData); // Install hook if provided if (hook != address(0)) { diff --git a/src/modular/AuraAccountFactory.sol b/src/modular/AuraAccountFactory.sol index fd6e4cc..0b8c6c0 100644 --- a/src/modular/AuraAccountFactory.sol +++ b/src/modular/AuraAccountFactory.sol @@ -9,8 +9,8 @@ import {AuraAccount} from "./AuraAccount.sol"; * @title AuraAccountFactory * @notice Factory for deploying AuraAccount instances * @dev Uses Solady's canonical ERC1967Factory for hyper-optimized proxy deployment - * All accounts are initialized with P256MFAValidatorModule as the mandatory default validator. - * Users can upgrade to PQMFAValidatorModule later for post-quantum security. + * All accounts are initialized with the configured default validator. + * Users can upgrade to a different validator later (e.g., for post-quantum security). */ contract AuraAccountFactory { /*////////////////////////////////////////////////////////////// @@ -20,9 +20,9 @@ contract AuraAccountFactory { /// @notice The account implementation address address public immutable accountImplementation; - /// @notice The mandatory P256MFAValidator module address - /// @dev All accounts use this as the default validator at creation - address public immutable p256MFAValidator; + /// @notice The default validator module address + /// @dev All accounts use this as the initial validator at creation + address public immutable validator; /// @notice Solady's canonical ERC1967Factory for deploying proxies /// @dev Uses the canonical address: 0x0000000000006396FF2a80c067f99B3d2Ab4Df24 @@ -46,16 +46,16 @@ contract AuraAccountFactory { //////////////////////////////////////////////////////////////*/ /** - * @notice Deploy the factory with the mandatory P256MFAValidator address - * @param _p256MFAValidator The P256MFAValidatorModule address (mandatory for all accounts) + * @notice Deploy the factory with the default validator address + * @param _validator The default validator module address (used for all accounts) */ - constructor(address _p256MFAValidator) { - if (_p256MFAValidator == address(0)) revert InvalidValidator(); + constructor(address _validator) { + if (_validator == address(0)) revert InvalidValidator(); // Deploy the implementation accountImplementation = address(new AuraAccount()); - // Set the mandatory validator - p256MFAValidator = _p256MFAValidator; + // Set the default validator + validator = _validator; // Use Solady's canonical ERC1967Factory (saves deployment gas) PROXY_FACTORY = ERC1967Factory(ERC1967FactoryConstants.ADDRESS); } @@ -65,9 +65,9 @@ contract AuraAccountFactory { //////////////////////////////////////////////////////////////*/ /** - * @notice Create a new modular account with P256MFAValidator as mandatory default + * @notice Create a new modular account with the configured default validator * @param owner The owner address (e.g., Web3Auth address) - * @param validatorData Initialization data for the P256MFAValidator + * @param validatorData Initialization data for the validator * @param hook The global hook address (optional, address(0) for none) * @param hookData Initialization data for the hook * @param salt Salt for CREATE2 deterministic deployment @@ -92,12 +92,12 @@ contract AuraAccountFactory { bytes32 finalSalt = _computeSalt(owner, salt); // Deploy proxy using Solady's canonical ERC1967Factory - // Always initialize with P256MFAValidator as the mandatory default + // Initialize with the configured default validator account = PROXY_FACTORY.deployDeterministicAndCall( accountImplementation, address(0), // No admin - proxies are not upgradeable finalSalt, - abi.encodeCall(AuraAccount.initialize, (p256MFAValidator, validatorData, hook, hookData)) + abi.encodeCall(AuraAccount.initialize, (validator, validatorData, hook, hookData)) ); emit AccountCreated(account, owner, salt); From 77b05a6a7808c4ec82c2710d7c854e5ff7990976 Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 22:48:50 +0700 Subject: [PATCH 12/22] security: disable delegatecall execution mode - Removed CALLTYPE_DELEGATECALL from imports - _execute() now reverts on delegatecall mode with UnsupportedExecutionMode - Removed _executeDelegatecall() function entirely - supportsExecutionMode() returns false for delegatecall - Prevents storage corruption from arbitrary delegatecalls Note: fallback() still uses delegatecall to installed fallback handlers, which is safe because only installed modules can be handlers. --- src/modular/AuraAccount.sol | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/modular/AuraAccount.sol b/src/modular/AuraAccount.sol index 28d4ecc..1e3f688 100644 --- a/src/modular/AuraAccount.sol +++ b/src/modular/AuraAccount.sol @@ -27,7 +27,6 @@ import { CALLTYPE_SINGLE, CALLTYPE_BATCH, CALLTYPE_STATIC, - CALLTYPE_DELEGATECALL, EXECTYPE_DEFAULT, EXECTYPE_TRY } from "@erc7579/lib/ModeLib.sol"; @@ -233,9 +232,9 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { returnData = _executeBatch(executionCalldata, execType); } else if (callType == CALLTYPE_STATIC) { returnData = _executeStatic(executionCalldata); - } else if (callType == CALLTYPE_DELEGATECALL) { - returnData = _executeDelegatecall(executionCalldata, execType); } else { + // CALLTYPE_DELEGATECALL and other modes are not supported + // Delegatecall is disabled for security - can corrupt account storage revert UnsupportedExecutionMode(mode); } } @@ -294,26 +293,6 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { } } - /** - * @notice Execute a delegate call (DANGEROUS - use with care) - */ - function _executeDelegatecall(bytes calldata executionCalldata, ExecType execType) - internal - returns (bytes[] memory returnData) - { - (address target,, bytes calldata data) = ExecutionLib.decodeSingle(executionCalldata); - - // TODO: Add whitelist check for delegatecall targets - - returnData = new bytes[](1); - bool success; - (success, returnData[0]) = target.delegatecall(data); - - if (execType == EXECTYPE_DEFAULT && !success) { - revert ExecutionFailed(); - } - } - /*////////////////////////////////////////////////////////////// MODULE MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -485,17 +464,13 @@ contract AuraAccount is IAccount, IERC7579Account, Initializable { function supportsExecutionMode(ModeCode mode) external pure returns (bool) { (CallType callType, ExecType execType,,) = ModeLib.decode(mode); - // Support single, batch, static calls + // Support single, batch, static calls only + // Delegatecall is disabled for security - can corrupt account storage if (callType == CALLTYPE_SINGLE || callType == CALLTYPE_BATCH || callType == CALLTYPE_STATIC) { // Support default and try exec types return execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY; } - // Delegatecall is optional and dangerous - if (callType == CALLTYPE_DELEGATECALL) { - return execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY; - } - return false; } From 2f39619d82e3d5df0a750f2b4bd94f386556a546 Mon Sep 17 00:00:00 2001 From: prpeh Date: Tue, 2 Dec 2025 22:49:56 +0700 Subject: [PATCH 13/22] happy lint --- .../executors/SessionKeyExecutorModule.sol | 47 +++++-------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/src/modular/modules/executors/SessionKeyExecutorModule.sol b/src/modular/modules/executors/SessionKeyExecutorModule.sol index ddf75c0..3765c40 100644 --- a/src/modular/modules/executors/SessionKeyExecutorModule.sol +++ b/src/modular/modules/executors/SessionKeyExecutorModule.sol @@ -25,13 +25,13 @@ contract SessionKeyExecutorModule is IExecutor { /// @notice Session key permission structure struct SessionKeyPermission { - address sessionKey; // EOA that can sign - uint48 validAfter; // Start timestamp - uint48 validUntil; // Expiry timestamp - address[] allowedTargets; // Contracts it can call (empty = any) - bytes4[] allowedSelectors; // Functions it can call (empty = any) - uint256 spendLimitPerTx; // Max ETH per transaction (0 = unlimited) - uint256 spendLimitTotal; // Max ETH total (0 = unlimited) + address sessionKey; // EOA that can sign + uint48 validAfter; // Start timestamp + uint48 validUntil; // Expiry timestamp + address[] allowedTargets; // Contracts it can call (empty = any) + bytes4[] allowedSelectors; // Functions it can call (empty = any) + uint256 spendLimitPerTx; // Max ETH per transaction (0 = unlimited) + uint256 spendLimitTotal; // Max ETH total (0 = unlimited) } /// @notice Internal storage for a session key @@ -42,7 +42,7 @@ contract SessionKeyExecutorModule is IExecutor { uint256 spendLimitPerTx; uint256 spendLimitTotal; uint256 spentTotal; - uint256 nonce; // Replay protection + uint256 nonce; // Replay protection } /*////////////////////////////////////////////////////////////// @@ -78,11 +78,7 @@ contract SessionKeyExecutorModule is IExecutor { event SessionKeyRevoked(address indexed account, address indexed sessionKey); event SessionKeySpent(address indexed account, address indexed sessionKey, uint256 amount); event SessionKeyExecuted( - address indexed account, - address indexed sessionKey, - address target, - uint256 value, - bytes4 selector + address indexed account, address indexed sessionKey, address target, uint256 value, bytes4 selector ); /*////////////////////////////////////////////////////////////// @@ -192,16 +188,7 @@ contract SessionKeyExecutorModule is IExecutor { if (nonce != keyData.nonce) revert InvalidNonce(); // Verify signature - bytes32 messageHash = keccak256( - abi.encodePacked( - account, - target, - value, - keccak256(data), - nonce, - block.chainid - ) - ); + bytes32 messageHash = keccak256(abi.encodePacked(account, target, value, keccak256(data), nonce, block.chainid)); address recovered = messageHash.toEthSignedMessageHash().recover(signature); if (recovered != sessionKey) revert InvalidSignature(); @@ -220,18 +207,10 @@ contract SessionKeyExecutorModule is IExecutor { ++keyData.nonce; // Execute via the account - bytes[] memory results = IERC7579Account(account).executeFromExecutor( - _encodeExecutionMode(), - abi.encodePacked(target, value, data) - ); + bytes[] memory results = + IERC7579Account(account).executeFromExecutor(_encodeExecutionMode(), abi.encodePacked(target, value, data)); - emit SessionKeyExecuted( - account, - sessionKey, - target, - value, - data.length >= 4 ? bytes4(data[:4]) : bytes4(0) - ); + emit SessionKeyExecuted(account, sessionKey, target, value, data.length >= 4 ? bytes4(data[:4]) : bytes4(0)); // Return first result (single execution) return results.length > 0 ? results[0] : bytes(""); From 6d27af63b2dec83c5a4cc8362d45bcf69888caf2 Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 3 Dec 2025 09:21:01 +0700 Subject: [PATCH 14/22] test: add additional unit tests for modular components - Add tests for MultiHook: preCheck/postCheck with hooks, removeHookFromMiddle - Add tests for LargeTransactionExecutorModule: getPendingTx, getThreshold, disable - Add tests for LargeTransactionGuardHook: revert on large tx not from executor - Add tests for AuraAccount: fallback handler success/failure, accountId - Total 251 tests passing for modular components --- test/modular/AuraAccount.t.sol | 458 ++++++++++++- test/modular/AuraAccountFactory.t.sol | 158 +++++ test/modular/ModuleTests.t.sol | 541 ++++++++++++++++ test/modular/P256MFAValidatorModule.t.sol | 389 ++++++++++- test/modular/SessionKeyExecutorModule.t.sol | 673 ++++++++++++++++++++ test/modular/SocialRecoveryModule.t.sol | 323 ++++++++++ 6 files changed, 2540 insertions(+), 2 deletions(-) create mode 100644 test/modular/AuraAccountFactory.t.sol create mode 100644 test/modular/SessionKeyExecutorModule.t.sol diff --git a/test/modular/AuraAccount.t.sol b/test/modular/AuraAccount.t.sol index 109550a..3bc04cb 100644 --- a/test/modular/AuraAccount.t.sol +++ b/test/modular/AuraAccount.t.sol @@ -14,7 +14,21 @@ import { MODULE_TYPE_FALLBACK, MODULE_TYPE_HOOK } from "@erc7579/interfaces/IERC7579Module.sol"; -import {ModeLib, ModeCode} from "@erc7579/lib/ModeLib.sol"; +import { + ModeLib, + ModeCode, + CallType, + ExecType, + ModeSelector, + ModePayload, + CALLTYPE_SINGLE, + CALLTYPE_BATCH, + CALLTYPE_STATIC, + CALLTYPE_DELEGATECALL, + EXECTYPE_DEFAULT, + EXECTYPE_TRY, + MODE_DEFAULT +} from "@erc7579/lib/ModeLib.sol"; import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; import {MockValidator} from "./mocks/MockValidator.sol"; @@ -336,5 +350,447 @@ contract AuraAccountTest is Test { assertEq(address(account).balance, balanceBefore + 1 ether); } + + /*////////////////////////////////////////////////////////////// + STATIC CALL TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ExecuteStatic() public { + // First set a value + target.setValue(42); + + // Prepare static call to read the value + bytes memory callData = abi.encodeCall(MockTarget.getValue, ()); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + // Create static mode + ModeCode staticMode = ModeLib.encode(CALLTYPE_STATIC, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00)); + + // Execute from EntryPoint - static call should work + vm.prank(ENTRYPOINT); + account.execute(staticMode, executionData); + + // Value should still be 42 (static call doesn't change state) + assertEq(target.value(), 42); + } + + function test_RevertExecuteStatic_StateChange() public { + // Prepare static call that tries to change state + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + // Create static mode + ModeCode staticMode = ModeLib.encode(CALLTYPE_STATIC, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00)); + + vm.prank(ENTRYPOINT); + vm.expectRevert(AuraAccount.ExecutionFailed.selector); + account.execute(staticMode, executionData); + } + + /*////////////////////////////////////////////////////////////// + DELEGATECALL DISABLED TESTS + //////////////////////////////////////////////////////////////*/ + + function test_RevertExecuteDelegatecall() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + // Create delegatecall mode + ModeCode delegatecallMode = + ModeLib.encode(CALLTYPE_DELEGATECALL, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00)); + + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.UnsupportedExecutionMode.selector, delegatecallMode)); + account.execute(delegatecallMode, executionData); + } + + function test_SupportsExecutionMode_NoDelegatecall() public view { + // Create delegatecall mode + ModeCode delegatecallMode = + ModeLib.encode(CALLTYPE_DELEGATECALL, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00)); + + assertFalse(account.supportsExecutionMode(delegatecallMode)); + } + + /*////////////////////////////////////////////////////////////// + TRY EXECUTION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ExecuteTry_SuccessfulCall() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + // Create try mode + ModeCode tryMode = ModeLib.encode(CALLTYPE_SINGLE, EXECTYPE_TRY, MODE_DEFAULT, ModePayload.wrap(0x00)); + + vm.prank(ENTRYPOINT); + account.execute(tryMode, executionData); + + assertEq(target.value(), 42); + } + + function test_ExecuteTry_FailedCallDoesNotRevert() public { + // Set target to revert + target.setShouldRevert(true); + + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + // Create try mode + ModeCode tryMode = ModeLib.encode(CALLTYPE_SINGLE, EXECTYPE_TRY, MODE_DEFAULT, ModePayload.wrap(0x00)); + + // Should not revert even though target reverts + vm.prank(ENTRYPOINT); + account.execute(tryMode, executionData); + + // Value should not have changed + assertEq(target.value(), 0); + } + + function test_ExecuteTryBatch_PartialFailure() public { + // Prepare batch with one failing call + Execution[] memory executions = new Execution[](3); + executions[0] = + Execution({target: address(target), value: 0, callData: abi.encodeCall(MockTarget.setValue, (10))}); + executions[1] = + Execution({target: address(target), value: 0, callData: abi.encodeCall(MockTarget.increment, ())}); + executions[2] = + Execution({target: address(target), value: 0, callData: abi.encodeCall(MockTarget.increment, ())}); + + bytes memory executionData = ExecutionLib.encodeBatch(executions); + + // Create try batch mode + ModeCode tryBatchMode = ModeLib.encode(CALLTYPE_BATCH, EXECTYPE_TRY, MODE_DEFAULT, ModePayload.wrap(0x00)); + + vm.prank(ENTRYPOINT); + account.execute(tryBatchMode, executionData); + + // All calls should have succeeded + assertEq(target.value(), 12); // 10 + 1 + 1 + } + + /*////////////////////////////////////////////////////////////// + UNSUPPORTED MODE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_RevertUnsupportedCallType() public { + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, ""); + + // Create mode with unsupported call type + ModeCode unsupportedMode = + ModeLib.encode(CallType.wrap(0xAA), EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00)); + + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.UnsupportedExecutionMode.selector, unsupportedMode)); + account.execute(unsupportedMode, executionData); + } + + /*////////////////////////////////////////////////////////////// + BATCH WITH VALUE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ExecuteBatchWithValue() public { + Execution[] memory executions = new Execution[](2); + executions[0] = + Execution({target: address(target), value: 1 ether, callData: abi.encodeCall(MockTarget.setValue, (100))}); + executions[1] = Execution({ + target: address(target), + value: 0.5 ether, + callData: abi.encodeCall(MockTarget.setValue, (101)) // Use setValue which is payable + }); + + bytes memory executionData = ExecutionLib.encodeBatch(executions); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleBatch(), executionData); + + assertEq(target.value(), 101); + assertEq(address(target).balance, 1.5 ether); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNT ID TESTS + //////////////////////////////////////////////////////////////*/ + + function test_AccountId() public view { + assertEq(account.accountId(), "ethaura.aura.0.1.0"); + } + + /*////////////////////////////////////////////////////////////// + ENTRYPOINT TESTS + //////////////////////////////////////////////////////////////*/ + + function test_EntryPoint() public view { + assertEq(address(account.ENTRYPOINT()), ENTRYPOINT); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS TESTS + //////////////////////////////////////////////////////////////*/ + + function test_GetValidator() public view { + assertEq(account.getValidator(), address(validator)); + } + + function test_GetGlobalHook() public view { + // No hook installed initially + assertEq(account.getGlobalHook(), address(0)); + } + + function test_GetGlobalHook_WithHook() public { + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_HOOK, address(hook), ""); + + assertEq(account.getGlobalHook(), address(hook)); + } + + function test_GetFallbackHandler() public view { + // No fallback handler installed initially + assertEq(account.getFallbackHandler(bytes4(0x12345678)), address(0)); + } + + /*////////////////////////////////////////////////////////////// + FALLBACK HANDLER TESTS + //////////////////////////////////////////////////////////////*/ + + function test_InstallFallbackHandler() public { + // Create a mock fallback handler + MockFallbackHandler fallbackHandler = new MockFallbackHandler(); + bytes4 selector = MockFallbackHandler.handleCall.selector; + + // Install fallback handler with selector + bytes memory initData = abi.encodePacked(selector); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_FALLBACK, address(fallbackHandler), initData); + + // Verify installation + assertTrue( + account.isModuleInstalled(MODULE_TYPE_FALLBACK, address(fallbackHandler), abi.encodePacked(selector)) + ); + assertEq(account.getFallbackHandler(selector), address(fallbackHandler)); + } + + function test_UninstallFallbackHandler() public { + // Create and install a mock fallback handler + MockFallbackHandler fallbackHandler = new MockFallbackHandler(); + bytes4 selector = MockFallbackHandler.handleCall.selector; + bytes memory initData = abi.encodePacked(selector); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_FALLBACK, address(fallbackHandler), initData); + + // Uninstall + vm.prank(ENTRYPOINT); + account.uninstallModule(MODULE_TYPE_FALLBACK, address(fallbackHandler), initData); + + // Verify uninstallation + assertFalse( + account.isModuleInstalled(MODULE_TYPE_FALLBACK, address(fallbackHandler), abi.encodePacked(selector)) + ); + assertEq(account.getFallbackHandler(selector), address(0)); + } + + function test_FallbackHandlerExecution() public { + // Create and install a mock fallback handler + MockFallbackHandler fallbackHandler = new MockFallbackHandler(); + bytes4 selector = MockFallbackHandler.handleCall.selector; + bytes memory initData = abi.encodePacked(selector); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_FALLBACK, address(fallbackHandler), initData); + + // Call the fallback handler through the account + (bool success, bytes memory result) = + address(account).call(abi.encodeCall(MockFallbackHandler.handleCall, (42))); + assertTrue(success); + assertEq(abi.decode(result, (uint256)), 42); + } + + function test_RevertFallbackHandler_NoHandler() public { + // Call with unknown selector + (bool success,) = address(account).call(abi.encodeWithSelector(bytes4(0x12345678))); + assertFalse(success); + } + + function test_RevertInstallFallbackHandler_AlreadyInstalled() public { + MockFallbackHandler fallbackHandler = new MockFallbackHandler(); + bytes4 selector = MockFallbackHandler.handleCall.selector; + bytes memory initData = abi.encodePacked(selector); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_FALLBACK, address(fallbackHandler), initData); + + // Try to install again with same selector + MockFallbackHandler fallbackHandler2 = new MockFallbackHandler(); + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.ModuleAlreadyInstalled.selector, address(fallbackHandler2))); + account.installModule(MODULE_TYPE_FALLBACK, address(fallbackHandler2), initData); + } + + function test_RevertUninstallFallbackHandler_NotInstalled() public { + MockFallbackHandler fallbackHandler = new MockFallbackHandler(); + bytes4 selector = MockFallbackHandler.handleCall.selector; + bytes memory initData = abi.encodePacked(selector); + + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.ModuleNotInstalled.selector, address(fallbackHandler))); + account.uninstallModule(MODULE_TYPE_FALLBACK, address(fallbackHandler), initData); + } + + /*////////////////////////////////////////////////////////////// + HOOK UNINSTALL TESTS + //////////////////////////////////////////////////////////////*/ + + function test_UninstallHook() public { + // Install hook first + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_HOOK, address(hook), ""); + assertTrue(account.isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + + // Uninstall hook + vm.prank(ENTRYPOINT); + account.uninstallModule(MODULE_TYPE_HOOK, address(hook), ""); + assertFalse(account.isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + } + + function test_RevertUninstallHook_NotInstalled() public { + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.ModuleNotInstalled.selector, address(hook))); + account.uninstallModule(MODULE_TYPE_HOOK, address(hook), ""); + } + + /*////////////////////////////////////////////////////////////// + IS MODULE INSTALLED TESTS + //////////////////////////////////////////////////////////////*/ + + function test_IsModuleInstalled_Validator() public view { + assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); + assertFalse(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(0x1234), "")); + } + + function test_IsModuleInstalled_Executor() public { + assertFalse(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(executor), "")); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), ""); + + assertTrue(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(executor), "")); + } + + function test_IsModuleInstalled_Hook() public { + assertFalse(account.isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_HOOK, address(hook), ""); + + assertTrue(account.isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + } + + function test_IsModuleInstalled_FallbackWithContext() public { + MockFallbackHandler fallbackHandler = new MockFallbackHandler(); + bytes4 selector = MockFallbackHandler.handleCall.selector; + bytes memory initData = abi.encodePacked(selector); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_FALLBACK, address(fallbackHandler), initData); + + // Check with selector context + assertTrue( + account.isModuleInstalled(MODULE_TYPE_FALLBACK, address(fallbackHandler), abi.encodePacked(selector)) + ); + // Check with wrong selector + assertFalse( + account.isModuleInstalled( + MODULE_TYPE_FALLBACK, address(fallbackHandler), abi.encodePacked(bytes4(0x12345678)) + ) + ); + } + + function test_IsModuleInstalled_FallbackWithoutContext() public { + MockFallbackHandler fallbackHandler = new MockFallbackHandler(); + bytes4 selector = MockFallbackHandler.handleCall.selector; + bytes memory initData = abi.encodePacked(selector); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_FALLBACK, address(fallbackHandler), initData); + + // Check without context - should return false (can't verify without selector) + assertFalse(account.isModuleInstalled(MODULE_TYPE_FALLBACK, address(fallbackHandler), "")); + } + + function test_IsModuleInstalled_UnsupportedType() public view { + // Module type 5 is not supported + assertFalse(account.isModuleInstalled(5, address(validator), "")); + } + + /*////////////////////////////////////////////////////////////// + UNSUPPORTED MODULE TYPE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_RevertInstallModule_UnsupportedType() public { + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.UnsupportedModuleType.selector, 5)); + account.installModule(5, address(validator), ""); + } + + function test_RevertUninstallModule_UnsupportedType() public { + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.UnsupportedModuleType.selector, 5)); + account.uninstallModule(5, address(validator), ""); + } + + /*////////////////////////////////////////////////////////////// + ADDITIONAL FALLBACK TESTS + //////////////////////////////////////////////////////////////*/ + + function test_FallbackHandler_Success() public { + MockFallbackHandler fallbackHandler = new MockFallbackHandler(); + bytes4 selector = MockFallbackHandler.handleCall.selector; + bytes memory initData = abi.encodePacked(selector); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_FALLBACK, address(fallbackHandler), initData); + + // Call the fallback handler through the account + (bool success, bytes memory result) = address(account).call(abi.encodeWithSelector(selector, uint256(42))); + assertTrue(success); + assertEq(abi.decode(result, (uint256)), 42); + } + + function test_FallbackHandler_NoHandler() public { + // Call with unknown selector should revert + bytes4 unknownSelector = bytes4(keccak256("unknownFunction()")); + (bool success,) = address(account).call(abi.encodeWithSelector(unknownSelector)); + assertFalse(success); + } +} + +/*////////////////////////////////////////////////////////////// + MOCK FALLBACK HANDLER +//////////////////////////////////////////////////////////////*/ + +contract MockFallbackHandler { + bool public installed; + + function onInstall(bytes calldata) external { + installed = true; + } + + function onUninstall(bytes calldata) external { + installed = false; + } + + function isModuleType(uint256 moduleTypeId) external pure returns (bool) { + return moduleTypeId == MODULE_TYPE_FALLBACK; + } + + function isInitialized(address) external view returns (bool) { + return installed; + } + + function handleCall(uint256 value) external pure returns (uint256) { + return value; + } } diff --git a/test/modular/AuraAccountFactory.t.sol b/test/modular/AuraAccountFactory.t.sol new file mode 100644 index 0000000..afd92c1 --- /dev/null +++ b/test/modular/AuraAccountFactory.t.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; + +import {MODULE_TYPE_VALIDATOR, MODULE_TYPE_HOOK} from "@erc7579/interfaces/IERC7579Module.sol"; + +import {MockValidator} from "./mocks/MockValidator.sol"; +import {MockHook} from "./mocks/MockHook.sol"; + +contract AuraAccountFactoryTest is Test { + AuraAccountFactory public factory; + MockValidator public validator; + MockHook public hook; + + address owner = address(0x1234); + address owner2 = address(0x5678); + + function setUp() public { + // Deploy canonical ERC1967Factory if not already deployed + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + // Deploy mock modules + validator = new MockValidator(); + hook = new MockHook(); + + // Deploy factory with the mandatory validator + factory = new AuraAccountFactory(address(validator)); + } + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR TESTS + //////////////////////////////////////////////////////////////*/ + + function test_Constructor() public view { + assertEq(factory.validator(), address(validator)); + assertTrue(factory.accountImplementation() != address(0)); + assertEq(address(factory.PROXY_FACTORY()), ERC1967FactoryConstants.ADDRESS); + } + + function test_RevertConstructor_InvalidValidator() public { + vm.expectRevert(AuraAccountFactory.InvalidValidator.selector); + new AuraAccountFactory(address(0)); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNT CREATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_CreateAccount() public { + address accountAddr = factory.createAccount( + owner, + abi.encode(true), // shouldValidate = true + address(0), // no hook + "", + 0 // salt + ); + + assertTrue(accountAddr != address(0)); + assertTrue(accountAddr.code.length > 0); + + AuraAccount account = AuraAccount(payable(accountAddr)); + assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); + assertEq(account.getValidator(), address(validator)); + } + + function test_CreateAccount_WithHook() public { + address accountAddr = factory.createAccount(owner, abi.encode(true), address(hook), "", 0); + + AuraAccount account = AuraAccount(payable(accountAddr)); + assertTrue(account.isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + assertEq(account.getGlobalHook(), address(hook)); + } + + function test_CreateAccount_DifferentSalts() public { + address account1 = factory.createAccount(owner, abi.encode(true), address(0), "", 0); + address account2 = factory.createAccount(owner, abi.encode(true), address(0), "", 1); + address account3 = factory.createAccount(owner, abi.encode(true), address(0), "", 2); + + assertTrue(account1 != account2); + assertTrue(account2 != account3); + assertTrue(account1 != account3); + } + + function test_CreateAccount_DifferentOwners() public { + address account1 = factory.createAccount(owner, abi.encode(true), address(0), "", 0); + address account2 = factory.createAccount(owner2, abi.encode(true), address(0), "", 0); + + assertTrue(account1 != account2); + } + + function test_CreateAccount_ReturnsExistingIfDeployed() public { + address account1 = factory.createAccount(owner, abi.encode(true), address(0), "", 0); + address account2 = factory.createAccount(owner, abi.encode(true), address(0), "", 0); + + assertEq(account1, account2); + } + + /*////////////////////////////////////////////////////////////// + ADDRESS PREDICTION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_GetAddress_MatchesDeployed() public { + address predicted = factory.getAddress(owner, 0); + address deployed = factory.createAccount(owner, abi.encode(true), address(0), "", 0); + + assertEq(predicted, deployed); + } + + function test_GetAddress_DifferentSalts() public { + address addr1 = factory.getAddress(owner, 0); + address addr2 = factory.getAddress(owner, 1); + address addr3 = factory.getAddress(owner, 2); + + assertTrue(addr1 != addr2); + assertTrue(addr2 != addr3); + assertTrue(addr1 != addr3); + } + + function test_GetAddress_DifferentOwners() public { + address addr1 = factory.getAddress(owner, 0); + address addr2 = factory.getAddress(owner2, 0); + + assertTrue(addr1 != addr2); + } + + function test_GetAddress_Deterministic() public view { + address addr1 = factory.getAddress(owner, 123); + address addr2 = factory.getAddress(owner, 123); + + assertEq(addr1, addr2); + } + + /*////////////////////////////////////////////////////////////// + COUNTERFACTUAL DEPLOYMENT TESTS + //////////////////////////////////////////////////////////////*/ + + function test_CounterfactualDeployment() public { + // Get predicted address before deployment + address predicted = factory.getAddress(owner, 42); + + // Verify no code at predicted address + assertEq(predicted.code.length, 0); + + // Deploy + address deployed = factory.createAccount(owner, abi.encode(true), address(0), "", 42); + + // Verify deployment + assertEq(predicted, deployed); + assertTrue(deployed.code.length > 0); + } +} + diff --git a/test/modular/ModuleTests.t.sol b/test/modular/ModuleTests.t.sol index 25dc2cc..0d5b1cf 100644 --- a/test/modular/ModuleTests.t.sol +++ b/test/modular/ModuleTests.t.sol @@ -503,6 +503,16 @@ contract ERC721ReceiverModuleTest is Test { bytes4 result = receiver.onERC721Received(address(this), address(this), 1, ""); assertEq(result, bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))); } + + function test_OnInstall() public { + // Should not revert + receiver.onInstall(""); + } + + function test_OnUninstall() public { + // Should not revert + receiver.onUninstall(""); + } } /*////////////////////////////////////////////////////////////// @@ -547,5 +557,536 @@ contract ERC1155ReceiverModuleTest is Test { bytes4 interfaceId = type(IERC1155Receiver).interfaceId; assertTrue(receiver.supportsInterface(interfaceId)); } + + function test_OnInstall() public { + // Should not revert + receiver.onInstall(""); + } + + function test_OnUninstall() public { + // Should not revert + receiver.onUninstall(""); + } +} + +/*////////////////////////////////////////////////////////////// + ADDITIONAL MULTI HOOK TESTS +//////////////////////////////////////////////////////////////*/ + +contract MultiHookAdditionalTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + MultiHook public multiHook; + MockHook public hook1; + MockHook public hook2; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address owner = address(0x1234); + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + validator = new MockValidator(); + factory = new AuraAccountFactory(address(validator)); + multiHook = new MultiHook(); + hook1 = new MockHook(); + hook2 = new MockHook(); + target = new MockTarget(); + + address accountAddr = factory.createAccount(owner, abi.encode(true), address(multiHook), "", 0); + account = AuraAccount(payable(accountAddr)); + vm.deal(address(account), 10 ether); + } + + function test_IsModuleType() public view { + assertTrue(multiHook.isModuleType(MODULE_TYPE_HOOK)); + assertFalse(multiHook.isModuleType(MODULE_TYPE_VALIDATOR)); + } + + function test_IsInitialized() public view { + assertTrue(multiHook.isInitialized(address(account))); + } + + function test_OnUninstall() public { + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + multiHook.addHook(address(hook2)); + vm.stopPrank(); + + vm.prank(address(account)); + multiHook.onUninstall(""); + + address[] memory hooks = multiHook.getHooks(address(account)); + assertEq(hooks.length, 0); + } + + function test_GetHooks() public { + address[] memory hooks = multiHook.getHooks(address(account)); + assertEq(hooks.length, 0); + + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + hooks = multiHook.getHooks(address(account)); + assertEq(hooks.length, 1); + + multiHook.addHook(address(hook2)); + hooks = multiHook.getHooks(address(account)); + assertEq(hooks.length, 2); + vm.stopPrank(); + } + + function test_RevertAddHook_InvalidHook() public { + vm.prank(address(account)); + vm.expectRevert(MultiHook.InvalidHook.selector); + multiHook.addHook(address(0)); + } + + function test_OnInstallWithManager() public { + // Create a new account with manager set during install + address manager = address(0x5678); + address accountAddr = factory.createAccount(owner, abi.encode(true), address(multiHook), abi.encode(manager), 1); + AuraAccount newAccount = AuraAccount(payable(accountAddr)); + + assertEq(multiHook.getManager(address(newAccount)), manager); + } + + function test_PreCheckWithNoHooks() public { + // preCheck with no hooks should return empty bytes + vm.prank(address(account)); + bytes memory result = multiHook.preCheck(address(this), 0, ""); + (address[] memory hooks, bytes[] memory contexts) = abi.decode(result, (address[], bytes[])); + assertEq(hooks.length, 0); + assertEq(contexts.length, 0); + } + + function test_PostCheckWithNoHooks() public { + // postCheck with empty data should not revert + address[] memory hooks = new address[](0); + bytes[] memory contexts = new bytes[](0); + bytes memory preCheckData = abi.encode(hooks, contexts); + multiHook.postCheck(preCheckData); + } + + function test_RevertAddDuplicateHook() public { + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + + vm.expectRevert(abi.encodeWithSelector(MultiHook.HookAlreadyInstalled.selector, address(hook1))); + multiHook.addHook(address(hook1)); + vm.stopPrank(); + } + + function test_RevertRemoveNonExistentHook() public { + vm.prank(address(account)); + vm.expectRevert(abi.encodeWithSelector(MultiHook.HookNotInstalled.selector, address(hook1))); + multiHook.removeHook(address(hook1)); + } + + function test_PreCheckWithHooks() public { + // Add hooks + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + multiHook.addHook(address(hook2)); + vm.stopPrank(); + + // preCheck should call all hooks + vm.prank(address(account)); + bytes memory result = multiHook.preCheck(address(this), 1 ether, ""); + (address[] memory hooks, bytes[] memory contexts) = abi.decode(result, (address[], bytes[])); + assertEq(hooks.length, 2); + assertEq(contexts.length, 2); + } + + function test_PostCheckWithHooks() public { + // Add hooks + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + multiHook.addHook(address(hook2)); + vm.stopPrank(); + + // First do preCheck to get the data + vm.prank(address(account)); + bytes memory preCheckData = multiHook.preCheck(address(this), 1 ether, ""); + + // Then postCheck should not revert + multiHook.postCheck(preCheckData); + } + + function test_RemoveHookFromMiddle() public { + MockHook hook3 = new MockHook(); + + vm.startPrank(address(account)); + multiHook.addHook(address(hook1)); + multiHook.addHook(address(hook2)); + multiHook.addHook(address(hook3)); + + // Remove middle hook + multiHook.removeHook(address(hook2)); + vm.stopPrank(); + + address[] memory hooks = multiHook.getHooks(address(account)); + assertEq(hooks.length, 2); + assertFalse(multiHook.isHookInstalled(address(account), address(hook2))); + } +} + +/*////////////////////////////////////////////////////////////// + ADDITIONAL LARGE TRANSACTION EXECUTOR TESTS +//////////////////////////////////////////////////////////////*/ + +contract LargeTransactionExecutorAdditionalTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + LargeTransactionExecutorModule public executor; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address owner = address(0x1234); + + uint256 constant THRESHOLD = 1 ether; + uint256 constant TIMELOCK = 1 hours; + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + validator = new MockValidator(); + factory = new AuraAccountFactory(address(validator)); + executor = new LargeTransactionExecutorModule(); + target = new MockTarget(); + + address accountAddr = factory.createAccount(owner, abi.encode(true), address(0), "", 0); + account = AuraAccount(payable(accountAddr)); + vm.deal(address(account), 10 ether); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), abi.encode(THRESHOLD, TIMELOCK)); + } + + function test_IsModuleType() public view { + assertTrue(executor.isModuleType(MODULE_TYPE_EXECUTOR)); + assertFalse(executor.isModuleType(MODULE_TYPE_VALIDATOR)); + } + + function test_IsInitialized() public view { + assertTrue(executor.isInitialized(address(account))); + } + + function test_OnUninstall() public { + vm.prank(address(account)); + executor.onUninstall(""); + + assertFalse(executor.isInitialized(address(account))); + } + + function test_SetTimelockPeriod() public { + vm.prank(address(account)); + executor.setTimelockPeriod(2 hours); + + assertEq(executor.getTimelockPeriod(address(account)), 2 hours); + } + + function test_RevertCancelNonExistentTransaction() public { + bytes32 txHash = keccak256("nonexistent"); + + vm.prank(address(account)); + vm.expectRevert(LargeTransactionExecutorModule.TransactionNotFound.selector); + executor.cancel(txHash); + } + + function test_CancelledTransactionCannotBeExecuted() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + + vm.prank(address(account)); + executor.execute(address(target), 2 ether, callData); + + bytes32 txHash = keccak256(abi.encode(address(account), address(target), 2 ether, callData)); + + vm.prank(address(account)); + executor.cancel(txHash); + + // Warp past timelock + vm.warp(block.timestamp + TIMELOCK + 1); + + // Try to execute - should fail because cancelled + vm.prank(address(account)); + vm.expectRevert(LargeTransactionExecutorModule.TransactionWasCancelled.selector); + executor.execute(address(target), 2 ether, callData); + } + + function test_RevertCancelAlreadyExecutedTransaction() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + + vm.prank(address(account)); + executor.execute(address(target), 2 ether, callData); + + bytes32 txHash = keccak256(abi.encode(address(account), address(target), 2 ether, callData)); + + vm.warp(block.timestamp + TIMELOCK + 1); + + vm.prank(address(account)); + executor.execute(address(target), 2 ether, callData); + + vm.prank(address(account)); + vm.expectRevert(LargeTransactionExecutorModule.TransactionAlreadyExecuted.selector); + executor.cancel(txHash); + } + + function test_GetPendingTxHashes() public { + bytes memory callData1 = abi.encodeCall(MockTarget.setValue, (100)); + bytes memory callData2 = abi.encodeCall(MockTarget.setValue, (200)); + + vm.startPrank(address(account)); + executor.execute(address(target), 2 ether, callData1); + executor.execute(address(target), 3 ether, callData2); + vm.stopPrank(); + + bytes32[] memory hashes = executor.getPendingTxHashes(address(account)); + assertEq(hashes.length, 2); + } + + function test_GetPendingTx() public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + + vm.prank(address(account)); + executor.execute(address(target), 2 ether, callData); + + bytes32 txHash = keccak256(abi.encode(address(account), address(target), 2 ether, callData)); + + ( + address txTarget, + uint256 txValue, + bytes memory txData, + uint256 proposedAt, + uint256 executeAfter, + bool executed, + bool cancelled + ) = executor.getPendingTx(address(account), txHash); + + assertEq(txTarget, address(target)); + assertEq(txValue, 2 ether); + assertEq(txData, callData); + assertGt(proposedAt, 0); + assertEq(executeAfter, proposedAt + TIMELOCK); + assertFalse(executed); + assertFalse(cancelled); + } + + function test_GetThreshold() public view { + assertEq(executor.getThreshold(address(account)), THRESHOLD); + } + + function test_GetThreshold_Uninitialized() public view { + // Uninitialized account should return max + assertEq(executor.getThreshold(address(0x9999)), type(uint256).max); + } + + function test_Disable() public { + vm.prank(address(account)); + executor.disable(); + + assertEq(executor.getThreshold(address(account)), type(uint256).max); + } +} + +/*////////////////////////////////////////////////////////////// + ADDITIONAL LARGE TRANSACTION GUARD HOOK TESTS +//////////////////////////////////////////////////////////////*/ + +contract LargeTransactionGuardHookAdditionalTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + LargeTransactionExecutorModule public executor; + LargeTransactionGuardHook public guardHook; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address owner = address(0x1234); + + uint256 constant THRESHOLD = 1 ether; + uint256 constant TIMELOCK = 1 hours; + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + validator = new MockValidator(); + factory = new AuraAccountFactory(address(validator)); + executor = new LargeTransactionExecutorModule(); + guardHook = new LargeTransactionGuardHook(); + target = new MockTarget(); + + address accountAddr = + factory.createAccount(owner, abi.encode(true), address(guardHook), abi.encode(address(executor)), 0); + account = AuraAccount(payable(accountAddr)); + vm.deal(address(account), 10 ether); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executor), abi.encode(THRESHOLD, TIMELOCK)); + } + + function test_IsModuleType() public view { + assertTrue(guardHook.isModuleType(MODULE_TYPE_HOOK)); + assertFalse(guardHook.isModuleType(MODULE_TYPE_VALIDATOR)); + } + + function test_IsInitialized() public view { + assertTrue(guardHook.isInitialized(address(account))); + } + + function test_OnUninstall() public { + vm.prank(address(account)); + guardHook.onUninstall(""); + + assertFalse(guardHook.isInitialized(address(account))); + } + + function test_PostCheck() public view { + // postCheck should not revert + guardHook.postCheck(""); + } + + function test_PreCheckWithUninitializedAccount() public { + // Create a new guard hook without initializing + LargeTransactionGuardHook newGuardHook = new LargeTransactionGuardHook(); + + // preCheck should return empty bytes (fail-open) + bytes memory result = newGuardHook.preCheck(address(this), 10 ether, ""); + assertEq(result.length, 0); + } + + function test_PreCheckAllowsSmallTransaction() public view { + // Small transaction should be allowed + bytes memory result = guardHook.preCheck(address(this), 0.5 ether, ""); + assertEq(result.length, 0); + } + + function test_PreCheckAllowsExecutorForLargeTransaction() public view { + // Large transaction from executor should be allowed + bytes memory result = guardHook.preCheck(address(executor), 2 ether, ""); + assertEq(result.length, 0); + } + + function test_RevertPreCheck_LargeTransactionNotFromExecutor() public { + // Large transaction not from executor should revert + vm.prank(address(account)); + vm.expectRevert(LargeTransactionGuardHook.LargeTransactionMustUseExecutor.selector); + guardHook.preCheck(address(0x9999), 2 ether, ""); + } +} + +/*////////////////////////////////////////////////////////////// + ADDITIONAL HOOK MANAGER MODULE TESTS +//////////////////////////////////////////////////////////////*/ + +contract HookManagerModuleAdditionalTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + MultiHook public multiHook; + HookManagerModule public hookManager; + MockHook public hook1; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address owner = address(0x1234); + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + validator = new MockValidator(); + factory = new AuraAccountFactory(address(validator)); + multiHook = new MultiHook(); + hookManager = new HookManagerModule(); + hook1 = new MockHook(); + + address accountAddr = + factory.createAccount(owner, abi.encode(true), address(multiHook), abi.encode(address(hookManager)), 0); + account = AuraAccount(payable(accountAddr)); + vm.deal(address(account), 10 ether); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(hookManager), abi.encode(address(multiHook))); + + vm.prank(address(account)); + multiHook.setManager(address(hookManager)); + } + + function test_IsModuleType() public view { + assertTrue(hookManager.isModuleType(MODULE_TYPE_EXECUTOR)); + assertFalse(hookManager.isModuleType(MODULE_TYPE_VALIDATOR)); + } + + function test_IsInitialized() public view { + assertTrue(hookManager.isInitialized(address(account))); + } + + function test_OnUninstall() public { + vm.prank(address(account)); + hookManager.onUninstall(""); + + assertFalse(hookManager.isInitialized(address(account))); + } + + function test_RevertExecuteEmergencyUninstall_NoProposal() public { + vm.prank(address(account)); + vm.expectRevert(HookManagerModule.NoEmergencyUninstallProposed.selector); + hookManager.executeEmergencyUninstall(); + } + + function test_RevertInstallHook_InvalidHook() public { + vm.prank(address(account)); + vm.expectRevert(HookManagerModule.InvalidHook.selector); + hookManager.installHook(address(0), ""); + } + + function test_RevertInstallHook_MultiHookNotSet() public { + // Create a new account without MultiHook + address accountAddr = factory.createAccount(owner, abi.encode(true), address(0), "", 2); + AuraAccount newAccount = AuraAccount(payable(accountAddr)); + + // Install HookManager without MultiHook + vm.prank(ENTRYPOINT); + newAccount.installModule(MODULE_TYPE_EXECUTOR, address(hookManager), abi.encode(address(0))); + + vm.prank(address(newAccount)); + vm.expectRevert(HookManagerModule.MultiHookNotSet.selector); + hookManager.installHook(address(hook1), ""); + } + + function test_RevertUninstallHook_MultiHookNotSet() public { + // Create a new account without MultiHook + address accountAddr = factory.createAccount(owner, abi.encode(true), address(0), "", 3); + AuraAccount newAccount = AuraAccount(payable(accountAddr)); + + // Install HookManager without MultiHook + vm.prank(ENTRYPOINT); + newAccount.installModule(MODULE_TYPE_EXECUTOR, address(hookManager), abi.encode(address(0))); + + vm.prank(address(newAccount)); + vm.expectRevert(HookManagerModule.MultiHookNotSet.selector); + hookManager.uninstallHook(address(hook1)); + } + + function test_RevertExecuteEmergencyUninstall_TimelockNotPassed() public { + vm.startPrank(address(account)); + hookManager.installHook(address(hook1), ""); + hookManager.proposeEmergencyUninstall(address(hook1)); + vm.stopPrank(); + + // Try to execute immediately (before timelock) + vm.prank(address(account)); + vm.expectRevert(HookManagerModule.EmergencyTimelockNotPassed.selector); + hookManager.executeEmergencyUninstall(); + } } diff --git a/test/modular/P256MFAValidatorModule.t.sol b/test/modular/P256MFAValidatorModule.t.sol index 3ca4931..5143f45 100644 --- a/test/modular/P256MFAValidatorModule.t.sol +++ b/test/modular/P256MFAValidatorModule.t.sol @@ -16,14 +16,17 @@ contract P256MFAValidatorModuleTest is Test { address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - address owner = address(0x1234); uint256 ownerPrivateKey = 0x1234; + address owner; // Test passkey coordinates (from existing tests) bytes32 constant QX = 0x65a2fa44daad46eab0278703edb6c4dcf5e30b8a9aec09fdc71a56f52aa392e4; bytes32 constant QY = 0x4a7a9e4604aa36898209997288e902ac544a555e4b5e0a9efef2b59233f3f437; function setUp() public { + // Derive owner address from private key + owner = vm.addr(ownerPrivateKey); + // Deploy canonical ERC1967Factory if not already deployed if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); @@ -169,5 +172,389 @@ contract P256MFAValidatorModuleTest is Test { // Owner's owner should be newOwner assertEq(validator.getOwner(owner), newOwner); } + + /*////////////////////////////////////////////////////////////// + ERROR CASES TESTS + //////////////////////////////////////////////////////////////*/ + + function test_RevertSetOwner_InvalidOwner() public { + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.InvalidOwner.selector); + validator.setOwner(address(0)); + } + + function test_RevertAddPasskey_InvalidPasskey() public { + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.InvalidPasskey.selector); + validator.addPasskey(bytes32(0), bytes32(0), "Test"); + } + + function test_RevertAddPasskey_InvalidPasskeyQxZero() public { + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.InvalidPasskey.selector); + validator.addPasskey(bytes32(0), bytes32(uint256(1)), "Test"); + } + + function test_RevertAddPasskey_InvalidPasskeyQyZero() public { + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.InvalidPasskey.selector); + validator.addPasskey(bytes32(uint256(1)), bytes32(0), "Test"); + } + + function test_RevertAddPasskey_AlreadyExists() public { + // Try to add the same passkey that was added during setup + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.PasskeyAlreadyExists.selector); + validator.addPasskey(QX, QY, "Duplicate"); + } + + function test_RevertRemovePasskey_DoesNotExist() public { + bytes32 nonExistentId = keccak256(abi.encodePacked(bytes32(uint256(999)), bytes32(uint256(999)))); + + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.PasskeyDoesNotExist.selector); + validator.removePasskey(nonExistentId); + } + + function test_RevertRemovePasskey_CannotRemoveLastPasskey() public { + // MFA is enabled and there's only one passkey + bytes32 passkeyId = keccak256(abi.encodePacked(QX, QY)); + + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.CannotRemoveLastPasskey.selector); + validator.removePasskey(passkeyId); + } + + function test_RevertEnableMFA_RequiresPasskey() public { + // Create a new account without passkey + bytes memory initData = abi.encode(owner, bytes32(0), bytes32(0), bytes32(0), false); + address newAccountAddr = factory.createAccount(owner, initData, address(0), "", 1); + + vm.prank(newAccountAddr); + vm.expectRevert(P256MFAValidatorModule.MFARequiresPasskey.selector); + validator.enableMFA(); + } + + /*////////////////////////////////////////////////////////////// + MODULE TYPE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_IsModuleType() public view { + assertTrue(validator.isModuleType(MODULE_TYPE_VALIDATOR)); + assertFalse(validator.isModuleType(2)); // Executor + assertFalse(validator.isModuleType(3)); // Fallback + assertFalse(validator.isModuleType(4)); // Hook + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS TESTS + //////////////////////////////////////////////////////////////*/ + + function test_GetPasskey() public view { + bytes32 passkeyId = keccak256(abi.encodePacked(QX, QY)); + P256MFAValidatorModule.PasskeyInfo memory info = validator.getPasskey(address(account), passkeyId); + + assertEq(info.qx, QX); + assertEq(info.qy, QY); + assertTrue(info.active); + assertEq(info.deviceId, bytes32("Test Device")); + } + + function test_IsPasskeyActive() public view { + bytes32 passkeyId = keccak256(abi.encodePacked(QX, QY)); + assertTrue(validator.isPasskeyActive(address(account), passkeyId)); + } + + function test_IsPasskeyActive_NonExistent() public view { + bytes32 nonExistentId = keccak256(abi.encodePacked(bytes32(uint256(999)), bytes32(uint256(999)))); + assertFalse(validator.isPasskeyActive(address(account), nonExistentId)); + } + + function test_GetPasskeyIds() public view { + bytes32[] memory ids = validator.getPasskeyIds(address(account)); + assertEq(ids.length, 1); + assertEq(ids[0], keccak256(abi.encodePacked(QX, QY))); + } + + /*////////////////////////////////////////////////////////////// + VALIDATION TESTS (OWNER-ONLY MODE) + //////////////////////////////////////////////////////////////*/ + + function test_ValidateUserOp_OwnerOnly() public { + // Create account without MFA + bytes memory initData = abi.encode(owner, bytes32(0), bytes32(0), bytes32(0), false); + address newAccountAddr = factory.createAccount(owner, initData, address(0), "", 2); + + // Create a mock UserOp + PackedUserOperation memory userOp; + userOp.sender = newAccountAddr; + userOp.nonce = 0; + userOp.callData = ""; + userOp.accountGasLimits = bytes32(0); + userOp.preVerificationGas = 0; + userOp.gasFees = bytes32(0); + userOp.paymasterAndData = ""; + + bytes32 userOpHash = keccak256("test"); + + // Sign with owner + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, userOpHash); + bytes memory ownerSig = abi.encodePacked(r, s, v); + + // Signature format: [validator(20B)][ownerSig(65B)] + userOp.signature = abi.encodePacked(address(validator), ownerSig); + + // Validate + vm.prank(newAccountAddr); + uint256 result = validator.validateUserOp(userOp, userOpHash); + assertEq(result, 0); // VALIDATION_SUCCESS + } + + function test_ValidateUserOp_OwnerOnly_InvalidSignature() public { + // Create account without MFA + bytes memory initData = abi.encode(owner, bytes32(0), bytes32(0), bytes32(0), false); + address newAccountAddr = factory.createAccount(owner, initData, address(0), "", 3); + + PackedUserOperation memory userOp; + userOp.sender = newAccountAddr; + bytes32 userOpHash = keccak256("test"); + + // Sign with wrong key + (uint8 v, bytes32 r, bytes32 s) = vm.sign(0x9999, userOpHash); + bytes memory wrongSig = abi.encodePacked(r, s, v); + + userOp.signature = abi.encodePacked(address(validator), wrongSig); + + vm.prank(newAccountAddr); + uint256 result = validator.validateUserOp(userOp, userOpHash); + assertEq(result, 1); // VALIDATION_FAILED + } + + function test_ValidateUserOp_TooShortSignature() public { + // Create account without MFA + bytes memory initData = abi.encode(owner, bytes32(0), bytes32(0), bytes32(0), false); + address newAccountAddr = factory.createAccount(owner, initData, address(0), "", 4); + + PackedUserOperation memory userOp; + userOp.sender = newAccountAddr; + bytes32 userOpHash = keccak256("test"); + + // Too short signature (less than 20 bytes) + userOp.signature = hex"1234"; + + vm.prank(newAccountAddr); + uint256 result = validator.validateUserOp(userOp, userOpHash); + assertEq(result, 1); // VALIDATION_FAILED + } + + function test_ValidateUserOp_OwnerOnly_WrongSignatureLength() public { + // Create account without MFA + bytes memory initData = abi.encode(owner, bytes32(0), bytes32(0), bytes32(0), false); + address newAccountAddr = factory.createAccount(owner, initData, address(0), "", 5); + + PackedUserOperation memory userOp; + userOp.sender = newAccountAddr; + bytes32 userOpHash = keccak256("test"); + + // Wrong length signature (not 65 bytes after validator address) + userOp.signature = abi.encodePacked(address(validator), hex"1234567890"); + + vm.prank(newAccountAddr); + uint256 result = validator.validateUserOp(userOp, userOpHash); + assertEq(result, 1); // VALIDATION_FAILED + } + + /*////////////////////////////////////////////////////////////// + ERC-1271 SIGNATURE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_IsValidSignatureWithSender_OwnerOnly() public { + // Create account without MFA + bytes memory initData = abi.encode(owner, bytes32(0), bytes32(0), bytes32(0), false); + address newAccountAddr = factory.createAccount(owner, initData, address(0), "", 6); + + bytes32 hash = keccak256("test message"); + + // Sign with owner + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory ownerSig = abi.encodePacked(r, s, v); + + // Signature format: [validator(20B)][ownerSig(65B)] + bytes memory signature = abi.encodePacked(address(validator), ownerSig); + + vm.prank(newAccountAddr); + bytes4 result = validator.isValidSignatureWithSender(address(0), hash, signature); + assertEq(result, bytes4(0x1626ba7e)); // ERC1271_MAGIC_VALUE + } + + function test_IsValidSignatureWithSender_TooShort() public { + bytes32 hash = keccak256("test message"); + bytes memory signature = hex"1234"; // Too short + + vm.prank(address(account)); + bytes4 result = validator.isValidSignatureWithSender(address(0), hash, signature); + assertEq(result, bytes4(0xffffffff)); + } + + function test_IsValidSignatureWithSender_OwnerOnly_WrongLength() public { + // Create account without MFA + bytes memory initData = abi.encode(owner, bytes32(0), bytes32(0), bytes32(0), false); + address newAccountAddr = factory.createAccount(owner, initData, address(0), "", 7); + + bytes32 hash = keccak256("test message"); + bytes memory signature = abi.encodePacked(address(validator), hex"1234567890"); // Wrong length + + vm.prank(newAccountAddr); + bytes4 result = validator.isValidSignatureWithSender(address(0), hash, signature); + assertEq(result, bytes4(0xffffffff)); + } + + function test_IsValidSignatureWithSender_OwnerOnly_InvalidSignature() public { + // Create account without MFA + bytes memory initData = abi.encode(owner, bytes32(0), bytes32(0), bytes32(0), false); + address newAccountAddr = factory.createAccount(owner, initData, address(0), "", 8); + + bytes32 hash = keccak256("test message"); + + // Sign with wrong key + (uint8 v, bytes32 r, bytes32 s) = vm.sign(0x9999, hash); + bytes memory wrongSig = abi.encodePacked(r, s, v); + + bytes memory signature = abi.encodePacked(address(validator), wrongSig); + + vm.prank(newAccountAddr); + bytes4 result = validator.isValidSignatureWithSender(address(0), hash, signature); + assertEq(result, bytes4(0xffffffff)); + } + + /*////////////////////////////////////////////////////////////// + UNINSTALL TESTS + //////////////////////////////////////////////////////////////*/ + + function test_OnUninstall() public { + // Verify initial state + assertTrue(validator.isInitialized(address(account))); + assertEq(validator.getPasskeyCount(address(account)), 1); + assertTrue(validator.isMFAEnabled(address(account))); + + // Uninstall + vm.prank(address(account)); + validator.onUninstall(""); + + // Verify cleared state + assertFalse(validator.isInitialized(address(account))); + assertEq(validator.getPasskeyCount(address(account)), 0); + assertFalse(validator.isMFAEnabled(address(account))); + } + + /*////////////////////////////////////////////////////////////// + INSTALL ERROR TESTS + //////////////////////////////////////////////////////////////*/ + + function test_RevertOnInstall_InvalidOwner() public { + // Deploy a new validator for this test + P256MFAValidatorModule newValidator = new P256MFAValidatorModule(); + + bytes memory initData = abi.encode(address(0), bytes32(0), bytes32(0), bytes32(0), false); + + vm.prank(address(0x9999)); + vm.expectRevert(P256MFAValidatorModule.InvalidOwner.selector); + newValidator.onInstall(initData); + } + + function test_RevertOnInstall_MFAWithoutPasskey() public { + // Deploy a new validator for this test + P256MFAValidatorModule newValidator = new P256MFAValidatorModule(); + + // Try to enable MFA without passkey + bytes memory initData = abi.encode(owner, bytes32(0), bytes32(0), bytes32(0), true); + + vm.prank(address(0x9999)); + vm.expectRevert(P256MFAValidatorModule.MFARequiresPasskey.selector); + newValidator.onInstall(initData); + } + + /*////////////////////////////////////////////////////////////// + MFA VALIDATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ValidateUserOp_MFA_TooShortSignature() public { + // Account has MFA enabled + PackedUserOperation memory userOp; + userOp.sender = address(account); + bytes32 userOpHash = keccak256("test"); + + // Signature too short for MFA (less than 224 bytes after validator address) + userOp.signature = abi.encodePacked(address(validator), new bytes(100)); + + vm.prank(address(account)); + uint256 result = validator.validateUserOp(userOp, userOpHash); + assertEq(result, 1); // VALIDATION_FAILED + } + + function test_IsValidSignatureWithSender_MFA_TooShort() public { + // Account has MFA enabled + bytes32 hash = keccak256("test message"); + + // Signature too short for MFA + bytes memory signature = abi.encodePacked(address(validator), new bytes(100)); + + vm.prank(address(account)); + bytes4 result = validator.isValidSignatureWithSender(address(0), hash, signature); + assertEq(result, bytes4(0xffffffff)); + } + + function test_ValidateUserOp_MFA_InvalidPasskeyId() public { + PackedUserOperation memory userOp; + userOp.sender = address(account); + bytes32 userOpHash = keccak256("test"); + + // Create a signature with invalid passkey ID + // Format: [validator(20B)][webAuthnSig(127B)][passkeyId(32B)][ownerSig(65B)] + bytes memory webAuthnSig = new bytes(127); + bytes32 invalidPasskeyId = keccak256("invalid"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, userOpHash); + bytes memory ownerSig = abi.encodePacked(r, s, v); + + userOp.signature = abi.encodePacked(address(validator), webAuthnSig, invalidPasskeyId, ownerSig); + + vm.prank(address(account)); + uint256 result = validator.validateUserOp(userOp, userOpHash); + assertEq(result, 1); // VALIDATION_FAILED - passkey not found + } + + function test_IsValidSignatureWithSender_MFA_InvalidPasskeyId() public { + bytes32 hash = keccak256("test message"); + + // Create a signature with invalid passkey ID + bytes memory webAuthnSig = new bytes(127); + bytes32 invalidPasskeyId = keccak256("invalid"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory ownerSig = abi.encodePacked(r, s, v); + + bytes memory signature = abi.encodePacked(address(validator), webAuthnSig, invalidPasskeyId, ownerSig); + + vm.prank(address(account)); + bytes4 result = validator.isValidSignatureWithSender(address(0), hash, signature); + assertEq(result, bytes4(0xffffffff)); + } + + /*////////////////////////////////////////////////////////////// + REMOVE PASSKEY WITH MFA DISABLED + //////////////////////////////////////////////////////////////*/ + + function test_RemoveLastPasskey_MFADisabled() public { + // Disable MFA first + vm.prank(address(account)); + validator.disableMFA(); + + // Now we can remove the last passkey + bytes32 passkeyId = keccak256(abi.encodePacked(QX, QY)); + + vm.prank(address(account)); + validator.removePasskey(passkeyId); + + assertEq(validator.getPasskeyCount(address(account)), 0); + } } diff --git a/test/modular/SessionKeyExecutorModule.t.sol b/test/modular/SessionKeyExecutorModule.t.sol new file mode 100644 index 0000000..620db75 --- /dev/null +++ b/test/modular/SessionKeyExecutorModule.t.sol @@ -0,0 +1,673 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../src/modular/AuraAccountFactory.sol"; +import {SessionKeyExecutorModule} from "../../src/modular/modules/executors/SessionKeyExecutorModule.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; + +import {IERC7579Account} from "@erc7579/interfaces/IERC7579Account.sol"; +import {MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {ModeLib} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +import {MockValidator} from "./mocks/MockValidator.sol"; +import {MockTarget} from "./mocks/MockTarget.sol"; + +contract SessionKeyExecutorModuleTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + SessionKeyExecutorModule public sessionKeyModule; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + address owner = address(0x1234); + uint256 sessionKeyPrivateKey = 0xA11CE; + address sessionKey; + + function setUp() public { + // Deploy canonical ERC1967Factory if not already deployed + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + // Derive session key address from private key + sessionKey = vm.addr(sessionKeyPrivateKey); + + // Deploy mock modules + validator = new MockValidator(); + sessionKeyModule = new SessionKeyExecutorModule(); + target = new MockTarget(); + + // Deploy factory with the mandatory validator + factory = new AuraAccountFactory(address(validator)); + + // Create account + address accountAddr = factory.createAccount( + owner, + abi.encode(true), // shouldValidate = true + address(0), // no hook + "", + 0 // salt + ); + account = AuraAccount(payable(accountAddr)); + + // Fund account + vm.deal(address(account), 10 ether); + + // Install session key executor module + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(sessionKeyModule), ""); + } + + /*////////////////////////////////////////////////////////////// + MODULE INSTALLATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ModuleInstalled() public view { + assertTrue(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(sessionKeyModule), "")); + } + + function test_IsModuleType() public view { + assertTrue(sessionKeyModule.isModuleType(MODULE_TYPE_EXECUTOR)); + assertFalse(sessionKeyModule.isModuleType(1)); // Not a validator + } + + /*////////////////////////////////////////////////////////////// + SESSION KEY CREATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_CreateSessionKey() public { + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 1 ether, + spendLimitTotal: 5 ether + }); + + // Execute createSessionKey via account + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + + // Verify session key was created + ( + bool active, + uint48 validAfter, + uint48 validUntil, + uint256 spendLimitPerTx, + uint256 spendLimitTotal, + uint256 spentTotal, + uint256 nonce + ) = sessionKeyModule.getSessionKey(address(account), sessionKey); + + assertTrue(active); + assertEq(validAfter, uint48(block.timestamp)); + assertEq(validUntil, uint48(block.timestamp + 1 days)); + assertEq(spendLimitPerTx, 1 ether); + assertEq(spendLimitTotal, 5 ether); + assertEq(spentTotal, 0); + assertEq(nonce, 0); + } + + function test_CreateSessionKey_WithTargetRestrictions() public { + address[] memory allowedTargets = new address[](1); + allowedTargets[0] = address(target); + + bytes4[] memory allowedSelectors = new bytes4[](1); + allowedSelectors[0] = MockTarget.setValue.selector; + + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: allowedTargets, + allowedSelectors: allowedSelectors, + spendLimitPerTx: 0, + spendLimitTotal: 0 + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + + // Verify restrictions + address[] memory targets = sessionKeyModule.getAllowedTargets(address(account), sessionKey); + bytes4[] memory selectors = sessionKeyModule.getAllowedSelectors(address(account), sessionKey); + + assertEq(targets.length, 1); + assertEq(targets[0], address(target)); + assertEq(selectors.length, 1); + assertEq(selectors[0], MockTarget.setValue.selector); + } + + function test_RevertCreateSessionKey_InvalidAddress() public { + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: address(0), // Invalid + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 0, + spendLimitTotal: 0 + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + + vm.prank(ENTRYPOINT); + // Account wraps inner errors in ExecutionFailed + vm.expectRevert(AuraAccount.ExecutionFailed.selector); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + } + + function test_RevertCreateSessionKey_InvalidTimeRange() public { + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp + 1 days), // After validUntil + validUntil: uint48(block.timestamp), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 0, + spendLimitTotal: 0 + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + + vm.prank(ENTRYPOINT); + // Account wraps inner errors in ExecutionFailed + vm.expectRevert(AuraAccount.ExecutionFailed.selector); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + } + + function test_RevertCreateSessionKey_AlreadyExists() public { + _createDefaultSessionKey(); + + // Try to create again + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 0, + spendLimitTotal: 0 + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + + vm.prank(ENTRYPOINT); + // Account wraps inner errors in ExecutionFailed + vm.expectRevert(AuraAccount.ExecutionFailed.selector); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + } + + /*////////////////////////////////////////////////////////////// + SESSION KEY REVOCATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_RevokeSessionKey() public { + _createDefaultSessionKey(); + + // Revoke session key + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.revokeSessionKey, (sessionKey)); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + + // Verify session key was revoked + (bool active,,,,,,) = sessionKeyModule.getSessionKey(address(account), sessionKey); + assertFalse(active); + assertEq(sessionKeyModule.getSessionKeyCount(address(account)), 0); + } + + function test_RevertRevokeSessionKey_DoesNotExist() public { + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.revokeSessionKey, (sessionKey)); + + vm.prank(ENTRYPOINT); + // Account wraps inner errors in ExecutionFailed + vm.expectRevert(AuraAccount.ExecutionFailed.selector); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + } + + /*////////////////////////////////////////////////////////////// + SESSION KEY EXECUTION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ExecuteWithSessionKey() public { + _createDefaultSessionKey(); + + // Prepare execution data + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + + // Sign the message + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), + address(target), + uint256(0), // value + keccak256(targetCallData), + nonce, + block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + // Execute + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, nonce, signature + ); + + // Verify execution + assertEq(target.value(), 42); + assertEq(target.lastCaller(), address(account)); + } + + function test_ExecuteWithSessionKey_WithValue() public { + _createDefaultSessionKey(); + + // Prepare execution data + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (100)); + uint256 nonce = 0; + uint256 value = 0.5 ether; + + // Sign the message + bytes32 messageHash = keccak256( + abi.encodePacked(address(account), address(target), value, keccak256(targetCallData), nonce, block.chainid) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + // Execute + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), value, targetCallData, nonce, signature + ); + + // Verify spending was tracked + (,,,,, uint256 spentTotal,) = sessionKeyModule.getSessionKey(address(account), sessionKey); + assertEq(spentTotal, value); + } + + function test_RevertExecuteWithSessionKey_NotActive() public { + // Don't create session key - try to execute + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory signature = new bytes(65); + + vm.expectRevert(SessionKeyExecutorModule.SessionKeyNotActive.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, 0, signature + ); + } + + function test_RevertExecuteWithSessionKey_NotYetValid() public { + // Create session key that starts in the future + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp + 1 hours), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 1 ether, + spendLimitTotal: 5 ether + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + + // Try to execute before valid + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory signature = new bytes(65); + + vm.expectRevert(SessionKeyExecutorModule.SessionKeyNotYetValid.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, 0, signature + ); + } + + function test_RevertExecuteWithSessionKey_Expired() public { + _createDefaultSessionKey(); + + // Warp past expiry + vm.warp(block.timestamp + 2 days); + + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory signature = new bytes(65); + + vm.expectRevert(SessionKeyExecutorModule.SessionKeyExpired.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, 0, signature + ); + } + + function test_RevertExecuteWithSessionKey_InvalidNonce() public { + _createDefaultSessionKey(); + + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 wrongNonce = 999; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), address(target), uint256(0), keccak256(targetCallData), wrongNonce, block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.InvalidNonce.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, wrongNonce, signature + ); + } + + function test_RevertExecuteWithSessionKey_InvalidSignature() public { + _createDefaultSessionKey(); + + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + + // Sign with wrong private key + uint256 wrongPrivateKey = 0xBAD; + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), address(target), uint256(0), keccak256(targetCallData), nonce, block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.InvalidSignature.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, nonce, signature + ); + } + + function test_RevertExecuteWithSessionKey_SelfCallNotAllowed() public { + _createDefaultSessionKey(); + + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), + address(account), // Self-call + uint256(0), + keccak256(targetCallData), + nonce, + block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.SelfCallNotAllowed.selector); + sessionKeyModule.executeWithSessionKey( + address(account), + sessionKey, + address(account), // Self-call + 0, + targetCallData, + nonce, + signature + ); + } + + /*////////////////////////////////////////////////////////////// + SPENDING LIMIT TESTS + //////////////////////////////////////////////////////////////*/ + + function test_RevertExecuteWithSessionKey_SpendLimitPerTxExceeded() public { + _createDefaultSessionKey(); // 1 ether per tx limit + + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + uint256 value = 2 ether; // Exceeds 1 ether limit + + bytes32 messageHash = keccak256( + abi.encodePacked(address(account), address(target), value, keccak256(targetCallData), nonce, block.chainid) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.SpendLimitPerTxExceeded.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), value, targetCallData, nonce, signature + ); + } + + function test_RevertExecuteWithSessionKey_SpendLimitTotalExceeded() public { + _createDefaultSessionKey(); // 5 ether total limit + + // Execute multiple transactions to exhaust limit + for (uint256 i = 0; i < 5; i++) { + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (i)); + uint256 nonce = i; + uint256 value = 1 ether; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), address(target), value, keccak256(targetCallData), nonce, block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), value, targetCallData, nonce, signature + ); + } + + // Now try to spend more - should fail + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (999)); + uint256 nonce = 5; + uint256 value = 0.1 ether; + + bytes32 messageHash = keccak256( + abi.encodePacked(address(account), address(target), value, keccak256(targetCallData), nonce, block.chainid) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.SpendLimitTotalExceeded.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), value, targetCallData, nonce, signature + ); + } + + /*////////////////////////////////////////////////////////////// + TARGET/SELECTOR RESTRICTION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_RevertExecuteWithSessionKey_TargetNotAllowed() public { + // Create session key with target restriction + address[] memory allowedTargets = new address[](1); + allowedTargets[0] = address(0xDEAD); // Only allow this target + + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: allowedTargets, + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 1 ether, + spendLimitTotal: 5 ether + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + + // Try to call a different target + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), + address(target), // Not in allowed list + uint256(0), + keccak256(targetCallData), + nonce, + block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.TargetNotAllowed.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, nonce, signature + ); + } + + function test_RevertExecuteWithSessionKey_SelectorNotAllowed() public { + // Create session key with selector restriction + bytes4[] memory allowedSelectors = new bytes4[](1); + allowedSelectors[0] = MockTarget.increment.selector; // Only allow increment + + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: new address[](0), + allowedSelectors: allowedSelectors, + spendLimitPerTx: 1 ether, + spendLimitTotal: 5 ether + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + + // Try to call setValue (not allowed) + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), address(target), uint256(0), keccak256(targetCallData), nonce, block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.SelectorNotAllowed.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, nonce, signature + ); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_GetSessionKeyCount() public { + assertEq(sessionKeyModule.getSessionKeyCount(address(account)), 0); + + _createDefaultSessionKey(); + + assertEq(sessionKeyModule.getSessionKeyCount(address(account)), 1); + } + + function test_GetSessionKeys() public { + _createDefaultSessionKey(); + + address[] memory keys = sessionKeyModule.getSessionKeys(address(account)); + assertEq(keys.length, 1); + assertEq(keys[0], sessionKey); + } + + function test_IsSessionKeyValid() public { + _createDefaultSessionKey(); + + assertTrue(sessionKeyModule.isSessionKeyValid(address(account), sessionKey)); + + // Warp past expiry + vm.warp(block.timestamp + 2 days); + assertFalse(sessionKeyModule.isSessionKeyValid(address(account), sessionKey)); + } + + function test_GetSessionKeyNonce() public { + _createDefaultSessionKey(); + + assertEq(sessionKeyModule.getSessionKeyNonce(address(account), sessionKey), 0); + + // Execute once to increment nonce + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), address(target), uint256(0), keccak256(targetCallData), nonce, block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, nonce, signature + ); + + assertEq(sessionKeyModule.getSessionKeyNonce(address(account), sessionKey), 1); + } + + /*////////////////////////////////////////////////////////////// + MODULE UNINSTALL TESTS + //////////////////////////////////////////////////////////////*/ + + function test_OnUninstall_ClearsAllSessionKeys() public { + _createDefaultSessionKey(); + + // Uninstall module + vm.prank(ENTRYPOINT); + account.uninstallModule(MODULE_TYPE_EXECUTOR, address(sessionKeyModule), ""); + + // Verify session keys are cleared + assertEq(sessionKeyModule.getSessionKeyCount(address(account)), 0); + (bool active,,,,,,) = sessionKeyModule.getSessionKey(address(account), sessionKey); + assertFalse(active); + } + + /*////////////////////////////////////////////////////////////// + HELPER FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _createDefaultSessionKey() internal { + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 1 ether, + spendLimitTotal: 5 ether + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + } +} + diff --git a/test/modular/SocialRecoveryModule.t.sol b/test/modular/SocialRecoveryModule.t.sol index 0fa563d..7cf1c1b 100644 --- a/test/modular/SocialRecoveryModule.t.sol +++ b/test/modular/SocialRecoveryModule.t.sol @@ -230,5 +230,328 @@ contract SocialRecoveryModuleTest is Test { vm.expectRevert(SocialRecoveryModule.RecoveryAlreadyApproved.selector); recovery.approveRecovery(address(account), 0); } + + /*////////////////////////////////////////////////////////////// + ERROR CASES TESTS + //////////////////////////////////////////////////////////////*/ + + function test_RevertAddGuardian_AlreadyGuardian() public { + vm.prank(address(account)); + vm.expectRevert(SocialRecoveryModule.AlreadyGuardian.selector); + recovery.addGuardian(guardian1); + } + + function test_RevertRemoveGuardian_NotFound() public { + vm.prank(address(account)); + vm.expectRevert(SocialRecoveryModule.GuardianNotFound.selector); + recovery.removeGuardian(guardian3); + } + + function test_RevertSetRecoveryConfig_InvalidThreshold() public { + vm.prank(address(account)); + vm.expectRevert(SocialRecoveryModule.InvalidThreshold.selector); + recovery.setRecoveryConfig(5, 24 hours); // threshold > guardian count + } + + function test_RevertApproveRecovery_NotGuardian() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(address(0xdead)); + vm.expectRevert(SocialRecoveryModule.NotGuardian.selector); + recovery.approveRecovery(address(account), 0); + } + + function test_RevertApproveRecovery_NotFound() public { + vm.prank(guardian1); + vm.expectRevert(SocialRecoveryModule.RecoveryNotFound.selector); + recovery.approveRecovery(address(account), 999); + } + + function test_RevertApproveRecovery_AlreadyExecuted() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + // Initiate and approve + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(guardian2); + recovery.approveRecovery(address(account), 0); + + // Wait for timelock + vm.warp(block.timestamp + 25 hours); + + // Execute + recovery.executeRecovery(address(account), 0, address(validator)); + + // Try to approve again + vm.prank(guardian1); + vm.expectRevert(SocialRecoveryModule.RecoveryAlreadyExecuted.selector); + recovery.approveRecovery(address(account), 0); + } + + function test_RevertApproveRecovery_Cancelled() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(address(account)); + recovery.cancelRecovery(0); + + vm.prank(guardian2); + vm.expectRevert(SocialRecoveryModule.RecoveryAlreadyCancelled.selector); + recovery.approveRecovery(address(account), 0); + } + + function test_RevertExecuteRecovery_NotFound() public { + vm.expectRevert(SocialRecoveryModule.RecoveryNotFound.selector); + recovery.executeRecovery(address(account), 999, address(validator)); + } + + function test_RevertExecuteRecovery_ThresholdNotMet() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + // Only 1 approval, threshold is 2 + vm.expectRevert(SocialRecoveryModule.ThresholdNotMet.selector); + recovery.executeRecovery(address(account), 0, address(validator)); + } + + function test_RevertExecuteRecovery_AlreadyExecuted() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(guardian2); + recovery.approveRecovery(address(account), 0); + + vm.warp(block.timestamp + 25 hours); + + recovery.executeRecovery(address(account), 0, address(validator)); + + vm.expectRevert(SocialRecoveryModule.RecoveryAlreadyExecuted.selector); + recovery.executeRecovery(address(account), 0, address(validator)); + } + + function test_RevertExecuteRecovery_Cancelled() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(guardian2); + recovery.approveRecovery(address(account), 0); + + vm.prank(address(account)); + recovery.cancelRecovery(0); + + vm.warp(block.timestamp + 25 hours); + + vm.expectRevert(SocialRecoveryModule.RecoveryAlreadyCancelled.selector); + recovery.executeRecovery(address(account), 0, address(validator)); + } + + function test_RevertCancelRecovery_NotFound() public { + vm.prank(address(account)); + vm.expectRevert(SocialRecoveryModule.RecoveryNotFound.selector); + recovery.cancelRecovery(999); + } + + function test_RevertCancelRecovery_AlreadyExecuted() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(guardian2); + recovery.approveRecovery(address(account), 0); + + vm.warp(block.timestamp + 25 hours); + + recovery.executeRecovery(address(account), 0, address(validator)); + + vm.prank(address(account)); + vm.expectRevert(SocialRecoveryModule.RecoveryAlreadyExecuted.selector); + recovery.cancelRecovery(0); + } + + function test_RevertCancelRecovery_AlreadyCancelled() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(address(account)); + recovery.cancelRecovery(0); + + vm.prank(address(account)); + vm.expectRevert(SocialRecoveryModule.RecoveryAlreadyCancelled.selector); + recovery.cancelRecovery(0); + } + + /*////////////////////////////////////////////////////////////// + EXECUTE RECOVERY TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ExecuteRecovery() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(guardian2); + recovery.approveRecovery(address(account), 0); + + vm.warp(block.timestamp + 25 hours); + + recovery.executeRecovery(address(account), 0, address(validator)); + + (,,,,,,, bool executed,) = recovery.getRecoveryRequest(address(account), 0); + assertTrue(executed); + + // Verify new owner is set + assertEq(validator.getOwner(address(account)), newOwner); + } + + /*////////////////////////////////////////////////////////////// + MODULE TYPE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_IsModuleType() public view { + assertTrue(recovery.isModuleType(MODULE_TYPE_EXECUTOR)); + assertFalse(recovery.isModuleType(MODULE_TYPE_VALIDATOR)); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS TESTS + //////////////////////////////////////////////////////////////*/ + + function test_GetGuardians() public view { + address[] memory guardians = recovery.getGuardians(address(account)); + assertEq(guardians.length, 2); + assertEq(guardians[0], guardian1); + assertEq(guardians[1], guardian2); + } + + function test_GetRecoveryNonce() public { + assertEq(recovery.getRecoveryNonce(address(account)), 0); + + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + assertEq(recovery.getRecoveryNonce(address(account)), 1); + } + + function test_HasApproved() public { + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardian1); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + assertTrue(recovery.hasApproved(address(account), 0, guardian1)); + assertFalse(recovery.hasApproved(address(account), 0, guardian2)); + } + + /*////////////////////////////////////////////////////////////// + UNINSTALL TESTS + //////////////////////////////////////////////////////////////*/ + + function test_OnUninstall() public { + assertTrue(recovery.isInitialized(address(account))); + + vm.prank(address(account)); + recovery.onUninstall(""); + + assertFalse(recovery.isInitialized(address(account))); + } + + /*////////////////////////////////////////////////////////////// + INSTALL TESTS + //////////////////////////////////////////////////////////////*/ + + function test_OnInstall_WithZeroThreshold() public { + SocialRecoveryModule newRecovery = new SocialRecoveryModule(); + + address[] memory guardians = new address[](1); + guardians[0] = guardian1; + + bytes memory recoveryData = abi.encode( + uint256(0), // zero threshold - should default to 1 + uint256(24 hours), + guardians + ); + + vm.prank(address(0x9999)); + newRecovery.onInstall(recoveryData); + + (uint256 threshold,) = newRecovery.getRecoveryConfig(address(0x9999)); + assertEq(threshold, 1); + } + + function test_OnInstall_WithZeroTimelock() public { + SocialRecoveryModule newRecovery = new SocialRecoveryModule(); + + address[] memory guardians = new address[](1); + guardians[0] = guardian1; + + bytes memory recoveryData = abi.encode( + uint256(1), + uint256(0), // zero timelock - should default to 24 hours + guardians + ); + + vm.prank(address(0x9999)); + newRecovery.onInstall(recoveryData); + + (, uint256 timelockPeriod) = newRecovery.getRecoveryConfig(address(0x9999)); + assertEq(timelockPeriod, 24 hours); + } + + function test_OnInstall_WithDuplicateGuardians() public { + SocialRecoveryModule newRecovery = new SocialRecoveryModule(); + + address[] memory guardians = new address[](3); + guardians[0] = guardian1; + guardians[1] = guardian1; // duplicate + guardians[2] = guardian2; + + bytes memory recoveryData = abi.encode(uint256(2), uint256(24 hours), guardians); + + vm.prank(address(0x9999)); + newRecovery.onInstall(recoveryData); + + // Should only have 2 unique guardians + assertEq(newRecovery.getGuardianCount(address(0x9999)), 2); + } } From 9a2966b27ed106781f44c9c9464ef2f834c4bcc7 Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 3 Dec 2025 11:15:22 +0700 Subject: [PATCH 15/22] refactor: migrate to ERC-7579 modular architecture BREAKING CHANGE: Remove legacy P256Account in favor of modular AuraAccount Deleted: - src/P256Account.sol - Legacy monolithic account - src/P256AccountFactory.sol - Legacy factory - test/P256Account.t.sol - Legacy tests - test/P256AccountFactory.t.sol - Legacy tests - script/CreateAccount.s.sol - Legacy script - script/Demo2FA.s.sol - Legacy demo - script/GetInitCodeHash.s.sol - Legacy helper - script/Verify.s.sol - Legacy verification - script/VerifyCreate2.s.sol - Legacy verification - script/VerifyVanityAddress.s.sol - Legacy verification Updated: - script/Deploy.s.sol - Now deploys AuraAccountFactory and P256MFAValidatorModule using Solady CREATE2 factory for deterministic vanity addresses across all networks The new modular architecture provides: - ERC-7579 compliant modular smart accounts - Pluggable validator modules (P256MFAValidatorModule) - Support for executor, fallback, and hook modules - Same deterministic deployment via CREATE2 --- script/CreateAccount.s.sol | 50 -- script/Demo2FA.s.sol | 109 ---- script/Deploy.s.sol | 141 ++-- script/GetInitCodeHash.s.sol | 47 -- script/Verify.s.sol | 108 ---- script/VerifyCreate2.s.sol | 41 -- script/VerifyVanityAddress.s.sol | 61 -- src/P256Account.sol | 1044 ------------------------------ src/P256AccountFactory.sol | 199 ------ test/P256Account.t.sol | 958 --------------------------- test/P256AccountFactory.t.sol | 272 -------- 11 files changed, 78 insertions(+), 2952 deletions(-) delete mode 100644 script/CreateAccount.s.sol delete mode 100644 script/Demo2FA.s.sol delete mode 100644 script/GetInitCodeHash.s.sol delete mode 100644 script/Verify.s.sol delete mode 100644 script/VerifyCreate2.s.sol delete mode 100644 script/VerifyVanityAddress.s.sol delete mode 100644 src/P256Account.sol delete mode 100644 src/P256AccountFactory.sol delete mode 100644 test/P256Account.t.sol delete mode 100644 test/P256AccountFactory.t.sol diff --git a/script/CreateAccount.s.sol b/script/CreateAccount.s.sol deleted file mode 100644 index 901c3b9..0000000 --- a/script/CreateAccount.s.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Script, console2} from "forge-std/Script.sol"; -import {P256AccountFactory} from "../src/P256AccountFactory.sol"; -import {P256Account} from "../src/P256Account.sol"; - -/** - * @title CreateAccountScript - * @notice Script to create a new P256Account - */ -contract CreateAccountScript is Script { - function run() external { - // Get factory address from environment or command line - address factoryAddr = vm.envAddress("FACTORY_ADDRESS"); - P256AccountFactory factory = P256AccountFactory(factoryAddr); - - // Get public key from environment - bytes32 qx = vm.envBytes32("PUBLIC_KEY_X"); - bytes32 qy = vm.envBytes32("PUBLIC_KEY_Y"); - - // Get owner address - address owner = vm.envAddress("OWNER_ADDRESS"); - - // Get salt (default to 0) - uint256 salt = vm.envOr("SALT", uint256(0)); - - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - - vm.startBroadcast(deployerPrivateKey); - - // Predict address - address predictedAddr = factory.getAddress(qx, qy, owner, salt); - console2.log("Predicted account address:", predictedAddr); - - // Create account with 2FA enabled - P256Account account = factory.createAccount(qx, qy, owner, salt, true, bytes32("My Device")); - - console2.log("=== Account Created ==="); - console2.log("Account address:", address(account)); - console2.log("Public Key X:", vm.toString(qx)); - console2.log("Public Key Y:", vm.toString(qy)); - console2.log("Owner:", owner); - console2.log("Salt:", salt); - console2.log("2FA Enabled:", account.twoFactorEnabled()); - console2.log("======================"); - - vm.stopBroadcast(); - } -} diff --git a/script/Demo2FA.s.sol b/script/Demo2FA.s.sol deleted file mode 100644 index 05216f3..0000000 --- a/script/Demo2FA.s.sol +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Script, console2} from "forge-std/Script.sol"; -import {P256Account} from "../src/P256Account.sol"; -import {P256AccountFactory} from "../src/P256AccountFactory.sol"; -import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; - -/** - * @title Demo2FA - * @notice Demo script showing how to use Two-Factor Authentication feature - * @dev Run with: forge script script/Demo2FA.s.sol --rpc-url sepolia --broadcast - */ -contract Demo2FA is Script { - // Sepolia EntryPoint address - address constant ENTRYPOINT_ADDR = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - - P256AccountFactory public factory; - P256Account public account; - address public owner; - - function run() public { - // Get deployer private key from environment - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - owner = vm.addr(deployerPrivateKey); - - console2.log("=== Two-Factor Authentication Demo ==="); - console2.log("Owner address:", owner); - console2.log(""); - - vm.startBroadcast(deployerPrivateKey); - - // Step 1: Deploy factory (if not already deployed) - console2.log("Step 1: Deploying P256AccountFactory..."); - factory = new P256AccountFactory(IEntryPoint(ENTRYPOINT_ADDR)); - console2.log("Factory deployed at:", address(factory)); - console2.log(""); - - // Step 2: Create account with mock passkey - console2.log("Step 2: Creating P256Account with 2FA..."); - bytes32 qx = bytes32(uint256(0x1234567890abcdef)); // Mock public key X - bytes32 qy = bytes32(uint256(0xfedcba0987654321)); // Mock public key Y - uint256 salt = 0; - - account = factory.createAccount(qx, qy, owner, salt, true, bytes32("Demo Device")); - console2.log("Account created at:", address(account)); - console2.log("Public key (qx):", vm.toString(qx)); - console2.log("Public key (qy):", vm.toString(qy)); - console2.log("2FA enabled:", account.twoFactorEnabled()); - console2.log(""); - - // Step 3: Check initial 2FA status - console2.log("Step 3: Checking initial 2FA status..."); - bool is2FAEnabled = account.twoFactorEnabled(); - console2.log("2FA enabled:", is2FAEnabled ? "YES" : "NO"); - console2.log(""); - - // Step 4: Enable 2FA - console2.log("Step 4: Enabling Two-Factor Authentication..."); - account.enableTwoFactor(); - is2FAEnabled = account.twoFactorEnabled(); - console2.log("2FA enabled:", is2FAEnabled ? "YES" : "NO"); - console2.log(""); - - // Step 5: Show signature requirements - console2.log("Step 5: Signature Requirements"); - console2.log("Normal mode (2FA disabled):"); - console2.log(" - Signature format: r (32) || s (32) = 64 bytes"); - console2.log(" - Only P-256 passkey signature required"); - console2.log(""); - console2.log("2FA mode (2FA enabled):"); - console2.log(" - Signature format: r (32) || s (32) || ownerSig (65) = 129 bytes"); - console2.log(" - Both P-256 passkey AND owner ECDSA signatures required"); - console2.log(""); - - // Step 6: Disable 2FA - console2.log("Step 6: Disabling Two-Factor Authentication..."); - account.disableTwoFactor(); - is2FAEnabled = account.twoFactorEnabled(); - console2.log("2FA enabled:", is2FAEnabled ? "YES" : "NO"); - console2.log(""); - - // Step 7: Re-enable 2FA for production use - console2.log("Step 7: Re-enabling 2FA for production..."); - account.enableTwoFactor(); - is2FAEnabled = account.twoFactorEnabled(); - console2.log("2FA enabled:", is2FAEnabled ? "YES" : "NO"); - console2.log(""); - - vm.stopBroadcast(); - - // Summary - console2.log("=== Demo Complete ==="); - console2.log(""); - console2.log("Summary:"); - console2.log(" Factory:", address(factory)); - console2.log(" Account:", address(account)); - console2.log(" Owner:", owner); - console2.log(" 2FA Status:", is2FAEnabled ? "ENABLED" : "DISABLED"); - console2.log(""); - console2.log("Next Steps:"); - console2.log(" 1. Fund the account with ETH"); - console2.log(" 2. Add deposit to EntryPoint for gas"); - console2.log(" 3. Create UserOperation with dual signatures"); - console2.log(" 4. Submit to bundler"); - console2.log(""); - console2.log("For more info, see: docs/TWO_FACTOR_AUTH.md"); - } -} diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index ff49d60..b9a3372 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -2,109 +2,124 @@ pragma solidity ^0.8.23; import {Script, console2} from "forge-std/Script.sol"; -import {P256AccountFactory} from "../src/P256AccountFactory.sol"; -import {P256Account} from "../src/P256Account.sol"; -import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; +import {AuraAccountFactory} from "../src/modular/AuraAccountFactory.sol"; +import {AuraAccount} from "../src/modular/AuraAccount.sol"; +import {P256MFAValidatorModule} from "../src/modular/modules/validators/P256MFAValidatorModule.sol"; /** * @title DeployScript - * @notice Deployment script for P256AccountFactory using CREATE2 for deterministic addresses - * @dev Deploys factory to the SAME address on ALL networks + * @notice Deployment script for ERC-7579 Modular Smart Account using CREATE2 + * @dev Deploys to deterministic addresses on ALL networks: + * 1. P256MFAValidatorModule - The default validator with passkey MFA support + * 2. AuraAccountFactory - Factory that creates modular accounts * * How it works: - * 1. Uses CREATE2 for deterministic deployment - * 2. Same salt + same bytecode = same factory address on all chains + * 1. Uses Solady's CREATE2 factory for deterministic deployment + * 2. Same salt + same bytecode = same addresses on all chains * 3. Users get same account addresses across all networks * * Usage: * forge script script/Deploy.s.sol:DeployScript --rpc-url --broadcast --verify */ contract DeployScript is Script { - // EntryPoint v0.7 address (same on all networks) - address constant ENTRYPOINT_V07 = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - // Solady's canonical CREATE2 factory (deployed on all major chains) address constant SOLADY_CREATE2_FACTORY = 0x0000000000FFe8B47B3e2130213B802212439497; - // Salt for CREATE2 deployment (vanity salt for 0x0000000000 prefix - 5 zero bytes!) - // IMPORTANT: Use the SAME salt on ALL networks to get the same factory address - // First 20 bytes must match deployer address (0x18Ee4C040568238643C07e7aFd6c53efc196D26b) for Solady factory - // Predicted factory address: 0x0000000000569C25b117231B791C9ACaD7A930c7 - bytes32 constant SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b000000000000000dc8cf832f; + // Salt for P256MFAValidatorModule CREATE2 deployment + // First 20 bytes must match deployer address for Solady factory + // TODO: Update with your deployer address and vanity salt + bytes32 constant VALIDATOR_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b0000000000000000000000a1; + + // Salt for AuraAccountFactory CREATE2 deployment + // TODO: Update with your deployer address and vanity salt + bytes32 constant FACTORY_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b0000000000000000000000a2; function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address deployer = vm.addr(deployerPrivateKey); - // Prepare creation code (bytecode + constructor args) - bytes memory creationCode = abi.encodePacked(type(P256AccountFactory).creationCode, abi.encode(ENTRYPOINT_V07)); - - // Calculate init code hash dynamically - bytes32 initCodeHash = keccak256(creationCode); - - // Calculate expected address using CREATE2 formula with Solady factory - address expectedAddress = computeCreate2Address(SOLADY_CREATE2_FACTORY, SALT, initCodeHash); - - console2.log("=== CREATE2 Deployment ==="); + console2.log("=== ERC-7579 Modular Account CREATE2 Deployment ==="); console2.log("Deployer:", deployer); - console2.log("Salt:", vm.toString(SALT)); - console2.log("Init Code Hash:", vm.toString(initCodeHash)); - console2.log("Init Code Length:", creationCode.length, "bytes"); - console2.log("Expected Factory Address:", expectedAddress); + console2.log("CREATE2 Factory:", SOLADY_CREATE2_FACTORY); console2.log(""); - vm.startBroadcast(deployerPrivateKey); + // Step 1: Compute validator creation code and expected address + bytes memory validatorCreationCode = type(P256MFAValidatorModule).creationCode; + bytes32 validatorInitCodeHash = keccak256(validatorCreationCode); + address expectedValidator = computeCreate2Address(SOLADY_CREATE2_FACTORY, VALIDATOR_SALT, validatorInitCodeHash); - // Deploy using Solady's CREATE2 factory - // Call safeCreate2(bytes32 salt, bytes memory initializationCode) - (bool success, bytes memory returnData) = SOLADY_CREATE2_FACTORY.call{value: 0}( - abi.encodeWithSignature("safeCreate2(bytes32,bytes)", SALT, creationCode) - ); + console2.log("=== Step 1: P256MFAValidatorModule ==="); + console2.log("Salt:", vm.toString(VALIDATOR_SALT)); + console2.log("Init Code Hash:", vm.toString(validatorInitCodeHash)); + console2.log("Expected Address:", expectedValidator); + console2.log(""); - require(success, "Solady CREATE2 factory deployment failed"); + // Step 2: Compute factory creation code (depends on validator address) + bytes memory factoryCreationCode = abi.encodePacked( + type(AuraAccountFactory).creationCode, + abi.encode(expectedValidator) + ); + bytes32 factoryInitCodeHash = keccak256(factoryCreationCode); + address expectedFactory = computeCreate2Address(SOLADY_CREATE2_FACTORY, FACTORY_SALT, factoryInitCodeHash); - address factoryAddress = abi.decode(returnData, (address)); + console2.log("=== Step 2: AuraAccountFactory ==="); + console2.log("Salt:", vm.toString(FACTORY_SALT)); + console2.log("Init Code Hash:", vm.toString(factoryInitCodeHash)); + console2.log("Expected Address:", expectedFactory); + console2.log(""); - require(factoryAddress == expectedAddress, "Deployed address mismatch"); + vm.startBroadcast(deployerPrivateKey); - // Try to get implementation address (may fail in simulation if using expected address) - P256AccountFactory factory = P256AccountFactory(factoryAddress); - address implementationAddress; - address proxyFactoryAddress; + // Deploy P256MFAValidatorModule via CREATE2 + address validatorAddress = _deployViaCreate2(VALIDATOR_SALT, validatorCreationCode); + require(validatorAddress == expectedValidator, "Validator address mismatch"); + console2.log("P256MFAValidatorModule deployed at:", validatorAddress); - try factory.IMPLEMENTATION() returns (P256Account impl) { - implementationAddress = address(impl); - proxyFactoryAddress = address(factory.PROXY_FACTORY()); - } catch { - console2.log("Note: Cannot read factory state in simulation (expected for collision case)"); - console2.log("Factory will be deployed on-chain at:", expectedAddress); - } + // Deploy AuraAccountFactory via CREATE2 + address factoryAddress = _deployViaCreate2(FACTORY_SALT, factoryCreationCode); + require(factoryAddress == expectedFactory, "Factory address mismatch"); + console2.log("AuraAccountFactory deployed at:", factoryAddress); vm.stopBroadcast(); + // Get additional info from factory + AuraAccountFactory factory = AuraAccountFactory(factoryAddress); + address implementationAddress = factory.accountImplementation(); + address proxyFactoryAddress = address(factory.PROXY_FACTORY()); + + console2.log(""); console2.log("=== Deployment Complete ==="); - console2.log("EntryPoint:", ENTRYPOINT_V07); - console2.log("P256AccountFactory:", factoryAddress); - if (implementationAddress != address(0)) { - console2.log("P256Account Implementation:", implementationAddress); - console2.log("Solady ERC1967Factory:", proxyFactoryAddress); - } - console2.log("========================"); + console2.log("P256MFAValidatorModule:", validatorAddress); + console2.log("AuraAccountFactory:", factoryAddress); + console2.log("AuraAccount Implementation:", implementationAddress); + console2.log("Solady ERC1967Factory:", proxyFactoryAddress); + console2.log("==========================="); console2.log(""); - console2.log("This factory address is DETERMINISTIC across all networks!"); - console2.log("Deploy with the same salt on other networks to get the same address."); + console2.log("These addresses are DETERMINISTIC across all networks!"); + console2.log("Deploy with the same salts on other networks to get the same addresses."); console2.log(""); - console2.log("Note: Factory uses Solady's canonical ERC-1967 proxy pattern."); - console2.log("Each account is a minimal proxy (~121 bytes) pointing to the implementation."); - console2.log("This saves ~60-70% gas on account deployment."); + console2.log("To create an account, call factory.createAccount() with:"); + console2.log(" - owner: Web3Auth address"); + console2.log(" - validatorData: abi.encode(owner, qx, qy, deviceId, enableMFA)"); + console2.log(" - hook: Optional hook address (address(0) for none)"); + console2.log(" - hookData: Hook initialization data"); + console2.log(" - salt: Unique salt for deterministic address"); + } + + function _deployViaCreate2(bytes32 salt, bytes memory creationCode) internal returns (address) { + (bool success, bytes memory returnData) = SOLADY_CREATE2_FACTORY.call( + abi.encodeWithSignature("safeCreate2(bytes32,bytes)", salt, creationCode) + ); + require(success, "CREATE2 deployment failed"); + return abi.decode(returnData, (address)); } - function computeCreate2Address(address deployer, bytes32 salt, bytes32 initCodeHash) + function computeCreate2Address(address factory, bytes32 salt, bytes32 initCodeHash) internal pure returns (address) { - bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, initCodeHash)); + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), factory, salt, initCodeHash)); return address(uint160(uint256(hash))); } } diff --git a/script/GetInitCodeHash.s.sol b/script/GetInitCodeHash.s.sol deleted file mode 100644 index b194e76..0000000 --- a/script/GetInitCodeHash.s.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Script, console2} from "forge-std/Script.sol"; -import {P256AccountFactory} from "../src/P256AccountFactory.sol"; -import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; - -/** - * @title GetInitCodeHashScript - * @notice Script to compute the init code hash for P256AccountFactory - * @dev This hash is needed for vanity address mining with CREATE2 - */ -contract GetInitCodeHashScript is Script { - // EntryPoint v0.7 address (same on all networks) - address constant ENTRYPOINT_V07 = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - - function run() external view { - // Get the creation code (bytecode + constructor args) - bytes memory creationCode = abi.encodePacked(type(P256AccountFactory).creationCode, abi.encode(ENTRYPOINT_V07)); - - // Compute the keccak256 hash - bytes32 initCodeHash = keccak256(creationCode); - - console2.log("=== P256AccountFactory Init Code Hash ==="); - console2.log("EntryPoint:", ENTRYPOINT_V07); - console2.log(""); - console2.log("Init Code Hash:"); - console2.log(vm.toString(initCodeHash)); - console2.log(""); - console2.log("Init Code (first 100 bytes):"); - console2.logBytes(slice(creationCode, 0, 100)); - console2.log(""); - console2.log("Total Init Code Length:", creationCode.length, "bytes"); - console2.log(""); - console2.log("Use this hash for vanity address mining:"); - console2.log("export INIT_CODE_HASH=\"%s\"", vm.toString(initCodeHash)); - } - - function slice(bytes memory data, uint256 start, uint256 length) internal pure returns (bytes memory) { - bytes memory result = new bytes(length); - for (uint256 i = 0; i < length; i++) { - result[i] = data[start + i]; - } - return result; - } -} - diff --git a/script/Verify.s.sol b/script/Verify.s.sol deleted file mode 100644 index 0fbaf3f..0000000 --- a/script/Verify.s.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Script, console2} from "forge-std/Script.sol"; -import {P256AccountFactory} from "../src/P256AccountFactory.sol"; -import {P256Account} from "../src/P256Account.sol"; -import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; - -/** - * @title VerifyScript - * @notice Script to verify all deployed contracts on Etherscan - * @dev Verifies Factory, Implementation, and optionally Proxy contracts - */ -contract VerifyScript is Script { - // EntryPoint v0.7 address on Sepolia - address constant ENTRYPOINT_V07 = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - - function run() external view { - // Get factory address from environment - address factoryAddress = vm.envAddress("FACTORY_ADDRESS"); - require(factoryAddress != address(0), "FACTORY_ADDRESS not set"); - - P256AccountFactory factory = P256AccountFactory(factoryAddress); - P256Account implementation = factory.IMPLEMENTATION(); - - console2.log("=== Contract Verification Guide ==="); - console2.log(""); - console2.log("Factory Address:", factoryAddress); - console2.log("Implementation Address:", address(implementation)); - console2.log("EntryPoint Address:", ENTRYPOINT_V07); - console2.log(""); - - // Factory verification command - console2.log("=== 1. Verify Factory Contract ==="); - console2.log(""); - console2.log("forge verify-contract \\"); - console2.log(" --chain-id 11155111 \\"); - console2.log(" --num-of-optimizations 200 \\"); - console2.log(" --watch \\"); - console2.log(" --constructor-args $(cast abi-encode \"constructor(address)\"", ENTRYPOINT_V07, ") \\"); - console2.log(" --etherscan-api-key $ETHERSCAN_API_KEY \\"); - console2.log(" --compiler-version v0.8.23 \\"); - console2.log(" ", factoryAddress, "\\"); - console2.log(" src/P256AccountFactory.sol:P256AccountFactory"); - console2.log(""); - - // Implementation verification command - console2.log("=== 2. Verify Implementation Contract ==="); - console2.log(""); - console2.log("forge verify-contract \\"); - console2.log(" --chain-id 11155111 \\"); - console2.log(" --num-of-optimizations 200 \\"); - console2.log(" --watch \\"); - console2.log(" --constructor-args $(cast abi-encode \"constructor(address)\"", ENTRYPOINT_V07, ") \\"); - console2.log(" --etherscan-api-key $ETHERSCAN_API_KEY \\"); - console2.log(" --compiler-version v0.8.23 \\"); - console2.log(" ", address(implementation), "\\"); - console2.log(" src/P256Account.sol:P256Account"); - console2.log(""); - - // Proxy verification note - console2.log("=== 3. Verify Proxy Contracts (Optional) ==="); - console2.log(""); - console2.log("ERC-1967 proxies are automatically recognized by Etherscan."); - console2.log("Once the implementation is verified, Etherscan will show:"); - console2.log(" - 'Read as Proxy' tab"); - console2.log(" - 'Write as Proxy' tab"); - console2.log(" - Implementation address link"); - console2.log(""); - console2.log("To verify a specific proxy account:"); - console2.log(""); - console2.log("forge verify-contract \\"); - console2.log(" --chain-id 11155111 \\"); - console2.log(" --num-of-optimizations 200 \\"); - console2.log(" --watch \\"); - console2.log( - " --constructor-args $(cast abi-encode \"constructor(address,bytes)\"", address(implementation), "0x) \\" - ); - console2.log(" --etherscan-api-key $ETHERSCAN_API_KEY \\"); - console2.log(" --compiler-version v0.8.23 \\"); - console2.log(" \\"); - console2.log(" lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy"); - console2.log(""); - - console2.log("=== Verification Checklist ==="); - console2.log(""); - console2.log("After verification, check on Etherscan:"); - console2.log(" [ ] Factory contract shows green checkmark"); - console2.log(" [ ] Implementation contract shows green checkmark"); - console2.log(" [ ] Factory 'Read Contract' tab works"); - console2.log(" [ ] Can call IMPLEMENTATION() to see implementation address"); - console2.log(" [ ] Proxy accounts show 'Read as Proxy' tab"); - console2.log(" [ ] Proxy points to correct implementation"); - console2.log(""); - console2.log("=== Troubleshooting ==="); - console2.log(""); - console2.log("If verification fails:"); - console2.log(" 1. Check compiler version matches (v0.8.23)"); - console2.log(" 2. Check optimization settings (200 runs)"); - console2.log(" 3. Verify constructor args are correct"); - console2.log(" 4. Check ETHERSCAN_API_KEY is set"); - console2.log(" 5. Wait a few minutes and try again"); - console2.log(""); - console2.log("For detailed logs, add --show-standard-json-input flag"); - console2.log(""); - } -} - diff --git a/script/VerifyCreate2.s.sol b/script/VerifyCreate2.s.sol deleted file mode 100644 index 2e2144a..0000000 --- a/script/VerifyCreate2.s.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Script, console2} from "forge-std/Script.sol"; - -/** - * @title VerifyCreate2Script - * @notice Manually verify CREATE2 address calculation - */ -contract VerifyCreate2Script is Script { - function run() external pure { - // Solady's ImmutableCreate2Factory - the actual deployer for CREATE2 - address deployer = 0x0000000000FFe8B47B3e2130213B802212439497; - // Salt to verify (first 20 bytes = your EOA address) - bytes32 salt = 0x18ee4c040568238643c07e7afd6c53efc196d26b000000000000000dc8cf832f; - // Init code hash for P256AccountFactory (run GetInitCodeHash.s.sol to get this) - bytes32 initCodeHash = 0x747dd63dfae991117debeb008f2fb0533bb59a6eee74ba0e197e21099d034c7a; - - bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, initCodeHash)); - address predicted = address(uint160(uint256(hash))); - - console2.log("=== Manual CREATE2 Verification ==="); - console2.log("Deployer:", deployer); - console2.log("Salt:", vm.toString(salt)); - console2.log("Init Code Hash:", vm.toString(initCodeHash)); - console2.log(""); - console2.log("Predicted Address:", predicted); - console2.log(""); - - // Check if it starts with 0x000000 - bytes20 addrBytes = bytes20(predicted); - bool hasVanityPrefix = addrBytes[0] == 0x00 && addrBytes[1] == 0x00 && addrBytes[2] == 0x00; - - if (hasVanityPrefix) { - console2.log("SUCCESS: Address starts with 0x000000!"); - } else { - console2.log("ERROR: Address does NOT start with 0x000000"); - } - } -} - diff --git a/script/VerifyVanityAddress.s.sol b/script/VerifyVanityAddress.s.sol deleted file mode 100644 index d7541da..0000000 --- a/script/VerifyVanityAddress.s.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Script, console2} from "forge-std/Script.sol"; -import {P256AccountFactory} from "../src/P256AccountFactory.sol"; -import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; - -/** - * @title VerifyVanityAddressScript - * @notice Verifies the vanity address calculation for P256AccountFactory - */ -contract VerifyVanityAddressScript is Script { - // Your deployer address - address constant DEPLOYER = 0x18Ee4C040568238643C07e7aFd6c53efc196D26b; - - // EntryPoint v0.7 address - address constant ENTRYPOINT_V07 = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - - // Vanity salt to verify - bytes32 constant SALT = 0x1eb54b512d3bb151834ddb91521c2344e3b598a3facafb4d2c2b633c93e628c1; - - function run() external view { - console2.log("=== Vanity Address Verification ==="); - console2.log("Deployer:", DEPLOYER); - console2.log("Salt:", vm.toString(SALT)); - console2.log(""); - - // Get the creation code - bytes memory creationCode = abi.encodePacked(type(P256AccountFactory).creationCode, abi.encode(ENTRYPOINT_V07)); - - bytes32 initCodeHash = keccak256(creationCode); - console2.log("Init Code Hash:", vm.toString(initCodeHash)); - console2.log(""); - - // Compute CREATE2 address - address predicted = computeCreate2Address(DEPLOYER, SALT, initCodeHash); - - console2.log("Predicted Factory Address:", predicted); - console2.log(""); - - // Verify it starts with 0x000000 - bytes20 addrBytes = bytes20(predicted); - bool hasVanityPrefix = addrBytes[0] == 0x00 && addrBytes[1] == 0x00 && addrBytes[2] == 0x00; - - if (hasVanityPrefix) { - console2.log("SUCCESS: Address starts with 0x000000!"); - } else { - console2.log("ERROR: Address does NOT start with 0x000000"); - } - } - - function computeCreate2Address(address deployer, bytes32 salt, bytes32 initCodeHash) - internal - pure - returns (address) - { - bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, initCodeHash)); - return address(uint160(uint256(hash))); - } -} - diff --git a/src/P256Account.sol b/src/P256Account.sol deleted file mode 100644 index 0e9b598..0000000 --- a/src/P256Account.sol +++ /dev/null @@ -1,1044 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {IAccount} from "@account-abstraction/interfaces/IAccount.sol"; -import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; -import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; -import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {Initializable} from "solady/utils/Initializable.sol"; -import {P256} from "solady/utils/P256.sol"; -import {WebAuthn} from "solady/utils/WebAuthn.sol"; -import {LibBytes} from "solady/utils/LibBytes.sol"; - -/** - * @title P256Account - * @notice ERC-4337 Account Abstraction wallet with P-256/secp256r1 signature support - * @dev Supports both raw P-256 signatures and WebAuthn/Passkey signatures - * @dev Designed to be used with ERC-1967 proxy pattern for gas-efficient deployment - */ -contract P256Account is IAccount, IERC1271, Ownable, Initializable { - /*////////////////////////////////////////////////////////////// - CONSTANTS - //////////////////////////////////////////////////////////////*/ - - /// @notice EIP-1271 magic value for valid signature - bytes4 internal constant MAGICVALUE = 0x1626ba7e; - - /// @notice The EntryPoint contract - IEntryPoint public immutable ENTRYPOINT; - - /*////////////////////////////////////////////////////////////// - STORAGE - //////////////////////////////////////////////////////////////*/ - - /// @notice Nonce for replay protection (in addition to EntryPoint nonce) - uint256 public nonce; - - /// @notice Two-factor authentication enabled flag - /// @dev When enabled, transactions require both P-256 passkey signature and owner ECDSA signature - bool public twoFactorEnabled; - - /*////////////////////////////////////////////////////////////// - MULTIPLE PASSKEYS - //////////////////////////////////////////////////////////////*/ - - /// @notice Passkey information - struct PasskeyInfo { - bytes32 qx; // Public key x-coordinate - bytes32 qy; // Public key y-coordinate - uint256 addedAt; // Timestamp when passkey was added - bool active; // Whether the passkey is active - bytes32 deviceId; // Short device identifier (e.g., "iPhone 15", "YubiKey 5") - } - - /// @notice Passkey storage by ID - /// @dev passkeyId = keccak256(abi.encodePacked(qx, qy)) - mapping(bytes32 => PasskeyInfo) public passkeys; - - /// @notice List of ACTIVE passkey IDs for enumeration - /// @dev Gas optimization: Only active passkeys are stored in this array - /// @dev Inactive passkeys remain in the mapping for history but are removed from array - bytes32[] public passkeyIds; - - /*////////////////////////////////////////////////////////////// - GUARDIAN & RECOVERY - //////////////////////////////////////////////////////////////*/ - - /// @notice Timelock duration for recovery execution (24 hours) - /// @dev Gives users time to detect and cancel malicious recovery attempts - uint256 public constant RECOVERY_TIMELOCK = 24 hours; - - /// @notice Guardian addresses - mapping(address => bool) public guardians; - - /// @notice List of guardian addresses - address[] public guardianList; - - /// @notice Number of guardian approvals required for recovery - uint256 public guardianThreshold; - - /// @notice Recovery request nonce - uint256 public recoveryNonce; - - /// @notice List of all pending action hashes (for enumeration) - bytes32[] public pendingActionHashes; - - /// @notice Recovery request - struct RecoveryRequest { - bytes32 newQx; - bytes32 newQy; - address newOwner; - uint256 approvalCount; - mapping(address => bool) approvals; - uint256 executeAfter; - bool executed; - bool cancelled; - } - - /// @notice Active recovery requests - mapping(uint256 => RecoveryRequest) public recoveryRequests; - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - event PublicKeyUpdated(bytes32 indexed qx, bytes32 indexed qy); - event P256AccountInitialized(IEntryPoint indexed entryPoint, bytes32 qx, bytes32 qy); - event TwoFactorEnabled(address indexed owner); - event TwoFactorDisabled(address indexed owner); - - // Passkey events - event PasskeyAdded(bytes32 indexed passkeyId, bytes32 qx, bytes32 qy, uint256 timestamp); - event PasskeyRemoved(bytes32 indexed passkeyId, bytes32 qx, bytes32 qy); - - // Guardian events - event GuardianAdded(address indexed guardian); - event GuardianRemoved(address indexed guardian); - event GuardianThresholdChanged(uint256 newThreshold); - - // Recovery events - event RecoveryInitiated( - uint256 indexed nonce, address indexed initiator, bytes32 newQx, bytes32 newQy, address newOwner - ); - event RecoveryApproved(uint256 indexed nonce, address indexed guardian); - event RecoveryExecuted(uint256 indexed nonce); - event RecoveryCancelled(uint256 indexed nonce); - - // Timelock events - - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error OnlyEntryPoint(); - error OnlyEntryPointOrOwner(); - error InvalidSignature(); - error InvalidSignatureLength(); - error CallFailed(bytes result); - error TwoFactorSignatureRequired(); - error InvalidOwnerSignature(); - - // Passkey errors - error PasskeyAlreadyExists(); - error PasskeyDoesNotExist(); - error PasskeyNotActive(); - error CannotRemoveLastPasskey(); - error InvalidPasskeyCoordinates(); - - // Guardian errors - error NotGuardian(); - error GuardianAlreadyExists(); - error GuardianDoesNotExist(); - error InvalidThreshold(); - error InsufficientGuardians(); - - // Recovery errors - error RecoveryNotFound(); - error RecoveryAlreadyExecuted(); - error RecoveryAlreadyCancelled(); - error RecoveryAlreadyApproved(); - error RecoveryNotReady(); - error InsufficientApprovals(); - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Constructor for P256Account - * @param _entryPoint The EntryPoint contract address - * @dev Locks the implementation contract to prevent initialization - */ - constructor(IEntryPoint _entryPoint) Ownable(msg.sender) { - ENTRYPOINT = _entryPoint; - _disableInitializers(); // Lock the implementation contract - } - - /** - * @notice Initialize the account with a P-256 public key - * @param _qx The x-coordinate of the public key (can be 0 for owner-only mode) - * @param _qy The y-coordinate of the public key (can be 0 for owner-only mode) - * @param _owner The owner of the account - * @param _enable2FA Whether to enable two-factor authentication immediately - * @param _deviceId Short device identifier (e.g., "iPhone 15", "YubiKey 5") - * @dev If _qx and _qy are both 0, the account operates in owner-only mode (no passkey) - * @dev If _qx and _qy are set but _enable2FA is false, passkey can be used but 2FA is not required - * @dev If _enable2FA is true, both _qx and _qy must be non-zero - * @dev Uses Solady's Initializable to ensure this can only be called once per proxy - */ - function initialize(bytes32 _qx, bytes32 _qy, address _owner, bool _enable2FA, bytes32 _deviceId) - external - initializer - { - // If enabling 2FA, passkey must be provided - if (_enable2FA) { - require(_qx != bytes32(0) && _qy != bytes32(0), "2FA requires passkey"); - } - - // Add first passkey if provided - if (_qx != bytes32(0) && _qy != bytes32(0)) { - _addPasskeyInternal(_qx, _qy, _deviceId); - } - - _transferOwnership(_owner); - - // Add owner as the first guardian - guardians[_owner] = true; - guardianList.push(_owner); - guardianThreshold = 1; // Owner alone can initiate recovery - - // Set two-factor authentication based on parameter (default: false) - twoFactorEnabled = _enable2FA; - - emit P256AccountInitialized(ENTRYPOINT, _qx, _qy); - emit GuardianAdded(_owner); - - if (_enable2FA) { - emit TwoFactorEnabled(_owner); - } - } - - /*////////////////////////////////////////////////////////////// - ERC-4337 FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Validate a user operation - * @param userOp The user operation to validate - * @param userOpHash The hash of the user operation - * @param missingAccountFunds The amount of funds missing from the account - * @return validationData Packed validation data (sigFailed, validUntil, validAfter) - */ - function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) - external - returns (uint256 validationData) - { - if (msg.sender != address(ENTRYPOINT)) { - revert OnlyEntryPoint(); - } - - // Verify the signature - validationData = _validateSignature(userOp, userOpHash); - - // Pay the EntryPoint if needed - _payPrefund(missingAccountFunds); - } - - /** - * @notice Validate the signature of a user operation - * @param userOp The user operation - * @param userOpHash The hash of the user operation - * @return validationData 0 if valid, 1 if invalid - * @dev Two signature modes: - * 1. Owner-only: 65-byte ECDSA signature from owner - * - Used when no passkeys configured - * - Used when passkeys exist but twoFactorEnabled=false - * 2. Passkey with 2FA: WebAuthn signature (Solady compact format) + passkeyId(32) + 65-byte ECDSA signature from owner - * - Used when passkeys exist and twoFactorEnabled=true - * - Client specifies which passkey to verify against (no loop needed) - * - * WebAuthn signature format (2FA mode) - Solady compact encoding: - * authDataLen(2) || authenticatorData || clientDataJSON || challengeIdx(2) || typeIdx(2) || r(32) || s(32) || passkeyId(32) || ownerSig(65) - * - * SECURITY: The challenge field in clientDataJSON MUST contain the base64url-encoded userOpHash. - * This ensures the WebAuthn signature is actually authorizing this specific transaction. - * SECURITY: Any active passkey can authorize - client specifies which one via passkeyId - * GAS OPTIMIZATION: Client passes passkeyId to avoid looping through all passkeys (O(1) instead of O(n)) - */ - function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) - internal - view - returns (uint256 validationData) - { - bytes calldata sig = userOp.signature; - - // During counterfactual deployment, storage is empty, so we need to extract - // the public key and 2FA setting from the initCode - (bytes32 _qx, bytes32 _qy, bool _twoFactorEnabled, address _owner) = _getAccountParams(userOp); - - // Owner-only mode: no passkey OR passkey configured but 2FA disabled - if (_qx == bytes32(0) || !_twoFactorEnabled) { - if (sig.length != 65) return 1; - return _recoverSigner(userOpHash, sig) == _owner ? 0 : 1; - } - - // Passkey with 2FA: requires WebAuthn signature + passkeyId + owner signature - // Minimum: authDataLen(2) + authData(37) + clientData(20) + challengeIdx(2) + typeIdx(2) + r(32) + s(32) + passkeyId(32) + ownerSig(65) = 224 bytes - if (sig.length < 224) return 1; - - // Extract owner signature (last 65 bytes) - bytes calldata ownerSig = sig[sig.length - 65:]; - - // Extract passkeyId (32 bytes before owner signature) - bytes32 passkeyId = bytes32(sig[sig.length - 97:sig.length - 65]); - - // Extract WebAuthn compact signature (everything except passkeyId and owner signature) - bytes calldata webAuthnSig = sig[:sig.length - 97]; - - // Decode WebAuthn signature using Solady's compact decoder - // This handles the format: authDataLen(2) || authenticatorData || clientDataJSON || challengeIdx(2) || typeIdx(2) || r(32) || s(32) - WebAuthn.WebAuthnAuth memory auth = WebAuthn.tryDecodeAuthCompactCalldata(webAuthnSig); - - bytes memory challenge = abi.encodePacked(userOpHash); - - // SECURITY: Verify signature against the specified passkey (O(1) lookup) - // Client specifies which passkey they're using via passkeyId - bool webAuthnValid = false; - - // During counterfactual deployment, check against the initial passkey from initCode - if (userOp.initCode.length >= 184) { - webAuthnValid = WebAuthn.verify( - challenge, - true, // requireUserVerification - enforce UV flag for security - auth, - _qx, - _qy - ); - } else { - // After deployment, check against the specified passkey - PasskeyInfo storage passkeyInfo = passkeys[passkeyId]; - - // SECURITY: Verify passkey exists and is active - if (passkeyInfo.active && passkeyInfo.qx != bytes32(0)) { - webAuthnValid = WebAuthn.verify( - challenge, - true, // requireUserVerification - enforce UV flag for security - auth, - passkeyInfo.qx, - passkeyInfo.qy - ); - } - } - - if (!webAuthnValid) return 1; - - // Verify owner signature - if (_recoverSigner(userOpHash, ownerSig) != _owner) return 1; - - return 0; // SIG_VALIDATION_SUCCESS - } - - /** - * @notice Get account parameters (qx, qy, twoFactorEnabled, owner) - * During counterfactual deployment, extract from initCode; otherwise from storage - * @param userOp The user operation - * @return _qx Public key X coordinate - * @return _qy Public key Y coordinate - * @return _twoFactorEnabled Whether 2FA is enabled - * @return _owner Owner address - */ - function _getAccountParams(PackedUserOperation calldata userOp) - internal - view - returns (bytes32 _qx, bytes32 _qy, bool _twoFactorEnabled, address _owner) - { - // Default to storage values - // Get first passkey if available - if (passkeyIds.length > 0) { - PasskeyInfo storage firstPasskey = passkeys[passkeyIds[0]]; - _qx = firstPasskey.qx; - _qy = firstPasskey.qy; - } - _twoFactorEnabled = twoFactorEnabled; - _owner = owner(); - - // Check if this is a counterfactual deployment (initCode is set in userOp) - // initCode format: factory (20 bytes) + factoryData - // factoryData format: createAccount(bytes32 qx, bytes32 qy, address owner, uint256 salt, bool enable2FA) - bytes calldata initCode = userOp.initCode; - if (initCode.length >= 184) { - // Extract parameters from initCode using Solady's LibBytes - // Skip factory address (20 bytes) and selector (4 bytes) = 24 bytes offset - // Load qx at offset 24 - _qx = LibBytes.loadCalldata(initCode, 24); - // Load qy at offset 56 (24 + 32) - _qy = LibBytes.loadCalldata(initCode, 56); - // Load owner at offset 88 (24 + 32 + 32), shift right 96 bits to get address - _owner = address(uint160(uint256(LibBytes.loadCalldata(initCode, 88)))); - // Skip salt at offset 120 (24 + 32 + 32 + 32) - // Load enable2FA at offset 152 (24 + 32 + 32 + 32 + 32) - _twoFactorEnabled = uint256(LibBytes.loadCalldata(initCode, 152)) != 0; - } - } - - /** - * @notice Pay the EntryPoint the required prefund - * @param missingAccountFunds The amount to pay - */ - function _payPrefund(uint256 missingAccountFunds) internal { - if (missingAccountFunds != 0) { - (bool success,) = payable(msg.sender).call{value: missingAccountFunds}(""); - require(success, "Prefund failed"); - } - } - - /** - * @notice Recover signer address from ECDSA signature - * @param hash The hash that was signed - * @param signature The ECDSA signature (65 bytes: r, s, v) - * @return The recovered signer address - */ - function _recoverSigner(bytes32 hash, bytes calldata signature) internal pure returns (address) { - require(signature.length == 65, "Invalid signature length"); - - bytes32 r; - bytes32 s; - uint8 v; - - assembly { - r := calldataload(signature.offset) - s := calldataload(add(signature.offset, 32)) - v := byte(0, calldataload(add(signature.offset, 64))) - } - - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. - if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - revert InvalidOwnerSignature(); - } - - if (v != 27 && v != 28) { - revert InvalidOwnerSignature(); - } - - // If the signature is valid (and not malleable), return the signer address - address signer = ecrecover(hash, v, r, s); - if (signer == address(0)) { - revert InvalidOwnerSignature(); - } - - return signer; - } - - /*////////////////////////////////////////////////////////////// - EIP-1271 FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Verify a signature according to EIP-1271 - * @param hash The hash of the data to verify - * @param signature The signature to verify (r || s || passkeyId - 96 bytes) - * @return magicValue The EIP-1271 magic value if valid - * @dev Signature format: r(32) || s(32) || passkeyId(32) - 96 bytes - * The passkeyId enables O(1) lookup instead of iterating through all passkeys - */ - function isValidSignature(bytes32 hash, bytes calldata signature) - external - view - override - returns (bytes4 magicValue) - { - if (signature.length != 96) { - revert InvalidSignatureLength(); - } - - bytes32 r; - bytes32 s; - assembly { - r := calldataload(signature.offset) - s := calldataload(add(signature.offset, 32)) - } - - // Use SHA-256 for consistency with validateUserOp - bytes32 messageHash = sha256(abi.encodePacked(hash)); - - // Extract passkeyId from signature - bytes32 passkeyId = bytes32(signature[64:96]); - PasskeyInfo storage passkeyInfo = passkeys[passkeyId]; - - // SECURITY: Verify passkey exists and is active - bool isValid = false; - if (passkeyInfo.active && passkeyInfo.qx != bytes32(0)) { - isValid = P256.verifySignature(messageHash, r, s, passkeyInfo.qx, passkeyInfo.qy); - } - - return isValid ? MAGICVALUE : bytes4(0); - } - - /*////////////////////////////////////////////////////////////// - ACCOUNT MANAGEMENT - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Enable two-factor authentication - * @dev When enabled, all transactions require both P-256 passkey signature and owner ECDSA signature - * @dev Can only be called via UserOperation (passkey signature or owner signature) - * @dev Requires at least one active passkey to be configured - */ - function enableTwoFactor() external { - if (msg.sender != address(ENTRYPOINT)) revert OnlyEntryPoint(); - require(!twoFactorEnabled, "2FA already enabled"); - - // Check for at least one ACTIVE passkey - require(passkeyIds.length > 0, "Active passkey required for 2FA"); - - twoFactorEnabled = true; - emit TwoFactorEnabled(owner()); - } - - /** - * @notice Disable two-factor authentication - * @dev When disabled, transactions only require P-256 passkey signature (or owner signature if no passkey) - * @dev Can only be called via UserOperation (passkey signature or owner signature) - */ - function disableTwoFactor() external { - if (msg.sender != address(ENTRYPOINT)) revert OnlyEntryPoint(); - require(twoFactorEnabled, "2FA already disabled"); - twoFactorEnabled = false; - emit TwoFactorDisabled(owner()); - } - - /*////////////////////////////////////////////////////////////// - PASSKEY MANAGEMENT - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Add a new passkey to the account - * @param _qx The x-coordinate of the new passkey - * @param _qy The y-coordinate of the new passkey - * @param _deviceId Short device identifier (e.g., "iPhone 15", "YubiKey 5") - * @dev SECURITY: Only callable via EntryPoint (requires existing signature) - * @dev Validates that coordinates are non-zero and passkey doesn't already exist - */ - function addPasskey(bytes32 _qx, bytes32 _qy, bytes32 _deviceId) external { - if (msg.sender != address(ENTRYPOINT)) revert OnlyEntryPoint(); - - // Validate coordinates - if (_qx == bytes32(0) || _qy == bytes32(0)) revert InvalidPasskeyCoordinates(); - - // Add the passkey immediately - _addPasskeyInternal(_qx, _qy, _deviceId); - } - - /** - * @notice Remove a passkey from the account - * @param _qx The x-coordinate of the passkey to remove - * @param _qy The y-coordinate of the passkey to remove - * @dev SECURITY: Only callable via EntryPoint (requires existing passkey signature) - * @dev Cannot remove the last active passkey when 2FA is enabled - */ - function removePasskey(bytes32 _qx, bytes32 _qy) external { - if (msg.sender != address(ENTRYPOINT)) revert OnlyEntryPoint(); - - bytes32 passkeyId = keccak256(abi.encodePacked(_qx, _qy)); - - // Verify passkey exists and is active - if (!passkeys[passkeyId].active) revert PasskeyNotActive(); - - // Prevent removing last passkey when 2FA is enabled - if (passkeyIds.length <= 1 && twoFactorEnabled) revert CannotRemoveLastPasskey(); - - PasskeyInfo storage passkeyInfo = passkeys[passkeyId]; - - // Mark as inactive and remove from passkeyIds array - passkeyInfo.active = false; - _removePasskeyFromArray(passkeyId); - - emit PasskeyRemoved(passkeyId, passkeyInfo.qx, passkeyInfo.qy); - } - - /** - * @notice Internal function to add a passkey - * @param _qx The x-coordinate of the passkey - * @param _qy The y-coordinate of the passkey - * @param _deviceId Short device identifier (e.g., "iPhone 15", "YubiKey 5") - * @dev Used by initialize() and addPasskey() - */ - function _addPasskeyInternal(bytes32 _qx, bytes32 _qy, bytes32 _deviceId) internal { - bytes32 passkeyId = keccak256(abi.encodePacked(_qx, _qy)); - - // Check if passkey already exists - if (passkeys[passkeyId].qx != bytes32(0)) revert PasskeyAlreadyExists(); - - // Add passkey - passkeys[passkeyId] = - PasskeyInfo({qx: _qx, qy: _qy, addedAt: block.timestamp, active: true, deviceId: _deviceId}); - - passkeyIds.push(passkeyId); - - emit PasskeyAdded(passkeyId, _qx, _qy, block.timestamp); - } - - /** - * @notice Remove a passkey from the passkeyIds array - * @param passkeyId The ID of the passkey to remove - * @dev Uses swap-and-pop pattern for gas efficiency - */ - function _removePasskeyFromArray(bytes32 passkeyId) internal { - uint256 length = passkeyIds.length; - for (uint256 i = 0; i < length; i++) { - if (passkeyIds[i] == passkeyId) { - // Swap with last element and pop - passkeyIds[i] = passkeyIds[length - 1]; - passkeyIds.pop(); - return; - } - } - } - - /** - * @notice Execute a call from this account - * @param dest The destination address - * @param value The amount of ETH to send - * @param func The calldata - * @dev SECURITY: Only callable via EntryPoint (passkey signature required) - */ - function execute(address dest, uint256 value, bytes calldata func) external { - if (msg.sender != address(ENTRYPOINT)) revert OnlyEntryPoint(); - _call(dest, value, func); - } - - /** - * @notice Execute a batch of calls from this account - * @param dest Array of destination addresses - * @param value Array of ETH amounts - * @param func Array of calldata - * @dev SECURITY: Only callable via EntryPoint (passkey signature required) - */ - function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external { - if (msg.sender != address(ENTRYPOINT)) revert OnlyEntryPoint(); - require(dest.length == func.length && dest.length == value.length, "Length mismatch"); - - for (uint256 i = 0; i < dest.length; i++) { - _call(dest[i], value[i], func[i]); - } - } - - /*////////////////////////////////////////////////////////////// - GUARDIAN MANAGEMENT - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Add a guardian (via passkey signature through EntryPoint) - * @param guardian The guardian address to add - * @dev Only callable via UserOperation (passkey signature) through execute() - */ - function addGuardian(address guardian) external { - if (msg.sender != address(this)) revert OnlyEntryPointOrOwner(); - if (guardians[guardian]) revert GuardianAlreadyExists(); - if (guardian == address(0)) revert InvalidThreshold(); - - guardians[guardian] = true; - guardianList.push(guardian); - - emit GuardianAdded(guardian); - } - - /** - * @notice Remove a guardian (via passkey signature through EntryPoint) - * @param guardian The guardian address to remove - * @dev Only callable via UserOperation (passkey signature) through execute() - */ - function removeGuardian(address guardian) external { - if (msg.sender != address(this)) revert OnlyEntryPointOrOwner(); - if (!guardians[guardian]) revert GuardianDoesNotExist(); - - guardians[guardian] = false; - - // Remove from guardianList - for (uint256 i = 0; i < guardianList.length; i++) { - if (guardianList[i] == guardian) { - guardianList[i] = guardianList[guardianList.length - 1]; - guardianList.pop(); - break; - } - } - - emit GuardianRemoved(guardian); - } - - /** - * @notice Set the guardian threshold (via passkey signature through EntryPoint) - * @param threshold The new threshold - * @dev Only callable via UserOperation (passkey signature) through execute() - */ - function setGuardianThreshold(uint256 threshold) external { - if (msg.sender != address(this)) revert OnlyEntryPointOrOwner(); - if (threshold == 0 || threshold > guardianList.length) revert InvalidThreshold(); - - guardianThreshold = threshold; - emit GuardianThresholdChanged(threshold); - } - - /** - * @notice Get the list of guardians - * @return The array of guardian addresses - */ - function getGuardians() external view returns (address[] memory) { - return guardianList; - } - - /** - * @notice Get the number of guardians - * @return The count of guardians - */ - function getGuardianCount() external view returns (uint256) { - return guardianList.length; - } - - /*////////////////////////////////////////////////////////////// - SOCIAL RECOVERY - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Initiate a recovery request - * @param newQx The new x-coordinate of the public key - * @param newQy The new y-coordinate of the public key - * @param newOwner The new owner address - * @dev Can be called by any guardian - */ - function initiateRecovery(bytes32 newQx, bytes32 newQy, address newOwner) external { - if (!guardians[msg.sender]) revert NotGuardian(); - if (guardianThreshold == 0) revert InvalidThreshold(); - - uint256 requestNonce = recoveryNonce++; - RecoveryRequest storage request = recoveryRequests[requestNonce]; - - request.newQx = newQx; - request.newQy = newQy; - request.newOwner = newOwner; - request.approvalCount = 1; - request.approvals[msg.sender] = true; - request.executeAfter = block.timestamp + RECOVERY_TIMELOCK; - request.executed = false; - request.cancelled = false; - - emit RecoveryInitiated(requestNonce, msg.sender, newQx, newQy, newOwner); - emit RecoveryApproved(requestNonce, msg.sender); - } - - /** - * @notice Approve a recovery request - * @param requestNonce The recovery request nonce - * @dev Can be called by any guardian - */ - function approveRecovery(uint256 requestNonce) external { - if (!guardians[msg.sender]) revert NotGuardian(); - - RecoveryRequest storage request = recoveryRequests[requestNonce]; - if (request.executeAfter == 0) revert RecoveryNotFound(); - if (request.executed) revert RecoveryAlreadyExecuted(); - if (request.cancelled) revert RecoveryAlreadyCancelled(); - if (request.approvals[msg.sender]) revert RecoveryAlreadyApproved(); - - request.approvals[msg.sender] = true; - request.approvalCount++; - - emit RecoveryApproved(requestNonce, msg.sender); - } - - /** - * @notice Execute a recovery request - * @param requestNonce The recovery request nonce - * @dev Can be called by anyone after threshold is met and timelock expires - * @dev SECURITY: Replaces ALL passkeys with the new recovery passkey - */ - function executeRecovery(uint256 requestNonce) external { - RecoveryRequest storage request = recoveryRequests[requestNonce]; - - if (request.executeAfter == 0) revert RecoveryNotFound(); - if (request.executed) revert RecoveryAlreadyExecuted(); - if (request.cancelled) revert RecoveryAlreadyCancelled(); - if (request.approvalCount < guardianThreshold) revert InsufficientApprovals(); - if (block.timestamp < request.executeAfter) revert RecoveryNotReady(); - - request.executed = true; - - // SECURITY: Deactivate ALL existing passkeys during recovery - // This prevents the compromised passkeys from being used - // Clear the array by iterating backwards to avoid index issues - while (passkeyIds.length > 0) { - bytes32 passkeyId = passkeyIds[passkeyIds.length - 1]; - passkeys[passkeyId].active = false; - passkeyIds.pop(); - } - - // Add the new recovery passkey (if provided) - // If qx=0 and qy=0, recovery is to owner-only mode (no passkey) - if (request.newQx != bytes32(0) || request.newQy != bytes32(0)) { - _addPasskeyInternal(request.newQx, request.newQy, bytes32(0)); // No device ID for recovery - } else { - // SECURITY: Auto-disable 2FA when recovering to owner-only mode - // Cannot have 2FA enabled without any passkeys - if (twoFactorEnabled) { - twoFactorEnabled = false; - emit TwoFactorDisabled(request.newOwner); - } - } - - _transferOwnership(request.newOwner); - - emit RecoveryExecuted(requestNonce); - emit PublicKeyUpdated(request.newQx, request.newQy); - } - - /** - * @notice Cancel a recovery request (via passkey signature through EntryPoint) - * @param requestNonce The recovery request nonce - * @dev Only callable via UserOperation (passkey signature) through execute() - */ - function cancelRecovery(uint256 requestNonce) external { - if (msg.sender != address(this)) revert OnlyEntryPointOrOwner(); - - RecoveryRequest storage request = recoveryRequests[requestNonce]; - if (request.executeAfter == 0) revert RecoveryNotFound(); - if (request.executed) revert RecoveryAlreadyExecuted(); - if (request.cancelled) revert RecoveryAlreadyCancelled(); - - request.cancelled = true; - emit RecoveryCancelled(requestNonce); - } - - /** - * @notice Get recovery request details - * @param requestNonce The recovery request nonce - * @return newQx The new x-coordinate - * @return newQy The new y-coordinate - * @return newOwner The new owner address - * @return approvalCount The number of approvals - * @return executeAfter The timestamp after which the request can be executed - * @return executed Whether the request has been executed - * @return cancelled Whether the request has been cancelled - */ - function getRecoveryRequest(uint256 requestNonce) - external - view - returns ( - bytes32 newQx, - bytes32 newQy, - address newOwner, - uint256 approvalCount, - uint256 executeAfter, - bool executed, - bool cancelled - ) - { - RecoveryRequest storage request = recoveryRequests[requestNonce]; - return ( - request.newQx, - request.newQy, - request.newOwner, - request.approvalCount, - request.executeAfter, - request.executed, - request.cancelled - ); - } - - /** - * @notice Check if a guardian has approved a recovery request - * @param requestNonce The recovery request nonce - * @param guardian The guardian address - * @return Whether the guardian has approved - */ - function hasApprovedRecovery(uint256 requestNonce, address guardian) external view returns (bool) { - return recoveryRequests[requestNonce].approvals[guardian]; - } - - /*////////////////////////////////////////////////////////////// - PASSKEY VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Get the number of active passkeys - * @return The count of active passkeys - * @dev passkeyIds array only contains active passkeys - */ - function getActivePasskeyCount() external view returns (uint256) { - return passkeyIds.length; - } - - /** - * @notice Get passkey information by index - * @param index The index in the passkeyIds array - * @return passkeyId The passkey ID - * @return qx The x-coordinate - * @return qy The y-coordinate - * @return addedAt The timestamp when added - * @return active Whether the passkey is active - */ - function getPasskeyByIndex(uint256 index) - external - view - returns (bytes32 passkeyId, bytes32 qx, bytes32 qy, uint256 addedAt, bool active) - { - require(index < passkeyIds.length, "Index out of bounds"); - passkeyId = passkeyIds[index]; - PasskeyInfo storage info = passkeys[passkeyId]; - return (passkeyId, info.qx, info.qy, info.addedAt, info.active); - } - - /** - * @notice Get passkey information by ID - * @param passkeyId The passkey ID (keccak256(qx, qy)) - * @return qx The x-coordinate - * @return qy The y-coordinate - * @return addedAt The timestamp when added - * @return active Whether the passkey is active - */ - function getPasskeyById(bytes32 passkeyId) - external - view - returns (bytes32 qx, bytes32 qy, uint256 addedAt, bool active) - { - PasskeyInfo storage info = passkeys[passkeyId]; - require(info.qx != bytes32(0), "Passkey does not exist"); - return (info.qx, info.qy, info.addedAt, info.active); - } - - /** - * @notice Get paginated passkeys to prevent DoS/gas issues - * @param offset Starting index - * @param limit Maximum number of passkeys to return (capped at 50) - * @return passkeyIdList Array of passkey IDs - * @return qxList Array of x-coordinates - * @return qyList Array of y-coordinates - * @return addedAtList Array of timestamps - * @return activeList Array of active flags - * @return deviceIdList Array of device identifiers - * @return total Total number of passkeys - */ - function getPasskeys(uint256 offset, uint256 limit) - external - view - returns ( - bytes32[] memory passkeyIdList, - bytes32[] memory qxList, - bytes32[] memory qyList, - uint256[] memory addedAtList, - bool[] memory activeList, - bytes32[] memory deviceIdList, - uint256 total - ) - { - total = passkeyIds.length; - - // Return empty arrays if offset is beyond total - if (offset >= total) { - return ( - new bytes32[](0), - new bytes32[](0), - new bytes32[](0), - new uint256[](0), - new bool[](0), - new bytes32[](0), - total - ); - } - - // Cap limit at 50 to prevent gas issues - uint256 maxLimit = 50; - if (limit > maxLimit) { - limit = maxLimit; - } - - // Calculate actual length to return - uint256 remaining = total - offset; - uint256 length = remaining < limit ? remaining : limit; - - passkeyIdList = new bytes32[](length); - qxList = new bytes32[](length); - qyList = new bytes32[](length); - addedAtList = new uint256[](length); - activeList = new bool[](length); - deviceIdList = new bytes32[](length); - - for (uint256 i = 0; i < length; i++) { - bytes32 passkeyId = passkeyIds[offset + i]; - PasskeyInfo storage info = passkeys[passkeyId]; - passkeyIdList[i] = passkeyId; - qxList[i] = info.qx; - qyList[i] = info.qy; - addedAtList[i] = info.addedAt; - activeList[i] = true; // All passkeys in array are active - deviceIdList[i] = info.deviceId; - } - } - - /*////////////////////////////////////////////////////////////// - INTERNAL FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Internal function to execute a call - * @param target The target address - * @param value The amount of ETH to send - * @param data The calldata - */ - function _call(address target, uint256 value, bytes memory data) internal { - (bool success, bytes memory result) = target.call{value: value}(data); - if (!success) { - revert CallFailed(result); - } - } - - /** - * @notice Internal function to remove an action hash from the pending list - * @param actionHash The hash to remove - */ - function _removePendingActionHash(bytes32 actionHash) internal { - uint256 length = pendingActionHashes.length; - for (uint256 i = 0; i < length; i++) { - if (pendingActionHashes[i] == actionHash) { - // Move the last element to this position and pop - pendingActionHashes[i] = pendingActionHashes[length - 1]; - pendingActionHashes.pop(); - break; - } - } - } - - /*////////////////////////////////////////////////////////////// - RECEIVE - //////////////////////////////////////////////////////////////*/ - - /// @notice Allow the account to receive ETH - receive() external payable {} - - /** - * @notice Get the current deposit in the EntryPoint - */ - function getDeposit() public view returns (uint256) { - return ENTRYPOINT.balanceOf(address(this)); - } - - /** - * @notice Add deposit to the EntryPoint - */ - function addDeposit() public payable { - ENTRYPOINT.depositTo{value: msg.value}(address(this)); - } - - /** - * @notice Withdraw deposit from the EntryPoint - * @param withdrawAddress The address to withdraw to - * @param amount The amount to withdraw - * @dev SECURITY: Only callable via EntryPoint (passkey signature required) - */ - function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public { - if (msg.sender != address(ENTRYPOINT)) revert OnlyEntryPoint(); - ENTRYPOINT.withdrawTo(withdrawAddress, amount); - } -} diff --git a/src/P256AccountFactory.sol b/src/P256AccountFactory.sol deleted file mode 100644 index c7d8c5a..0000000 --- a/src/P256AccountFactory.sol +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; -import {P256Account} from "./P256Account.sol"; -import {ERC1967Factory} from "solady/utils/ERC1967Factory.sol"; -import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; - -/** - * @title P256AccountFactory - * @notice Factory contract for deploying P256Account instances using Solady's ERC-1967 proxies - * @dev Uses Solady's hyper-optimized proxy pattern for maximum gas efficiency - * @dev Allows deterministic account addresses based on owner and salt - * @dev Uses canonical ERC1967Factory at 0x0000000000006396FF2a80c067f99B3d2Ab4Df24 - */ -contract P256AccountFactory { - /*////////////////////////////////////////////////////////////// - STORAGE - //////////////////////////////////////////////////////////////*/ - - /// @notice The EntryPoint contract - IEntryPoint public immutable ENTRYPOINT; - - /// @notice The P256Account implementation contract (deployed once) - P256Account public immutable IMPLEMENTATION; - - /// @notice Solady's canonical ERC1967Factory for deploying proxies - /// @dev Uses the canonical address: 0x0000000000006396FF2a80c067f99B3d2Ab4Df24 - ERC1967Factory public immutable PROXY_FACTORY; - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - event AccountCreated(address indexed account, bytes32 indexed qx, bytes32 indexed qy, address owner, uint256 salt); - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Constructor - deploys the implementation contract - * @param _entryPoint The EntryPoint contract address - * @dev The implementation is deployed once and reused by all proxies - * @dev Uses canonical ERC1967Factory - must be deployed on the chain first - */ - constructor(IEntryPoint _entryPoint) { - ENTRYPOINT = _entryPoint; - // Deploy the implementation contract once - IMPLEMENTATION = new P256Account(_entryPoint); - // Use Solady's canonical ERC1967Factory (saves deployment gas) - PROXY_FACTORY = ERC1967Factory(ERC1967FactoryConstants.ADDRESS); - } - - /*////////////////////////////////////////////////////////////// - ACCOUNT CREATION - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Create a new P256Account proxy - * @param qx The x-coordinate of the P-256 public key (can be 0 for owner-only mode) - * @param qy The y-coordinate of the P-256 public key (can be 0 for owner-only mode) - * @param owner The owner address for the account - * @param salt A salt for CREATE2 deployment - * @param enable2FA Whether to enable two-factor authentication immediately - * @param deviceId Short device identifier (e.g., "iPhone 15", "YubiKey 5") - * @return account The address of the created account - * @dev If qx=0 and qy=0, creates an owner-only account (no passkey) - * @dev If qx and qy are set but enable2FA=false, passkey is registered but 2FA is not enforced - * @dev If enable2FA=true, both qx and qy must be non-zero - * @dev IMPORTANT: Account address depends ONLY on owner and salt, NOT on passkey (qx, qy) or enable2FA - * @dev This allows users to add/change passkey later without changing the account address - * @dev Uses Solady's hyper-optimized ERC-1967 proxy pattern for maximum gas efficiency - */ - function createAccount(bytes32 qx, bytes32 qy, address owner, uint256 salt, bool enable2FA, bytes32 deviceId) - public - returns (P256Account account) - { - address addr = getAddress(qx, qy, owner, salt); - - // If account already exists, return it - uint256 codeSize = addr.code.length; - if (codeSize > 0) { - return P256Account(payable(addr)); - } - - // Deploy ERC-1967 proxy using Solady's factory with CREATE2 - // Salt includes owner, implementation, and salt to make address independent of passkey/2FA - // but dependent on contract version (implementation address) - bytes32 finalSalt = _computeSalt(owner, salt); - - // Deploy proxy using Solady's factory - // Admin is set to address(0) since we don't need upgradeability (account is immutable) - address proxy = PROXY_FACTORY.deployDeterministicAndCall( - address(IMPLEMENTATION), - address(0), // No admin - proxies are not upgradeable - finalSalt, - abi.encodeCall(P256Account.initialize, (qx, qy, owner, enable2FA, deviceId)) - ); - - account = P256Account(payable(proxy)); - - emit AccountCreated(address(account), qx, qy, owner, salt); - } - - /** - * @notice Get the deterministic address for an account proxy - * @param qx The x-coordinate of the P-256 public key (NOT used in address calculation) - * @param qy The y-coordinate of the P-256 public key (NOT used in address calculation) - * @param owner The owner address for the account - * @param salt A salt for CREATE2 deployment - * @return The predicted proxy address - * @dev Address is calculated from owner, implementation address, and salt (NOT from passkey or enable2FA) - * @dev This allows users to add/change passkey later without changing the account address - * @dev Including implementation ensures different contract versions get different addresses - * @dev Uses Solady's ERC1967Factory.predictDeterministicAddress() - */ - function getAddress(bytes32 qx, bytes32 qy, address owner, uint256 salt) public view returns (address) { - // Silence unused parameter warnings - qx and qy are intentionally not used - // to ensure address is independent of passkey choice - (qx, qy); - - // IMPORTANT: Use owner, implementation, and salt for address calculation - // This allows the same address regardless of passkey choice or 2FA setting - // Users can receive funds first, then decide on passkey/2FA later - // Different contract versions (implementations) will get different addresses - bytes32 finalSalt = _computeSalt(owner, salt); - - // Use Solady's factory to predict the deterministic address - return PROXY_FACTORY.predictDeterministicAddress(finalSalt); - } - - /** - * @notice Compute the final salt for CREATE2 deployment - * @param owner The owner address - * @param salt The user-provided salt - * @return The final salt (includes owner and implementation address to prevent collisions) - * @dev Solady's factory requires salt to start with caller address or zero address - * @dev We use zero address prefix to allow anyone to deploy on behalf of users - * @dev Salt format: [20 bytes: zero address][12 bytes: hash of owner+implementation+salt] - * @dev Including implementation address ensures different contract versions get different addresses - * @dev This allows reusing the same salt (index) during development when contract code changes - */ - function _computeSalt(address owner, uint256 salt) internal view returns (bytes32) { - // Combine owner, implementation address, and salt to create unique hash - // Including implementation ensures different contract versions get different addresses - bytes32 combinedSalt = keccak256(abi.encodePacked(owner, address(IMPLEMENTATION), salt)); - // Keep only the last 96 bits (12 bytes) of the hash - // The first 160 bits (20 bytes) will be zero, satisfying Solady's requirement - return bytes32(uint256(combinedSalt) & ((1 << 96) - 1)); - } - - /** - * @notice Create account and add initial deposit - * @param qx The x-coordinate of the P-256 public key - * @param qy The y-coordinate of the P-256 public key - * @param owner The owner address for the account - * @param salt A salt for CREATE2 deployment - * @param enable2FA Whether to enable two-factor authentication immediately - * @param deviceId Short device identifier (e.g., "iPhone 15", "YubiKey 5") - * @return account The address of the created account - */ - function createAccountWithDeposit( - bytes32 qx, - bytes32 qy, - address owner, - uint256 salt, - bool enable2FA, - bytes32 deviceId - ) external payable returns (P256Account account) { - account = createAccount(qx, qy, owner, salt, enable2FA, deviceId); - - // Add deposit to EntryPoint if ETH was sent - if (msg.value > 0) { - ENTRYPOINT.depositTo{value: msg.value}(address(account)); - } - } - - /** - * @notice Generate initCode for account creation - * @dev This is used in UserOperation.initCode - * @param qx The x-coordinate of the P-256 public key - * @param qy The y-coordinate of the P-256 public key - * @param owner The owner address for the account - * @param salt A salt for CREATE2 deployment - * @param enable2FA Whether to enable two-factor authentication immediately - * @param deviceId Short device identifier (e.g., "iPhone 15", "YubiKey 5") - * @return initCode The initCode bytes - */ - function getInitCode(bytes32 qx, bytes32 qy, address owner, uint256 salt, bool enable2FA, bytes32 deviceId) - external - view - returns (bytes memory initCode) - { - return abi.encodePacked( - address(this), abi.encodeCall(this.createAccount, (qx, qy, owner, salt, enable2FA, deviceId)) - ); - } -} diff --git a/test/P256Account.t.sol b/test/P256Account.t.sol deleted file mode 100644 index ab407ca..0000000 --- a/test/P256Account.t.sol +++ /dev/null @@ -1,958 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Test, console2} from "forge-std/Test.sol"; -import {P256Account} from "../src/P256Account.sol"; -import {P256AccountFactory} from "../src/P256AccountFactory.sol"; -import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; -import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; -import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; - -/** - * @title P256AccountTest - * @notice Test suite for P256Account - */ -contract P256AccountTest is Test { - P256Account public account; - P256AccountFactory public factory; - IEntryPoint public entryPoint; - - address public owner; - bytes32 public qx; - bytes32 public qy; - - // Mock EntryPoint for testing - address constant ENTRYPOINT_ADDR = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - - function setUp() public { - owner = makeAddr("owner"); - - // Use mock public key for testing - qx = bytes32(uint256(0x1234)); - qy = bytes32(uint256(0x5678)); - - // Mock EntryPoint contract at the canonical address - // This allows tests to call EntryPoint functions without reverting - vm.etch(ENTRYPOINT_ADDR, hex"00"); - entryPoint = IEntryPoint(ENTRYPOINT_ADDR); - - // Deploy canonical ERC1967Factory if not already deployed - if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { - vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); - } - - // Deploy factory - factory = new P256AccountFactory(entryPoint); - - // Create account with 2FA enabled - account = factory.createAccount(qx, qy, owner, 0, true, bytes32("Test Device")); - } - - function test_Initialization() public view { - // Verify first passkey - assertEq(account.getActivePasskeyCount(), 1, "Should have 1 passkey"); - bytes32 passkeyId = account.passkeyIds(0); - (bytes32 storedQx, bytes32 storedQy,,,) = account.passkeys(passkeyId); - assertEq(storedQx, qx, "QX mismatch"); - assertEq(storedQy, qy, "QY mismatch"); - - assertEq(account.owner(), owner, "Owner mismatch"); - assertEq(address(account.ENTRYPOINT()), address(entryPoint), "EntryPoint mismatch"); - - // Verify owner is added as first guardian - assertTrue(account.guardians(owner), "Owner should be a guardian"); - assertEq(account.guardianList(0), owner, "Owner should be first guardian in list"); - assertEq(account.guardianThreshold(), 1, "Guardian threshold should be 1"); - - // Verify two-factor authentication is enabled - assertTrue(account.twoFactorEnabled(), "Two-factor should be enabled"); - } - - function test_InitializationWithout2FA() public { - // Create account without 2FA - P256Account account2 = factory.createAccount(qx, qy, owner, 1, false, bytes32("Device 2")); - - // Verify first passkey - assertEq(account2.getActivePasskeyCount(), 1, "Should have 1 passkey"); - bytes32 passkeyId = account2.passkeyIds(0); - (bytes32 storedQx, bytes32 storedQy,,,) = account2.passkeys(passkeyId); - assertEq(storedQx, qx, "QX mismatch"); - assertEq(storedQy, qy, "QY mismatch"); - - assertEq(account2.owner(), owner, "Owner mismatch"); - - // Verify two-factor authentication is disabled - assertFalse(account2.twoFactorEnabled(), "Two-factor should be disabled"); - } - - function test_OwnerOnlyMode() public { - // Create account in owner-only mode (no passkey) - P256Account ownerOnlyAccount = factory.createAccount(bytes32(0), bytes32(0), owner, 2, false, bytes32(0)); - - // Verify no passkeys - assertEq(ownerOnlyAccount.getActivePasskeyCount(), 0, "Should have 0 passkeys"); - - assertEq(ownerOnlyAccount.owner(), owner, "Owner mismatch"); - - // Verify two-factor authentication is disabled - assertFalse(ownerOnlyAccount.twoFactorEnabled(), "Two-factor should be disabled in owner-only mode"); - } - - function test_CannotEnable2FAWithoutPasskey() public { - // Create account in owner-only mode - P256Account ownerOnlyAccount = factory.createAccount(bytes32(0), bytes32(0), owner, 3, false, bytes32(0)); - - // Try to enable 2FA (should fail because no active passkey) - vm.prank(ENTRYPOINT_ADDR); - vm.expectRevert("Active passkey required for 2FA"); - ownerOnlyAccount.enableTwoFactor(); - } - - function test_CannotReinitialize() public { - // OpenZeppelin's Initializable uses InvalidInitialization() error - vm.expectRevert(abi.encodeWithSignature("InvalidInitialization()")); - account.initialize(bytes32(uint256(1)), bytes32(uint256(2)), owner, true, bytes32("Device")); - } - - function test_ExecuteOnlyViaEntryPoint() public { - address target = makeAddr("target"); - uint256 value = 1 ether; - bytes memory data = ""; - - // Fund the account - vm.deal(address(account), 2 ether); - - // Execute from EntryPoint (simulating passkey signature) - vm.prank(ENTRYPOINT_ADDR); - account.execute(target, value, data); - - assertEq(target.balance, value, "Execute failed"); - } - - function test_CannotExecuteDirectlyFromOwner() public { - address target = makeAddr("target"); - uint256 value = 1 ether; - bytes memory data = ""; - - // Fund the account - vm.deal(address(account), 2 ether); - - // Try to execute from owner - should fail - vm.prank(owner); - vm.expectRevert(P256Account.OnlyEntryPoint.selector); - account.execute(target, value, data); - } - - function test_ExecuteBatchOnlyViaEntryPoint() public { - address[] memory targets = new address[](2); - targets[0] = makeAddr("target1"); - targets[1] = makeAddr("target2"); - - uint256[] memory values = new uint256[](2); - values[0] = 0.5 ether; - values[1] = 0.3 ether; - - bytes[] memory datas = new bytes[](2); - datas[0] = ""; - datas[1] = ""; - - // Fund the account - vm.deal(address(account), 2 ether); - - // Execute batch from EntryPoint - vm.prank(ENTRYPOINT_ADDR); - account.executeBatch(targets, values, datas); - - assertEq(targets[0].balance, values[0], "Batch execute failed for target 1"); - assertEq(targets[1].balance, values[1], "Batch execute failed for target 2"); - } - - function test_CannotExecuteBatchDirectlyFromOwner() public { - address[] memory targets = new address[](2); - targets[0] = makeAddr("target1"); - targets[1] = makeAddr("target2"); - - uint256[] memory values = new uint256[](2); - values[0] = 0.5 ether; - values[1] = 0.3 ether; - - bytes[] memory datas = new bytes[](2); - datas[0] = ""; - datas[1] = ""; - - // Fund the account - vm.deal(address(account), 2 ether); - - // Try to execute batch from owner - should fail - vm.prank(owner); - vm.expectRevert(P256Account.OnlyEntryPoint.selector); - account.executeBatch(targets, values, datas); - } - - function test_ReceiveETH() public { - uint256 amount = 1 ether; - - vm.deal(address(this), amount); - (bool success,) = address(account).call{value: amount}(""); - - assertTrue(success, "Receive failed"); - assertEq(address(account).balance, amount, "Balance mismatch"); - } - - function test_AddDeposit() public { - uint256 depositAmount = 1 ether; - vm.deal(address(account), depositAmount); - - vm.prank(address(account)); - account.addDeposit{value: depositAmount}(); - - // Note: This will fail if EntryPoint is not properly deployed - // In production, use actual EntryPoint - } - - function test_IsValidSignature() public view { - // Create a test hash - bytes32 hash = keccak256("test message"); - - // Create a dummy signature (96 bytes: r || s || passkeyId) - bytes memory signature = new bytes(96); - - // Set a dummy passkeyId (last 32 bytes) - use a non-existent passkey - bytes32 dummyPasskeyId = keccak256("non-existent-passkey"); - assembly { - mstore(add(signature, 96), dummyPasskeyId) - } - - // This will return invalid magic value since passkey doesn't exist - bytes4 result = account.isValidSignature(hash, signature); - - // Should return 0x00000000 for invalid signature - assertEq(result, bytes4(0), "Should return 0 for invalid signature"); - } - - function test_GetAddress() public view { - address predicted = factory.getAddress(qx, qy, owner, 0); - assertEq(predicted, address(account), "Address prediction mismatch"); - } - - function test_CreateAccountIdempotent() public { - // Creating account with same parameters should return existing account - P256Account account2 = factory.createAccount(qx, qy, owner, 0, true, bytes32("Test Device")); - assertEq(address(account2), address(account), "Should return same account"); - } - - function test_CreateAccountWithDifferentSalt() public { - // Creating account with different salt should create new account - P256Account account2 = factory.createAccount(qx, qy, owner, 1, true, bytes32("Test Device")); - assertTrue(address(account2) != address(account), "Should create different account"); - } - - function test_GetInitCode() public view { - bytes memory initCode = factory.getInitCode(qx, qy, owner, 0, true, bytes32("Test Device")); - - // InitCode should start with factory address - address factoryAddr; - assembly { - factoryAddr := mload(add(initCode, 20)) - } - assertEq(factoryAddr, address(factory), "InitCode should start with factory address"); - } - - /*////////////////////////////////////////////////////////////// - TWO-FACTOR AUTH TESTS - //////////////////////////////////////////////////////////////*/ - - function test_EnableTwoFactor() public { - // 2FA is now enabled by default - assertTrue(account.twoFactorEnabled(), "2FA should be enabled by default"); - - // Disable it first - vm.prank(ENTRYPOINT_ADDR); - account.disableTwoFactor(); - assertFalse(account.twoFactorEnabled(), "2FA should be disabled"); - - // Re-enable 2FA via EntryPoint (passkey signature) - vm.prank(ENTRYPOINT_ADDR); - account.enableTwoFactor(); - - // Check 2FA is enabled again - assertTrue(account.twoFactorEnabled(), "2FA should be enabled"); - } - - function test_DisableTwoFactor() public { - // 2FA is already enabled by default - assertTrue(account.twoFactorEnabled(), "2FA should be enabled by default"); - - // Disable 2FA via EntryPoint (passkey signature) - vm.prank(ENTRYPOINT_ADDR); - account.disableTwoFactor(); - - // Check 2FA is disabled - assertFalse(account.twoFactorEnabled(), "2FA should be disabled"); - } - - function test_EnableTwoFactorOnlyViaEntryPoint() public { - address attacker = makeAddr("attacker"); - - vm.prank(attacker); - vm.expectRevert(P256Account.OnlyEntryPoint.selector); - account.enableTwoFactor(); - } - - function test_DisableTwoFactorOnlyViaEntryPoint() public { - // 2FA is already enabled by default - assertTrue(account.twoFactorEnabled(), "2FA should be enabled by default"); - - address attacker = makeAddr("attacker"); - - // Try to disable from non-EntryPoint - should fail - vm.prank(attacker); - vm.expectRevert(P256Account.OnlyEntryPoint.selector); - account.disableTwoFactor(); - } - - function test_CannotEnableTwoFactorTwice() public { - // 2FA is already enabled by default - assertTrue(account.twoFactorEnabled(), "2FA should be enabled by default"); - - // Try to enable again - should fail - vm.prank(ENTRYPOINT_ADDR); - vm.expectRevert("2FA already enabled"); - account.enableTwoFactor(); - } - - function test_CannotDisableTwoFactorWhenNotEnabled() public { - // Disable 2FA first (it's enabled by default) - vm.prank(ENTRYPOINT_ADDR); - account.disableTwoFactor(); - assertFalse(account.twoFactorEnabled(), "2FA should be disabled"); - - // Try to disable again - should fail - vm.prank(ENTRYPOINT_ADDR); - vm.expectRevert("2FA already disabled"); - account.disableTwoFactor(); - } - - function test_ValidateUserOp_WithoutTwoFactor() public { - // Disable 2FA first (it's enabled by default) - vm.prank(ENTRYPOINT_ADDR); - account.disableTwoFactor(); - assertFalse(account.twoFactorEnabled(), "2FA should be disabled"); - - // Create a mock UserOperation - PackedUserOperation memory userOp; - - // Mock P-256 signature (r || s = 64 bytes) - bytes32 r = bytes32(uint256(0x1111)); - bytes32 s = bytes32(uint256(0x2222)); - userOp.signature = abi.encodePacked(r, s); - - bytes32 userOpHash = keccak256("test"); - - // Mock the EntryPoint call - vm.prank(ENTRYPOINT_ADDR); - - // This will fail because we're using mock signature, but it should accept 64-byte signature - uint256 validationData = account.validateUserOp(userOp, userOpHash, 0); - - // Should return 1 (failed) because signature is invalid, but length is correct - assertEq(validationData, 1, "Should fail with invalid signature"); - } - - function test_ValidateUserOp_WithTwoFactor_RejectsSingleSignature() public { - // 2FA is already enabled by default - assertTrue(account.twoFactorEnabled(), "2FA should be enabled by default"); - - // Create a mock UserOperation with only P-256 signature (64 bytes) - PackedUserOperation memory userOp; - bytes32 r = bytes32(uint256(0x1111)); - bytes32 s = bytes32(uint256(0x2222)); - userOp.signature = abi.encodePacked(r, s); - - bytes32 userOpHash = keccak256("test"); - - // Mock the EntryPoint call - vm.prank(ENTRYPOINT_ADDR); - - // Should fail because 2FA requires 129 bytes (64 + 65) - uint256 validationData = account.validateUserOp(userOp, userOpHash, 0); - - assertEq(validationData, 1, "Should fail with wrong signature length"); - } - - function test_ValidateUserOp_WithTwoFactor_AcceptsDualSignature() public { - // 2FA is already enabled by default - assertTrue(account.twoFactorEnabled(), "2FA should be enabled by default"); - - // Create a mock UserOperation with dual signature - PackedUserOperation memory userOp; - - // Mock P-256 signature (r || s = 64 bytes) - bytes32 r = bytes32(uint256(0x1111)); - bytes32 s = bytes32(uint256(0x2222)); - - // Mock WebAuthn data - bytes memory authenticatorData = hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000"; - string memory clientDataJSON = - '{"type":"webauthn.get","challenge":"test","origin":"http://localhost:3000","crossOrigin":false}'; - - // Encode using Solady compact format: - // authDataLen(2) || authenticatorData || clientDataJSON || challengeIdx(2) || typeIdx(2) || r(32) || s(32) || ownerSig(65) - uint16 authDataLen = uint16(authenticatorData.length); - uint16 challengeIndex = 23; // Index of "challenge" in clientDataJSON - uint16 typeIndex = 1; // Index of "type" in clientDataJSON - - // Create a real owner signature - bytes32 userOpHash = keccak256("test"); - - // Sign with owner's private key - uint256 ownerPrivateKey = 0x1234567890abcdef; - address ownerAddr = vm.addr(ownerPrivateKey); - - // Transfer ownership to the test owner (from current owner) - vm.prank(owner); - account.transferOwnership(ownerAddr); - - // Sign the userOpHash - (uint8 v, bytes32 sigR, bytes32 sigS) = vm.sign(ownerPrivateKey, userOpHash); - bytes memory ownerSig = abi.encodePacked(sigR, sigS, v); - - // Combine signatures using Solady compact format: - // authDataLen(2) || authenticatorData || clientDataJSON || challengeIdx(2) || typeIdx(2) || r(32) || s(32) || ownerSig(65) - userOp.signature = abi.encodePacked( - authDataLen, authenticatorData, bytes(clientDataJSON), challengeIndex, typeIndex, r, s, ownerSig - ); - - // Mock the EntryPoint call - vm.prank(ENTRYPOINT_ADDR); - - // Should still fail because P-256 signature is invalid, but length is correct - uint256 validationData = account.validateUserOp(userOp, userOpHash, 0); - - // Will fail due to invalid P-256 signature, but that's expected - assertEq(validationData, 1, "Should fail with invalid P-256 signature"); - } - - /*////////////////////////////////////////////////////////////// - GUARDIAN TESTS - //////////////////////////////////////////////////////////////*/ - - function test_AddGuardian() public { - address guardian = makeAddr("guardian"); - - // Owner is already a guardian (count = 1) - assertEq(account.getGuardianCount(), 1, "Should start with 1 guardian (owner)"); - - // Add guardian via execute() (called from EntryPoint) - bytes memory data = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian); - vm.prank(ENTRYPOINT_ADDR); - account.execute(address(account), 0, data); - - assertTrue(account.guardians(guardian), "Guardian not added"); - assertEq(account.getGuardianCount(), 2, "Guardian count should be 2 (owner + new guardian)"); - } - - function test_CannotAddGuardianDirectly() public { - address guardian = makeAddr("guardian"); - - // Try to add guardian from owner - should fail - vm.prank(owner); - vm.expectRevert(P256Account.OnlyEntryPointOrOwner.selector); - account.addGuardian(guardian); - } - - function test_RemoveGuardian() public { - address guardian = makeAddr("guardian"); - - // Add guardian via execute() - bytes memory addData = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian); - vm.prank(ENTRYPOINT_ADDR); - account.execute(address(account), 0, addData); - assertEq(account.getGuardianCount(), 2, "Should have 2 guardians"); - - // Remove guardian via execute() - bytes memory removeData = abi.encodeWithSelector(P256Account.removeGuardian.selector, guardian); - vm.prank(ENTRYPOINT_ADDR); - account.execute(address(account), 0, removeData); - - assertFalse(account.guardians(guardian), "Guardian not removed"); - assertEq(account.getGuardianCount(), 1, "Should have 1 guardian (owner) remaining"); - } - - function test_SetGuardianThreshold() public { - address guardian1 = makeAddr("guardian1"); - address guardian2 = makeAddr("guardian2"); - - // Owner is already a guardian (count = 1) - // Add 2 more guardians (total = 3) via execute() - vm.startPrank(ENTRYPOINT_ADDR); - bytes memory addData1 = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian1); - account.execute(address(account), 0, addData1); - bytes memory addData2 = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian2); - account.execute(address(account), 0, addData2); - assertEq(account.getGuardianCount(), 3, "Should have 3 guardians"); - - // Set threshold to 2 (out of 3) via execute() - bytes memory thresholdData = abi.encodeWithSelector(P256Account.setGuardianThreshold.selector, 2); - account.execute(address(account), 0, thresholdData); - vm.stopPrank(); - - assertEq(account.guardianThreshold(), 2, "Threshold not set"); - } - - /*////////////////////////////////////////////////////////////// - RECOVERY TESTS - //////////////////////////////////////////////////////////////*/ - - function test_OwnerCanInitiateRecoveryImmediately() public { - // Owner is already a guardian (added during initialization) - // No need to add guardians or set threshold - - bytes32 newQx = bytes32(uint256(0xAAAA)); - bytes32 newQy = bytes32(uint256(0xBBBB)); - address newOwner = makeAddr("newOwner"); - - // Owner can initiate recovery immediately - vm.prank(owner); - account.initiateRecovery(newQx, newQy, newOwner); - - // Check recovery request - ( - bytes32 reqQx, - bytes32 reqQy, - address reqOwner, - uint256 approvalCount, - uint256 executeAfter, - bool executed, - bool cancelled - ) = account.getRecoveryRequest(0); - - assertEq(reqQx, newQx, "Recovery qx mismatch"); - assertEq(reqQy, newQy, "Recovery qy mismatch"); - assertEq(reqOwner, newOwner, "Recovery owner mismatch"); - assertEq(approvalCount, 1, "Approval count should be 1"); - assertEq(executeAfter, block.timestamp + 24 hours, "Execute after mismatch"); - assertFalse(executed, "Should not be executed"); - assertFalse(cancelled, "Should not be cancelled"); - } - - function test_InitiateRecovery() public { - address guardian = makeAddr("guardian"); - bytes32 newQx = bytes32(uint256(0xAAAA)); - bytes32 newQy = bytes32(uint256(0xBBBB)); - address newOwner = makeAddr("newOwner"); - - // Setup guardian via execute() - vm.startPrank(ENTRYPOINT_ADDR); - bytes memory addData = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian); - account.execute(address(account), 0, addData); - bytes memory thresholdData = abi.encodeWithSelector(P256Account.setGuardianThreshold.selector, 2); - account.execute(address(account), 0, thresholdData); - vm.stopPrank(); - - // Initiate recovery - vm.prank(guardian); - account.initiateRecovery(newQx, newQy, newOwner); - - // Check recovery request - ( - bytes32 reqQx, - bytes32 reqQy, - address reqOwner, - uint256 approvalCount, - uint256 executeAfter, - bool executed, - bool cancelled - ) = account.getRecoveryRequest(0); - - assertEq(reqQx, newQx, "Recovery qx mismatch"); - assertEq(reqQy, newQy, "Recovery qy mismatch"); - assertEq(reqOwner, newOwner, "Recovery owner mismatch"); - assertEq(approvalCount, 1, "Approval count mismatch"); - assertFalse(executed, "Should not be executed"); - assertFalse(cancelled, "Should not be cancelled"); - } - - function test_ApproveRecovery() public { - address guardian1 = makeAddr("guardian1"); - address guardian2 = makeAddr("guardian2"); - bytes32 newQx = bytes32(uint256(0xAAAA)); - bytes32 newQy = bytes32(uint256(0xBBBB)); - address newOwner = makeAddr("newOwner"); - - // Setup guardians via execute() - vm.startPrank(ENTRYPOINT_ADDR); - bytes memory addData1 = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian1); - account.execute(address(account), 0, addData1); - bytes memory addData2 = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian2); - account.execute(address(account), 0, addData2); - bytes memory thresholdData = abi.encodeWithSelector(P256Account.setGuardianThreshold.selector, 2); - account.execute(address(account), 0, thresholdData); - vm.stopPrank(); - - // Initiate recovery - vm.prank(guardian1); - account.initiateRecovery(newQx, newQy, newOwner); - - // Approve recovery - vm.prank(guardian2); - account.approveRecovery(0); - - // Check approval count - (,,, uint256 approvalCount,,,) = account.getRecoveryRequest(0); - assertEq(approvalCount, 2, "Approval count mismatch"); - } - - function test_ExecuteRecovery() public { - address guardian = makeAddr("guardian"); - bytes32 newQx = bytes32(uint256(0xAAAA)); - bytes32 newQy = bytes32(uint256(0xBBBB)); - address newOwner = makeAddr("newOwner"); - - // Setup guardian via execute() - vm.startPrank(ENTRYPOINT_ADDR); - bytes memory addData = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian); - account.execute(address(account), 0, addData); - bytes memory thresholdData = abi.encodeWithSelector(P256Account.setGuardianThreshold.selector, 1); - account.execute(address(account), 0, thresholdData); - vm.stopPrank(); - - // Initiate recovery - vm.prank(guardian); - account.initiateRecovery(newQx, newQy, newOwner); - - // Fast forward past timelock (24 hours) - vm.warp(block.timestamp + 24 hours + 1); - - // Execute recovery - account.executeRecovery(0); - - // Verify account updated - old passkeys should be deactivated, new passkey added - assertEq(account.getActivePasskeyCount(), 1, "Should have 1 active passkey"); - bytes32 newPasskeyId = keccak256(abi.encodePacked(newQx, newQy)); - (bytes32 storedQx, bytes32 storedQy,, bool active,) = account.passkeys(newPasskeyId); - assertEq(storedQx, newQx, "QX not updated"); - assertEq(storedQy, newQy, "QY not updated"); - assertTrue(active, "New passkey should be active"); - - assertEq(account.owner(), newOwner, "Owner not updated"); - } - - function test_CannotExecuteRecoveryBeforeTimelock() public { - address guardian = makeAddr("guardian"); - bytes32 newQx = bytes32(uint256(0xAAAA)); - bytes32 newQy = bytes32(uint256(0xBBBB)); - address newOwner = makeAddr("newOwner"); - - // Setup guardian via execute() - vm.startPrank(ENTRYPOINT_ADDR); - bytes memory addData = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian); - account.execute(address(account), 0, addData); - bytes memory thresholdData = abi.encodeWithSelector(P256Account.setGuardianThreshold.selector, 1); - account.execute(address(account), 0, thresholdData); - vm.stopPrank(); - - // Initiate recovery - vm.prank(guardian); - account.initiateRecovery(newQx, newQy, newOwner); - - // Try to execute immediately - vm.expectRevert(P256Account.RecoveryNotReady.selector); - account.executeRecovery(0); - } - - function test_CancelRecovery() public { - address guardian = makeAddr("guardian"); - bytes32 newQx = bytes32(uint256(0xAAAA)); - bytes32 newQy = bytes32(uint256(0xBBBB)); - address newOwner = makeAddr("newOwner"); - - // Setup guardian via execute() - vm.startPrank(ENTRYPOINT_ADDR); - bytes memory addData = abi.encodeWithSelector(P256Account.addGuardian.selector, guardian); - account.execute(address(account), 0, addData); - bytes memory thresholdData = abi.encodeWithSelector(P256Account.setGuardianThreshold.selector, 1); - account.execute(address(account), 0, thresholdData); - vm.stopPrank(); - - // Initiate recovery - vm.prank(guardian); - account.initiateRecovery(newQx, newQy, newOwner); - - // Cancel recovery via execute() (called from EntryPoint with passkey signature) - bytes memory cancelData = abi.encodeWithSelector(P256Account.cancelRecovery.selector, 0); - vm.prank(ENTRYPOINT_ADDR); - account.execute(address(account), 0, cancelData); - - // Fast forward past timelock - vm.warp(block.timestamp + 24 hours + 1); - - // Try to execute - should fail - vm.expectRevert(P256Account.RecoveryAlreadyCancelled.selector); - account.executeRecovery(0); - } - - function test_GetPasskeysPagination() public { - // Add multiple passkeys - bytes32[] memory testQx = new bytes32[](5); - bytes32[] memory testQy = new bytes32[](5); - - for (uint256 i = 0; i < 5; i++) { - testQx[i] = bytes32(uint256(qx) + i + 1); - testQy[i] = bytes32(uint256(qy) + i + 1); - - vm.prank(ENTRYPOINT_ADDR); - account.addPasskey(testQx[i], testQy[i], bytes32(abi.encodePacked("Device", i))); - } - - // Total should be 6 (1 initial + 5 added) - assertEq(account.getActivePasskeyCount(), 6, "Should have 6 passkeys"); - - // Test pagination: get first 3 - ( - bytes32[] memory ids1, - bytes32[] memory qxList1, - bytes32[] memory qyList1, - uint256[] memory addedAt1, - bool[] memory active1, - bytes32[] memory deviceIds1, - uint256 total1 - ) = account.getPasskeys(0, 3); - - assertEq(total1, 6, "Total should be 6"); - assertEq(ids1.length, 3, "Should return 3 passkeys"); - assertEq(qxList1.length, 3, "Should return 3 qx values"); - - // Verify first passkey is the initial one - assertEq(qxList1[0], qx, "First passkey should be initial qx"); - assertEq(qyList1[0], qy, "First passkey should be initial qy"); - assertTrue(active1[0], "First passkey should be active"); - - // Test pagination: get next 3 - ( - bytes32[] memory ids2, - bytes32[] memory qxList2, - bytes32[] memory qyList2,, - bool[] memory active2,, - uint256 total2 - ) = account.getPasskeys(3, 3); - - assertEq(total2, 6, "Total should still be 6"); - assertEq(ids2.length, 3, "Should return 3 passkeys"); - - // Verify these are the added passkeys (offset 3 = index 3, 4, 5) - // testQx[0] is at index 1, so index 3 is testQx[2] - assertEq(qxList2[0], testQx[2], "Should match 3rd added passkey"); - assertEq(qyList2[0], testQy[2], "Should match 3rd added passkey"); - assertTrue(active2[0], "Added passkey should be active"); - - // Test pagination: offset beyond total - (bytes32[] memory ids3,,,,,, uint256 total3) = account.getPasskeys(10, 3); - - assertEq(total3, 6, "Total should still be 6"); - assertEq(ids3.length, 0, "Should return empty array"); - - // Test pagination: limit exceeds remaining - (bytes32[] memory ids4,,,,,, uint256 total4) = account.getPasskeys(4, 10); - - assertEq(total4, 6, "Total should still be 6"); - assertEq(ids4.length, 2, "Should return only 2 remaining passkeys"); - } - - function test_GetPasskeysLimitCap() public { - // The limit should be capped at 50 to prevent gas issues - (bytes32[] memory ids,,,,,, uint256 total) = account.getPasskeys(0, 100); - - // With only 1 passkey, should return 1 - assertEq(total, 1, "Total should be 1"); - assertEq(ids.length, 1, "Should return 1 passkey"); - - // Note: The actual cap of 50 is enforced in the contract - // If we had 100 passkeys and requested 100, we'd only get 50 - } - - /*////////////////////////////////////////////////////////////// - MULTIPLE PASSKEYS TESTS - //////////////////////////////////////////////////////////////*/ - - function test_AddPasskey() public { - bytes32 newQx = bytes32(uint256(0x9999)); - bytes32 newQy = bytes32(uint256(0xAAAA)); - bytes32 deviceId = bytes32("iPhone 15"); - - // Add passkey via EntryPoint - vm.prank(ENTRYPOINT_ADDR); - account.addPasskey(newQx, newQy, deviceId); - - // Verify passkey was added - assertEq(account.getActivePasskeyCount(), 2, "Should have 2 passkeys"); - - bytes32 passkeyId = keccak256(abi.encodePacked(newQx, newQy)); - (bytes32 storedQx, bytes32 storedQy,, bool active, bytes32 storedDeviceId) = account.passkeys(passkeyId); - - assertEq(storedQx, newQx, "QX mismatch"); - assertEq(storedQy, newQy, "QY mismatch"); - assertTrue(active, "Passkey should be active"); - assertEq(storedDeviceId, deviceId, "Device ID mismatch"); - } - - function test_AddPasskeyOnlyViaEntryPoint() public { - bytes32 newQx = bytes32(uint256(0x9999)); - bytes32 newQy = bytes32(uint256(0xAAAA)); - address attacker = makeAddr("attacker"); - - vm.prank(attacker); - vm.expectRevert(P256Account.OnlyEntryPoint.selector); - account.addPasskey(newQx, newQy, bytes32("Device")); - } - - function test_CannotAddDuplicatePasskey() public { - // Try to add the same passkey that was added during initialization - vm.prank(ENTRYPOINT_ADDR); - vm.expectRevert(P256Account.PasskeyAlreadyExists.selector); - account.addPasskey(qx, qy, bytes32("Duplicate")); - } - - function test_CannotAddPasskeyWithZeroCoordinates() public { - vm.prank(ENTRYPOINT_ADDR); - vm.expectRevert(P256Account.InvalidPasskeyCoordinates.selector); - account.addPasskey(bytes32(0), bytes32(0), bytes32("Device")); - - vm.prank(ENTRYPOINT_ADDR); - vm.expectRevert(P256Account.InvalidPasskeyCoordinates.selector); - account.addPasskey(bytes32(uint256(1)), bytes32(0), bytes32("Device")); - - vm.prank(ENTRYPOINT_ADDR); - vm.expectRevert(P256Account.InvalidPasskeyCoordinates.selector); - account.addPasskey(bytes32(0), bytes32(uint256(1)), bytes32("Device")); - } - - function test_RemovePasskey() public { - // Add a second passkey first - bytes32 newQx = bytes32(uint256(0x9999)); - bytes32 newQy = bytes32(uint256(0xAAAA)); - - vm.prank(ENTRYPOINT_ADDR); - account.addPasskey(newQx, newQy, bytes32("Device 2")); - - assertEq(account.getActivePasskeyCount(), 2, "Should have 2 passkeys"); - - // Remove the new passkey - vm.prank(ENTRYPOINT_ADDR); - account.removePasskey(newQx, newQy); - - // Verify passkey was removed - assertEq(account.getActivePasskeyCount(), 1, "Should have 1 passkey"); - - bytes32 passkeyId = keccak256(abi.encodePacked(newQx, newQy)); - (,,, bool active,) = account.passkeys(passkeyId); - assertFalse(active, "Passkey should be inactive"); - } - - function test_RemovePasskeyOnlyViaEntryPoint() public { - address attacker = makeAddr("attacker"); - - vm.prank(attacker); - vm.expectRevert(P256Account.OnlyEntryPoint.selector); - account.removePasskey(qx, qy); - } - - function test_CannotRemoveNonexistentPasskey() public { - bytes32 fakeQx = bytes32(uint256(0x9999)); - bytes32 fakeQy = bytes32(uint256(0xAAAA)); - - vm.prank(ENTRYPOINT_ADDR); - vm.expectRevert(P256Account.PasskeyNotActive.selector); - account.removePasskey(fakeQx, fakeQy); - } - - function test_CannotRemoveLastPasskeyWhen2FAEnabled() public { - // Account has 2FA enabled by default with 1 passkey - assertTrue(account.twoFactorEnabled(), "2FA should be enabled"); - assertEq(account.getActivePasskeyCount(), 1, "Should have 1 passkey"); - - // Try to remove the last passkey - vm.prank(ENTRYPOINT_ADDR); - vm.expectRevert(P256Account.CannotRemoveLastPasskey.selector); - account.removePasskey(qx, qy); - } - - function test_CanRemoveLastPasskeyWhen2FADisabled() public { - // Disable 2FA first - vm.prank(ENTRYPOINT_ADDR); - account.disableTwoFactor(); - - assertFalse(account.twoFactorEnabled(), "2FA should be disabled"); - assertEq(account.getActivePasskeyCount(), 1, "Should have 1 passkey"); - - // Now we can remove the last passkey - vm.prank(ENTRYPOINT_ADDR); - account.removePasskey(qx, qy); - - assertEq(account.getActivePasskeyCount(), 0, "Should have 0 passkeys"); - } - - function test_AddMultiplePasskeys() public { - // Add 5 passkeys - for (uint256 i = 0; i < 5; i++) { - bytes32 newQx = bytes32(uint256(qx) + i + 1); - bytes32 newQy = bytes32(uint256(qy) + i + 1); - bytes32 deviceId = bytes32(abi.encodePacked("Device", i)); - - vm.prank(ENTRYPOINT_ADDR); - account.addPasskey(newQx, newQy, deviceId); - } - - // Should have 6 total (1 initial + 5 added) - assertEq(account.getActivePasskeyCount(), 6, "Should have 6 passkeys"); - - // Verify all passkeys are active - (,,,, bool[] memory activeList,,) = account.getPasskeys(0, 10); - for (uint256 i = 0; i < activeList.length; i++) { - assertTrue(activeList[i], "All passkeys should be active"); - } - } - - function test_RemoveMultiplePasskeys() public { - // Add 3 passkeys - bytes32[] memory testQx = new bytes32[](3); - bytes32[] memory testQy = new bytes32[](3); - - for (uint256 i = 0; i < 3; i++) { - testQx[i] = bytes32(uint256(qx) + i + 1); - testQy[i] = bytes32(uint256(qy) + i + 1); - - vm.prank(ENTRYPOINT_ADDR); - account.addPasskey(testQx[i], testQy[i], bytes32(abi.encodePacked("Device", i))); - } - - assertEq(account.getActivePasskeyCount(), 4, "Should have 4 passkeys"); - - // Remove 2 passkeys - vm.prank(ENTRYPOINT_ADDR); - account.removePasskey(testQx[0], testQy[0]); - - vm.prank(ENTRYPOINT_ADDR); - account.removePasskey(testQx[1], testQy[1]); - - assertEq(account.getActivePasskeyCount(), 2, "Should have 2 passkeys"); - } - - function test_GetPasskeyByIndex() public { - // Get the initial passkey - (bytes32 passkeyId, bytes32 storedQx, bytes32 storedQy, uint256 addedAt, bool active) = - account.getPasskeyByIndex(0); - - bytes32 expectedId = keccak256(abi.encodePacked(qx, qy)); - assertEq(passkeyId, expectedId, "Passkey ID mismatch"); - assertEq(storedQx, qx, "QX mismatch"); - assertEq(storedQy, qy, "QY mismatch"); - assertTrue(active, "Passkey should be active"); - assertGt(addedAt, 0, "Added timestamp should be set"); - } - - function test_GetPasskeyByIndexOutOfBounds() public { - vm.expectRevert("Index out of bounds"); - account.getPasskeyByIndex(999); - } -} diff --git a/test/P256AccountFactory.t.sol b/test/P256AccountFactory.t.sol deleted file mode 100644 index ad5ccf1..0000000 --- a/test/P256AccountFactory.t.sol +++ /dev/null @@ -1,272 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import "forge-std/Test.sol"; -import {P256AccountFactory} from "../src/P256AccountFactory.sol"; -import {P256Account} from "../src/P256Account.sol"; -import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; -import {ERC1967Factory} from "solady/utils/ERC1967Factory.sol"; -import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; - -contract P256AccountFactoryTest is Test { - P256AccountFactory factory; - IEntryPoint entryPoint; - - // Test public keys - bytes32 constant QX1 = bytes32(uint256(1)); - bytes32 constant QY1 = bytes32(uint256(2)); - bytes32 constant QX2 = bytes32(uint256(3)); - bytes32 constant QY2 = bytes32(uint256(4)); - - // Test owners - address owner1 = address(0x1); - address owner2 = address(0x2); - - function setUp() public { - // Deploy EntryPoint (using a mock address for testing) - entryPoint = IEntryPoint(address(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); - - // Deploy canonical ERC1967Factory if not already deployed - if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { - vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); - } - - // Deploy factory - factory = new P256AccountFactory(entryPoint); - } - - /** - * Test: Different public keys should produce SAME address (only owner and salt matter) - */ - function test_DifferentPublicKeysProduceSameAddress() public view { - uint256 salt = 0; - - address addr1 = factory.getAddress(QX1, QY1, owner1, salt); - address addr2 = factory.getAddress(QX2, QY2, owner1, salt); - - assertEq(addr1, addr2, "Different public keys should produce SAME address (only owner and salt matter)"); - } - - /** - * Test: Different owners should produce different addresses - */ - function test_DifferentOwnersProduceDifferentAddresses() public view { - uint256 salt = 0; - - address addr1 = factory.getAddress(QX1, QY1, owner1, salt); - address addr2 = factory.getAddress(QX1, QY1, owner2, salt); - - assertTrue(addr1 != addr2, "Different owners should produce different addresses"); - } - - /** - * Test: Different salts should produce different addresses - */ - function test_DifferentSaltsProduceDifferentAddresses() public view { - address addr1 = factory.getAddress(QX1, QY1, owner1, 0); - address addr2 = factory.getAddress(QX1, QY1, owner1, 1); - - assertTrue(addr1 != addr2, "Different salts should produce different addresses"); - } - - /** - * Test: Same parameters should produce same address (deterministic) - */ - function test_SameParametersProduceSameAddress() public view { - uint256 salt = 0; - - address addr1 = factory.getAddress(QX1, QY1, owner1, salt); - address addr2 = factory.getAddress(QX1, QY1, owner1, salt); - - assertEq(addr1, addr2, "Same parameters should produce same address"); - } - - /** - * Test: getAddress matches actual deployment address - */ - function test_GetAddressMatchesDeployment() public { - uint256 salt = 0; - - // Get predicted address - address predicted = factory.getAddress(QX1, QY1, owner1, salt); - - // Deploy account with 2FA enabled - vm.deal(address(this), 1 ether); - P256Account account = factory.createAccount(QX1, QY1, owner1, salt, true, bytes32("Device 1")); - - // Verify addresses match - assertEq(address(account), predicted, "Predicted address should match deployed address"); - } - - /** - * Test: Multiple users can use same salt without collision - * Different owners = different addresses (passkey doesn't matter) - */ - function test_NoCollisionWithSameSalt() public view { - uint256 salt = 42; // Same salt for all - - address addr1 = factory.getAddress(QX1, QY1, owner1, salt); - address addr2 = factory.getAddress(QX2, QY2, owner1, salt); // Same owner, different passkey - address addr3 = factory.getAddress(QX1, QY1, owner2, salt); // Different owner, same passkey - - // Same owner = same address (passkey doesn't matter) - assertEq(addr1, addr2, "Same owner should have SAME address regardless of passkey"); - // Different owners = different addresses - assertTrue(addr1 != addr3, "Different owners should have different addresses"); - assertTrue(addr2 != addr3, "Different owners should have different addresses"); - } - - /** - * Test: Fuzz test - same owner produces same address regardless of passkey - */ - function testFuzz_SameOwnerSameAddress( - bytes32 qx1, - bytes32 qy1, - bytes32 qx2, - bytes32 qy2, - address owner, - uint256 salt - ) public view { - // Skip if public keys are the same (we want to test different keys) - vm.assume(qx1 != qx2 || qy1 != qy2); - // Skip zero address - vm.assume(owner != address(0)); - - address addr1 = factory.getAddress(qx1, qy1, owner, salt); - address addr2 = factory.getAddress(qx2, qy2, owner, salt); - - assertEq(addr1, addr2, "Same owner should always produce SAME address regardless of passkey"); - } - - /** - * Test: Verify initCode produces correct address - */ - function test_InitCodeProducesCorrectAddress() public view { - uint256 salt = 0; - - // Get predicted address - address predicted = factory.getAddress(QX1, QY1, owner1, salt); - - // Get initCode with 2FA enabled - bytes memory initCode = factory.getInitCode(QX1, QY1, owner1, salt, true, bytes32("Device 1")); - - // Verify initCode contains factory address - address factoryFromInitCode; - assembly { - factoryFromInitCode := mload(add(initCode, 20)) - } - assertEq(factoryFromInitCode, address(factory), "InitCode should contain factory address"); - } - - /** - * Test: CREATE2 salt includes owner, implementation, and salt (NOT passkey) - */ - function test_SaltIncludesOwnerImplementationAndSalt() public view { - // Address should depend on owner, implementation, and salt, NOT on passkey (qx, qy) - address addr1 = factory.getAddress(QX1, QY1, owner1, 0); - address addr2 = factory.getAddress(QX2, QY1, owner1, 0); // Different qx - address addr3 = factory.getAddress(QX1, QY2, owner1, 0); // Different qy - address addr4 = factory.getAddress(QX1, QY1, owner2, 0); // Different owner - address addr5 = factory.getAddress(QX1, QY1, owner1, 1); // Different salt - - // qx should NOT affect address (same owner, same salt, same implementation) - assertEq(addr1, addr2, "qx should NOT affect address"); - // qy should NOT affect address (same owner, same salt, same implementation) - assertEq(addr1, addr3, "qy should NOT affect address"); - // owner SHOULD affect address - assertTrue(addr1 != addr4, "owner should affect address"); - // salt SHOULD affect address - assertTrue(addr1 != addr5, "salt should affect address"); - } - - /** - * Test: Different implementations produce different addresses - * This allows reusing the same salt (index) during development when contract code changes - */ - function test_DifferentImplementationsProduceDifferentAddresses() public { - // Get address with current implementation - address addr1 = factory.getAddress(QX1, QY1, owner1, 0); - - // Deploy a new implementation (simulating contract upgrade during development) - P256Account newImplementation = new P256Account(entryPoint); - - // Create a new factory with the new implementation - P256AccountFactory newFactory = new P256AccountFactory(entryPoint); - - // Get address with new implementation (same owner, same salt) - address addr2 = newFactory.getAddress(QX1, QY1, owner1, 0); - - // Addresses should be different because implementation changed - assertTrue(addr1 != addr2, "Different implementations should produce different addresses"); - - // Verify both factories have different implementations - assertTrue( - address(factory.IMPLEMENTATION()) != address(newFactory.IMPLEMENTATION()), - "Factories should have different implementations" - ); - } - - /** - * Test: Implementation contract should be locked (cannot be initialized) - */ - function test_ImplementationContractIsLocked() public { - P256Account implementation = factory.IMPLEMENTATION(); - - // Try to initialize the implementation contract - should revert - vm.expectRevert(abi.encodeWithSignature("InvalidInitialization()")); - implementation.initialize(QX1, QY1, owner1, true, bytes32("Device 1")); - } - - /** - * Test: Proxy accounts should be minimal in size - */ - function test_ProxyAccountsAreMinimal() public { - P256Account account = factory.createAccount(QX1, QY1, owner1, 0, true, bytes32("Device 1")); - - // Get the bytecode size of the deployed proxy - uint256 proxySize = address(account).code.length; - - // ERC-1967 proxy should be around 160 bytes (much smaller than full implementation) - // Allow some margin for variations - assertTrue(proxySize < 500, "Proxy should be minimal (<500 bytes)"); - - // For reference, log the actual size - emit log_named_uint("Proxy bytecode size", proxySize); - } - - /** - * Test: All proxies should point to the same implementation - */ - function test_AllProxiesShareSameImplementation() public { - P256Account account1 = factory.createAccount(QX1, QY1, owner1, 0, true, bytes32("Device 1")); - P256Account account2 = factory.createAccount(QX2, QY2, owner2, 1, false, bytes32("Device 2")); - - // Both should point to the same implementation - P256Account impl = factory.IMPLEMENTATION(); - - // Verify both accounts are functional (have the same interface) - assertEq(address(account1.ENTRYPOINT()), address(entryPoint)); - assertEq(address(account2.ENTRYPOINT()), address(entryPoint)); - - // Verify implementation is set correctly - assertTrue(address(impl) != address(0), "Implementation should be deployed"); - } - - /** - * Test: Gas benchmark for proxy deployment vs full deployment - */ - function test_GasBenchmarkProxyDeployment() public { - // Measure gas for proxy deployment - uint256 gasBefore = gasleft(); - P256Account account = factory.createAccount(QX1, QY1, owner1, 0, true, bytes32("Device 1")); - uint256 gasUsed = gasBefore - gasleft(); - - // Log gas usage - emit log_named_uint("Gas used for proxy deployment", gasUsed); - - // Verify account is functional - assertEq(address(account.ENTRYPOINT()), address(entryPoint)); - assertEq(account.owner(), owner1); - assertTrue(account.twoFactorEnabled()); - } -} From eca7614c1fc472d2ffea2787f9062aaaab9afdc9 Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 3 Dec 2025 11:27:52 +0700 Subject: [PATCH 16/22] feat: add init code hash scripts for CREATE2 vanity mining - GetInitCodeHash.s.sol: Computes hash for P256MFAValidatorModule - GetFactoryInitCodeHash.s.sol: Computes hash for AuraAccountFactory (requires validator address as input since it's a constructor arg) Workflow: 1. Run GetInitCodeHash.s.sol to get validator hash 2. Mine validator vanity salt, compute expected address 3. Run GetFactoryInitCodeHash.s.sol with expected validator address 4. Mine factory vanity salt 5. Update Deploy.s.sol with both salts --- script/GetFactoryInitCodeHash.s.sol | 45 +++++++++++++++++++++++++++++ script/GetInitCodeHash.s.sol | 37 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 script/GetFactoryInitCodeHash.s.sol create mode 100644 script/GetInitCodeHash.s.sol diff --git a/script/GetFactoryInitCodeHash.s.sol b/script/GetFactoryInitCodeHash.s.sol new file mode 100644 index 0000000..a9d8755 --- /dev/null +++ b/script/GetFactoryInitCodeHash.s.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Script, console2} from "forge-std/Script.sol"; +import {AuraAccountFactory} from "../src/modular/AuraAccountFactory.sol"; + +/** + * @title GetFactoryInitCodeHashScript + * @notice Computes init code hash for AuraAccountFactory (CREATE2 vanity mining) + * @dev Usage: forge script script/GetFactoryInitCodeHash.s.sol \ + * --sig 'run(address)' + * + * The validator address is required because it's a constructor argument, + * which affects the init code hash. + * + * Workflow: + * 1. First deploy or compute expected P256MFAValidatorModule address + * 2. Run this script with that address + * 3. Mine vanity salt with create2crunch + * 4. Update Deploy.s.sol with the salt + */ +contract GetFactoryInitCodeHashScript is Script { + address constant SOLADY_CREATE2_FACTORY = 0x0000000000FFe8B47B3e2130213B802212439497; + + function run(address validatorAddress) external view { + require(validatorAddress != address(0), "Validator address required"); + + bytes memory creationCode = abi.encodePacked( + type(AuraAccountFactory).creationCode, + abi.encode(validatorAddress) + ); + bytes32 initCodeHash = keccak256(creationCode); + + console2.log("=== AuraAccountFactory Init Code Hash ==="); + console2.log("Solady CREATE2 Factory:", SOLADY_CREATE2_FACTORY); + console2.log("Validator Address:", validatorAddress); + console2.log(""); + console2.log("Init Code Hash:", vm.toString(initCodeHash)); + console2.log("Init Code Length:", creationCode.length, "bytes"); + console2.log(""); + console2.log("Export for vanity mining:"); + console2.log(" export INIT_CODE_HASH=%s", vm.toString(initCodeHash)); + } +} + diff --git a/script/GetInitCodeHash.s.sol b/script/GetInitCodeHash.s.sol new file mode 100644 index 0000000..a7667bc --- /dev/null +++ b/script/GetInitCodeHash.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Script, console2} from "forge-std/Script.sol"; +import {P256MFAValidatorModule} from "../src/modular/modules/validators/P256MFAValidatorModule.sol"; + +/** + * @title GetInitCodeHashScript + * @notice Computes init code hash for P256MFAValidatorModule (CREATE2 vanity mining) + * @dev Usage: forge script script/GetInitCodeHash.s.sol + * + * Workflow: + * 1. Run this script to get validator init code hash + * 2. Mine vanity salt with create2crunch + * 3. Compute expected validator address + * 4. Run GetFactoryInitCodeHash.s.sol with expected validator address + * 5. Mine factory vanity salt + * 6. Update Deploy.s.sol with both salts + */ +contract GetInitCodeHashScript is Script { + address constant SOLADY_CREATE2_FACTORY = 0x0000000000FFe8B47B3e2130213B802212439497; + + function run() external view { + bytes memory creationCode = type(P256MFAValidatorModule).creationCode; + bytes32 initCodeHash = keccak256(creationCode); + + console2.log("=== P256MFAValidatorModule Init Code Hash ==="); + console2.log("Solady CREATE2 Factory:", SOLADY_CREATE2_FACTORY); + console2.log(""); + console2.log("Init Code Hash:", vm.toString(initCodeHash)); + console2.log("Init Code Length:", creationCode.length, "bytes"); + console2.log(""); + console2.log("Export for vanity mining:"); + console2.log(" export INIT_CODE_HASH=%s", vm.toString(initCodeHash)); + } +} + From 70b0c3f37d2b62fa98b6e50ecf9986ff0734a60e Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 3 Dec 2025 11:31:24 +0700 Subject: [PATCH 17/22] docs: add vanity address mining guide --- docs/VANITY_MINING.md | 134 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/VANITY_MINING.md diff --git a/docs/VANITY_MINING.md b/docs/VANITY_MINING.md new file mode 100644 index 0000000..d8bb40c --- /dev/null +++ b/docs/VANITY_MINING.md @@ -0,0 +1,134 @@ +# Vanity Address Mining Guide + +This guide explains how to deploy EthAura contracts to deterministic vanity addresses using CREATE2. + +## Overview + +Both `P256MFAValidatorModule` and `AuraAccountFactory` can be deployed to vanity addresses (e.g., `0x000000...`) using Solady's CREATE2 factory. This ensures the same contract addresses across all EVM networks. + +**Solady CREATE2 Factory:** `0x0000000000FFe8B47B3e2130213B802212439497` + +## Prerequisites + +Install [create2crunch](https://github.com/0age/create2crunch) for GPU-accelerated vanity mining: + +```bash +cargo install create2crunch +``` + +## Workflow + +### Step 1: Get Validator Init Code Hash + +```bash +forge script script/GetInitCodeHash.s.sol +``` + +Output: +``` +=== P256MFAValidatorModule Init Code Hash === +Init Code Hash: 0x1234...abcd +``` + +### Step 2: Mine Validator Vanity Salt + +```bash +create2crunch \ + --factory 0x0000000000FFe8B47B3e2130213B802212439497 \ + --caller \ + --init-code-hash \ + --leading 6 \ + --output validator_salt.txt +``` + +> **Note:** For Solady's factory, the first 20 bytes of the salt must match your deployer address. + +### Step 3: Compute Expected Validator Address + +Use the mined salt to compute the expected validator address: + +```bash +cast compute-address2 \ + --starts-with 000000 \ + --deployer 0x0000000000FFe8B47B3e2130213B802212439497 \ + --salt \ + --init-code-hash +``` + +Or calculate manually: +``` +address = keccak256(0xff ++ factory ++ salt ++ initCodeHash)[12:] +``` + +### Step 4: Get Factory Init Code Hash + +Run with the expected validator address from Step 3: + +```bash +forge script script/GetFactoryInitCodeHash.s.sol \ + --sig 'run(address)' +``` + +Output: +``` +=== AuraAccountFactory Init Code Hash === +Validator Address: 0x000000... +Init Code Hash: 0x5678...efgh +``` + +### Step 5: Mine Factory Vanity Salt + +```bash +create2crunch \ + --factory 0x0000000000FFe8B47B3e2130213B802212439497 \ + --caller \ + --init-code-hash \ + --leading 6 \ + --output factory_salt.txt +``` + +### Step 6: Update Deploy Script + +Edit `script/Deploy.s.sol` with the mined salts: + +```solidity +bytes32 constant VALIDATOR_SALT = 0x; +bytes32 constant FACTORY_SALT = 0x; +``` + +### Step 7: Deploy + +```bash +forge script script/Deploy.s.sol:DeployScript \ + --rpc-url \ + --broadcast \ + --verify +``` + +## Important Notes + +1. **Order matters:** Deploy validator first, then factory (factory depends on validator address) + +2. **Deterministic addresses:** Same salts + same bytecode = same addresses on all EVM chains + +3. **Salt format:** For Solady's CREATE2 factory, salt format is: + ``` + [deployer address (20 bytes)][arbitrary suffix (12 bytes)] + ``` + +4. **Mining time:** Finding 6 leading zero bytes typically takes minutes to hours depending on GPU + +## Example + +```bash +# Deployer: 0x18Ee4C040568238643C07e7aFd6c53efc196D26b + +# After mining, you might get: +VALIDATOR_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b0000000000000000deadbeef +FACTORY_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b00000000000000001234cafe + +# Resulting in addresses like: +# P256MFAValidatorModule: 0x000000a1b2c3d4e5f6... +# AuraAccountFactory: 0x000000f6e5d4c3b2a1... +``` + From 3f45cae7fd7e4ddacad16b8282b076911a727c2b Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 3 Dec 2025 11:38:35 +0700 Subject: [PATCH 18/22] refactor: rename GetInitCodeHash to GetValidatorInitCodeHash Consistent naming with GetFactoryInitCodeHash.s.sol --- docs/VANITY_MINING.md | 2 +- script/Deploy.s.sol | 11 ++++------- script/GetFactoryInitCodeHash.s.sol | 6 ++---- ...tCodeHash.s.sol => GetValidatorInitCodeHash.s.sol} | 6 +++--- 4 files changed, 10 insertions(+), 15 deletions(-) rename script/{GetInitCodeHash.s.sol => GetValidatorInitCodeHash.s.sol} (89%) diff --git a/docs/VANITY_MINING.md b/docs/VANITY_MINING.md index d8bb40c..65886ec 100644 --- a/docs/VANITY_MINING.md +++ b/docs/VANITY_MINING.md @@ -21,7 +21,7 @@ cargo install create2crunch ### Step 1: Get Validator Init Code Hash ```bash -forge script script/GetInitCodeHash.s.sol +forge script script/GetValidatorInitCodeHash.s.sol ``` Output: diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index b9a3372..366806f 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -55,10 +55,8 @@ contract DeployScript is Script { console2.log(""); // Step 2: Compute factory creation code (depends on validator address) - bytes memory factoryCreationCode = abi.encodePacked( - type(AuraAccountFactory).creationCode, - abi.encode(expectedValidator) - ); + bytes memory factoryCreationCode = + abi.encodePacked(type(AuraAccountFactory).creationCode, abi.encode(expectedValidator)); bytes32 factoryInitCodeHash = keccak256(factoryCreationCode); address expectedFactory = computeCreate2Address(SOLADY_CREATE2_FACTORY, FACTORY_SALT, factoryInitCodeHash); @@ -107,9 +105,8 @@ contract DeployScript is Script { } function _deployViaCreate2(bytes32 salt, bytes memory creationCode) internal returns (address) { - (bool success, bytes memory returnData) = SOLADY_CREATE2_FACTORY.call( - abi.encodeWithSignature("safeCreate2(bytes32,bytes)", salt, creationCode) - ); + (bool success, bytes memory returnData) = + SOLADY_CREATE2_FACTORY.call(abi.encodeWithSignature("safeCreate2(bytes32,bytes)", salt, creationCode)); require(success, "CREATE2 deployment failed"); return abi.decode(returnData, (address)); } diff --git a/script/GetFactoryInitCodeHash.s.sol b/script/GetFactoryInitCodeHash.s.sol index a9d8755..1cd482b 100644 --- a/script/GetFactoryInitCodeHash.s.sol +++ b/script/GetFactoryInitCodeHash.s.sol @@ -25,10 +25,8 @@ contract GetFactoryInitCodeHashScript is Script { function run(address validatorAddress) external view { require(validatorAddress != address(0), "Validator address required"); - bytes memory creationCode = abi.encodePacked( - type(AuraAccountFactory).creationCode, - abi.encode(validatorAddress) - ); + bytes memory creationCode = + abi.encodePacked(type(AuraAccountFactory).creationCode, abi.encode(validatorAddress)); bytes32 initCodeHash = keccak256(creationCode); console2.log("=== AuraAccountFactory Init Code Hash ==="); diff --git a/script/GetInitCodeHash.s.sol b/script/GetValidatorInitCodeHash.s.sol similarity index 89% rename from script/GetInitCodeHash.s.sol rename to script/GetValidatorInitCodeHash.s.sol index a7667bc..d19ef9c 100644 --- a/script/GetInitCodeHash.s.sol +++ b/script/GetValidatorInitCodeHash.s.sol @@ -5,9 +5,9 @@ import {Script, console2} from "forge-std/Script.sol"; import {P256MFAValidatorModule} from "../src/modular/modules/validators/P256MFAValidatorModule.sol"; /** - * @title GetInitCodeHashScript + * @title GetValidatorInitCodeHashScript * @notice Computes init code hash for P256MFAValidatorModule (CREATE2 vanity mining) - * @dev Usage: forge script script/GetInitCodeHash.s.sol + * @dev Usage: forge script script/GetValidatorInitCodeHash.s.sol * * Workflow: * 1. Run this script to get validator init code hash @@ -17,7 +17,7 @@ import {P256MFAValidatorModule} from "../src/modular/modules/validators/P256MFAV * 5. Mine factory vanity salt * 6. Update Deploy.s.sol with both salts */ -contract GetInitCodeHashScript is Script { +contract GetValidatorInitCodeHashScript is Script { address constant SOLADY_CREATE2_FACTORY = 0x0000000000FFe8B47B3e2130213B802212439497; function run() external view { From cff2737a1e433678dec5f6b4659891532fbb5cb2 Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 3 Dec 2025 11:49:21 +0700 Subject: [PATCH 19/22] deploy: update CREATE2 salts for vanity addresses P256MFAValidatorModule: 0x000000b07799b322d076669ef32b247d02279c7e AuraAccountFactory: 0x0000004b2941659deb7472b46f7b84caf27dce44 Successfully deployed to Sepolia testnet. --- .../11155111/run-1764737340817.json | 117 ++++++++++++++++++ .../Deploy.s.sol/11155111/run-latest.json | 112 ++++++++++------- script/Deploy.s.sol | 8 +- 3 files changed, 190 insertions(+), 47 deletions(-) create mode 100644 broadcast/Deploy.s.sol/11155111/run-1764737340817.json diff --git a/broadcast/Deploy.s.sol/11155111/run-1764737340817.json b/broadcast/Deploy.s.sol/11155111/run-1764737340817.json new file mode 100644 index 0000000..464f4c7 --- /dev/null +++ b/broadcast/Deploy.s.sol/11155111/run-1764737340817.json @@ -0,0 +1,117 @@ +{ + "transactions": [ + { + "hash": "0xd93f45dec98dc26096f4bcc3a502e5f340ec48ed7223d523d4bd40926202f033", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0x0000000000ffe8b47b3e2130213b802212439497", + "function": null, + "arguments": null, + "transaction": { + "from": "0x18ee4c040568238643c07e7afd6c53efc196d26b", + "to": "0x0000000000ffe8b47b3e2130213b802212439497", + "gas": "0x2ea619", + "value": "0x0", + "input": "0x64e0308718ee4c040568238643c07e7afd6c53efc196d26b00000000000000000028c56f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000241460808060405234610016576123f9908161001b8239f35b5f80fdfe60406080815260049081361015610014575f80fd5b5f3560e01c908163053518f3146110d457816313af403514610fc25781631c848fb914610ec75781631eac094714610e32578163541c03b714610daa5781635aedae9114610d115781635bd865c214610c845781636d61fe7014610a3757816385030d58146109235781638a91b0e3146106f4578163970032031461068c578163ab18c40f146102e9578163d60b347f1461028c578163ecd059611461024f578163f551e2ee146101bf578163fa54416114610121575063fac7932e146100d9575f80fd5b3461011d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5761011b9060443590602435903533611e89565b005b5f80fd5b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209073ffffffffffffffffffffffffffffffffffffffff6101b661017361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b54169051908152f35b90503461011d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d576101f861117b565b5060443567ffffffffffffffff811161011d5761024761023e6020947fffffffff000000000000000000000000000000000000000000000000000000009336910161119e565b906024356118af565b915191168152f35b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d576001602092519135148152f35b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209073ffffffffffffffffffffffffffffffffffffffff6102de61017361117b565b541615159051908152f35b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d578035916103633373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b835f526020526003906003815f20019081549060ff8216156106645760ff6103c83373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541680610613575b6105eb57507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6026020526040902080549081156105bf577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9182019055335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260409020905f5b8254808210156105b4578661049a8386611272565b905490871b1c146104ae5750600101610485565b8281969394959601908111610588576104da6104cd61050f9287611272565b905490881b1c9286611272565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b9055825490811561055c57508101926105288484611272565b81939154921b1b19169055555b337f2d7aa68d7c6e29815818dcd0deea0f49ceac033240ac3b69b6a6734c23e5843d5f80a3005b6031907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b6011837f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b505050505050610535565b6011847f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b8490517f53f56588000000000000000000000000000000000000000000000000000000008152fd5b50600161065d3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b54146103d0565b8490517fddb63d7c000000000000000000000000000000000000000000000000000000008152fd5b90503461011d577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc818136011261011d5782359067ffffffffffffffff821161011d5761012090823603011261011d576020926106ed916024359101611358565b9051908152f35b90503461011d5760209060207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57823567ffffffffffffffff811161011d57610746903690850161119e565b5050335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6036020908152604080832080547fffffffffffffffffffffffff00000000000000000000000000000000000000001690557f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d604825280832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690557f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d601909152812093905b845481101561089d576001905f836108683373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b610872848a611272565b919054600392831b1c8452885282878120918183558187840155816002840155820155015501610813565b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260408120805491815581610905575b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6026020526040812055005b5f5260205f20908101905b818110156108d5575f8155600101610910565b823461011d57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760a0916109c761096061117b565b5f6080845161096e816111cc565b8281528260208201528286820152826060820152015273ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b6024355f52602052805f208151916109de836111cc565b815493848452600183015460208501908152608060028501549284870193845260ff6003870154169560608801961515875201549501948552825195865251602086015251908401525115156060830152516080820152f35b90503461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57813567ffffffffffffffff811161011d57610a8860a0913690850161119e565b908092918101031261011d5780359073ffffffffffffffffffffffffffffffffffffffff821680920361011d57602081013583820135608083013593841515850361011d578015610c5c57335f8181527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6036020526040812080547fffffffffffffffffffffffff000000000000000000000000000000000000000016841790557f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a7359080a381151580610c53575b610c3c575b505050610b6357005b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d602602052604090205415610c16575050335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d604602052604090205b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00825416179055337f5e0647e9a594598413ffd03f39a420df134735f631208f3728e01ef447daa9eb5f80a2005b517f29ec01e9000000000000000000000000000000000000000000000000000000008152fd5b6060610c4b9301359133611e89565b5f8080610b5a565b50801515610b55565b8686517f49e27cff000000000000000000000000000000000000000000000000000000008152fd5b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209060ff610d06610cc361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541690519015158152f35b3461011d57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57602090610d90610d4d61117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b6024355f52825260ff6003825f2001541690519015158152f35b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57602090610e2a610de761117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b549051908152f35b90503461011d575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d602602052604090205415610c1657335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260409020610bc6565b3461011d576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5790610f46610f0361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260405f2090565b8151928381835491828152019081935f52825f20905f5b818110610fae5750505084610f73910385611231565b825181815293518185018190528493840192915f5b828110610f9757505050500390f35b835185528695509381019392810192600101610f88565b825484529284019260019283019201610f5d565b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5773ffffffffffffffffffffffffffffffffffffffff61100f61117b565b169182156110ae578261105f3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b817fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055337f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a7355f80a3005b517f49e27cff000000000000000000000000000000000000000000000000000000008152fd5b3461011d575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57335f8181527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6046020526040812080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690557f7a146c6d6578acec91e6f3f4afddbba61d50a19aecf2cedf2753fa04168b61879080a2005b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361011d57565b9181601f8401121561011d5782359167ffffffffffffffff831161011d576020838186019501011161011d57565b60a0810190811067ffffffffffffffff8211176111e857604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040810190811067ffffffffffffffff8211176111e857604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176111e857604052565b8054821015611287575f5260205f2001905f90565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18136030182121561011d570180359067ffffffffffffffff821161011d5760200191813603831361011d57565b9093929384831161011d57841161011d578101920390565b35906020811061132b575090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060200360031b1b1690565b9073ffffffffffffffffffffffffffffffffffffffff6113b53373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b541660ff6114003373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b54169261010090818101601461141682846112b4565b90501061187557611426916112b4565b908160141161011d57601401947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec820190156118805760e08110611875577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffab82019281841161184857836114bd837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8b96818b611305565b9590940192808411611848576114de91846114d8928b611305565b9061131d565b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260409020905f526020908152604090815f209860039460ff60038c01541615801561183f575b61182f5761153a91612252565b825193898386015282855261154e85611215565b8a546001809c0154905f978d886060998151908161173e575b50505090508585015197885191888701516060880151908351918c8b600d85017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c885284830189600d87890101106022602d89840101515f1a141691838160138c012092012014169388846014011090841760801c10927f2274797065223a22776562617574686e2e67657422000000000000000000000060581c9201015160581c1416169252865180519260058060218401511614908a851116169a8b61170b575b505050505086611662575b50505050505090501561165b5761164e93612116565b1561165857505f90565b90565b5050505090565b849650918395979160a0938460805f9701519801518a819b51998a96875289870152850152606084015260808301528380525afa503d156116d4575b50507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a85f518714911110805f8080808080611638565b6d1ab2e8006fd8b71907bf06a5bdee3b61169e5760a05f916dd01ea45f9efd5c54f037fa57ea1a5afa15611709575f8061169e565bfe5b83949c5089808095840101809e82808084519a019601940160025afa831b5afa5198523d15611709575f808e818061162d565b8a91929b50899060026003600286010460021b9584519e8f987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f927f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5289878c019b01968981890194010194855196825f88525b6117e5575b50505050505091600393915f959352018b5206600204809303520387525f8d818080611567565b859c8460049297959697019d8e51818160121c16515f538181600c1c16518653818160061c16518553165186535f518152019b858d101561182a5782959493956117b9565b6117be565b5050505050505050505050600190565b508a541561152d565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b505050505050600190565b80939495925060419150036118a65761189893612116565b156118a1575f90565b600190565b50505050600190565b919060148210611e62578160141161011d576014017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec82019173ffffffffffffffffffffffffffffffffffffffff6119443373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b54169260ff6119903373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541615611e4e5760e08110611e25577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffab82019281841161184857836119f8837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8b968185611305565b959094019280841161184857611a1391846114d89285611305565b90611a5b3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b915f526020918252604091825f209160039460ff600385015416158015611e1c575b611dee57611a8a91612252565b9383518983820152828152611a9e81611215565b83546001968780960154925f9481606092805180611cf9575b50505050858201519081519089840151606085015190825191600d83017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c855282870186600d85870101106022602d87840101515f1a1416918d81601389012092012014169185826014011090821760801c10908b7f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918801015160581c14161691528784519b8c926005806021865196015116149083851116169c8d9586611cc1575b505050505050611c16575b5050505050505015611bee57611ba193612116565b15611bca577f1626ba7e0000000000000000000000000000000000000000000000000000000090565b7fffffffff0000000000000000000000000000000000000000000000000000000090565b505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b909192939495965060a06080820151910151968791815195865286860152840152606083015260808201525f8052815f60a0836101005afa503d15611c8c575b50507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8905f51149111105f808080808080611b8c565b6d1ab2e8006fd8b71907bf06a5bdee3b611c565760a05f916dd01ea45f9efd5c54f037fa57ea1a5afa15611709575f80611c56565b91939550918080959a5086840101809a82808084519a019601940160025afa831b5afa5194523d1561170957875f8087898280611b81565b9093508a896002936003600285010460021b948d8451987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f987f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f52858b0199898c01968981890194010194855196855f88525b611d9e575b50505050505091600393915f959352018d5206600204809303520381525f808080611ab7565b859c83856004939894959697980193845190828260121c16515f538282600c1c16519053818160061c16518653165186535f518152019b858d1015611de857949392919085611d73565b94611d78565b505050505050505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b50835415611a7d565b50505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b91905060418203611bee57611ba193612116565b5050507fffffffff0000000000000000000000000000000000000000000000000000000090565b92908015801561210e575b6120e4576040918251946020928387018181528686890152858852606088019780891067ffffffffffffffff8a11176111e8578887525190209673ffffffffffffffffffffffffffffffffffffffff831696875f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60091828752875f208a5f52875260ff6003895f200154166120bc575090600491875193611f35856111cc565b845286840190815287840142815260608501916001835260808601938885528b5f528952895f208c5f528952895f2095518655516001860155516002850155600384019051151560ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00835416911617905551910155611ff28173ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260405f2090565b90815491680100000000000000008310156111e85761201e6104da848a93600161206397018155611272565b905573ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b928354937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff85146118485760017f6d847e8457dd04dfa090a722cff190d721a734963c5036ea7d44809f60eac2af9501905551908152a3565b807f692d285a0000000000000000000000000000000000000000000000000000000060049252fd5b60046040517f65a3c412000000000000000000000000000000000000000000000000000000008152fd5b508215611e94565b92919067ffffffffffffffff82116111e857602090604094855193612162847fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8401160186611231565b80855283850192368282011161011d57815f928692863786010152855193805187816040146121f957506041146121a95750505050505050505b638baa579f5f526004601cfd5b86606091828101515f1a8652015190525b5f52518452600160805f825afa51925f606052523d6121db5750505061219c565b73ffffffffffffffffffffffffffffffffffffffff80911691161490565b90507f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff910151601b8160ff1c018552166060526121ba565b6040909291928382519485926020840137808252015f602082015201604052565b91909160405160c0810181811067ffffffffffffffff8211176111e857604052606081526020810160608152604082015f8152606083015f815260808401915f835260a08501955f8752859860468110156122b2575b5050505050505050565b8101947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc8601916002813560f01c80830191820191858311156122f9575b505050506122a8565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09a61234c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffba9360026123579701612231565b905289030190612231565b90523560f01c90527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbe83013560f01c90527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08201359052013590525f80808080808080808080806122f056fea26469706673582212206e916195c5615e46311c599d70fbea075235964ce56d6588e52c7187e8f214ee64736f6c63430008170033000000000000000000000000", + "nonce": "0x8e", + "chainId": "0xaa36a7" + }, + "additionalContracts": [ + { + "transactionType": "CREATE2", + "contractName": "P256MFAValidatorModule", + "address": "0x000000b07799b322d076669ef32b247d02279c7e", + "initCode": "0x60808060405234610016576123f9908161001b8239f35b5f80fdfe60406080815260049081361015610014575f80fd5b5f3560e01c908163053518f3146110d457816313af403514610fc25781631c848fb914610ec75781631eac094714610e32578163541c03b714610daa5781635aedae9114610d115781635bd865c214610c845781636d61fe7014610a3757816385030d58146109235781638a91b0e3146106f4578163970032031461068c578163ab18c40f146102e9578163d60b347f1461028c578163ecd059611461024f578163f551e2ee146101bf578163fa54416114610121575063fac7932e146100d9575f80fd5b3461011d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5761011b9060443590602435903533611e89565b005b5f80fd5b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209073ffffffffffffffffffffffffffffffffffffffff6101b661017361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b54169051908152f35b90503461011d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d576101f861117b565b5060443567ffffffffffffffff811161011d5761024761023e6020947fffffffff000000000000000000000000000000000000000000000000000000009336910161119e565b906024356118af565b915191168152f35b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d576001602092519135148152f35b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209073ffffffffffffffffffffffffffffffffffffffff6102de61017361117b565b541615159051908152f35b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d578035916103633373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b835f526020526003906003815f20019081549060ff8216156106645760ff6103c83373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541680610613575b6105eb57507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6026020526040902080549081156105bf577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9182019055335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260409020905f5b8254808210156105b4578661049a8386611272565b905490871b1c146104ae5750600101610485565b8281969394959601908111610588576104da6104cd61050f9287611272565b905490881b1c9286611272565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b9055825490811561055c57508101926105288484611272565b81939154921b1b19169055555b337f2d7aa68d7c6e29815818dcd0deea0f49ceac033240ac3b69b6a6734c23e5843d5f80a3005b6031907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b6011837f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b505050505050610535565b6011847f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b8490517f53f56588000000000000000000000000000000000000000000000000000000008152fd5b50600161065d3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b54146103d0565b8490517fddb63d7c000000000000000000000000000000000000000000000000000000008152fd5b90503461011d577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc818136011261011d5782359067ffffffffffffffff821161011d5761012090823603011261011d576020926106ed916024359101611358565b9051908152f35b90503461011d5760209060207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57823567ffffffffffffffff811161011d57610746903690850161119e565b5050335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6036020908152604080832080547fffffffffffffffffffffffff00000000000000000000000000000000000000001690557f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d604825280832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690557f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d601909152812093905b845481101561089d576001905f836108683373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b610872848a611272565b919054600392831b1c8452885282878120918183558187840155816002840155820155015501610813565b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260408120805491815581610905575b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6026020526040812055005b5f5260205f20908101905b818110156108d5575f8155600101610910565b823461011d57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760a0916109c761096061117b565b5f6080845161096e816111cc565b8281528260208201528286820152826060820152015273ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b6024355f52602052805f208151916109de836111cc565b815493848452600183015460208501908152608060028501549284870193845260ff6003870154169560608801961515875201549501948552825195865251602086015251908401525115156060830152516080820152f35b90503461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57813567ffffffffffffffff811161011d57610a8860a0913690850161119e565b908092918101031261011d5780359073ffffffffffffffffffffffffffffffffffffffff821680920361011d57602081013583820135608083013593841515850361011d578015610c5c57335f8181527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6036020526040812080547fffffffffffffffffffffffff000000000000000000000000000000000000000016841790557f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a7359080a381151580610c53575b610c3c575b505050610b6357005b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d602602052604090205415610c16575050335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d604602052604090205b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00825416179055337f5e0647e9a594598413ffd03f39a420df134735f631208f3728e01ef447daa9eb5f80a2005b517f29ec01e9000000000000000000000000000000000000000000000000000000008152fd5b6060610c4b9301359133611e89565b5f8080610b5a565b50801515610b55565b8686517f49e27cff000000000000000000000000000000000000000000000000000000008152fd5b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209060ff610d06610cc361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541690519015158152f35b3461011d57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57602090610d90610d4d61117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b6024355f52825260ff6003825f2001541690519015158152f35b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57602090610e2a610de761117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b549051908152f35b90503461011d575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d602602052604090205415610c1657335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260409020610bc6565b3461011d576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5790610f46610f0361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260405f2090565b8151928381835491828152019081935f52825f20905f5b818110610fae5750505084610f73910385611231565b825181815293518185018190528493840192915f5b828110610f9757505050500390f35b835185528695509381019392810192600101610f88565b825484529284019260019283019201610f5d565b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5773ffffffffffffffffffffffffffffffffffffffff61100f61117b565b169182156110ae578261105f3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b817fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055337f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a7355f80a3005b517f49e27cff000000000000000000000000000000000000000000000000000000008152fd5b3461011d575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57335f8181527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6046020526040812080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690557f7a146c6d6578acec91e6f3f4afddbba61d50a19aecf2cedf2753fa04168b61879080a2005b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361011d57565b9181601f8401121561011d5782359167ffffffffffffffff831161011d576020838186019501011161011d57565b60a0810190811067ffffffffffffffff8211176111e857604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040810190811067ffffffffffffffff8211176111e857604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176111e857604052565b8054821015611287575f5260205f2001905f90565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18136030182121561011d570180359067ffffffffffffffff821161011d5760200191813603831361011d57565b9093929384831161011d57841161011d578101920390565b35906020811061132b575090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060200360031b1b1690565b9073ffffffffffffffffffffffffffffffffffffffff6113b53373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b541660ff6114003373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b54169261010090818101601461141682846112b4565b90501061187557611426916112b4565b908160141161011d57601401947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec820190156118805760e08110611875577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffab82019281841161184857836114bd837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8b96818b611305565b9590940192808411611848576114de91846114d8928b611305565b9061131d565b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260409020905f526020908152604090815f209860039460ff60038c01541615801561183f575b61182f5761153a91612252565b825193898386015282855261154e85611215565b8a546001809c0154905f978d886060998151908161173e575b50505090508585015197885191888701516060880151908351918c8b600d85017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c885284830189600d87890101106022602d89840101515f1a141691838160138c012092012014169388846014011090841760801c10927f2274797065223a22776562617574686e2e67657422000000000000000000000060581c9201015160581c1416169252865180519260058060218401511614908a851116169a8b61170b575b505050505086611662575b50505050505090501561165b5761164e93612116565b1561165857505f90565b90565b5050505090565b849650918395979160a0938460805f9701519801518a819b51998a96875289870152850152606084015260808301528380525afa503d156116d4575b50507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a85f518714911110805f8080808080611638565b6d1ab2e8006fd8b71907bf06a5bdee3b61169e5760a05f916dd01ea45f9efd5c54f037fa57ea1a5afa15611709575f8061169e565bfe5b83949c5089808095840101809e82808084519a019601940160025afa831b5afa5198523d15611709575f808e818061162d565b8a91929b50899060026003600286010460021b9584519e8f987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f927f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5289878c019b01968981890194010194855196825f88525b6117e5575b50505050505091600393915f959352018b5206600204809303520387525f8d818080611567565b859c8460049297959697019d8e51818160121c16515f538181600c1c16518653818160061c16518553165186535f518152019b858d101561182a5782959493956117b9565b6117be565b5050505050505050505050600190565b508a541561152d565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b505050505050600190565b80939495925060419150036118a65761189893612116565b156118a1575f90565b600190565b50505050600190565b919060148210611e62578160141161011d576014017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec82019173ffffffffffffffffffffffffffffffffffffffff6119443373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b54169260ff6119903373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541615611e4e5760e08110611e25577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffab82019281841161184857836119f8837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8b968185611305565b959094019280841161184857611a1391846114d89285611305565b90611a5b3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b915f526020918252604091825f209160039460ff600385015416158015611e1c575b611dee57611a8a91612252565b9383518983820152828152611a9e81611215565b83546001968780960154925f9481606092805180611cf9575b50505050858201519081519089840151606085015190825191600d83017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c855282870186600d85870101106022602d87840101515f1a1416918d81601389012092012014169185826014011090821760801c10908b7f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918801015160581c14161691528784519b8c926005806021865196015116149083851116169c8d9586611cc1575b505050505050611c16575b5050505050505015611bee57611ba193612116565b15611bca577f1626ba7e0000000000000000000000000000000000000000000000000000000090565b7fffffffff0000000000000000000000000000000000000000000000000000000090565b505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b909192939495965060a06080820151910151968791815195865286860152840152606083015260808201525f8052815f60a0836101005afa503d15611c8c575b50507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8905f51149111105f808080808080611b8c565b6d1ab2e8006fd8b71907bf06a5bdee3b611c565760a05f916dd01ea45f9efd5c54f037fa57ea1a5afa15611709575f80611c56565b91939550918080959a5086840101809a82808084519a019601940160025afa831b5afa5194523d1561170957875f8087898280611b81565b9093508a896002936003600285010460021b948d8451987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f987f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f52858b0199898c01968981890194010194855196855f88525b611d9e575b50505050505091600393915f959352018d5206600204809303520381525f808080611ab7565b859c83856004939894959697980193845190828260121c16515f538282600c1c16519053818160061c16518653165186535f518152019b858d1015611de857949392919085611d73565b94611d78565b505050505050505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b50835415611a7d565b50505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b91905060418203611bee57611ba193612116565b5050507fffffffff0000000000000000000000000000000000000000000000000000000090565b92908015801561210e575b6120e4576040918251946020928387018181528686890152858852606088019780891067ffffffffffffffff8a11176111e8578887525190209673ffffffffffffffffffffffffffffffffffffffff831696875f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60091828752875f208a5f52875260ff6003895f200154166120bc575090600491875193611f35856111cc565b845286840190815287840142815260608501916001835260808601938885528b5f528952895f208c5f528952895f2095518655516001860155516002850155600384019051151560ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00835416911617905551910155611ff28173ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260405f2090565b90815491680100000000000000008310156111e85761201e6104da848a93600161206397018155611272565b905573ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b928354937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff85146118485760017f6d847e8457dd04dfa090a722cff190d721a734963c5036ea7d44809f60eac2af9501905551908152a3565b807f692d285a0000000000000000000000000000000000000000000000000000000060049252fd5b60046040517f65a3c412000000000000000000000000000000000000000000000000000000008152fd5b508215611e94565b92919067ffffffffffffffff82116111e857602090604094855193612162847fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8401160186611231565b80855283850192368282011161011d57815f928692863786010152855193805187816040146121f957506041146121a95750505050505050505b638baa579f5f526004601cfd5b86606091828101515f1a8652015190525b5f52518452600160805f825afa51925f606052523d6121db5750505061219c565b73ffffffffffffffffffffffffffffffffffffffff80911691161490565b90507f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff910151601b8160ff1c018552166060526121ba565b6040909291928382519485926020840137808252015f602082015201604052565b91909160405160c0810181811067ffffffffffffffff8211176111e857604052606081526020810160608152604082015f8152606083015f815260808401915f835260a08501955f8752859860468110156122b2575b5050505050505050565b8101947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc8601916002813560f01c80830191820191858311156122f9575b505050506122a8565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09a61234c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffba9360026123579701612231565b905289030190612231565b90523560f01c90527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbe83013560f01c90527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08201359052013590525f80808080808080808080806122f056fea26469706673582212206e916195c5615e46311c599d70fbea075235964ce56d6588e52c7187e8f214ee64736f6c63430008170033" + } + ], + "isFixedGasLimit": false + }, + { + "hash": "0x572875cd08fec02682e4390e06e9c7fd30c7fca916d030f4e098b1f0d5fe7cf6", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0x0000000000ffe8b47b3e2130213b802212439497", + "function": null, + "arguments": null, + "transaction": { + "from": "0x18ee4c040568238643c07e7afd6c53efc196d26b", + "to": "0x0000000000ffe8b47b3e2130213b802212439497", + "gas": "0x39679f", + "value": "0x0", + "input": "0x64e0308718ee4c040568238643c07e7afd6c53efc196d26b000000000000000001b2f72400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000002d7260e034610117576001600160401b0390601f612d5238819003918201601f1916830191848311848410176100f1578084926020946040528339810103126101175751906001600160a01b03908183168084036101175715610105576040519061246690818301908111838210176100f15782916108ec833903905ff080156100e6571660805260a0526d6396ff2a80c067f99b3d2ab4df2460c0526040516107d0908161011c823960805181818161028a0152818161051b0152610739015260a05181818161021e0152610451015260c051818181609e0152818161058001526106b60152f35b6040513d5f823e3d90fd5b634e487b7160e01b5f52604160045260245ffd5b604051631a0a9b9f60e21b8152600490fd5b5f80fdfe60806040908082526004361015610014575f80fd5b5f3560e01c90816311464fbe14610242575080633a5381b5146101d45780633a8fcd9a146101295780638cb84e18146100c65763f5382f0314610055575f80fd5b346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5f80fd5b50346100c257807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c25760209073ffffffffffffffffffffffffffffffffffffffff6101216101186102ae565b60243590610663565b915191168152f35b50346100c25760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576101616102ae565b67ffffffffffffffff91906024358381116100c2576101849036906004016102d1565b9390916044359473ffffffffffffffffffffffffffffffffffffffff9384871687036100c2576064359384116100c2576020966101c86101219536906004016102d1565b939092608435956103d7565b50346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c25760209073ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b6004359073ffffffffffffffffffffffffffffffffffffffff821682036100c257565b9181601f840112156100c25782359167ffffffffffffffff83116100c257602083818601950101116100c257565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761037e57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b908160209103126100c2575173ffffffffffffffffffffffffffffffffffffffff811681036100c25790565b969593949092916103e88589610663565b803b610659575094610487939195610400868a610705565b916104bd73ffffffffffffffffffffffffffffffffffffffff95604051998a938860209a8b997f0d1bf5d3000000000000000000000000000000000000000000000000000000008b89015260a498837f00000000000000000000000000000000000000000000000000000000000000001660248a0152608060448a015260a48901916102ff565b931660648601527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc8584030160848601526102ff565b03906104ef7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0928381018a528961033d565b6040519788937fa97b90d5000000000000000000000000000000000000000000000000000000008552867f00000000000000000000000000000000000000000000000000000000000000001660048601525f60248601526044850152608060648501528051918260848601525f5b8381106106425750505060a491601f825f85879586010152011681010301815f857f0000000000000000000000000000000000000000000000000000000000000000165af1938415610637575f946105e4575b5090817f33310a89c32d8cc00057ad6ef6274d2f8fe22389a992cf89983e09fc84f6cfff92859760405195865216941692a3565b82919450610628907f33310a89c32d8cc00057ad6ef6274d2f8fe22389a992cf89983e09fc84f6cfff933d8411610630575b610620818361033d565b8101906103ab565b9390916105b0565b503d610616565b6040513d5f823e3d90fd5b8281018701518b820183015288968b96500161055d565b9750505050505050565b9061066d91610705565b604051907f5414dff0000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115610637575f916106e9575090565b610702915060203d60201161063057610620818361033d565b90565b6040519160208301917fffffffffffffffffffffffffffffffffffffffff000000000000000000000000809160601b1683527f000000000000000000000000000000000000000000000000000000000000000060601b166034840152604883015260488252608082019180831067ffffffffffffffff84111761037e576bffffffffffffffffffffffff92604052519020169056fea264697066735822122007e02d6bc6a2534a3e464491617058f3405dc92cdb96be09e8deece4731ec84464736f6c63430008170033608080604052346100895763409feecd198054906001821661007c576001600160401b039160011c6002600160401b031901610042575b6123d8838161008e8239f35b6002600160411b03905560209081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29080a15f80610036565b63f92ee8a95f526004601cfd5b5f80fdfe60806040526004361015610015575b3661142857005b5f3560e01c80630d1bf5d31461010f57806310b0a63d146100e7578063112d3a7d1461010a5780631195e07e146101055780631626ba7e1461010057806319822f7c146100fb57806346d09cea146100f65780637833fa04146100f15780639517e29f146100ec5780639cfd7cff146100e7578063a71763a8146100e2578063d03c7914146100dd578063d691c964146100d8578063e8eb3cc6146100d3578063e9ae5c53146100ce5763f2dc691d0361000e57610f05565b610ec8565b610e80565b610dac565b610d22565b610c03565b6104fe565b610a11565b610988565b61090d565b610781565b610651565b610601565b6105db565b610171565b73ffffffffffffffffffffffffffffffffffffffff81160361013257565b5f80fd5b359061014182610114565b565b9181601f840112156101325782359167ffffffffffffffff8311610132576020838186019501011161013257565b346101325760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760048035906101ae82610114565b67ffffffffffffffff602435818111610132576101ce9036908401610143565b939092604435926101de84610114565b606435908111610132576101f59036908301610143565b9290917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf6011329687548060038a55610377575b5073ffffffffffffffffffffffffffffffffffffffff9283811690811561034e5761028a9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b803b15610132576102cc975f80946040519a8b95869485937f6d61fe700000000000000000000000000000000000000000000000000000000085528401610fbd565b03925af1948515610349578695610330575b50831661031f575b5050506102ef57005b6002905560016020527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602080a1005b610328926114ef565b5f80806102e6565b8061033d610343926103d6565b8061039f565b5f6102de565b610fce565b836040517f682a6e7c000000000000000000000000000000000000000000000000000000008152fd5b600181819a939a1c14303b10156103935760ff1b1b965f610226565b8263f92ee8a95f52601cfd5b5f91031261013257565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff81116103ea57604052565b6103a9565b6040810190811067ffffffffffffffff8211176103ea57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176103ea57604052565b67ffffffffffffffff81116103ea57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b5f5b8381106104975750505f910152565b8181015183820152602001610488565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f6020936104e381518092818752878088019101610486565b0116010190565b9060206104fb9281815201906104a7565b90565b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325761057960405161053b816103ef565b601281527f657468617572612e617572612e302e312e30000000000000000000000000000060208201526040519182916020835260208301906104a7565b0390f35b60607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc82011261013257600435916024356105b781610114565b916044359067ffffffffffffffff8211610132576105d791600401610143565b9091565b346101325760206105f76105ee3661057d565b92919091611066565b6040519015158152f35b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff5f5416604051908152f35b346101325760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760243567ffffffffffffffff81116101325760206106a5610709923690600401610143565b73ffffffffffffffffffffffffffffffffffffffff5f5416906040518095819482937ff551e2ee0000000000000000000000000000000000000000000000000000000084523360048501526004356024850152606060448501526064840191610f7f565b03915afa801561034957610579915f91610752575b506040517fffffffff0000000000000000000000000000000000000000000000000000000090911681529081906020820190565b610774915060203d60201161077a575b61076c818361040b565b810190611198565b5f61071e565b503d610762565b34610132577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc606081360112610132576004359067ffffffffffffffff821161013257610120908236030112610132576044356f71727de22e5e9d8baf0edac6f37da03233036108e357602061086c5f9361082f610816610816875473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b906040519586809481937f97003203000000000000000000000000000000000000000000000000000000008352602435906004016004840161120c565b03925af190811561034957610579925f926108b2575b508061089a575b506040519081529081906020820190565b5f80808093335af1506108ab61131a565b505f610889565b6108d591925060203d6020116108dc575b6108cd818361040b565b8101906111ad565b905f610882565b503d6108c3565b60046040517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b7fffffffff0000000000000000000000000000000000000000000000000000000081160361013257565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610132577fffffffff000000000000000000000000000000000000000000000000000000006004356109e48161095e565b165f526002602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b610a1a3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610bf9575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610ab2577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610a8a9184611869565b6040805191825273ffffffffffffffffffffffffffffffffffffffff929092166020820152a1005b60028403610aec577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae79184611746565b610a8a565b60038403610b21577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846115d2565b60048403610b56577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846114ef565b6040517f41c38b3000000000000000000000000000000000000000000000000000000000815260048101859052602490fd5b6040517fb927fe5e00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff84166004820152602490fd5b60046040517fa2e5c09a000000000000000000000000000000000000000000000000000000008152fd5b5030331415610a35565b610c0c3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610d18575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610c795760046040517fa23dd761000000000000000000000000000000000000000000000000000000008152fd5b60028403610cae577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610a8a9184611c1c565b60038403610ce3577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae79184611abc565b60048403610b56577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae791846119f5565b5030331415610c27565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206105f7600435611349565b9060407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc83011261013257600435916024359067ffffffffffffffff8211610132576105d791600401610143565b610db536610d5e565b91335f526001926020926001845260ff60405f20541615610e5657610dd992611dce565b9160405191808301818452845180915260408401918060408360051b8701019601925f905b838210610e0b5786880387f35b90919293948380610e458a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08b869d0301865289516104a7565b999701959493919091019101610dfe565b60046040517fc51267d8000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206040516f71727de22e5e9d8baf0edac6f37da0328152f35b610ed136610d5e565b906f71727de22e5e9d8baf0edac6f37da03233141580610efb575b610bcf57610ef992611dce565b005b5030331415610eec565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602060043560018114908115610f74575b8115610f69575b8115610f5e575b506040519015158152f35b60049150145f610f53565b600381149150610f4c565b600281149150610f45565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b9160206104fb938181520191610f7f565b6040513d5f823e3d90fd5b906004116101325790600490565b909291928360041161013257831161013257600401917ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0190565b7fffffffff00000000000000000000000000000000000000000000000000000000903581811693926004811061105757505050565b60040360031b82901b16169150565b90929190600181036110b1575050506110935f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff90811691161490565b600281036110ee575050506110e76104fb9173ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b5460ff1690565b6003810361116a57506004821015611107575050505f90565b61112061111a6110939361115093610fd9565b90611022565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b5473ffffffffffffffffffffffffffffffffffffffff1690565b9050600491501461117a57505f90565b60035473ffffffffffffffffffffffffffffffffffffffff16611093565b9081602091031261013257516104fb8161095e565b90816020910312610132575190565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18236030181121561013257016020813591019167ffffffffffffffff821161013257813603831361013257565b929190611315611276602092604087526112466040880161122c83610136565b73ffffffffffffffffffffffffffffffffffffffff169052565b83810135606088015261130561125f60408301836111bc565b9390610120948560808c01526101608b0191610f7f565b916112fc6112bd61128a60608401846111bc565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc096918d60a08982860301910152610f7f565b608083013560c08c015260a083013560e08c01528a6101009660c0850135888301526112ec60e08601866111bc565b9290918882860301910152610f7f565b938101906111bc565b9188840301610140890152610f7f565b930152565b3d15611344573d9061132b8261044c565b91611339604051938461040b565b82523d5f602084013e565b606090565b61135d818060081b918160301b9160501b90565b50507fff0000000000000000000000000000000000000000000000000000000000000080921680159081156113fe575b81156113d4575b5061139f5750505f90565b1680159081156113ad575090565b7f010000000000000000000000000000000000000000000000000000000000000091501490565b7ffe000000000000000000000000000000000000000000000000000000000000009150145f611394565b7f01000000000000000000000000000000000000000000000000000000000000008114915061138d565b7fffffffff000000000000000000000000000000000000000000000000000000005f35165f52600260205273ffffffffffffffffffffffffffffffffffffffff60405f2054168015611491575f8091368280378136915af43d5f803e1561148d573d5ff35b3d5ffd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f4e6f2066616c6c6261636b2068616e646c6572000000000000000000000000006044820152fd5b929160035473ffffffffffffffffffffffffffffffffffffffff9485821661159f57947fffffffffffffffffffffffff00000000000000000000000000000000000000009495169384911617600355823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b03925af18015610349576115925750565b8061033d610141926103d6565b60249086604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b90916115ec90806115e661111a8287610fd9565b94610fe7565b92909173ffffffffffffffffffffffffffffffffffffffff918261163d611150837fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b166116ff578161167a6116ba927fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055565b1691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f5c426a4200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602490fd5b60ff6117708273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541661182257806117b473ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008254161790551691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b916118885f5473ffffffffffffffffffffffffffffffffffffffff1690565b9273ffffffffffffffffffffffffffffffffffffffff8082169416908482146119b05781611938575b6118f5915073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b813b15610132575f60405180937f8a91b0e300000000000000000000000000000000000000000000000000000000825281838161198360048201604090602081525f60208201520190565b03925af1918215610349576118f59261199d575b506118b1565b8061033d6119aa926103d6565b5f611997565b5050823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b919060035473ffffffffffffffffffffffffffffffffffffffff8094168094821603611a8b577fffffffffffffffffffffffff000000000000000000000000000000000000000016600355823b1561013257611581925f92836040518096819582947f8a91b0e3000000000000000000000000000000000000000000000000000000008452602060048501526024840191610f7f565b602484604051907f026d96390000000000000000000000000000000000000000000000000000000082526004820152fd5b91611ad59080611acf61111a8286610fd9565b93610fe7565b9091611b0e611150827fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b73ffffffffffffffffffffffffffffffffffffffff808616959116859003611bd35750611b68611b90917fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b7fffffffffffffffffffffffff00000000000000000000000000000000000000008154169055565b823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f026d963900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602490fd5b60ff611c468273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541615611cf65780611c8b73ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0081541690551691823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f026d9639000000000000000000000000000000000000000000000000000000008352166004820152fd5b6020818303126101325780519067ffffffffffffffff8211610132570181601f82011215610132578051611d708161044c565b92611d7e604051948561040b565b81845260208284010111610132576104fb9160208085019101610486565b6104fb949273ffffffffffffffffffffffffffffffffffffffff60609316825260208201528160408201520191610f7f565b90929160609073ffffffffffffffffffffffffffffffffffffffff9485611e0a60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e8a575b50611e1c9293611f06565b92611e3c60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e46575050565b803b15610132576115815f929183926040519485809481937f173bf7da000000000000000000000000000000000000000000000000000000008352600483016104ea565b92505f60405180947fd68f6025000000000000000000000000000000000000000000000000000000008252818381611ec88888343360048601611d9c565b03925af1801561034957611e1c935f91611ee4575b5092611e11565b611f0091503d805f833e611ef8818361040b565b810190611d3d565b5f611edd565b600881901b91907fff00000000000000000000000000000000000000000000000000000000000000811680611f41575050916104fb926122de565b7f01000000000000000000000000000000000000000000000000000000000000008103611f745750506104fb92506121e2565b919250907ffe00000000000000000000000000000000000000000000000000000000000000036120165750611fab5f92839261235c565b9094929150611fb861205f565b94816040519283928337810184815203915afa611fd361131a565b82511561201157602083015215611fe75790565b60046040517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b612106565b602490604051907fba70d2310000000000000000000000000000000000000000000000000000000082526004820152fd5b67ffffffffffffffff81116103ea5760051b60200190565b60405161206b816103ef565b60018152805f5b60208082101561208d57906060602092828501015201612072565b50505090565b9061209d82612047565b6120aa604051918261040b565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06120d88294612047565b01905f5b8281106120e857505050565b8060606020809385010152016120dc565b908092918237015f815290565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b80518210156120115760209160051b010190565b91908110156120115760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa181360301821215610132570190565b356104fb81610114565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610132570180359067ffffffffffffffff82116101325760200191813603831361013257565b91909180350191602092602081019035916121fc83612093565b945f5b84811061220d575050505050565b61222061221b828787612147565b612187565b5f808461222e858a8a612147565b01359261223c858a8a612147565b9361224c60409586810190612191565b919061225c8751809481936120f9565b03925af161226861131a565b612272848b612133565b527fff0000000000000000000000000000000000000000000000000000000000000085161590816122d5575b506122ac57506001016121ff565b600490517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b9050155f61229e565b6122ec5f949392859261235c565b909692916122f861205f565b97826040519384928337810185815203925af19061231461131a565b90845115612011577fff0000000000000000000000000000000000000000000000000000000000000091602086015216159081612353575b50611fe757565b9050155f61234c565b908060141161013257813560601c9281603411610132577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffcc60346014850135940192019056fea2646970667358221220ea0107d2fc265c63c0a8bd55e9d1937ca16a9aea1ea523588fa7e4f3af23626764736f6c63430008170033000000000000000000000000000000b07799b322d076669ef32b247d02279c7e0000000000000000000000000000", + "nonce": "0x8f", + "chainId": "0xaa36a7" + }, + "additionalContracts": [ + { + "transactionType": "CREATE2", + "contractName": "AuraAccountFactory", + "address": "0x0000004b2941659deb7472b46f7b84caf27dce44", + "initCode": "0x60e034610117576001600160401b0390601f612d5238819003918201601f1916830191848311848410176100f1578084926020946040528339810103126101175751906001600160a01b03908183168084036101175715610105576040519061246690818301908111838210176100f15782916108ec833903905ff080156100e6571660805260a0526d6396ff2a80c067f99b3d2ab4df2460c0526040516107d0908161011c823960805181818161028a0152818161051b0152610739015260a05181818161021e0152610451015260c051818181609e0152818161058001526106b60152f35b6040513d5f823e3d90fd5b634e487b7160e01b5f52604160045260245ffd5b604051631a0a9b9f60e21b8152600490fd5b5f80fdfe60806040908082526004361015610014575f80fd5b5f3560e01c90816311464fbe14610242575080633a5381b5146101d45780633a8fcd9a146101295780638cb84e18146100c65763f5382f0314610055575f80fd5b346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5f80fd5b50346100c257807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c25760209073ffffffffffffffffffffffffffffffffffffffff6101216101186102ae565b60243590610663565b915191168152f35b50346100c25760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576101616102ae565b67ffffffffffffffff91906024358381116100c2576101849036906004016102d1565b9390916044359473ffffffffffffffffffffffffffffffffffffffff9384871687036100c2576064359384116100c2576020966101c86101219536906004016102d1565b939092608435956103d7565b50346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c25760209073ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b6004359073ffffffffffffffffffffffffffffffffffffffff821682036100c257565b9181601f840112156100c25782359167ffffffffffffffff83116100c257602083818601950101116100c257565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761037e57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b908160209103126100c2575173ffffffffffffffffffffffffffffffffffffffff811681036100c25790565b969593949092916103e88589610663565b803b610659575094610487939195610400868a610705565b916104bd73ffffffffffffffffffffffffffffffffffffffff95604051998a938860209a8b997f0d1bf5d3000000000000000000000000000000000000000000000000000000008b89015260a498837f00000000000000000000000000000000000000000000000000000000000000001660248a0152608060448a015260a48901916102ff565b931660648601527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc8584030160848601526102ff565b03906104ef7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0928381018a528961033d565b6040519788937fa97b90d5000000000000000000000000000000000000000000000000000000008552867f00000000000000000000000000000000000000000000000000000000000000001660048601525f60248601526044850152608060648501528051918260848601525f5b8381106106425750505060a491601f825f85879586010152011681010301815f857f0000000000000000000000000000000000000000000000000000000000000000165af1938415610637575f946105e4575b5090817f33310a89c32d8cc00057ad6ef6274d2f8fe22389a992cf89983e09fc84f6cfff92859760405195865216941692a3565b82919450610628907f33310a89c32d8cc00057ad6ef6274d2f8fe22389a992cf89983e09fc84f6cfff933d8411610630575b610620818361033d565b8101906103ab565b9390916105b0565b503d610616565b6040513d5f823e3d90fd5b8281018701518b820183015288968b96500161055d565b9750505050505050565b9061066d91610705565b604051907f5414dff0000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115610637575f916106e9575090565b610702915060203d60201161063057610620818361033d565b90565b6040519160208301917fffffffffffffffffffffffffffffffffffffffff000000000000000000000000809160601b1683527f000000000000000000000000000000000000000000000000000000000000000060601b166034840152604883015260488252608082019180831067ffffffffffffffff84111761037e576bffffffffffffffffffffffff92604052519020169056fea264697066735822122007e02d6bc6a2534a3e464491617058f3405dc92cdb96be09e8deece4731ec84464736f6c63430008170033608080604052346100895763409feecd198054906001821661007c576001600160401b039160011c6002600160401b031901610042575b6123d8838161008e8239f35b6002600160411b03905560209081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29080a15f80610036565b63f92ee8a95f526004601cfd5b5f80fdfe60806040526004361015610015575b3661142857005b5f3560e01c80630d1bf5d31461010f57806310b0a63d146100e7578063112d3a7d1461010a5780631195e07e146101055780631626ba7e1461010057806319822f7c146100fb57806346d09cea146100f65780637833fa04146100f15780639517e29f146100ec5780639cfd7cff146100e7578063a71763a8146100e2578063d03c7914146100dd578063d691c964146100d8578063e8eb3cc6146100d3578063e9ae5c53146100ce5763f2dc691d0361000e57610f05565b610ec8565b610e80565b610dac565b610d22565b610c03565b6104fe565b610a11565b610988565b61090d565b610781565b610651565b610601565b6105db565b610171565b73ffffffffffffffffffffffffffffffffffffffff81160361013257565b5f80fd5b359061014182610114565b565b9181601f840112156101325782359167ffffffffffffffff8311610132576020838186019501011161013257565b346101325760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760048035906101ae82610114565b67ffffffffffffffff602435818111610132576101ce9036908401610143565b939092604435926101de84610114565b606435908111610132576101f59036908301610143565b9290917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf6011329687548060038a55610377575b5073ffffffffffffffffffffffffffffffffffffffff9283811690811561034e5761028a9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b803b15610132576102cc975f80946040519a8b95869485937f6d61fe700000000000000000000000000000000000000000000000000000000085528401610fbd565b03925af1948515610349578695610330575b50831661031f575b5050506102ef57005b6002905560016020527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602080a1005b610328926114ef565b5f80806102e6565b8061033d610343926103d6565b8061039f565b5f6102de565b610fce565b836040517f682a6e7c000000000000000000000000000000000000000000000000000000008152fd5b600181819a939a1c14303b10156103935760ff1b1b965f610226565b8263f92ee8a95f52601cfd5b5f91031261013257565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff81116103ea57604052565b6103a9565b6040810190811067ffffffffffffffff8211176103ea57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176103ea57604052565b67ffffffffffffffff81116103ea57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b5f5b8381106104975750505f910152565b8181015183820152602001610488565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f6020936104e381518092818752878088019101610486565b0116010190565b9060206104fb9281815201906104a7565b90565b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325761057960405161053b816103ef565b601281527f657468617572612e617572612e302e312e30000000000000000000000000000060208201526040519182916020835260208301906104a7565b0390f35b60607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc82011261013257600435916024356105b781610114565b916044359067ffffffffffffffff8211610132576105d791600401610143565b9091565b346101325760206105f76105ee3661057d565b92919091611066565b6040519015158152f35b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff5f5416604051908152f35b346101325760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760243567ffffffffffffffff81116101325760206106a5610709923690600401610143565b73ffffffffffffffffffffffffffffffffffffffff5f5416906040518095819482937ff551e2ee0000000000000000000000000000000000000000000000000000000084523360048501526004356024850152606060448501526064840191610f7f565b03915afa801561034957610579915f91610752575b506040517fffffffff0000000000000000000000000000000000000000000000000000000090911681529081906020820190565b610774915060203d60201161077a575b61076c818361040b565b810190611198565b5f61071e565b503d610762565b34610132577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc606081360112610132576004359067ffffffffffffffff821161013257610120908236030112610132576044356f71727de22e5e9d8baf0edac6f37da03233036108e357602061086c5f9361082f610816610816875473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b906040519586809481937f97003203000000000000000000000000000000000000000000000000000000008352602435906004016004840161120c565b03925af190811561034957610579925f926108b2575b508061089a575b506040519081529081906020820190565b5f80808093335af1506108ab61131a565b505f610889565b6108d591925060203d6020116108dc575b6108cd818361040b565b8101906111ad565b905f610882565b503d6108c3565b60046040517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b7fffffffff0000000000000000000000000000000000000000000000000000000081160361013257565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610132577fffffffff000000000000000000000000000000000000000000000000000000006004356109e48161095e565b165f526002602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b610a1a3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610bf9575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610ab2577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610a8a9184611869565b6040805191825273ffffffffffffffffffffffffffffffffffffffff929092166020820152a1005b60028403610aec577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae79184611746565b610a8a565b60038403610b21577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846115d2565b60048403610b56577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846114ef565b6040517f41c38b3000000000000000000000000000000000000000000000000000000000815260048101859052602490fd5b6040517fb927fe5e00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff84166004820152602490fd5b60046040517fa2e5c09a000000000000000000000000000000000000000000000000000000008152fd5b5030331415610a35565b610c0c3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610d18575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610c795760046040517fa23dd761000000000000000000000000000000000000000000000000000000008152fd5b60028403610cae577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610a8a9184611c1c565b60038403610ce3577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae79184611abc565b60048403610b56577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae791846119f5565b5030331415610c27565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206105f7600435611349565b9060407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc83011261013257600435916024359067ffffffffffffffff8211610132576105d791600401610143565b610db536610d5e565b91335f526001926020926001845260ff60405f20541615610e5657610dd992611dce565b9160405191808301818452845180915260408401918060408360051b8701019601925f905b838210610e0b5786880387f35b90919293948380610e458a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08b869d0301865289516104a7565b999701959493919091019101610dfe565b60046040517fc51267d8000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206040516f71727de22e5e9d8baf0edac6f37da0328152f35b610ed136610d5e565b906f71727de22e5e9d8baf0edac6f37da03233141580610efb575b610bcf57610ef992611dce565b005b5030331415610eec565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602060043560018114908115610f74575b8115610f69575b8115610f5e575b506040519015158152f35b60049150145f610f53565b600381149150610f4c565b600281149150610f45565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b9160206104fb938181520191610f7f565b6040513d5f823e3d90fd5b906004116101325790600490565b909291928360041161013257831161013257600401917ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0190565b7fffffffff00000000000000000000000000000000000000000000000000000000903581811693926004811061105757505050565b60040360031b82901b16169150565b90929190600181036110b1575050506110935f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff90811691161490565b600281036110ee575050506110e76104fb9173ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b5460ff1690565b6003810361116a57506004821015611107575050505f90565b61112061111a6110939361115093610fd9565b90611022565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b5473ffffffffffffffffffffffffffffffffffffffff1690565b9050600491501461117a57505f90565b60035473ffffffffffffffffffffffffffffffffffffffff16611093565b9081602091031261013257516104fb8161095e565b90816020910312610132575190565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18236030181121561013257016020813591019167ffffffffffffffff821161013257813603831361013257565b929190611315611276602092604087526112466040880161122c83610136565b73ffffffffffffffffffffffffffffffffffffffff169052565b83810135606088015261130561125f60408301836111bc565b9390610120948560808c01526101608b0191610f7f565b916112fc6112bd61128a60608401846111bc565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc096918d60a08982860301910152610f7f565b608083013560c08c015260a083013560e08c01528a6101009660c0850135888301526112ec60e08601866111bc565b9290918882860301910152610f7f565b938101906111bc565b9188840301610140890152610f7f565b930152565b3d15611344573d9061132b8261044c565b91611339604051938461040b565b82523d5f602084013e565b606090565b61135d818060081b918160301b9160501b90565b50507fff0000000000000000000000000000000000000000000000000000000000000080921680159081156113fe575b81156113d4575b5061139f5750505f90565b1680159081156113ad575090565b7f010000000000000000000000000000000000000000000000000000000000000091501490565b7ffe000000000000000000000000000000000000000000000000000000000000009150145f611394565b7f01000000000000000000000000000000000000000000000000000000000000008114915061138d565b7fffffffff000000000000000000000000000000000000000000000000000000005f35165f52600260205273ffffffffffffffffffffffffffffffffffffffff60405f2054168015611491575f8091368280378136915af43d5f803e1561148d573d5ff35b3d5ffd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f4e6f2066616c6c6261636b2068616e646c6572000000000000000000000000006044820152fd5b929160035473ffffffffffffffffffffffffffffffffffffffff9485821661159f57947fffffffffffffffffffffffff00000000000000000000000000000000000000009495169384911617600355823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b03925af18015610349576115925750565b8061033d610141926103d6565b60249086604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b90916115ec90806115e661111a8287610fd9565b94610fe7565b92909173ffffffffffffffffffffffffffffffffffffffff918261163d611150837fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b166116ff578161167a6116ba927fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055565b1691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f5c426a4200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602490fd5b60ff6117708273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541661182257806117b473ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008254161790551691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b916118885f5473ffffffffffffffffffffffffffffffffffffffff1690565b9273ffffffffffffffffffffffffffffffffffffffff8082169416908482146119b05781611938575b6118f5915073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b813b15610132575f60405180937f8a91b0e300000000000000000000000000000000000000000000000000000000825281838161198360048201604090602081525f60208201520190565b03925af1918215610349576118f59261199d575b506118b1565b8061033d6119aa926103d6565b5f611997565b5050823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b919060035473ffffffffffffffffffffffffffffffffffffffff8094168094821603611a8b577fffffffffffffffffffffffff000000000000000000000000000000000000000016600355823b1561013257611581925f92836040518096819582947f8a91b0e3000000000000000000000000000000000000000000000000000000008452602060048501526024840191610f7f565b602484604051907f026d96390000000000000000000000000000000000000000000000000000000082526004820152fd5b91611ad59080611acf61111a8286610fd9565b93610fe7565b9091611b0e611150827fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b73ffffffffffffffffffffffffffffffffffffffff808616959116859003611bd35750611b68611b90917fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b7fffffffffffffffffffffffff00000000000000000000000000000000000000008154169055565b823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f026d963900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602490fd5b60ff611c468273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541615611cf65780611c8b73ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0081541690551691823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f026d9639000000000000000000000000000000000000000000000000000000008352166004820152fd5b6020818303126101325780519067ffffffffffffffff8211610132570181601f82011215610132578051611d708161044c565b92611d7e604051948561040b565b81845260208284010111610132576104fb9160208085019101610486565b6104fb949273ffffffffffffffffffffffffffffffffffffffff60609316825260208201528160408201520191610f7f565b90929160609073ffffffffffffffffffffffffffffffffffffffff9485611e0a60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e8a575b50611e1c9293611f06565b92611e3c60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e46575050565b803b15610132576115815f929183926040519485809481937f173bf7da000000000000000000000000000000000000000000000000000000008352600483016104ea565b92505f60405180947fd68f6025000000000000000000000000000000000000000000000000000000008252818381611ec88888343360048601611d9c565b03925af1801561034957611e1c935f91611ee4575b5092611e11565b611f0091503d805f833e611ef8818361040b565b810190611d3d565b5f611edd565b600881901b91907fff00000000000000000000000000000000000000000000000000000000000000811680611f41575050916104fb926122de565b7f01000000000000000000000000000000000000000000000000000000000000008103611f745750506104fb92506121e2565b919250907ffe00000000000000000000000000000000000000000000000000000000000000036120165750611fab5f92839261235c565b9094929150611fb861205f565b94816040519283928337810184815203915afa611fd361131a565b82511561201157602083015215611fe75790565b60046040517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b612106565b602490604051907fba70d2310000000000000000000000000000000000000000000000000000000082526004820152fd5b67ffffffffffffffff81116103ea5760051b60200190565b60405161206b816103ef565b60018152805f5b60208082101561208d57906060602092828501015201612072565b50505090565b9061209d82612047565b6120aa604051918261040b565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06120d88294612047565b01905f5b8281106120e857505050565b8060606020809385010152016120dc565b908092918237015f815290565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b80518210156120115760209160051b010190565b91908110156120115760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa181360301821215610132570190565b356104fb81610114565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610132570180359067ffffffffffffffff82116101325760200191813603831361013257565b91909180350191602092602081019035916121fc83612093565b945f5b84811061220d575050505050565b61222061221b828787612147565b612187565b5f808461222e858a8a612147565b01359261223c858a8a612147565b9361224c60409586810190612191565b919061225c8751809481936120f9565b03925af161226861131a565b612272848b612133565b527fff0000000000000000000000000000000000000000000000000000000000000085161590816122d5575b506122ac57506001016121ff565b600490517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b9050155f61229e565b6122ec5f949392859261235c565b909692916122f861205f565b97826040519384928337810185815203925af19061231461131a565b90845115612011577fff0000000000000000000000000000000000000000000000000000000000000091602086015216159081612353575b50611fe757565b9050155f61234c565b908060141161013257813560601c9281603411610132577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffcc60346014850135940192019056fea2646970667358221220ea0107d2fc265c63c0a8bd55e9d1937ca16a9aea1ea523588fa7e4f3af23626764736f6c63430008170033000000000000000000000000000000b07799b322d076669ef32b247d02279c7e" + }, + { + "transactionType": "CREATE", + "contractName": "AuraAccount", + "address": "0x565cf8324c9d856a540b3873701242bf27d82307", + "initCode": "0x608080604052346100895763409feecd198054906001821661007c576001600160401b039160011c6002600160401b031901610042575b6123d8838161008e8239f35b6002600160411b03905560209081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29080a15f80610036565b63f92ee8a95f526004601cfd5b5f80fdfe60806040526004361015610015575b3661142857005b5f3560e01c80630d1bf5d31461010f57806310b0a63d146100e7578063112d3a7d1461010a5780631195e07e146101055780631626ba7e1461010057806319822f7c146100fb57806346d09cea146100f65780637833fa04146100f15780639517e29f146100ec5780639cfd7cff146100e7578063a71763a8146100e2578063d03c7914146100dd578063d691c964146100d8578063e8eb3cc6146100d3578063e9ae5c53146100ce5763f2dc691d0361000e57610f05565b610ec8565b610e80565b610dac565b610d22565b610c03565b6104fe565b610a11565b610988565b61090d565b610781565b610651565b610601565b6105db565b610171565b73ffffffffffffffffffffffffffffffffffffffff81160361013257565b5f80fd5b359061014182610114565b565b9181601f840112156101325782359167ffffffffffffffff8311610132576020838186019501011161013257565b346101325760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760048035906101ae82610114565b67ffffffffffffffff602435818111610132576101ce9036908401610143565b939092604435926101de84610114565b606435908111610132576101f59036908301610143565b9290917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf6011329687548060038a55610377575b5073ffffffffffffffffffffffffffffffffffffffff9283811690811561034e5761028a9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b803b15610132576102cc975f80946040519a8b95869485937f6d61fe700000000000000000000000000000000000000000000000000000000085528401610fbd565b03925af1948515610349578695610330575b50831661031f575b5050506102ef57005b6002905560016020527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602080a1005b610328926114ef565b5f80806102e6565b8061033d610343926103d6565b8061039f565b5f6102de565b610fce565b836040517f682a6e7c000000000000000000000000000000000000000000000000000000008152fd5b600181819a939a1c14303b10156103935760ff1b1b965f610226565b8263f92ee8a95f52601cfd5b5f91031261013257565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff81116103ea57604052565b6103a9565b6040810190811067ffffffffffffffff8211176103ea57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176103ea57604052565b67ffffffffffffffff81116103ea57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b5f5b8381106104975750505f910152565b8181015183820152602001610488565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f6020936104e381518092818752878088019101610486565b0116010190565b9060206104fb9281815201906104a7565b90565b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325761057960405161053b816103ef565b601281527f657468617572612e617572612e302e312e30000000000000000000000000000060208201526040519182916020835260208301906104a7565b0390f35b60607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc82011261013257600435916024356105b781610114565b916044359067ffffffffffffffff8211610132576105d791600401610143565b9091565b346101325760206105f76105ee3661057d565b92919091611066565b6040519015158152f35b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff5f5416604051908152f35b346101325760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760243567ffffffffffffffff81116101325760206106a5610709923690600401610143565b73ffffffffffffffffffffffffffffffffffffffff5f5416906040518095819482937ff551e2ee0000000000000000000000000000000000000000000000000000000084523360048501526004356024850152606060448501526064840191610f7f565b03915afa801561034957610579915f91610752575b506040517fffffffff0000000000000000000000000000000000000000000000000000000090911681529081906020820190565b610774915060203d60201161077a575b61076c818361040b565b810190611198565b5f61071e565b503d610762565b34610132577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc606081360112610132576004359067ffffffffffffffff821161013257610120908236030112610132576044356f71727de22e5e9d8baf0edac6f37da03233036108e357602061086c5f9361082f610816610816875473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b906040519586809481937f97003203000000000000000000000000000000000000000000000000000000008352602435906004016004840161120c565b03925af190811561034957610579925f926108b2575b508061089a575b506040519081529081906020820190565b5f80808093335af1506108ab61131a565b505f610889565b6108d591925060203d6020116108dc575b6108cd818361040b565b8101906111ad565b905f610882565b503d6108c3565b60046040517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b7fffffffff0000000000000000000000000000000000000000000000000000000081160361013257565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610132577fffffffff000000000000000000000000000000000000000000000000000000006004356109e48161095e565b165f526002602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b610a1a3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610bf9575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610ab2577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610a8a9184611869565b6040805191825273ffffffffffffffffffffffffffffffffffffffff929092166020820152a1005b60028403610aec577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae79184611746565b610a8a565b60038403610b21577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846115d2565b60048403610b56577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846114ef565b6040517f41c38b3000000000000000000000000000000000000000000000000000000000815260048101859052602490fd5b6040517fb927fe5e00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff84166004820152602490fd5b60046040517fa2e5c09a000000000000000000000000000000000000000000000000000000008152fd5b5030331415610a35565b610c0c3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610d18575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610c795760046040517fa23dd761000000000000000000000000000000000000000000000000000000008152fd5b60028403610cae577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610a8a9184611c1c565b60038403610ce3577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae79184611abc565b60048403610b56577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae791846119f5565b5030331415610c27565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206105f7600435611349565b9060407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc83011261013257600435916024359067ffffffffffffffff8211610132576105d791600401610143565b610db536610d5e565b91335f526001926020926001845260ff60405f20541615610e5657610dd992611dce565b9160405191808301818452845180915260408401918060408360051b8701019601925f905b838210610e0b5786880387f35b90919293948380610e458a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08b869d0301865289516104a7565b999701959493919091019101610dfe565b60046040517fc51267d8000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206040516f71727de22e5e9d8baf0edac6f37da0328152f35b610ed136610d5e565b906f71727de22e5e9d8baf0edac6f37da03233141580610efb575b610bcf57610ef992611dce565b005b5030331415610eec565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602060043560018114908115610f74575b8115610f69575b8115610f5e575b506040519015158152f35b60049150145f610f53565b600381149150610f4c565b600281149150610f45565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b9160206104fb938181520191610f7f565b6040513d5f823e3d90fd5b906004116101325790600490565b909291928360041161013257831161013257600401917ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0190565b7fffffffff00000000000000000000000000000000000000000000000000000000903581811693926004811061105757505050565b60040360031b82901b16169150565b90929190600181036110b1575050506110935f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff90811691161490565b600281036110ee575050506110e76104fb9173ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b5460ff1690565b6003810361116a57506004821015611107575050505f90565b61112061111a6110939361115093610fd9565b90611022565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b5473ffffffffffffffffffffffffffffffffffffffff1690565b9050600491501461117a57505f90565b60035473ffffffffffffffffffffffffffffffffffffffff16611093565b9081602091031261013257516104fb8161095e565b90816020910312610132575190565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18236030181121561013257016020813591019167ffffffffffffffff821161013257813603831361013257565b929190611315611276602092604087526112466040880161122c83610136565b73ffffffffffffffffffffffffffffffffffffffff169052565b83810135606088015261130561125f60408301836111bc565b9390610120948560808c01526101608b0191610f7f565b916112fc6112bd61128a60608401846111bc565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc096918d60a08982860301910152610f7f565b608083013560c08c015260a083013560e08c01528a6101009660c0850135888301526112ec60e08601866111bc565b9290918882860301910152610f7f565b938101906111bc565b9188840301610140890152610f7f565b930152565b3d15611344573d9061132b8261044c565b91611339604051938461040b565b82523d5f602084013e565b606090565b61135d818060081b918160301b9160501b90565b50507fff0000000000000000000000000000000000000000000000000000000000000080921680159081156113fe575b81156113d4575b5061139f5750505f90565b1680159081156113ad575090565b7f010000000000000000000000000000000000000000000000000000000000000091501490565b7ffe000000000000000000000000000000000000000000000000000000000000009150145f611394565b7f01000000000000000000000000000000000000000000000000000000000000008114915061138d565b7fffffffff000000000000000000000000000000000000000000000000000000005f35165f52600260205273ffffffffffffffffffffffffffffffffffffffff60405f2054168015611491575f8091368280378136915af43d5f803e1561148d573d5ff35b3d5ffd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f4e6f2066616c6c6261636b2068616e646c6572000000000000000000000000006044820152fd5b929160035473ffffffffffffffffffffffffffffffffffffffff9485821661159f57947fffffffffffffffffffffffff00000000000000000000000000000000000000009495169384911617600355823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b03925af18015610349576115925750565b8061033d610141926103d6565b60249086604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b90916115ec90806115e661111a8287610fd9565b94610fe7565b92909173ffffffffffffffffffffffffffffffffffffffff918261163d611150837fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b166116ff578161167a6116ba927fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055565b1691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f5c426a4200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602490fd5b60ff6117708273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541661182257806117b473ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008254161790551691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b916118885f5473ffffffffffffffffffffffffffffffffffffffff1690565b9273ffffffffffffffffffffffffffffffffffffffff8082169416908482146119b05781611938575b6118f5915073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b813b15610132575f60405180937f8a91b0e300000000000000000000000000000000000000000000000000000000825281838161198360048201604090602081525f60208201520190565b03925af1918215610349576118f59261199d575b506118b1565b8061033d6119aa926103d6565b5f611997565b5050823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b919060035473ffffffffffffffffffffffffffffffffffffffff8094168094821603611a8b577fffffffffffffffffffffffff000000000000000000000000000000000000000016600355823b1561013257611581925f92836040518096819582947f8a91b0e3000000000000000000000000000000000000000000000000000000008452602060048501526024840191610f7f565b602484604051907f026d96390000000000000000000000000000000000000000000000000000000082526004820152fd5b91611ad59080611acf61111a8286610fd9565b93610fe7565b9091611b0e611150827fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b73ffffffffffffffffffffffffffffffffffffffff808616959116859003611bd35750611b68611b90917fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b7fffffffffffffffffffffffff00000000000000000000000000000000000000008154169055565b823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f026d963900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602490fd5b60ff611c468273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541615611cf65780611c8b73ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0081541690551691823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f026d9639000000000000000000000000000000000000000000000000000000008352166004820152fd5b6020818303126101325780519067ffffffffffffffff8211610132570181601f82011215610132578051611d708161044c565b92611d7e604051948561040b565b81845260208284010111610132576104fb9160208085019101610486565b6104fb949273ffffffffffffffffffffffffffffffffffffffff60609316825260208201528160408201520191610f7f565b90929160609073ffffffffffffffffffffffffffffffffffffffff9485611e0a60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e8a575b50611e1c9293611f06565b92611e3c60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e46575050565b803b15610132576115815f929183926040519485809481937f173bf7da000000000000000000000000000000000000000000000000000000008352600483016104ea565b92505f60405180947fd68f6025000000000000000000000000000000000000000000000000000000008252818381611ec88888343360048601611d9c565b03925af1801561034957611e1c935f91611ee4575b5092611e11565b611f0091503d805f833e611ef8818361040b565b810190611d3d565b5f611edd565b600881901b91907fff00000000000000000000000000000000000000000000000000000000000000811680611f41575050916104fb926122de565b7f01000000000000000000000000000000000000000000000000000000000000008103611f745750506104fb92506121e2565b919250907ffe00000000000000000000000000000000000000000000000000000000000000036120165750611fab5f92839261235c565b9094929150611fb861205f565b94816040519283928337810184815203915afa611fd361131a565b82511561201157602083015215611fe75790565b60046040517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b612106565b602490604051907fba70d2310000000000000000000000000000000000000000000000000000000082526004820152fd5b67ffffffffffffffff81116103ea5760051b60200190565b60405161206b816103ef565b60018152805f5b60208082101561208d57906060602092828501015201612072565b50505090565b9061209d82612047565b6120aa604051918261040b565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06120d88294612047565b01905f5b8281106120e857505050565b8060606020809385010152016120dc565b908092918237015f815290565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b80518210156120115760209160051b010190565b91908110156120115760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa181360301821215610132570190565b356104fb81610114565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610132570180359067ffffffffffffffff82116101325760200191813603831361013257565b91909180350191602092602081019035916121fc83612093565b945f5b84811061220d575050505050565b61222061221b828787612147565b612187565b5f808461222e858a8a612147565b01359261223c858a8a612147565b9361224c60409586810190612191565b919061225c8751809481936120f9565b03925af161226861131a565b612272848b612133565b527fff0000000000000000000000000000000000000000000000000000000000000085161590816122d5575b506122ac57506001016121ff565b600490517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b9050155f61229e565b6122ec5f949392859261235c565b909692916122f861205f565b97826040519384928337810185815203925af19061231461131a565b90845115612011577fff0000000000000000000000000000000000000000000000000000000000000091602086015216159081612353575b50611fe757565b9050155f61234c565b908060141161013257813560601c9281603411610132577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffcc60346014850135940192019056fea2646970667358221220ea0107d2fc265c63c0a8bd55e9d1937ca16a9aea1ea523588fa7e4f3af23626764736f6c63430008170033" + } + ], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "status": "0x1", + "cumulativeGasUsed": "0x24ea440", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0xd93f45dec98dc26096f4bcc3a502e5f340ec48ed7223d523d4bd40926202f033", + "transactionIndex": "0x58", + "blockHash": "0x1100e5b50f0c05a44c321b29aa9f43b9092f4a0550b8a5f1179300263fbcd491", + "blockNumber": "0x94e46f", + "gasUsed": "0x1fe58a", + "effectiveGasPrice": "0xf4249", + "from": "0x18ee4c040568238643c07e7afd6c53efc196d26b", + "to": "0x0000000000ffe8b47b3e2130213b802212439497", + "contractAddress": null + }, + { + "status": "0x1", + "cumulativeGasUsed": "0x27699c0", + "logs": [ + { + "address": "0x565cf8324c9d856a540b3873701242bf27d82307", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", + "blockHash": "0x1100e5b50f0c05a44c321b29aa9f43b9092f4a0550b8a5f1179300263fbcd491", + "blockNumber": "0x94e46f", + "blockTimestamp": "0x692fc13c", + "transactionHash": "0x572875cd08fec02682e4390e06e9c7fd30c7fca916d030f4e098b1f0d5fe7cf6", + "transactionIndex": "0x5a", + "logIndex": "0x434", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000080000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000800000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0x572875cd08fec02682e4390e06e9c7fd30c7fca916d030f4e098b1f0d5fe7cf6", + "transactionIndex": "0x5a", + "blockHash": "0x1100e5b50f0c05a44c321b29aa9f43b9092f4a0550b8a5f1179300263fbcd491", + "blockNumber": "0x94e46f", + "gasUsed": "0x27404a", + "effectiveGasPrice": "0xf4249", + "from": "0x18ee4c040568238643c07e7afd6c53efc196d26b", + "to": "0x0000000000ffe8b47b3e2130213b802212439497", + "contractAddress": null + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1764737340817, + "chain": 11155111, + "commit": "3f45cae" +} \ No newline at end of file diff --git a/broadcast/Deploy.s.sol/11155111/run-latest.json b/broadcast/Deploy.s.sol/11155111/run-latest.json index 58ea957..464f4c7 100644 --- a/broadcast/Deploy.s.sol/11155111/run-latest.json +++ b/broadcast/Deploy.s.sol/11155111/run-latest.json @@ -1,7 +1,7 @@ { "transactions": [ { - "hash": "0xf337387edbb47eeb3fce9ef2062237b8e966c1297eb203e1e8448b73389cc713", + "hash": "0xd93f45dec98dc26096f4bcc3a502e5f340ec48ed7223d523d4bd40926202f033", "transactionType": "CALL", "contractName": null, "contractAddress": "0x0000000000ffe8b47b3e2130213b802212439497", @@ -10,24 +10,50 @@ "transaction": { "from": "0x18ee4c040568238643c07e7afd6c53efc196d26b", "to": "0x0000000000ffe8b47b3e2130213b802212439497", - "gas": "0x59edf6", + "gas": "0x2ea619", "value": "0x0", - "input": "0x64e0308718ee4c040568238643c07e7afd6c53efc196d26b000000000000000dc8cf832f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004c2e60e034610103576001600160401b0390601f614c0e38819003918201601f1916830191848311848410176100ef5780849260209460405283398101031261010357516001600160a01b0381169081810361010357608052604051916141f790818401908111848210176100ef576020928492610a17843981520301905ff080156100e45760a0526d6396ff2a80c067f99b3d2ab4df2460c05260405161090f9081610108823960805181818161012201526103ad015260a05181818161033f015281816106580152610878015260c05181818160b0015281816106cc01526107e50152f35b6040513d5f823e3d90fd5b634e487b7160e01b5f52604160045260245ffd5b5f80fdfe6040608081526004361015610012575f80fd5b5f3560e01c806323771738146103635780633a4741bd146102f557806356ceaeda146102bf578063cfde1317146101af578063e1aa8cce14610146578063e8eb3cc6146100d85763f5382f0314610067575f80fd5b346100d4575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100d4576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5f80fd5b50346100d4575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100d4576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346100d45760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100d4576044359073ffffffffffffffffffffffffffffffffffffffff80831683036100d4576101a760209360643590610792565b915191168152f35b50346100d45773ffffffffffffffffffffffffffffffffffffffff906101d436610453565b93959496909285519660208801987f56ceaeda000000000000000000000000000000000000000000000000000000008a52602489015260448801521660648601526084850152151560a484015260c483015260c4825261010082019180831067ffffffffffffffff84111761029257603461027c9161028e958585523060601b61012083015261026c825180926101348501906104bd565b8101036014810185520183610521565b519182916020835260208301906104de565b0390f35b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b50346100d45760209073ffffffffffffffffffffffffffffffffffffffff6101a76102e936610453565b9493909392919261058e565b50346100d4575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100d4576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b506103706102e936610453565b34610395575b73ffffffffffffffffffffffffffffffffffffffff6020925191168152f35b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001690813b156100d4575f9060248551809481937fb760faf90000000000000000000000000000000000000000000000000000000083528716600483015234905af180156104495761041e575b50610376565b67ffffffffffffffff811161029257825273ffffffffffffffffffffffffffffffffffffffff610418565b83513d5f823e3d90fd5b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc60c09101126100d457600435906024359060443573ffffffffffffffffffffffffffffffffffffffff811681036100d457906064359060843580151581036100d4579060a43590565b5f5b8381106104ce5750505f910152565b81810151838201526020016104bf565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f60209361051a815180928187528780880191016104bd565b0116010190565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761029257604052565b908160209103126100d4575173ffffffffffffffffffffffffffffffffffffffff811681036100d45790565b9291959490939561059f8382610792565b803b61077357506105b08382610844565b9173ffffffffffffffffffffffffffffffffffffffff9081604051937f2cdeb30c00000000000000000000000000000000000000000000000000000000602086015287602486015288604486015216988960648501521515608484015260a483015260a4825260e082019282841067ffffffffffffffff85111761029257836040527fa97b90d5000000000000000000000000000000000000000000000000000000008452817f00000000000000000000000000000000000000000000000000000000000000001660e48401525f61010484015261012483015260806101448301526020837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff20846106c56101648201826104de565b0301815f857f0000000000000000000000000000000000000000000000000000000000000000165af18015610768577f569add262e905a4f49f55725922d01603540b7aabad95ebd8f07e00670bc845a936040935f92610733575b50501696879382519182526020820152a4565b610759925060e0906020903d602011610760575b6107518285610521565b010190610562565b5f80610720565b3d9150610747565b6040513d5f823e3d90fd5b73ffffffffffffffffffffffffffffffffffffffff1696505050505050565b9061079c91610844565b604051907f5414dff0000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115610768575f91610818575090565b61083a915060203d60201161083d575b6108328183610521565b810190610562565b90565b503d610828565b6040519160208301917fffffffffffffffffffffffffffffffffffffffff000000000000000000000000809160601b1683527f000000000000000000000000000000000000000000000000000000000000000060601b166034840152604883015260488252608082019180831067ffffffffffffffff841117610292576bffffffffffffffffffffffff92604052519020169056fea2646970667358221220759c8ed5754d32c5e74dd7bf101962b65816d374844b959bf1fb045a627161ed64736f6c6343000817003360a0346200017e576001600160401b0390601f620041f738819003918201601f19168301918483118484101762000182578084926020946040528339810103126200017e57516001600160a01b03919082811681036200017e57331562000166575f543360018060a01b03198216175f55604051933391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a360805263409feecd198054600181166200015957829060011c036200011e575b614060838162000197823960805181818161024501528181610407015281816105780152818161075f01528181610825015281816109b60152818161196901528181611a6a01528181611b9001528181611e28015281816120d10152612a560152f35b6002600160411b03905560209081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29080a15f80620000bb565b63f92ee8a95f526004601cfd5b604051631e4fbdf760e01b81525f6004820152602490fd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe608060409080825260049182361015610022575b5050503615610020575f80fd5b005b5f915f3560e01c908163011cfdbc14612a0e5750806304802c9a1461295c5780630633b14a146128f55780630665f04b146127fb5780630904b9ed1461275157806309b7026e1461271057806310de2676146125bd578063113647bb14612400578063136dd7551461238b5780631506ac5a146123505780631626ba7e146121b857806319822f7c146120645780631d32c2c314611f925780632cdeb30c14611cf257806336aea01e14611cb657806347e1da2a14611aef5780634a58db1914611a275780634d44560d1461191657806350113dc31461181257806356570571146113765780635ec005fa146113095780636a1ce0031461125b5780637140415614611011578063715018a614610f755780638382c10114610d5a5780638da5cb5b14610d0a5780639dff1bc414610c94578063a526d83b14610b47578063ad605f9314610a5c578063affed0e014610a20578063b61d27f614610942578063b6474f9214610906578063b80ae261146107db578063c399ec88146106e6578063c56ce32f1461052e578063cad0e95e146104c0578063d5af4e2014610484578063e1f950c51461042b578063e8eb3cc6146103bd578063ed894cd314610381578063f2fde38b146102e65763fac7932e0361001357346102e25760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2578235906024359073ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633036102ba57821580156102b2575b61028a5750906102879160443591613c24565b80f35b8490517f9c02fde3000000000000000000000000000000000000000000000000000000008152fd5b508115610274565b8490517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b5080fd5b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5761031f612bbc565b91610328613e66565b73ffffffffffffffffffffffffffffffffffffffff83161561034e578361028784613d65565b908360249251917f1e4fbdf7000000000000000000000000000000000000000000000000000000008352820152fd5b8280fd5b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020906008549051908152f35b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5090346104815760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261048157823592548310156104815750610473602092612c94565b91905490519160031b1c8152f35b80fd5b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020906007549051908152f35b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d573560095481101561037d576020925060095f527f6e1540171b6c0c960b71a7020d9f60077f6af931a8bbf590da0223dacf75c7af01549051908152f35b5082903461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5773ffffffffffffffffffffffffffffffffffffffff91827f00000000000000000000000000000000000000000000000000000000000000001633036106bf576002549160ff8316610662578154156106055750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001176002558154167f4b31ed05a4d8ce9de4f9d476a272179c7af15d7fddd9adaaea416ab9e799edeb8280a280f35b90602060649251917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601f60248201527f41637469766520706173736b657920726571756972656420666f7220324641006044820152fd5b90602060649251917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601360248201527f32464120616c726561647920656e61626c6564000000000000000000000000006044820152fd5b90517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b5082903461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d578051917f70a08231000000000000000000000000000000000000000000000000000000008352309083015260208260248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9182156107d1578392610799575b6020838351908152f35b9091506020813d6020116107c9575b816107b560209383612ddc565b8101031261037d576020925051908361078f565b3d91506107a8565b81513d85823e3d90fd5b5082903461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5773ffffffffffffffffffffffffffffffffffffffff91827f00000000000000000000000000000000000000000000000000000000000000001633036106bf576002549160ff8316156108a95750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166002558154167f53c421f8b75959dde39c41de22c62b6a71338810b36a9fd9c7ba5c010e2fa1e38280a280f35b90602060649251917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601460248201527f32464120616c72656164792064697361626c65640000000000000000000000006044820152fd5b50823461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5760209250549051908152f35b5082903461037d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5761097c612bbc565b9160443567ffffffffffffffff8111610a1c5761099c9036908301612c02565b91909273ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633036109f65785610287866109ec368789612f75565b9060243590613dcf565b517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b8480fd5b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020906001549051908152f35b5091346102e25760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2578035908054821015610aea57509180610aa760a094612c94565b90549060031b1c9283815260036020522080549160018201549060ff60036002850154940154169381519586526020860152840152606083015215156080820152f35b60649060208551917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601360248201527f496e646578206f7574206f6620626f756e6473000000000000000000000000006044820152fd5b5082903461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d57610b81612bbc565b90303303610c6c5773ffffffffffffffffffffffffffffffffffffffff821692838552600560205260ff8286205416610c45578315610c1e57508284526005602052832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055610bf790612eb0565b7f038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f9698280a280f35b90517faabd5a09000000000000000000000000000000000000000000000000000000008152fd5b90517ffecca77f000000000000000000000000000000000000000000000000000000008152fd5b9050517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b5082903461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d578060a0938335815260036020522080549260018201549260028301549160ff6003850154169301549381519586526020860152840152151560608301526080820152f35b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e25773ffffffffffffffffffffffffffffffffffffffff60209254169051908152f35b5082903461037d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5781359060243590610d9b612bdf565b91338652600560205260ff828720541615610f4d5760075415610f255760085494610dc586612e1d565b600855858752600a6020528287209085825582600183015573ffffffffffffffffffffffffffffffffffffffff60028301951694857fffffffffffffffffffffffff000000000000000000000000000000000000000082541617905560016003830155335f52808201602052835f2060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0082541617905562015180420190814211610ef95750906006916005820155017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000815416905581519384526020840152820152817ff1b0340f9d1d7162a64944ed64fb8166ac5d84071763a1ddb0c849a09193d64c60603393a333907f75cc0fdddfa6e54a2f04c21b76aa220e6673bd272744ed9848e6305ad53b89ea8380a380f35b8860116024927f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b8482517faabd5a09000000000000000000000000000000000000000000000000000000008152fd5b8482517fef6d0f02000000000000000000000000000000000000000000000000000000008152fd5b823461048157807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261048157610fac613e66565b5f73ffffffffffffffffffffffffffffffffffffffff81547fffffffffffffffffffffffff000000000000000000000000000000000000000081168355167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08280a380f35b5082903461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5761104b612bbc565b3033036112345773ffffffffffffffffffffffffffffffffffffffff80911691828552600560205260ff81862054161561120c57828552600560205284207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008154169055835b600680548083101561120157908391856110ca85612cf6565b949054600395861b1c16146110e4575050506001016110b1565b9194959093927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff928381019081116111d557906111348561112761116094612cf6565b9054908a1b1c1691612cf6565b90919073ffffffffffffffffffffffffffffffffffffffff8084549260031b9316831b921b1916179055565b83549081156111a95750019261117584612cf6565b81939154921b1b19169055555b7fb8107d0c6b40be480ce3172ee66ba6d64b71f6b1685a851340036e6e2e3e3c528280a280f35b8760316024927f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b6024896011857f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b505050509050611182565b8390517f6677f3c8000000000000000000000000000000000000000000000000000000008152fd5b50517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d57358252600a6020908152918190208054600182015460028301546003840154600585015460069095015495519384529583019190915273ffffffffffffffffffffffffffffffffffffffff1660408201526060810193909352608083015260ff808216151560a084015260089190911c16151560c082015260e090f35b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d573591600654831015610481575073ffffffffffffffffffffffffffffffffffffffff611367602093612cf6565b92905490519260031b1c168152f35b50823461037d57602091827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261180e57813590818552600a84528085209360058501549182156117e6576006860194855460ff81166117be5760ff8160081c1661179657600394858901546007541161176e5742106117465760018097817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008094161790555b6116b5575b8754158015906116a9575b1561164757508654928688015491835182810190868252848682015285815261145781612d88565b51902095865f52808352845f205461161f5781876114e887519361147a85612d6c565b8985528685018881528d8a870190428252606088019281845260808901965f88525f52848b528c5f209851895551908801555160028701555115159085019060ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0083541691151516179055565b51910155805490680100000000000000008210156115f357916115708761153a84606098968d7fc645c03b7d3e6cfd2c5b9bae2fe247afa8d49da53a910bf1416646b86033e0129b9997019055612c94565b9091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83549160031b92831b921b1916179055565b82519384528301524290820152a25b6115a273ffffffffffffffffffffffffffffffffffffffff600285015416613d65565b7f351b86967219b1fe72918b6d4fb11cd45d6ef80d0d8cbe71807845366082e7628480a28154910154907ff7c19ce202c69a7e855eea96077c7b37560e363f244b0d7a10f5b014558494618380a380f35b6041907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b5083517f692d285a000000000000000000000000000000000000000000000000000000008152fd5b935050505060025460ff811661165f575b505061157f565b1660025573ffffffffffffffffffffffffffffffffffffffff6002840154167f53c421f8b75959dde39c41de22c62b6a71338810b36a9fd9c7ba5c010e2fa1e38580a28480611658565b5086880154151561142f565b81548015611740577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101908111611714576116f090612c94565b905490861b1c895284845284838a20018181541690558661170f6131c6565b61141f565b60248a6011857f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b50611424565b5090517ff8aef316000000000000000000000000000000000000000000000000000000008152fd5b8284517f8af69cf1000000000000000000000000000000000000000000000000000000008152fd5b5090517f9c432834000000000000000000000000000000000000000000000000000000008152fd5b5090517f3c719eee000000000000000000000000000000000000000000000000000000008152fd5b8490517f8b934514000000000000000000000000000000000000000000000000000000008152fd5b8380fd5b50823461037d57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5791611854611891936024359035613026565b969492936118846118758a93989a9b949b519a60e08c5260e08c0190612c61565b6020958b8203878d0152612c61565b91898303908a0152612c61565b8681036060880152818084519283815201930190845b8181106119025750505085820360808701528080885193848152019701925b8281106118ec578680876118e28b8984820360a0860152612c61565b9060c08301520390f35b83511515885296810196928101926001016118c6565b8251855293830193918301916001016118a7565b50346102e257807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e25782359073ffffffffffffffffffffffffffffffffffffffff80831680930361180e577f000000000000000000000000000000000000000000000000000000000000000016918233036119ff57938394833b15610a1c576044859283855196879485937f205c287800000000000000000000000000000000000000000000000000000000855284015260243560248401525af19081156119f657506119e65750f35b6119ef90612d2b565b6104815780f35b513d84823e3d90fd5b8482517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b5082905f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5773ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001691823b15611aeb575f9060248351809581937fb760faf9000000000000000000000000000000000000000000000000000000008352309083015234905af1908115611ae25750611ad8575080f35b6100209150612d2b565b513d5f823e3d90fd5b5f80fd5b508234611aeb5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5767ffffffffffffffff8135818111611aeb57611b3f9036908401612c30565b9092602490602435848111611aeb57611b5b9036908301612c30565b9094604435908111611aeb57611b749036908401612c30565b95909473ffffffffffffffffffffffffffffffffffffffff98897f00000000000000000000000000000000000000000000000000000000000000001633036102ba5787821480611cad575b15611c5157505f5b818110611bd057005b611bdb81838b612eda565b358a81168103611aeb57611bf0828686612eda565b3589831015611c26578291611c2091611c1a611c1360019660051b8d018d612eea565b3691612f75565b91613dcf565b01611bc7565b876032887f4e487b71000000000000000000000000000000000000000000000000000000005f52525ffd5b517f08c379a0000000000000000000000000000000000000000000000000000000008152602081860152600f60248201527f4c656e677468206d69736d6174636800000000000000000000000000000000006044820152606490fd5b50838214611bbf565b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb576020906006549051908152f35b508234611aeb5760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb578035602435611d30612bdf565b916064359182151590818403611aeb577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf6011329586548060038955611f63575b5084611ef0575b50957f9f8c60a88a90d9985a9dcdf9cc51a4bfd6b3a7215dca14b0c10d2281a76c791f91869784151580611ee7575b611ed5575b611db287613d65565b73ffffffffffffffffffffffffffffffffffffffff9160ff83891698895f526005602052611e0d865f20917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0092600184825416179055612eb0565b600160075560025416911617600255825194855260208501527f00000000000000000000000000000000000000000000000000000000000000001692a2817f038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f9695f80a2611eab575b50611e7b57005b6002905560016020527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602080a1005b7f4b31ed05a4d8ce9de4f9d476a272179c7af15d7fddd9adaaea416ab9e799edeb5f80a282611e74565b611ee26084358287613c24565b611da9565b50801515611da4565b83151580611f5a575b611d755760649060208951917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601460248201527f32464120726571756972657320706173736b65790000000000000000000000006044820152fd5b50811515611ef9565b9796600189819897981c14303b1015611f865786979860ff9796971b1b96611d6e565b5063f92ee8a95f52601cfd5b508234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5780355f526003602052815f20805491821561200757509182600160809401549160ff6003600284015493015416928151948552602085015283015215156060820152f35b60649060208551917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601660248201527f506173736b657920646f6573206e6f74206578697374000000000000000000006044820152fd5b508234611aeb577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc90606082360112611aeb5780359167ffffffffffffffff8311611aeb57610120908336030112611aeb576044359173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163303612190576121069060243590830161338b565b9180612117575b6020838551908152f35b5f80808093335af1612127613bf5565b5015612133578061210d565b60649060208451917f08c379a0000000000000000000000000000000000000000000000000000000008352820152600e60248201527f50726566756e64206661696c65640000000000000000000000000000000000006044820152fd5b5082517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b838234611aeb57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760243567ffffffffffffffff8111611aeb576122079036908401612c02565b606081036123285761223e935f602095869286518481019135825284815261222e81612da4565b8751928392839251928391612e77565b8101039060025afa1561231e575f5190606011611aeb57828201355f5260038452825f205f9260ff60038301541680612314575b6122f3575b5050505f146122cb577fffffffff000000000000000000000000000000000000000000000000000000007f1626ba7e00000000000000000000000000000000000000000000000000000000915b5191168152f35b7fffffffff000000000000000000000000000000000000000000000000000000005f916122c4565b61230c93506001825492015492868201359135906132f0565b838080612277565b5081541515612272565b82513d5f823e3d90fd5b5050517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760209051620151808152f35b838234611aeb57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760243573ffffffffffffffffffffffffffffffffffffffff8116809103611aeb5782602093355f52600a8452825f2001905f52825260ff815f20541690519015158152f35b838234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb57813590335f52600560205260ff815f2054161561259557815f52600a602052805f209060058201541561256d57600682015460ff81166125455760081c60ff1661251d57838201335f528060205260ff825f2054166124f5576003939450335f526020525f2060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00825416179055016124cb8154612e1d565b905533907f75cc0fdddfa6e54a2f04c21b76aa220e6673bd272744ed9848e6305ad53b89ea5f80a3005b8482517f6a543dee000000000000000000000000000000000000000000000000000000008152fd5b8390517f9c432834000000000000000000000000000000000000000000000000000000008152fd5b8482517f3c719eee000000000000000000000000000000000000000000000000000000008152fd5b8390517f8b934514000000000000000000000000000000000000000000000000000000008152fd5b9050517fef6d0f02000000000000000000000000000000000000000000000000000000008152fd5b508234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb578035913033036126ea57825f52600a602052805f206005810154156126c3576006019182549160ff831661269d5760ff8360081c166126775750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790557f02019f620969ef34023e1048f0b0f0e1c337f94acda5cfad6aaceb56989674555f80a2005b517f9c432834000000000000000000000000000000000000000000000000000000008152fd5b517f3c719eee000000000000000000000000000000000000000000000000000000008152fd5b50517f8b934514000000000000000000000000000000000000000000000000000000008152fd5b517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760209060ff6002541690519015158152f35b838234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb578135913033036127d457821580156127c9575b610c1e577f4ff5b0bd81d83bbabe0f0bfaceb9711047a031ba8563e95bbffe3b094a3cffd0602084848160075551908152a1005b506006548311612795565b90517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5780519081600654908181526020809101809260065f527ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f905f5b8181106128cb575050508461287a910385612ddc565b825181815293518185018190528493840192915f5b82811061289e57505050500390f35b835173ffffffffffffffffffffffffffffffffffffffff168552869550938101939281019260010161288f565b825473ffffffffffffffffffffffffffffffffffffffff1684529284019260019283019201612864565b5034611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760209073ffffffffffffffffffffffffffffffffffffffff612945612bbc565b165f526005825260ff815f20541690519015158152f35b508234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb57355f908152600a6020908152908290208054600182015460028301546003840154600585015460069095015496519384529483019190915273ffffffffffffffffffffffffffffffffffffffff1660408201526060810192909252608082015260ff808316151560a083015260089290921c909116151560c082015260e090f35b82859134611aeb57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5773ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163303612b955750805160208101908335825260243583820152828152612a9b81612d88565b51902091825f52600360205260ff6003835f2001541615612b6e5760018154111580612b62575b612b3b57507fc8896d5eff3687c3973d1b15100fdf1a4453cd98c445d826c855740a6bcc51e090825f526003602052805f20600381017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008154169055612b278461323c565b6001815491015482519182526020820152a2005b90517f53f56588000000000000000000000000000000000000000000000000000000008152fd5b5060ff60025416612ac2565b90517f2e329a08000000000000000000000000000000000000000000000000000000008152fd5b90507fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b6004359073ffffffffffffffffffffffffffffffffffffffff82168203611aeb57565b6044359073ffffffffffffffffffffffffffffffffffffffff82168203611aeb57565b9181601f84011215611aeb5782359167ffffffffffffffff8311611aeb5760208381860195010111611aeb57565b9181601f84011215611aeb5782359167ffffffffffffffff8311611aeb576020808501948460051b010111611aeb57565b9081518082526020808093019301915f5b828110612c80575050505090565b835185529381019392810192600101612c72565b600454811015612cc95760045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b01905f90565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b600654811015612cc95760065f527ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f01905f90565b67ffffffffffffffff8111612d3f57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b60a0810190811067ffffffffffffffff821117612d3f57604052565b6060810190811067ffffffffffffffff821117612d3f57604052565b6040810190811067ffffffffffffffff821117612d3f57604052565b6020810190811067ffffffffffffffff821117612d3f57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117612d3f57604052565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114612e4a5760010190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f5b838110612e885750505f910152565b8181015183820152602001612e79565b90939293848311611aeb578411611aeb578101920390565b6006549068010000000000000000821015612d3f57611134826001612ed89401600655612cf6565b565b9190811015612cc95760051b0190565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215611aeb570180359067ffffffffffffffff8211611aeb57602001918136038313611aeb57565b67ffffffffffffffff8111612d3f57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b929192612f8182612f3b565b91612f8f6040519384612ddc565b829481845281830111611aeb578281602093845f960137010152565b67ffffffffffffffff8111612d3f5760051b60200190565b90612fcd82612fab565b612fda6040519182612ddc565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06130088294612fab565b0190602036910137565b8051821015612cc95760209160051b010190565b9160048054928385101561315b5760328111613153575b84840390848211612e4a578082101561314c57505b61305b81612fc3565b9461306582612fc3565b9461306f83612fc3565b9461307984612fc3565b9461308385612fc3565b9461308d81612fc3565b945f5b82811061309d5750505050565b8082018083116131205790848d6130b5600194612c94565b9054600391821b1c90815f526020526130d28460405f2093613012565b528d6130e084835492613012565b52838d6130f1858385015492613012565b526002820154613101858f613012565b5261310c848d613012565b520154613119828a613012565b5201613090565b6011857f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b9050613052565b50603261303d565b505060408051935061316c84612dc0565b5f845280519361317b85612dc0565b5f855281519361318a85612dc0565b5f855282519361319985612dc0565b5f85528351936131a885612dc0565b5f855251926131b684612dc0565b5f84525f36813796959493929190565b600454801561320f577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff809101906131fd82612c94565b909182549160031b1b19169055600455565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603160045260245ffd5b6004906004545f5b8181106132515750505050565b8261325b82612c94565b919054600392831b1c146132725750600101613244565b9250927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82019182116132c457506132bc92916132b161153a92612c94565b9054911b1c91612c94565b612ed86131c6565b6011907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b92909391936040519384526020840152836040840152606083015260808201525f805260205f60a0836101005afa503d15613353575b507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a85f5160011491111090565b6d1ab2e8006fd8b71907bf06a5bdee3b613326575f60a06020926dd01ea45f9efd5c54f037fa57ea1a5afa15613389575f613326565bfe5b613399610100820182612eea565b90925f925f94600454158015613bb0575b5060ff600254169573ffffffffffffffffffffffffffffffffffffffff95865f54169760b86133dc6040870187612eea565b9190911015613b8c575b508115908115613b83575b50613b5c5760e08610613b4f577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf860195808711612e4a5761343581888187612e98565b959097827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9f810111612e4a5761348f907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9f84018488612e98565b9590359560208110613b1b575b506040519260c0840184811067ffffffffffffffff821117612d3f5760405260608452606060208501525f60408501525f60608501525f60808501525f60a085015260467fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9f82011015613a03575b5050604051948760208701526020865261352386612da4565b60b86135345f936040810190612eea565b905010155f146137875750505f9084606095805191826136aa575b50505060208101518051604083015160608401519088519189600d84017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c825283870190602081601389600d898b0101106022602d8b880101515f1a141695012092012014169184826014011090821760801c109060207f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918701015160581c141616975282519687519060058060218b015116149060208311161697889384613672575b50505050613654575b505050505b1561364a57839261363892613eb6565b9216911603613645575f90565b600190565b5050505050600190565b613669945060a06080820151910151916132f0565b5f808080613623565b60208080959850846001959750840101809882808084519a019601940160025afa831b5afa5192523d1561338957845f80808061361a565b90919650600380600289010460021b91604051987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f7f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5260208b01858c0194602084818801990101926004828551975f87525b0193828551818160121c16515f538181600c1c1651600153818160061c1651600253165184535f518152019289841015613762576004908390613724565b50505095505f93600393604092520160405206600204809303520385525f808061354f565b9193509391505f526003918260205260405f2060ff8482015416806139f9575b6137b5575b50505050613628565b8091929394505491600180920154935f9281906060978351908161390d575b505050505060208101518051604083015160608401519088519189600d84017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c825283870190602081601389600d898b0101106022602d8b880101515f1a141695012092012014169184826014011090821760801c109060207f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918701015160581c141616975282519687519060058060218b0151161490602083111616978893846138d5575b505050506138b7575b505050505f8080806137ac565b6138cc945060a06080820151910151916132f0565b5f8080806138aa565b60208080959850846001959750840101809882808084519a019601940160025afa831b5afa5192523d1561338957845f8080806138a1565b81929394995060028192010460021b91604051997f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f957f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5260208c0196858d019260208581860192010191825193895f85525b6139b2575b5050505f9596509060409291520160405206600204809303520385525f808080806137d4565b87600491019a828c51818160121c16515f538181600c1c16518d53818160061c1651600253165189535f5181520198828a10156139f157989989613987565b899a5061398c565b50805415156137a7565b8101813560f01c80830192600284017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b84019283821115613a48575b5050505061350a565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f95613a9b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff59926002613aa69601614009565b895285030190614009565b60208601523560f01c60408501527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5d81013560f01c60608501527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5f8101356080850152013560a08301525f8080808080613a3f565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060209792970360031b1b16945f61349c565b5050505050505050600190565b50509290506041810361364a578392613b7492613eb6565b92169116145f14613645575f90565b9050155f6133f1565b9850505050601886013560388701359060988760588a01351698013515155f6133e6565b9194509450612cc9577f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b545f52600360205260405f209260018454940154945f6133aa565b3d15613c1f573d90613c0682612f3b565b91613c146040519384612ddc565b82523d5f602084013e565b606090565b604090815160208101908282528484820152838152613c4281612d88565b51902093845f526003602052825f2054613d3c576004835191613c6483612d6c565b838352613cd960208401878152868501428152606086019160018352608087019485528a5f526003602052885f2096518755516001870155516002860155511515600385019060ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0083541691151516179055565b519101556004549168010000000000000000831015612d3f577fc645c03b7d3e6cfd2c5b9bae2fe247afa8d49da53a910bf1416646b86033e01293613d2a8661153a86600160609801600455612c94565b815192835260208301524290820152a2565b600483517f692d285a000000000000000000000000000000000000000000000000000000008152fd5b5f549073ffffffffffffffffffffffffffffffffffffffff80911691827fffffffffffffffffffffffff00000000000000000000000000000000000000008216175f55167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b915f928392602083519301915af1613de5613bf5565b9015613dee5750565b6044601f917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06040519384927fa5fa8d2b00000000000000000000000000000000000000000000000000000000845260206004850152613e5d8151809281602488015260208888019101612e77565b01168101030190fd5b73ffffffffffffffffffffffffffffffffffffffff5f54163303613e8657565b60246040517f118cdaa7000000000000000000000000000000000000000000000000000000008152336004820152fd5b9091604103613fab576020820135604092838101355f1a7f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08311613f7757601b81141580613fa0575b613f7757925f926080926020958751938452868401523586830152606082015282805260015afa15611ae2575f519073ffffffffffffffffffffffffffffffffffffffff821615613f4e575090565b600490517f38a85a8d000000000000000000000000000000000000000000000000000000008152fd5b600485517f38a85a8d000000000000000000000000000000000000000000000000000000008152fd5b50601c811415613eff565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601860248201527f496e76616c6964207369676e6174757265206c656e67746800000000000000006044820152fd5b6040909291928382519485926020840137808252015f60208201520160405256fea2646970667358221220034579a9babad05cc114a63f72473ca5e7c9cf8137e450b073cc4a56e90486d864736f6c634300081700330000000000000000000000000000000071727de22e5e9d8baf0edac6f37da032000000000000000000000000000000000000", - "nonce": "0x83", + "input": "0x64e0308718ee4c040568238643c07e7afd6c53efc196d26b00000000000000000028c56f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000241460808060405234610016576123f9908161001b8239f35b5f80fdfe60406080815260049081361015610014575f80fd5b5f3560e01c908163053518f3146110d457816313af403514610fc25781631c848fb914610ec75781631eac094714610e32578163541c03b714610daa5781635aedae9114610d115781635bd865c214610c845781636d61fe7014610a3757816385030d58146109235781638a91b0e3146106f4578163970032031461068c578163ab18c40f146102e9578163d60b347f1461028c578163ecd059611461024f578163f551e2ee146101bf578163fa54416114610121575063fac7932e146100d9575f80fd5b3461011d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5761011b9060443590602435903533611e89565b005b5f80fd5b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209073ffffffffffffffffffffffffffffffffffffffff6101b661017361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b54169051908152f35b90503461011d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d576101f861117b565b5060443567ffffffffffffffff811161011d5761024761023e6020947fffffffff000000000000000000000000000000000000000000000000000000009336910161119e565b906024356118af565b915191168152f35b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d576001602092519135148152f35b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209073ffffffffffffffffffffffffffffffffffffffff6102de61017361117b565b541615159051908152f35b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d578035916103633373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b835f526020526003906003815f20019081549060ff8216156106645760ff6103c83373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541680610613575b6105eb57507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6026020526040902080549081156105bf577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9182019055335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260409020905f5b8254808210156105b4578661049a8386611272565b905490871b1c146104ae5750600101610485565b8281969394959601908111610588576104da6104cd61050f9287611272565b905490881b1c9286611272565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b9055825490811561055c57508101926105288484611272565b81939154921b1b19169055555b337f2d7aa68d7c6e29815818dcd0deea0f49ceac033240ac3b69b6a6734c23e5843d5f80a3005b6031907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b6011837f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b505050505050610535565b6011847f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b8490517f53f56588000000000000000000000000000000000000000000000000000000008152fd5b50600161065d3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b54146103d0565b8490517fddb63d7c000000000000000000000000000000000000000000000000000000008152fd5b90503461011d577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc818136011261011d5782359067ffffffffffffffff821161011d5761012090823603011261011d576020926106ed916024359101611358565b9051908152f35b90503461011d5760209060207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57823567ffffffffffffffff811161011d57610746903690850161119e565b5050335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6036020908152604080832080547fffffffffffffffffffffffff00000000000000000000000000000000000000001690557f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d604825280832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690557f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d601909152812093905b845481101561089d576001905f836108683373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b610872848a611272565b919054600392831b1c8452885282878120918183558187840155816002840155820155015501610813565b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260408120805491815581610905575b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6026020526040812055005b5f5260205f20908101905b818110156108d5575f8155600101610910565b823461011d57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760a0916109c761096061117b565b5f6080845161096e816111cc565b8281528260208201528286820152826060820152015273ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b6024355f52602052805f208151916109de836111cc565b815493848452600183015460208501908152608060028501549284870193845260ff6003870154169560608801961515875201549501948552825195865251602086015251908401525115156060830152516080820152f35b90503461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57813567ffffffffffffffff811161011d57610a8860a0913690850161119e565b908092918101031261011d5780359073ffffffffffffffffffffffffffffffffffffffff821680920361011d57602081013583820135608083013593841515850361011d578015610c5c57335f8181527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6036020526040812080547fffffffffffffffffffffffff000000000000000000000000000000000000000016841790557f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a7359080a381151580610c53575b610c3c575b505050610b6357005b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d602602052604090205415610c16575050335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d604602052604090205b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00825416179055337f5e0647e9a594598413ffd03f39a420df134735f631208f3728e01ef447daa9eb5f80a2005b517f29ec01e9000000000000000000000000000000000000000000000000000000008152fd5b6060610c4b9301359133611e89565b5f8080610b5a565b50801515610b55565b8686517f49e27cff000000000000000000000000000000000000000000000000000000008152fd5b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209060ff610d06610cc361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541690519015158152f35b3461011d57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57602090610d90610d4d61117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b6024355f52825260ff6003825f2001541690519015158152f35b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57602090610e2a610de761117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b549051908152f35b90503461011d575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d602602052604090205415610c1657335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260409020610bc6565b3461011d576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5790610f46610f0361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260405f2090565b8151928381835491828152019081935f52825f20905f5b818110610fae5750505084610f73910385611231565b825181815293518185018190528493840192915f5b828110610f9757505050500390f35b835185528695509381019392810192600101610f88565b825484529284019260019283019201610f5d565b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5773ffffffffffffffffffffffffffffffffffffffff61100f61117b565b169182156110ae578261105f3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b817fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055337f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a7355f80a3005b517f49e27cff000000000000000000000000000000000000000000000000000000008152fd5b3461011d575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57335f8181527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6046020526040812080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690557f7a146c6d6578acec91e6f3f4afddbba61d50a19aecf2cedf2753fa04168b61879080a2005b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361011d57565b9181601f8401121561011d5782359167ffffffffffffffff831161011d576020838186019501011161011d57565b60a0810190811067ffffffffffffffff8211176111e857604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040810190811067ffffffffffffffff8211176111e857604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176111e857604052565b8054821015611287575f5260205f2001905f90565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18136030182121561011d570180359067ffffffffffffffff821161011d5760200191813603831361011d57565b9093929384831161011d57841161011d578101920390565b35906020811061132b575090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060200360031b1b1690565b9073ffffffffffffffffffffffffffffffffffffffff6113b53373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b541660ff6114003373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b54169261010090818101601461141682846112b4565b90501061187557611426916112b4565b908160141161011d57601401947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec820190156118805760e08110611875577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffab82019281841161184857836114bd837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8b96818b611305565b9590940192808411611848576114de91846114d8928b611305565b9061131d565b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260409020905f526020908152604090815f209860039460ff60038c01541615801561183f575b61182f5761153a91612252565b825193898386015282855261154e85611215565b8a546001809c0154905f978d886060998151908161173e575b50505090508585015197885191888701516060880151908351918c8b600d85017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c885284830189600d87890101106022602d89840101515f1a141691838160138c012092012014169388846014011090841760801c10927f2274797065223a22776562617574686e2e67657422000000000000000000000060581c9201015160581c1416169252865180519260058060218401511614908a851116169a8b61170b575b505050505086611662575b50505050505090501561165b5761164e93612116565b1561165857505f90565b90565b5050505090565b849650918395979160a0938460805f9701519801518a819b51998a96875289870152850152606084015260808301528380525afa503d156116d4575b50507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a85f518714911110805f8080808080611638565b6d1ab2e8006fd8b71907bf06a5bdee3b61169e5760a05f916dd01ea45f9efd5c54f037fa57ea1a5afa15611709575f8061169e565bfe5b83949c5089808095840101809e82808084519a019601940160025afa831b5afa5198523d15611709575f808e818061162d565b8a91929b50899060026003600286010460021b9584519e8f987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f927f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5289878c019b01968981890194010194855196825f88525b6117e5575b50505050505091600393915f959352018b5206600204809303520387525f8d818080611567565b859c8460049297959697019d8e51818160121c16515f538181600c1c16518653818160061c16518553165186535f518152019b858d101561182a5782959493956117b9565b6117be565b5050505050505050505050600190565b508a541561152d565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b505050505050600190565b80939495925060419150036118a65761189893612116565b156118a1575f90565b600190565b50505050600190565b919060148210611e62578160141161011d576014017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec82019173ffffffffffffffffffffffffffffffffffffffff6119443373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b54169260ff6119903373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541615611e4e5760e08110611e25577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffab82019281841161184857836119f8837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8b968185611305565b959094019280841161184857611a1391846114d89285611305565b90611a5b3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b915f526020918252604091825f209160039460ff600385015416158015611e1c575b611dee57611a8a91612252565b9383518983820152828152611a9e81611215565b83546001968780960154925f9481606092805180611cf9575b50505050858201519081519089840151606085015190825191600d83017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c855282870186600d85870101106022602d87840101515f1a1416918d81601389012092012014169185826014011090821760801c10908b7f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918801015160581c14161691528784519b8c926005806021865196015116149083851116169c8d9586611cc1575b505050505050611c16575b5050505050505015611bee57611ba193612116565b15611bca577f1626ba7e0000000000000000000000000000000000000000000000000000000090565b7fffffffff0000000000000000000000000000000000000000000000000000000090565b505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b909192939495965060a06080820151910151968791815195865286860152840152606083015260808201525f8052815f60a0836101005afa503d15611c8c575b50507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8905f51149111105f808080808080611b8c565b6d1ab2e8006fd8b71907bf06a5bdee3b611c565760a05f916dd01ea45f9efd5c54f037fa57ea1a5afa15611709575f80611c56565b91939550918080959a5086840101809a82808084519a019601940160025afa831b5afa5194523d1561170957875f8087898280611b81565b9093508a896002936003600285010460021b948d8451987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f987f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f52858b0199898c01968981890194010194855196855f88525b611d9e575b50505050505091600393915f959352018d5206600204809303520381525f808080611ab7565b859c83856004939894959697980193845190828260121c16515f538282600c1c16519053818160061c16518653165186535f518152019b858d1015611de857949392919085611d73565b94611d78565b505050505050505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b50835415611a7d565b50505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b91905060418203611bee57611ba193612116565b5050507fffffffff0000000000000000000000000000000000000000000000000000000090565b92908015801561210e575b6120e4576040918251946020928387018181528686890152858852606088019780891067ffffffffffffffff8a11176111e8578887525190209673ffffffffffffffffffffffffffffffffffffffff831696875f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60091828752875f208a5f52875260ff6003895f200154166120bc575090600491875193611f35856111cc565b845286840190815287840142815260608501916001835260808601938885528b5f528952895f208c5f528952895f2095518655516001860155516002850155600384019051151560ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00835416911617905551910155611ff28173ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260405f2090565b90815491680100000000000000008310156111e85761201e6104da848a93600161206397018155611272565b905573ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b928354937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff85146118485760017f6d847e8457dd04dfa090a722cff190d721a734963c5036ea7d44809f60eac2af9501905551908152a3565b807f692d285a0000000000000000000000000000000000000000000000000000000060049252fd5b60046040517f65a3c412000000000000000000000000000000000000000000000000000000008152fd5b508215611e94565b92919067ffffffffffffffff82116111e857602090604094855193612162847fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8401160186611231565b80855283850192368282011161011d57815f928692863786010152855193805187816040146121f957506041146121a95750505050505050505b638baa579f5f526004601cfd5b86606091828101515f1a8652015190525b5f52518452600160805f825afa51925f606052523d6121db5750505061219c565b73ffffffffffffffffffffffffffffffffffffffff80911691161490565b90507f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff910151601b8160ff1c018552166060526121ba565b6040909291928382519485926020840137808252015f602082015201604052565b91909160405160c0810181811067ffffffffffffffff8211176111e857604052606081526020810160608152604082015f8152606083015f815260808401915f835260a08501955f8752859860468110156122b2575b5050505050505050565b8101947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc8601916002813560f01c80830191820191858311156122f9575b505050506122a8565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09a61234c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffba9360026123579701612231565b905289030190612231565b90523560f01c90527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbe83013560f01c90527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08201359052013590525f80808080808080808080806122f056fea26469706673582212206e916195c5615e46311c599d70fbea075235964ce56d6588e52c7187e8f214ee64736f6c63430008170033000000000000000000000000", + "nonce": "0x8e", "chainId": "0xaa36a7" }, "additionalContracts": [ { "transactionType": "CREATE2", - "contractName": "P256AccountFactory", - "address": "0x0000000000569c25b117231b791c9acad7a930c7", - "initCode": "0x60e034610103576001600160401b0390601f614c0e38819003918201601f1916830191848311848410176100ef5780849260209460405283398101031261010357516001600160a01b0381169081810361010357608052604051916141f790818401908111848210176100ef576020928492610a17843981520301905ff080156100e45760a0526d6396ff2a80c067f99b3d2ab4df2460c05260405161090f9081610108823960805181818161012201526103ad015260a05181818161033f015281816106580152610878015260c05181818160b0015281816106cc01526107e50152f35b6040513d5f823e3d90fd5b634e487b7160e01b5f52604160045260245ffd5b5f80fdfe6040608081526004361015610012575f80fd5b5f3560e01c806323771738146103635780633a4741bd146102f557806356ceaeda146102bf578063cfde1317146101af578063e1aa8cce14610146578063e8eb3cc6146100d85763f5382f0314610067575f80fd5b346100d4575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100d4576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5f80fd5b50346100d4575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100d4576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346100d45760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100d4576044359073ffffffffffffffffffffffffffffffffffffffff80831683036100d4576101a760209360643590610792565b915191168152f35b50346100d45773ffffffffffffffffffffffffffffffffffffffff906101d436610453565b93959496909285519660208801987f56ceaeda000000000000000000000000000000000000000000000000000000008a52602489015260448801521660648601526084850152151560a484015260c483015260c4825261010082019180831067ffffffffffffffff84111761029257603461027c9161028e958585523060601b61012083015261026c825180926101348501906104bd565b8101036014810185520183610521565b519182916020835260208301906104de565b0390f35b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b50346100d45760209073ffffffffffffffffffffffffffffffffffffffff6101a76102e936610453565b9493909392919261058e565b50346100d4575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100d4576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b506103706102e936610453565b34610395575b73ffffffffffffffffffffffffffffffffffffffff6020925191168152f35b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001690813b156100d4575f9060248551809481937fb760faf90000000000000000000000000000000000000000000000000000000083528716600483015234905af180156104495761041e575b50610376565b67ffffffffffffffff811161029257825273ffffffffffffffffffffffffffffffffffffffff610418565b83513d5f823e3d90fd5b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc60c09101126100d457600435906024359060443573ffffffffffffffffffffffffffffffffffffffff811681036100d457906064359060843580151581036100d4579060a43590565b5f5b8381106104ce5750505f910152565b81810151838201526020016104bf565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f60209361051a815180928187528780880191016104bd565b0116010190565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761029257604052565b908160209103126100d4575173ffffffffffffffffffffffffffffffffffffffff811681036100d45790565b9291959490939561059f8382610792565b803b61077357506105b08382610844565b9173ffffffffffffffffffffffffffffffffffffffff9081604051937f2cdeb30c00000000000000000000000000000000000000000000000000000000602086015287602486015288604486015216988960648501521515608484015260a483015260a4825260e082019282841067ffffffffffffffff85111761029257836040527fa97b90d5000000000000000000000000000000000000000000000000000000008452817f00000000000000000000000000000000000000000000000000000000000000001660e48401525f61010484015261012483015260806101448301526020837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff20846106c56101648201826104de565b0301815f857f0000000000000000000000000000000000000000000000000000000000000000165af18015610768577f569add262e905a4f49f55725922d01603540b7aabad95ebd8f07e00670bc845a936040935f92610733575b50501696879382519182526020820152a4565b610759925060e0906020903d602011610760575b6107518285610521565b010190610562565b5f80610720565b3d9150610747565b6040513d5f823e3d90fd5b73ffffffffffffffffffffffffffffffffffffffff1696505050505050565b9061079c91610844565b604051907f5414dff0000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115610768575f91610818575090565b61083a915060203d60201161083d575b6108328183610521565b810190610562565b90565b503d610828565b6040519160208301917fffffffffffffffffffffffffffffffffffffffff000000000000000000000000809160601b1683527f000000000000000000000000000000000000000000000000000000000000000060601b166034840152604883015260488252608082019180831067ffffffffffffffff841117610292576bffffffffffffffffffffffff92604052519020169056fea2646970667358221220759c8ed5754d32c5e74dd7bf101962b65816d374844b959bf1fb045a627161ed64736f6c6343000817003360a0346200017e576001600160401b0390601f620041f738819003918201601f19168301918483118484101762000182578084926020946040528339810103126200017e57516001600160a01b03919082811681036200017e57331562000166575f543360018060a01b03198216175f55604051933391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a360805263409feecd198054600181166200015957829060011c036200011e575b614060838162000197823960805181818161024501528181610407015281816105780152818161075f01528181610825015281816109b60152818161196901528181611a6a01528181611b9001528181611e28015281816120d10152612a560152f35b6002600160411b03905560209081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29080a15f80620000bb565b63f92ee8a95f526004601cfd5b604051631e4fbdf760e01b81525f6004820152602490fd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe608060409080825260049182361015610022575b5050503615610020575f80fd5b005b5f915f3560e01c908163011cfdbc14612a0e5750806304802c9a1461295c5780630633b14a146128f55780630665f04b146127fb5780630904b9ed1461275157806309b7026e1461271057806310de2676146125bd578063113647bb14612400578063136dd7551461238b5780631506ac5a146123505780631626ba7e146121b857806319822f7c146120645780631d32c2c314611f925780632cdeb30c14611cf257806336aea01e14611cb657806347e1da2a14611aef5780634a58db1914611a275780634d44560d1461191657806350113dc31461181257806356570571146113765780635ec005fa146113095780636a1ce0031461125b5780637140415614611011578063715018a614610f755780638382c10114610d5a5780638da5cb5b14610d0a5780639dff1bc414610c94578063a526d83b14610b47578063ad605f9314610a5c578063affed0e014610a20578063b61d27f614610942578063b6474f9214610906578063b80ae261146107db578063c399ec88146106e6578063c56ce32f1461052e578063cad0e95e146104c0578063d5af4e2014610484578063e1f950c51461042b578063e8eb3cc6146103bd578063ed894cd314610381578063f2fde38b146102e65763fac7932e0361001357346102e25760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2578235906024359073ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633036102ba57821580156102b2575b61028a5750906102879160443591613c24565b80f35b8490517f9c02fde3000000000000000000000000000000000000000000000000000000008152fd5b508115610274565b8490517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b5080fd5b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5761031f612bbc565b91610328613e66565b73ffffffffffffffffffffffffffffffffffffffff83161561034e578361028784613d65565b908360249251917f1e4fbdf7000000000000000000000000000000000000000000000000000000008352820152fd5b8280fd5b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020906008549051908152f35b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5090346104815760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261048157823592548310156104815750610473602092612c94565b91905490519160031b1c8152f35b80fd5b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020906007549051908152f35b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d573560095481101561037d576020925060095f527f6e1540171b6c0c960b71a7020d9f60077f6af931a8bbf590da0223dacf75c7af01549051908152f35b5082903461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5773ffffffffffffffffffffffffffffffffffffffff91827f00000000000000000000000000000000000000000000000000000000000000001633036106bf576002549160ff8316610662578154156106055750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001176002558154167f4b31ed05a4d8ce9de4f9d476a272179c7af15d7fddd9adaaea416ab9e799edeb8280a280f35b90602060649251917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601f60248201527f41637469766520706173736b657920726571756972656420666f7220324641006044820152fd5b90602060649251917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601360248201527f32464120616c726561647920656e61626c6564000000000000000000000000006044820152fd5b90517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b5082903461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d578051917f70a08231000000000000000000000000000000000000000000000000000000008352309083015260208260248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9182156107d1578392610799575b6020838351908152f35b9091506020813d6020116107c9575b816107b560209383612ddc565b8101031261037d576020925051908361078f565b3d91506107a8565b81513d85823e3d90fd5b5082903461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5773ffffffffffffffffffffffffffffffffffffffff91827f00000000000000000000000000000000000000000000000000000000000000001633036106bf576002549160ff8316156108a95750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166002558154167f53c421f8b75959dde39c41de22c62b6a71338810b36a9fd9c7ba5c010e2fa1e38280a280f35b90602060649251917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601460248201527f32464120616c72656164792064697361626c65640000000000000000000000006044820152fd5b50823461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5760209250549051908152f35b5082903461037d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5761097c612bbc565b9160443567ffffffffffffffff8111610a1c5761099c9036908301612c02565b91909273ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633036109f65785610287866109ec368789612f75565b9060243590613dcf565b517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b8480fd5b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020906001549051908152f35b5091346102e25760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2578035908054821015610aea57509180610aa760a094612c94565b90549060031b1c9283815260036020522080549160018201549060ff60036002850154940154169381519586526020860152840152606083015215156080820152f35b60649060208551917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601360248201527f496e646578206f7574206f6620626f756e6473000000000000000000000000006044820152fd5b5082903461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d57610b81612bbc565b90303303610c6c5773ffffffffffffffffffffffffffffffffffffffff821692838552600560205260ff8286205416610c45578315610c1e57508284526005602052832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055610bf790612eb0565b7f038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f9698280a280f35b90517faabd5a09000000000000000000000000000000000000000000000000000000008152fd5b90517ffecca77f000000000000000000000000000000000000000000000000000000008152fd5b9050517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b5082903461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d578060a0938335815260036020522080549260018201549260028301549160ff6003850154169301549381519586526020860152840152151560608301526080820152f35b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e25773ffffffffffffffffffffffffffffffffffffffff60209254169051908152f35b5082903461037d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5781359060243590610d9b612bdf565b91338652600560205260ff828720541615610f4d5760075415610f255760085494610dc586612e1d565b600855858752600a6020528287209085825582600183015573ffffffffffffffffffffffffffffffffffffffff60028301951694857fffffffffffffffffffffffff000000000000000000000000000000000000000082541617905560016003830155335f52808201602052835f2060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0082541617905562015180420190814211610ef95750906006916005820155017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000815416905581519384526020840152820152817ff1b0340f9d1d7162a64944ed64fb8166ac5d84071763a1ddb0c849a09193d64c60603393a333907f75cc0fdddfa6e54a2f04c21b76aa220e6673bd272744ed9848e6305ad53b89ea8380a380f35b8860116024927f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b8482517faabd5a09000000000000000000000000000000000000000000000000000000008152fd5b8482517fef6d0f02000000000000000000000000000000000000000000000000000000008152fd5b823461048157807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261048157610fac613e66565b5f73ffffffffffffffffffffffffffffffffffffffff81547fffffffffffffffffffffffff000000000000000000000000000000000000000081168355167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08280a380f35b5082903461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5761104b612bbc565b3033036112345773ffffffffffffffffffffffffffffffffffffffff80911691828552600560205260ff81862054161561120c57828552600560205284207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008154169055835b600680548083101561120157908391856110ca85612cf6565b949054600395861b1c16146110e4575050506001016110b1565b9194959093927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff928381019081116111d557906111348561112761116094612cf6565b9054908a1b1c1691612cf6565b90919073ffffffffffffffffffffffffffffffffffffffff8084549260031b9316831b921b1916179055565b83549081156111a95750019261117584612cf6565b81939154921b1b19169055555b7fb8107d0c6b40be480ce3172ee66ba6d64b71f6b1685a851340036e6e2e3e3c528280a280f35b8760316024927f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b6024896011857f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b505050509050611182565b8390517f6677f3c8000000000000000000000000000000000000000000000000000000008152fd5b50517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d57358252600a6020908152918190208054600182015460028301546003840154600585015460069095015495519384529583019190915273ffffffffffffffffffffffffffffffffffffffff1660408201526060810193909352608083015260ff808216151560a084015260089190911c16151560c082015260e090f35b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d573591600654831015610481575073ffffffffffffffffffffffffffffffffffffffff611367602093612cf6565b92905490519260031b1c168152f35b50823461037d57602091827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261180e57813590818552600a84528085209360058501549182156117e6576006860194855460ff81166117be5760ff8160081c1661179657600394858901546007541161176e5742106117465760018097817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008094161790555b6116b5575b8754158015906116a9575b1561164757508654928688015491835182810190868252848682015285815261145781612d88565b51902095865f52808352845f205461161f5781876114e887519361147a85612d6c565b8985528685018881528d8a870190428252606088019281845260808901965f88525f52848b528c5f209851895551908801555160028701555115159085019060ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0083541691151516179055565b51910155805490680100000000000000008210156115f357916115708761153a84606098968d7fc645c03b7d3e6cfd2c5b9bae2fe247afa8d49da53a910bf1416646b86033e0129b9997019055612c94565b9091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83549160031b92831b921b1916179055565b82519384528301524290820152a25b6115a273ffffffffffffffffffffffffffffffffffffffff600285015416613d65565b7f351b86967219b1fe72918b6d4fb11cd45d6ef80d0d8cbe71807845366082e7628480a28154910154907ff7c19ce202c69a7e855eea96077c7b37560e363f244b0d7a10f5b014558494618380a380f35b6041907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b5083517f692d285a000000000000000000000000000000000000000000000000000000008152fd5b935050505060025460ff811661165f575b505061157f565b1660025573ffffffffffffffffffffffffffffffffffffffff6002840154167f53c421f8b75959dde39c41de22c62b6a71338810b36a9fd9c7ba5c010e2fa1e38580a28480611658565b5086880154151561142f565b81548015611740577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101908111611714576116f090612c94565b905490861b1c895284845284838a20018181541690558661170f6131c6565b61141f565b60248a6011857f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b50611424565b5090517ff8aef316000000000000000000000000000000000000000000000000000000008152fd5b8284517f8af69cf1000000000000000000000000000000000000000000000000000000008152fd5b5090517f9c432834000000000000000000000000000000000000000000000000000000008152fd5b5090517f3c719eee000000000000000000000000000000000000000000000000000000008152fd5b8490517f8b934514000000000000000000000000000000000000000000000000000000008152fd5b8380fd5b50823461037d57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5791611854611891936024359035613026565b969492936118846118758a93989a9b949b519a60e08c5260e08c0190612c61565b6020958b8203878d0152612c61565b91898303908a0152612c61565b8681036060880152818084519283815201930190845b8181106119025750505085820360808701528080885193848152019701925b8281106118ec578680876118e28b8984820360a0860152612c61565b9060c08301520390f35b83511515885296810196928101926001016118c6565b8251855293830193918301916001016118a7565b50346102e257807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e25782359073ffffffffffffffffffffffffffffffffffffffff80831680930361180e577f000000000000000000000000000000000000000000000000000000000000000016918233036119ff57938394833b15610a1c576044859283855196879485937f205c287800000000000000000000000000000000000000000000000000000000855284015260243560248401525af19081156119f657506119e65750f35b6119ef90612d2b565b6104815780f35b513d84823e3d90fd5b8482517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b5082905f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5773ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001691823b15611aeb575f9060248351809581937fb760faf9000000000000000000000000000000000000000000000000000000008352309083015234905af1908115611ae25750611ad8575080f35b6100209150612d2b565b513d5f823e3d90fd5b5f80fd5b508234611aeb5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5767ffffffffffffffff8135818111611aeb57611b3f9036908401612c30565b9092602490602435848111611aeb57611b5b9036908301612c30565b9094604435908111611aeb57611b749036908401612c30565b95909473ffffffffffffffffffffffffffffffffffffffff98897f00000000000000000000000000000000000000000000000000000000000000001633036102ba5787821480611cad575b15611c5157505f5b818110611bd057005b611bdb81838b612eda565b358a81168103611aeb57611bf0828686612eda565b3589831015611c26578291611c2091611c1a611c1360019660051b8d018d612eea565b3691612f75565b91613dcf565b01611bc7565b876032887f4e487b71000000000000000000000000000000000000000000000000000000005f52525ffd5b517f08c379a0000000000000000000000000000000000000000000000000000000008152602081860152600f60248201527f4c656e677468206d69736d6174636800000000000000000000000000000000006044820152606490fd5b50838214611bbf565b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb576020906006549051908152f35b508234611aeb5760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb578035602435611d30612bdf565b916064359182151590818403611aeb577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf6011329586548060038955611f63575b5084611ef0575b50957f9f8c60a88a90d9985a9dcdf9cc51a4bfd6b3a7215dca14b0c10d2281a76c791f91869784151580611ee7575b611ed5575b611db287613d65565b73ffffffffffffffffffffffffffffffffffffffff9160ff83891698895f526005602052611e0d865f20917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0092600184825416179055612eb0565b600160075560025416911617600255825194855260208501527f00000000000000000000000000000000000000000000000000000000000000001692a2817f038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f9695f80a2611eab575b50611e7b57005b6002905560016020527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602080a1005b7f4b31ed05a4d8ce9de4f9d476a272179c7af15d7fddd9adaaea416ab9e799edeb5f80a282611e74565b611ee26084358287613c24565b611da9565b50801515611da4565b83151580611f5a575b611d755760649060208951917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601460248201527f32464120726571756972657320706173736b65790000000000000000000000006044820152fd5b50811515611ef9565b9796600189819897981c14303b1015611f865786979860ff9796971b1b96611d6e565b5063f92ee8a95f52601cfd5b508234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5780355f526003602052815f20805491821561200757509182600160809401549160ff6003600284015493015416928151948552602085015283015215156060820152f35b60649060208551917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601660248201527f506173736b657920646f6573206e6f74206578697374000000000000000000006044820152fd5b508234611aeb577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc90606082360112611aeb5780359167ffffffffffffffff8311611aeb57610120908336030112611aeb576044359173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163303612190576121069060243590830161338b565b9180612117575b6020838551908152f35b5f80808093335af1612127613bf5565b5015612133578061210d565b60649060208451917f08c379a0000000000000000000000000000000000000000000000000000000008352820152600e60248201527f50726566756e64206661696c65640000000000000000000000000000000000006044820152fd5b5082517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b838234611aeb57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760243567ffffffffffffffff8111611aeb576122079036908401612c02565b606081036123285761223e935f602095869286518481019135825284815261222e81612da4565b8751928392839251928391612e77565b8101039060025afa1561231e575f5190606011611aeb57828201355f5260038452825f205f9260ff60038301541680612314575b6122f3575b5050505f146122cb577fffffffff000000000000000000000000000000000000000000000000000000007f1626ba7e00000000000000000000000000000000000000000000000000000000915b5191168152f35b7fffffffff000000000000000000000000000000000000000000000000000000005f916122c4565b61230c93506001825492015492868201359135906132f0565b838080612277565b5081541515612272565b82513d5f823e3d90fd5b5050517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760209051620151808152f35b838234611aeb57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760243573ffffffffffffffffffffffffffffffffffffffff8116809103611aeb5782602093355f52600a8452825f2001905f52825260ff815f20541690519015158152f35b838234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb57813590335f52600560205260ff815f2054161561259557815f52600a602052805f209060058201541561256d57600682015460ff81166125455760081c60ff1661251d57838201335f528060205260ff825f2054166124f5576003939450335f526020525f2060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00825416179055016124cb8154612e1d565b905533907f75cc0fdddfa6e54a2f04c21b76aa220e6673bd272744ed9848e6305ad53b89ea5f80a3005b8482517f6a543dee000000000000000000000000000000000000000000000000000000008152fd5b8390517f9c432834000000000000000000000000000000000000000000000000000000008152fd5b8482517f3c719eee000000000000000000000000000000000000000000000000000000008152fd5b8390517f8b934514000000000000000000000000000000000000000000000000000000008152fd5b9050517fef6d0f02000000000000000000000000000000000000000000000000000000008152fd5b508234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb578035913033036126ea57825f52600a602052805f206005810154156126c3576006019182549160ff831661269d5760ff8360081c166126775750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790557f02019f620969ef34023e1048f0b0f0e1c337f94acda5cfad6aaceb56989674555f80a2005b517f9c432834000000000000000000000000000000000000000000000000000000008152fd5b517f3c719eee000000000000000000000000000000000000000000000000000000008152fd5b50517f8b934514000000000000000000000000000000000000000000000000000000008152fd5b517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760209060ff6002541690519015158152f35b838234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb578135913033036127d457821580156127c9575b610c1e577f4ff5b0bd81d83bbabe0f0bfaceb9711047a031ba8563e95bbffe3b094a3cffd0602084848160075551908152a1005b506006548311612795565b90517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5780519081600654908181526020809101809260065f527ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f905f5b8181106128cb575050508461287a910385612ddc565b825181815293518185018190528493840192915f5b82811061289e57505050500390f35b835173ffffffffffffffffffffffffffffffffffffffff168552869550938101939281019260010161288f565b825473ffffffffffffffffffffffffffffffffffffffff1684529284019260019283019201612864565b5034611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760209073ffffffffffffffffffffffffffffffffffffffff612945612bbc565b165f526005825260ff815f20541690519015158152f35b508234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb57355f908152600a6020908152908290208054600182015460028301546003840154600585015460069095015496519384529483019190915273ffffffffffffffffffffffffffffffffffffffff1660408201526060810192909252608082015260ff808316151560a083015260089290921c909116151560c082015260e090f35b82859134611aeb57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5773ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163303612b955750805160208101908335825260243583820152828152612a9b81612d88565b51902091825f52600360205260ff6003835f2001541615612b6e5760018154111580612b62575b612b3b57507fc8896d5eff3687c3973d1b15100fdf1a4453cd98c445d826c855740a6bcc51e090825f526003602052805f20600381017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008154169055612b278461323c565b6001815491015482519182526020820152a2005b90517f53f56588000000000000000000000000000000000000000000000000000000008152fd5b5060ff60025416612ac2565b90517f2e329a08000000000000000000000000000000000000000000000000000000008152fd5b90507fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b6004359073ffffffffffffffffffffffffffffffffffffffff82168203611aeb57565b6044359073ffffffffffffffffffffffffffffffffffffffff82168203611aeb57565b9181601f84011215611aeb5782359167ffffffffffffffff8311611aeb5760208381860195010111611aeb57565b9181601f84011215611aeb5782359167ffffffffffffffff8311611aeb576020808501948460051b010111611aeb57565b9081518082526020808093019301915f5b828110612c80575050505090565b835185529381019392810192600101612c72565b600454811015612cc95760045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b01905f90565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b600654811015612cc95760065f527ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f01905f90565b67ffffffffffffffff8111612d3f57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b60a0810190811067ffffffffffffffff821117612d3f57604052565b6060810190811067ffffffffffffffff821117612d3f57604052565b6040810190811067ffffffffffffffff821117612d3f57604052565b6020810190811067ffffffffffffffff821117612d3f57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117612d3f57604052565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114612e4a5760010190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f5b838110612e885750505f910152565b8181015183820152602001612e79565b90939293848311611aeb578411611aeb578101920390565b6006549068010000000000000000821015612d3f57611134826001612ed89401600655612cf6565b565b9190811015612cc95760051b0190565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215611aeb570180359067ffffffffffffffff8211611aeb57602001918136038313611aeb57565b67ffffffffffffffff8111612d3f57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b929192612f8182612f3b565b91612f8f6040519384612ddc565b829481845281830111611aeb578281602093845f960137010152565b67ffffffffffffffff8111612d3f5760051b60200190565b90612fcd82612fab565b612fda6040519182612ddc565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06130088294612fab565b0190602036910137565b8051821015612cc95760209160051b010190565b9160048054928385101561315b5760328111613153575b84840390848211612e4a578082101561314c57505b61305b81612fc3565b9461306582612fc3565b9461306f83612fc3565b9461307984612fc3565b9461308385612fc3565b9461308d81612fc3565b945f5b82811061309d5750505050565b8082018083116131205790848d6130b5600194612c94565b9054600391821b1c90815f526020526130d28460405f2093613012565b528d6130e084835492613012565b52838d6130f1858385015492613012565b526002820154613101858f613012565b5261310c848d613012565b520154613119828a613012565b5201613090565b6011857f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b9050613052565b50603261303d565b505060408051935061316c84612dc0565b5f845280519361317b85612dc0565b5f855281519361318a85612dc0565b5f855282519361319985612dc0565b5f85528351936131a885612dc0565b5f855251926131b684612dc0565b5f84525f36813796959493929190565b600454801561320f577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff809101906131fd82612c94565b909182549160031b1b19169055600455565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603160045260245ffd5b6004906004545f5b8181106132515750505050565b8261325b82612c94565b919054600392831b1c146132725750600101613244565b9250927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82019182116132c457506132bc92916132b161153a92612c94565b9054911b1c91612c94565b612ed86131c6565b6011907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b92909391936040519384526020840152836040840152606083015260808201525f805260205f60a0836101005afa503d15613353575b507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a85f5160011491111090565b6d1ab2e8006fd8b71907bf06a5bdee3b613326575f60a06020926dd01ea45f9efd5c54f037fa57ea1a5afa15613389575f613326565bfe5b613399610100820182612eea565b90925f925f94600454158015613bb0575b5060ff600254169573ffffffffffffffffffffffffffffffffffffffff95865f54169760b86133dc6040870187612eea565b9190911015613b8c575b508115908115613b83575b50613b5c5760e08610613b4f577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf860195808711612e4a5761343581888187612e98565b959097827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9f810111612e4a5761348f907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9f84018488612e98565b9590359560208110613b1b575b506040519260c0840184811067ffffffffffffffff821117612d3f5760405260608452606060208501525f60408501525f60608501525f60808501525f60a085015260467fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9f82011015613a03575b5050604051948760208701526020865261352386612da4565b60b86135345f936040810190612eea565b905010155f146137875750505f9084606095805191826136aa575b50505060208101518051604083015160608401519088519189600d84017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c825283870190602081601389600d898b0101106022602d8b880101515f1a141695012092012014169184826014011090821760801c109060207f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918701015160581c141616975282519687519060058060218b015116149060208311161697889384613672575b50505050613654575b505050505b1561364a57839261363892613eb6565b9216911603613645575f90565b600190565b5050505050600190565b613669945060a06080820151910151916132f0565b5f808080613623565b60208080959850846001959750840101809882808084519a019601940160025afa831b5afa5192523d1561338957845f80808061361a565b90919650600380600289010460021b91604051987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f7f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5260208b01858c0194602084818801990101926004828551975f87525b0193828551818160121c16515f538181600c1c1651600153818160061c1651600253165184535f518152019289841015613762576004908390613724565b50505095505f93600393604092520160405206600204809303520385525f808061354f565b9193509391505f526003918260205260405f2060ff8482015416806139f9575b6137b5575b50505050613628565b8091929394505491600180920154935f9281906060978351908161390d575b505050505060208101518051604083015160608401519088519189600d84017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c825283870190602081601389600d898b0101106022602d8b880101515f1a141695012092012014169184826014011090821760801c109060207f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918701015160581c141616975282519687519060058060218b0151161490602083111616978893846138d5575b505050506138b7575b505050505f8080806137ac565b6138cc945060a06080820151910151916132f0565b5f8080806138aa565b60208080959850846001959750840101809882808084519a019601940160025afa831b5afa5192523d1561338957845f8080806138a1565b81929394995060028192010460021b91604051997f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f957f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5260208c0196858d019260208581860192010191825193895f85525b6139b2575b5050505f9596509060409291520160405206600204809303520385525f808080806137d4565b87600491019a828c51818160121c16515f538181600c1c16518d53818160061c1651600253165189535f5181520198828a10156139f157989989613987565b899a5061398c565b50805415156137a7565b8101813560f01c80830192600284017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b84019283821115613a48575b5050505061350a565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f95613a9b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff59926002613aa69601614009565b895285030190614009565b60208601523560f01c60408501527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5d81013560f01c60608501527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5f8101356080850152013560a08301525f8080808080613a3f565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060209792970360031b1b16945f61349c565b5050505050505050600190565b50509290506041810361364a578392613b7492613eb6565b92169116145f14613645575f90565b9050155f6133f1565b9850505050601886013560388701359060988760588a01351698013515155f6133e6565b9194509450612cc9577f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b545f52600360205260405f209260018454940154945f6133aa565b3d15613c1f573d90613c0682612f3b565b91613c146040519384612ddc565b82523d5f602084013e565b606090565b604090815160208101908282528484820152838152613c4281612d88565b51902093845f526003602052825f2054613d3c576004835191613c6483612d6c565b838352613cd960208401878152868501428152606086019160018352608087019485528a5f526003602052885f2096518755516001870155516002860155511515600385019060ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0083541691151516179055565b519101556004549168010000000000000000831015612d3f577fc645c03b7d3e6cfd2c5b9bae2fe247afa8d49da53a910bf1416646b86033e01293613d2a8661153a86600160609801600455612c94565b815192835260208301524290820152a2565b600483517f692d285a000000000000000000000000000000000000000000000000000000008152fd5b5f549073ffffffffffffffffffffffffffffffffffffffff80911691827fffffffffffffffffffffffff00000000000000000000000000000000000000008216175f55167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b915f928392602083519301915af1613de5613bf5565b9015613dee5750565b6044601f917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06040519384927fa5fa8d2b00000000000000000000000000000000000000000000000000000000845260206004850152613e5d8151809281602488015260208888019101612e77565b01168101030190fd5b73ffffffffffffffffffffffffffffffffffffffff5f54163303613e8657565b60246040517f118cdaa7000000000000000000000000000000000000000000000000000000008152336004820152fd5b9091604103613fab576020820135604092838101355f1a7f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08311613f7757601b81141580613fa0575b613f7757925f926080926020958751938452868401523586830152606082015282805260015afa15611ae2575f519073ffffffffffffffffffffffffffffffffffffffff821615613f4e575090565b600490517f38a85a8d000000000000000000000000000000000000000000000000000000008152fd5b600485517f38a85a8d000000000000000000000000000000000000000000000000000000008152fd5b50601c811415613eff565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601860248201527f496e76616c6964207369676e6174757265206c656e67746800000000000000006044820152fd5b6040909291928382519485926020840137808252015f60208201520160405256fea2646970667358221220034579a9babad05cc114a63f72473ca5e7c9cf8137e450b073cc4a56e90486d864736f6c634300081700330000000000000000000000000000000071727de22e5e9d8baf0edac6f37da032" + "contractName": "P256MFAValidatorModule", + "address": "0x000000b07799b322d076669ef32b247d02279c7e", + "initCode": "0x60808060405234610016576123f9908161001b8239f35b5f80fdfe60406080815260049081361015610014575f80fd5b5f3560e01c908163053518f3146110d457816313af403514610fc25781631c848fb914610ec75781631eac094714610e32578163541c03b714610daa5781635aedae9114610d115781635bd865c214610c845781636d61fe7014610a3757816385030d58146109235781638a91b0e3146106f4578163970032031461068c578163ab18c40f146102e9578163d60b347f1461028c578163ecd059611461024f578163f551e2ee146101bf578163fa54416114610121575063fac7932e146100d9575f80fd5b3461011d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5761011b9060443590602435903533611e89565b005b5f80fd5b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209073ffffffffffffffffffffffffffffffffffffffff6101b661017361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b54169051908152f35b90503461011d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d576101f861117b565b5060443567ffffffffffffffff811161011d5761024761023e6020947fffffffff000000000000000000000000000000000000000000000000000000009336910161119e565b906024356118af565b915191168152f35b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d576001602092519135148152f35b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209073ffffffffffffffffffffffffffffffffffffffff6102de61017361117b565b541615159051908152f35b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d578035916103633373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b835f526020526003906003815f20019081549060ff8216156106645760ff6103c83373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541680610613575b6105eb57507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6026020526040902080549081156105bf577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9182019055335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260409020905f5b8254808210156105b4578661049a8386611272565b905490871b1c146104ae5750600101610485565b8281969394959601908111610588576104da6104cd61050f9287611272565b905490881b1c9286611272565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b9055825490811561055c57508101926105288484611272565b81939154921b1b19169055555b337f2d7aa68d7c6e29815818dcd0deea0f49ceac033240ac3b69b6a6734c23e5843d5f80a3005b6031907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b6011837f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b505050505050610535565b6011847f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b8490517f53f56588000000000000000000000000000000000000000000000000000000008152fd5b50600161065d3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b54146103d0565b8490517fddb63d7c000000000000000000000000000000000000000000000000000000008152fd5b90503461011d577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc818136011261011d5782359067ffffffffffffffff821161011d5761012090823603011261011d576020926106ed916024359101611358565b9051908152f35b90503461011d5760209060207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57823567ffffffffffffffff811161011d57610746903690850161119e565b5050335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6036020908152604080832080547fffffffffffffffffffffffff00000000000000000000000000000000000000001690557f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d604825280832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690557f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d601909152812093905b845481101561089d576001905f836108683373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b610872848a611272565b919054600392831b1c8452885282878120918183558187840155816002840155820155015501610813565b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260408120805491815581610905575b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6026020526040812055005b5f5260205f20908101905b818110156108d5575f8155600101610910565b823461011d57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760a0916109c761096061117b565b5f6080845161096e816111cc565b8281528260208201528286820152826060820152015273ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b6024355f52602052805f208151916109de836111cc565b815493848452600183015460208501908152608060028501549284870193845260ff6003870154169560608801961515875201549501948552825195865251602086015251908401525115156060830152516080820152f35b90503461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57813567ffffffffffffffff811161011d57610a8860a0913690850161119e565b908092918101031261011d5780359073ffffffffffffffffffffffffffffffffffffffff821680920361011d57602081013583820135608083013593841515850361011d578015610c5c57335f8181527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6036020526040812080547fffffffffffffffffffffffff000000000000000000000000000000000000000016841790557f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a7359080a381151580610c53575b610c3c575b505050610b6357005b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d602602052604090205415610c16575050335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d604602052604090205b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00825416179055337f5e0647e9a594598413ffd03f39a420df134735f631208f3728e01ef447daa9eb5f80a2005b517f29ec01e9000000000000000000000000000000000000000000000000000000008152fd5b6060610c4b9301359133611e89565b5f8080610b5a565b50801515610b55565b8686517f49e27cff000000000000000000000000000000000000000000000000000000008152fd5b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5760209060ff610d06610cc361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541690519015158152f35b3461011d57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57602090610d90610d4d61117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b6024355f52825260ff6003825f2001541690519015158152f35b3461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57602090610e2a610de761117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b549051908152f35b90503461011d575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d602602052604090205415610c1657335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260409020610bc6565b3461011d576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5790610f46610f0361117b565b73ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260405f2090565b8151928381835491828152019081935f52825f20905f5b818110610fae5750505084610f73910385611231565b825181815293518185018190528493840192915f5b828110610f9757505050500390f35b835185528695509381019392810192600101610f88565b825484529284019260019283019201610f5d565b823461011d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d5773ffffffffffffffffffffffffffffffffffffffff61100f61117b565b169182156110ae578261105f3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b817fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055337f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a7355f80a3005b517f49e27cff000000000000000000000000000000000000000000000000000000008152fd5b3461011d575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011d57335f8181527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6046020526040812080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690557f7a146c6d6578acec91e6f3f4afddbba61d50a19aecf2cedf2753fa04168b61879080a2005b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361011d57565b9181601f8401121561011d5782359167ffffffffffffffff831161011d576020838186019501011161011d57565b60a0810190811067ffffffffffffffff8211176111e857604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040810190811067ffffffffffffffff8211176111e857604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176111e857604052565b8054821015611287575f5260205f2001905f90565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18136030182121561011d570180359067ffffffffffffffff821161011d5760200191813603831361011d57565b9093929384831161011d57841161011d578101920390565b35906020811061132b575090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060200360031b1b1690565b9073ffffffffffffffffffffffffffffffffffffffff6113b53373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b541660ff6114003373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b54169261010090818101601461141682846112b4565b90501061187557611426916112b4565b908160141161011d57601401947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec820190156118805760e08110611875577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffab82019281841161184857836114bd837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8b96818b611305565b9590940192808411611848576114de91846114d8928b611305565b9061131d565b335f9081527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260409020905f526020908152604090815f209860039460ff60038c01541615801561183f575b61182f5761153a91612252565b825193898386015282855261154e85611215565b8a546001809c0154905f978d886060998151908161173e575b50505090508585015197885191888701516060880151908351918c8b600d85017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c885284830189600d87890101106022602d89840101515f1a141691838160138c012092012014169388846014011090841760801c10927f2274797065223a22776562617574686e2e67657422000000000000000000000060581c9201015160581c1416169252865180519260058060218401511614908a851116169a8b61170b575b505050505086611662575b50505050505090501561165b5761164e93612116565b1561165857505f90565b90565b5050505090565b849650918395979160a0938460805f9701519801518a819b51998a96875289870152850152606084015260808301528380525afa503d156116d4575b50507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a85f518714911110805f8080808080611638565b6d1ab2e8006fd8b71907bf06a5bdee3b61169e5760a05f916dd01ea45f9efd5c54f037fa57ea1a5afa15611709575f8061169e565bfe5b83949c5089808095840101809e82808084519a019601940160025afa831b5afa5198523d15611709575f808e818061162d565b8a91929b50899060026003600286010460021b9584519e8f987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f927f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5289878c019b01968981890194010194855196825f88525b6117e5575b50505050505091600393915f959352018b5206600204809303520387525f8d818080611567565b859c8460049297959697019d8e51818160121c16515f538181600c1c16518653818160061c16518553165186535f518152019b858d101561182a5782959493956117b9565b6117be565b5050505050505050505050600190565b508a541561152d565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b505050505050600190565b80939495925060419150036118a65761189893612116565b156118a1575f90565b600190565b50505050600190565b919060148210611e62578160141161011d576014017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec82019173ffffffffffffffffffffffffffffffffffffffff6119443373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60360205260405f2090565b54169260ff6119903373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60460205260405f2090565b541615611e4e5760e08110611e25577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffab82019281841161184857836119f8837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8b968185611305565b959094019280841161184857611a1391846114d89285611305565b90611a5b3373ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60060205260405f2090565b915f526020918252604091825f209160039460ff600385015416158015611e1c575b611dee57611a8a91612252565b9383518983820152828152611a9e81611215565b83546001968780960154925f9481606092805180611cf9575b50505050858201519081519089840151606085015190825191600d83017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c855282870186600d85870101106022602d87840101515f1a1416918d81601389012092012014169185826014011090821760801c10908b7f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918801015160581c14161691528784519b8c926005806021865196015116149083851116169c8d9586611cc1575b505050505050611c16575b5050505050505015611bee57611ba193612116565b15611bca577f1626ba7e0000000000000000000000000000000000000000000000000000000090565b7fffffffff0000000000000000000000000000000000000000000000000000000090565b505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b909192939495965060a06080820151910151968791815195865286860152840152606083015260808201525f8052815f60a0836101005afa503d15611c8c575b50507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8905f51149111105f808080808080611b8c565b6d1ab2e8006fd8b71907bf06a5bdee3b611c565760a05f916dd01ea45f9efd5c54f037fa57ea1a5afa15611709575f80611c56565b91939550918080959a5086840101809a82808084519a019601940160025afa831b5afa5194523d1561170957875f8087898280611b81565b9093508a896002936003600285010460021b948d8451987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f987f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f52858b0199898c01968981890194010194855196855f88525b611d9e575b50505050505091600393915f959352018d5206600204809303520381525f808080611ab7565b859c83856004939894959697980193845190828260121c16515f538282600c1c16519053818160061c16518653165186535f518152019b858d1015611de857949392919085611d73565b94611d78565b505050505050505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b50835415611a7d565b50505050507fffffffff0000000000000000000000000000000000000000000000000000000090565b91905060418203611bee57611ba193612116565b5050507fffffffff0000000000000000000000000000000000000000000000000000000090565b92908015801561210e575b6120e4576040918251946020928387018181528686890152858852606088019780891067ffffffffffffffff8a11176111e8578887525190209673ffffffffffffffffffffffffffffffffffffffff831696875f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60091828752875f208a5f52875260ff6003895f200154166120bc575090600491875193611f35856111cc565b845286840190815287840142815260608501916001835260808601938885528b5f528952895f208c5f528952895f2095518655516001860155516002850155600384019051151560ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00835416911617905551910155611ff28173ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60160205260405f2090565b90815491680100000000000000008310156111e85761201e6104da848a93600161206397018155611272565b905573ffffffffffffffffffffffffffffffffffffffff165f527f8a0c9d8ec1d9f8b8c1a5e6f7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d60260205260405f2090565b928354937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff85146118485760017f6d847e8457dd04dfa090a722cff190d721a734963c5036ea7d44809f60eac2af9501905551908152a3565b807f692d285a0000000000000000000000000000000000000000000000000000000060049252fd5b60046040517f65a3c412000000000000000000000000000000000000000000000000000000008152fd5b508215611e94565b92919067ffffffffffffffff82116111e857602090604094855193612162847fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8401160186611231565b80855283850192368282011161011d57815f928692863786010152855193805187816040146121f957506041146121a95750505050505050505b638baa579f5f526004601cfd5b86606091828101515f1a8652015190525b5f52518452600160805f825afa51925f606052523d6121db5750505061219c565b73ffffffffffffffffffffffffffffffffffffffff80911691161490565b90507f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff910151601b8160ff1c018552166060526121ba565b6040909291928382519485926020840137808252015f602082015201604052565b91909160405160c0810181811067ffffffffffffffff8211176111e857604052606081526020810160608152604082015f8152606083015f815260808401915f835260a08501955f8752859860468110156122b2575b5050505050505050565b8101947fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc8601916002813560f01c80830191820191858311156122f9575b505050506122a8565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09a61234c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffba9360026123579701612231565b905289030190612231565b90523560f01c90527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbe83013560f01c90527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08201359052013590525f80808080808080808080806122f056fea26469706673582212206e916195c5615e46311c599d70fbea075235964ce56d6588e52c7187e8f214ee64736f6c63430008170033" + } + ], + "isFixedGasLimit": false + }, + { + "hash": "0x572875cd08fec02682e4390e06e9c7fd30c7fca916d030f4e098b1f0d5fe7cf6", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0x0000000000ffe8b47b3e2130213b802212439497", + "function": null, + "arguments": null, + "transaction": { + "from": "0x18ee4c040568238643c07e7afd6c53efc196d26b", + "to": "0x0000000000ffe8b47b3e2130213b802212439497", + "gas": "0x39679f", + "value": "0x0", + "input": "0x64e0308718ee4c040568238643c07e7afd6c53efc196d26b000000000000000001b2f72400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000002d7260e034610117576001600160401b0390601f612d5238819003918201601f1916830191848311848410176100f1578084926020946040528339810103126101175751906001600160a01b03908183168084036101175715610105576040519061246690818301908111838210176100f15782916108ec833903905ff080156100e6571660805260a0526d6396ff2a80c067f99b3d2ab4df2460c0526040516107d0908161011c823960805181818161028a0152818161051b0152610739015260a05181818161021e0152610451015260c051818181609e0152818161058001526106b60152f35b6040513d5f823e3d90fd5b634e487b7160e01b5f52604160045260245ffd5b604051631a0a9b9f60e21b8152600490fd5b5f80fdfe60806040908082526004361015610014575f80fd5b5f3560e01c90816311464fbe14610242575080633a5381b5146101d45780633a8fcd9a146101295780638cb84e18146100c65763f5382f0314610055575f80fd5b346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5f80fd5b50346100c257807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c25760209073ffffffffffffffffffffffffffffffffffffffff6101216101186102ae565b60243590610663565b915191168152f35b50346100c25760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576101616102ae565b67ffffffffffffffff91906024358381116100c2576101849036906004016102d1565b9390916044359473ffffffffffffffffffffffffffffffffffffffff9384871687036100c2576064359384116100c2576020966101c86101219536906004016102d1565b939092608435956103d7565b50346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c25760209073ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b6004359073ffffffffffffffffffffffffffffffffffffffff821682036100c257565b9181601f840112156100c25782359167ffffffffffffffff83116100c257602083818601950101116100c257565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761037e57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b908160209103126100c2575173ffffffffffffffffffffffffffffffffffffffff811681036100c25790565b969593949092916103e88589610663565b803b610659575094610487939195610400868a610705565b916104bd73ffffffffffffffffffffffffffffffffffffffff95604051998a938860209a8b997f0d1bf5d3000000000000000000000000000000000000000000000000000000008b89015260a498837f00000000000000000000000000000000000000000000000000000000000000001660248a0152608060448a015260a48901916102ff565b931660648601527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc8584030160848601526102ff565b03906104ef7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0928381018a528961033d565b6040519788937fa97b90d5000000000000000000000000000000000000000000000000000000008552867f00000000000000000000000000000000000000000000000000000000000000001660048601525f60248601526044850152608060648501528051918260848601525f5b8381106106425750505060a491601f825f85879586010152011681010301815f857f0000000000000000000000000000000000000000000000000000000000000000165af1938415610637575f946105e4575b5090817f33310a89c32d8cc00057ad6ef6274d2f8fe22389a992cf89983e09fc84f6cfff92859760405195865216941692a3565b82919450610628907f33310a89c32d8cc00057ad6ef6274d2f8fe22389a992cf89983e09fc84f6cfff933d8411610630575b610620818361033d565b8101906103ab565b9390916105b0565b503d610616565b6040513d5f823e3d90fd5b8281018701518b820183015288968b96500161055d565b9750505050505050565b9061066d91610705565b604051907f5414dff0000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115610637575f916106e9575090565b610702915060203d60201161063057610620818361033d565b90565b6040519160208301917fffffffffffffffffffffffffffffffffffffffff000000000000000000000000809160601b1683527f000000000000000000000000000000000000000000000000000000000000000060601b166034840152604883015260488252608082019180831067ffffffffffffffff84111761037e576bffffffffffffffffffffffff92604052519020169056fea264697066735822122007e02d6bc6a2534a3e464491617058f3405dc92cdb96be09e8deece4731ec84464736f6c63430008170033608080604052346100895763409feecd198054906001821661007c576001600160401b039160011c6002600160401b031901610042575b6123d8838161008e8239f35b6002600160411b03905560209081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29080a15f80610036565b63f92ee8a95f526004601cfd5b5f80fdfe60806040526004361015610015575b3661142857005b5f3560e01c80630d1bf5d31461010f57806310b0a63d146100e7578063112d3a7d1461010a5780631195e07e146101055780631626ba7e1461010057806319822f7c146100fb57806346d09cea146100f65780637833fa04146100f15780639517e29f146100ec5780639cfd7cff146100e7578063a71763a8146100e2578063d03c7914146100dd578063d691c964146100d8578063e8eb3cc6146100d3578063e9ae5c53146100ce5763f2dc691d0361000e57610f05565b610ec8565b610e80565b610dac565b610d22565b610c03565b6104fe565b610a11565b610988565b61090d565b610781565b610651565b610601565b6105db565b610171565b73ffffffffffffffffffffffffffffffffffffffff81160361013257565b5f80fd5b359061014182610114565b565b9181601f840112156101325782359167ffffffffffffffff8311610132576020838186019501011161013257565b346101325760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760048035906101ae82610114565b67ffffffffffffffff602435818111610132576101ce9036908401610143565b939092604435926101de84610114565b606435908111610132576101f59036908301610143565b9290917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf6011329687548060038a55610377575b5073ffffffffffffffffffffffffffffffffffffffff9283811690811561034e5761028a9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b803b15610132576102cc975f80946040519a8b95869485937f6d61fe700000000000000000000000000000000000000000000000000000000085528401610fbd565b03925af1948515610349578695610330575b50831661031f575b5050506102ef57005b6002905560016020527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602080a1005b610328926114ef565b5f80806102e6565b8061033d610343926103d6565b8061039f565b5f6102de565b610fce565b836040517f682a6e7c000000000000000000000000000000000000000000000000000000008152fd5b600181819a939a1c14303b10156103935760ff1b1b965f610226565b8263f92ee8a95f52601cfd5b5f91031261013257565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff81116103ea57604052565b6103a9565b6040810190811067ffffffffffffffff8211176103ea57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176103ea57604052565b67ffffffffffffffff81116103ea57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b5f5b8381106104975750505f910152565b8181015183820152602001610488565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f6020936104e381518092818752878088019101610486565b0116010190565b9060206104fb9281815201906104a7565b90565b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325761057960405161053b816103ef565b601281527f657468617572612e617572612e302e312e30000000000000000000000000000060208201526040519182916020835260208301906104a7565b0390f35b60607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc82011261013257600435916024356105b781610114565b916044359067ffffffffffffffff8211610132576105d791600401610143565b9091565b346101325760206105f76105ee3661057d565b92919091611066565b6040519015158152f35b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff5f5416604051908152f35b346101325760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760243567ffffffffffffffff81116101325760206106a5610709923690600401610143565b73ffffffffffffffffffffffffffffffffffffffff5f5416906040518095819482937ff551e2ee0000000000000000000000000000000000000000000000000000000084523360048501526004356024850152606060448501526064840191610f7f565b03915afa801561034957610579915f91610752575b506040517fffffffff0000000000000000000000000000000000000000000000000000000090911681529081906020820190565b610774915060203d60201161077a575b61076c818361040b565b810190611198565b5f61071e565b503d610762565b34610132577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc606081360112610132576004359067ffffffffffffffff821161013257610120908236030112610132576044356f71727de22e5e9d8baf0edac6f37da03233036108e357602061086c5f9361082f610816610816875473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b906040519586809481937f97003203000000000000000000000000000000000000000000000000000000008352602435906004016004840161120c565b03925af190811561034957610579925f926108b2575b508061089a575b506040519081529081906020820190565b5f80808093335af1506108ab61131a565b505f610889565b6108d591925060203d6020116108dc575b6108cd818361040b565b8101906111ad565b905f610882565b503d6108c3565b60046040517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b7fffffffff0000000000000000000000000000000000000000000000000000000081160361013257565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610132577fffffffff000000000000000000000000000000000000000000000000000000006004356109e48161095e565b165f526002602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b610a1a3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610bf9575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610ab2577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610a8a9184611869565b6040805191825273ffffffffffffffffffffffffffffffffffffffff929092166020820152a1005b60028403610aec577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae79184611746565b610a8a565b60038403610b21577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846115d2565b60048403610b56577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846114ef565b6040517f41c38b3000000000000000000000000000000000000000000000000000000000815260048101859052602490fd5b6040517fb927fe5e00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff84166004820152602490fd5b60046040517fa2e5c09a000000000000000000000000000000000000000000000000000000008152fd5b5030331415610a35565b610c0c3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610d18575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610c795760046040517fa23dd761000000000000000000000000000000000000000000000000000000008152fd5b60028403610cae577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610a8a9184611c1c565b60038403610ce3577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae79184611abc565b60048403610b56577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae791846119f5565b5030331415610c27565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206105f7600435611349565b9060407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc83011261013257600435916024359067ffffffffffffffff8211610132576105d791600401610143565b610db536610d5e565b91335f526001926020926001845260ff60405f20541615610e5657610dd992611dce565b9160405191808301818452845180915260408401918060408360051b8701019601925f905b838210610e0b5786880387f35b90919293948380610e458a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08b869d0301865289516104a7565b999701959493919091019101610dfe565b60046040517fc51267d8000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206040516f71727de22e5e9d8baf0edac6f37da0328152f35b610ed136610d5e565b906f71727de22e5e9d8baf0edac6f37da03233141580610efb575b610bcf57610ef992611dce565b005b5030331415610eec565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602060043560018114908115610f74575b8115610f69575b8115610f5e575b506040519015158152f35b60049150145f610f53565b600381149150610f4c565b600281149150610f45565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b9160206104fb938181520191610f7f565b6040513d5f823e3d90fd5b906004116101325790600490565b909291928360041161013257831161013257600401917ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0190565b7fffffffff00000000000000000000000000000000000000000000000000000000903581811693926004811061105757505050565b60040360031b82901b16169150565b90929190600181036110b1575050506110935f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff90811691161490565b600281036110ee575050506110e76104fb9173ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b5460ff1690565b6003810361116a57506004821015611107575050505f90565b61112061111a6110939361115093610fd9565b90611022565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b5473ffffffffffffffffffffffffffffffffffffffff1690565b9050600491501461117a57505f90565b60035473ffffffffffffffffffffffffffffffffffffffff16611093565b9081602091031261013257516104fb8161095e565b90816020910312610132575190565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18236030181121561013257016020813591019167ffffffffffffffff821161013257813603831361013257565b929190611315611276602092604087526112466040880161122c83610136565b73ffffffffffffffffffffffffffffffffffffffff169052565b83810135606088015261130561125f60408301836111bc565b9390610120948560808c01526101608b0191610f7f565b916112fc6112bd61128a60608401846111bc565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc096918d60a08982860301910152610f7f565b608083013560c08c015260a083013560e08c01528a6101009660c0850135888301526112ec60e08601866111bc565b9290918882860301910152610f7f565b938101906111bc565b9188840301610140890152610f7f565b930152565b3d15611344573d9061132b8261044c565b91611339604051938461040b565b82523d5f602084013e565b606090565b61135d818060081b918160301b9160501b90565b50507fff0000000000000000000000000000000000000000000000000000000000000080921680159081156113fe575b81156113d4575b5061139f5750505f90565b1680159081156113ad575090565b7f010000000000000000000000000000000000000000000000000000000000000091501490565b7ffe000000000000000000000000000000000000000000000000000000000000009150145f611394565b7f01000000000000000000000000000000000000000000000000000000000000008114915061138d565b7fffffffff000000000000000000000000000000000000000000000000000000005f35165f52600260205273ffffffffffffffffffffffffffffffffffffffff60405f2054168015611491575f8091368280378136915af43d5f803e1561148d573d5ff35b3d5ffd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f4e6f2066616c6c6261636b2068616e646c6572000000000000000000000000006044820152fd5b929160035473ffffffffffffffffffffffffffffffffffffffff9485821661159f57947fffffffffffffffffffffffff00000000000000000000000000000000000000009495169384911617600355823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b03925af18015610349576115925750565b8061033d610141926103d6565b60249086604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b90916115ec90806115e661111a8287610fd9565b94610fe7565b92909173ffffffffffffffffffffffffffffffffffffffff918261163d611150837fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b166116ff578161167a6116ba927fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055565b1691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f5c426a4200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602490fd5b60ff6117708273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541661182257806117b473ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008254161790551691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b916118885f5473ffffffffffffffffffffffffffffffffffffffff1690565b9273ffffffffffffffffffffffffffffffffffffffff8082169416908482146119b05781611938575b6118f5915073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b813b15610132575f60405180937f8a91b0e300000000000000000000000000000000000000000000000000000000825281838161198360048201604090602081525f60208201520190565b03925af1918215610349576118f59261199d575b506118b1565b8061033d6119aa926103d6565b5f611997565b5050823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b919060035473ffffffffffffffffffffffffffffffffffffffff8094168094821603611a8b577fffffffffffffffffffffffff000000000000000000000000000000000000000016600355823b1561013257611581925f92836040518096819582947f8a91b0e3000000000000000000000000000000000000000000000000000000008452602060048501526024840191610f7f565b602484604051907f026d96390000000000000000000000000000000000000000000000000000000082526004820152fd5b91611ad59080611acf61111a8286610fd9565b93610fe7565b9091611b0e611150827fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b73ffffffffffffffffffffffffffffffffffffffff808616959116859003611bd35750611b68611b90917fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b7fffffffffffffffffffffffff00000000000000000000000000000000000000008154169055565b823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f026d963900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602490fd5b60ff611c468273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541615611cf65780611c8b73ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0081541690551691823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f026d9639000000000000000000000000000000000000000000000000000000008352166004820152fd5b6020818303126101325780519067ffffffffffffffff8211610132570181601f82011215610132578051611d708161044c565b92611d7e604051948561040b565b81845260208284010111610132576104fb9160208085019101610486565b6104fb949273ffffffffffffffffffffffffffffffffffffffff60609316825260208201528160408201520191610f7f565b90929160609073ffffffffffffffffffffffffffffffffffffffff9485611e0a60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e8a575b50611e1c9293611f06565b92611e3c60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e46575050565b803b15610132576115815f929183926040519485809481937f173bf7da000000000000000000000000000000000000000000000000000000008352600483016104ea565b92505f60405180947fd68f6025000000000000000000000000000000000000000000000000000000008252818381611ec88888343360048601611d9c565b03925af1801561034957611e1c935f91611ee4575b5092611e11565b611f0091503d805f833e611ef8818361040b565b810190611d3d565b5f611edd565b600881901b91907fff00000000000000000000000000000000000000000000000000000000000000811680611f41575050916104fb926122de565b7f01000000000000000000000000000000000000000000000000000000000000008103611f745750506104fb92506121e2565b919250907ffe00000000000000000000000000000000000000000000000000000000000000036120165750611fab5f92839261235c565b9094929150611fb861205f565b94816040519283928337810184815203915afa611fd361131a565b82511561201157602083015215611fe75790565b60046040517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b612106565b602490604051907fba70d2310000000000000000000000000000000000000000000000000000000082526004820152fd5b67ffffffffffffffff81116103ea5760051b60200190565b60405161206b816103ef565b60018152805f5b60208082101561208d57906060602092828501015201612072565b50505090565b9061209d82612047565b6120aa604051918261040b565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06120d88294612047565b01905f5b8281106120e857505050565b8060606020809385010152016120dc565b908092918237015f815290565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b80518210156120115760209160051b010190565b91908110156120115760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa181360301821215610132570190565b356104fb81610114565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610132570180359067ffffffffffffffff82116101325760200191813603831361013257565b91909180350191602092602081019035916121fc83612093565b945f5b84811061220d575050505050565b61222061221b828787612147565b612187565b5f808461222e858a8a612147565b01359261223c858a8a612147565b9361224c60409586810190612191565b919061225c8751809481936120f9565b03925af161226861131a565b612272848b612133565b527fff0000000000000000000000000000000000000000000000000000000000000085161590816122d5575b506122ac57506001016121ff565b600490517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b9050155f61229e565b6122ec5f949392859261235c565b909692916122f861205f565b97826040519384928337810185815203925af19061231461131a565b90845115612011577fff0000000000000000000000000000000000000000000000000000000000000091602086015216159081612353575b50611fe757565b9050155f61234c565b908060141161013257813560601c9281603411610132577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffcc60346014850135940192019056fea2646970667358221220ea0107d2fc265c63c0a8bd55e9d1937ca16a9aea1ea523588fa7e4f3af23626764736f6c63430008170033000000000000000000000000000000b07799b322d076669ef32b247d02279c7e0000000000000000000000000000", + "nonce": "0x8f", + "chainId": "0xaa36a7" + }, + "additionalContracts": [ + { + "transactionType": "CREATE2", + "contractName": "AuraAccountFactory", + "address": "0x0000004b2941659deb7472b46f7b84caf27dce44", + "initCode": "0x60e034610117576001600160401b0390601f612d5238819003918201601f1916830191848311848410176100f1578084926020946040528339810103126101175751906001600160a01b03908183168084036101175715610105576040519061246690818301908111838210176100f15782916108ec833903905ff080156100e6571660805260a0526d6396ff2a80c067f99b3d2ab4df2460c0526040516107d0908161011c823960805181818161028a0152818161051b0152610739015260a05181818161021e0152610451015260c051818181609e0152818161058001526106b60152f35b6040513d5f823e3d90fd5b634e487b7160e01b5f52604160045260245ffd5b604051631a0a9b9f60e21b8152600490fd5b5f80fdfe60806040908082526004361015610014575f80fd5b5f3560e01c90816311464fbe14610242575080633a5381b5146101d45780633a8fcd9a146101295780638cb84e18146100c65763f5382f0314610055575f80fd5b346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5f80fd5b50346100c257807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c25760209073ffffffffffffffffffffffffffffffffffffffff6101216101186102ae565b60243590610663565b915191168152f35b50346100c25760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576101616102ae565b67ffffffffffffffff91906024358381116100c2576101849036906004016102d1565b9390916044359473ffffffffffffffffffffffffffffffffffffffff9384871687036100c2576064359384116100c2576020966101c86101219536906004016102d1565b939092608435956103d7565b50346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100c2575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c25760209073ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b6004359073ffffffffffffffffffffffffffffffffffffffff821682036100c257565b9181601f840112156100c25782359167ffffffffffffffff83116100c257602083818601950101116100c257565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761037e57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b908160209103126100c2575173ffffffffffffffffffffffffffffffffffffffff811681036100c25790565b969593949092916103e88589610663565b803b610659575094610487939195610400868a610705565b916104bd73ffffffffffffffffffffffffffffffffffffffff95604051998a938860209a8b997f0d1bf5d3000000000000000000000000000000000000000000000000000000008b89015260a498837f00000000000000000000000000000000000000000000000000000000000000001660248a0152608060448a015260a48901916102ff565b931660648601527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc8584030160848601526102ff565b03906104ef7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0928381018a528961033d565b6040519788937fa97b90d5000000000000000000000000000000000000000000000000000000008552867f00000000000000000000000000000000000000000000000000000000000000001660048601525f60248601526044850152608060648501528051918260848601525f5b8381106106425750505060a491601f825f85879586010152011681010301815f857f0000000000000000000000000000000000000000000000000000000000000000165af1938415610637575f946105e4575b5090817f33310a89c32d8cc00057ad6ef6274d2f8fe22389a992cf89983e09fc84f6cfff92859760405195865216941692a3565b82919450610628907f33310a89c32d8cc00057ad6ef6274d2f8fe22389a992cf89983e09fc84f6cfff933d8411610630575b610620818361033d565b8101906103ab565b9390916105b0565b503d610616565b6040513d5f823e3d90fd5b8281018701518b820183015288968b96500161055d565b9750505050505050565b9061066d91610705565b604051907f5414dff0000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115610637575f916106e9575090565b610702915060203d60201161063057610620818361033d565b90565b6040519160208301917fffffffffffffffffffffffffffffffffffffffff000000000000000000000000809160601b1683527f000000000000000000000000000000000000000000000000000000000000000060601b166034840152604883015260488252608082019180831067ffffffffffffffff84111761037e576bffffffffffffffffffffffff92604052519020169056fea264697066735822122007e02d6bc6a2534a3e464491617058f3405dc92cdb96be09e8deece4731ec84464736f6c63430008170033608080604052346100895763409feecd198054906001821661007c576001600160401b039160011c6002600160401b031901610042575b6123d8838161008e8239f35b6002600160411b03905560209081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29080a15f80610036565b63f92ee8a95f526004601cfd5b5f80fdfe60806040526004361015610015575b3661142857005b5f3560e01c80630d1bf5d31461010f57806310b0a63d146100e7578063112d3a7d1461010a5780631195e07e146101055780631626ba7e1461010057806319822f7c146100fb57806346d09cea146100f65780637833fa04146100f15780639517e29f146100ec5780639cfd7cff146100e7578063a71763a8146100e2578063d03c7914146100dd578063d691c964146100d8578063e8eb3cc6146100d3578063e9ae5c53146100ce5763f2dc691d0361000e57610f05565b610ec8565b610e80565b610dac565b610d22565b610c03565b6104fe565b610a11565b610988565b61090d565b610781565b610651565b610601565b6105db565b610171565b73ffffffffffffffffffffffffffffffffffffffff81160361013257565b5f80fd5b359061014182610114565b565b9181601f840112156101325782359167ffffffffffffffff8311610132576020838186019501011161013257565b346101325760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760048035906101ae82610114565b67ffffffffffffffff602435818111610132576101ce9036908401610143565b939092604435926101de84610114565b606435908111610132576101f59036908301610143565b9290917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf6011329687548060038a55610377575b5073ffffffffffffffffffffffffffffffffffffffff9283811690811561034e5761028a9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b803b15610132576102cc975f80946040519a8b95869485937f6d61fe700000000000000000000000000000000000000000000000000000000085528401610fbd565b03925af1948515610349578695610330575b50831661031f575b5050506102ef57005b6002905560016020527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602080a1005b610328926114ef565b5f80806102e6565b8061033d610343926103d6565b8061039f565b5f6102de565b610fce565b836040517f682a6e7c000000000000000000000000000000000000000000000000000000008152fd5b600181819a939a1c14303b10156103935760ff1b1b965f610226565b8263f92ee8a95f52601cfd5b5f91031261013257565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff81116103ea57604052565b6103a9565b6040810190811067ffffffffffffffff8211176103ea57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176103ea57604052565b67ffffffffffffffff81116103ea57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b5f5b8381106104975750505f910152565b8181015183820152602001610488565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f6020936104e381518092818752878088019101610486565b0116010190565b9060206104fb9281815201906104a7565b90565b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325761057960405161053b816103ef565b601281527f657468617572612e617572612e302e312e30000000000000000000000000000060208201526040519182916020835260208301906104a7565b0390f35b60607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc82011261013257600435916024356105b781610114565b916044359067ffffffffffffffff8211610132576105d791600401610143565b9091565b346101325760206105f76105ee3661057d565b92919091611066565b6040519015158152f35b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff5f5416604051908152f35b346101325760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760243567ffffffffffffffff81116101325760206106a5610709923690600401610143565b73ffffffffffffffffffffffffffffffffffffffff5f5416906040518095819482937ff551e2ee0000000000000000000000000000000000000000000000000000000084523360048501526004356024850152606060448501526064840191610f7f565b03915afa801561034957610579915f91610752575b506040517fffffffff0000000000000000000000000000000000000000000000000000000090911681529081906020820190565b610774915060203d60201161077a575b61076c818361040b565b810190611198565b5f61071e565b503d610762565b34610132577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc606081360112610132576004359067ffffffffffffffff821161013257610120908236030112610132576044356f71727de22e5e9d8baf0edac6f37da03233036108e357602061086c5f9361082f610816610816875473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b906040519586809481937f97003203000000000000000000000000000000000000000000000000000000008352602435906004016004840161120c565b03925af190811561034957610579925f926108b2575b508061089a575b506040519081529081906020820190565b5f80808093335af1506108ab61131a565b505f610889565b6108d591925060203d6020116108dc575b6108cd818361040b565b8101906111ad565b905f610882565b503d6108c3565b60046040517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b7fffffffff0000000000000000000000000000000000000000000000000000000081160361013257565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610132577fffffffff000000000000000000000000000000000000000000000000000000006004356109e48161095e565b165f526002602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b610a1a3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610bf9575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610ab2577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610a8a9184611869565b6040805191825273ffffffffffffffffffffffffffffffffffffffff929092166020820152a1005b60028403610aec577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae79184611746565b610a8a565b60038403610b21577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846115d2565b60048403610b56577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846114ef565b6040517f41c38b3000000000000000000000000000000000000000000000000000000000815260048101859052602490fd5b6040517fb927fe5e00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff84166004820152602490fd5b60046040517fa2e5c09a000000000000000000000000000000000000000000000000000000008152fd5b5030331415610a35565b610c0c3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610d18575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610c795760046040517fa23dd761000000000000000000000000000000000000000000000000000000008152fd5b60028403610cae577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610a8a9184611c1c565b60038403610ce3577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae79184611abc565b60048403610b56577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae791846119f5565b5030331415610c27565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206105f7600435611349565b9060407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc83011261013257600435916024359067ffffffffffffffff8211610132576105d791600401610143565b610db536610d5e565b91335f526001926020926001845260ff60405f20541615610e5657610dd992611dce565b9160405191808301818452845180915260408401918060408360051b8701019601925f905b838210610e0b5786880387f35b90919293948380610e458a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08b869d0301865289516104a7565b999701959493919091019101610dfe565b60046040517fc51267d8000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206040516f71727de22e5e9d8baf0edac6f37da0328152f35b610ed136610d5e565b906f71727de22e5e9d8baf0edac6f37da03233141580610efb575b610bcf57610ef992611dce565b005b5030331415610eec565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602060043560018114908115610f74575b8115610f69575b8115610f5e575b506040519015158152f35b60049150145f610f53565b600381149150610f4c565b600281149150610f45565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b9160206104fb938181520191610f7f565b6040513d5f823e3d90fd5b906004116101325790600490565b909291928360041161013257831161013257600401917ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0190565b7fffffffff00000000000000000000000000000000000000000000000000000000903581811693926004811061105757505050565b60040360031b82901b16169150565b90929190600181036110b1575050506110935f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff90811691161490565b600281036110ee575050506110e76104fb9173ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b5460ff1690565b6003810361116a57506004821015611107575050505f90565b61112061111a6110939361115093610fd9565b90611022565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b5473ffffffffffffffffffffffffffffffffffffffff1690565b9050600491501461117a57505f90565b60035473ffffffffffffffffffffffffffffffffffffffff16611093565b9081602091031261013257516104fb8161095e565b90816020910312610132575190565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18236030181121561013257016020813591019167ffffffffffffffff821161013257813603831361013257565b929190611315611276602092604087526112466040880161122c83610136565b73ffffffffffffffffffffffffffffffffffffffff169052565b83810135606088015261130561125f60408301836111bc565b9390610120948560808c01526101608b0191610f7f565b916112fc6112bd61128a60608401846111bc565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc096918d60a08982860301910152610f7f565b608083013560c08c015260a083013560e08c01528a6101009660c0850135888301526112ec60e08601866111bc565b9290918882860301910152610f7f565b938101906111bc565b9188840301610140890152610f7f565b930152565b3d15611344573d9061132b8261044c565b91611339604051938461040b565b82523d5f602084013e565b606090565b61135d818060081b918160301b9160501b90565b50507fff0000000000000000000000000000000000000000000000000000000000000080921680159081156113fe575b81156113d4575b5061139f5750505f90565b1680159081156113ad575090565b7f010000000000000000000000000000000000000000000000000000000000000091501490565b7ffe000000000000000000000000000000000000000000000000000000000000009150145f611394565b7f01000000000000000000000000000000000000000000000000000000000000008114915061138d565b7fffffffff000000000000000000000000000000000000000000000000000000005f35165f52600260205273ffffffffffffffffffffffffffffffffffffffff60405f2054168015611491575f8091368280378136915af43d5f803e1561148d573d5ff35b3d5ffd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f4e6f2066616c6c6261636b2068616e646c6572000000000000000000000000006044820152fd5b929160035473ffffffffffffffffffffffffffffffffffffffff9485821661159f57947fffffffffffffffffffffffff00000000000000000000000000000000000000009495169384911617600355823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b03925af18015610349576115925750565b8061033d610141926103d6565b60249086604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b90916115ec90806115e661111a8287610fd9565b94610fe7565b92909173ffffffffffffffffffffffffffffffffffffffff918261163d611150837fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b166116ff578161167a6116ba927fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055565b1691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f5c426a4200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602490fd5b60ff6117708273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541661182257806117b473ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008254161790551691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b916118885f5473ffffffffffffffffffffffffffffffffffffffff1690565b9273ffffffffffffffffffffffffffffffffffffffff8082169416908482146119b05781611938575b6118f5915073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b813b15610132575f60405180937f8a91b0e300000000000000000000000000000000000000000000000000000000825281838161198360048201604090602081525f60208201520190565b03925af1918215610349576118f59261199d575b506118b1565b8061033d6119aa926103d6565b5f611997565b5050823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b919060035473ffffffffffffffffffffffffffffffffffffffff8094168094821603611a8b577fffffffffffffffffffffffff000000000000000000000000000000000000000016600355823b1561013257611581925f92836040518096819582947f8a91b0e3000000000000000000000000000000000000000000000000000000008452602060048501526024840191610f7f565b602484604051907f026d96390000000000000000000000000000000000000000000000000000000082526004820152fd5b91611ad59080611acf61111a8286610fd9565b93610fe7565b9091611b0e611150827fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b73ffffffffffffffffffffffffffffffffffffffff808616959116859003611bd35750611b68611b90917fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b7fffffffffffffffffffffffff00000000000000000000000000000000000000008154169055565b823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f026d963900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602490fd5b60ff611c468273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541615611cf65780611c8b73ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0081541690551691823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f026d9639000000000000000000000000000000000000000000000000000000008352166004820152fd5b6020818303126101325780519067ffffffffffffffff8211610132570181601f82011215610132578051611d708161044c565b92611d7e604051948561040b565b81845260208284010111610132576104fb9160208085019101610486565b6104fb949273ffffffffffffffffffffffffffffffffffffffff60609316825260208201528160408201520191610f7f565b90929160609073ffffffffffffffffffffffffffffffffffffffff9485611e0a60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e8a575b50611e1c9293611f06565b92611e3c60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e46575050565b803b15610132576115815f929183926040519485809481937f173bf7da000000000000000000000000000000000000000000000000000000008352600483016104ea565b92505f60405180947fd68f6025000000000000000000000000000000000000000000000000000000008252818381611ec88888343360048601611d9c565b03925af1801561034957611e1c935f91611ee4575b5092611e11565b611f0091503d805f833e611ef8818361040b565b810190611d3d565b5f611edd565b600881901b91907fff00000000000000000000000000000000000000000000000000000000000000811680611f41575050916104fb926122de565b7f01000000000000000000000000000000000000000000000000000000000000008103611f745750506104fb92506121e2565b919250907ffe00000000000000000000000000000000000000000000000000000000000000036120165750611fab5f92839261235c565b9094929150611fb861205f565b94816040519283928337810184815203915afa611fd361131a565b82511561201157602083015215611fe75790565b60046040517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b612106565b602490604051907fba70d2310000000000000000000000000000000000000000000000000000000082526004820152fd5b67ffffffffffffffff81116103ea5760051b60200190565b60405161206b816103ef565b60018152805f5b60208082101561208d57906060602092828501015201612072565b50505090565b9061209d82612047565b6120aa604051918261040b565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06120d88294612047565b01905f5b8281106120e857505050565b8060606020809385010152016120dc565b908092918237015f815290565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b80518210156120115760209160051b010190565b91908110156120115760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa181360301821215610132570190565b356104fb81610114565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610132570180359067ffffffffffffffff82116101325760200191813603831361013257565b91909180350191602092602081019035916121fc83612093565b945f5b84811061220d575050505050565b61222061221b828787612147565b612187565b5f808461222e858a8a612147565b01359261223c858a8a612147565b9361224c60409586810190612191565b919061225c8751809481936120f9565b03925af161226861131a565b612272848b612133565b527fff0000000000000000000000000000000000000000000000000000000000000085161590816122d5575b506122ac57506001016121ff565b600490517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b9050155f61229e565b6122ec5f949392859261235c565b909692916122f861205f565b97826040519384928337810185815203925af19061231461131a565b90845115612011577fff0000000000000000000000000000000000000000000000000000000000000091602086015216159081612353575b50611fe757565b9050155f61234c565b908060141161013257813560601c9281603411610132577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffcc60346014850135940192019056fea2646970667358221220ea0107d2fc265c63c0a8bd55e9d1937ca16a9aea1ea523588fa7e4f3af23626764736f6c63430008170033000000000000000000000000000000b07799b322d076669ef32b247d02279c7e" }, { "transactionType": "CREATE", - "contractName": "P256Account", - "address": "0xcd506c3632c03817b1b938e6440558da8d2ea328", - "initCode": "0x60a0346200017e576001600160401b0390601f620041f738819003918201601f19168301918483118484101762000182578084926020946040528339810103126200017e57516001600160a01b03919082811681036200017e57331562000166575f543360018060a01b03198216175f55604051933391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a360805263409feecd198054600181166200015957829060011c036200011e575b614060838162000197823960805181818161024501528181610407015281816105780152818161075f01528181610825015281816109b60152818161196901528181611a6a01528181611b9001528181611e28015281816120d10152612a560152f35b6002600160411b03905560209081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29080a15f80620000bb565b63f92ee8a95f526004601cfd5b604051631e4fbdf760e01b81525f6004820152602490fd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe608060409080825260049182361015610022575b5050503615610020575f80fd5b005b5f915f3560e01c908163011cfdbc14612a0e5750806304802c9a1461295c5780630633b14a146128f55780630665f04b146127fb5780630904b9ed1461275157806309b7026e1461271057806310de2676146125bd578063113647bb14612400578063136dd7551461238b5780631506ac5a146123505780631626ba7e146121b857806319822f7c146120645780631d32c2c314611f925780632cdeb30c14611cf257806336aea01e14611cb657806347e1da2a14611aef5780634a58db1914611a275780634d44560d1461191657806350113dc31461181257806356570571146113765780635ec005fa146113095780636a1ce0031461125b5780637140415614611011578063715018a614610f755780638382c10114610d5a5780638da5cb5b14610d0a5780639dff1bc414610c94578063a526d83b14610b47578063ad605f9314610a5c578063affed0e014610a20578063b61d27f614610942578063b6474f9214610906578063b80ae261146107db578063c399ec88146106e6578063c56ce32f1461052e578063cad0e95e146104c0578063d5af4e2014610484578063e1f950c51461042b578063e8eb3cc6146103bd578063ed894cd314610381578063f2fde38b146102e65763fac7932e0361001357346102e25760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2578235906024359073ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633036102ba57821580156102b2575b61028a5750906102879160443591613c24565b80f35b8490517f9c02fde3000000000000000000000000000000000000000000000000000000008152fd5b508115610274565b8490517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b5080fd5b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5761031f612bbc565b91610328613e66565b73ffffffffffffffffffffffffffffffffffffffff83161561034e578361028784613d65565b908360249251917f1e4fbdf7000000000000000000000000000000000000000000000000000000008352820152fd5b8280fd5b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020906008549051908152f35b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020905173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5090346104815760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261048157823592548310156104815750610473602092612c94565b91905490519160031b1c8152f35b80fd5b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020906007549051908152f35b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d573560095481101561037d576020925060095f527f6e1540171b6c0c960b71a7020d9f60077f6af931a8bbf590da0223dacf75c7af01549051908152f35b5082903461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5773ffffffffffffffffffffffffffffffffffffffff91827f00000000000000000000000000000000000000000000000000000000000000001633036106bf576002549160ff8316610662578154156106055750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001176002558154167f4b31ed05a4d8ce9de4f9d476a272179c7af15d7fddd9adaaea416ab9e799edeb8280a280f35b90602060649251917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601f60248201527f41637469766520706173736b657920726571756972656420666f7220324641006044820152fd5b90602060649251917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601360248201527f32464120616c726561647920656e61626c6564000000000000000000000000006044820152fd5b90517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b5082903461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d578051917f70a08231000000000000000000000000000000000000000000000000000000008352309083015260208260248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9182156107d1578392610799575b6020838351908152f35b9091506020813d6020116107c9575b816107b560209383612ddc565b8101031261037d576020925051908361078f565b3d91506107a8565b81513d85823e3d90fd5b5082903461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5773ffffffffffffffffffffffffffffffffffffffff91827f00000000000000000000000000000000000000000000000000000000000000001633036106bf576002549160ff8316156108a95750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166002558154167f53c421f8b75959dde39c41de22c62b6a71338810b36a9fd9c7ba5c010e2fa1e38280a280f35b90602060649251917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601460248201527f32464120616c72656164792064697361626c65640000000000000000000000006044820152fd5b50823461037d57827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5760209250549051908152f35b5082903461037d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5761097c612bbc565b9160443567ffffffffffffffff8111610a1c5761099c9036908301612c02565b91909273ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633036109f65785610287866109ec368789612f75565b9060243590613dcf565b517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b8480fd5b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2576020906001549051908152f35b5091346102e25760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e2578035908054821015610aea57509180610aa760a094612c94565b90549060031b1c9283815260036020522080549160018201549060ff60036002850154940154169381519586526020860152840152606083015215156080820152f35b60649060208551917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601360248201527f496e646578206f7574206f6620626f756e6473000000000000000000000000006044820152fd5b5082903461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d57610b81612bbc565b90303303610c6c5773ffffffffffffffffffffffffffffffffffffffff821692838552600560205260ff8286205416610c45578315610c1e57508284526005602052832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055610bf790612eb0565b7f038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f9698280a280f35b90517faabd5a09000000000000000000000000000000000000000000000000000000008152fd5b90517ffecca77f000000000000000000000000000000000000000000000000000000008152fd5b9050517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b5082903461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d578060a0938335815260036020522080549260018201549260028301549160ff6003850154169301549381519586526020860152840152151560608301526080820152f35b50346102e257817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e25773ffffffffffffffffffffffffffffffffffffffff60209254169051908152f35b5082903461037d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5781359060243590610d9b612bdf565b91338652600560205260ff828720541615610f4d5760075415610f255760085494610dc586612e1d565b600855858752600a6020528287209085825582600183015573ffffffffffffffffffffffffffffffffffffffff60028301951694857fffffffffffffffffffffffff000000000000000000000000000000000000000082541617905560016003830155335f52808201602052835f2060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0082541617905562015180420190814211610ef95750906006916005820155017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000815416905581519384526020840152820152817ff1b0340f9d1d7162a64944ed64fb8166ac5d84071763a1ddb0c849a09193d64c60603393a333907f75cc0fdddfa6e54a2f04c21b76aa220e6673bd272744ed9848e6305ad53b89ea8380a380f35b8860116024927f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b8482517faabd5a09000000000000000000000000000000000000000000000000000000008152fd5b8482517fef6d0f02000000000000000000000000000000000000000000000000000000008152fd5b823461048157807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261048157610fac613e66565b5f73ffffffffffffffffffffffffffffffffffffffff81547fffffffffffffffffffffffff000000000000000000000000000000000000000081168355167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08280a380f35b5082903461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5761104b612bbc565b3033036112345773ffffffffffffffffffffffffffffffffffffffff80911691828552600560205260ff81862054161561120c57828552600560205284207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008154169055835b600680548083101561120157908391856110ca85612cf6565b949054600395861b1c16146110e4575050506001016110b1565b9194959093927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff928381019081116111d557906111348561112761116094612cf6565b9054908a1b1c1691612cf6565b90919073ffffffffffffffffffffffffffffffffffffffff8084549260031b9316831b921b1916179055565b83549081156111a95750019261117584612cf6565b81939154921b1b19169055555b7fb8107d0c6b40be480ce3172ee66ba6d64b71f6b1685a851340036e6e2e3e3c528280a280f35b8760316024927f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b6024896011857f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b505050509050611182565b8390517f6677f3c8000000000000000000000000000000000000000000000000000000008152fd5b50517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d57358252600a6020908152918190208054600182015460028301546003840154600585015460069095015495519384529583019190915273ffffffffffffffffffffffffffffffffffffffff1660408201526060810193909352608083015260ff808216151560a084015260089190911c16151560c082015260e090f35b50823461037d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d573591600654831015610481575073ffffffffffffffffffffffffffffffffffffffff611367602093612cf6565b92905490519260031b1c168152f35b50823461037d57602091827ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261180e57813590818552600a84528085209360058501549182156117e6576006860194855460ff81166117be5760ff8160081c1661179657600394858901546007541161176e5742106117465760018097817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008094161790555b6116b5575b8754158015906116a9575b1561164757508654928688015491835182810190868252848682015285815261145781612d88565b51902095865f52808352845f205461161f5781876114e887519361147a85612d6c565b8985528685018881528d8a870190428252606088019281845260808901965f88525f52848b528c5f209851895551908801555160028701555115159085019060ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0083541691151516179055565b51910155805490680100000000000000008210156115f357916115708761153a84606098968d7fc645c03b7d3e6cfd2c5b9bae2fe247afa8d49da53a910bf1416646b86033e0129b9997019055612c94565b9091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83549160031b92831b921b1916179055565b82519384528301524290820152a25b6115a273ffffffffffffffffffffffffffffffffffffffff600285015416613d65565b7f351b86967219b1fe72918b6d4fb11cd45d6ef80d0d8cbe71807845366082e7628480a28154910154907ff7c19ce202c69a7e855eea96077c7b37560e363f244b0d7a10f5b014558494618380a380f35b6041907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b5083517f692d285a000000000000000000000000000000000000000000000000000000008152fd5b935050505060025460ff811661165f575b505061157f565b1660025573ffffffffffffffffffffffffffffffffffffffff6002840154167f53c421f8b75959dde39c41de22c62b6a71338810b36a9fd9c7ba5c010e2fa1e38580a28480611658565b5086880154151561142f565b81548015611740577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101908111611714576116f090612c94565b905490861b1c895284845284838a20018181541690558661170f6131c6565b61141f565b60248a6011857f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b50611424565b5090517ff8aef316000000000000000000000000000000000000000000000000000000008152fd5b8284517f8af69cf1000000000000000000000000000000000000000000000000000000008152fd5b5090517f9c432834000000000000000000000000000000000000000000000000000000008152fd5b5090517f3c719eee000000000000000000000000000000000000000000000000000000008152fd5b8490517f8b934514000000000000000000000000000000000000000000000000000000008152fd5b8380fd5b50823461037d57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261037d5791611854611891936024359035613026565b969492936118846118758a93989a9b949b519a60e08c5260e08c0190612c61565b6020958b8203878d0152612c61565b91898303908a0152612c61565b8681036060880152818084519283815201930190845b8181106119025750505085820360808701528080885193848152019701925b8281106118ec578680876118e28b8984820360a0860152612c61565b9060c08301520390f35b83511515885296810196928101926001016118c6565b8251855293830193918301916001016118a7565b50346102e257807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102e25782359073ffffffffffffffffffffffffffffffffffffffff80831680930361180e577f000000000000000000000000000000000000000000000000000000000000000016918233036119ff57938394833b15610a1c576044859283855196879485937f205c287800000000000000000000000000000000000000000000000000000000855284015260243560248401525af19081156119f657506119e65750f35b6119ef90612d2b565b6104815780f35b513d84823e3d90fd5b8482517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b5082905f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5773ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001691823b15611aeb575f9060248351809581937fb760faf9000000000000000000000000000000000000000000000000000000008352309083015234905af1908115611ae25750611ad8575080f35b6100209150612d2b565b513d5f823e3d90fd5b5f80fd5b508234611aeb5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5767ffffffffffffffff8135818111611aeb57611b3f9036908401612c30565b9092602490602435848111611aeb57611b5b9036908301612c30565b9094604435908111611aeb57611b749036908401612c30565b95909473ffffffffffffffffffffffffffffffffffffffff98897f00000000000000000000000000000000000000000000000000000000000000001633036102ba5787821480611cad575b15611c5157505f5b818110611bd057005b611bdb81838b612eda565b358a81168103611aeb57611bf0828686612eda565b3589831015611c26578291611c2091611c1a611c1360019660051b8d018d612eea565b3691612f75565b91613dcf565b01611bc7565b876032887f4e487b71000000000000000000000000000000000000000000000000000000005f52525ffd5b517f08c379a0000000000000000000000000000000000000000000000000000000008152602081860152600f60248201527f4c656e677468206d69736d6174636800000000000000000000000000000000006044820152606490fd5b50838214611bbf565b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb576020906006549051908152f35b508234611aeb5760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb578035602435611d30612bdf565b916064359182151590818403611aeb577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf6011329586548060038955611f63575b5084611ef0575b50957f9f8c60a88a90d9985a9dcdf9cc51a4bfd6b3a7215dca14b0c10d2281a76c791f91869784151580611ee7575b611ed5575b611db287613d65565b73ffffffffffffffffffffffffffffffffffffffff9160ff83891698895f526005602052611e0d865f20917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0092600184825416179055612eb0565b600160075560025416911617600255825194855260208501527f00000000000000000000000000000000000000000000000000000000000000001692a2817f038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f9695f80a2611eab575b50611e7b57005b6002905560016020527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602080a1005b7f4b31ed05a4d8ce9de4f9d476a272179c7af15d7fddd9adaaea416ab9e799edeb5f80a282611e74565b611ee26084358287613c24565b611da9565b50801515611da4565b83151580611f5a575b611d755760649060208951917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601460248201527f32464120726571756972657320706173736b65790000000000000000000000006044820152fd5b50811515611ef9565b9796600189819897981c14303b1015611f865786979860ff9796971b1b96611d6e565b5063f92ee8a95f52601cfd5b508234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5780355f526003602052815f20805491821561200757509182600160809401549160ff6003600284015493015416928151948552602085015283015215156060820152f35b60649060208551917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601660248201527f506173736b657920646f6573206e6f74206578697374000000000000000000006044820152fd5b508234611aeb577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc90606082360112611aeb5780359167ffffffffffffffff8311611aeb57610120908336030112611aeb576044359173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163303612190576121069060243590830161338b565b9180612117575b6020838551908152f35b5f80808093335af1612127613bf5565b5015612133578061210d565b60649060208451917f08c379a0000000000000000000000000000000000000000000000000000000008352820152600e60248201527f50726566756e64206661696c65640000000000000000000000000000000000006044820152fd5b5082517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b838234611aeb57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760243567ffffffffffffffff8111611aeb576122079036908401612c02565b606081036123285761223e935f602095869286518481019135825284815261222e81612da4565b8751928392839251928391612e77565b8101039060025afa1561231e575f5190606011611aeb57828201355f5260038452825f205f9260ff60038301541680612314575b6122f3575b5050505f146122cb577fffffffff000000000000000000000000000000000000000000000000000000007f1626ba7e00000000000000000000000000000000000000000000000000000000915b5191168152f35b7fffffffff000000000000000000000000000000000000000000000000000000005f916122c4565b61230c93506001825492015492868201359135906132f0565b838080612277565b5081541515612272565b82513d5f823e3d90fd5b5050517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760209051620151808152f35b838234611aeb57807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760243573ffffffffffffffffffffffffffffffffffffffff8116809103611aeb5782602093355f52600a8452825f2001905f52825260ff815f20541690519015158152f35b838234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb57813590335f52600560205260ff815f2054161561259557815f52600a602052805f209060058201541561256d57600682015460ff81166125455760081c60ff1661251d57838201335f528060205260ff825f2054166124f5576003939450335f526020525f2060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00825416179055016124cb8154612e1d565b905533907f75cc0fdddfa6e54a2f04c21b76aa220e6673bd272744ed9848e6305ad53b89ea5f80a3005b8482517f6a543dee000000000000000000000000000000000000000000000000000000008152fd5b8390517f9c432834000000000000000000000000000000000000000000000000000000008152fd5b8482517f3c719eee000000000000000000000000000000000000000000000000000000008152fd5b8390517f8b934514000000000000000000000000000000000000000000000000000000008152fd5b9050517fef6d0f02000000000000000000000000000000000000000000000000000000008152fd5b508234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb578035913033036126ea57825f52600a602052805f206005810154156126c3576006019182549160ff831661269d5760ff8360081c166126775750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790557f02019f620969ef34023e1048f0b0f0e1c337f94acda5cfad6aaceb56989674555f80a2005b517f9c432834000000000000000000000000000000000000000000000000000000008152fd5b517f3c719eee000000000000000000000000000000000000000000000000000000008152fd5b50517f8b934514000000000000000000000000000000000000000000000000000000008152fd5b517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760209060ff6002541690519015158152f35b838234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb578135913033036127d457821580156127c9575b610c1e577f4ff5b0bd81d83bbabe0f0bfaceb9711047a031ba8563e95bbffe3b094a3cffd0602084848160075551908152a1005b506006548311612795565b90517f1753fe1a000000000000000000000000000000000000000000000000000000008152fd5b5034611aeb575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5780519081600654908181526020809101809260065f527ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f905f5b8181106128cb575050508461287a910385612ddc565b825181815293518185018190528493840192915f5b82811061289e57505050500390f35b835173ffffffffffffffffffffffffffffffffffffffff168552869550938101939281019260010161288f565b825473ffffffffffffffffffffffffffffffffffffffff1684529284019260019283019201612864565b5034611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5760209073ffffffffffffffffffffffffffffffffffffffff612945612bbc565b165f526005825260ff815f20541690519015158152f35b508234611aeb5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb57355f908152600a6020908152908290208054600182015460028301546003840154600585015460069095015496519384529483019190915273ffffffffffffffffffffffffffffffffffffffff1660408201526060810192909252608082015260ff808316151560a083015260089290921c909116151560c082015260e090f35b82859134611aeb57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112611aeb5773ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163303612b955750805160208101908335825260243583820152828152612a9b81612d88565b51902091825f52600360205260ff6003835f2001541615612b6e5760018154111580612b62575b612b3b57507fc8896d5eff3687c3973d1b15100fdf1a4453cd98c445d826c855740a6bcc51e090825f526003602052805f20600381017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008154169055612b278461323c565b6001815491015482519182526020820152a2005b90517f53f56588000000000000000000000000000000000000000000000000000000008152fd5b5060ff60025416612ac2565b90517f2e329a08000000000000000000000000000000000000000000000000000000008152fd5b90507fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b6004359073ffffffffffffffffffffffffffffffffffffffff82168203611aeb57565b6044359073ffffffffffffffffffffffffffffffffffffffff82168203611aeb57565b9181601f84011215611aeb5782359167ffffffffffffffff8311611aeb5760208381860195010111611aeb57565b9181601f84011215611aeb5782359167ffffffffffffffff8311611aeb576020808501948460051b010111611aeb57565b9081518082526020808093019301915f5b828110612c80575050505090565b835185529381019392810192600101612c72565b600454811015612cc95760045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b01905f90565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b600654811015612cc95760065f527ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f01905f90565b67ffffffffffffffff8111612d3f57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b60a0810190811067ffffffffffffffff821117612d3f57604052565b6060810190811067ffffffffffffffff821117612d3f57604052565b6040810190811067ffffffffffffffff821117612d3f57604052565b6020810190811067ffffffffffffffff821117612d3f57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117612d3f57604052565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114612e4a5760010190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f5b838110612e885750505f910152565b8181015183820152602001612e79565b90939293848311611aeb578411611aeb578101920390565b6006549068010000000000000000821015612d3f57611134826001612ed89401600655612cf6565b565b9190811015612cc95760051b0190565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215611aeb570180359067ffffffffffffffff8211611aeb57602001918136038313611aeb57565b67ffffffffffffffff8111612d3f57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b929192612f8182612f3b565b91612f8f6040519384612ddc565b829481845281830111611aeb578281602093845f960137010152565b67ffffffffffffffff8111612d3f5760051b60200190565b90612fcd82612fab565b612fda6040519182612ddc565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06130088294612fab565b0190602036910137565b8051821015612cc95760209160051b010190565b9160048054928385101561315b5760328111613153575b84840390848211612e4a578082101561314c57505b61305b81612fc3565b9461306582612fc3565b9461306f83612fc3565b9461307984612fc3565b9461308385612fc3565b9461308d81612fc3565b945f5b82811061309d5750505050565b8082018083116131205790848d6130b5600194612c94565b9054600391821b1c90815f526020526130d28460405f2093613012565b528d6130e084835492613012565b52838d6130f1858385015492613012565b526002820154613101858f613012565b5261310c848d613012565b520154613119828a613012565b5201613090565b6011857f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b9050613052565b50603261303d565b505060408051935061316c84612dc0565b5f845280519361317b85612dc0565b5f855281519361318a85612dc0565b5f855282519361319985612dc0565b5f85528351936131a885612dc0565b5f855251926131b684612dc0565b5f84525f36813796959493929190565b600454801561320f577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff809101906131fd82612c94565b909182549160031b1b19169055600455565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603160045260245ffd5b6004906004545f5b8181106132515750505050565b8261325b82612c94565b919054600392831b1c146132725750600101613244565b9250927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82019182116132c457506132bc92916132b161153a92612c94565b9054911b1c91612c94565b612ed86131c6565b6011907f4e487b71000000000000000000000000000000000000000000000000000000005f525260245ffd5b92909391936040519384526020840152836040840152606083015260808201525f805260205f60a0836101005afa503d15613353575b507f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a85f5160011491111090565b6d1ab2e8006fd8b71907bf06a5bdee3b613326575f60a06020926dd01ea45f9efd5c54f037fa57ea1a5afa15613389575f613326565bfe5b613399610100820182612eea565b90925f925f94600454158015613bb0575b5060ff600254169573ffffffffffffffffffffffffffffffffffffffff95865f54169760b86133dc6040870187612eea565b9190911015613b8c575b508115908115613b83575b50613b5c5760e08610613b4f577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf860195808711612e4a5761343581888187612e98565b959097827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9f810111612e4a5761348f907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9f84018488612e98565b9590359560208110613b1b575b506040519260c0840184811067ffffffffffffffff821117612d3f5760405260608452606060208501525f60408501525f60608501525f60808501525f60a085015260467fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9f82011015613a03575b5050604051948760208701526020865261352386612da4565b60b86135345f936040810190612eea565b905010155f146137875750505f9084606095805191826136aa575b50505060208101518051604083015160608401519088519189600d84017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c825283870190602081601389600d898b0101106022602d8b880101515f1a141695012092012014169184826014011090821760801c109060207f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918701015160581c141616975282519687519060058060218b015116149060208311161697889384613672575b50505050613654575b505050505b1561364a57839261363892613eb6565b9216911603613645575f90565b600190565b5050505050600190565b613669945060a06080820151910151916132f0565b5f808080613623565b60208080959850846001959750840101809882808084519a019601940160025afa831b5afa5192523d1561338957845f80808061361a565b90919650600380600289010460021b91604051987f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f7f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5260208b01858c0194602084818801990101926004828551975f87525b0193828551818160121c16515f538181600c1c1651600153818160061c1651600253165184535f518152019289841015613762576004908390613724565b50505095505f93600393604092520160405206600204809303520385525f808061354f565b9193509391505f526003918260205260405f2060ff8482015416806139f9575b6137b5575b50505050613628565b8091929394505491600180920154935f9281906060978351908161390d575b505050505060208101518051604083015160608401519088519189600d84017f226368616c6c656e6765223a220000000000000000000000000000000000000060981c825283870190602081601389600d898b0101106022602d8b880101515f1a141695012092012014169184826014011090821760801c109060207f2274797065223a22776562617574686e2e67657422000000000000000000000060581c918701015160581c141616975282519687519060058060218b0151161490602083111616978893846138d5575b505050506138b7575b505050505f8080806137ac565b6138cc945060a06080820151910151916132f0565b5f8080806138aa565b60208080959850846001959750840101809882808084519a019601940160025afa831b5afa5192523d1561338957845f8080806138a1565b81929394995060028192010460021b91604051997f4142434445464748494a4b4c4d4e4f505152535455565758595a616263646566601f52603f957f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f603f5260208c0196858d019260208581860192010191825193895f85525b6139b2575b5050505f9596509060409291520160405206600204809303520385525f808080806137d4565b87600491019a828c51818160121c16515f538181600c1c16518d53818160061c1651600253165189535f5181520198828a10156139f157989989613987565b899a5061398c565b50805415156137a7565b8101813560f01c80830192600284017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b84019283821115613a48575b5050505061350a565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f95613a9b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff59926002613aa69601614009565b895285030190614009565b60208601523560f01c60408501527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5d81013560f01c60608501527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5f8101356080850152013560a08301525f8080808080613a3f565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060209792970360031b1b16945f61349c565b5050505050505050600190565b50509290506041810361364a578392613b7492613eb6565b92169116145f14613645575f90565b9050155f6133f1565b9850505050601886013560388701359060988760588a01351698013515155f6133e6565b9194509450612cc9577f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b545f52600360205260405f209260018454940154945f6133aa565b3d15613c1f573d90613c0682612f3b565b91613c146040519384612ddc565b82523d5f602084013e565b606090565b604090815160208101908282528484820152838152613c4281612d88565b51902093845f526003602052825f2054613d3c576004835191613c6483612d6c565b838352613cd960208401878152868501428152606086019160018352608087019485528a5f526003602052885f2096518755516001870155516002860155511515600385019060ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0083541691151516179055565b519101556004549168010000000000000000831015612d3f577fc645c03b7d3e6cfd2c5b9bae2fe247afa8d49da53a910bf1416646b86033e01293613d2a8661153a86600160609801600455612c94565b815192835260208301524290820152a2565b600483517f692d285a000000000000000000000000000000000000000000000000000000008152fd5b5f549073ffffffffffffffffffffffffffffffffffffffff80911691827fffffffffffffffffffffffff00000000000000000000000000000000000000008216175f55167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b915f928392602083519301915af1613de5613bf5565b9015613dee5750565b6044601f917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06040519384927fa5fa8d2b00000000000000000000000000000000000000000000000000000000845260206004850152613e5d8151809281602488015260208888019101612e77565b01168101030190fd5b73ffffffffffffffffffffffffffffffffffffffff5f54163303613e8657565b60246040517f118cdaa7000000000000000000000000000000000000000000000000000000008152336004820152fd5b9091604103613fab576020820135604092838101355f1a7f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08311613f7757601b81141580613fa0575b613f7757925f926080926020958751938452868401523586830152606082015282805260015afa15611ae2575f519073ffffffffffffffffffffffffffffffffffffffff821615613f4e575090565b600490517f38a85a8d000000000000000000000000000000000000000000000000000000008152fd5b600485517f38a85a8d000000000000000000000000000000000000000000000000000000008152fd5b50601c811415613eff565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601860248201527f496e76616c6964207369676e6174757265206c656e67746800000000000000006044820152fd5b6040909291928382519485926020840137808252015f60208201520160405256fea2646970667358221220034579a9babad05cc114a63f72473ca5e7c9cf8137e450b073cc4a56e90486d864736f6c634300081700330000000000000000000000000000000071727de22e5e9d8baf0edac6f37da032" + "contractName": "AuraAccount", + "address": "0x565cf8324c9d856a540b3873701242bf27d82307", + "initCode": "0x608080604052346100895763409feecd198054906001821661007c576001600160401b039160011c6002600160401b031901610042575b6123d8838161008e8239f35b6002600160411b03905560209081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29080a15f80610036565b63f92ee8a95f526004601cfd5b5f80fdfe60806040526004361015610015575b3661142857005b5f3560e01c80630d1bf5d31461010f57806310b0a63d146100e7578063112d3a7d1461010a5780631195e07e146101055780631626ba7e1461010057806319822f7c146100fb57806346d09cea146100f65780637833fa04146100f15780639517e29f146100ec5780639cfd7cff146100e7578063a71763a8146100e2578063d03c7914146100dd578063d691c964146100d8578063e8eb3cc6146100d3578063e9ae5c53146100ce5763f2dc691d0361000e57610f05565b610ec8565b610e80565b610dac565b610d22565b610c03565b6104fe565b610a11565b610988565b61090d565b610781565b610651565b610601565b6105db565b610171565b73ffffffffffffffffffffffffffffffffffffffff81160361013257565b5f80fd5b359061014182610114565b565b9181601f840112156101325782359167ffffffffffffffff8311610132576020838186019501011161013257565b346101325760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760048035906101ae82610114565b67ffffffffffffffff602435818111610132576101ce9036908401610143565b939092604435926101de84610114565b606435908111610132576101f59036908301610143565b9290917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffbf6011329687548060038a55610377575b5073ffffffffffffffffffffffffffffffffffffffff9283811690811561034e5761028a9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b803b15610132576102cc975f80946040519a8b95869485937f6d61fe700000000000000000000000000000000000000000000000000000000085528401610fbd565b03925af1948515610349578695610330575b50831661031f575b5050506102ef57005b6002905560016020527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602080a1005b610328926114ef565b5f80806102e6565b8061033d610343926103d6565b8061039f565b5f6102de565b610fce565b836040517f682a6e7c000000000000000000000000000000000000000000000000000000008152fd5b600181819a939a1c14303b10156103935760ff1b1b965f610226565b8263f92ee8a95f52601cfd5b5f91031261013257565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff81116103ea57604052565b6103a9565b6040810190811067ffffffffffffffff8211176103ea57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176103ea57604052565b67ffffffffffffffff81116103ea57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b5f5b8381106104975750505f910152565b8181015183820152602001610488565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f6020936104e381518092818752878088019101610486565b0116010190565b9060206104fb9281815201906104a7565b90565b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325761057960405161053b816103ef565b601281527f657468617572612e617572612e302e312e30000000000000000000000000000060208201526040519182916020835260208301906104a7565b0390f35b60607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc82011261013257600435916024356105b781610114565b916044359067ffffffffffffffff8211610132576105d791600401610143565b9091565b346101325760206105f76105ee3661057d565b92919091611066565b6040519015158152f35b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff5f5416604051908152f35b346101325760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760243567ffffffffffffffff81116101325760206106a5610709923690600401610143565b73ffffffffffffffffffffffffffffffffffffffff5f5416906040518095819482937ff551e2ee0000000000000000000000000000000000000000000000000000000084523360048501526004356024850152606060448501526064840191610f7f565b03915afa801561034957610579915f91610752575b506040517fffffffff0000000000000000000000000000000000000000000000000000000090911681529081906020820190565b610774915060203d60201161077a575b61076c818361040b565b810190611198565b5f61071e565b503d610762565b34610132577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc606081360112610132576004359067ffffffffffffffff821161013257610120908236030112610132576044356f71727de22e5e9d8baf0edac6f37da03233036108e357602061086c5f9361082f610816610816875473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b906040519586809481937f97003203000000000000000000000000000000000000000000000000000000008352602435906004016004840161120c565b03925af190811561034957610579925f926108b2575b508061089a575b506040519081529081906020820190565b5f80808093335af1506108ab61131a565b505f610889565b6108d591925060203d6020116108dc575b6108cd818361040b565b8101906111ad565b905f610882565b503d6108c3565b60046040517fbd07c551000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b7fffffffff0000000000000000000000000000000000000000000000000000000081160361013257565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610132577fffffffff000000000000000000000000000000000000000000000000000000006004356109e48161095e565b165f526002602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b610a1a3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610bf9575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610ab2577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610a8a9184611869565b6040805191825273ffffffffffffffffffffffffffffffffffffffff929092166020820152a1005b60028403610aec577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae79184611746565b610a8a565b60038403610b21577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846115d2565b60048403610b56577fd21d0b289f126c4b473ea641963e766833c2f13866e4ff480abd787c100ef1239391610ae791846114ef565b6040517f41c38b3000000000000000000000000000000000000000000000000000000000815260048101859052602490fd5b6040517fb927fe5e00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff84166004820152602490fd5b60046040517fa2e5c09a000000000000000000000000000000000000000000000000000000008152fd5b5030331415610a35565b610c0c3661057d565b906f71727de22e5e9d8baf0edac6f37da03233141580610d18575b610bcf5773ffffffffffffffffffffffffffffffffffffffff831615610b885760018403610c795760046040517fa23dd761000000000000000000000000000000000000000000000000000000008152fd5b60028403610cae577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610a8a9184611c1c565b60038403610ce3577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae79184611abc565b60048403610b56577f341347516a9de374859dfda710fa4828b2d48cb57d4fbe4c1149612b8e02276e9391610ae791846119f5565b5030331415610c27565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206105f7600435611349565b9060407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc83011261013257600435916024359067ffffffffffffffff8211610132576105d791600401610143565b610db536610d5e565b91335f526001926020926001845260ff60405f20541615610e5657610dd992611dce565b9160405191808301818452845180915260408401918060408360051b8701019601925f905b838210610e0b5786880387f35b90919293948380610e458a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08b869d0301865289516104a7565b999701959493919091019101610dfe565b60046040517fc51267d8000000000000000000000000000000000000000000000000000000008152fd5b34610132575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101325760206040516f71727de22e5e9d8baf0edac6f37da0328152f35b610ed136610d5e565b906f71727de22e5e9d8baf0edac6f37da03233141580610efb575b610bcf57610ef992611dce565b005b5030331415610eec565b346101325760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013257602060043560018114908115610f74575b8115610f69575b8115610f5e575b506040519015158152f35b60049150145f610f53565b600381149150610f4c565b600281149150610f45565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b9160206104fb938181520191610f7f565b6040513d5f823e3d90fd5b906004116101325790600490565b909291928360041161013257831161013257600401917ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0190565b7fffffffff00000000000000000000000000000000000000000000000000000000903581811693926004811061105757505050565b60040360031b82901b16169150565b90929190600181036110b1575050506110935f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff90811691161490565b600281036110ee575050506110e76104fb9173ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b5460ff1690565b6003810361116a57506004821015611107575050505f90565b61112061111a6110939361115093610fd9565b90611022565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b5473ffffffffffffffffffffffffffffffffffffffff1690565b9050600491501461117a57505f90565b60035473ffffffffffffffffffffffffffffffffffffffff16611093565b9081602091031261013257516104fb8161095e565b90816020910312610132575190565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18236030181121561013257016020813591019167ffffffffffffffff821161013257813603831361013257565b929190611315611276602092604087526112466040880161122c83610136565b73ffffffffffffffffffffffffffffffffffffffff169052565b83810135606088015261130561125f60408301836111bc565b9390610120948560808c01526101608b0191610f7f565b916112fc6112bd61128a60608401846111bc565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc096918d60a08982860301910152610f7f565b608083013560c08c015260a083013560e08c01528a6101009660c0850135888301526112ec60e08601866111bc565b9290918882860301910152610f7f565b938101906111bc565b9188840301610140890152610f7f565b930152565b3d15611344573d9061132b8261044c565b91611339604051938461040b565b82523d5f602084013e565b606090565b61135d818060081b918160301b9160501b90565b50507fff0000000000000000000000000000000000000000000000000000000000000080921680159081156113fe575b81156113d4575b5061139f5750505f90565b1680159081156113ad575090565b7f010000000000000000000000000000000000000000000000000000000000000091501490565b7ffe000000000000000000000000000000000000000000000000000000000000009150145f611394565b7f01000000000000000000000000000000000000000000000000000000000000008114915061138d565b7fffffffff000000000000000000000000000000000000000000000000000000005f35165f52600260205273ffffffffffffffffffffffffffffffffffffffff60405f2054168015611491575f8091368280378136915af43d5f803e1561148d573d5ff35b3d5ffd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f4e6f2066616c6c6261636b2068616e646c6572000000000000000000000000006044820152fd5b929160035473ffffffffffffffffffffffffffffffffffffffff9485821661159f57947fffffffffffffffffffffffff00000000000000000000000000000000000000009495169384911617600355823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b03925af18015610349576115925750565b8061033d610141926103d6565b60249086604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b90916115ec90806115e661111a8287610fd9565b94610fe7565b92909173ffffffffffffffffffffffffffffffffffffffff918261163d611150837fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b166116ff578161167a6116ba927fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b9073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055565b1691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f5c426a4200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602490fd5b60ff6117708273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541661182257806117b473ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008254161790551691823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f5c426a42000000000000000000000000000000000000000000000000000000008352166004820152fd5b916118885f5473ffffffffffffffffffffffffffffffffffffffff1690565b9273ffffffffffffffffffffffffffffffffffffffff8082169416908482146119b05781611938575b6118f5915073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000005f5416175f55565b823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b813b15610132575f60405180937f8a91b0e300000000000000000000000000000000000000000000000000000000825281838161198360048201604090602081525f60208201520190565b03925af1918215610349576118f59261199d575b506118b1565b8061033d6119aa926103d6565b5f611997565b5050823b1561013257611581925f92836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260048401610fbd565b919060035473ffffffffffffffffffffffffffffffffffffffff8094168094821603611a8b577fffffffffffffffffffffffff000000000000000000000000000000000000000016600355823b1561013257611581925f92836040518096819582947f8a91b0e3000000000000000000000000000000000000000000000000000000008452602060048501526024840191610f7f565b602484604051907f026d96390000000000000000000000000000000000000000000000000000000082526004820152fd5b91611ad59080611acf61111a8286610fd9565b93610fe7565b9091611b0e611150827fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b73ffffffffffffffffffffffffffffffffffffffff808616959116859003611bd35750611b68611b90917fffffffff00000000000000000000000000000000000000000000000000000000165f52600260205260405f2090565b7fffffffffffffffffffffffff00000000000000000000000000000000000000008154169055565b823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b6040517f026d963900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602490fd5b60ff611c468273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b541615611cf65780611c8b73ffffffffffffffffffffffffffffffffffffffff9273ffffffffffffffffffffffffffffffffffffffff165f52600160205260405f2090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0081541690551691823b1561013257611581925f92836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260048401610fbd565b60249073ffffffffffffffffffffffffffffffffffffffff604051917f026d9639000000000000000000000000000000000000000000000000000000008352166004820152fd5b6020818303126101325780519067ffffffffffffffff8211610132570181601f82011215610132578051611d708161044c565b92611d7e604051948561040b565b81845260208284010111610132576104fb9160208085019101610486565b6104fb949273ffffffffffffffffffffffffffffffffffffffff60609316825260208201528160408201520191610f7f565b90929160609073ffffffffffffffffffffffffffffffffffffffff9485611e0a60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e8a575b50611e1c9293611f06565b92611e3c60035473ffffffffffffffffffffffffffffffffffffffff1690565b1680611e46575050565b803b15610132576115815f929183926040519485809481937f173bf7da000000000000000000000000000000000000000000000000000000008352600483016104ea565b92505f60405180947fd68f6025000000000000000000000000000000000000000000000000000000008252818381611ec88888343360048601611d9c565b03925af1801561034957611e1c935f91611ee4575b5092611e11565b611f0091503d805f833e611ef8818361040b565b810190611d3d565b5f611edd565b600881901b91907fff00000000000000000000000000000000000000000000000000000000000000811680611f41575050916104fb926122de565b7f01000000000000000000000000000000000000000000000000000000000000008103611f745750506104fb92506121e2565b919250907ffe00000000000000000000000000000000000000000000000000000000000000036120165750611fab5f92839261235c565b9094929150611fb861205f565b94816040519283928337810184815203915afa611fd361131a565b82511561201157602083015215611fe75790565b60046040517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b612106565b602490604051907fba70d2310000000000000000000000000000000000000000000000000000000082526004820152fd5b67ffffffffffffffff81116103ea5760051b60200190565b60405161206b816103ef565b60018152805f5b60208082101561208d57906060602092828501015201612072565b50505090565b9061209d82612047565b6120aa604051918261040b565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06120d88294612047565b01905f5b8281106120e857505050565b8060606020809385010152016120dc565b908092918237015f815290565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b80518210156120115760209160051b010190565b91908110156120115760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa181360301821215610132570190565b356104fb81610114565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610132570180359067ffffffffffffffff82116101325760200191813603831361013257565b91909180350191602092602081019035916121fc83612093565b945f5b84811061220d575050505050565b61222061221b828787612147565b612187565b5f808461222e858a8a612147565b01359261223c858a8a612147565b9361224c60409586810190612191565b919061225c8751809481936120f9565b03925af161226861131a565b612272848b612133565b527fff0000000000000000000000000000000000000000000000000000000000000085161590816122d5575b506122ac57506001016121ff565b600490517facfdb444000000000000000000000000000000000000000000000000000000008152fd5b9050155f61229e565b6122ec5f949392859261235c565b909692916122f861205f565b97826040519384928337810185815203925af19061231461131a565b90845115612011577fff0000000000000000000000000000000000000000000000000000000000000091602086015216159081612353575b50611fe757565b9050155f61234c565b908060141161013257813560601c9281603411610132577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffcc60346014850135940192019056fea2646970667358221220ea0107d2fc265c63c0a8bd55e9d1937ca16a9aea1ea523588fa7e4f3af23626764736f6c63430008170033" } ], "isFixedGasLimit": false @@ -36,47 +62,47 @@ "receipts": [ { "status": "0x1", - "cumulativeGasUsed": "0xc0f176", + "cumulativeGasUsed": "0x24ea440", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0xd93f45dec98dc26096f4bcc3a502e5f340ec48ed7223d523d4bd40926202f033", + "transactionIndex": "0x58", + "blockHash": "0x1100e5b50f0c05a44c321b29aa9f43b9092f4a0550b8a5f1179300263fbcd491", + "blockNumber": "0x94e46f", + "gasUsed": "0x1fe58a", + "effectiveGasPrice": "0xf4249", + "from": "0x18ee4c040568238643c07e7afd6c53efc196d26b", + "to": "0x0000000000ffe8b47b3e2130213b802212439497", + "contractAddress": null + }, + { + "status": "0x1", + "cumulativeGasUsed": "0x27699c0", "logs": [ { - "address": "0xcd506c3632c03817b1b938e6440558da8d2ea328", - "topics": [ - "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000569c25b117231b791c9acad7a930c7" - ], - "data": "0x", - "blockHash": "0x570d7b4c7c4da4fb688f5edef750d1646b4cd6f3b7c82d8ffb4058fcd9a4357e", - "blockNumber": "0x94743d", - "blockTimestamp": "0x692a7c74", - "transactionHash": "0xf337387edbb47eeb3fce9ef2062237b8e966c1297eb203e1e8448b73389cc713", - "transactionIndex": "0x35", - "logIndex": "0x5c", - "removed": false - }, - { - "address": "0xcd506c3632c03817b1b938e6440558da8d2ea328", + "address": "0x565cf8324c9d856a540b3873701242bf27d82307", "topics": [ "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" ], "data": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "blockHash": "0x570d7b4c7c4da4fb688f5edef750d1646b4cd6f3b7c82d8ffb4058fcd9a4357e", - "blockNumber": "0x94743d", - "blockTimestamp": "0x692a7c74", - "transactionHash": "0xf337387edbb47eeb3fce9ef2062237b8e966c1297eb203e1e8448b73389cc713", - "transactionIndex": "0x35", - "logIndex": "0x5d", + "blockHash": "0x1100e5b50f0c05a44c321b29aa9f43b9092f4a0550b8a5f1179300263fbcd491", + "blockNumber": "0x94e46f", + "blockTimestamp": "0x692fc13c", + "transactionHash": "0x572875cd08fec02682e4390e06e9c7fd30c7fca916d030f4e098b1f0d5fe7cf6", + "transactionIndex": "0x5a", + "logIndex": "0x434", "removed": false } ], - "logsBloom": "0x00000000000000000000000000000000000000000000000000800000800000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000200000000000000000000020000000000000000000800000000000000000000000000000000400000000000000000000800000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000204000000000000000020000000000000000000000000000040000000000000000000000008000000000000", + "logsBloom": "0x00000000000000000000080000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000800000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "type": "0x2", - "transactionHash": "0xf337387edbb47eeb3fce9ef2062237b8e966c1297eb203e1e8448b73389cc713", - "transactionIndex": "0x35", - "blockHash": "0x570d7b4c7c4da4fb688f5edef750d1646b4cd6f3b7c82d8ffb4058fcd9a4357e", - "blockNumber": "0x94743d", - "gasUsed": "0x411b7d", - "effectiveGasPrice": "0xf4250", + "transactionHash": "0x572875cd08fec02682e4390e06e9c7fd30c7fca916d030f4e098b1f0d5fe7cf6", + "transactionIndex": "0x5a", + "blockHash": "0x1100e5b50f0c05a44c321b29aa9f43b9092f4a0550b8a5f1179300263fbcd491", + "blockNumber": "0x94e46f", + "gasUsed": "0x27404a", + "effectiveGasPrice": "0xf4249", "from": "0x18ee4c040568238643c07e7afd6c53efc196d26b", "to": "0x0000000000ffe8b47b3e2130213b802212439497", "contractAddress": null @@ -85,7 +111,7 @@ "libraries": [], "pending": [], "returns": {}, - "timestamp": 1764392127881, + "timestamp": 1764737340817, "chain": 11155111, - "commit": "6fd7ce8" + "commit": "3f45cae" } \ No newline at end of file diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 366806f..35f83f8 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -27,12 +27,12 @@ contract DeployScript is Script { // Salt for P256MFAValidatorModule CREATE2 deployment // First 20 bytes must match deployer address for Solady factory - // TODO: Update with your deployer address and vanity salt - bytes32 constant VALIDATOR_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b0000000000000000000000a1; + // Expected address: 0x000000b07799b322d076669ef32b247d02279c7e + bytes32 constant VALIDATOR_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b00000000000000000028c56f; // Salt for AuraAccountFactory CREATE2 deployment - // TODO: Update with your deployer address and vanity salt - bytes32 constant FACTORY_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b0000000000000000000000a2; + // Expected address: 0x0000004b2941659deb7472b46f7b84caf27dce44 + bytes32 constant FACTORY_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b000000000000000001b2f724; function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); From e1cadfa7a30c2f81e588d8505541d2fa8354bfcc Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 3 Dec 2025 14:01:44 +0700 Subject: [PATCH 20/22] fix: remove unused P256AccountArtifact import causing CI failure --- frontend/src/lib/accountManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/lib/accountManager.js b/frontend/src/lib/accountManager.js index 813339a..250d92e 100644 --- a/frontend/src/lib/accountManager.js +++ b/frontend/src/lib/accountManager.js @@ -4,7 +4,6 @@ import { ethers } from 'ethers' import { P256_ACCOUNT_FACTORY_ABI, P256_ACCOUNT_ABI, ENTRYPOINT_ADDRESS } from './constants.js' -import P256AccountArtifact from '@contracts/P256Account.sol/P256Account.json' /** * P256AccountManager class for managing P256 accounts From 55c18e09c0617a24a983ede6d37b8a74177adb93 Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 3 Dec 2025 14:16:19 +0700 Subject: [PATCH 21/22] feat(frontend): add ERC-7579 modular account support (task 5.4) - Add modular factory addresses to NetworkContext - Create ModularAccountManager and SessionKeyManager classes - Add useModularAccount hooks for React integration - Update HomeScreen with account type selector (modular vs legacy) - Add ModuleManager component for viewing installed modules - Add SessionKeyManager component for session key management - Add SpendingLimitConfig component for spending limits - Update WalletSettingsScreen with new tabs for modular accounts - Add ERC-7579 ABIs to constants Part of: ERC-7579 Modular Smart Account Architecture migration --- frontend/src/components/ModuleManager.jsx | 246 +++++++++++ frontend/src/components/SessionKeyManager.jsx | 234 ++++++++++ .../src/components/SpendingLimitConfig.jsx | 231 ++++++++++ frontend/src/contexts/NetworkContext.jsx | 24 ++ frontend/src/hooks/useModularAccount.js | 181 ++++++++ frontend/src/lib/constants.js | 75 ++++ frontend/src/lib/modularAccountManager.js | 402 ++++++++++++++++++ frontend/src/screens/HomeScreen.jsx | 174 ++++---- frontend/src/screens/WalletSettingsScreen.jsx | 57 ++- frontend/src/styles/HomeScreen.css | 46 ++ frontend/src/styles/ModuleManager.css | 190 +++++++++ frontend/src/styles/SessionKeyManager.css | 273 ++++++++++++ frontend/src/styles/SpendingLimitConfig.css | 222 ++++++++++ 13 files changed, 2278 insertions(+), 77 deletions(-) create mode 100644 frontend/src/components/ModuleManager.jsx create mode 100644 frontend/src/components/SessionKeyManager.jsx create mode 100644 frontend/src/components/SpendingLimitConfig.jsx create mode 100644 frontend/src/hooks/useModularAccount.js create mode 100644 frontend/src/lib/modularAccountManager.js create mode 100644 frontend/src/styles/ModuleManager.css create mode 100644 frontend/src/styles/SessionKeyManager.css create mode 100644 frontend/src/styles/SpendingLimitConfig.css diff --git a/frontend/src/components/ModuleManager.jsx b/frontend/src/components/ModuleManager.jsx new file mode 100644 index 0000000..145a3c3 --- /dev/null +++ b/frontend/src/components/ModuleManager.jsx @@ -0,0 +1,246 @@ +/** + * ModuleManager - Component for managing ERC-7579 modules + */ + +import { useState, useEffect, useCallback } from 'react' +import { ethers } from 'ethers' +import { useNetwork } from '../contexts/NetworkContext' +import { useModularAccountManager } from '../hooks/useModularAccount' +import { MODULE_TYPE } from '../lib/constants' +import { Layers, Check, X, AlertCircle, Loader2, Settings, Zap, Shield } from 'lucide-react' +import '../styles/ModuleManager.css' + +function ModuleManager({ accountAddress, isModular = false }) { + const { networkInfo } = useNetwork() + const manager = useModularAccountManager() + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [modules, setModules] = useState({ + validator: null, + executors: [], + hooks: [], + fallbacks: [], + }) + const [accountInfo, setAccountInfo] = useState(null) + + // Load installed modules + const loadModules = useCallback(async () => { + if (!manager || !accountAddress || !isModular) { + setLoading(false) + return + } + + setLoading(true) + setError(null) + + try { + // Get account info + const info = await manager.getAccountInfo(accountAddress) + setAccountInfo(info) + + // Get installed validator + const validator = await manager.getInstalledValidator(accountAddress) + + // Get global hook + const globalHook = await manager.getGlobalHook(accountAddress) + + // Check if session key module is installed + let sessionKeyInstalled = false + if (networkInfo.sessionKeyModuleAddress) { + sessionKeyInstalled = await manager.isModuleInstalled( + accountAddress, + MODULE_TYPE.EXECUTOR, + networkInfo.sessionKeyModuleAddress + ) + } + + setModules({ + validator: validator && validator !== ethers.ZeroAddress ? { + address: validator, + name: validator === networkInfo.validatorModuleAddress ? 'P256MFAValidator' : 'Unknown Validator', + type: MODULE_TYPE.VALIDATOR, + } : null, + executors: sessionKeyInstalled ? [{ + address: networkInfo.sessionKeyModuleAddress, + name: 'SessionKeyExecutor', + type: MODULE_TYPE.EXECUTOR, + }] : [], + hooks: globalHook && globalHook !== ethers.ZeroAddress ? [{ + address: globalHook, + name: 'Global Hook', + type: MODULE_TYPE.HOOK, + }] : [], + fallbacks: [], + }) + } catch (err) { + console.error('Failed to load modules:', err) + setError('Failed to load module information') + } finally { + setLoading(false) + } + }, [manager, accountAddress, isModular, networkInfo]) + + useEffect(() => { + loadModules() + }, [loadModules]) + + // If not a modular account, show info message + if (!isModular) { + return ( +
+
+ +

Module Management

+
+
+ +

This is a legacy P256Account. Module management is only available for ERC-7579 modular accounts.

+
+
+ ) + } + + if (loading) { + return ( +
+
+ +

Module Management

+
+
+ +

Loading modules...

+
+
+ ) + } + + if (error) { + return ( +
+
+ +

Module Management

+
+
+ +

{error}

+ +
+
+ ) + } + + const formatAddress = (addr) => { + if (!addr) return 'None' + return `${addr.slice(0, 6)}...${addr.slice(-4)}` + } + + const getModuleIcon = (type) => { + switch (type) { + case MODULE_TYPE.VALIDATOR: return + case MODULE_TYPE.EXECUTOR: return + case MODULE_TYPE.HOOK: return + default: return + } + } + + return ( +
+
+ +

Module Management

+
+ +
+

Manage the modules installed on your ERC-7579 modular smart account.

+
+ + {/* Account Status */} +
+

Account Status

+
+
+ Deployed + + {accountInfo?.deployed ? : } + {accountInfo?.deployed ? 'Yes' : 'No'} + +
+
+ MFA Enabled + + {accountInfo?.mfaEnabled ? : } + {accountInfo?.mfaEnabled ? 'Yes' : 'No'} + +
+
+
+ + {/* Validator Module */} +
+

Validator Module

+

The validator module handles signature verification for transactions.

+ {modules.validator ? ( +
+
{getModuleIcon(MODULE_TYPE.VALIDATOR)}
+
+ {modules.validator.name} + {formatAddress(modules.validator.address)} +
+ Installed +
+ ) : ( +
No validator module installed
+ )} +
+ + {/* Executor Modules */} +
+

Executor Modules

+

Executor modules can perform actions on behalf of your account.

+ {modules.executors.length > 0 ? ( +
+ {modules.executors.map((mod, idx) => ( +
+
{getModuleIcon(MODULE_TYPE.EXECUTOR)}
+
+ {mod.name} + {formatAddress(mod.address)} +
+ Installed +
+ ))} +
+ ) : ( +
No executor modules installed
+ )} +
+ + {/* Hook Modules */} +
+

Hook Modules

+

Hook modules can add pre/post execution checks to transactions.

+ {modules.hooks.length > 0 ? ( +
+ {modules.hooks.map((mod, idx) => ( +
+
{getModuleIcon(MODULE_TYPE.HOOK)}
+
+ {mod.name} + {formatAddress(mod.address)} +
+ Installed +
+ ))} +
+ ) : ( +
No hook modules installed
+ )} +
+
+ ) +} + +export default ModuleManager + diff --git a/frontend/src/components/SessionKeyManager.jsx b/frontend/src/components/SessionKeyManager.jsx new file mode 100644 index 0000000..17e4a8a --- /dev/null +++ b/frontend/src/components/SessionKeyManager.jsx @@ -0,0 +1,234 @@ +/** + * SessionKeyManager - Component for managing ERC-7579 session keys + */ + +import { useState, useEffect, useCallback } from 'react' +import { ethers } from 'ethers' +import { useNetwork } from '../contexts/NetworkContext' +import { useSessionKeyManager } from '../hooks/useModularAccount' +import { Key, Plus, Trash2, Clock, AlertCircle, Loader2, Check, X, DollarSign } from 'lucide-react' +import '../styles/SessionKeyManager.css' + +function SessionKeyManager({ accountAddress, isModular = false }) { + const { networkInfo } = useNetwork() + const sessionKeyManager = useSessionKeyManager() + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [sessionKeys, setSessionKeys] = useState([]) + const [showCreateModal, setShowCreateModal] = useState(false) + + // Load session keys + const loadSessionKeys = useCallback(async () => { + if (!sessionKeyManager || !accountAddress || !isModular) { + setLoading(false) + return + } + + setLoading(true) + setError(null) + + try { + const keys = await sessionKeyManager.getSessionKeys(accountAddress) + setSessionKeys(keys) + } catch (err) { + console.error('Failed to load session keys:', err) + setError('Failed to load session keys') + } finally { + setLoading(false) + } + }, [sessionKeyManager, accountAddress, isModular]) + + useEffect(() => { + loadSessionKeys() + }, [loadSessionKeys]) + + // If not a modular account, show info message + if (!isModular) { + return ( +
+
+ +

Session Keys

+
+
+ +

Session keys are only available for ERC-7579 modular accounts.

+
+
+ ) + } + + if (!sessionKeyManager) { + return ( +
+
+ +

Session Keys

+
+
+ +

Session key module not available on this network.

+
+
+ ) + } + + if (loading) { + return ( +
+
+ +

Session Keys

+
+
+ +

Loading session keys...

+
+
+ ) + } + + const formatAddress = (addr) => `${addr.slice(0, 6)}...${addr.slice(-4)}` + + const formatDate = (timestamp) => { + if (!timestamp) return 'N/A' + return new Date(timestamp * 1000).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } + + const formatEth = (wei) => { + if (!wei) return '0' + return ethers.formatEther(wei) + } + + const isExpired = (validUntil) => { + return validUntil && Date.now() / 1000 > validUntil + } + + const isNotYetValid = (validAfter) => { + return validAfter && Date.now() / 1000 < validAfter + } + + return ( +
+
+ +

Session Keys

+ +
+ +
+

Session keys allow delegated access to your account with time and spending limits.

+
+ + {error && ( +
+ +

{error}

+ +
+ )} + + {sessionKeys.length === 0 ? ( +
+ +

No session keys configured

+ Create a session key to allow delegated access to your account +
+ ) : ( +
+ {sessionKeys.map((key, idx) => ( +
+
+
+ + {formatAddress(key.address)} +
+ + {key.active ? : } + {key.active ? 'Active' : 'Revoked'} + +
+ +
+
+ + Valid: + + {formatDate(key.validAfter)} - {formatDate(key.validUntil)} + {isExpired(key.validUntil) && ' (Expired)'} + {isNotYetValid(key.validAfter) && ' (Not yet valid)'} + +
+ +
+ + Limit per tx: + {formatEth(key.spendLimitPerTx)} ETH +
+ +
+ + Total limit: + + {formatEth(key.spentTotal)} / {formatEth(key.spendLimitTotal)} ETH + +
+ + {key.allowedTargets && key.allowedTargets.length > 0 && ( +
+ Allowed targets: +
+ {key.allowedTargets.map((target, i) => ( + {formatAddress(target)} + ))} +
+
+ )} +
+ + {key.active && ( +
+ +
+ )} +
+ ))} +
+ )} + + {/* Create Session Key Modal - placeholder */} + {showCreateModal && ( +
setShowCreateModal(false)}> +
e.stopPropagation()}> +
+

Create Session Key

+ +
+
+
+ +

Coming Soon

+

Session key creation will be available in a future update.

+
+
+
+
+ )} +
+ ) +} + +export default SessionKeyManager + diff --git a/frontend/src/components/SpendingLimitConfig.jsx b/frontend/src/components/SpendingLimitConfig.jsx new file mode 100644 index 0000000..f79d77a --- /dev/null +++ b/frontend/src/components/SpendingLimitConfig.jsx @@ -0,0 +1,231 @@ +/** + * SpendingLimitConfig - Component for configuring spending limits and large transaction thresholds + */ + +import { useState, useEffect, useCallback } from 'react' +import { ethers } from 'ethers' +import { useNetwork } from '../contexts/NetworkContext' +import { useModularAccountManager } from '../hooks/useModularAccount' +import { DollarSign, AlertCircle, Loader2, Shield, Clock, Settings } from 'lucide-react' +import '../styles/SpendingLimitConfig.css' + +function SpendingLimitConfig({ accountAddress, isModular = false }) { + const { networkInfo } = useNetwork() + const manager = useModularAccountManager() + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [config, setConfig] = useState({ + largeTransactionThreshold: '1.0', + dailyLimit: '10.0', + timelockDelay: 3600, // 1 hour in seconds + hasLargeTransactionHook: false, + }) + + // Load current configuration + const loadConfig = useCallback(async () => { + if (!manager || !accountAddress || !isModular) { + setLoading(false) + return + } + + setLoading(true) + setError(null) + + try { + // Check if large transaction executor module is installed + // For now, we'll just show the configuration UI + // Actual module detection would require checking installed modules + const accountInfo = await manager.getAccountInfo(accountAddress) + + setConfig(prev => ({ + ...prev, + hasLargeTransactionHook: false, // Will be updated when module is detected + })) + } catch (err) { + console.error('Failed to load spending config:', err) + setError('Failed to load spending limit configuration') + } finally { + setLoading(false) + } + }, [manager, accountAddress, isModular]) + + useEffect(() => { + loadConfig() + }, [loadConfig]) + + // If not a modular account, show info message + if (!isModular) { + return ( +
+
+ +

Spending Limits

+
+
+ +

Spending limits are only available for ERC-7579 modular accounts.

+
+
+ ) + } + + if (loading) { + return ( +
+
+ +

Spending Limits

+
+
+ +

Loading configuration...

+
+
+ ) + } + + const formatDuration = (seconds) => { + if (seconds < 60) return `${seconds} seconds` + if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes` + if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours` + return `${Math.floor(seconds / 86400)} days` + } + + return ( +
+
+ +

Spending Limits

+
+ +
+

Configure spending limits and transaction thresholds for enhanced security.

+
+ + {error && ( +
+ +

{error}

+ +
+ )} + + {/* Large Transaction Threshold */} +
+
+ +

Large Transaction Protection

+
+

+ Transactions above this threshold will require a timelock delay before execution. +

+ +
+
+ +
+ setConfig(prev => ({ ...prev, largeTransactionThreshold: e.target.value }))} + placeholder="1.0" + step="0.1" + min="0" + disabled + /> + ETH +
+
+ +
+ +
+ +
+
+ +
+ + + Transactions over {config.largeTransactionThreshold} ETH will have a {formatDuration(config.timelockDelay)} delay + +
+
+
+ + {/* Daily Spending Limit */} +
+
+ +

Daily Spending Limit

+
+

+ Set a maximum amount that can be spent per day without additional approval. +

+ +
+
+ +
+ setConfig(prev => ({ ...prev, dailyLimit: e.target.value }))} + placeholder="10.0" + step="0.1" + min="0" + disabled + /> + ETH +
+
+ +
+ + Daily limits reset at midnight UTC +
+
+
+ + {/* Module Status */} +
+
+ +

Module Status

+
+ +
+
+ Large Transaction Executor + + {config.hasLargeTransactionHook ? 'Installed' : 'Not Installed'} + +
+

+ {config.hasLargeTransactionHook + ? 'Large transaction protection is active on your account.' + : 'Install the Large Transaction Executor module to enable spending limits.'} +

+ {!config.hasLargeTransactionHook && ( + + )} +
+
+
+ ) +} + +export default SpendingLimitConfig + diff --git a/frontend/src/contexts/NetworkContext.jsx b/frontend/src/contexts/NetworkContext.jsx index 3d7611e..98b9bec 100644 --- a/frontend/src/contexts/NetworkContext.jsx +++ b/frontend/src/contexts/NetworkContext.jsx @@ -22,6 +22,10 @@ export const AVAILABLE_NETWORKS = [ bundlerUrl: 'https://api.pimlico.io/v2/1/rpc?apikey=YOUR_API_KEY', explorerUrl: 'https://etherscan.io', factoryAddress: import.meta.env.VITE_FACTORY_ADDRESS, + // ERC-7579 modular account contracts + modularFactoryAddress: import.meta.env.VITE_MODULAR_FACTORY_ADDRESS, + validatorModuleAddress: import.meta.env.VITE_VALIDATOR_MODULE_ADDRESS, + sessionKeyModuleAddress: import.meta.env.VITE_SESSION_KEY_MODULE_ADDRESS, supported: false, // Factory not yet deployed on mainnet }, { @@ -31,6 +35,9 @@ export const AVAILABLE_NETWORKS = [ bundlerUrl: 'https://api.pimlico.io/v2/optimism/rpc?apikey=YOUR_API_KEY', explorerUrl: 'https://optimistic.etherscan.io', factoryAddress: import.meta.env.VITE_FACTORY_ADDRESS, + modularFactoryAddress: import.meta.env.VITE_MODULAR_FACTORY_ADDRESS, + validatorModuleAddress: import.meta.env.VITE_VALIDATOR_MODULE_ADDRESS, + sessionKeyModuleAddress: import.meta.env.VITE_SESSION_KEY_MODULE_ADDRESS, supported: false, // Factory not yet deployed on Optimism }, { @@ -40,6 +47,9 @@ export const AVAILABLE_NETWORKS = [ bundlerUrl: 'https://api.pimlico.io/v2/polygon/rpc?apikey=YOUR_API_KEY', explorerUrl: 'https://polygonscan.com', factoryAddress: import.meta.env.VITE_FACTORY_ADDRESS, + modularFactoryAddress: import.meta.env.VITE_MODULAR_FACTORY_ADDRESS, + validatorModuleAddress: import.meta.env.VITE_VALIDATOR_MODULE_ADDRESS, + sessionKeyModuleAddress: import.meta.env.VITE_SESSION_KEY_MODULE_ADDRESS, supported: false, // Factory not yet deployed on Polygon }, { @@ -49,6 +59,9 @@ export const AVAILABLE_NETWORKS = [ bundlerUrl: 'https://api.pimlico.io/v2/arbitrum/rpc?apikey=YOUR_API_KEY', explorerUrl: 'https://arbiscan.io', factoryAddress: import.meta.env.VITE_FACTORY_ADDRESS, + modularFactoryAddress: import.meta.env.VITE_MODULAR_FACTORY_ADDRESS, + validatorModuleAddress: import.meta.env.VITE_VALIDATOR_MODULE_ADDRESS, + sessionKeyModuleAddress: import.meta.env.VITE_SESSION_KEY_MODULE_ADDRESS, supported: false, // Factory not yet deployed on Arbitrum }, { @@ -58,6 +71,9 @@ export const AVAILABLE_NETWORKS = [ bundlerUrl: import.meta.env.VITE_BASE_BUNDLER_URL || 'https://api.pimlico.io/v2/base/rpc?apikey=YOUR_API_KEY', explorerUrl: 'https://basescan.org', factoryAddress: import.meta.env.VITE_FACTORY_ADDRESS, + modularFactoryAddress: import.meta.env.VITE_MODULAR_FACTORY_ADDRESS, + validatorModuleAddress: import.meta.env.VITE_VALIDATOR_MODULE_ADDRESS, + sessionKeyModuleAddress: import.meta.env.VITE_SESSION_KEY_MODULE_ADDRESS, supported: false, // Factory will be deployed after Base Sepolia testing }, { @@ -67,6 +83,10 @@ export const AVAILABLE_NETWORKS = [ bundlerUrl: import.meta.env.VITE_BASE_SEPOLIA_BUNDLER_URL || 'https://api.pimlico.io/v2/base-sepolia/rpc?apikey=YOUR_API_KEY', explorerUrl: 'https://sepolia.basescan.org', factoryAddress: import.meta.env.VITE_BASE_SEPOLIA_FACTORY_ADDRESS || '0xF913EF5101Dcb4fDB9A62666D18593aea5509262', + // ERC-7579 modular account contracts (to be deployed) + modularFactoryAddress: import.meta.env.VITE_BASE_SEPOLIA_MODULAR_FACTORY_ADDRESS, + validatorModuleAddress: import.meta.env.VITE_BASE_SEPOLIA_VALIDATOR_MODULE_ADDRESS, + sessionKeyModuleAddress: import.meta.env.VITE_BASE_SEPOLIA_SESSION_KEY_MODULE_ADDRESS, supported: true, // ✅ Deployed on 2025-11-22 }, { @@ -76,6 +96,10 @@ export const AVAILABLE_NETWORKS = [ bundlerUrl: import.meta.env.VITE_BUNDLER_URL || 'https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_API_KEY', explorerUrl: 'https://sepolia.etherscan.io', factoryAddress: import.meta.env.VITE_FACTORY_ADDRESS, + // ERC-7579 modular account contracts (to be deployed) + modularFactoryAddress: import.meta.env.VITE_MODULAR_FACTORY_ADDRESS, + validatorModuleAddress: import.meta.env.VITE_VALIDATOR_MODULE_ADDRESS, + sessionKeyModuleAddress: import.meta.env.VITE_SESSION_KEY_MODULE_ADDRESS, supported: true, // Factory is deployed on Sepolia }, ]; diff --git a/frontend/src/hooks/useModularAccount.js b/frontend/src/hooks/useModularAccount.js new file mode 100644 index 0000000..2dca945 --- /dev/null +++ b/frontend/src/hooks/useModularAccount.js @@ -0,0 +1,181 @@ +/** + * React hook for ERC-7579 Modular Account SDK + */ + +import { useMemo, useState, useCallback } from 'react' +import { useNetwork } from '../contexts/NetworkContext' +import { + createModularAccountManager, + createSessionKeyManager, +} from '../lib/modularAccountManager.js' +import { ethers } from 'ethers' + +/** + * Hook to use ModularAccountManager + * @returns {Object} Modular account manager instance + */ +export function useModularAccountManager() { + const { networkInfo } = useNetwork() + + const manager = useMemo(() => { + if (!networkInfo.modularFactoryAddress) { + return null + } + + const provider = new ethers.JsonRpcProvider(networkInfo.rpcUrl) + + return createModularAccountManager( + networkInfo.modularFactoryAddress, + networkInfo.validatorModuleAddress, + provider + ) + }, [networkInfo]) + + return manager +} + +/** + * Hook to use SessionKeyManager + * @returns {Object} Session key manager instance + */ +export function useSessionKeyManager() { + const { networkInfo } = useNetwork() + + const manager = useMemo(() => { + if (!networkInfo.sessionKeyModuleAddress) { + return null + } + + const provider = new ethers.JsonRpcProvider(networkInfo.rpcUrl) + return createSessionKeyManager(networkInfo.sessionKeyModuleAddress, provider) + }, [networkInfo]) + + return manager +} + +/** + * Hook for managing modular account + * @returns {Object} Account management functions and state + */ +export function useModularAccount() { + const manager = useModularAccountManager() + const [accountInfo, setAccountInfo] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + /** + * Create a new modular account (counterfactual) + * @param {string} ownerAddress - Owner address + * @param {bigint} salt - Salt for CREATE2 + * @param {Object|null} _passkeyPublicKey - Reserved for future passkey support + * @param {boolean} enableMFA - Whether to enable MFA + */ + const createAccount = useCallback(async (ownerAddress, salt = 0n, _passkeyPublicKey = null, enableMFA = false) => { + if (!manager) { + throw new Error('Modular account not available on this network') + } + + setLoading(true) + setError(null) + + try { + const address = await manager.getAccountAddress(ownerAddress, salt) + const info = await manager.getAccountInfo(address, enableMFA) + setAccountInfo(info) + return info + } catch (err) { + setError(err.message) + throw err + } finally { + setLoading(false) + } + }, [manager]) + + /** + * Load existing account info + */ + const loadAccount = useCallback(async (accountAddress) => { + if (!manager) { + throw new Error('Modular account not available on this network') + } + + setLoading(true) + setError(null) + + try { + const info = await manager.getAccountInfo(accountAddress) + setAccountInfo(info) + return info + } catch (err) { + setError(err.message) + throw err + } finally { + setLoading(false) + } + }, [manager]) + + /** + * Check if modular accounts are supported + */ + const isSupported = useMemo(() => { + return manager !== null + }, [manager]) + + return { + manager, + accountInfo, + loading, + error, + createAccount, + loadAccount, + isSupported, + } +} + +/** + * Hook for managing session keys + * @param {string} accountAddress - The account address + * @returns {Object} Session key management functions and state + */ +export function useSessionKeys(accountAddress) { + const manager = useSessionKeyManager() + const [sessionKeys, setSessionKeys] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + /** + * Refresh session keys list + */ + const refresh = useCallback(async () => { + if (!manager || !accountAddress) return + + setLoading(true) + setError(null) + + try { + const keys = await manager.getSessionKeys(accountAddress) + setSessionKeys(keys) + } catch (err) { + setError(err.message) + } finally { + setLoading(false) + } + }, [manager, accountAddress]) + + /** + * Check if session keys are supported + */ + const isSupported = useMemo(() => { + return manager !== null + }, [manager]) + + return { + manager, + sessionKeys, + loading, + error, + refresh, + isSupported, + } +} + diff --git a/frontend/src/lib/constants.js b/frontend/src/lib/constants.js index 64b19b7..f9272ee 100644 --- a/frontend/src/lib/constants.js +++ b/frontend/src/lib/constants.js @@ -129,6 +129,81 @@ export const WETH_ABI = [ 'function transfer(address to, uint256 amount) returns (bool)', ] +// ERC-7579 Module Type IDs +export const MODULE_TYPE = { + VALIDATOR: 1, + EXECUTOR: 2, + FALLBACK: 3, + HOOK: 4, +} + +// AuraAccountFactory ABI (ERC-7579 modular account factory) +export const AURA_ACCOUNT_FACTORY_ABI = [ + 'function createAccount(address owner, bytes validatorData, address hook, bytes hookData, uint256 salt) returns (address)', + 'function getAddress(address owner, uint256 salt) view returns (address)', + 'function accountImplementation() view returns (address)', + 'function validator() view returns (address)', +] + +// AuraAccount ABI (ERC-7579 modular account) +export const AURA_ACCOUNT_ABI = [ + // ERC-7579 execution + 'function execute(bytes32 mode, bytes executionCalldata) payable', + 'function executeFromExecutor(bytes32 mode, bytes executionCalldata) payable returns (bytes[])', + // Module management + 'function installModule(uint256 moduleTypeId, address module, bytes initData) payable', + 'function uninstallModule(uint256 moduleTypeId, address module, bytes deInitData) payable', + 'function isModuleInstalled(uint256 moduleTypeId, address module, bytes additionalContext) view returns (bool)', + // Account info + 'function getValidator() view returns (address)', + 'function getGlobalHook() view returns (address)', + 'function accountId() view returns (string)', + 'function supportsModule(uint256 moduleTypeId) view returns (bool)', + // ERC-1271 + 'function isValidSignature(bytes32 hash, bytes signature) view returns (bytes4)', +] + +// P256MFAValidatorModule ABI +export const P256_MFA_VALIDATOR_ABI = [ + 'function getOwner(address account) view returns (address)', + 'function isMFAEnabled(address account) view returns (bool)', + 'function getPasskeyCount(address account) view returns (uint256)', + 'function getPasskey(address account, bytes32 passkeyId) view returns ((bytes32 qx, bytes32 qy, uint256 addedAt, bool active, bytes32 deviceId))', + 'function isPasskeyActive(address account, bytes32 passkeyId) view returns (bool)', + 'function getPasskeyIds(address account) view returns (bytes32[])', + // Management functions (called via account.execute) + 'function addPasskey(bytes32 qx, bytes32 qy, bytes32 deviceId) external', + 'function removePasskey(bytes32 passkeyId) external', + 'function enableMFA() external', + 'function disableMFA() external', + 'function setOwner(address newOwner) external', +] + +// SessionKeyExecutorModule ABI +export const SESSION_KEY_EXECUTOR_ABI = [ + 'function getSessionKey(address account, address sessionKey) view returns (bool active, uint48 validAfter, uint48 validUntil, uint256 spendLimitPerTx, uint256 spendLimitTotal, uint256 spentTotal, uint256 nonce)', + 'function getSessionKeyCount(address account) view returns (uint256)', + 'function getSessionKeys(address account) view returns (address[])', + 'function getAllowedTargets(address account, address sessionKey) view returns (address[])', + 'function getAllowedSelectors(address account, address sessionKey) view returns (bytes4[])', + 'function isSessionKeyValid(address account, address sessionKey) view returns (bool)', + // Management functions (called via account.execute) + 'function createSessionKey((address sessionKey, uint48 validAfter, uint48 validUntil, address[] allowedTargets, bytes4[] allowedSelectors, uint256 spendLimitPerTx, uint256 spendLimitTotal)) external', + 'function revokeSessionKey(address sessionKey) external', + // Execution + 'function executeWithSessionKey(address account, address sessionKey, address target, uint256 value, bytes data, uint256 nonce, bytes signature) returns (bytes)', +] + +// LargeTransactionExecutorModule ABI +export const LARGE_TX_EXECUTOR_ABI = [ + 'function getThreshold(address account) view returns (uint256)', + 'function setThreshold(uint256 threshold) external', + 'function proposeTransaction(address target, uint256 value, bytes data) external returns (bytes32)', + 'function executeProposedTransaction(bytes32 proposalId) external returns (bytes)', + 'function cancelProposal(bytes32 proposalId) external', + 'function getProposal(address account, bytes32 proposalId) view returns ((address target, uint256 value, bytes data, uint256 executeAfter, bool executed, bool cancelled))', +] + // Import token icons import ethIcon from '../assets/tokens/eth.svg' import wethIcon from '../assets/tokens/weth.svg' diff --git a/frontend/src/lib/modularAccountManager.js b/frontend/src/lib/modularAccountManager.js new file mode 100644 index 0000000..d38ff78 --- /dev/null +++ b/frontend/src/lib/modularAccountManager.js @@ -0,0 +1,402 @@ +/** + * ERC-7579 Modular Account management utilities for AuraAccount + */ + +import { ethers } from 'ethers' +import { ENTRYPOINT_ADDRESS } from './constants.js' + +// AuraAccountFactory ABI - minimal interface for account creation +export const AURA_ACCOUNT_FACTORY_ABI = [ + 'function createAccount(address owner, bytes validatorData, address hook, bytes hookData, uint256 salt) returns (address)', + 'function getAddress(address owner, uint256 salt) view returns (address)', + 'function accountImplementation() view returns (address)', + 'function validator() view returns (address)', +] + +// AuraAccount ABI - ERC-7579 modular account interface +export const AURA_ACCOUNT_ABI = [ + // ERC-4337 + 'function validateUserOp((address sender, uint256 nonce, bytes initCode, bytes callData, bytes32 accountGasLimits, uint256 preVerificationGas, bytes32 gasFees, bytes paymasterAndData, bytes signature), bytes32 userOpHash, uint256 missingAccountFunds) returns (uint256)', + // ERC-7579 execution + 'function execute(bytes32 mode, bytes executionCalldata) payable', + 'function executeFromExecutor(bytes32 mode, bytes executionCalldata) payable returns (bytes[])', + // Module management + 'function installModule(uint256 moduleTypeId, address module, bytes initData) payable', + 'function uninstallModule(uint256 moduleTypeId, address module, bytes deInitData) payable', + 'function isModuleInstalled(uint256 moduleTypeId, address module, bytes additionalContext) view returns (bool)', + // Account info + 'function getValidator() view returns (address)', + 'function getGlobalHook() view returns (address)', + 'function accountId() view returns (string)', + 'function supportsModule(uint256 moduleTypeId) view returns (bool)', + // ERC-1271 + 'function isValidSignature(bytes32 hash, bytes signature) view returns (bytes4)', +] + +// P256MFAValidatorModule ABI +export const P256_MFA_VALIDATOR_ABI = [ + 'function getOwner(address account) view returns (address)', + 'function isMFAEnabled(address account) view returns (bool)', + 'function getPasskeyCount(address account) view returns (uint256)', + 'function getPasskey(address account, bytes32 passkeyId) view returns ((bytes32 qx, bytes32 qy, uint256 addedAt, bool active, bytes32 deviceId))', + 'function isPasskeyActive(address account, bytes32 passkeyId) view returns (bool)', + 'function getPasskeyIds(address account) view returns (bytes32[])', + // Management functions (called via account.execute) + 'function addPasskey(bytes32 qx, bytes32 qy, bytes32 deviceId) external', + 'function removePasskey(bytes32 passkeyId) external', + 'function enableMFA() external', + 'function disableMFA() external', + 'function setOwner(address newOwner) external', +] + +// SessionKeyExecutorModule ABI +export const SESSION_KEY_EXECUTOR_ABI = [ + 'function getSessionKey(address account, address sessionKey) view returns (bool active, uint48 validAfter, uint48 validUntil, uint256 spendLimitPerTx, uint256 spendLimitTotal, uint256 spentTotal, uint256 nonce)', + 'function getSessionKeyCount(address account) view returns (uint256)', + 'function getSessionKeys(address account) view returns (address[])', + 'function getAllowedTargets(address account, address sessionKey) view returns (address[])', + 'function getAllowedSelectors(address account, address sessionKey) view returns (bytes4[])', + 'function isSessionKeyValid(address account, address sessionKey) view returns (bool)', + // Management functions (called via account.execute) + 'function createSessionKey((address sessionKey, uint48 validAfter, uint48 validUntil, address[] allowedTargets, bytes4[] allowedSelectors, uint256 spendLimitPerTx, uint256 spendLimitTotal)) external', + 'function revokeSessionKey(address sessionKey) external', + // Execution + 'function executeWithSessionKey(address account, address sessionKey, address target, uint256 value, bytes data, uint256 nonce, bytes signature) returns (bytes)', +] + +// Module type IDs (ERC-7579) +export const MODULE_TYPE = { + VALIDATOR: 1, + EXECUTOR: 2, + FALLBACK: 3, + HOOK: 4, +} + +/** + * ModularAccountManager class for managing ERC-7579 modular accounts + */ +export class ModularAccountManager { + constructor(factoryAddress, validatorAddress, provider) { + this.factoryAddress = factoryAddress + this.validatorAddress = validatorAddress + this.provider = provider + this.factory = new ethers.Contract(factoryAddress, AURA_ACCOUNT_FACTORY_ABI, provider) + this.validator = validatorAddress ? new ethers.Contract(validatorAddress, P256_MFA_VALIDATOR_ABI, provider) : null + + // Simple cache + this.cache = { + deployedStatus: new Map(), + accountInfo: new Map(), + } + this.cacheExpiry = 30000 // 30 seconds + } + + /** + * Clear cache for an address + */ + clearCache(address) { + this.cache.deployedStatus.delete(address) + this.cache.accountInfo.delete(address) + } + + /** + * Calculate the counterfactual address for an AuraAccount + * @param {string} owner - Owner address + * @param {bigint} salt - Salt for CREATE2 + * @returns {Promise} Account address + */ + async getAccountAddress(owner, salt = 0n) { + try { + if (!this.factoryAddress || this.factoryAddress === ethers.ZeroAddress) { + throw new Error('Modular factory address not configured for this network') + } + + const factoryCode = await this.provider.getCode(this.factoryAddress) + if (factoryCode === '0x') { + throw new Error(`Modular factory not deployed on this network at ${this.factoryAddress}`) + } + + console.log('🏭 Calling modular factory.getAddress:', { owner, salt: salt.toString() }) + const address = await this.factory.getAddress(owner, salt) + console.log('🏭 Modular factory returned:', address) + return address + } catch (error) { + console.error('❌ ModularFactory.getAddress() failed:', error) + throw new Error(`Failed to get modular account address: ${error.message}`) + } + } + + /** + * Get initCode for counterfactual deployment + */ + async getInitCode(owner, qx, qy, deviceId, enableMFA, hook, hookData, salt = 0n) { + // Encode validator initialization data: (owner, qx, qy, deviceId, enableMFA) + const validatorData = ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'bytes32', 'bytes32', 'bytes32', 'bool'], + [owner, qx, qy, deviceId, enableMFA] + ) + + // Encode factory.createAccount call + const createAccountData = this.factory.interface.encodeFunctionData('createAccount', [ + owner, + validatorData, + hook || ethers.ZeroAddress, + hookData || '0x', + salt + ]) + + // initCode = factory address + createAccount calldata + return ethers.concat([this.factoryAddress, createAccountData]) + } + + /** + * Check if account is deployed + */ + async isDeployed(accountAddress) { + const cached = this.cache.deployedStatus.get(accountAddress) + if (cached && Date.now() - cached.timestamp < this.cacheExpiry) { + return cached.deployed + } + + const code = await this.provider.getCode(accountAddress) + const deployed = code !== '0x' + + this.cache.deployedStatus.set(accountAddress, { deployed, timestamp: Date.now() }) + return deployed + } + + /** + * Get AuraAccount contract instance + */ + getAccountContract(accountAddress, signerOrProvider = null) { + return new ethers.Contract(accountAddress, AURA_ACCOUNT_ABI, signerOrProvider || this.provider) + } + + /** + * Get account's nonce from EntryPoint + */ + async getNonce(accountAddress) { + const entryPoint = new ethers.Contract( + ENTRYPOINT_ADDRESS, + ['function getNonce(address sender, uint192 key) view returns (uint256)'], + this.provider + ) + return await entryPoint.getNonce(accountAddress, 0) + } + + /** + * Get account info (for modular accounts) + */ + async getAccountInfo(accountAddress, expectedMFAEnabled = null) { + try { + const isDeployed = await this.isDeployed(accountAddress) + + if (!isDeployed) { + return { + address: accountAddress, + deployed: false, + isModular: true, + mfaEnabled: expectedMFAEnabled !== null ? expectedMFAEnabled : false, + deposit: 0n, + nonce: 0n, + validator: this.validatorAddress, + hasPasskey: false, + } + } + + const account = this.getAccountContract(accountAddress) + const [validator, nonce] = await Promise.all([ + account.getValidator(), + this.getNonce(accountAddress), + ]) + + // Get MFA and passkey info from validator module + let mfaEnabled = false + let passkeyCount = 0 + let hasPasskey = false + + if (this.validator) { + try { + [mfaEnabled, passkeyCount] = await Promise.all([ + this.validator.isMFAEnabled(accountAddress), + this.validator.getPasskeyCount(accountAddress), + ]) + hasPasskey = passkeyCount > 0 + } catch (e) { + console.warn('Failed to read validator module state:', e) + } + } + + return { + address: accountAddress, + deployed: true, + isModular: true, + mfaEnabled, + deposit: 0n, // TODO: Get from EntryPoint + nonce, + validator, + hasPasskey, + passkeyCount, + } + } catch (e) { + console.error('Error getting modular account info:', e) + throw e + } + } + + /** + * Get passkeys from validator module + */ + async getPasskeys(accountAddress) { + if (!this.validator) return { passkeyIds: [], passkeys: [], total: 0 } + + try { + const passkeyIds = await this.validator.getPasskeyIds(accountAddress) + const passkeys = await Promise.all( + passkeyIds.map(async (id) => { + const info = await this.validator.getPasskey(accountAddress, id) + return { passkeyId: id, ...info } + }) + ) + + return { + passkeyIds, + passkeys: passkeys.filter(p => p.active), + total: passkeyIds.length, + } + } catch (e) { + console.warn('Failed to get passkeys:', e) + return { passkeyIds: [], passkeys: [], total: 0 } + } + } + + /** + * Check if a module is installed + */ + async isModuleInstalled(accountAddress, moduleTypeId, moduleAddress) { + try { + const account = this.getAccountContract(accountAddress) + return await account.isModuleInstalled(moduleTypeId, moduleAddress, '0x') + } catch (e) { + return false + } + } + + /** + * Get installed validator address + */ + async getInstalledValidator(accountAddress) { + try { + const account = this.getAccountContract(accountAddress) + return await account.getValidator() + } catch (e) { + return null + } + } + + /** + * Get global hook address + */ + async getGlobalHook(accountAddress) { + try { + const account = this.getAccountContract(accountAddress) + return await account.getGlobalHook() + } catch (e) { + return null + } + } +} + +/** + * SessionKeyManager for managing session keys + */ +export class SessionKeyManager { + constructor(moduleAddress, provider) { + this.moduleAddress = moduleAddress + this.provider = provider + this.module = new ethers.Contract(moduleAddress, SESSION_KEY_EXECUTOR_ABI, provider) + } + + /** + * Get all session keys for an account + */ + async getSessionKeys(accountAddress) { + try { + const keys = await this.module.getSessionKeys(accountAddress) + const sessionKeys = await Promise.all( + keys.map(async (key) => { + const data = await this.module.getSessionKey(accountAddress, key) + const allowedTargets = await this.module.getAllowedTargets(accountAddress, key) + const allowedSelectors = await this.module.getAllowedSelectors(accountAddress, key) + return { + address: key, + active: data.active, + validAfter: Number(data.validAfter), + validUntil: Number(data.validUntil), + spendLimitPerTx: data.spendLimitPerTx, + spendLimitTotal: data.spendLimitTotal, + spentTotal: data.spentTotal, + nonce: data.nonce, + allowedTargets, + allowedSelectors, + } + }) + ) + return sessionKeys.filter(k => k.active) + } catch (e) { + console.warn('Failed to get session keys:', e) + return [] + } + } + + /** + * Get session key count + */ + async getSessionKeyCount(accountAddress) { + try { + return Number(await this.module.getSessionKeyCount(accountAddress)) + } catch (e) { + return 0 + } + } + + /** + * Check if session key is valid + */ + async isSessionKeyValid(accountAddress, sessionKeyAddress) { + try { + return await this.module.isSessionKeyValid(accountAddress, sessionKeyAddress) + } catch (e) { + return false + } + } + + /** + * Encode createSessionKey call for execution via account.execute + */ + encodeCreateSessionKey(permission) { + return this.module.interface.encodeFunctionData('createSessionKey', [permission]) + } + + /** + * Encode revokeSessionKey call for execution via account.execute + */ + encodeRevokeSessionKey(sessionKeyAddress) { + return this.module.interface.encodeFunctionData('revokeSessionKey', [sessionKeyAddress]) + } +} + +/** + * Create modular account manager instance + */ +export function createModularAccountManager(factoryAddress, validatorAddress, provider) { + return new ModularAccountManager(factoryAddress, validatorAddress, provider) +} + +/** + * Create session key manager instance + */ +export function createSessionKeyManager(moduleAddress, provider) { + return new SessionKeyManager(moduleAddress, provider) +} + diff --git a/frontend/src/screens/HomeScreen.jsx b/frontend/src/screens/HomeScreen.jsx index 21b1dd8..f2ac598 100644 --- a/frontend/src/screens/HomeScreen.jsx +++ b/frontend/src/screens/HomeScreen.jsx @@ -1,8 +1,9 @@ import { useState, useEffect, useRef } from 'react' -import { TrendingUp, TrendingDown, AlertTriangle, Lightbulb, ArrowLeftRight, ArrowUp, ArrowDown, MoreVertical, Plus, Pencil, Trash2, Wallet } from 'lucide-react' +import { TrendingUp, TrendingDown, AlertTriangle, Lightbulb, ArrowLeftRight, ArrowUp, ArrowDown, MoreVertical, Plus, Pencil, Trash2, Wallet, Layers } from 'lucide-react' import { useWeb3Auth } from '../contexts/Web3AuthContext' import { useNetwork } from '../contexts/NetworkContext' import { useP256SDK } from '../hooks/useP256SDK' +import { useModularAccountManager } from '../hooks/useModularAccount' import { ethers } from 'ethers' import Header from '../components/Header' import { Identicon } from '../utils/identicon.jsx' @@ -19,6 +20,7 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap const { userInfo, address: ownerAddress } = useWeb3Auth() const { networkInfo } = useNetwork() const sdk = useP256SDK() + const modularManager = useModularAccountManager() const [wallets, setWallets] = useState([]) const [loading, setLoading] = useState(false) const [showAddModal, setShowAddModal] = useState(false) @@ -28,6 +30,7 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap const [walletIndex, setWalletIndex] = useState('0') const [addError, setAddError] = useState('') const [isAdding, setIsAdding] = useState(false) + const [accountType, setAccountType] = useState('modular') // 'legacy' or 'modular' // Menu and modal states const [openMenuId, setOpenMenuId] = useState(null) @@ -365,8 +368,13 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap return } - // Check if SDK is ready - if (!sdk) { + // Check if SDK is ready (for legacy) or modular manager (for modular) + const isModular = accountType === 'modular' + if (isModular && !modularManager) { + setAddError('Modular accounts not available on this network. Please use legacy account type.') + return + } + if (!isModular && !sdk) { setAddError('SDK not initialized. Please try again.') return } @@ -374,92 +382,66 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap // Get existing wallets to check for duplicate index const walletsList = JSON.parse(localStorage.getItem('ethaura_wallets_list') || '[]') - // Check if this index has already been used by this owner - const existingWallet = walletsList.find(w => w.index === indexNum && w.owner === ownerAddress) + // Check if this index has already been used by this owner (for same account type) + const existingWallet = walletsList.find(w => + w.index === indexNum && + w.owner === ownerAddress && + (w.isModular || false) === isModular + ) if (existingWallet) { setAddError(`Index ${indexNum} is already used for this owner. The wallet already exists at ${existingWallet.address}. Please use a different index (e.g., ${indexNum + 1}).`) return } - // Note: Same index with different owner is OK (will produce different address) - const sameIndexDifferentOwner = walletsList.find(w => w.index === indexNum && w.owner && w.owner !== ownerAddress) - if (sameIndexDifferentOwner) { - console.log('ℹ️ Same index with different owner - this is OK, will produce different address:', { - existingOwner: sameIndexDifferentOwner.owner, - currentOwner: ownerAddress, - index: indexNum, - }) - } - setIsAdding(true) setAddError('') // Clear any previous errors try { - console.log('🔧 Creating new wallet with index:', { - owner: ownerAddress, - index: indexNum, - name: walletName.trim(), - }) - - // Create account with owner-only mode (no passkey) - // User can add passkey later via Settings > Device Management const saltBigInt = BigInt(indexNum) + let accountAddress + let isDeployed = false - console.log('🧂 Salt calculation:', { - indexNum, - saltBigInt: saltBigInt.toString(), + console.log('🔧 Creating new wallet:', { owner: ownerAddress, - expectedSalt: `keccak256(${ownerAddress}, ${saltBigInt.toString()})`, + index: indexNum, + name: walletName.trim(), + type: isModular ? 'modular (ERC-7579)' : 'legacy (P256Account)', }) - console.log('Calling sdk.createAccount...') - - // Add timeout to prevent indefinite hanging - const createAccountPromise = sdk.createAccount( - null, // no passkey - owner-only mode - ownerAddress, - saltBigInt, - false // 2FA disabled - ) - - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Account creation timed out after 45 seconds. Please check your network connection and try again.')), 45000) - ) - - const accountData = await Promise.race([createAccountPromise, timeoutPromise]) - console.log('sdk.createAccount completed') + if (isModular) { + // Create modular account (ERC-7579 AuraAccount) + console.log('🏗️ Creating modular account via AuraAccountFactory...') + accountAddress = await modularManager.getAccountAddress(ownerAddress, saltBigInt) + isDeployed = await modularManager.isDeployed(accountAddress) + console.log('📍 Modular account address:', accountAddress, 'deployed:', isDeployed) + } else { + // Create legacy account (P256Account) + console.log('🏗️ Creating legacy account via P256AccountFactory...') + const createAccountPromise = sdk.createAccount( + null, // no passkey - owner-only mode + ownerAddress, + saltBigInt, + false // 2FA disabled + ) - console.log('📍 New wallet created:', { - address: accountData.address, - isDeployed: accountData.isDeployed, - owner: ownerAddress, - salt: saltBigInt.toString(), - index: indexNum, - }) + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Account creation timed out after 45 seconds.')), 45000) + ) - // Verify: same salt should give same address - console.log('Address determinism check:', { - message: 'Same owner + same salt should ALWAYS give this address', - address: accountData.address, - formula: `CREATE2(factory, keccak256(owner=${ownerAddress}, salt=${indexNum}), initCodeHash)`, - }) + const accountData = await Promise.race([createAccountPromise, timeoutPromise]) + accountAddress = accountData.address + isDeployed = accountData.isDeployed + console.log('📍 Legacy account address:', accountAddress, 'deployed:', isDeployed) + } // Double-check if wallet already exists (by address) - const exists = walletsList.some(w => w.address.toLowerCase() === accountData.address.toLowerCase()) + const exists = walletsList.some(w => w.address.toLowerCase() === accountAddress.toLowerCase()) if (exists) { setAddError('This wallet already exists in your list') setIsAdding(false) return } - // If account is already deployed on-chain, show a note - if (accountData.isDeployed) { - console.log('ℹ️ Account already deployed on-chain:', { - address: accountData.address, - message: 'This account was previously deployed (possibly from another device or session)', - }) - } - // Fetch balance for the new wallet console.log('Fetching balance...') const provider = new ethers.JsonRpcProvider(networkInfo.rpcUrl) @@ -469,20 +451,13 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap let totalBalanceUSD = 0 try { - // Fetch all token balances (ETH + ERC20 tokens) with real prices - const tokenBalances = await tokenService.getAllTokenBalances(accountData.address, false, true) - - // Calculate total portfolio value in USD + const tokenBalances = await tokenService.getAllTokenBalances(accountAddress, false, true) totalBalanceUSD = tokenBalances.reduce((sum, token) => sum + (token.valueUSD || 0), 0) - - // Get ETH balance for display const ethToken = tokenBalances.find(t => t.symbol === 'ETH') balanceEth = ethToken ? ethToken.amount.toString() : '0' - console.log('Balance fetched:', balanceEth, 'ETH, Total USD:', totalBalanceUSD) } catch (balanceError) { console.warn('Failed to fetch balance, using 0.0:', balanceError.message) - // Continue with 0 balance instead of failing the whole operation } const percentChange = (Math.random() * 4 - 2).toFixed(2) @@ -491,13 +466,14 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap const newWallet = { id: Date.now().toString(), name: walletName.trim(), - address: accountData.address, + address: accountAddress, balance: balanceEth, balanceUSD: totalBalanceUSD.toFixed(2), percentChange, - index: indexNum, // Store the index for reference - owner: ownerAddress, // Store the owner address used to create this wallet + index: indexNum, + owner: ownerAddress, createdAt: new Date().toISOString(), + isModular, // Track account type } walletsList.push(newWallet) @@ -513,6 +489,7 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap setWalletIndex('0') setAddError('') setModalMode('import') + setAccountType('modular') } catch (err) { console.error('Error creating wallet:', err) setAddError(err.message || 'Failed to create wallet. Please try again.') @@ -802,6 +779,7 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap setWalletName('') setWalletIndex('0') setAddError('') + setAccountType('modular') }}>
e.stopPropagation()}>
@@ -813,6 +791,7 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap setWalletName('') setWalletIndex('0') setAddError('') + setAccountType('modular') }}>×
@@ -890,6 +869,48 @@ function HomeScreen({ onWalletClick, onAddWallet, onCreateWallet, onSend, onSwap />
+ {/* Account Type Selector */} +
+ +
+ + +
+ {!modularManager && ( +

Modular accounts not available on this network

+ )} +
+
@@ -141,6 +169,33 @@ function WalletSettingsScreen({ wallet, onBack, onHome, onLogout, credential, on )} + {activeTab === 'modules' && ( +
+ +
+ )} + + {activeTab === 'sessionkeys' && ( +
+ +
+ )} + + {activeTab === 'spending' && ( +
+ +
+ )} + diff --git a/frontend/src/styles/HomeScreen.css b/frontend/src/styles/HomeScreen.css index 8f6e195..eaa3662 100644 --- a/frontend/src/styles/HomeScreen.css +++ b/frontend/src/styles/HomeScreen.css @@ -1180,3 +1180,49 @@ background: #1f1f1f; border-color: #1f1f1f; } + +/* Account Type Selector */ +.account-type-selector { + display: flex; + gap: 12px; + margin-top: 8px; +} + +.account-type-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 16px; + background: #f9fafb; + border: 2px solid #e5e7eb; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + color: #6b7280; + cursor: pointer; + transition: all 0.2s ease; +} + +.account-type-btn:hover:not(:disabled) { + background: #f3f4f6; + border-color: #d1d5db; +} + +.account-type-btn.active { + background: #f0f9ff; + border-color: #3b82f6; + color: #1d4ed8; +} + +.account-type-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.form-hint.warning { + color: #f59e0b; + font-size: 12px; + margin-top: 4px; +} diff --git a/frontend/src/styles/ModuleManager.css b/frontend/src/styles/ModuleManager.css new file mode 100644 index 0000000..23e4638 --- /dev/null +++ b/frontend/src/styles/ModuleManager.css @@ -0,0 +1,190 @@ +.module-manager { + padding: 24px; +} + +.module-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.module-header .header-icon { + color: #3b82f6; +} + +.module-header h2 { + font-size: 24px; + font-weight: 600; + color: #111827; + margin: 0; +} + +.module-description { + color: #6b7280; + margin-bottom: 24px; +} + +.module-description p { + margin: 0; +} + +.module-info-message, +.module-loading, +.module-error { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background: #f9fafb; + border-radius: 8px; + color: #6b7280; +} + +.module-error { + background: #fef2f2; + color: #dc2626; +} + +.module-error button { + margin-left: auto; + padding: 8px 16px; + background: #dc2626; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; +} + +.module-loading .spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.module-section { + margin-bottom: 24px; +} + +.module-section h3 { + font-size: 16px; + font-weight: 600; + color: #374151; + margin: 0 0 8px 0; +} + +.section-description { + font-size: 14px; + color: #6b7280; + margin: 0 0 12px 0; +} + +.status-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.status-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #f9fafb; + border-radius: 8px; + border: 1px solid #e5e7eb; +} + +.status-label { + font-size: 14px; + color: #6b7280; +} + +.status-value { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 500; + color: #9ca3af; +} + +.status-value.active { + color: #10b981; +} + +.module-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.module-card { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + transition: border-color 0.2s; +} + +.module-card:hover { + border-color: #d1d5db; +} + +.module-icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: #f0f9ff; + border-radius: 8px; + color: #3b82f6; +} + +.module-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.module-name { + font-size: 14px; + font-weight: 500; + color: #111827; +} + +.module-address { + font-size: 12px; + color: #9ca3af; + font-family: monospace; +} + +.module-status { + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.module-status.installed { + background: #d1fae5; + color: #059669; +} + +.module-empty { + padding: 24px; + text-align: center; + color: #9ca3af; + background: #f9fafb; + border-radius: 8px; + border: 1px dashed #e5e7eb; +} + diff --git a/frontend/src/styles/SessionKeyManager.css b/frontend/src/styles/SessionKeyManager.css new file mode 100644 index 0000000..4bb24c9 --- /dev/null +++ b/frontend/src/styles/SessionKeyManager.css @@ -0,0 +1,273 @@ +.session-key-manager { + padding: 24px; +} + +.session-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.session-header .header-icon { + color: #3b82f6; +} + +.session-header h2 { + font-size: 24px; + font-weight: 600; + color: #111827; + margin: 0; + flex: 1; +} + +.add-session-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background: #3b82f6; + color: white; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.add-session-btn:hover { + background: #2563eb; +} + +.session-description { + color: #6b7280; + margin-bottom: 24px; +} + +.session-description p { + margin: 0; +} + +.session-info-message, +.session-loading, +.session-error { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background: #f9fafb; + border-radius: 8px; + color: #6b7280; +} + +.session-error { + background: #fef2f2; + color: #dc2626; +} + +.session-error button { + margin-left: auto; + padding: 8px 16px; + background: #dc2626; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; +} + +.session-loading .spinner { + animation: spin 1s linear infinite; +} + +.session-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px; + text-align: center; + color: #9ca3af; + background: #f9fafb; + border-radius: 8px; + border: 1px dashed #e5e7eb; +} + +.session-empty p { + margin: 12px 0 4px; + font-weight: 500; + color: #6b7280; +} + +.session-empty span { + font-size: 14px; +} + +.session-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.session-card { + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + overflow: hidden; +} + +.session-card.inactive { + opacity: 0.6; +} + +.session-card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background: #f9fafb; + border-bottom: 1px solid #e5e7eb; +} + +.session-address { + display: flex; + align-items: center; + gap: 8px; +} + +.session-address .address { + font-family: monospace; + font-size: 14px; + font-weight: 500; + color: #111827; +} + +.session-status { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.session-status.active { + background: #d1fae5; + color: #059669; +} + +.session-status.inactive { + background: #fee2e2; + color: #dc2626; +} + +.session-card-body { + padding: 16px; +} + +.session-info-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + font-size: 14px; + color: #6b7280; +} + +.session-info-row .label { + color: #9ca3af; +} + +.session-info-row .value { + color: #374151; +} + +.session-info-row .value.expired { + color: #dc2626; +} + +.session-info-row .value.pending { + color: #f59e0b; +} + +.session-targets { + margin-top: 12px; +} + +.session-targets .label { + display: block; + font-size: 12px; + color: #9ca3af; + margin-bottom: 6px; +} + +.target-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.target-badge { + padding: 4px 8px; + background: #f3f4f6; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + color: #6b7280; +} + +.session-card-actions { + padding: 12px 16px; + border-top: 1px solid #e5e7eb; + display: flex; + justify-content: flex-end; +} + +.revoke-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background: #fee2e2; + color: #dc2626; + border: none; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + transition: background 0.2s; +} + +.revoke-btn:hover:not(:disabled) { + background: #fecaca; +} + +.revoke-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Modal styles */ +.session-modal { + max-width: 500px; +} + +.coming-soon { + display: flex; + flex-direction: column; + align-items: center; + padding: 48px 24px; + text-align: center; + color: #9ca3af; +} + +.coming-soon h3 { + margin: 16px 0 8px; + color: #374151; +} + +.coming-soon p { + margin: 0; + color: #6b7280; +} + diff --git a/frontend/src/styles/SpendingLimitConfig.css b/frontend/src/styles/SpendingLimitConfig.css new file mode 100644 index 0000000..4d57313 --- /dev/null +++ b/frontend/src/styles/SpendingLimitConfig.css @@ -0,0 +1,222 @@ +.spending-limit-config { + padding: 24px; +} + +.spending-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.spending-header .header-icon { + color: #3b82f6; +} + +.spending-header h2 { + font-size: 24px; + font-weight: 600; + color: #111827; + margin: 0; +} + +.spending-description { + color: #6b7280; + margin-bottom: 24px; +} + +.spending-description p { + margin: 0; +} + +.spending-info-message, +.spending-loading, +.spending-error { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background: #f9fafb; + border-radius: 8px; + color: #6b7280; +} + +.spending-error { + background: #fef2f2; + color: #dc2626; +} + +.spending-error button { + margin-left: auto; + padding: 8px 16px; + background: #dc2626; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; +} + +.spending-loading .spinner { + animation: spin 1s linear infinite; +} + +.spending-section { + margin-bottom: 24px; +} + +.section-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.section-header h3 { + font-size: 16px; + font-weight: 600; + color: #374151; + margin: 0; +} + +.section-description { + font-size: 14px; + color: #6b7280; + margin: 0 0 12px 0; +} + +.config-card { + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 16px; +} + +.config-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.config-row:last-child { + margin-bottom: 0; +} + +.config-row label { + font-size: 14px; + font-weight: 500; + color: #374151; +} + +.input-group { + display: flex; + align-items: center; + gap: 8px; +} + +.input-group input, +.input-group select { + padding: 8px 12px; + border: 1px solid #e5e7eb; + border-radius: 6px; + font-size: 14px; + width: 120px; + text-align: right; +} + +.input-group input:disabled, +.input-group select:disabled { + background: #f9fafb; + color: #9ca3af; + cursor: not-allowed; +} + +.input-suffix { + font-size: 14px; + color: #6b7280; + font-weight: 500; +} + +.config-status, +.config-info { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + background: #f0f9ff; + border-radius: 8px; + font-size: 13px; + color: #1d4ed8; + margin-top: 12px; +} + +.config-info { + background: #fefce8; + color: #a16207; +} + +.module-status-card { + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 16px; +} + +.status-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.status-label { + font-size: 14px; + font-weight: 500; + color: #374151; +} + +.status-badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.status-badge.installed { + background: #d1fae5; + color: #059669; +} + +.status-badge.not-installed { + background: #f3f4f6; + color: #6b7280; +} + +.status-description { + font-size: 13px; + color: #6b7280; + margin: 0 0 12px 0; +} + +.install-module-btn { + width: 100%; + padding: 12px; + background: #3b82f6; + color: white; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.install-module-btn:hover:not(:disabled) { + background: #2563eb; +} + +.install-module-btn:disabled { + background: #9ca3af; + cursor: not-allowed; +} + From bbbb10b1f9fba7618911970392ce029c992d544e Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 3 Dec 2025 15:17:05 +0700 Subject: [PATCH 22/22] feat(contracts): add comprehensive fuzz and invariant testing (#163) - Configure Foundry fuzz settings (1000 runs, dictionary weight 40) - Configure invariant testing (256 runs, depth 15) Fuzz Tests (43 tests): - AuraAccount: execution, module installation, access control - P256MFAValidator: passkey management, signature validation, MFA toggle - SessionKeyExecutor: validity periods, spending limits, permissions - SocialRecovery: guardian management, timelock, recovery flow Invariant Tests (9 tests): - Account always has owner and at least one passkey - Validator module always installed - Session key tracking consistency - Account balance non-negative All 303 tests pass including 52 new fuzz/invariant tests. Closes #163 --- foundry.toml | 9 + test/modular/fuzz/AuraAccount.fuzz.t.sol | 304 ++++++++++++++++ test/modular/fuzz/P256MFAValidator.fuzz.t.sol | 323 ++++++++++++++++ .../fuzz/SessionKeyExecutor.fuzz.t.sol | 344 ++++++++++++++++++ test/modular/fuzz/SocialRecovery.fuzz.t.sol | 343 +++++++++++++++++ .../invariant/AuraAccountInvariant.t.sol | 134 +++++++ .../invariant/handlers/AccountHandler.sol | 143 ++++++++ 7 files changed, 1600 insertions(+) create mode 100644 test/modular/fuzz/AuraAccount.fuzz.t.sol create mode 100644 test/modular/fuzz/P256MFAValidator.fuzz.t.sol create mode 100644 test/modular/fuzz/SessionKeyExecutor.fuzz.t.sol create mode 100644 test/modular/fuzz/SocialRecovery.fuzz.t.sol create mode 100644 test/modular/invariant/AuraAccountInvariant.t.sol create mode 100644 test/modular/invariant/handlers/AccountHandler.sol diff --git a/foundry.toml b/foundry.toml index e0af161..d8b19e1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -27,3 +27,12 @@ base-sepolia = { key = "${ETHERSCAN_API_KEY}", url = "https://api-sepolia.basesc # See more config options https://github.com/foundry-rs/foundry/tree/master/config +[fuzz] +runs = 1000 # Number of fuzz runs per test +max_test_rejects = 65536 # Max inputs rejected before stopping +dictionary_weight = 40 # Weight for dictionary-based fuzzing + +[invariant] +runs = 256 # Invariant test runs +depth = 15 # Maximum call depth per run +fail_on_revert = false # Don't fail on reverts diff --git a/test/modular/fuzz/AuraAccount.fuzz.t.sol b/test/modular/fuzz/AuraAccount.fuzz.t.sol new file mode 100644 index 0000000..daab493 --- /dev/null +++ b/test/modular/fuzz/AuraAccount.fuzz.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../../src/modular/AuraAccountFactory.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; + +import {IERC7579Account, Execution} from "@erc7579/interfaces/IERC7579Account.sol"; +import { + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK +} from "@erc7579/interfaces/IERC7579Module.sol"; +import { + ModeLib, + ModeCode, + CallType, + ExecType, + ModeSelector, + ModePayload, + CALLTYPE_SINGLE, + CALLTYPE_BATCH, + CALLTYPE_STATIC, + CALLTYPE_DELEGATECALL, + EXECTYPE_DEFAULT, + EXECTYPE_TRY, + MODE_DEFAULT +} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +import {MockValidator} from "../mocks/MockValidator.sol"; +import {MockExecutor} from "../mocks/MockExecutor.sol"; +import {MockHook} from "../mocks/MockHook.sol"; +import {MockTarget} from "../mocks/MockTarget.sol"; + +/** + * @title AuraAccount Fuzz Tests + * @notice Fuzz tests for the AuraAccount ERC-7579 modular smart account + */ +contract AuraAccountFuzzTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + MockExecutor public executor; + MockHook public hook; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address owner = address(0x1234); + + function setUp() public { + // Deploy canonical ERC1967Factory if not already deployed + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + // Deploy mock modules + validator = new MockValidator(); + executor = new MockExecutor(); + hook = new MockHook(); + target = new MockTarget(); + + // Deploy factory with the mandatory P256MFAValidator (using mock for tests) + factory = new AuraAccountFactory(address(validator)); + + // Create account + address accountAddr = factory.createAccount(owner, abi.encode(true), address(0), "", 0); + account = AuraAccount(payable(accountAddr)); + + // Fund account generously + vm.deal(address(account), 1000 ether); + } + + /*////////////////////////////////////////////////////////////// + EXECUTION VALUE FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test for single execution with various ETH values + function testFuzz_ExecuteSingleWithValue(uint256 amount) public { + // Bound amount to account balance + amount = bound(amount, 0, address(account).balance); + + bytes memory callData = abi.encodeCall(MockTarget.setValue, (100)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), amount, callData); + + uint256 targetBalanceBefore = address(target).balance; + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + + assertEq(target.value(), 100); + assertEq(address(target).balance, targetBalanceBefore + amount); + } + + /// @notice Fuzz test for setting arbitrary values via execution + function testFuzz_ExecuteSetValue(uint256 value) public { + bytes memory callData = abi.encodeCall(MockTarget.setValue, (value)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, callData); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + + assertEq(target.value(), value); + } + + /// @notice Fuzz test for batch execution with multiple values + function testFuzz_ExecuteBatchWithValues(uint8 batchSize, uint256 seed) public { + // Bound batch size to reasonable range + batchSize = uint8(bound(batchSize, 1, 10)); + + Execution[] memory executions = new Execution[](batchSize); + uint256 totalValue; + + for (uint256 i = 0; i < batchSize; i++) { + uint256 value = uint256(keccak256(abi.encode(seed, i))) % 1 ether; + totalValue += value; + executions[i] = + Execution({target: address(target), value: value, callData: abi.encodeCall(MockTarget.setValue, (i))}); + } + + // Ensure we don't exceed account balance + vm.assume(totalValue <= address(account).balance); + + bytes memory executionData = ExecutionLib.encodeBatch(executions); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleBatch(), executionData); + + // Last value set should be batchSize - 1 + assertEq(target.value(), batchSize - 1); + assertEq(target.callCount(), batchSize); + } + + /*////////////////////////////////////////////////////////////// + MODULE INSTALLATION FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test for installing multiple executors + function testFuzz_InstallMultipleExecutors(uint8 count) public { + // Bound count to reasonable range + count = uint8(bound(count, 1, 10)); + + for (uint256 i = 0; i < count; i++) { + MockExecutor newExecutor = new MockExecutor(); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(newExecutor), ""); + + assertTrue(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(newExecutor), "")); + assertTrue(newExecutor.isInitialized(address(account))); + } + } + + /// @notice Fuzz test for install and uninstall sequence + function testFuzz_ModuleInstallUninstallSequence(uint256 seed) public { + MockExecutor[] memory executors = new MockExecutor[](5); + + // Install 5 executors + for (uint256 i = 0; i < 5; i++) { + executors[i] = new MockExecutor(); + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(executors[i]), ""); + } + + // Randomly uninstall and verify + for (uint256 i = 0; i < 5; i++) { + // Use seed to determine if we should uninstall + bool shouldUninstall = uint256(keccak256(abi.encode(seed, i))) % 2 == 0; + + if (shouldUninstall) { + vm.prank(ENTRYPOINT); + account.uninstallModule(MODULE_TYPE_EXECUTOR, address(executors[i]), ""); + assertFalse(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(executors[i]), "")); + } else { + assertTrue(account.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(executors[i]), "")); + } + } + } + + /*////////////////////////////////////////////////////////////// + UNSUPPORTED MODE FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test for unsupported call types + function testFuzz_RevertUnsupportedCallType(bytes1 callTypeByte) public { + // Skip valid call types + vm.assume(callTypeByte != bytes1(0x00)); // SINGLE + vm.assume(callTypeByte != bytes1(0x01)); // BATCH + vm.assume(callTypeByte != bytes1(0xfe)); // STATIC + // 0xff (DELEGATECALL) should also revert but with different error + + CallType callType = CallType.wrap(callTypeByte); + ModeCode mode = ModeLib.encode(callType, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00)); + + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, ""); + + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.UnsupportedExecutionMode.selector, mode)); + account.execute(mode, executionData); + } + + /// @notice Fuzz test that delegatecall is always rejected + function testFuzz_RevertDelegatecall(uint256 value) public { + value = bound(value, 0, 1 ether); + + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), value, callData); + + ModeCode delegatecallMode = + ModeLib.encode(CALLTYPE_DELEGATECALL, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00)); + + vm.prank(ENTRYPOINT); + vm.expectRevert(abi.encodeWithSelector(AuraAccount.UnsupportedExecutionMode.selector, delegatecallMode)); + account.execute(delegatecallMode, executionData); + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test that random addresses cannot execute + function testFuzz_RevertExecuteFromRandomAddress(address caller) public { + vm.assume(caller != ENTRYPOINT); + vm.assume(caller != address(account)); + // Exclude ERC1967Factory address + vm.assume(caller != ERC1967FactoryConstants.ADDRESS); + + bytes memory executionData = ExecutionLib.encodeSingle(address(target), 0, ""); + + vm.prank(caller); + vm.expectRevert(AuraAccount.OnlyEntryPointOrSelf.selector); + account.execute(ModeLib.encodeSimpleSingle(), executionData); + } + + /// @notice Fuzz test that random addresses cannot install modules + function testFuzz_RevertInstallModuleFromRandomAddress(address caller) public { + vm.assume(caller != ENTRYPOINT); + vm.assume(caller != address(account)); + // Exclude ERC1967Factory address + vm.assume(caller != ERC1967FactoryConstants.ADDRESS); + + MockExecutor newExecutor = new MockExecutor(); + + vm.prank(caller); + vm.expectRevert(AuraAccount.OnlyEntryPointOrSelf.selector); + account.installModule(MODULE_TYPE_EXECUTOR, address(newExecutor), ""); + } + + /*////////////////////////////////////////////////////////////// + TRY EXECUTION FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test that try mode doesn't revert on failed calls + function testFuzz_ExecuteTryDoesNotRevert(uint256 value, bool shouldRevert) public { + value = bound(value, 0, 100 ether); + vm.assume(value <= address(account).balance); + + target.setShouldRevert(shouldRevert); + + bytes memory callData = abi.encodeCall(MockTarget.setValue, (42)); + bytes memory executionData = ExecutionLib.encodeSingle(address(target), value, callData); + + ModeCode tryMode = ModeLib.encode(CALLTYPE_SINGLE, EXECTYPE_TRY, MODE_DEFAULT, ModePayload.wrap(0x00)); + + // Should not revert regardless of target behavior + vm.prank(ENTRYPOINT); + account.execute(tryMode, executionData); + + if (!shouldRevert) { + assertEq(target.value(), 42); + } + } + + /*////////////////////////////////////////////////////////////// + ACCOUNT FACTORY FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test for creating accounts with different salts + function testFuzz_CreateAccountWithDifferentSalts(uint256 salt) public { + address accountAddr = factory.createAccount(owner, abi.encode(true), address(0), "", salt); + + // Account should be deployed + assertTrue(accountAddr.code.length > 0); + + // Should get same address with same parameters + address sameAddr = factory.createAccount(owner, abi.encode(true), address(0), "", salt); + assertEq(accountAddr, sameAddr); + } + + /// @notice Fuzz test that different owners get different addresses + function testFuzz_DifferentOwnersGetDifferentAddresses(address owner1, address owner2, uint256 salt) public { + vm.assume(owner1 != owner2); + vm.assume(owner1 != address(0)); + vm.assume(owner2 != address(0)); + + address account1 = factory.createAccount(owner1, abi.encode(true), address(0), "", salt); + address account2 = factory.createAccount(owner2, abi.encode(true), address(0), "", salt); + + assertNotEq(account1, account2); + } +} + diff --git a/test/modular/fuzz/P256MFAValidator.fuzz.t.sol b/test/modular/fuzz/P256MFAValidator.fuzz.t.sol new file mode 100644 index 0000000..0dee5f7 --- /dev/null +++ b/test/modular/fuzz/P256MFAValidator.fuzz.t.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../../src/modular/AuraAccountFactory.sol"; +import {P256MFAValidatorModule} from "../../../src/modular/modules/validators/P256MFAValidatorModule.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; + +import {MODULE_TYPE_VALIDATOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; + +/** + * @title P256MFAValidatorModule Fuzz Tests + * @notice Fuzz tests for signature validation and passkey management + */ +contract P256MFAValidatorFuzzTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + P256MFAValidatorModule public validator; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + uint256 ownerPrivateKey = 0x1234; + address owner; + + // Test passkey coordinates + bytes32 constant QX = 0x65a2fa44daad46eab0278703edb6c4dcf5e30b8a9aec09fdc71a56f52aa392e4; + bytes32 constant QY = 0x4a7a9e4604aa36898209997288e902ac544a555e4b5e0a9efef2b59233f3f437; + + function setUp() public { + owner = vm.addr(ownerPrivateKey); + + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + validator = new P256MFAValidatorModule(); + factory = new AuraAccountFactory(address(validator)); + + bytes memory initData = abi.encode(owner, QX, QY, bytes32("Test Device"), true); + + address accountAddr = factory.createAccount(owner, initData, address(0), "", 0); + account = AuraAccount(payable(accountAddr)); + + vm.deal(address(account), 10 ether); + } + + /*////////////////////////////////////////////////////////////// + SIGNATURE VALIDATION FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test that random signatures are rejected + function testFuzz_InvalidSignatureRejected(bytes memory signature) public view { + // Any random signature should fail validation + bytes32 hash = keccak256("test message"); + + // With MFA enabled, we need owner signature + passkey signature + // Random bytes should not form a valid signature + bytes4 result = account.isValidSignature(hash, signature); + + // Random signatures should be invalid (not return magic value) + // Note: There's a small chance random bytes could be valid, but extremely unlikely + assertTrue(result == bytes4(0xffffffff) || signature.length == 0, "Random signature should not be valid"); + } + + /// @notice Fuzz test owner-only signature validation (MFA disabled) + function testFuzz_ValidOwnerSignature(bytes32 hash) public { + // Disable MFA first + vm.prank(address(account)); + validator.disableMFA(); + + // Sign with owner private key - validator expects raw hash, not eth signed hash + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + // Signature format: [validator address (20 bytes)][actual signature (65 bytes)] + bytes memory signature = abi.encodePacked(address(validator), r, s, v); + + bytes4 result = account.isValidSignature(hash, signature); + assertEq(result, bytes4(0x1626ba7e), "Valid owner signature should be accepted"); + } + + /// @notice Fuzz test wrong owner signature is rejected + function testFuzz_WrongOwnerSignatureRejected(uint256 wrongPrivateKey, bytes32 hash) public { + // Ensure wrong private key is different from owner's + vm.assume(wrongPrivateKey != 0); + vm.assume(wrongPrivateKey != ownerPrivateKey); + vm.assume(wrongPrivateKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337); + + // Disable MFA + vm.prank(address(account)); + validator.disableMFA(); + + // Sign with wrong private key + bytes32 ethSignedHash = ECDSA.toEthSignedMessageHash(hash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + bytes4 result = account.isValidSignature(hash, signature); + assertEq(result, bytes4(0xffffffff), "Wrong owner signature should be rejected"); + } + + /*////////////////////////////////////////////////////////////// + PASSKEY MANAGEMENT FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test adding passkeys with random coordinates + function testFuzz_AddPasskeyWithRandomCoordinates(bytes32 qx, bytes32 qy) public { + // Must be non-zero + vm.assume(qx != bytes32(0)); + vm.assume(qy != bytes32(0)); + // Must not be the existing passkey + vm.assume(qx != QX || qy != QY); + + uint256 countBefore = validator.getPasskeyCount(address(account)); + + vm.prank(address(account)); + validator.addPasskey(qx, qy, "Fuzz Passkey"); + + assertEq(validator.getPasskeyCount(address(account)), countBefore + 1); + } + + /// @notice Fuzz test that duplicate passkeys are rejected + function testFuzz_DuplicatePasskeyRejected(bytes32 qx, bytes32 qy) public { + vm.assume(qx != bytes32(0)); + vm.assume(qy != bytes32(0)); + vm.assume(qx != QX || qy != QY); + + // Add passkey first time + vm.prank(address(account)); + validator.addPasskey(qx, qy, "First"); + + // Try to add same passkey again + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.PasskeyAlreadyExists.selector); + validator.addPasskey(qx, qy, "Duplicate"); + } + + /// @notice Fuzz test that invalid passkeys (zero coords) are rejected + function testFuzz_InvalidPasskeyRejected(bool zeroQx, bytes32 nonZeroValue) public { + vm.assume(nonZeroValue != bytes32(0)); + + bytes32 qx; + bytes32 qy; + + if (zeroQx) { + qx = bytes32(0); + qy = nonZeroValue; + } else { + qx = nonZeroValue; + qy = bytes32(0); + } + + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.InvalidPasskey.selector); + validator.addPasskey(qx, qy, "Invalid"); + } + + /// @notice Test that both zero coords are rejected + function test_BothZeroPasskeyRejected() public { + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.InvalidPasskey.selector); + validator.addPasskey(bytes32(0), bytes32(0), "Invalid"); + } + + /// @notice Fuzz test adding multiple passkeys + function testFuzz_AddMultiplePasskeys(uint8 count) public { + count = uint8(bound(count, 1, 10)); + + uint256 initialCount = validator.getPasskeyCount(address(account)); + + for (uint256 i = 0; i < count; i++) { + bytes32 qx = bytes32(uint256(keccak256(abi.encode("qx", i)))); + bytes32 qy = bytes32(uint256(keccak256(abi.encode("qy", i)))); + + vm.prank(address(account)); + validator.addPasskey(qx, qy, bytes32(i)); + } + + assertEq(validator.getPasskeyCount(address(account)), initialCount + count); + } + + /*////////////////////////////////////////////////////////////// + OWNER MANAGEMENT FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test setting owner to various addresses + function testFuzz_SetOwner(address newOwner) public { + vm.assume(newOwner != address(0)); + + vm.prank(address(account)); + validator.setOwner(newOwner); + + assertEq(validator.getOwner(address(account)), newOwner); + } + + /// @notice Fuzz test that setting owner to zero reverts + function testFuzz_SetOwnerZeroReverts() public { + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.InvalidOwner.selector); + validator.setOwner(address(0)); + } + + /*////////////////////////////////////////////////////////////// + MFA TOGGLE FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test toggling MFA state + function testFuzz_ToggleMFA(uint8 toggleCount) public { + toggleCount = uint8(bound(toggleCount, 1, 10)); + + bool expectedState = true; // MFA starts enabled + + for (uint256 i = 0; i < toggleCount; i++) { + if (expectedState) { + vm.prank(address(account)); + validator.disableMFA(); + expectedState = false; + } else { + vm.prank(address(account)); + validator.enableMFA(); + expectedState = true; + } + + assertEq(validator.isMFAEnabled(address(account)), expectedState); + } + } + + /*////////////////////////////////////////////////////////////// + ACCOUNT CREATION FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test creating accounts with different owners + function testFuzz_CreateAccountWithDifferentOwners(uint256 ownerPrivKey, uint256 salt) public { + vm.assume(ownerPrivKey != 0); + vm.assume(ownerPrivKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337); + + address newOwner = vm.addr(ownerPrivKey); + bytes32 newQx = bytes32(uint256(keccak256(abi.encode("qx", salt)))); + bytes32 newQy = bytes32(uint256(keccak256(abi.encode("qy", salt)))); + + bytes memory initData = abi.encode(newOwner, newQx, newQy, bytes32("Device"), true); + + address newAccount = factory.createAccount(newOwner, initData, address(0), "", salt); + + assertTrue(newAccount.code.length > 0); + assertEq(validator.getOwner(newAccount), newOwner); + assertEq(validator.getPasskeyCount(newAccount), 1); + assertTrue(validator.isMFAEnabled(newAccount)); + } + + /// @notice Fuzz test creating accounts with MFA disabled + function testFuzz_CreateAccountMFADisabled(uint256 ownerPrivKey, uint256 salt) public { + vm.assume(ownerPrivKey != 0); + vm.assume(ownerPrivKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337); + + address newOwner = vm.addr(ownerPrivKey); + bytes32 newQx = bytes32(uint256(keccak256(abi.encode("qx", salt)))); + bytes32 newQy = bytes32(uint256(keccak256(abi.encode("qy", salt)))); + + // enableMFA = false + bytes memory initData = abi.encode(newOwner, newQx, newQy, bytes32("Device"), false); + + address newAccount = factory.createAccount(newOwner, initData, address(0), "", salt); + + assertFalse(validator.isMFAEnabled(newAccount)); + } + + /*////////////////////////////////////////////////////////////// + PASSKEY REMOVAL FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test removing passkeys (keeping at least one) + function testFuzz_RemovePasskeys(uint8 addCount, uint8 removeCount) public { + addCount = uint8(bound(addCount, 2, 5)); + removeCount = uint8(bound(removeCount, 1, addCount - 1)); // Keep at least one + + bytes32[] memory passkeyIds = new bytes32[](addCount); + + // Add passkeys + for (uint256 i = 0; i < addCount; i++) { + bytes32 qx = bytes32(uint256(keccak256(abi.encode("qx", i)))); + bytes32 qy = bytes32(uint256(keccak256(abi.encode("qy", i)))); + passkeyIds[i] = keccak256(abi.encodePacked(qx, qy)); + + vm.prank(address(account)); + validator.addPasskey(qx, qy, bytes32(i)); + } + + uint256 countAfterAdd = validator.getPasskeyCount(address(account)); + + // Remove passkeys + for (uint256 i = 0; i < removeCount; i++) { + vm.prank(address(account)); + validator.removePasskey(passkeyIds[i]); + } + + assertEq(validator.getPasskeyCount(address(account)), countAfterAdd - removeCount); + } + + /// @notice Fuzz test that removing last passkey reverts + function testFuzz_CannotRemoveLastPasskey() public { + // Account has exactly 1 passkey from setup + assertEq(validator.getPasskeyCount(address(account)), 1); + + bytes32 passkeyId = keccak256(abi.encodePacked(QX, QY)); + + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.CannotRemoveLastPasskey.selector); + validator.removePasskey(passkeyId); + } + + /// @notice Fuzz test removing non-existent passkey reverts + function testFuzz_RemoveNonExistentPasskeyReverts(bytes32 randomId) public { + bytes32 existingId = keccak256(abi.encodePacked(QX, QY)); + vm.assume(randomId != existingId); + + vm.prank(address(account)); + vm.expectRevert(P256MFAValidatorModule.PasskeyDoesNotExist.selector); + validator.removePasskey(randomId); + } +} + diff --git a/test/modular/fuzz/SessionKeyExecutor.fuzz.t.sol b/test/modular/fuzz/SessionKeyExecutor.fuzz.t.sol new file mode 100644 index 0000000..8b306fd --- /dev/null +++ b/test/modular/fuzz/SessionKeyExecutor.fuzz.t.sol @@ -0,0 +1,344 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../../src/modular/AuraAccountFactory.sol"; +import {SessionKeyExecutorModule} from "../../../src/modular/modules/executors/SessionKeyExecutorModule.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; + +import {MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {ModeLib} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +import {MockValidator} from "../mocks/MockValidator.sol"; +import {MockTarget} from "../mocks/MockTarget.sol"; + +/** + * @title SessionKeyExecutorModule Fuzz Tests + * @notice Fuzz tests for spending limits, time validity, and permissions + */ +contract SessionKeyExecutorFuzzTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + MockValidator public validator; + SessionKeyExecutorModule public sessionKeyModule; + MockTarget public target; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + address owner = address(0x1234); + uint256 sessionKeyPrivateKey = 0xA11CE; + address sessionKey; + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + sessionKey = vm.addr(sessionKeyPrivateKey); + + validator = new MockValidator(); + sessionKeyModule = new SessionKeyExecutorModule(); + target = new MockTarget(); + + factory = new AuraAccountFactory(address(validator)); + + address accountAddr = factory.createAccount(owner, abi.encode(true), address(0), "", 0); + account = AuraAccount(payable(accountAddr)); + + vm.deal(address(account), 1000 ether); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(sessionKeyModule), ""); + } + + /*////////////////////////////////////////////////////////////// + SPENDING LIMIT FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test that per-tx spending limit is enforced + function testFuzz_SpendLimitPerTxEnforced(uint256 limit, uint256 amount) public { + // Reasonable bounds + limit = bound(limit, 0.01 ether, 100 ether); + amount = bound(amount, 0, 200 ether); + + _createSessionKeyWithLimit(limit, type(uint256).max); + + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + + bytes32 messageHash = keccak256( + abi.encodePacked(address(account), address(target), amount, keccak256(targetCallData), nonce, block.chainid) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + if (amount > limit) { + vm.expectRevert(SessionKeyExecutorModule.SpendLimitPerTxExceeded.selector); + } + + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), amount, targetCallData, nonce, signature + ); + + if (amount <= limit && amount <= address(account).balance) { + assertEq(target.value(), 42); + } + } + + /// @notice Fuzz test that total spending limit is tracked correctly + function testFuzz_SpendLimitTotalTracking(uint8 numTxs, uint256 seed) public { + numTxs = uint8(bound(numTxs, 1, 5)); + uint256 perTxLimit = 10 ether; + uint256 totalLimit = 20 ether; + + _createSessionKeyWithLimit(perTxLimit, totalLimit); + + uint256 totalSpent; + + for (uint256 i = 0; i < numTxs; i++) { + uint256 amount = uint256(keccak256(abi.encode(seed, i))) % perTxLimit; + if (amount == 0) amount = 0.01 ether; + + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (i)); + + bytes32 messageHash = keccak256( + abi.encodePacked(address(account), address(target), amount, keccak256(targetCallData), i, block.chainid) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + if (totalSpent + amount > totalLimit) { + vm.expectRevert(SessionKeyExecutorModule.SpendLimitTotalExceeded.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), amount, targetCallData, i, signature + ); + break; + } else { + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), amount, targetCallData, i, signature + ); + totalSpent += amount; + } + } + } + + /*////////////////////////////////////////////////////////////// + TIME VALIDITY FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test that session key validity period is enforced + function testFuzz_SessionKeyValidityPeriod(uint256 afterOffset, uint256 duration) public { + // Bound to reasonable ranges to avoid overflow + afterOffset = bound(afterOffset, 0, 30 days); + duration = bound(duration, 1 hours, 365 days); + + uint48 validAfter = uint48(block.timestamp + afterOffset); + uint48 validUntil = uint48(block.timestamp + afterOffset + duration); + + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: validAfter, + validUntil: validUntil, + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 10 ether, + spendLimitTotal: 100 ether + }); + + bytes memory createCallData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + vm.prank(ENTRYPOINT); + account.execute( + ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, createCallData) + ); + + // Verify stored values + (bool active, uint48 storedValidAfter, uint48 storedValidUntil,,,,) = + sessionKeyModule.getSessionKey(address(account), sessionKey); + + assertTrue(active); + assertEq(storedValidAfter, validAfter); + assertEq(storedValidUntil, validUntil); + } + + /// @notice Fuzz test that expired session keys are rejected + function testFuzz_ExpiredSessionKeyRejected(uint256 timeDelta) public { + timeDelta = bound(timeDelta, 1, 365 days); + + _createSessionKeyWithLimit(10 ether, 100 ether); + + // Fast forward past validity + vm.warp(block.timestamp + 1 days + timeDelta); + + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), address(target), uint256(0), keccak256(targetCallData), nonce, block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.SessionKeyExpired.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, nonce, signature + ); + } + + /// @notice Fuzz test that session keys before validAfter are rejected + function testFuzz_SessionKeyNotYetValid(uint256 delay) public { + delay = bound(delay, 1 hours, 30 days); + + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp + delay), + validUntil: uint48(block.timestamp + delay + 1 days), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 10 ether, + spendLimitTotal: 100 ether + }); + + bytes memory createCallData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + vm.prank(ENTRYPOINT); + account.execute( + ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, createCallData) + ); + + bytes memory targetCallData = abi.encodeCall(MockTarget.setValue, (42)); + uint256 nonce = 0; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), address(target), uint256(0), keccak256(targetCallData), nonce, block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.SessionKeyNotYetValid.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, nonce, signature + ); + } + + /*////////////////////////////////////////////////////////////// + PERMISSION FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test that target restrictions are enforced + function testFuzz_AllowedTargetsEnforced(address randomTarget) public { + vm.assume(randomTarget != address(0)); + vm.assume(randomTarget != address(target)); + vm.assume(randomTarget.code.length == 0); // Not a contract + + // Create session key with specific target allowed + address[] memory allowedTargets = new address[](1); + allowedTargets[0] = address(target); + + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: allowedTargets, + allowedSelectors: new bytes4[](0), + spendLimitPerTx: 10 ether, + spendLimitTotal: 100 ether + }); + + bytes memory createCallData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + vm.prank(ENTRYPOINT); + account.execute( + ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, createCallData) + ); + + // Try to call disallowed target + bytes memory targetCallData = ""; + uint256 nonce = 0; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), randomTarget, uint256(0), keccak256(targetCallData), nonce, block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.TargetNotAllowed.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, randomTarget, 0, targetCallData, nonce, signature + ); + } + + /// @notice Fuzz test that selector restrictions are enforced + function testFuzz_AllowedSelectorsEnforced(bytes4 randomSelector) public { + vm.assume(randomSelector != MockTarget.setValue.selector); + + // Create session key with specific selector allowed + bytes4[] memory allowedSelectors = new bytes4[](1); + allowedSelectors[0] = MockTarget.setValue.selector; + + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: new address[](0), + allowedSelectors: allowedSelectors, + spendLimitPerTx: 10 ether, + spendLimitTotal: 100 ether + }); + + bytes memory createCallData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + vm.prank(ENTRYPOINT); + account.execute( + ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, createCallData) + ); + + // Try to call disallowed selector + bytes memory targetCallData = abi.encodeWithSelector(randomSelector); + uint256 nonce = 0; + + bytes32 messageHash = keccak256( + abi.encodePacked( + address(account), address(target), uint256(0), keccak256(targetCallData), nonce, block.chainid + ) + ); + bytes32 ethSignedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, ethSignedHash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(SessionKeyExecutorModule.SelectorNotAllowed.selector); + sessionKeyModule.executeWithSessionKey( + address(account), sessionKey, address(target), 0, targetCallData, nonce, signature + ); + } + + /*////////////////////////////////////////////////////////////// + HELPER FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _createSessionKeyWithLimit(uint256 perTxLimit, uint256 totalLimit) internal { + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + 1 days), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: perTxLimit, + spendLimitTotal: totalLimit + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + } +} + diff --git a/test/modular/fuzz/SocialRecovery.fuzz.t.sol b/test/modular/fuzz/SocialRecovery.fuzz.t.sol new file mode 100644 index 0000000..e2be13c --- /dev/null +++ b/test/modular/fuzz/SocialRecovery.fuzz.t.sol @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../../src/modular/AuraAccountFactory.sol"; +import {P256MFAValidatorModule} from "../../../src/modular/modules/validators/P256MFAValidatorModule.sol"; +import {SocialRecoveryModule} from "../../../src/modular/modules/executors/SocialRecoveryModule.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; + +import {MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; + +/** + * @title SocialRecoveryModule Fuzz Tests + * @notice Fuzz tests for guardian thresholds, timelocks, and recovery flows + */ +contract SocialRecoveryFuzzTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + P256MFAValidatorModule public validator; + SocialRecoveryModule public recovery; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + address owner = address(0x1234); + + // Test passkey coordinates + bytes32 constant QX = 0x65a2fa44daad46eab0278703edb6c4dcf5e30b8a9aec09fdc71a56f52aa392e4; + bytes32 constant QY = 0x4a7a9e4604aa36898209997288e902ac544a555e4b5e0a9efef2b59233f3f437; + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + validator = new P256MFAValidatorModule(); + recovery = new SocialRecoveryModule(); + + factory = new AuraAccountFactory(address(validator)); + + bytes memory validatorData = abi.encode(owner, QX, QY, bytes32("Test Device"), true); + + address accountAddr = factory.createAccount(owner, validatorData, address(0), "", 0); + account = AuraAccount(payable(accountAddr)); + + vm.deal(address(account), 10 ether); + } + + /*////////////////////////////////////////////////////////////// + GUARDIAN THRESHOLD FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test for setting up guardians with various thresholds + function testFuzz_GuardianThreshold(uint8 numGuardians, uint8 threshold) public { + // Bound to reasonable range + numGuardians = uint8(bound(numGuardians, 1, 10)); + threshold = uint8(bound(threshold, 1, numGuardians)); + + address[] memory guardians = new address[](numGuardians); + for (uint256 i = 0; i < numGuardians; i++) { + guardians[i] = address(uint160(0x1000 + i)); + } + + bytes memory recoveryData = abi.encode(uint256(threshold), uint256(24 hours), guardians); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), recoveryData); + + // Verify setup + (uint256 storedThreshold,) = recovery.getRecoveryConfig(address(account)); + assertEq(storedThreshold, threshold); + assertEq(recovery.getGuardianCount(address(account)), numGuardians); + + for (uint256 i = 0; i < numGuardians; i++) { + assertTrue(recovery.isGuardian(address(account), guardians[i])); + } + } + + /// @notice Fuzz test that recovery requires exact threshold of approvals + function testFuzz_RecoveryQuorumRequired(uint8 numGuardians, uint8 threshold, uint8 approvals) public { + // Bound to reasonable range + numGuardians = uint8(bound(numGuardians, 2, 5)); + threshold = uint8(bound(threshold, 2, numGuardians)); + approvals = uint8(bound(approvals, 1, numGuardians)); + + address[] memory guardians = new address[](numGuardians); + for (uint256 i = 0; i < numGuardians; i++) { + guardians[i] = address(uint160(0x1000 + i)); + } + + bytes memory recoveryData = abi.encode(uint256(threshold), uint256(24 hours), guardians); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), recoveryData); + + // First guardian initiates + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardians[0]); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + // Additional guardians approve + for (uint256 i = 1; i < approvals && i < numGuardians; i++) { + vm.prank(guardians[i]); + recovery.approveRecovery(address(account), 0); + } + + (,,, uint256 approvalCount,,, bool thresholdMet,,) = recovery.getRecoveryRequest(address(account), 0); + + assertEq(approvalCount, approvals); + + if (approvals >= threshold) { + assertTrue(thresholdMet); + } else { + assertFalse(thresholdMet); + } + } + + /*////////////////////////////////////////////////////////////// + TIMELOCK FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test that timelock period is properly enforced + function testFuzz_RecoveryTimelockEnforced(uint256 timelockPeriod) public { + // Bound to reasonable range: 1 hour to 7 days + timelockPeriod = bound(timelockPeriod, 1 hours, 7 days); + + address[] memory guardians = new address[](2); + guardians[0] = address(0x1001); + guardians[1] = address(0x1002); + + bytes memory recoveryData = abi.encode( + uint256(2), // threshold + timelockPeriod, + guardians + ); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), recoveryData); + + // Verify timelock is set + (, uint256 storedTimelock) = recovery.getRecoveryConfig(address(account)); + assertEq(storedTimelock, timelockPeriod); + + // Initiate and approve to meet threshold + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardians[0]); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(guardians[1]); + recovery.approveRecovery(address(account), 0); + + // Verify executeAfter is set correctly + (,,,,, uint256 executeAfter, bool thresholdMet,,) = recovery.getRecoveryRequest(address(account), 0); + assertTrue(thresholdMet); + assertEq(executeAfter, block.timestamp + timelockPeriod); + + // Cannot execute before timelock expires + vm.expectRevert(SocialRecoveryModule.TimelockNotPassed.selector); + recovery.executeRecovery(address(account), 0, address(validator)); + + // Can execute after timelock + vm.warp(executeAfter + 1); + recovery.executeRecovery(address(account), 0, address(validator)); + + // Verify execution + (,,,,,,, bool executed,) = recovery.getRecoveryRequest(address(account), 0); + assertTrue(executed); + } + + /// @notice Fuzz test that recovery can be cancelled before execution + function testFuzz_CancelRecoveryBeforeExecution(uint256 cancelDelay) public { + // Cancel can happen anytime before execution + cancelDelay = bound(cancelDelay, 0, 23 hours); + + address[] memory guardians = new address[](2); + guardians[0] = address(0x1001); + guardians[1] = address(0x1002); + + bytes memory recoveryData = abi.encode(uint256(2), uint256(24 hours), guardians); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), recoveryData); + + // Initiate and approve + bytes32 newQx = bytes32(uint256(1)); + bytes32 newQy = bytes32(uint256(2)); + address newOwner = address(0x9999); + + vm.prank(guardians[0]); + recovery.initiateRecovery(address(account), newQx, newQy, newOwner); + + vm.prank(guardians[1]); + recovery.approveRecovery(address(account), 0); + + // Wait some time + vm.warp(block.timestamp + cancelDelay); + + // Account can cancel + vm.prank(address(account)); + recovery.cancelRecovery(0); + + (,,,,,,,, bool cancelled) = recovery.getRecoveryRequest(address(account), 0); + assertTrue(cancelled); + + // Cannot execute cancelled recovery + vm.warp(block.timestamp + 24 hours); + vm.expectRevert(SocialRecoveryModule.RecoveryAlreadyCancelled.selector); + recovery.executeRecovery(address(account), 0, address(validator)); + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test that only guardians can initiate/approve recovery + function testFuzz_OnlyGuardiansCanInitiate(address randomCaller) public { + vm.assume(randomCaller != address(0)); + vm.assume(randomCaller != address(0x1001)); + vm.assume(randomCaller != address(0x1002)); + + address[] memory guardians = new address[](2); + guardians[0] = address(0x1001); + guardians[1] = address(0x1002); + + bytes memory recoveryData = abi.encode(uint256(2), uint256(24 hours), guardians); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), recoveryData); + + vm.prank(randomCaller); + vm.expectRevert(SocialRecoveryModule.NotGuardian.selector); + recovery.initiateRecovery(address(account), bytes32(0), bytes32(0), address(0x9999)); + } + + /// @notice Fuzz test that only account can cancel its own recovery + /// @dev cancelRecovery uses msg.sender as the account key, so random callers + /// get RecoveryNotFound because there's no recovery for their address + function testFuzz_OnlyAccountCanCancel(address randomCaller) public { + vm.assume(randomCaller != address(account)); + vm.assume(randomCaller != address(0)); + + address[] memory guardians = new address[](2); + guardians[0] = address(0x1001); + guardians[1] = address(0x1002); + + bytes memory recoveryData = abi.encode(uint256(2), uint256(24 hours), guardians); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), recoveryData); + + // Initiate recovery for the account + vm.prank(guardians[0]); + recovery.initiateRecovery(address(account), bytes32(0), bytes32(0), address(0x9999)); + + // Random caller cannot cancel - they get RecoveryNotFound because + // cancelRecovery looks up recovery by msg.sender, not by account parameter + vm.prank(randomCaller); + vm.expectRevert(SocialRecoveryModule.RecoveryNotFound.selector); + recovery.cancelRecovery(0); + + // Verify account can still cancel its own recovery + vm.prank(address(account)); + recovery.cancelRecovery(0); + + (,,,,,,,, bool cancelled) = recovery.getRecoveryRequest(address(account), 0); + assertTrue(cancelled); + } + + /*////////////////////////////////////////////////////////////// + GUARDIAN MANAGEMENT FUZZ TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Fuzz test for adding and removing guardians + function testFuzz_AddRemoveGuardians(uint8 numToAdd, uint256 seed) public { + numToAdd = uint8(bound(numToAdd, 1, 10)); + + // Start with 2 guardians + address[] memory initialGuardians = new address[](2); + initialGuardians[0] = address(0x1001); + initialGuardians[1] = address(0x1002); + + bytes memory recoveryData = abi.encode( + uint256(1), // threshold of 1 for flexibility + uint256(24 hours), + initialGuardians + ); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), recoveryData); + + // Add new guardians + for (uint256 i = 0; i < numToAdd; i++) { + address newGuardian = address(uint160(0x2000 + i)); + vm.prank(address(account)); + recovery.addGuardian(newGuardian); + assertTrue(recovery.isGuardian(address(account), newGuardian)); + } + + assertEq(recovery.getGuardianCount(address(account)), 2 + numToAdd); + + // Randomly remove some guardians + uint256 toRemove = uint256(keccak256(abi.encode(seed))) % numToAdd; + for (uint256 i = 0; i < toRemove; i++) { + address guardianToRemove = address(uint160(0x2000 + i)); + vm.prank(address(account)); + recovery.removeGuardian(guardianToRemove); + assertFalse(recovery.isGuardian(address(account), guardianToRemove)); + } + + assertEq(recovery.getGuardianCount(address(account)), 2 + numToAdd - toRemove); + } + + /// @notice Fuzz test for threshold configuration + function testFuzz_SetRecoveryConfig(uint8 threshold, uint256 timelock) public { + // Start with basic setup + address[] memory guardians = new address[](5); + for (uint256 i = 0; i < 5; i++) { + guardians[i] = address(uint160(0x1000 + i)); + } + + bytes memory recoveryData = abi.encode(uint256(2), uint256(24 hours), guardians); + + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), recoveryData); + + // Update config with fuzzed values + threshold = uint8(bound(threshold, 1, 5)); + timelock = bound(timelock, 1 hours, 30 days); + + vm.prank(address(account)); + recovery.setRecoveryConfig(threshold, timelock); + + (uint256 storedThreshold, uint256 storedTimelock) = recovery.getRecoveryConfig(address(account)); + assertEq(storedThreshold, threshold); + assertEq(storedTimelock, timelock); + } +} diff --git a/test/modular/invariant/AuraAccountInvariant.t.sol b/test/modular/invariant/AuraAccountInvariant.t.sol new file mode 100644 index 0000000..864417d --- /dev/null +++ b/test/modular/invariant/AuraAccountInvariant.t.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test, console} from "forge-std/Test.sol"; +import {AuraAccount} from "../../../src/modular/AuraAccount.sol"; +import {AuraAccountFactory} from "../../../src/modular/AuraAccountFactory.sol"; +import {SessionKeyExecutorModule} from "../../../src/modular/modules/executors/SessionKeyExecutorModule.sol"; +import {P256MFAValidatorModule} from "../../../src/modular/modules/validators/P256MFAValidatorModule.sol"; +import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"; + +import {MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; + +import {AccountHandler} from "./handlers/AccountHandler.sol"; + +/** + * @title AuraAccount Invariant Tests + * @notice Invariant tests to verify critical properties hold under all conditions + */ +contract AuraAccountInvariantTest is Test { + AuraAccountFactory public factory; + AuraAccount public account; + P256MFAValidatorModule public validator; + SessionKeyExecutorModule public sessionKeyModule; + AccountHandler public handler; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + address owner = address(0x1234); + + // Test passkey coordinates + bytes32 constant QX = 0x65a2fa44daad46eab0278703edb6c4dcf5e30b8a9aec09fdc71a56f52aa392e4; + bytes32 constant QY = 0x4a7a9e4604aa36898209997288e902ac544a555e4b5e0a9efef2b59233f3f437; + + function setUp() public { + if (ERC1967FactoryConstants.ADDRESS.code.length == 0) { + vm.etch(ERC1967FactoryConstants.ADDRESS, ERC1967FactoryConstants.BYTECODE); + } + + validator = new P256MFAValidatorModule(); + sessionKeyModule = new SessionKeyExecutorModule(); + + factory = new AuraAccountFactory(address(validator)); + + bytes memory initData = abi.encode(owner, QX, QY, bytes32("Test Device"), true); + + address accountAddr = factory.createAccount(owner, initData, address(0), "", 0); + account = AuraAccount(payable(accountAddr)); + + vm.deal(address(account), 1000 ether); + + // Install session key module + vm.prank(ENTRYPOINT); + account.installModule(MODULE_TYPE_EXECUTOR, address(sessionKeyModule), ""); + + // Create handler + handler = new AccountHandler(account, sessionKeyModule, validator); + + // Target only the handler for invariant testing + targetContract(address(handler)); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNT INVARIANTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Account must always have at least one passkey + function invariant_AccountHasAtLeastOnePasskey() public view { + assertGe(validator.getPasskeyCount(address(account)), 1, "Account must have at least one passkey"); + } + + /// @notice Account must always have an owner + function invariant_AccountHasOwner() public view { + assertTrue(validator.getOwner(address(account)) != address(0), "Account must have an owner"); + } + + /// @notice Session key created count >= revoked count + function invariant_SessionKeyCountsConsistent() public view { + assertGe( + handler.totalSessionKeysCreated(), + handler.totalSessionKeysRevoked(), + "Created session keys must be >= revoked" + ); + } + + /// @notice Active session keys = created - revoked + function invariant_ActiveSessionKeysMatchDelta() public view { + uint256 expectedActive = handler.totalSessionKeysCreated() - handler.totalSessionKeysRevoked(); + assertEq(handler.getActiveSessionKeyCount(), expectedActive, "Active session keys must equal created - revoked"); + } + + /// @notice Account balance should never go negative (implicit in Solidity, but good to verify) + function invariant_AccountBalanceNonNegative() public view { + assertGe(address(account).balance, 0, "Account balance must be non-negative"); + } + + /// @notice Validator module must always be installed + function invariant_ValidatorModuleInstalled() public view { + assertTrue(account.isModuleInstalled(1, address(validator), ""), "Validator module must be installed"); + } + + /// @notice Session key module must always be installed (after setup) + function invariant_SessionKeyModuleInstalled() public view { + assertTrue(account.isModuleInstalled(2, address(sessionKeyModule), ""), "Session key module must be installed"); + } + + /*////////////////////////////////////////////////////////////// + PASSKEY INVARIANTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Passkey count must be >= 1 + added - removed + function invariant_PasskeyCountConsistent() public view { + uint256 initialCount = 1; // From setup + uint256 expectedMin = initialCount + handler.totalPasskeysAdded() - handler.totalPasskeysRemoved(); + assertGe( + validator.getPasskeyCount(address(account)), + expectedMin, + "Passkey count must be consistent with add/remove operations" + ); + } + + /*////////////////////////////////////////////////////////////// + CALL SUMMARY + //////////////////////////////////////////////////////////////*/ + + /// @notice Log summary of invariant test calls + function invariant_callSummary() public view { + console.log("Session keys created:", handler.totalSessionKeysCreated()); + console.log("Session keys revoked:", handler.totalSessionKeysRevoked()); + console.log("Active session keys:", handler.getActiveSessionKeyCount()); + console.log("Passkeys added:", handler.totalPasskeysAdded()); + console.log("Account balance:", address(account).balance); + } +} + diff --git a/test/modular/invariant/handlers/AccountHandler.sol b/test/modular/invariant/handlers/AccountHandler.sol new file mode 100644 index 0000000..0f137d0 --- /dev/null +++ b/test/modular/invariant/handlers/AccountHandler.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {AuraAccount} from "../../../../src/modular/AuraAccount.sol"; +import {SessionKeyExecutorModule} from "../../../../src/modular/modules/executors/SessionKeyExecutorModule.sol"; +import {P256MFAValidatorModule} from "../../../../src/modular/modules/validators/P256MFAValidatorModule.sol"; + +import {MODULE_TYPE_EXECUTOR} from "@erc7579/interfaces/IERC7579Module.sol"; +import {ModeLib} from "@erc7579/lib/ModeLib.sol"; +import {ExecutionLib} from "@erc7579/lib/ExecutionLib.sol"; + +/** + * @title AccountHandler + * @notice Handler contract for invariant testing of AuraAccount + * @dev Exposes bounded actions that can be called during invariant testing + */ +contract AccountHandler is Test { + AuraAccount public account; + SessionKeyExecutorModule public sessionKeyModule; + P256MFAValidatorModule public validator; + + address public constant ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + + // Ghost variables for tracking state + uint256 public totalSessionKeysCreated; + uint256 public totalSessionKeysRevoked; + uint256 public totalPasskeysAdded; + uint256 public totalPasskeysRemoved; + uint256 public totalExecutions; + uint256 public totalValueSent; + + // Session key tracking + address[] public sessionKeys; + mapping(address => bool) public isActiveSessionKey; + mapping(address => bool) public sessionKeyExists; // Track if key was ever created + + constructor(AuraAccount _account, SessionKeyExecutorModule _sessionKeyModule, P256MFAValidatorModule _validator) { + account = _account; + sessionKeyModule = _sessionKeyModule; + validator = _validator; + } + + /*////////////////////////////////////////////////////////////// + SESSION KEY ACTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Create a session key with bounded parameters + function createSessionKey( + uint256 privateKeySeed, + uint256 validDuration, + uint256 spendLimitPerTx, + uint256 spendLimitTotal + ) external { + // Bound inputs + privateKeySeed = bound(privateKeySeed, 1, 1000); + validDuration = bound(validDuration, 1 hours, 30 days); + spendLimitPerTx = bound(spendLimitPerTx, 0.01 ether, 100 ether); + spendLimitTotal = bound(spendLimitTotal, spendLimitPerTx, 1000 ether); + + address sessionKey = vm.addr(privateKeySeed); + + // Skip if already exists + if (isActiveSessionKey[sessionKey]) return; + + SessionKeyExecutorModule.SessionKeyPermission memory permission = SessionKeyExecutorModule.SessionKeyPermission({ + sessionKey: sessionKey, + validAfter: uint48(block.timestamp), + validUntil: uint48(block.timestamp + validDuration), + allowedTargets: new address[](0), + allowedSelectors: new bytes4[](0), + spendLimitPerTx: spendLimitPerTx, + spendLimitTotal: spendLimitTotal + }); + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.createSessionKey, (permission)); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + + // Only push to array if this is a new key (not a re-creation) + if (!sessionKeyExists[sessionKey]) { + sessionKeys.push(sessionKey); + sessionKeyExists[sessionKey] = true; + } + isActiveSessionKey[sessionKey] = true; + totalSessionKeysCreated++; + } + + /// @notice Revoke a session key + function revokeSessionKey(uint256 index) external { + if (sessionKeys.length == 0) return; + + index = bound(index, 0, sessionKeys.length - 1); + address sessionKey = sessionKeys[index]; + + if (!isActiveSessionKey[sessionKey]) return; + + bytes memory callData = abi.encodeCall(SessionKeyExecutorModule.revokeSessionKey, (sessionKey)); + + vm.prank(ENTRYPOINT); + account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(sessionKeyModule), 0, callData)); + + isActiveSessionKey[sessionKey] = false; + totalSessionKeysRevoked++; + } + + /*////////////////////////////////////////////////////////////// + PASSKEY ACTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Add a passkey with random coordinates + function addPasskey(bytes32 qx, bytes32 qy, bytes32 deviceId) external { + // Ensure non-zero + if (qx == bytes32(0) || qy == bytes32(0)) return; + + bytes memory callData = abi.encodeCall(P256MFAValidatorModule.addPasskey, (qx, qy, deviceId)); + + vm.prank(ENTRYPOINT); + try account.execute(ModeLib.encodeSimpleSingle(), ExecutionLib.encodeSingle(address(validator), 0, callData)) { + totalPasskeysAdded++; + } catch { + // Passkey might already exist + } + } + + /*////////////////////////////////////////////////////////////// + GETTERS + //////////////////////////////////////////////////////////////*/ + + function getSessionKeyCount() external view returns (uint256) { + return sessionKeys.length; + } + + function getActiveSessionKeyCount() external view returns (uint256) { + uint256 count; + for (uint256 i = 0; i < sessionKeys.length; i++) { + if (isActiveSessionKey[sessionKeys[i]]) count++; + } + return count; + } +} +