Skip to content

Maturity Mechanism and Score Release

RunTimeAdmin edited this page Oct 27, 2026 · 7 revisions

Maturity Mechanism and Score Release

**Referenced Files in This Document** - [SigvaraReputation.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/SigvaraReputation.sol) - [reputation-model.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/reputation-model.md) - [SigvaraReputation.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/SigvaraReputation.t.sol) - [SigvaraReader.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/SigvaraReader.sol)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. Conclusion
  10. Appendices

Introduction

This document explains the maturity mechanism that controls when an agent’s reputation score becomes spendable. The system separates two concepts:

  • Earned score: the raw total computed from the six reputation factors after a proposal is finalized.
  • Spendable (matured) score: what consumers such as threshold checks actually read, released gradually over time.

The design intentionally prevents farm-and-cash-out behavior by making newly earned points available only at a bounded daily rate. It also makes score falls immediate while rises are gradual, and it restarts maturity clocks when an agent changes operators.

Project Structure

The maturity logic lives in the on-chain reputation contract, with supporting documentation and tests:

graph TB
Reputation["SigvaraReputation.sol<br/>On-chain reputation store"]
Docs["reputation-model.md<br/>Economic model and maturity policy"]
Tests["SigvaraReputation.t.sol<br/>Maturity and edge-case tests"]
Identity["SigvaraIdentity.sol<br/>Agent registration and operator state"]
Reputation --> Identity
Reputation --> Docs
Reputation --> Tests
Loading

Diagram sources

Section sources

Core Components

The maturity mechanism is centered around these storage fields and functions:

Symbol Purpose Behavior
maturityRatePerDay Points released per day Set through admin initialization or update; zero is rejected to avoid freezing maturity forever, and so is anything above 100 (MAX_MATURITY_RATE_PER_DAY), which already releases a whole score in a day (the ceiling is on main; the Arc testnet proxy rejects only zero)
maturedScore[didHash] Anchor point for a given agent The spendable score at maturedAt; written on finalize and slash. A handover is not written: getTotalScore restarts the anchor at zero on read
maturedAt[didHash] Anchor timestamp Wall-clock time used to extrapolate how much has been released since the anchor
getTotalScore(didHash) Spendable score view Returns matured value, capped at earned score
getEarnedScore(didHash) Raw score view Sum of the six factors before maturity
finalizeReputation(didHash) Applies oracle proposal Saves earned data and anchors maturity at the current spendable score
zeroReputation(didHash) Slash path Zeros earned factors, pending proposals, evidence root, matured anchor, and anchor timestamp
meetsThreshold(didHash, threshold) Consumer gate Uses getTotalScore, not getEarnedScore

Section sources

Architecture Overview

The maturity flow connects oracle proposals, finalization, wall-clock extrapolation, and consumer reads.

sequenceDiagram
participant Oracle as "Oracle"
participant Contract as "SigvaraReputation"
participant Identity as "SigvaraIdentity"
participant Consumer as "Consumer Contract"
Oracle->>Contract : "proposeReputation(didHash, data, evidenceRoot)"
Contract->>Contract : "validate factor caps"
Contract->>Contract : "store PendingScore"
Note over Contract : "Challenge window must expire"
Consumer->>Contract : "finalizeReputation(didHash)"
Contract->>Contract : "read getTotalScore(didHash) as anchor"
Contract->>Contract : "write earned ReputationData"
Contract->>Contract : "set maturedScore = anchor"
Contract->>Contract : "set maturedAt = block.timestamp"
Consumer->>Contract : "getTotalScore(didHash)"
Contract->>Identity : "staticcall operatorChangedAt(didHash)"
Identity-->>Contract : "operatorChangedAt or fallback"
Contract->>Contract : "extrapolate from anchor at maturityRatePerDay"
Contract-->>Consumer : "spendable score"
Loading

Diagram sources

Detailed Component Analysis

How getTotalScore Calculates Spendable Scores

