Repository navigation
Maturity Mechanism and Score Release
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
- Appendices
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.
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
Diagram sources
Section sources
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
- SigvaraReputation.sol:184-189
- SigvaraReputation.sol:454-504
- SigvaraReputation.sol:541-557
- SigvaraReputation.sol:566-577
- SigvaraReputation.sol:658-665
- SigvaraReputation.sol:696-752
- SigvaraReputation.sol:771-788
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"
Diagram sources
getTotalScore is the canonical view for consumers. Its logic follows this order:
- Read the earned score from the latest finalized reputation.
- Read the matured anchor and its timestamp.
- Check whether the agent changed operators at or after the last maturity anchor (
operatorChangedAtis non-zero and not earlier thanmaturedAt). 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. - If the earned score is less than or equal to the anchor, return the earned score immediately. This handles score falls.
- Guard against missing anchors or disabled maturity rates.
- Extrapolate the released amount using wall-clock time since the anchor and the configured daily rate.
- 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"]
Diagram sources
Section sources
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
getTotalScoresee a value that climbs toward the earned score atmaturityRatePerDay. - 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
meetsThresholdgate 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"]
Diagram sources
Section sources
- reputation-model.md:226-243
- SigvaraReputation.sol:68-75
- SigvaraReputation.sol:686-752
- SigvaraReputation.t.sol:972-980
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
Diagram sources
Section sources
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
Diagram sources
Section sources
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
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
- SigvaraReputation.sol:566-577
- SigvaraReputation.sol:658-665
- SigvaraReputation.sol:701-744
- SigvaraReputation.sol:786-788
- SigvaraReputation.t.sol:454-456
- SigvaraReputation.t.sol:1043-1057
The delayed availability of reputation serves economic security goals:
- Raises the cost of farming: A short-lived burst of activity cannot be converted into immediate trust.
- Aligns incentives with sustained behavior: Trust grows only while the agent remains active and accountable.
- Gives dispute mechanisms time to act: Slashing, flagging, and governance responses can intervene before a farmed score is spent.
- Prevents liquidated identities: Transferring an agent does not transfer the right to spend its matured reputation instantly.
- 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
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"
Diagram sources
- SigvaraReputation.sol:12
- SigvaraReputation.sol:180-197
- SigvaraReputation.sol:600-631
- SigvaraReputation.sol:696-752
Section sources
- SigvaraReputation.sol:12
- SigvaraReputation.sol:180-197
- SigvaraReputation.sol:600-631
- SigvaraReputation.sol:696-752
-
View-only maturity calculation:
getTotalScorecomputes 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.timestampmeans maturity is independent of oracle liveness, protecting honest agents from outages. -
Staticcall degradation: Reading
operatorChangedAtthrough a selector avoids breaking upgrades where the identity registry lacks that function. -
Capped arithmetic: The final cast to
uint8is safe because the branch only runs when the released value is below the earned score, which is at most 100.
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
- SigvaraReputation.sol:566-577
- SigvaraReputation.sol:658-665
- SigvaraReputation.sol:701-744
- SigvaraReputation.t.sol:972-1057
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.
| 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
- Agent Registration and Lifecycle
- Contract Overview and Architecture
- Economic Security Model and Incentives
- Evidence Verification and Merkle Proofs
- Key Management and Rotation
- Maturity Mechanism and Score Release
- Proposal Workflow and Challenge System
- Score Storage and Data Model
- SigvaraIdentity Contract
- SigvaraOracleBond Contract
- SigvaraReputation Contract
- SigvaraStaking Contract
- Slashing Lifecycle and Dispute Resolution
- Stake Management Functions
- Status Management and Control
- Token and Fee Contracts
- Unbonding Period and Withdrawal Security
- Anti-Manipulation and Security Measures
- Community Flag Processing
- Evidence Sources and Verification
- External Trust Integration
- Merkle Proof System
- Oracle Score Computation
- Payment-Backed Attestations
- Scoring Model and Formulas
- Agent Creation and Key Management
- Agent Operations
- Agent Registration Workflow
- Challenge-Response Authentication
- Gate Access Control
- Integration Patterns and Examples
- SDK Overview and Setup
- Verifier Functions