aere-contracts/contracts/compliance/AereForensicEventRegistry.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

214 lines
8.3 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title AereForensicEventRegistry — Forta-style detection bot output
* registry for Chain 2800
* @notice Open registry where third-party detection bots (mirror of the
* Forta Network ecosystem) post structured alerts about
* suspicious on-chain activity. Bots register first via
* registerBot(); they then submit alert events tagged with a
* severity and an alert type. Consumer dApps (lending markets,
* settlement venues) query the registry to gate counterparty
* actions when a high-severity recent alert flags an address.
*
* AERE Foundation operates a small initial bot fleet (anomalous-
* flow detection, sandwich-tx detection, sanctioned-flow
* detection); third-party operators are welcomed via
* permissionless self-registration.
*
* No asset custody. No transaction interception. Read-only audit
* surface.
*
* @dev LISTING + RANKING:
* - Each bot has an immutable owner-set stakeRequirement (default
* 0 in Phase 1 — purely reputational). Phase 2 (Q2 2027) will
* require an AERE bond + Foundation gating for high-severity
* alerts.
* - Per-bot reputation accrues from confirmed alerts (Foundation
* confirms in Phase 1; community-staked in Phase 2).
*
* ALERT TAXONOMY (initial; bots can extend):
* 0 = unknown / catch-all
* 1 = sanctioned-flow detected (counterparty matches Sanctions
* Registry)
* 2 = sandwich-tx / MEV pattern
* 3 = anomalous-flow (sudden TVL drain, unusual contract)
* 4 = oracle deviation
* 5 = bridge exploit pattern
* 6 = phishing contract pattern
* 7 = governance vote-buying pattern
* 8 = honeypot pattern
* 9 = rug-pull pattern
*
* SEVERITY:
* 1 = info
* 2 = low
* 3 = medium
* 4 = high
* 5 = critical
*/
contract AereForensicEventRegistry is Ownable {
struct Bot {
bool registered;
bool active;
address owner;
string name;
string metadataUri; // ipfs / https pointer to bot metadata
uint256 stakeRequirement;
uint256 alertCount;
uint256 confirmedCount;
}
struct Alert {
uint256 botId;
address subject; // the address the alert is about
uint8 alertType;
uint8 severity;
uint64 timestamp;
string detailUri; // off-chain pointer (e.g. ipfs) with full details
bool confirmed; // Foundation confirmation in Phase 1
}
/// @notice botId → Bot
mapping(uint256 => Bot) public bots;
uint256 public nextBotId;
/// @notice alertId → Alert
mapping(uint256 => Alert) public alerts;
uint256 public nextAlertId;
/// @notice subject → recent alert ids (most recent first; capped at
/// RECENT_ALERTS_CAP per address by the off-chain indexer).
mapping(address => uint256[]) public subjectAlerts;
/* --------------------------------- events ------------------------------- */
event BotRegistered(uint256 indexed botId, address indexed owner, string name, string metadataUri);
event BotUpdated(uint256 indexed botId, string newMetadataUri, bool active);
event AlertEmitted(uint256 indexed alertId, uint256 indexed botId, address indexed subject, uint8 alertType, uint8 severity, string detailUri);
event AlertConfirmed(uint256 indexed alertId, uint256 indexed botId);
event AlertRejected(uint256 indexed alertId, uint256 indexed botId, string reason);
/* --------------------------------- errors ------------------------------- */
error NotBotOwner();
error UnknownBot();
error UnknownAlert();
error BotInactive();
error ZeroSubject();
/* ----------------------------- bot registration ------------------------- */
function registerBot(string calldata name, string calldata metadataUri) external returns (uint256 botId) {
botId = nextBotId++;
bots[botId] = Bot({
registered: true,
active: true,
owner: msg.sender,
name: name,
metadataUri: metadataUri,
stakeRequirement: 0,
alertCount: 0,
confirmedCount: 0
});
emit BotRegistered(botId, msg.sender, name, metadataUri);
}
function updateBot(uint256 botId, string calldata newMetadataUri, bool active) external {
Bot storage b = bots[botId];
if (!b.registered) revert UnknownBot();
if (msg.sender != b.owner) revert NotBotOwner();
b.metadataUri = newMetadataUri;
b.active = active;
emit BotUpdated(botId, newMetadataUri, active);
}
/* ----------------------------- alert emission --------------------------- */
function emitAlert(
uint256 botId,
address subject,
uint8 alertType,
uint8 severity,
string calldata detailUri
) external returns (uint256 alertId) {
Bot storage b = bots[botId];
if (!b.registered) revert UnknownBot();
if (!b.active) revert BotInactive();
if (msg.sender != b.owner) revert NotBotOwner();
if (subject == address(0)) revert ZeroSubject();
alertId = nextAlertId++;
alerts[alertId] = Alert({
botId: botId,
subject: subject,
alertType: alertType,
severity: severity,
timestamp: uint64(block.timestamp),
detailUri: detailUri,
confirmed: false
});
subjectAlerts[subject].push(alertId);
b.alertCount++;
emit AlertEmitted(alertId, botId, subject, alertType, severity, detailUri);
}
/* ------------------------------- confirmation --------------------------- */
/// @notice Phase 1: Foundation reviews + confirms valid alerts. Phase 2
/// replaces with staked-community confirmation.
function confirmAlert(uint256 alertId) external onlyOwner {
Alert storage a = alerts[alertId];
if (a.timestamp == 0) revert UnknownAlert();
if (a.confirmed) return;
a.confirmed = true;
bots[a.botId].confirmedCount++;
emit AlertConfirmed(alertId, a.botId);
}
function rejectAlert(uint256 alertId, string calldata reason) external onlyOwner {
Alert storage a = alerts[alertId];
if (a.timestamp == 0) revert UnknownAlert();
emit AlertRejected(alertId, a.botId, reason);
}
/* ----------------------------------- views ------------------------------ */
/// @notice ROUND-3 FIX: cap maximum iterations to prevent griefable DoS
/// when an attacker spams a target subject with alerts. Consumers
/// (lending markets, etc.) that query this view in a transaction
/// path would otherwise be reverted by an attacker who emitted
/// millions of low-severity alerts against the subject.
uint256 public constant MAX_RECENT_ITERATIONS = 256;
/// @notice Returns the count of CONFIRMED alerts of `minSeverity` or
/// higher emitted within the last `windowSeconds` against the
/// given subject. Bounded at MAX_RECENT_ITERATIONS — for subjects
/// with > 256 recent alerts, only the most-recent 256 are
/// counted (still a useful signal; spam pushes older legit
/// alerts out of the window).
function recentConfirmedAlertCount(
address subject,
uint8 minSeverity,
uint256 windowSeconds
) external view returns (uint256 count) {
uint256[] storage ids = subjectAlerts[subject];
uint256 cutoff = block.timestamp > windowSeconds ? block.timestamp - windowSeconds : 0;
uint256 iter = 0;
for (uint256 i = ids.length; i > 0 && iter < MAX_RECENT_ITERATIONS; i--) {
Alert storage a = alerts[ids[i - 1]];
if (a.timestamp < cutoff) break;
if (a.confirmed && a.severity >= minSeverity) count++;
unchecked { iter++; }
}
}
function subjectAlertCount(address subject) external view returns (uint256) {
return subjectAlerts[subject].length;
}
}