aere-contracts/contracts/delegation/AereDelegationRegistry.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

128 lines
5.2 KiB
Solidity

// 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];
}
}
}