Contract reference

The AIOUM Protocol v2 on-chain stack (src/), contract by contract: purpose, key functions, events, access control and constructor arguments, so you know exactly what a function checks before it moves money, which address may call it, and what a deploy script has to pass in.

Across the whole stack: the toolchain is Solidity 0.8.28, OpenZeppelin v5.1 and Foundry (the ERC-4337 lib requires ^0.8.28). Settlement assets are USDC / USDT / ETH, and address(0) is the ETH sentinel everywhere. ZAI is for staking and governance only, never a payment asset. Every Ownable protocol contract is owned by the DAO TimelockController (dao in constructor args), not an EOA; see SECURITY.md and ARCHITECTURE.md. The off-chain surface that drives these contracts is the SDK & HTTP API reference.

Contract index

ContractPathRole
AIOUMTokentoken/AIOUMToken.solStaking + governance ERC-20 (Votes/Permit/Capped/Burnable)
AIOUMGovernorgovernance/AIOUMGovernor.solToken-weighted DAO Governor → Timelock
StakingManagerstaking/StakingManager.solStake / delegate / slash / multi-asset rewards
IdentityRegistryregistry/IdentityRegistry.solERC-8004 identity
EndpointRegistryregistry/EndpointRegistry.solWhere each operator's node answers
OperatorSignersregistry/OperatorSigners.solThe hot key an operator names for its node
ReputationRegistryregistry/ReputationRegistry.solSettlement-gated feedback + penalties
ValidationRegistryregistry/ValidationRegistry.solStaked validation attestations
ServiceRegistryregistry/ServiceRegistry.solOn-chain service catalogue
PaymentManagerpayments/PaymentManager.solMetered sessions + EIP-712 vouchers + x402 funding
DisputeManagerdisputes/DisputeManager.solOptimistic challenge → slash + penalty + refund
ValidationQuorumArbitratordisputes/ValidationQuorumArbitrator.solStake-weighted quorum arbitrator
CommitRevealArbitratordisputes/CommitRevealArbitrator.solDedicated-stake commit-reveal jury
AIOUMAccountaa/AIOUMAccount.solERC-4337 smart account with budgeted session keys + ERC-1271
AIOUMAccountFactoryaa/AIOUMAccountFactory.solCREATE2 deterministic deployer for AIOUMAccount
AIOUMPaymasteraa/AIOUMPaymaster.solERC-4337 paymaster for the local development network — sponsors every UserOp
AIOUMVerifyingPaymasteraa/AIOUMVerifyingPaymaster.solERC-4337 paymaster — sponsors signature-approved UserOps

Interfaces live in src/interfaces/ (IIdentityRegistry, IPaymentManager, IReputationRegistry, IServiceRegistry, IStakingManager) and external standard interfaces in src/external/ (IPermit2, IERC3009). The account-abstraction contracts build on the canonical ERC-4337 library (@account-abstraction/contracts v0.9.0) and, on live chains, run against the canonical v0.7 EntryPoint.

One more fact behind every onlyOwner: after a deploy, the bootstrap deployer transfers ownership of the Ownable protocol contracts (StakingManager, ReputationRegistry, PaymentManager, DisputeManager, ValidationRegistry) to the DAO/Timelock via npm run handover (cwd offchain/, DAO_OWNER=0x<timelock>). It transfers ownership and never renounces it to 0x0, so every setter stays live under the DAO.


AIOUMToken

src/token/AIOUMToken.sol

Purpose. The network's staking and governance asset — not a payment asset. Used only to secure nodes (stake/slash) and to govern the DAO; all user payments settle in stablecoins/ETH via PaymentManager. Composes OZ ERC20, ERC20Burnable, ERC20Capped, ERC20Permit, ERC20Votes, and Ownable: Votes powers on-chain governance, Capped bounds governance inflation, Permit (EIP-2612) enables gasless approvals.

Constructor.

constructor(address treasury, address daoOwner, uint256 initialSupply, uint256 cap_)
  • treasury — receives the entire initialSupply mint (must be non-zero).
  • daoOwner — becomes owner() (intended: the TimelockController, never an EOA).
  • initialSupply — minted to treasury at deploy.
  • cap_ — hard supply cap (ERC20Capped); mint can never exceed it.
  • Fixed metadata: name "AIOUM AI Superchain", symbol "ZAI"; permit/votes domain name "AIOUM AI Superchain".

Key functions.

  • mint(address to, uint256 amount) — onlyOwner; governance-controlled emission (e.g. staking incentives), hard-bounded by cap().
  • Inherited: permit(...), delegate(...), delegateBySig(...), getVotes, nonces, burn / burnFrom, plus standard ERC-20.
  • Internal overrides _update and nonces reconcile the OZ v5 multiple-inheritance graph.

Access control. mint is onlyOwner (the DAO). Voting power must be delegated (including self-delegation) before it counts.


AIOUMGovernor

src/governance/AIOUMGovernor.sol

Purpose. Token-weighted DAO governor that executes every privileged protocol action through a TimelockController. Replaces the original single-EOA onlyOwner admin model: each protocol contract is owned by the Timelock, and only this Governor (or a guardian multisig configured as a Timelock proposer) can move it, and only after the timelock delay. Composes OZ Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl.

Constructor.

constructor(IVotes token_, TimelockController timelock_)
  • token_ — the voting token (AIOUMToken, as IVotes).
  • timelock_ — the executor/queue target.
  • Fixed governance settings: votingDelay 7200 blocks (~1d), votingPeriod 50400 blocks (~7d), proposalThreshold 100_000e18 ZAI, quorum 4% of supply.

Key functions. Standard OZ Governor surface: propose, castVote / castVoteWithReason / castVoteBySig, queue, execute, cancel, plus views state, quorum(timepoint), votingDelay, votingPeriod, proposalThreshold, proposalNeedsQueuing. Internal overrides (_queueOperations, _executeOperations, _cancel, _executor) route execution through the Timelock.

