aere-contracts/contracts/agentic/AereAIReputation.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

176 lines
7.2 KiB
Solidity
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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