Skip to content

SigvaraIdentity Contract

RunTimeAdmin edited this page Nov 3, 2026 · 19 revisions

SigvaraIdentity Contract

**Referenced Files in This Document** - [SigvaraIdentity.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/SigvaraIdentity.sol) - [README.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/README.md) - [0001-erc8004-as-identity-layer.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/adr/0001-erc8004-as-identity-layer.md) - [SigvaraStaking.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/SigvaraStaking.sol) - [SigvaraIdentity.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/SigvaraIdentity.t.sol) - [verifier.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/verifier.ts) - [InflowGuard.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/InflowGuard.sol) - [TimelockedGrants.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/TimelockedGrants.sol) - [0005-governance-and-emergency-powers.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/adr/0005-governance-and-emergency-powers.md)

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

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.

Project Structure

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)"]
Loading

Diagram sources

Section sources

Core Components

This section documents the data model, roles, events, and primary write functions that define the agent registry.

AgentIdentity Struct

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"
Loading

Diagram sources

Section sources

Roles and Upgradeability

SigvaraIdentity uses OpenZeppelin upgradeable access control:

  • DEFAULT_ADMIN_ROLE and UPGRADER_ROLE are granted to the admin, intended to be a governance timelock on production.
  • STAKING_CORE_ROLE is 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 _authorizeUpgrade restricted to the upgrader role.
  • The mainnet target splits the two: an admin Safe holds DEFAULT_ADMIN_ROLE and a 48 hour timelock holds UPGRADER_ROLE. On main, initialize makes UPGRADER_ROLE its own role admin and the admin of STAKING_CORE_ROLE, so the admin cannot grant either role in the next transaction, and a grant of STAKING_CORE_ROLE waits out the timelock’s delay. The admin can still revoke STAKING_CORE_ROLE at once (TimelockedGrants), but not UPGRADER_ROLE.
  • setStakeView requires UPGRADER_ROLE on main, because the contract it points at decides who counts as bonded.
  • GUARDIAN_ROLE (from InflowGuard) may call pauseInflows(), which makes registerAgent revert with InflowsPausedUntil for 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 setStakeView is still a single-key call. The guardian role is granted.

Section sources

Events

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.
  • InflowsPaused and InflowsResumed (from InflowGuard): 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

Architecture Overview

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)
Loading

Diagram sources

Section sources

Detailed Component Analysis

Agent Lifecycle State Machine

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 Active requires collateral.
  • Slashed is 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"
Loading

Diagram sources

Key invariants:

  • Registration mints PendingBond, never Active.
  • Any transition into Active requires the agent to hold at least minimumStake.
  • Nothing returns to PendingBond; an agent does not become un-bonded, it suspends and exits.
  • Only STAKING_CORE_ROLE can set Slashed.
  • Slashed is terminal: no key rotation, no status change.
  • Suspended agents may rotate their Ed25519 key for compromise recovery.

Section sources

Registration Workflow: registerAgent

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 over registrationDigest.

Validation and behavior:

  • Reverts with InflowsPausedUntil while 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 didHash deterministically 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"])
Loading

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

Key Rotation: rotatePublicKey

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"])
Loading

Diagram sources

Section sources

Status Management: updateStatus

updateStatus is the central gatekeeper for lifecycle transitions.

Rules:

  • Slashed agents are immutable: no further status updates.
  • Transitions back to PendingBond are forbidden.
  • If the target status is Slashed, the caller must have STAKING_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 Active requires 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 sets Active, 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"])
Loading

Diagram sources

Section sources

Operator Transfer: offerOperatorTransfer, cancelOperatorTransfer, acceptOperatorTransfer

Operator transfer is a two-step process to prevent accidental loss of control:

  1. Current operator offers the agent to a new operator.
  2. 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 OperatorTransferred is emitted. The recorded change time (operatorChangedAt) restarts the score’s maturity in SigvaraReputation, 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
Loading

Diagram sources

Section sources

Relationship with ERC-8004 Identity Standards and DID Methodology

SigvaraIdentity implements a deterministic DID method:

  • didHash = keccak256("did:sigvara:", block.chainid, ":", agentAddress)
  • Clients can compute didHash off-chain via computeDidHash.
  • 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"]
Loading

Diagram sources

Section sources

Dependency Analysis

SigvaraIdentity has minimal but critical dependencies:

  • OpenZeppelin upgradeable contracts for access control and proxy upgradeability.
  • TimelockedGrants and InflowGuard, 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
Loading

Diagram sources

Section sources

Performance Considerations

  • 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. operatorAgentCount gives the length to page against, and getOperatorAgentsPaged works out the remaining length instead of adding offset and limit, 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 computeDidHash and isActive are cheap and suitable for off-chain checks before submitting transactions.

Troubleshooting Guide

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 Active always requires collateral.

Section sources

Conclusion

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.

Appendices

Practical Workflows

Agent Registration Workflow

  1. Client computes registrationDigest(agentAddress, operator, ed25519PubKey).
  2. Agent address signs the digest.
  3. Operator calls registerAgent(agentAddress, ed25519PubKey, signature).
  4. Registry emits AgentRegistered and stores the agent in PendingBond.
  5. Operator deposits stake through the staking contract until hasMinimumStake returns true.
  6. The deposit that carries the bond to minimumStake activates the agent: SigvaraStaking calls updateStatus(didHash, Active) itself, so the operator does not have to.
  7. Registry verifies collateral and emits AgentStatusUpdated.

Section sources

Key Rotation Procedure

  1. Operator detects key compromise or wants to rotate.
  2. Operator calls rotatePublicKey(didHash, newEd25519PubKey).
  3. Registry validates operator and rejects slashed agents.
  4. Registry emits PublicKeyRotated.

Section sources

Operator Transfer Example

  1. Current operator calls offerOperatorTransfer(didHash, newOperator).
  2. New operator calls acceptOperatorTransfer(didHash).
  3. Registry validates slash state and collateral.
  4. Registry updates operator index and emits OperatorTransferred.

Section sources

Overview

Clone this wiki locally