Access control. Proposing requires voting weight ≥ proposalThreshold. Execution is gated by quorum, the vote outcome, and the Timelock delay. The Timelock — not the Governor — holds the privileged roles on the protocol contracts.


StakingManager

src/staking/StakingManager.sol — implements IStakingManager.

Purpose. Node staking, delegation, slashing, and multi-asset reward accounting. Secures the network in the volatile, slashable ZAI token while paying rewards in the settlement assets users actually pay with (USDC/USDT/ETH). A node is keyed by its operator address (one staking position per operator). Delegations are tracked as shares of each node's delegated pool; slashing reduces the pool's backing assets while shares stay constant, diluting delegators pro-rata (delegated-PoS model). Reward accounting is MasterChef-style and credits accrual only after the asset is received, so accrued ≤ held.

Constructor.

constructor(
    IERC20 aioum_,
    address dao,
    uint256 minSelfStake_,
    uint256 minNodeStake_,
    uint256 tokensPerSession_,
    uint256 unbondingPeriod_,
    uint16 maxCommissionBps_
)
  • aioum_ — the ZAI token (must be non-zero); the only stakable asset.
  • dao — owner() (Timelock).
  • minSelfStake_ — the operator's own floor: enough to list (deploy default: 100 ZAI; must be ≤ minNodeStake_).
  • minNodeStake_ — the node's total floor, own + delegated, at which it is live (deploy default: 10,000 ZAI).
  • tokensPerSession_ — extra stake required per concurrent session (deploy default: 100 ZAI; must be > 0). A node may hold 1 + (total stake − minNodeStake) / tokensPerSession sessions at once: staked exactly at the floor, one; 20,000 ZAI, 101.
  • unbondingPeriod_ — cooldown before unstaked/undelegated ZAI can be withdrawn (deploy default: 7 days).
  • maxCommissionBps_ — cap on operator commission (deploy default: 2000 = 20%; must be ≤ 10000).

Key functions.

Governance (onlyOwner):

  • setRewarder(address r) — set the only address allowed to notifyReward (the PaymentManager).
  • setDisputeManager(address d) — set the only address allowed to slash.
  • setRewardAsset(address asset, bool allowed) — allow/disallow a settlement asset for rewards (adding appends to the iteration set once; disallowing only stops new notifications — accrued rewards stay claimable).
  • setParams(minSelfStake_, minNodeStake_, tokensPerSession_, unbondingPeriod_, maxCommissionBps_) — emits ParamsSet with all five; minSelfStake_ ≤ minNodeStake_.

Operator self-stake:

  • stake(uint256 amount) — pull amount ZAI in and increase self-stake (nonReentrant).
  • setCommission(uint16 bps) — set operator commission (≤ maxCommissionBps); node must exist.
  • initiateUnstake(uint256 amount) — start unbonding; remaining self-stake must be 0 or ≥ minSelfStake. It may take the node's total under minNodeStake: the node is then not live until backed again.
  • withdrawUnstaked() — withdraw matured unbonding entries.

Delegation:

  • delegate(address operator, uint256 amount) — delegate ZAI to a node; mints pool shares.
  • initiateUndelegate(address operator, uint256 amount) — burn shares for the requested asset amount (floored) and start unbonding. Never blocked by the floors: if the exit drops the node's total under minNodeStake it stops being live until backed again.
  • withdrawUndelegated(address operator) — withdraw matured undelegation entries.

Rewards:

  • notifyReward(address operator, address asset, uint256 amount) — payable; rewarder-only. Splits the reward: operator base (by self-stake share) + commission to the operator's operatorClaimable, the rest accrued per delegator share. For ETH, msg.value == amount; for ERC-20, the rewarder must transfer the asset in immediately before calling.
  • claimDelegatorRewards(address operator, address asset) / claimOperatorRewards(address asset).

Slashing:

  • slash(address operator, uint256 amount, address beneficiary) — DisputeManager-only (nonReentrant). Slashes pro-rata across self-stake and the delegated pool, takes any shortfall from the operator's pending self-unbonding queue (newest first), and transfers what it recovered to beneficiary. It never reverts: with nothing left to take it emits a zero-value Slashed and returns, so an upheld dispute always lands its refund and penalty. Delegators' own pending undelegations are not reached.

Views: totalStakeOf, selfStakeOf, isOperator (self-stake ≥ minSelfStake: may list), isActiveNode (live: isOperator and total stake ≥ minNodeStake), maxConcurrent (0 unless live, else 1 + surplus-stake / tokensPerSession), pendingDelegatorReward, delegatedAssetsOf, rewardAssetCount, plus public mappings nodes, delegatorShares, operatorClaimable, isRewardAsset.

Events. RewarderSet, DisputeManagerSet, RewardAssetSet, ParamsSet, Staked, UnstakeInitiated, UnstakeWithdrawn, CommissionSet, Delegated, UndelegateInitiated, UndelegatedWithdrawn, RewardNotified, DelegatorRewardClaimed, OperatorRewardClaimed, Slashed, DelegationSlashed.

Access control. Governance setters onlyOwner; notifyReward restricted to rewarder; slash restricted to disputeManager. nonReentrant on all asset-moving paths; SafeERC20 throughout.


IdentityRegistry

src/registry/IdentityRegistry.sol — implements IIdentityRegistry.

Purpose. ERC-8004-style identity for agents, nodes, and clients. Permissionless: any address self-registers exactly one portable, on-chain-resolvable identity (metadataURI points at an agent card — capabilities/endpoints). Discovery indexers (the subgraph) key off Registered.

Constructor. None (no constructor args; no ownership).

Key functions.

  • register(string metadataURI) returns (uint256 id) — self-register one identity (reverts if the caller already has one). Ids start at 1.
  • updateMetadata(string metadataURI) — caller updates their own agent card.
  • transferIdentity(address to) — move the identity to a new controlling address (key rotation / AA upgrade); to must be non-zero and unregistered.
  • isRegistered(address account) returns (bool) — view.
  • Public mappings: agents(uint256), agentIdOf(address), nextId.

