// SPDX-License-Identifier: MIT pragma solidity 0.8.23; interface IAereAgentBond { function bondOf(address operator, bytes32 agentId) external view returns ( uint256 amount, uint64 requestedAt, uint256 lifetimeSlashed, bool exists ); function operatorLifetimeSlashed(address operator) external view returns (uint256); } /** * @title AereAIReputation — composable on-chain reputation for agents * @notice Reputation = the sum of signed observations from registered * attestors. An attestor is any party the protocol delegates the * right to score from: the agent's customers (post AERE402 settle), * a third-party model auditor (Cantina/Anthropic), an EAS schema * operator, etc. * * The score is a uint256 — convention: 0 (no history) to 10_000 * (perfect, no slashes, many positive attestations). Apps decide * their own cutoffs (e.g. AERE402 require >= 3000 before routing). * * Score formula (kept simple for Phase 1): * * score = positiveCount * 10 * - 10 * lifetimeSlashed / SLASH_UNIT * - 20 * disputeCount * * clamped to [0, 10_000]. Apps that need a more sophisticated * curve (decay, recency-weighting, attestor weighting) read the * raw signal counts and compute their own. * * No PII. The agentId is a hash; the attestor identifier is a * registered address; observations include only a hash of the * off-chain evidence + an optional public URI. * * IMMUTABILITY: * - BOND immutable at deploy (the slashable-stake contract whose * lifetimeSlashed feeds the formula). * - SLASH_UNIT constant. * - Registered attestors set at deploy; no admin add post-deploy. * Phase 2 will use a DAO-voted attestor list; Phase 1 starts * with Foundation + a few hand-picked auditors. */ contract AereAIReputation { /* ================================ config ================================ */ IAereAgentBond public immutable BOND; uint256 public constant MAX_SCORE = 10_000; uint256 public constant SLASH_UNIT = 1e18; // 1 AERE slashed ⇒ -10 score points mapping(address => bool) public isAttestor; address[] public attestorList; /* ================================= state ================================= */ struct AgentStats { uint128 positiveCount; uint128 disputeCount; } /// @notice (operator, agentId) → counters fed by attestors. mapping(address => mapping(bytes32 => AgentStats)) internal _stats; /// AUDIT FIX (HIGH #20): dedup attestations by evidenceHash per attestor /// to prevent unbounded replay. mapping(address => mapping(bytes32 => bool)) public usedEvidence; /* ================================= events ================================= */ event Attested( address indexed operator, bytes32 indexed agentId, address indexed attestor, int8 delta, // +1 / -1 / 0 bytes32 evidenceHash, string evidenceUri ); /* ================================= errors ================================= */ error NotAttestor(); error InvalidDelta(); error EmptyEvidenceHash(); error EvidenceReplay(); error SelfAttest(); /* =============================== modifiers ============================== */ modifier onlyAttestor() { if (!isAttestor[msg.sender]) revert NotAttestor(); _; } /* ============================== constructor ============================= */ constructor(IAereAgentBond bond, address[] memory attestors) { require(address(bond) != address(0), "zero-bond"); BOND = bond; for (uint256 i = 0; i < attestors.length; i++) { require(attestors[i] != address(0), "zero-attestor"); require(!isAttestor[attestors[i]], "dup-attestor"); isAttestor[attestors[i]] = true; attestorList.push(attestors[i]); } } /* ================================= write ================================= */ /// @notice Attestor records an observation. delta ∈ {-1, 0, +1}: /// +1 positive interaction (settlement succeeded as agent /// described; model output matched committed attestation). /// 0 recorded-but-neutral (used for audit-trail without /// score change). /// -1 formal dispute (counted as malus; SEPARATE from a /// slash, which is the bond contract's territory). function attest( address operator, bytes32 agentId, int8 delta, bytes32 evidenceHash, string calldata evidenceUri ) external onlyAttestor { if (delta < -1 || delta > 1) revert InvalidDelta(); if (evidenceHash == bytes32(0)) revert EmptyEvidenceHash(); // AUDIT FIX (HIGH #22): block self-attest (attestor cannot rate // themselves as operator). if (msg.sender == operator) revert SelfAttest(); // AUDIT FIX (HIGH #20): dedup by (attestor, evidenceHash) so an // attestor cannot replay the same evidence 1000× for free score. if (usedEvidence[msg.sender][evidenceHash]) revert EvidenceReplay(); usedEvidence[msg.sender][evidenceHash] = true; AgentStats storage s = _stats[operator][agentId]; if (delta == 1) s.positiveCount += 1; else if (delta == -1) s.disputeCount += 1; emit Attested(operator, agentId, msg.sender, delta, evidenceHash, evidenceUri); } /* ================================= views ================================= */ function statsOf(address operator, bytes32 agentId) external view returns ( uint256 positiveCount, uint256 disputeCount, uint256 lifetimeSlashed ) { AgentStats storage s = _stats[operator][agentId]; ( , , uint256 slashed, ) = BOND.bondOf(operator, agentId); return (s.positiveCount, s.disputeCount, slashed); } /// @notice Score formula: positiveCount * 10 - 10 * slashed/SLASH_UNIT /// - 20 * disputeCount. Clamped to [0, 10_000]. function scoreOf(address operator, bytes32 agentId) external view returns (uint256) { AgentStats storage s = _stats[operator][agentId]; ( , , uint256 slashed, ) = BOND.bondOf(operator, agentId); // AUDIT FIX (HIGH #21): roll in operator-level lifetime slash so a // fresh agentId rotation cannot wipe the slash penalty. Apply at // half-weight against the per-agent score so a single bad agent // doesn't permanently destroy a separate clean agent's score — // but the penalty doesn't disappear. uint256 opSlashed = BOND.operatorLifetimeSlashed(operator); uint256 effectiveSlashed = slashed + (opSlashed > slashed ? (opSlashed - slashed) / 2 : 0); int256 raw = int256(uint256(s.positiveCount)) * 10 - int256(effectiveSlashed / SLASH_UNIT) * 10 - int256(uint256(s.disputeCount)) * 20; if (raw <= 0) return 0; uint256 r = uint256(raw); return r > MAX_SCORE ? MAX_SCORE : r; } function attestors() external view returns (address[] memory) { return attestorList; } }