getTotalScore is the canonical view for consumers. Its logic follows this order:

  1. Read the earned score from the latest finalized reputation.
  2. Read the matured anchor and its timestamp.
  3. Check whether the agent changed operators at or after the last maturity anchor (operatorChangedAt is non-zero and not earlier than maturedAt). If so, restart the clock: set the anchor timestamp to the operator change time and reset the anchor score to zero. This comes before the next step on purpose, so a score that had fully matured, or had fallen below its anchor, does not go to the buyer whole.
  4. If the earned score is less than or equal to the anchor, return the earned score immediately. This handles score falls.
  5. Guard against missing anchors or disabled maturity rates.
  6. Extrapolate the released amount using wall-clock time since the anchor and the configured daily rate.
  7. Cap the result at the earned score.
flowchart TD
Start(["getTotalScore entry"]) --> Earned["Read earned score"]
Earned --> Anchor["Read maturedScore and maturedAt"]
Anchor --> Handover["Check operatorChangedAt on identity registry"]
Handover --> Changed{"Operator changed at or after anchor?"}
Changed --> |Yes| Restart["Set anchor timestamp to change time<br/>Reset anchor score to 0"]
Changed --> |No| SkipRestart["Keep existing anchor"]
Restart --> FallCheck{"earned <= anchor?"}
SkipRestart --> FallCheck
FallCheck --> |Yes| ReturnEarned["Return earned score"]
FallCheck --> |No| TimeGuard{"since == 0 or rate == 0?"}
TimeGuard --> |Yes| ReturnEarned
TimeGuard --> |No| Released["anchor + elapsedDays * maturityRatePerDay"]
Released --> Capped{"released >= earned?"}
Capped --> |Yes| ReturnEarned
Capped --> |No| ReturnReleased["Return released"]
Loading

Diagram sources

Section sources

Anti-Farm-and-Cash-Out Protection

The farm-and-cash-out attack is the primary reason for maturity: an attacker could build a high score quickly, use it to gain trust, and then exit before decay or slashing can take effect.

Key protections include:

  • Gradual release: A new score does not become fully spendable immediately. Consumers reading getTotalScore see a value that climbs toward the earned score at maturityRatePerDay.
  • Anchor-based growth: Growth starts from the spendable anchor, not the earned total. Re-proposing a higher score does not instantly reset the maturity clock.
  • Immediate drops: Falls bypass maturity delay because slowing bad news would protect the agent rather than the party relying on the score.
  • Operator handover restart: Buying or transferring an aged, scored identity does not transfer the right to spend it immediately, however mature the score was.
  • Thresholds use matured score: Functions like meetsThreshold gate on spendable reputation, not raw earned reputation.
flowchart TD
Attack["Farm-and-cash-out attempt"] --> Earn["Earn high score quickly"]
Earn --> ImmediateUse{"Can consumer use full score now?"}
ImmediateUse --> |No| Mature["Spendable score grows at maturityRatePerDay"]
Mature --> Window["Dispute, flagging, and slashing have time to act"]
Window --> Safe["Attack cost exceeds expected benefit"]
ImmediateUse --> |Yes| Risk["Trust exploited before penalties apply"]
Loading

Diagram sources

Section sources

Operator Handover Handling

When an agent changes operators, the maturity clock restarts. The implementation performs a low-level static call to the identity registry to read operatorChangedAt(didHash):

  • If the operator change happened at or after the current anchor, the anchor timestamp moves forward to that change time. Equality counts: Arc timestamps have one second granularity and consecutive blocks can share one, so a transfer accepted in the same second as a finalize has operatorChangedAt == maturedAt. That finalize anchored what the seller had matured, so the transfer must still restart the clock. A finalize that runs after the transfer re-anchors from the restarted value.
  • The anchor score resets to zero. A change time of zero (the agent has never changed hands) is not a handover.
  • This runs before the check on earned versus anchor, so a score that had fully matured, one the oracle has just finalized again, and one that has fallen below its anchor all start the buyer at zero. That order and the equality case have been live on the Arc testnet reputation proxy since 26 October 2026. The September code it replaced checked earned against anchor first and compared strictly, so a score already at or below its anchor went to the buyer whole.
  • The caller cannot claim instant access to a previously matured score simply by buying the identity.
  • If the identity registry does not expose operatorChangedAt, the call degrades gracefully instead of reverting every score read.