Events. Registered, MetadataUpdated, OwnershipTransferred.

Access control. Fully permissionless; each mutator acts only on msg.sender's own identity.


EndpointRegistry

src/registry/EndpointRegistry.sol.

Purpose. Where an operator's node answers: one URL per operator and the fingerprint of the certificate the node serves, written by that operator and by nobody else, so a directory reads node addresses from the chain instead of a list someone keeps, and a client accepts the node's certificate by the fingerprint alone, with no domain and no certificate authority. A record maps only to its operator's own listings.

Constructor. None.

Key functions.

  • setEndpoint(string url, bytes32 certHash) — the caller's URL, 10 to 255 bytes, starting http:// or https://, and the SHA-256 fingerprint of the certificate the node serves (zero for a CA-issued certificate on a name, or plain http); the first write enumerates the caller.
  • clearEndpoint() — the caller's record emptied; the caller stays enumerable.
  • endpointOf(address operator) returns (string url, bytes32 certHash, uint64 updatedAt) — view; empty when none.
  • operatorCount(), operators(uint256 offset, uint256 limit) — every address that ever wrote, paged; an overflowing page is clamped.

Events. EndpointSet(operator, url, certHash), EndpointCleared(operator).

  • setEndpointFor(address operator, string url, bytes32 certHash), clearEndpointFor(address operator) — the same, by the operator or the signer it named (OperatorSigners).

Access control. Fully permissionless; each mutator acts only on msg.sender's own record, or on the record of the operator that named msg.sender its signer.


OperatorSigners

src/registry/OperatorSigners.sol.

Purpose. The hot key a node runs with, named by the operator's cold key. The signer settles sessions (open to anyone with the voucher), writes the node's address and certificate and proves the node's identity off chain; it can do nothing with the stake, the listings or withdrawals.

Key functions. setSigner(address signer) (one per operator, a signer serves one operator, address(0) clears), signerOf(address operator), operatorOf(address signer), isOperatorOrSigner(address operator, address who).

Events. SignerSet(operator, signer), SignerCleared(operator, signer).

Access control. Fully permissionless; each operator names its own signer.


ReputationRegistry

src/registry/ReputationRegistry.sol — implements IReputationRegistry, is Ownable.

Purpose. ERC-8004-style, settlement-gated feedback plus dispute penalties. Feedback is Sybil-resistant: a client may rate a node only after paying it; the PaymentManager grants one feedback allowance per settled session. In the app the agent itself spends it: after every settled session it rates the operator on-chain, 0–100, scored down from the errors and slow answers it met, so ratings are issued by agents from real results, not typed in by people. Penalties come only from the DisputeManager after an upheld dispute.

Constructor.

constructor(address dao)
  • dao — owner() (Timelock), which wires the privileged caller addresses.

Key functions.

  • setPaymentManager(address pm) / setDisputeManager(address dm) — onlyOwner.
  • recordSettlement(address client, address operator, uint256 volume) — PaymentManager-only; grants client one feedback allowance for operator and adds to operator's settledVolume.
  • submitFeedback(address operator, uint8 score, string uri) — leave one score (0..100) for a node you have paid; consumes one allowance. The app's agent runner calls this after each settled session with the score it computed from that run and aioum:run:<runId> as the uri, so the rating can be traced back to the run that produced it.
  • penalize(address operator, uint256 weight, string reasonURI) — DisputeManager-only; increments the operator's penalty count.
  • averageScore(address operator) returns (uint256) — average 0..100, or 0 if unrated.
  • Public mappings: reputation(address), feedbackAllowance(client, operator).

Events. PaymentManagerSet, DisputeManagerSet, SettlementRecorded, FeedbackSubmitted, Penalized.

Access control. Setters onlyOwner; recordSettlement restricted to paymentManager; penalize restricted to disputeManager; submitFeedback gated by an earned allowance.


ValidationRegistry

src/registry/ValidationRegistry.sol — is Ownable.

Purpose. ERC-8004-style optimistic validation of job results. Anyone can request independent validation of a node's result (referenced by a content hash); staked validators attest yes/no with stake-weighted votes. The tally feeds the DisputeManager / off-chain reputation — the "verifiable compute" hook used where re-execution or zkTLS proof is available; the base trust model remains crypto-economic.

Constructor.

constructor(IStakingManager staking_, uint256 minValidatorStake_, address dao)
  • staking_ — source of validator self-stake weights.
  • minValidatorStake_ — minimum self-stake to be an eligible validator.
  • dao — owner() (Timelock), which may retune the validator-stake bar.

Key functions.

  • setMinValidatorStake(uint256 v) — onlyOwner; the DAO tunes the validator-stake bar.
  • requestValidation(address operator, bytes32 dataHash) returns (uint256 id) — open a request over a hash of (input, output, model/service descriptor). Ids start at 1.
  • respond(uint256 id, bool ok) — staked validators attest; one vote per validator, weighted by staking.selfStakeOf(msg.sender) (which must be ≥ minValidatorStake).
  • Public mappings: requests(uint256), hasResponded(id, validator), nextId, minValidatorStake.

Events. ValidationRequested, ValidationResponded, MinValidatorStakeSet.

Access control. setMinValidatorStake is onlyOwner (the DAO); requests are permissionless; responding requires ≥ minValidatorStake self-stake. staking is immutable.


ServiceRegistry

src/registry/ServiceRegistry.sol — implements IServiceRegistry.

Purpose. On-chain catalogue of agent services (replaces the original NodeRegistration). A node lists the services it runs (an MCP endpoint, a resold API, a model). Listing requires the operator's own stake floor (isOperator); a listing is live once the node's total stake, own + delegated, reaches minNodeStake. Capacity is enforced downstream by the PaymentManager against StakingManager.maxConcurrent, not a node-settable counter. Discovery uses EnumerableSet with pagination, avoiding the original's unbounded-loop / div-by-zero DoS. The live marketplace an agent searches mid-run is indexed from this catalogue.

