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
| Contract | Path | Role |
|---|---|---|
| AIOUMToken | token/AIOUMToken.sol | Staking + governance ERC-20 (Votes/Permit/Capped/Burnable) |
| AIOUMGovernor | governance/AIOUMGovernor.sol | Token-weighted DAO Governor → Timelock |
| StakingManager | staking/StakingManager.sol | Stake / delegate / slash / multi-asset rewards |
| IdentityRegistry | registry/IdentityRegistry.sol | ERC-8004 identity |
| EndpointRegistry | registry/EndpointRegistry.sol | Where each operator's node answers |
| OperatorSigners | registry/OperatorSigners.sol | The hot key an operator names for its node |
| ReputationRegistry | registry/ReputationRegistry.sol | Settlement-gated feedback + penalties |
| ValidationRegistry | registry/ValidationRegistry.sol | Staked validation attestations |
| ServiceRegistry | registry/ServiceRegistry.sol | On-chain service catalogue |
| PaymentManager | payments/PaymentManager.sol | Metered sessions + EIP-712 vouchers + x402 funding |
| DisputeManager | disputes/DisputeManager.sol | Optimistic challenge → slash + penalty + refund |
| ValidationQuorumArbitrator | disputes/ValidationQuorumArbitrator.sol | Stake-weighted quorum arbitrator |
| CommitRevealArbitrator | disputes/CommitRevealArbitrator.sol | Dedicated-stake commit-reveal jury |
| AIOUMAccount | aa/AIOUMAccount.sol | ERC-4337 smart account with budgeted session keys + ERC-1271 |
| AIOUMAccountFactory | aa/AIOUMAccountFactory.sol | CREATE2 deterministic deployer for AIOUMAccount |
| AIOUMPaymaster | aa/AIOUMPaymaster.sol | ERC-4337 paymaster for the local development network — sponsors every UserOp |
| AIOUMVerifyingPaymaster | aa/AIOUMVerifyingPaymaster.sol | ERC-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 entireinitialSupplymint (must be non-zero).daoOwner— becomesowner()(intended: theTimelockController, never an EOA).initialSupply— minted totreasuryat deploy.cap_— hard supply cap (ERC20Capped);mintcan 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 bycap().- Inherited:
permit(...),delegate(...),delegateBySig(...),getVotes,nonces,burn/burnFrom, plus standard ERC-20. - Internal overrides
_updateandnoncesreconcile 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, asIVotes).timelock_— the executor/queue target.- Fixed governance settings:
votingDelay7200 blocks (~1d),votingPeriod50400 blocks (~7d),proposalThreshold100_000e18ZAI, 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 hold1 + (total stake − minNodeStake) / tokensPerSessionsessions 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 tonotifyReward(the PaymentManager).setDisputeManager(address d)— set the only address allowed toslash.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_)— emitsParamsSetwith all five;minSelfStake_ ≤ minNodeStake_.
Operator self-stake:
stake(uint256 amount)— pullamountZAI 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 be0or ≥minSelfStake. It may take the node's total underminNodeStake: 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 underminNodeStakeit 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'soperatorClaimable, 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 tobeneficiary. It never reverts: with nothing left to take it emits a zero-valueSlashedand 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);tomust 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, startinghttp://orhttps://, 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; grantsclientone feedback allowance foroperatorand adds tooperator'ssettledVolume.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 andaioum:run:<runId>as theuri, 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 bystaking.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 checkisOperatorat registration andisActiveNodeforisServiceLiveat 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), publicacceptedAssetList(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
SafeERC20throughout.
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). SetsserviceSplitSet[serviceId] = trueand recordsserviceDelegatorShareBps[serviceId]; settlement then uses this share in place of the global default.
Open a session (each nonReentrant, returns sessionId):
openSessionETH(uint256 serviceId)—payable; deposit ismsg.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 onowner's behalf).openSessionWithAuthorization(uint256 serviceId, address asset, uint256 amount, address from, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)— EIP-3009receiveWithAuthorization(USDC, gasless; relayer-submittable, funds move fromfrom). 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. Requiresstatus == Open,s.spent <= cumulativeAmount <= s.deposited, and a valid client signature. Marks the sessionSettledand distributes funds. The signature is checked with OpenZeppelinSignatureChecker.isValidSignatureNow(client, digest, signature), so an EOA client is verified viaECDSAand 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 likesettle, then optionally splits a fraction of the operator's net (post protocol-fee and delegator-share) among declared sub-providers.payeesandshareBpsmust be equal-length andsum(shareBps) <= 10000(≤ 100% of net); each contributor is paid in the session asset and recorded on-chain viaContributionPaid. Opt-in and node-controlled — the seed of value chains (a node credits whatever it built on). Plainsettleis unchanged.commitResults(uint256 sessionId, bytes32 resultsRoot)— operator-only. Records a root over the calls the node served for the session, each leafkeccak(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 insessionResultsRoot[sessionId]; one tx per session.reclaim(uint256 sessionId)—nonReentrant; the client reclaims the unspent deposit afterlastActivity + inactivityTimeout(liveness backstop). MarksRefunded.
_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 viasetArbitrator).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 (seesetSlashBps).penaltyWeight_— reputation penalty weight on an upheld dispute.disputeTimeout_— the window afterraisedAtpast whichresolveStalemay force-close a stalled dispute (deploy default: 3 days); updatable viasetDisputeTimeout(onlyOwner).
Key functions.
setArbitrator(address a)/setParams(IERC20 bondToken_, uint256 bondAmount_, uint256 slashAmount_, uint256 penaltyWeight_, address treasury_)/setDisputeTimeout(uint64 timeout)/setSlashBps(uint16 bps)—onlyOwner.slashBpsadds an exposure-proportional component: an upheld dispute slashesmax(slashAmount, stakeAtRaise * slashBps / 10000), wherestakeAtRaiseis 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 inbondAmount, recordsraisedAtand the operator'sstakeAtRaise, and for an open session callspayment.markDisputedto freeze settlement. A settled session's payout is already final, so it is flaggedpostSettlementinstead: 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 currentbondTokenandbondAmountare snapshotted into theDisputestruct at raise time: refunds always pay out the exact token/amount that was escrowed, even if governance swaps the bond token viasetParamswhile the dispute is open.resolve(uint256 disputeId, bool nodeAtFault)—onlyArbitrator,nonReentrant. IfnodeAtFault: for an open-session dispute, refund the client viapayment.resolveDispute(_, true), then slash the operator formax(slashAmount, stakeAtRaise * slashBps / 10000)into the DisputeManager and split what was actually recovered —challengerRewardBps(default 2000 = 20%,setChallengerReward,onlyOwner) goes to the client asChallengerRewarded, the rest totreasury. For a post-settlement dispute there is no escrow refund; the client receives the whole recovered slash instead. Either way,reputation.penalizethe operator and refund the bond to the client. Otherwise: reopen the session viapayment.resolveDispute(_, false)and award the bond to the operator. Bond payouts use the per-dispute token snapshot, not the currentbondToken.resolveStale(uint256 disputeId)—nonReentrant; anyone may force-close a dispute the arbitrator never ruled on, onceblock.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 disputeStatus.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, plusdisputeTimeout(the stale window). EachDisputerecords itsraisedAttimestamp, the snapshotted bond token/amount, and aStatus(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, callsdisputeManager.resolveonce. 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 viaresolve/resolveStale. - When the DisputeManager's
disputeTimeoutis non-zero, votes afterraisedAt + disputeTimeoutrevert with"vote window closed". The boundary is inclusive: a vote at exactlyraisedAt + disputeTimeoutis accepted — complementingresolveStale, which only fires strictly after that timestamp. - The dispute's own client and operator are recused:
vote()readsdisputeParties(disputeId)and reverts with"party recused"for either of them. - The configured DisputeManager must expose
disputeStatus(uint256),disputeTimeout()anddisputeParties(uint256). Pointing the arbitrator (viasetDisputeManager) at a contract without them makes everyvote()revert with empty revert data — fail-closed, and recoverable by the owner callingsetDisputeManageragain.
- The dispute must be Open on the DisputeManager, checked via its
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'sdisputeTimeoutmust exceed their sum (exposed asminVotingPeriod()).unbondingPeriod_— cooldown before unstaked arbitrator ZAI can be withdrawn.noRevealPenaltyBpsstarts 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 ≥quorumand no tie, then forwards the majority decision todisputeManager.resolveonce 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 losesnoRevealPenaltyBpsof 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-approvespayment_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 USDCspendCapand 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 signatureabi.encode(sessionId, cumulativeAmount, innerSig)whose inner signer is the session key that opened that session (openedBy[sessionId], set insideopenSessionFor). 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 targetopenSessionForandspent + amountmust stay withinspendCap; the runningspentis incremented on-chain, and the EntryPoint enforces thevalidUntilwindow. - 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_signstyle). It binds the op's sender, nonce,initCode,callDataand 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._validatePaymasterUserOpparsesvalidUntil/validAfterand the sponsor signature from thepaymasterAndDatatail, recovers the signer and returnsSIG_VALIDATION_FAILED(never reverts) on a wrong signature; the EntryPoint enforces the validity window.- Inherited
BasePaymastersurface (deposit,withdrawTo,addStake,unlockStake,withdrawStake);depositis open to anyone, the rest areonlyOwner.
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.