sequenceDiagram
participant Reader as "getTotalScore"
participant Registry as "SigvaraIdentity"
participant Agent as "Agent DID"
Reader->>Registry : "staticcall operatorChangedAt(Agent)"
Registry-->>Reader : "timestamp if supported"
Reader->>Reader : "compare with maturedAt"
alt "Operator changed at or after anchor"
Reader->>Reader : "set anchor timestamp to change time"
Reader->>Reader : "reset anchor score to 0"
else "No change or unsupported registry"
Reader->>Reader : "keep existing anchor"
end
Loading

Diagram sources

Section sources

Asymmetric Behavior: Immediate Falls vs Gradual Rises

The maturity mechanism is intentionally asymmetric:

Direction Behavior Reason
Rise Gradual release from the anchor at maturityRatePerDay Prevents immediate exploitation of newly earned trust
Fall Immediate return of the lower earned score Protects consumers who rely on the score

This asymmetry is enforced directly in getTotalScore: if the earned score is below the anchor, the function returns the earned score without applying maturity extrapolation.

flowchart TD
Earned["New earned score"] --> Anchor["Current anchor"]
Anchor --> Compare{"earned < anchor?"}
Compare --> |Yes| Drop["Return earned immediately"]
Compare --> |No| Grow["Grow from anchor at maturityRatePerDay"]
Drop --> Consumer["Consumers see reduced score now"]
Grow --> Consumer
Loading

Diagram sources

Section sources

Examples of Maturity Calculations

The test suite demonstrates several concrete scenarios:

Scenario Configuration Result
New finalized score Rate = 4 points/day Earned score is 100, but spendable score starts at 0
One day later Rate = 4 points/day Spendable score = 4
Ten days later Rate = 4 points/day Spendable score = 40
Long time later Rate = 4 points/day Spendable score approaches 100 but never overshoots
No further finalizations Rate = 4 points/day Maturity continues based on wall-clock time
Score drop Earned falls below anchor Spendable score reflects the lower earned value immediately
Re-finalize same high score Rate = 4 points/day Anchor stays at the previous spendable value; no jump
Slash Zero reputation Anchor and timestamp reset; score stays at 0
Operator handover Change at or after anchor Anchor timestamp moves forward and anchor score resets to 0
Handover of a fully matured score Rate = 4 points/day Buyer’s spendable score starts at 0 and climbs from there, although the earned score is unchanged

These behaviors are validated by tests covering release rate, cap behavior, wall-clock accrual, immediate drops, anchor anchoring, slash resets, and handover restarts.

Section sources

Edge Cases

Several edge cases are explicitly handled:

Edge Case Handling
No finalized score yet getTotalScore returns 0 because there is no matured anchor to grow from
First finalize with zero timestamp Treated as missing anchor; maturity does not instantly release everything
Zero maturity rate Admin setters reject zero, and on main anything above 100 points a day; if somehow active, maturity is treated as disabled and the earned score is returned
Handover in the same second as a finalize Counts as a handover after it: operatorChangedAt >= maturedAt restarts the clock
Handover of a score that already sits at or below its anchor The restart still applies, because the handover check runs before the earned-versus-anchor check
Legacy identity registry without operatorChangedAt Static call fails gracefully; maturity behaves as if no handover was recorded
Re-proposing a higher score Anchor is taken from the current spendable score, preventing a reset-to-peak exploit
Slash during challenge window Finalize still runs permissionlessly, but the write path validates scorability; slash clears both earned and matured state
Threshold check before maturity meetsThreshold uses getTotalScore, so a fresh high earned score cannot pass thresholds immediately

Section sources

Economic Rationale Behind Delayed Score Availability