Constructor.

constructor(IStakingManager staking_)
  • staking_ — used to check isOperator at registration and isActiveNode for isServiceLive at query time.

Key functions.

  • registerService(string category, string descriptorURI, uint256 pricePerCall, address[] acceptedAssets) returns (uint256 serviceId) — caller must be an operator (own stake ≥ minSelfStake) and supply ≥ 1 accepted asset. Ids start at 1.
  • updateService(uint256 serviceId, string descriptorURI, uint256 pricePerCall, bool active) — onlyServiceOperator.
  • setAcceptedAsset(uint256 serviceId, address asset, bool ok) — onlyServiceOperator.
  • setCommitRequired(uint256 serviceId, bool required) — onlyServiceOperator; declares (or withdraws) the results-root promise for a service. A flagged service anchors a results root (PaymentManager.commitResults) for every paid session, and the PaymentManager refuses to settle a paid session without one. Toggling affects future sessions only: the flag is snapshotted when a session opens. See the evidence format.
  • Views: getService, isAcceptedAsset, isCommitRequired, isServiceLive (active AND node live: own floor met, total stake ≥ minNodeStake), categoryCount, servicesByCategory(category, offset, limit) (paginated), servicesByOperator(operator), public acceptedAssetList(serviceId, i), nextServiceId.

Events. ServiceRegistered, ServiceUpdated, AcceptedAssetSet, CommitRequiredSet.

Access control. Registration is permissionless but gated on the operator's own stake floor; mutators are restricted to the service's operator via the onlyServiceOperator modifier. No owner.


PaymentManager

src/payments/PaymentManager.sol — implements IPaymentManager, is Ownable, ReentrancyGuard, EIP712.

Purpose. Metered, multi-asset agent-service payments (x402 model). A client opens a prepaid session for a service in USDC/USDT/ETH, then pays per call off-chain by signing monotonic EIP-712 vouchers; the node redeems the latest voucher on-chain to settle and the unspent remainder is refunded. A session can only exist after funds are actually pulled in (ETH / approval / Permit2 / EIP-3009), and the per-call amount is authorized by the client's EIP-712 signature, domain- separated by chainId and this contract so it cannot be forged or replayed. CEI + ReentrancyGuard

  • SafeERC20 throughout.

EIP-712 voucher. Domain name "AIOUMPayments", version "1"; typehash PaymentVoucher(uint256 sessionId,uint256 cumulativeAmount). Off-chain signing mirrors this exactly — see signVoucher.

Constructor.

constructor(
    IServiceRegistry services_,
    IStakingManager staking_,
    IReputationRegistry reputation_,
    IPermit2 permit2_,
    address dao,
    address treasury_,
    uint16 protocolFeeBps_,
    uint16 delegatorShareBps_,
    uint64 inactivityTimeout_
)
  • services_, staking_, reputation_, permit2_ — wired protocol dependencies (immutable).
  • dao — owner() (Timelock).
  • treasury_ — protocol-fee sink (must be non-zero).
  • protocolFeeBps_ — gross share to treasury (deploy default: 500 = 5%).
  • delegatorShareBps_ — gross share to the node's stakers (deploy default: 1000 = 10%); protocolFeeBps_ + delegatorShareBps_ must be ≤ 10000.
  • inactivityTimeout_ — idle period after which a client may reclaim (deploy default: 1 day).

Key functions.

Governance (onlyOwner): setTreasury, setDisputeManager, setFees, setInactivityTimeout, setSettleChallengeWindow(uint64 w), setEscrowCapPerStake(address asset, uint256 capPerStake). The protocolFeeBps is global, settable only by governance (setFees, onlyOwner); the global delegatorShareBps is the default share. settleChallengeWindow is how long a settled session stays challengeable after settlement (0 = final at settle); each session's own deadline is snapshotted into challengeableUntil[sessionId] at settle time, so a later retune never reopens or shortens an existing settlement. escrowCapPerStake[asset] bounds an operator's total open escrow per asset in proportion to their stake (units of asset per 1e18 staked ZAI; 0 disables the cap for that asset); escrowCapacityOf(operator, asset) reports the resulting ceiling and openEscrowOf(operator, asset) the current usage.

Per-service split (onlyServiceOperator):

  • setServiceSplit(uint256 serviceId, uint16 delegatorShareBps) — the service operator overrides the delegator share for their own service (the protocol fee is unaffected). Sets serviceSplitSet[serviceId] = true and records serviceDelegatorShareBps[serviceId]; settlement then uses this share in place of the global default.

