// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title AereDelegationRegistry — public index of EIP-7702 delegated EOAs * @notice Permissionless registry that records which EOAs have delegated * to the AereDelegate7702 implementation. dApps query this to: * - Render the "smart-wallet enabled" badge in UI * - Discover EOAs eligible for batch execution * - Coordinate paymaster eligibility checks * * Each registered account stores: * - delegationCodeHash (EIP-7702 designator hash for cross-check) * - registeredAt timestamp * - lastSeenNonce (for indexers; updated by the account itself or * permissionless `notifyNonce`) * * REGISTRATION is permissionless: any EOA can self-register via * `register()`. UNREGISTRATION is also permissionless to the * account itself (e.g. before reverting their EIP-7702 delegation). * * @dev The registry is purely INFORMATIONAL — no funds custody, no * access control over the account. Wrong registration data hurts * only the registrant's UX. */ contract AereDelegationRegistry { struct Entry { bool registered; bytes32 delegationCodeHash; // hash of msg.sender's delegated code at registration time uint64 registeredAt; uint64 lastSeenAt; uint256 lastSeenNonce; } /// @notice account → entry mapping(address => Entry) public entries; /// @notice paged listing for indexers address[] public registered; /// @notice account → index in `registered` array (1-based; 0 = not in list) mapping(address => uint256) internal _registeredIndex; /* --------------------------------- events ------------------------------- */ event Registered(address indexed account, bytes32 delegationCodeHash, uint64 registeredAt); event Unregistered(address indexed account); event NonceSeen(address indexed account, uint256 nonce, uint64 timestamp); /* --------------------------------- errors ------------------------------- */ error AlreadyRegistered(); error NotRegistered(); /* ------------------------------ registration ---------------------------- */ /// @notice Self-register the caller. The contract reads `msg.sender`'s /// current code hash via `extcodehash` — if the EOA has /// delegated under EIP-7702, this returns the designated /// implementation's code hash. If the EOA has NOT delegated, /// the hash will be the empty-account hash and the registration /// is still allowed (with that empty hash recorded) — front-end /// filters can ignore entries with the empty hash. function register() external { if (entries[msg.sender].registered) revert AlreadyRegistered(); bytes32 codeHash; assembly { codeHash := extcodehash(caller()) } entries[msg.sender] = Entry({ registered: true, delegationCodeHash: codeHash, registeredAt: uint64(block.timestamp), lastSeenAt: uint64(block.timestamp), lastSeenNonce: 0 }); registered.push(msg.sender); _registeredIndex[msg.sender] = registered.length; // 1-based emit Registered(msg.sender, codeHash, uint64(block.timestamp)); } function unregister() external { if (!entries[msg.sender].registered) revert NotRegistered(); delete entries[msg.sender]; uint256 idx = _registeredIndex[msg.sender]; if (idx != 0) { uint256 lastIdx = registered.length; if (idx != lastIdx) { address swapped = registered[lastIdx - 1]; registered[idx - 1] = swapped; _registeredIndex[swapped] = idx; } registered.pop(); delete _registeredIndex[msg.sender]; } emit Unregistered(msg.sender); } /// @notice Anyone may call to update `lastSeenNonce` for an account. /// Indexers + paymasters call this after observing a tx from /// the account so the registry reflects fresh activity. function notifyNonce(address account, uint256 nonce) external { Entry storage e = entries[account]; if (!e.registered) revert NotRegistered(); if (nonce > e.lastSeenNonce) { e.lastSeenNonce = nonce; e.lastSeenAt = uint64(block.timestamp); emit NonceSeen(account, nonce, uint64(block.timestamp)); } } /* --------------------------------- views -------------------------------- */ function registeredCount() external view returns (uint256) { return registered.length; } function isRegistered(address account) external view returns (bool) { return entries[account].registered; } function page(uint256 start, uint256 count) external view returns (address[] memory out) { uint256 n = registered.length; if (start >= n) return new address[](0); uint256 end = start + count; if (end > n) end = n; out = new address[](end - start); for (uint256 i = start; i < end; i++) { out[i - start] = registered[i]; } } }