The delayed availability of reputation serves economic security goals:

  1. Raises the cost of farming: A short-lived burst of activity cannot be converted into immediate trust.
  2. Aligns incentives with sustained behavior: Trust grows only while the agent remains active and accountable.
  3. Gives dispute mechanisms time to act: Slashing, flagging, and governance responses can intervene before a farmed score is spent.
  4. Prevents liquidated identities: Transferring an agent does not transfer the right to spend its matured reputation instantly.
  5. Protects consumers more than agents on bad news: Drops are immediate because downstream systems need accurate risk signals without delay.

The documentation states that maturity puts a floor under how fast trust can be acquired, turning a farm-and-cash-out opportunity into a longer, costlier process.

Section sources

Dependency Analysis

The maturity mechanism depends on several contracts and interfaces:

classDiagram
class SigvaraReputation {
+uint256 maturityRatePerDay
+mapping didHash => uint8 maturedScore
+mapping didHash => uint256 maturedAt
+getTotalScore(didHash) uint8
+getEarnedScore(didHash) uint8
+finalizeReputation(didHash) void
+zeroReputation(didHash) void
+meetsThreshold(didHash, threshold) bool
}
class SigvaraIdentity {
+getIdentity(didHash) AgentIdentity
+stakeView() IStakeView
+operatorChangedAt(didHash) uint256
}
class IOperatorSet {
+isActiveOperator(address) bool
}
class IStakeView {
+hasMinimumStake(bytes32) bool
}
SigvaraReputation --> SigvaraIdentity : "reads agent state"
SigvaraReputation --> IOperatorSet : "optional bonded operator check"
SigvaraIdentity --> IStakeView : "bond verification"
Loading

Diagram sources

Section sources

Performance Considerations

  • View-only maturity calculation: getTotalScore computes maturity on read, avoiding gas costs on every write and allowing maturity to accrue even when the oracle stops proposing.
  • Wall-clock extrapolation: Using block.timestamp means maturity is independent of oracle liveness, protecting honest agents from outages.
  • Staticcall degradation: Reading operatorChangedAt through a selector avoids breaking upgrades where the identity registry lacks that function.
  • Capped arithmetic: The final cast to uint8 is safe because the branch only runs when the released value is below the earned score, which is at most 100.

Troubleshooting Guide

Common issues and their likely causes:

Symptom Likely Cause Resolution
getTotalScore returns 0 after registration No finalized score exists yet Wait for the first oracle proposal and successful finalization
High earned score but threshold checks fail Maturity has not released enough points Allow time to pass or adjust expectations around maturityRatePerDay
Score jumps unexpectedly after re-proposal Misunderstanding of anchor behavior Remember the anchor is the spendable score, not the earned score
Score drops immediately after finalization Earned score fell below anchor This is intentional; falls are not delayed
Operator change appears to reset maturity Identity registry recorded an operator change at or after the anchor Expected behavior; buyer must re-earn spendable reputation
Maturity seems frozen maturityRatePerDay is zero or not initialized Admin must set a positive rate via initializer or setter
Legacy identity registry causes unexpected behavior operatorChangedAt not present Implementation degrades gracefully; maturity proceeds without handover restart

Section sources

Conclusion

The maturity mechanism is a deliberate economic control layer. It ensures that reputation is not just a number produced by an oracle, but a signal that reflects sustained, accountable behavior. By releasing scores gradually, applying drops immediately, and restarting maturity on operator handovers, the system discourages farming, cashing out, and identity trading. Consumers should always read getTotalScore or use meetsThreshold, because those reflect the spendable reputation that the protocol intends to protect.

Appendices

Key Function Reference

Function Role Important Detail
getTotalScore Spendable score Extrapolates from anchor at maturityRatePerDay; caps at earned score
getEarnedScore Raw score Sums six factors; unaffected by maturity
finalizeReputation Apply proposal Anchors maturity at current spendable score
zeroReputation Slash Clears earned, pending, evidence, and matured state
initializeV4 Set maturity rate Rejects zero to avoid freezing maturity, and on main anything above 100 points a day
setMaturityRate Update maturity rate Also rejects zero, and on main anything above 100
meetsThreshold Gate Uses matured score, and nothing else: it ignores status and bond, so SigvaraReader.isTrusted adds both

Section sources

Overview

Clone this wiki locally