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.
190 lines
8.5 KiB
Solidity
190 lines
8.5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
/// @notice Root-owner surface of AereModularAccount used by this module.
|
|
interface IAereModularAccountOwner {
|
|
function rootOwner() external view returns (address);
|
|
function setRootOwner(address newOwner) external;
|
|
}
|
|
|
|
/// @title AereSocialRecoveryModule - M-of-N guardian recovery for AereModularAccount
|
|
/// @notice Singleton executor module (ERC-7579 module type 2). Per-account state is
|
|
/// keyed by the account address. Install this module on the account as
|
|
/// module type 2; executeRecovery then calls account.setRootOwner(), which
|
|
/// AereModularAccount permits for installed executor modules.
|
|
///
|
|
/// Flow: guardian initiateRecovery -> other guardians supportRecovery ->
|
|
/// after a 48 hour timelock AND >= threshold approvals, any guardian may
|
|
/// executeRecovery. The current owner (or the account itself) can
|
|
/// cancelRecovery at any time before execution.
|
|
///
|
|
/// Guardian set and threshold changes are OWNER-ONLY: mutating config
|
|
/// functions require msg.sender == the account, and the account's
|
|
/// execute() path is gated to EntryPoint / self / root owner.
|
|
contract AereSocialRecoveryModule {
|
|
uint256 public constant RECOVERY_DELAY = 48 hours;
|
|
|
|
struct Recovery {
|
|
address newOwner;
|
|
uint64 executeAfter; // initiation time + RECOVERY_DELAY
|
|
uint32 approvals;
|
|
bool active;
|
|
}
|
|
|
|
/// @notice account => guardian => is guardian.
|
|
mapping(address => mapping(address => bool)) public isGuardian;
|
|
/// @notice account => guardian list (for views + uninstall cleanup).
|
|
mapping(address => address[]) internal _guardianList;
|
|
/// @notice account => M in M-of-N.
|
|
mapping(address => uint256) public thresholdOf;
|
|
/// @notice account => monotonically increasing recovery round id.
|
|
mapping(address => uint256) public recoveryNonce;
|
|
/// @notice account => currently active recovery (if any).
|
|
mapping(address => Recovery) public activeRecovery;
|
|
/// @notice account => round id => guardian => approved.
|
|
mapping(address => mapping(uint256 => mapping(address => bool))) public hasApproved;
|
|
|
|
event GuardianAdded(address indexed account, address indexed guardian);
|
|
event GuardianRemoved(address indexed account, address indexed guardian);
|
|
event ThresholdChanged(address indexed account, uint256 threshold);
|
|
event RecoveryInitiated(address indexed account, uint256 indexed round, address indexed newOwner, address guardian, uint64 executeAfter);
|
|
event RecoverySupported(address indexed account, uint256 indexed round, address indexed guardian, uint256 approvals);
|
|
event RecoveryExecuted(address indexed account, uint256 indexed round, address indexed newOwner);
|
|
event RecoveryCancelled(address indexed account, uint256 indexed round, address canceller);
|
|
|
|
error NotGuardian();
|
|
error NotOwner();
|
|
error InvalidGuardian();
|
|
error InvalidThreshold();
|
|
error InvalidNewOwner();
|
|
error RecoveryAlreadyActive();
|
|
error NoActiveRecovery();
|
|
error AlreadyApproved();
|
|
error BelowThreshold();
|
|
error TimelockNotElapsed(uint64 executeAfter, uint256 nowTimestamp);
|
|
|
|
// ---- ERC-7579-style module base -------------------------------------------
|
|
|
|
function isModuleType(uint256 moduleTypeId) external pure returns (bool) {
|
|
return moduleTypeId == 2;
|
|
}
|
|
|
|
/// @notice initData layout: abi.encode(address[] guardians, uint256 threshold).
|
|
function onInstall(bytes calldata data) external {
|
|
(address[] memory guardians, uint256 threshold) = abi.decode(data, (address[], uint256));
|
|
address account = msg.sender;
|
|
for (uint256 i = 0; i < guardians.length; i++) {
|
|
_addGuardian(account, guardians[i]);
|
|
}
|
|
if (threshold == 0 || threshold > _guardianList[account].length) revert InvalidThreshold();
|
|
thresholdOf[account] = threshold;
|
|
emit ThresholdChanged(account, threshold);
|
|
}
|
|
|
|
/// @notice Full per-account cleanup so a later reinstall starts clean.
|
|
function onUninstall(bytes calldata) external {
|
|
address account = msg.sender;
|
|
address[] storage list = _guardianList[account];
|
|
for (uint256 i = 0; i < list.length; i++) {
|
|
isGuardian[account][list[i]] = false;
|
|
}
|
|
delete _guardianList[account];
|
|
delete thresholdOf[account];
|
|
delete activeRecovery[account];
|
|
}
|
|
|
|
// ---- Owner-only guardian management (msg.sender = the smart account) ------
|
|
|
|
function addGuardian(address guardian) external {
|
|
_addGuardian(msg.sender, guardian);
|
|
}
|
|
|
|
function removeGuardian(address guardian) external {
|
|
address account = msg.sender;
|
|
if (!isGuardian[account][guardian]) revert InvalidGuardian();
|
|
isGuardian[account][guardian] = false;
|
|
address[] storage list = _guardianList[account];
|
|
for (uint256 i = 0; i < list.length; i++) {
|
|
if (list[i] == guardian) {
|
|
list[i] = list[list.length - 1];
|
|
list.pop();
|
|
break;
|
|
}
|
|
}
|
|
if (thresholdOf[account] > list.length) revert InvalidThreshold();
|
|
emit GuardianRemoved(account, guardian);
|
|
}
|
|
|
|
function setThreshold(uint256 threshold) external {
|
|
address account = msg.sender;
|
|
if (threshold == 0 || threshold > _guardianList[account].length) revert InvalidThreshold();
|
|
thresholdOf[account] = threshold;
|
|
emit ThresholdChanged(account, threshold);
|
|
}
|
|
|
|
function guardiansOf(address account) external view returns (address[] memory) {
|
|
return _guardianList[account];
|
|
}
|
|
|
|
function _addGuardian(address account, address guardian) internal {
|
|
if (guardian == address(0) || guardian == account) revert InvalidGuardian();
|
|
if (isGuardian[account][guardian]) revert InvalidGuardian();
|
|
isGuardian[account][guardian] = true;
|
|
_guardianList[account].push(guardian);
|
|
emit GuardianAdded(account, guardian);
|
|
}
|
|
|
|
// ---- Recovery flow ----------------------------------------------------------
|
|
|
|
function initiateRecovery(address account, address newOwner) external {
|
|
if (!isGuardian[account][msg.sender]) revert NotGuardian();
|
|
if (newOwner == address(0)) revert InvalidNewOwner();
|
|
if (activeRecovery[account].active) revert RecoveryAlreadyActive();
|
|
uint256 round = ++recoveryNonce[account];
|
|
uint64 executeAfter = uint64(block.timestamp + RECOVERY_DELAY);
|
|
activeRecovery[account] = Recovery({
|
|
newOwner: newOwner,
|
|
executeAfter: executeAfter,
|
|
approvals: 1,
|
|
active: true
|
|
});
|
|
hasApproved[account][round][msg.sender] = true;
|
|
emit RecoveryInitiated(account, round, newOwner, msg.sender, executeAfter);
|
|
}
|
|
|
|
function supportRecovery(address account) external {
|
|
if (!isGuardian[account][msg.sender]) revert NotGuardian();
|
|
Recovery storage r = activeRecovery[account];
|
|
if (!r.active) revert NoActiveRecovery();
|
|
uint256 round = recoveryNonce[account];
|
|
if (hasApproved[account][round][msg.sender]) revert AlreadyApproved();
|
|
hasApproved[account][round][msg.sender] = true;
|
|
r.approvals += 1;
|
|
emit RecoverySupported(account, round, msg.sender, r.approvals);
|
|
}
|
|
|
|
/// @notice Guardian-only. Requires >= threshold approvals AND the 48h timelock.
|
|
/// Calls account.setRootOwner(newOwner); this module must be installed
|
|
/// on the account as an executor (module type 2) for that to succeed.
|
|
function executeRecovery(address account) external {
|
|
if (!isGuardian[account][msg.sender]) revert NotGuardian();
|
|
Recovery memory r = activeRecovery[account];
|
|
if (!r.active) revert NoActiveRecovery();
|
|
if (r.approvals < thresholdOf[account]) revert BelowThreshold();
|
|
if (block.timestamp < r.executeAfter) revert TimelockNotElapsed(r.executeAfter, block.timestamp);
|
|
delete activeRecovery[account];
|
|
IAereModularAccountOwner(account).setRootOwner(r.newOwner);
|
|
emit RecoveryExecuted(account, recoveryNonce[account], r.newOwner);
|
|
}
|
|
|
|
/// @notice Cancellable by the current root owner or by the account itself.
|
|
function cancelRecovery(address account) external {
|
|
if (msg.sender != account && msg.sender != IAereModularAccountOwner(account).rootOwner()) {
|
|
revert NotOwner();
|
|
}
|
|
if (!activeRecovery[account].active) revert NoActiveRecovery();
|
|
delete activeRecovery[account];
|
|
emit RecoveryCancelled(account, recoveryNonce[account], msg.sender);
|
|
}
|
|
}
|