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