Repository navigation
SigvaraIdentity Contract
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
- Appendices
SigvaraIdentity is the foundational agent registry for the Sigvara system. It anchors AI agent identities on-chain, binds an Ethereum operator to each agent, stores the agent’s Ed25519 public key, and enforces a strict lifecycle around bonding, suspension, slashability, and operator control. The contract uses deterministic DID hashing so that clients can reproduce identity identifiers off-chain without querying storage.
The most important design principle is “bond before trust”: registration alone does not make an agent Active. An agent becomes Active only after its operator has deposited enough stake to satisfy the staking collateral check. This prevents free minting of unslashable identities and keeps reputation, slashing, and operator accountability aligned with real economic backing.
SigvaraIdentity lives under the main Solidity source tree alongside related contracts such as SigvaraStaking, SigvaraReputation, and SigvaraEpochFees. Its tests are in the Foundry test suite, and client-side usage is demonstrated in the SDK verifier helper.
graph TB
Client["Client or Off-chain Tool"] --> SDK["SDK Verifier Helper"]
SDK --> Registry["SigvaraIdentity"]
Registry --> StakingView["IStakeView<br/>hasMinimumStake(didHash)"]
Registry --> StakingCore["SigvaraStaking<br/>(STAKING_CORE_ROLE)"]
Diagram sources
- SigvaraIdentity.sol:18-20
- SigvaraIdentity.sol:37-41
- SigvaraIdentity.sol:177-195
- SigvaraStaking.sol:309-326
Section sources
This section documents the data model, roles, events, and primary write functions that define the agent registry.
AgentIdentity is the canonical record for one agent identity. It contains:
-
operator: the Ethereum address authorized to manage stake-related operations and status transitions. -
agentAddress: the agent’s Ethereum address; this forms the DID component. -
ed25519PubKey: a raw 32-byte Ed25519 public key used by off-chain challenge-response authentication. -
status: the current lifecycle state. -
registeredAt: the block timestamp when the identity was first registered.
classDiagram
class AgentIdentity {
+address operator
+address agentAddress
+bytes32 ed25519PubKey
+AgentStatus status
+uint256 registeredAt
}
class SigvaraIdentity {
+registerAgent(agentAddress, ed25519PubKey, signature) bytes32
+rotatePublicKey(didHash, newEd25519PubKey) void
+updateStatus(didHash, newStatus) void
+offerOperatorTransfer(didHash, newOperator) void
+acceptOperatorTransfer(didHash) void
+computeDidHash(agentAddress) bytes32
+getIdentity(didHash) AgentIdentity
+isActive(didHash) bool
}
SigvaraIdentity --> AgentIdentity : "stores"
Diagram sources
Section sources
SigvaraIdentity uses OpenZeppelin upgradeable access control:
-
DEFAULT_ADMIN_ROLEandUPGRADER_ROLEare granted to the admin, intended to be a governance timelock on production. -
STAKING_CORE_ROLEis granted to the staking core so it can initiate slashes, suspend agents during disputes, and reinstate them after dispute resolution. - The contract is UUPS-upgradeable, with
_authorizeUpgraderestricted to the upgrader role. - The mainnet target splits the two: an admin Safe holds
DEFAULT_ADMIN_ROLEand a 48 hour timelock holdsUPGRADER_ROLE. On main,initializemakesUPGRADER_ROLEits own role admin and the admin ofSTAKING_CORE_ROLE, so the admin cannot grant either role in the next transaction, and a grant ofSTAKING_CORE_ROLEwaits out the timelock’s delay. The admin can still revokeSTAKING_CORE_ROLEat once (TimelockedGrants), but notUPGRADER_ROLE. -
setStakeViewrequiresUPGRADER_ROLEon main, because the contract it points at decides who counts as bonded. -
GUARDIAN_ROLE(fromInflowGuard) may callpauseInflows(), which makesregisterAgentrevert withInflowsPausedUntilfor three days. A pause ends by itself, the guardian or the admin can lift it early, and the next one cannot start until a day after the last would have ended. Status changes, key rotation and operator transfers are never paused. - The Arc testnet proxy has run this implementation since 26 October 2026, but it was initialised by the September code, so its admin still administers every role. One EOA holds both the admin and the upgrade role there, so
setStakeViewis still a single-key call. The guardian role is granted.
Section sources
- SigvaraIdentity.sol:37-41
- SigvaraIdentity.sol:177-195
- SigvaraIdentity.sol:646-647
- InflowGuard.sol
- TimelockedGrants.sol
- 0005-governance-and-emergency-powers.md
Important emitted events include:
-
AgentRegistered: emitted when a new agent is registered. -
PublicKeyRotated: emitted when the Ed25519 key changes. -
AgentStatusUpdated: emitted for every status transition. -
OperatorTransferOffered,OperatorTransferCancelled,OperatorTransferred: emitted during operator handover. -
StakeViewSet: emitted when the collateral oracle is configured. -
InflowsPausedandInflowsResumed(fromInflowGuard): emitted when a guardian pause starts or is lifted early.
These events are the primary signal layer for indexers and off-chain monitors tracking agent lifecycle and operator changes.
Section sources
SigvaraIdentity is the identity layer. It does not hold tokens itself but depends on a staking view contract to enforce minimum collateral before allowing an agent to become Active. The staking core also holds special authority to mark agents as Slashed and to apply slash-related suspensions.
sequenceDiagram
participant Operator as "Operator"
participant Agent as "Agent Address"
participant Registry as "SigvaraIdentity"
participant StakeView as "IStakeView"
participant StakingCore as "SigvaraStaking"
Agent-->>Operator : signature over registrationDigest
Operator->>Registry : registerAgent(agentAddress, ed25519PubKey, signature)
Registry-->>Operator : didHash
Note over Registry : Status is PendingBond
Operator->>StakingCore : depositStake(didHash, amount)
StakingCore->>Registry : updateStatus(didHash, Active), once the stake reaches minimumStake
Registry->>StakeView : hasMinimumStake(didHash)
StakeView-->>Registry : true/false
Registry-->>StakingCore : success or revert
StakingCore->>Registry : updateStatus(didHash, Suspended)
StakingCore->>Registry : updateStatus(didHash, Slashed)
Diagram sources
Section sources
The lifecycle is intentionally restrictive:
- Registration creates
PendingBond. - Only after sufficient stake is confirmed does the agent become
Active. - Operators can suspend themselves; the staking core can suspend during slash initiation.
- Reinstatement into
Activerequires collateral. -
Slashedis terminal.
stateDiagram-v2
[*] --> PendingBond : "registerAgent()"
PendingBond --> Active : "depositStake() reaches minimumStake"
Active --> Suspended : "operator.updateStatus() or StakingCore.initiateSlash()"
Suspended --> Active : "operator.updateStatus() or StakingCore reinstates"
Active --> Slashed : "StakingCore.executeSlash()"
Suspended --> Slashed : "StakingCore.executeSlash()"
Slashed --> [*] : "terminal"
Diagram sources
Key invariants:
- Registration mints
PendingBond, neverActive. - Any transition into
Activerequires the agent to hold at leastminimumStake. - Nothing returns to
PendingBond; an agent does not become un-bonded, it suspends and exits. - Only
STAKING_CORE_ROLEcan setSlashed. -
Slashedis terminal: no key rotation, no status change. - Suspended agents may rotate their Ed25519 key for compromise recovery.
Section sources
Registration proves that the agent address controls the claimed identity and agrees to be associated with the caller as operator.
Inputs:
-
agentAddress: the agent’s Ethereum address. -
ed25519PubKey: raw 32-byte Ed25519 public key. -
signature: the agent’s signature overregistrationDigest.
Validation and behavior:
- Reverts with
InflowsPausedUntilwhile a guardian pause is running. - Rejects zero agent address and zero public key.
- Verifies the signature using
SignatureChecker, supporting both EOAs and ERC-1271 contracts. - Computes
didHashdeterministically from chain ID and agent address. - Rejects duplicate registrations.
- Stores the identity with status
PendingBond. - Adds the agent to the operator’s indexed list.
- Emits
AgentRegistered.
flowchart TD
Start(["registerAgent Entry"]) --> ValidateInputs["Validate agentAddress and ed25519PubKey"]
ValidateInputs --> VerifySig["Verify agent signature over registrationDigest"]
VerifySig --> ComputeDid["Compute didHash"]
ComputeDid --> CheckDuplicate{"Already registered?"}
CheckDuplicate --> |Yes| RevertDuplicate["Revert AlreadyRegistered"]
CheckDuplicate --> |No| StoreIdentity["Store AgentIdentity with PendingBond"]
StoreIdentity --> IndexOperator["Add didHash to operatorAgents"]
IndexOperator --> EmitEvent["Emit AgentRegistered"]
EmitEvent --> End(["Return didHash"])
Diagram sources
Security properties:
- The digest binds chain ID, registry address, agent address, operator, and Ed25519 key, preventing replay across chains, deployments, operators, and key swaps.
- Signature verification supports ERC-1271, so agent contracts like Safe wallets can register themselves.
Section sources
Only the operator may rotate the Ed25519 public key. Slashed agents cannot rotate. Suspended agents can rotate, enabling recovery after key compromise.
Behavior:
- Requires the agent to be registered.
- Requires the caller to be the operator.
- Rejects slashed agents.
- Rejects zero public keys.
- Updates the stored key and emits
PublicKeyRotated.
flowchart TD
Start(["rotatePublicKey Entry"]) --> RequireRegistered["Require registered"]
RequireRegistered --> RequireOperator["Require operator"]
RequireOperator --> SlashCheck{"Status is Slashed?"}
SlashCheck --> |Yes| RevertSlashed["Revert SlashedAgentImmutable"]
SlashCheck --> |No| ZeroKeyCheck{"New key is zero?"}
ZeroKeyCheck --> |Yes| RevertZero["Revert ZeroPubKey"]
ZeroKeyCheck --> UpdateKey["Update ed25519PubKey"]
UpdateKey --> EmitRotate["Emit PublicKeyRotated"]
EmitRotate --> End(["Exit"])
Diagram sources
Section sources
updateStatus is the central gatekeeper for lifecycle transitions.
Rules:
- Slashed agents are immutable: no further status updates.
- Transitions back to
PendingBondare forbidden. - If the target status is
Slashed, the caller must haveSTAKING_CORE_ROLE. - Otherwise, non-staking-core callers must be the operator.
- If the agent is under slash suspension (
slashSuspended), the operator cannot lift it; only the staking core can reinstate. - Transitioning to
Activerequires the stake view to be set and the agent to have sufficient collateral. - When the staking core sets
Suspended, it marks the slash suspension lock; when it setsActive, it clears the lock.
flowchart TD
Start(["updateStatus Entry"]) --> RequireRegistered["Require registered"]
RequireRegistered --> SlashImmutable{"Current status Slashed?"}
SlashImmutable --> |Yes| RevertSlashed["Revert SlashedAgentImmutable"]
SlashImmutable --> |No| PendingBondCheck{"Target is PendingBond?"}
PendingBondCheck --> |Yes| RevertPending["Revert CannotReturnToPendingBond"]
PendingBondCheck --> |No| RoleCheck{"Target is Slashed?"}
RoleCheck --> |Yes| RequireStakingCore["Require STAKING_CORE_ROLE"]
RoleCheck --> |No| OperatorCheck{"Caller is operator?"}
OperatorCheck --> |No| RevertNotOperator["Revert NotOperator"]
OperatorCheck --> |Yes| SlashLockCheck{"slashSuspended?"}
SlashLockCheck --> |Yes| RevertSlashLock["Revert SlashSuspensionLocked"]
SlashLockCheck --> |No| ActiveCollateral{"Target is Active?"}
ActiveCollateral --> |Yes| CheckCollateral["Check stakeView.hasMinimumStake"]
ActiveCollateral --> |No| UpdateState["Update status"]
CheckCollateral --> CollateralOk{"Collateral sufficient?"}
CollateralOk --> |No| RevertCollateral["Revert InsufficientCollateral"]
CollateralOk --> |Yes| UpdateState
UpdateState --> SlashLockUpdate{"Is staking core toggling Suspended/Active?"}
SlashLockUpdate --> SetLock["Set or clear slashSuspended"]
SetLock --> EmitStatus["Emit AgentStatusUpdated"]
EmitStatus --> End(["Exit"])
Diagram sources
Section sources
Operator transfer is a two-step process to prevent accidental loss of control:
- Current operator offers the agent to a new operator.
- New operator accepts only if they control the offered address and the agent is bonded.
Constraints:
- Slashed agents cannot be transferred.
- Transfers are blocked while slash suspension is pending.
- New operator cannot be zero or the same as the current operator.
- Acceptance requires sufficient collateral through the stake view.
- On acceptance, the operator index is updated, ownership metadata is recorded, and
OperatorTransferredis emitted. The recorded change time (operatorChangedAt) restarts the score’s maturity inSigvaraReputation, however mature the score was. The Arc testnet reputation proxy has done this since 26 October 2026.
sequenceDiagram
participant OldOp as "Old Operator"
participant NewOp as "New Operator"
participant Registry as "SigvaraIdentity"
OldOp->>Registry : offerOperatorTransfer(didHash, newOperator)
Registry-->>OldOp : emit OperatorTransferOffered
NewOp->>Registry : acceptOperatorTransfer(didHash)
Registry->>Registry : validate slash and collateral
Registry-->>NewOp : emit OperatorTransferred
Diagram sources
Section sources
SigvaraIdentity implements a deterministic DID method:
didHash = keccak256("did:sigvara:", block.chainid, ":", agentAddress)- Clients can compute
didHashoff-chain viacomputeDidHash. - The registry stores the agent’s Ethereum address and Ed25519 public key, enabling verifiers to bind on-chain identity to cryptographic credentials.
The repository’s architecture decision document explains why ERC-8004 is chosen as the identity layer. In practice, SigvaraIdentity provides the on-chain anchor and operator-controlled identity records, while higher layers use DID-based references and verifiable claims. The ADR names ERC-8004 agent ids as the long-term key and calls this contract legacy, as a direction of travel. Its 22 October 2026 update records that the cutover has not happened: every agent is still keyed by its didHash, script/Deploy.s.sol deploys this contract for mainnet, and it is what the deployed components read and write.
graph LR
AgentAddr["Agent Ethereum Address"] --> DidMethod["DID Method<br/>did:sigvara:<chainId>:<agentAddress>"]
DidMethod --> DidHash["Deterministic didHash"]
DidHash --> Registry["SigvaraIdentity Storage"]
Registry --> PublicKey["Ed25519 Public Key"]
Diagram sources
Section sources
SigvaraIdentity has minimal but critical dependencies:
- OpenZeppelin upgradeable contracts for access control and proxy upgradeability.
-
TimelockedGrantsandInflowGuard, two small abstract bases in this repository (main): the first lets the admin revoke at once any role the upgrader administers, the second adds the guardian’s pause on registrations. - OpenZeppelin cryptography utilities for signature verification.
- A staking view interface to enforce collateral requirements.
- Integration with SigvaraStaking through role-based slash and suspension flows.
graph TB
OZAccess["OpenZeppelin AccessControlUpgradeable"] --> Registry["SigvaraIdentity"]
OZProxy["OpenZeppelin UUPSUpgradeable"] --> Registry
OZCrypto["OpenZeppelin SignatureChecker"] --> Registry
IStakeView["IStakeView"] --> Registry
SigvaraStaking["SigvaraStaking"] --> Registry
Diagram sources
Section sources
- Operator agent indexing was optimized from linear scan to constant-time swap-and-pop removal using a position map. This prevents long operator lists from blocking transfers.
- Paging helpers exist for large operator lists, but consumers should page carefully because removal order is not stable due to swap-and-pop.
operatorAgentCountgives the length to page against, andgetOperatorAgentsPagedworks out the remaining length instead of addingoffsetandlimit, so a limit of the maximum value returns the rest of the list without overflowing. - Registration is gas-only and leaves agents in
PendingBond, avoiding expensive activation logic until collateral is posted. - View functions like
computeDidHashandisActiveare cheap and suitable for off-chain checks before submitting transactions.
Common issues and their causes:
-
Registration reverts with bad signature:
- Missing or invalid signature.
- Signature signed by wrong agent key.
- Signature lifted by another operator.
- Public key swapped after signing.
- Signature replayed against another registry deployment.
-
Registration reverts with
InflowsPausedUntil:- A guardian pause is running. It ends by itself within three days, or sooner if the guardian or admin lifts it.
-
Activation fails:
- Stake view not set.
- Agent does not meet minimum stake.
- Attempting to return to
PendingBond.
-
Operator transfer fails:
- No outstanding offer.
- Caller is not the offered recipient.
- Slash suspension is pending.
- Insufficient collateral.
- Target operator is zero or same as current operator.
-
Key rotation fails:
- Caller is not operator.
- Agent is slashed.
- New public key is zero.
-
Status manipulation prevention:
- Only staking core can slash.
- Operator cannot lift slash suspension.
- Slashed agents are immutable.
- Returning to
Activealways requires collateral.
Section sources
- SigvaraIdentity.sol:146-160
- SigvaraIdentity.sol:295-397
- SigvaraIdentity.sol:410-468
- SigvaraIdentity.t.sol:526-634
- SigvaraIdentity.t.sol:655-850
SigvaraIdentity provides a secure, deterministic, and economically grounded agent registry. Its design ensures that identity, operator control, cryptographic keys, and stake-backed accountability are tightly coupled. The lifecycle prevents free minting of unslashable identities, protects against key hijacking and operator abuse, and exposes clear events and views for off-chain systems to monitor and interact with agent identities.
- Client computes
registrationDigest(agentAddress, operator, ed25519PubKey). - Agent address signs the digest.
- Operator calls
registerAgent(agentAddress, ed25519PubKey, signature). - Registry emits
AgentRegisteredand stores the agent inPendingBond. - Operator deposits stake through the staking contract until
hasMinimumStakereturns true. - The deposit that carries the bond to
minimumStakeactivates the agent:SigvaraStakingcallsupdateStatus(didHash, Active)itself, so the operator does not have to. - Registry verifies collateral and emits
AgentStatusUpdated.
Section sources
- Operator detects key compromise or wants to rotate.
- Operator calls
rotatePublicKey(didHash, newEd25519PubKey). - Registry validates operator and rejects slashed agents.
- Registry emits
PublicKeyRotated.
Section sources
- Current operator calls
offerOperatorTransfer(didHash, newOperator). - New operator calls
acceptOperatorTransfer(didHash). - Registry validates slash state and collateral.
- Registry updates operator index and emits
OperatorTransferred.
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