Docs / Protocol reference
Bond Registry
Stake is slashable seller capital, held per service in a single stake token per registry deployment (USDC on the first deployment). It is not per-call insurance: a service’s total payouts can never exceed its stake. The registry also stores every terms version and every signing key a service has ever had.
Service states
Service states and transitions
Press Play to follow one service from registration to closure, or choose a state to see where it can go.
| State | Meaning | Offers | Claims paid |
|---|---|---|---|
Pending | Registered; stake has not yet reached min_stake | No | No receipts can exist; slash refuses |
Active | Stake is at or above min_stake | Yes | Yes |
Suspended | A slash or a fee took stake below min_stake | No | Yes, while stake remains |
Unbonding | A full exit is waiting out the delay | No | Yes |
Closed | Stake was withdrawn; final | No | No |
The registry sets the state during the slash, so suspension is automatic. The seller middleware issues offers only while Active, and the buyer SDK pays only Active services. Every change emits StateChanged { service_id, from, to }.
Functions
// seller
register_service(seller, signing_key, terms, min_stake) -> Result<service_id, Error>
update_terms(service_id, terms) -> Result<terms_hash, Error> // not while Unbonding or Closed
rotate_key(service_id, signing_key) -> Result<key_version, Error> // not while Closed
deposit(service_id, amount) -> Result<(), Error>
request_unbond(service_id, amount) -> Result<(), Error>
cancel_unbond(service_id) -> Result<(), Error>
withdraw(service_id) -> Result<amount, Error>
// claims contract only
slash(service_id, buyer, amount) -> Result<paid, Error>
// anyone
collect_fee(service_id) -> Result<fee, Error>
extend_service_ttl(service_id) -> Result<(), Error>
extend_instance_ttl()
apply_claims_contract() -> Result<Address, Error> apply_limits() -> Result<LaunchLimits, Error> apply_fees() -> Result<FeeConfig, Error>
// reads
get_service(service_id) -> Result<Service, Error> get_terms(service_id, terms_hash) -> Result<Terms, Error>
get_key(service_id, key_version) -> Result<SigningKey, Error> terms_hash(terms) -> BytesN<32>
get_config() -> ProtocolConfig get_token() -> Address get_claims_contract() -> Option<Address> get_pending_claims_contract() -> Option<PendingChange>
get_limits() -> LaunchLimits get_pending_limits() -> Option<PendingLimits> get_fees() -> FeeConfig get_pending_fees() -> Option<PendingFees>
is_paused() -> bool is_seller_allowed(seller) -> bool get_total_stake() -> i128
// admin
set_claims_contract(claims) -> Result<eta, Error> cancel_claims_change()
set_validator lives on the Claims contract
set_limits(limits) -> Result<eta, Error> cancel_limits_change()
set_fees(fees) -> Result<eta, Error> cancel_fees_change()
set_paused(paused) set_seller_allowed(seller, allowed)Every public function returns a typed result, so generated clients, apps and agents receive the named error code rather than an opaque host failure.
Rules per function
| Function | Requires | Effect |
|---|---|---|
register_service | Seller auth; not paused; seller allowlisted if allowlist_only; min_stake ≥ min_stake_floor; valid terms | Pays the registration fee to the treasury if any; derives service_id; stores terms version and key version 1; state Pending; max_claim_window = terms.claim_window; emits ServiceRegistered |
update_terms | Seller auth; state not Unbonding or Closed; valid terms | Stores a new terms version and makes it current; raises max_claim_window if larger; old versions stay readable; emits TermsUpdated |
rotate_key | Seller auth; not Closed | Old key gets retired_at = now; new key at version + 1; emits KeyRotated |
deposit | Seller auth; amount > 0; not Closed; not paused; within launch caps | Collects accrued fee; transfers; Pending or Suspended become Active when stake reaches the minimum; emits Deposited |
request_unbond | Seller auth; Active or Suspended; no withdrawal pending; 0 < amount ≤ stake | Collects fee; partial if stake − amount ≥ min_stake (stays Active) else full exit (whole stake pending, state Unbonding); unlock_at = now + max_claim_window + unbond_margin; emits UnbondRequested |
cancel_unbond | Seller auth; a withdrawal pending | Clears it; Unbonding returns to Active (stake ≥ min) or Suspended; emits UnbondCancelled |
withdraw | Seller auth; Pending, or a pending withdrawal past unlock_at | Collects fee; Pending pays the whole stake and closes; full exit pays the whole stake and closes; partial pays min(pending, stake) and suspends if that leaves stake below the minimum; emits Withdrawn |
slash | Claims contract auth; state not Pending or Closed; amount > 0; stake > 0 | Collects fee; payout = min(amount, stake, max_payout?); applies the rate limit; reduces stake and any pending withdrawal; suspends if below the minimum; transfers to the buyer; emits Slashed |
collect_fee | Service exists | Collects the stake fee accrued since the last collection; may suspend; emits FeeCollected |
Error codes
| Code | Error | Cause |
|---|---|---|
| 1 | AlreadyInitialized | Constructor called twice |
| 2 | ClaimsNotSet | slash before a Claims contract is wired |
| 3 | UnknownService | No such service |
| 4 | UnknownTerms | No terms version with that hash for the service |
| 5 | UnknownKey | No key at that version |
| 6 | InvalidTerms | Terms break a validity rule |
| 7 | StakeBelowFloor | min_stake under the protocol floor |
| 8 | InvalidAmount | Zero, negative or above stake |
| 9 | WrongState | The function is not allowed in the service’s state |
| 10 | WithdrawalPending | A withdrawal is already pending |
| 11 | NoWithdrawalPending | Nothing to cancel or withdraw |
| 12 | StillLocked | unlock_at not reached |
| 13 | NoStake | Stake is zero |
| 14 | EpochCapReached | Slash would exceed the epoch cap (surfaces as SlashRateLimited on a claim) |
| 15 | InvalidConfig | Constructor floors inconsistent |
| 16 | NoPendingChange | Nothing to apply or cancel |
| 17 | ChangeNotReady | Time lock not passed |
| 18 | Paused | Registrations and deposits are paused |
| 19 | NotAllowlisted | Seller not approved during an allowlist-only launch |
| 20 | StakeCapReached | Per-service or total stake limit |
| 21 | InvalidFees | Negative registration fee or more than 10,000 bps |
| 22 | InvalidLimits | Negative limit |
Events
| Event | Data |
|---|---|
ServiceRegistered | service_id (topic), seller, terms_hash, min_stake |
TermsUpdated | service_id (topic), terms_hash |
KeyRotated | service_id (topic), key_version |
Deposited | service_id (topic), amount, stake |
UnbondRequested | service_id (topic), amount, unlock_at, full_exit |
UnbondCancelled | service_id (topic) |
Withdrawn | service_id (topic), amount, stake |
Slashed | service_id (topic), buyer, amount, stake |
FeeCollected | service_id (topic), amount, stake |
StateChanged | service_id (topic), from, to |
ClaimsContractSet, ClaimsChangeProposed, ClaimsChangeCancelled | claims, and eta on the proposal |
LimitsSet, LimitsChangeProposed | limits, and eta on the proposal |
FeesSet, FeesChangeProposed | fees, and eta on the proposal |
PausedSet | paused |
SellerAllowed | seller (topic), allowed |
Protocol floors
Fixed at registry construction in ProtocolConfig. Values of the first deployment:
| Parameter | Value | Effect |
|---|---|---|
min_stake_floor | 100 USDC (1,000,000,000 in 7-decimal units) | Lowest allowed min_stake |
max_claim_window | 86,400 s (24 h) | Longest allowed claim window |
max_oracle_claim_window | 3,600 s (1 h) | Longest claim window for services that offer P6, inside the oracle’s history retention |
unbond_margin | 3,600 s (1 h) | Added to the largest claim window to form the unbonding delay and the slash epoch |
slash_epoch_cap_bps | 5,000 (50%) | Share of epoch-start stake that may be slashed per epoch |
Safeguards
- Claim cap. One receipt pays at most
max_claim, never more than the stake left, and never more thanmax_payoutwhen a launch limit is set. - Unbonding delay.
max_claim_window_ever + unbond_margin, so every receipt issued before an exit can still be claimed, whatever terms version it was issued under. Terms cannot change while a service is Unbonding. - Partial withdrawal. Stake above the minimum leaves through the same delay, and the service stays Active. The pending amount stays slashable until it is withdrawn.
- Automatic suspension. The registry sets
Suspendedduring the slash or fee collection that takes stake below the minimum. - Slash rate limit. Below.
- Time-locked administration. Below.
Slash rate limit
The contract never checks that a payment happened, so a seller could sign receipts for invented payments to addresses it controls and claim them, pulling stake out instantly and ahead of honest buyers. The rate limit removes the advantage: stake can leave through claims no faster than through a normal withdrawal.
epoch_len = max_claim_window + unbond_margin // same length as the unbonding delay
if epoch_base_stake == 0 or now >= epoch_start + epoch_len: // start a new epoch
epoch_start, epoch_base_stake, epoch_slashed = now, stake, 0
cap = epoch_base_stake * slash_epoch_cap_bps / 10000 // 50% of stake at epoch start
if epoch_slashed > 0 and epoch_slashed + payout > cap: fail EpochCapReached // first slash of an epoch is always allowed
epoch_slashed += payoutThe first slash of an epoch is always allowed (still bounded by max_claim and remaining stake), so a single honest claim is never blocked even when max_claim exceeds the cap. A claim refused by the cap fails with SlashRateLimited, records nothing, and can be retried in the next epoch if its window is still open. The trade-off: a genuine mass breach is paid out over more than one epoch.
Unbonding maths
unbonding_delay = max_claim_window_ever + unbond_margin
full_exit = stake - amount < min_stake
pending = full_exit ? stake : amount
unlock_at = now + unbonding_delay
withdraw pays = full_exit ? stake : min(pending, stake) // slashes and fees reduce pending with the stakeExamples: a service whose largest claim window ever was 1 hour unbonds in 2 hours; one that once registered a 24-hour window unbonds in 25 hours, for the rest of its life. The admin time lock (26 hours) is longer than any possible unbonding delay by design.
Calculator
Unbonding, slash cap and fee calculator
Integer maths in 7-decimal units, exactly as the contract computes it. Protocol floors: minimum min_stake 100 USDC, claim window at most 24 h, unbond margin 1 h, slash epoch cap 50 %.
Protocol fees
Two fees in the stake token, paid to a treasury address, both of which may be zero:
| Fee | When | How |
|---|---|---|
registration_fee | Once, on register_service | Transferred from the seller’s account to the treasury before the service is created |
stake_fee_bps_per_year | Continuously on stake held | Accrued per second; collected from stake whenever the service is touched (deposit, request_unbond, withdraw, slash) or when anyone calls collect_fee; emits FeeCollected |
fee = stake * stake_fee_bps_per_year * elapsed_seconds / (10000 * 31_536_000) // integer division, capped at stake
// example: 200 USDC at 100 bps for 30 days -> 2_000_000_000 * 100 * 2_592_000 / 315_360_000_000 = 1_643_835 units = 0.1643835 USDCThe fee can take a service below its minimum, which suspends it like a slash would, so sellers should stake above the minimum. The first deployment runs a 1 USDC registration fee and 100 bps (1%) per year; proposed launch values on a production network are 10 USDC and 100 bps. Fee changes are time-locked.
Guarded launch: limits, pause, allowlist
| Control | Meaning | Change rule |
|---|---|---|
max_stake_per_service | Most stake one service may hold (0 = no limit) | Tightening immediate; loosening time-locked |
max_total_stake | Most stake the registry may hold across all services | Same; backed by a running TotalStake counter |
max_payout | Ceiling on any single claim, whatever the offer’s max_claim | Same |
allowlist_only | Only sellers approved with set_seller_allowed may register | Turning on immediate; turning off time-locked |
set_paused | Stops new registrations and deposits; never claims, unbonding or withdrawals, so funds are never trapped | Immediate in both directions |
The first deployment runs with no limits and no allowlist. A guarded production launch is expected to start with low caps and allowlist_only on, then loosen them through the time lock.
Time locks
The one way slashing rights can move is through the admin, and every such change is announced on-chain and delayed long enough for every seller to withdraw first. The first setting of each value applies immediately (initial wiring); every later change is a two-step proposal.
| Change | Propose | Apply (anyone, after eta) | Cancel (admin) |
|---|---|---|---|
| Which contract may slash | set_claims_contract → ClaimsChangeProposed | apply_claims_contract → ClaimsContractSet | cancel_claims_change |
| Which validator decides a predicate | set_validator (Claims contract) → ValidatorChangeProposed | apply_validator → ValidatorSet | cancel_validator_change |
| Fees | set_fees → FeesChangeProposed | apply_fees → FeesSet | cancel_fees_change |
| Looser limits | set_limits → LimitsChangeProposed | apply_limits → LimitsSet | cancel_limits_change |
admin_delay = max_claim_window + 2 * unbond_margin = 86400 + 7200 = 93,600 s (26 h) // registry
change_delay = 93,600 s // Claims contract, set at construction26 hours is longer than any service’s unbonding delay (at most 25 hours), so every seller can fully withdraw before a change takes effect. Contracts have no upgrade function. Sellers and monitors should watch for the *ChangeProposed events.
Storage lifetime
Services, terms versions, keys and claim records live in persistent storage. Every access extends their lifetime (threshold 30 days, extended to 60 days, counted in ledgers of about five seconds). extend_service_ttl(service_id) refreshes a service’s entries and extend_instance_ttl() the contract’s own; anyone may call them, and a keeper does so on a schedule. Long-idle services may need such a refresh.