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.
128 lines
5.2 KiB
Solidity
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];
|
|
}
|
|
}
|
|
}
|