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.
176 lines
7.2 KiB
Solidity
176 lines
7.2 KiB
Solidity
// 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;
|
||
}
|
||
}
|