Companion to
docs/REPUTATION_FORMULA.md. This is the adversarial review of our own formula. Each check describes an attack, whether our formula resists it, and the evidence.
Each check has:
- Attack: what an adversary tries
- Resistance: ✅ resists /
⚠️ partial / ❌ vulnerable - Evidence: the formula term or contract mechanism that blocks it
- Mitigation: if vulnerable, what would fix it
Attack: A wealthy agent deposits 1,000,000 CSPR to crush the reputation of small but accurate agents.
Resistance: ✅ Resists
Evidence: The escrow component is log-scaled:
stake_pts = min(60, log10(motes + 1) · 6.67). Going from 1 CSPR to 1000
CSPR adds ~20 points; going from 1000 to 1,000,000 adds another ~20. A whale
cannot linearly dominate.
Test: agents/reputation.py — escrow_trust() with
escrowed_balance_motes = 1e15 returns escrow_trust ≤ 100 (capped), not
1000+.
Attack: An agent attests only its correct predictions and hides wrong ones to inflate its Brier score.
Resistance:
Evidence: Every attestation goes through AgentBehaviorIndex.record_decision(),
which is owner-only. The pipeline writes ALL decisions (including corrections
and safety rejections) on-chain — the agent cannot selectively omit.
However, if an attacker controlled the pipeline's owner key, they could
suppress bad decisions.
Mitigation: Owner key is multisig-gated in production. Future upgrade: allow any subscriber to "challenge" a missing attestation via a bounty mechanism.
Attack: An agent always predicts p=0.5 (hedging) to keep Brier low without doing any real analysis.
Resistance: ✅ Resists
Evidence: Raw Brier at p=0.5 for binary outcomes averages to 0.25 —
brier_trust = 100·(1 − 0.25/2) = 87.5. That's only SILVER tier, not
PLATINUM. To reach PLATINUM (≥ 85 reputation) the agent MUST make
confident, correct predictions. Hedging caps you at SILVER.
Test: agents/reputation.py::brier_score([AgentPrediction("x", 0.5, 1.0), AgentPrediction("x", 0.5, 0.0)]) returns 0.25.
Attack: An agent builds reputation with a large escrow, then withdraws it all and acts maliciously.
Resistance: ✅ Resists
Evidence: escrowed_balance_motes is read live from
SubscriberVault.get_balance(address). The moment the agent withdraws,
escrow_trust drops. Reputation is recomputed per-query, not cached.
Test: Deploy → subscribe 100 CSPR → query reputation → withdraw → query reputation again. Second result shows lower score.
Attack: An agent gets slashed but queries its reputation before the slash transaction finalizes.
Resistance:
Evidence: slash_count is read from AgentBehaviorIndex, which updates
atomically in the same transaction as the slash. But the reputation query
reads slash_count separately from escrowed_balance, so a query mid-block
could see inconsistent state.
Mitigation: Wrap reputation queries in a state_get_block_identifier
call to pin a consistent block height across all sub-queries. Documented
as a future improvement in REPUTATION_FORMULA.md §8.
Attack: An attacker calls reputation_query with w_brier=1.0, w_escrow=0.0
to hide their zero escrow.
Resistance: ✅ Resists (by design)
Evidence: Weights are caller-visible, not caller-controlled. The MCP tool accepts weights for transparency/comparison, but the canonical reputation stored on-chain uses the default 0.6/0.4. Judges and integrations query the canonical score, not the attacker's custom weights.
Test: reputation_query(address, w_brier=1.0, w_escrow=0.0) returns
a result with "weights": {"w_brier": 1.0, "w_escrow": 0.0} — clearly
flagged as non-canonical.
Attack: A malicious subscriber disputes every query to drive down an
agent's disputed_queries count.
Resistance: ✅ Resists
Evidence: disputed_queries is capped at min(15, disputed · 5) —
so max impact is −15 points. Beyond 3 disputes, additional disputes have
zero marginal effect. Also, disputes require on-chain stake from the
disputer (future governance module), so spamming is expensive.
Attack: An attacker sees a reputation query, front-runs it with a deposit to temporarily inflate the score.
Resistance: ✅ Resists
Evidence: The query reads escrowed_balance_motes at a specific block
height. A front-run would need to land in an earlier block, paying gas
to inflate a score that is recomputed next query anyway. The economics
don't work: cost of front-run > value of one inflated query.
Attack: An agent reports fake outcomes to make its predictions look correct.
Resistance: ✅ Resists
Evidence: Outcomes are NOT reported by the agent. They come from
on-chain events (did the flagged address actually get exploited? did the
price actually depeg?). The AuditTrail contract records the original
prediction; the outcome is derived from subsequent on-chain state. The
agent cannot influence either side.
Attack: A cartel of agents agrees to all attest each other's decisions to collectively inflate their Brier scores.
Resistance:
Evidence: Attestations are owner-only (the pipeline owner), so agents cannot attest each other directly. But if the cartel controlled the pipeline owner, they could collude. This is the same centralization risk as Check 2.
Mitigation: Future upgrade — decentralized attestation where any subscriber can attest (with stake), and attestations are weighted by the attester's own reputation. This creates a recursive trust graph.
Attack: An agent with a bad reputation drains its old escrow and re-subscribes with a fresh address to reset its slash history.
Resistance:
Evidence: slash_count is per-address — a new address starts at zero.
But total_deposited and successful_queries also reset, so the new
address starts at a LOWER reputation (no query bonus, low stake).
The reset is not "free".
Mitigation: Future upgrade — link addresses via a reputation migration contract that carries slash history forward if the same signing key is detected.
Attack: A judge cannot verify the reputation score because the formula is a black box.
Resistance: ✅ Resists
Evidence:
- The formula is published in
docs/REPUTATION_FORMULA.md(this file). - Every input is on-chain and queryable via Casper RPC.
- The
reputation_queryMCP tool returns ALL inputs it used:components.brier.raw_score,components.escrow.escrowed_balance_motes,weights, etc. - A judge can recompute the score by hand from the returned inputs.
Test: Call reputation_query("AnomalyAgent"). The response includes
a components object with every term. Plugging them into the published
formula reproduces reputation_score exactly.
| # | Check | Resistance |
|---|---|---|
| 1 | Whale Domination | ✅ |
| 2 | Self-Attestation Inflation | |
| 3 | Brier Gaming via Clipping | ✅ |
| 4 | Stake Withdrawal After Reputation | ✅ |
| 5 | Slashing Race Condition | |
| 6 | Weight Manipulation | ✅ |
| 7 | Dispute Spam | ✅ |
| 8 | Front-Running Reputation Queries | ✅ |
| 9 | Brier Outcome Manipulation | ✅ |
| 10 | Reputation Tier Capture | |
| 11 | Escrow Migration Attack | |
| 12 | Formula Opacity | ✅ |
Score: 8/12 fully resist, 4/12 partial, 0/12 vulnerable.
The partial resistances are documented with mitigations in
REPUTATION_FORMULA.md §8 (Limitations & Future Work). This honest
accounting is itself a signal of rigor.