Open a session (each nonReentrant, returns sessionId):

  • openSessionETH(uint256 serviceId) — payable; deposit is msg.value.
  • openSession(uint256 serviceId, address asset, uint256 amount) — prior ERC-20 approval (any token incl. USDT).
  • openSessionWithPermit2(uint256 serviceId, address asset, uint256 amount, address owner, IPermit2.PermitTransferFrom permit, bytes signature) — pull any ERC-20 via Permit2 (relayer-submittable on owner's behalf).
  • openSessionWithAuthorization(uint256 serviceId, address asset, uint256 amount, address from, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) — EIP-3009 receiveWithAuthorization (USDC, gasless; relayer-submittable, funds move from from). This is the function the facilitator relays.

_open validates the service is active, the asset is accepted, the node is active, and the node is below maxConcurrent capacity before recording the Session.

Settle / reclaim:

  • settle(uint256 sessionId, uint256 cumulativeAmount, bytes signature) — nonReentrant; the operator submits the highest client-signed voucher. Requires status == Open, s.spent <= cumulativeAmount <= s.deposited, and a valid client signature. Marks the session Settled and distributes funds. The signature is checked with OpenZeppelin SignatureChecker.isValidSignatureNow(client, digest, signature), so an EOA client is verified via ECDSA and a smart-account (ERC-4337) client via ERC-1271 — one settle path serves both, and the EOA flow is unchanged. See AIOUMAccount.
  • settleWithAttribution(uint256 sessionId, uint256 cumulativeAmount, bytes signature, address[] payees, uint16[] shareBps) — operator-only; settles exactly like settle, then optionally splits a fraction of the operator's net (post protocol-fee and delegator-share) among declared sub-providers. payees and shareBps must be equal-length and sum(shareBps) <= 10000 (≤ 100% of net); each contributor is paid in the session asset and recorded on-chain via ContributionPaid. Opt-in and node-controlled — the seed of value chains (a node credits whatever it built on). Plain settle is unchanged.
  • commitResults(uint256 sessionId, bytes32 resultsRoot) — operator-only. Records a root over the calls the node served for the session, each leaf keccak(index, requestHash, responseHash, toolIdHash) over the call's request, response and tool id, a cheap on-chain anchor so later verification or a dispute (re-execution / TEE / zkML) can point at exact bytes. Stored in sessionResultsRoot[sessionId]; one tx per session.
  • reclaim(uint256 sessionId) — nonReentrant; the client reclaims the unspent deposit after lastActivity + inactivityTimeout (liveness backstop). Marks Refunded.

_distribute splits gross into protocolFee (→ treasury), delegatorShare (→ StakingManager.notifyReward, falling back to treasury if the asset is not an allowed reward asset), operatorNet (→ operator), plus any refund (→ client), then calls reputation.recordSettlement. The delegator share is the service's own serviceDelegatorShareBps when serviceSplitSet[serviceId] is true, otherwise the global delegatorShareBps; operatorNet = gross - protocolFee - delegatorShare.

Disputes (DisputeManager-only): markDisputed(uint256 sessionId) freezes settlement; resolveDispute(uint256 sessionId, bool refundClient) either refunds the client (Refunded) or reopens the session for the normal settle path (Open).

Views: getSession, activeSessionCount(operator), voucherDigest(sessionId, cumulativeAmount) (helper to obtain the digest a client signs), plus public mappings serviceSplitSet(serviceId), serviceDelegatorShareBps(serviceId) (the per-service override state), and sessionResultsRoot(sessionId) (the operator-committed results root, if any), sessionCommitRequired(sessionId) (the service's commitRequired flag snapshotted at open), settledAt(sessionId) and challengeableUntil(sessionId).

Events. TreasurySet, DisputeManagerSet, FeesSet, InactivityTimeoutSet, SettleChallengeWindowSet, EscrowCapSet, ServiceSplitSet, SessionOpened, SessionSettled, SessionReclaimed, SessionClosedByOperator (the operator closed an idle session with operatorClose(sessionId) after lastActivity + inactivityTimeout, refunding the client's unspent deposit), SessionDisputed, DisputeResolved, ResultsCommitted, ContributionPaid (one per attributed payee in settleWithAttribution), RefundEscrowed and Withdrawn (a client refund whose push failed is credited to withdrawable[client][asset] instead of reverting, and the client later pulls it with withdraw(asset)).

Access control. Governance setters onlyOwner; setServiceSplit onlyServiceOperator; settle operator-only; reclaim client-only; markDisputed / resolveDispute restricted to disputeManager. nonReentrant on every asset-moving path.


DisputeManager

src/disputes/DisputeManager.sol — is Ownable, ReentrancyGuard.

Purpose. Optimistic challenge layer for service non-delivery / fraud. Because session amounts are client-signed, payment amounts cannot be forged; disputes are about a node taking payment without honestly delivering the service. A client bonds and challenges; the arbitrator — the DAO multisig/timelock, a ValidationQuorumArbitrator, or the CommitRevealArbitrator jury — rules. If upheld, the node is slashed, its reputation penalized, the client refunded, and the bond returned; if rejected, the bond compensates the node.

Constructor.

constructor(
    IPaymentManager payment_,
    IStakingManager staking_,
    IReputationRegistry reputation_,
    address dao,
    address arbitrator_,
    address treasury_,
    IERC20 bondToken_,
    uint256 bondAmount_,
    uint256 slashAmount_,
    uint256 penaltyWeight_,
    uint64 disputeTimeout_
)
  • payment_, staking_, reputation_ — wired protocol dependencies (immutable).
  • dao — owner() (Timelock).
  • arbitrator_ — rules on disputes (must be non-zero; swappable via setArbitrator).
  • treasury_ — receives slashed stake (must be non-zero).
  • bondToken_ — dispute-bond asset.
  • bondAmount_ — bond required to raise a dispute.
  • slashAmount_ — the fixed ZAI floor slashed from the node on an upheld dispute (see setSlashBps).
  • penaltyWeight_ — reputation penalty weight on an upheld dispute.
  • disputeTimeout_ — the window after raisedAt past which resolveStale may force-close a stalled dispute (deploy default: 3 days); updatable via setDisputeTimeout (onlyOwner).

Key functions.

  • setArbitrator(address a) / setParams(IERC20 bondToken_, uint256 bondAmount_, uint256 slashAmount_, uint256 penaltyWeight_, address treasury_) / setDisputeTimeout(uint64 timeout) / setSlashBps(uint16 bps) — onlyOwner. slashBps adds an exposure-proportional component: an upheld dispute slashes max(slashAmount, stakeAtRaise * slashBps / 10000), where stakeAtRaise is the operator's stake snapshotted when the dispute was raised, so unstaking mid-case cannot shrink the penalty.
  • raiseDispute(uint256 sessionId, string evidenceURI) returns (uint256 disputeId) — nonReentrant; the session's client challenges an open session, or a settled one that is still inside its settle challenge window (PaymentManager.isSettleChallengeable), pulls in bondAmount, records raisedAt and the operator's stakeAtRaise, and for an open session calls payment.markDisputed to freeze settlement. A settled session's payout is already final, so it is flagged postSettlement instead: if upheld, the client is made whole by a stake slash paid 100% to them rather than an escrow refund. Ids start at 1. The current bondToken and bondAmount are snapshotted into the Dispute struct at raise time: refunds always pay out the exact token/amount that was escrowed, even if governance swaps the bond token via setParams while the dispute is open.
  • resolve(uint256 disputeId, bool nodeAtFault) — onlyArbitrator, nonReentrant. If nodeAtFault: for an open-session dispute, refund the client via payment.resolveDispute(_, true), then slash the operator for max(slashAmount, stakeAtRaise * slashBps / 10000) into the DisputeManager and split what was actually recovered — challengerRewardBps (default 2000 = 20%, setChallengerReward, onlyOwner) goes to the client as ChallengerRewarded, the rest to treasury. For a post-settlement dispute there is no escrow refund; the client receives the whole recovered slash instead. Either way, reputation.penalize the operator and refund the bond to the client. Otherwise: reopen the session via payment.resolveDispute(_, false) and award the bond to the operator. Bond payouts use the per-dispute token snapshot, not the current bondToken.
  • resolveStale(uint256 disputeId) — nonReentrant; anyone may force-close a dispute the arbitrator never ruled on, once block.timestamp > raisedAt + disputeTimeout (strictly greater; deploy default 3 days). Refunds the client's unspent deposit when the disputed session was still open (a post-settlement dispute has no escrow to unwind) and returns the bond (via the snapshotted token) to the client — no slash, no penalty — and marks the dispute Status.TimedOut. This is the dispute-timeout liveness backstop, so a stalled dispute can't freeze funds indefinitely.
  • disputeStatus(uint256 disputeId) returns (Status status, uint64 raisedAt) — lightweight view for external arbitrators (e.g. the validation quorum) to read a dispute's status and raise timestamp without decoding the full struct.
  • Public mappings: disputes(uint256), sessionDisputed(uint256), nextDisputeId, plus disputeTimeout (the stale window). Each Dispute records its raisedAt timestamp, the snapshotted bond token/amount, and a Status (Open / resolved / TimedOut).

Events. ArbitratorSet, ParamsSet, DisputeTimeoutSet, ChallengerRewardSet, SlashBpsSet, DisputeRaised, DisputeResolved, ChallengerRewarded (emitted on an upheld dispute when challengerRewardBps > 0: the client's share of the recovered slash, with the remainder going to treasury). Views also include disputeParties(uint256 disputeId) returns (address client, address operator), which external arbitrators use to keep the two parties from voting on their own case; setChallengerReward(uint16 bps) is onlyOwner alongside the other setters.

Access control. Setters onlyOwner; raiseDispute restricted to the session client; resolve restricted to arbitrator via the onlyArbitrator modifier; resolveStale is permissionless but only after the disputeTimeout window.


ValidationQuorumArbitrator

src/disputes/ValidationQuorumArbitrator.sol — is Ownable.

Purpose. A stake-weighted validator-quorum arbitrator — a drop-in replacement for the DAO multisig arbitrator on the DisputeManager. Set as the DisputeManager's arbitrator (via setArbitrator), it lets staked validators (self-stake ≥ minValidatorStake) vote on whether the node was at fault, each vote weighted by self-stake. Once cumulative voted weight reaches quorum and one side holds a strict majority, it forwards the decision to IDisputeResolver(disputeManager).resolve(disputeId, decision) exactly once.

Constructor.

constructor(
    address dao,
    IStakingManager staking_,
    address disputeManager_,
    uint256 quorum_,
    uint256 minValidatorStake_
)
  • dao — owner() (the DAO).
  • staking_ — source of validator self-stake weights (must be non-zero).
  • disputeManager_ — the DisputeManager to drive (may be zero and set later).
  • quorum_ — total stake weight required before a dispute can resolve (must be > 0).
  • minValidatorStake_ — minimum self-stake to be an eligible validator.

Key functions.

  • setDisputeManager(address) / setQuorum(uint256) / setMinValidatorStake(uint256) — onlyOwner.
  • vote(uint256 disputeId, bool nodeAtFault) — cast a stake-weighted vote (weight = staking.selfStakeOf(msg.sender), ≥ minValidatorStake); one vote per validator; rejected once resolved. On reaching quorum with a strict majority, calls disputeManager.resolve once. Additional requirements:
    • The dispute must be Open on the DisputeManager, checked via its disputeStatus(uint256) view ("dispute not open"). This blocks pre-stacking votes on dispute ids that haven't been raised yet, and voting after the DisputeManager already closed the dispute via resolve/resolveStale.
    • When the DisputeManager's disputeTimeout is non-zero, votes after raisedAt + disputeTimeout revert with "vote window closed". The boundary is inclusive: a vote at exactly raisedAt + disputeTimeout is accepted — complementing resolveStale, which only fires strictly after that timestamp.
    • The dispute's own client and operator are recused: vote() reads disputeParties(disputeId) and reverts with "party recused" for either of them.
    • The configured DisputeManager must expose disputeStatus(uint256), disputeTimeout() and disputeParties(uint256). Pointing the arbitrator (via setDisputeManager) at a contract without them makes every vote() revert with empty revert data — fail-closed, and recoverable by the owner calling setDisputeManager again.
  • tallyOf(uint256 disputeId) returns (uint256 forFault, uint256 againstFault, bool resolved) — view. Public mappings: tallies(uint256), hasVoted(disputeId, validator).

Events. DisputeManagerSet, QuorumSet, MinValidatorStakeSet, MinVotingPeriodSet, ValidatorRewardSet, Voted, Resolved, ValidatorRewardClaimed. Two more knobs sit on the same contract: setMinVotingPeriod(uint256) (onlyOwner, default 0, must stay below the DisputeManager's disputeTimeout) holds resolution until raisedAt + minVotingPeriod even once quorum and a majority hold, and the permissionless finalize(disputeId) forwards a decision that was waiting out that floor; setValidatorReward(IERC20 token, uint256 perResolution) configures a per-resolution pot, backed by token the DAO sends to the contract, that winning-side voters split stake-weighted with claimValidatorReward(disputeId).

Access control. Setters onlyOwner; voting requires ≥ minValidatorStake self-stake. A tie at quorum does not resolve (waits for a tie-breaking vote).


CommitRevealArbitrator

src/disputes/CommitRevealArbitrator.sol — is Ownable, ReentrancyGuard.

Purpose. A dedicated-stake, commit-reveal jury for disputes — the successor to the ValidationQuorumArbitrator (design: arbitration.md). Arbitrators stake ZAI into this contract, separate from service-operator stake, so adjudication is an opt-in role with its own capital, rewards and penalties. Votes are submitted as salted hashes during the commit window and disclosed in the reveal window, so there is no running tally to herd on; vote weight is snapshotted at commit time and stake stays locked until the reveal deadline. A revealed vote that misses quorum or ties does not fall through to a no-fault timeout: the DAO rules it via escalate after the reveal deadline, and resolveStale on the DisputeManager remains only the final liveness valve. npm run deploy installs it as the DisputeManager's arbitrator when USE_COMMIT_REVEAL_ARBITRATOR=1.

Constructor.

constructor(
    address dao,
    IERC20 zai_,
    address disputeManager_,
    address treasury_,
    uint256 quorum_,
    uint256 minArbitratorStake_,
    uint64 commitPeriod_,
    uint64 revealPeriod_,
    uint256 unbondingPeriod_
)
  • dao — owner(); rules escalated cases.
  • zai_ — the arbitrator staking token (must be non-zero).
  • disputeManager_ — the DisputeManager to drive (may be set later).
  • treasury_ — receives no-reveal penalties (must be non-zero).
  • quorum_ — revealed weight required to resolve (must be > 0).
  • minArbitratorStake_ — minimum stake to commit a vote.
  • commitPeriod_ / revealPeriod_ — the two voting windows; the DisputeManager's disputeTimeout must exceed their sum (exposed as minVotingPeriod()).
  • unbondingPeriod_ — cooldown before unstaked arbitrator ZAI can be withdrawn.
  • noRevealPenaltyBps starts at 2000 (not revealing forfeits 20% of the committed weight).

Key functions.

  • Governance (onlyOwner): setDisputeManager, setTreasury, setQuorum, setMinArbitratorStake, setPeriods, setUnbondingPeriod, setNoRevealPenalty, setArbitratorReward(IERC20 token, uint256 perResolution).
  • Staking: stake(uint256 amount), initiateUnstake(uint256 amount), withdrawUnstaked().
  • Voting: commitmentOf(disputeId, arbitrator, nodeAtFault, salt) (the hash to commit), commit(uint256 disputeId, bytes32 commitment), reveal(uint256 disputeId, bool nodeAtFault, bytes32 salt).
  • finalize(uint256 disputeId) — permissionless after the reveal deadline; requires the dispute to be open on the DisputeManager, revealed weight ≥ quorum and no tie, then forwards the majority decision to disputeManager.resolve once and credits the winning side's reward pot.
  • escalate(uint256 disputeId, bool nodeAtFault) — onlyOwner; the DAO backstop for a vote that fell below quorum or tied, usable only after the reveal deadline.
  • slashNoReveal(uint256 disputeId, address arbitrator) — permissionless; an arbitrator that committed but never revealed loses noRevealPenaltyBps of its committed weight to the treasury (reaches the unbonding queue too).
  • claimArbitratorReward(uint256 disputeId) — winning-side revealers claim their stake-weighted share of the per-resolution reward.
  • Views: caseOf(uint256 disputeId), minVotingPeriod().

Events. DisputeManagerSet, TreasurySet, QuorumSet, MinArbitratorStakeSet, PeriodsSet, UnbondingPeriodSet, NoRevealPenaltySet, ArbitratorRewardSet, ArbitratorStaked, ArbitratorUnstakeInitiated, ArbitratorUnstakeWithdrawn, Committed, Revealed, Resolved, NoRevealPenalized, ArbitratorRewardClaimed.

Access control. Setters and escalate are onlyOwner; committing requires ≥ minArbitratorStake; finalize and slashNoReveal are permissionless once the reveal deadline has passed. Voting with the losing side is never slashed — only committing without revealing is penalized.


AIOUMAccount

src/aa/AIOUMAccount.sol — is BaseAccount (ERC-4337), IERC1271.

Purpose. An ERC-4337 smart account with budgeted, expiring session keys. The owner authorizes a session key once (a spend cap + expiry); that key then opens prepaid sessions and signs per-call PaymentVouchers autonomously, with the budget enforced on-chain — "set a budget once, the agent pays per call, no human in the loop." Vouchers validate via ERC-1271, so PaymentManager settle accepts this contract as a paying client unchanged. See the account-abstraction guide.

Constructor.

constructor(IEntryPoint ep, address owner_, address usdc_, address payment_)
  • ep — the canonical ERC-4337 EntryPoint (the v0.7 singleton on live chains).
  • owner_ — the account owner (may call anything; authorizes/revokes session keys).
  • usdc_ — the asset session-key budgets are denominated and funded in; the constructor pre-approves payment_ to pull funding from this account.
  • payment_ — the PaymentManager; the only contract a session key may spend at.

Key functions.

  • authorizeSessionKey(address key, uint48 validUntil, uint256 spendCap) — owner-or-self; scopes a key with a total USDC spendCap and expiry (validUntil). Re-authorizing tops up; a new cap may not be below what the key has already spent.
  • revokeSessionKey(address key) — owner-or-self; disables a key immediately.
  • openSessionFor(uint256 serviceId, uint256 amount) returns (uint256) — opens a prepaid session funded from this account; only reachable through the EntryPoint (a validated UserOp), and the only call a session key may make.
  • isValidSignature(bytes32 hash, bytes signature) returns (bytes4) — ERC-1271; a voucher is valid if it carries the owner's bare 65-byte signature, or an enriched signature abi.encode(sessionId, cumulativeAmount, innerSig) whose inner signer is the session key that opened that session (openedBy[sessionId], set inside openSessionFor). A key's authority is judged when it opens the session, so revoking or expiring it later does not invalidate vouchers for sessions it already opened, and a key that did not open the session cannot sign for it.
  • Internal _validateSignature (ERC-4337) enforces the budget before a session-key UserOp runs: the op must target openSessionFor and spent + amount must stay within spendCap; the running spent is incremented on-chain, and the EntryPoint enforces the validUntil window.
  • Public mapping: sessionKeys(address) → (uint48 validUntil, uint256 spendCap, uint256 spent, bool revoked).

Events. SessionKeyAuthorized, SessionKeyRevoked.

Access control. authorizeSessionKey / revokeSessionKey are owner-or-self; openSessionFor is EntryPoint-only; UserOps are owner-signed (full access) or session-key-signed (scoped to openSessionFor within budget). No Ownable (owner is a plain field set at construction).


AIOUMAccountFactory

src/aa/AIOUMAccountFactory.sol

Purpose. Deterministic CREATE2 deployer for AIOUMAccount. An account's address is a pure function of (owner, salt), so it is known before deploy — which is what lets the ERC-4337 initCode flow deploy the sender account on its first UserOp.

Constructor.

constructor(IEntryPoint ep, address usdc_, address payment_)
  • ep, usdc_, payment_ — forwarded into every account it deploys (all immutable).

Key functions.

  • createAccount(address owner, uint256 salt) returns (AIOUMAccount) — deploy (or return the existing) account for (owner, salt) via CREATE2.
  • getAddress(address owner, uint256 salt) returns (address) — the counterfactual address, computed off the creation code.

Access control. Permissionless; deployment is idempotent per (owner, salt). No owner.


AIOUMPaymaster

src/aa/AIOUMPaymaster.sol — is BasePaymaster (ERC-4337), Ownable.

Purpose. The open gas sponsor of the local development network: sponsors every UserOp so agents never need to hold the chain's gas token — pairs with the USDC-only payment model. Funded by its owner via deposit() into the EntryPoint. npm run deploy deploys and funds it on the local development network; live chains deploy the signature-gated AIOUMVerifyingPaymaster instead.

Constructor.

constructor(IEntryPoint ep, address owner_)
  • ep — the EntryPoint; owner_ — funds and manages the paymaster's deposit/stake.

Key functions. Inherited BasePaymaster surface (deposit, withdrawTo, addStake, unlockStake, withdrawStake). deposit is open to anyone — it forwards msg.value into the EntryPoint, the same thing npm run deploy and npm run paymaster -- refill do directly through EntryPoint.depositTo; withdrawing and stake management are onlyOwner. _validatePaymasterUserOp accepts (sponsors) every op, which is why this contract is only funded on the local development network; verify-deploy fails closed on a funded accept-everything paymaster on a live chain.

Access control. Withdrawals and stake management are onlyOwner (deposit is open); validation is invoked only by the EntryPoint.


AIOUMVerifyingPaymaster

src/aa/AIOUMVerifyingPaymaster.sol — is BasePaymaster (ERC-4337), Ownable.

Purpose. The signature-gated gas sponsor live chains run. It sponsors only UserOps that carry a valid, time-boxed sponsorship signature from sponsorSigner — the key of the off-chain sponsorship service (the managed billing runtime, npm run billing). Anyone can submit ops, but without a sponsorship signature the op fails validation and the deposit is untouchable. npm run deploy ships it unfunded on a live chain; fund it deliberately with npm run paymaster -- refill.

Constructor.

constructor(IEntryPoint entryPoint_, address owner_, address sponsorSigner_)
  • entryPoint_ — the EntryPoint; owner_ — funds and manages the deposit/stake and rotates the signer.
  • sponsorSigner_ — the sponsorship service's signing key (must be non-zero).

Key functions.

  • setSponsorSigner(address signer) — onlyOwner; rotation instantly invalidates every outstanding sponsorship.
  • getHash(PackedUserOperation userOp, uint48 validUntil, uint48 validAfter) returns (bytes32) — the digest the sponsorship service signs (eth_sign style). It binds the op's sender, nonce, initCode, callData and gas fields, the paymaster gas limits, this chain and this paymaster, so a sponsorship cannot be replayed for a different op, gas budget, chain or paymaster instance.
  • _validatePaymasterUserOp parses validUntil / validAfter and the sponsor signature from the paymasterAndData tail, recovers the signer and returns SIG_VALIDATION_FAILED (never reverts) on a wrong signature; the EntryPoint enforces the validity window.
  • Inherited BasePaymaster surface (deposit, withdrawTo, addStake, unlockStake, withdrawStake); deposit is open to anyone, the rest are onlyOwner.

paymasterAndData layout (ERC-4337 packed ops): [0:20] paymaster address, [20:36] validation gas limit, [36:52] postOp gas limit, [52:58] validUntil (0 = no expiry), [58:64] validAfter, [64:] sponsor ECDSA signature over getHash(userOp, validUntil, validAfter).

Events. SponsorSignerSet.

Access control. setSponsorSigner, withdrawals and stake management are onlyOwner (deposit is open); validation is invoked only by the EntryPoint.


Every contract a person deploying an agent ever touches, and none by hand: the app opens sessions and signs vouchers on their behalf, the node they paid settles, and the agent rates the operator afterwards, all within the budget set once. Two values point off-chain: PaymentManager.commitResults anchors a single resultsRoot for a session, and DisputeManager.raiseDispute takes an evidenceURI. Behind them stand the transcript the root is computed over, the exact hashing, and a dispute package anyone can check.

Next: Evidence format — the transcript and hashing behind commitResults, and the package a dispute points at.