LOPWDocumentation

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

Service state machine of the Bond Registrydeposit reaches min_stakewithdraw (no delay)slash or fee takes stake below min_stakedeposit restores min_stakerequest_unbond that would leave stake below min_stakerequest_unbond (full exit)cancel_unbond, stake at or above min_stakecancel_unbond, stake below min_stakewithdraw after the unbonding delayrequest_unbond of stake above min_stake (partial, stays Active)PendingActiveSuspendedUnbondingClosed

Press Play to follow one service from registration to closure, or choose a state to see where it can go.

    StateMeaningOffersClaims paid
    PendingRegistered; stake has not yet reached min_stakeNoNo receipts can exist; slash refuses
    ActiveStake is at or above min_stakeYesYes
    SuspendedA slash or a fee took stake below min_stakeNoYes, while stake remains
    UnbondingA full exit is waiting out the delayNoYes
    ClosedStake was withdrawn; finalNoNo

    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

    Bond Registry interface
    // 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

    FunctionRequiresEffect
    register_serviceSeller auth; not paused; seller allowlisted if allowlist_only; min_stake ≥ min_stake_floor; valid termsPays 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_termsSeller auth; state not Unbonding or Closed; valid termsStores a new terms version and makes it current; raises max_claim_window if larger; old versions stay readable; emits TermsUpdated
    rotate_keySeller auth; not ClosedOld key gets retired_at = now; new key at version + 1; emits KeyRotated
    depositSeller auth; amount > 0; not Closed; not paused; within launch capsCollects accrued fee; transfers; Pending or Suspended become Active when stake reaches the minimum; emits Deposited
    request_unbondSeller auth; Active or Suspended; no withdrawal pending; 0 < amount ≤ stakeCollects 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_unbondSeller auth; a withdrawal pendingClears it; Unbonding returns to Active (stake ≥ min) or Suspended; emits UnbondCancelled
    withdrawSeller auth; Pending, or a pending withdrawal past unlock_atCollects 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
    slashClaims contract auth; state not Pending or Closed; amount > 0; stake > 0Collects 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_feeService existsCollects the stake fee accrued since the last collection; may suspend; emits FeeCollected

    Error codes

    CodeErrorCause
    1AlreadyInitializedConstructor called twice
    2ClaimsNotSetslash before a Claims contract is wired
    3UnknownServiceNo such service
    4UnknownTermsNo terms version with that hash for the service
    5UnknownKeyNo key at that version
    6InvalidTermsTerms break a validity rule
    7StakeBelowFloormin_stake under the protocol floor
    8InvalidAmountZero, negative or above stake
    9WrongStateThe function is not allowed in the service’s state
    10WithdrawalPendingA withdrawal is already pending
    11NoWithdrawalPendingNothing to cancel or withdraw
    12StillLockedunlock_at not reached
    13NoStakeStake is zero
    14EpochCapReachedSlash would exceed the epoch cap (surfaces as SlashRateLimited on a claim)
    15InvalidConfigConstructor floors inconsistent
    16NoPendingChangeNothing to apply or cancel
    17ChangeNotReadyTime lock not passed
    18PausedRegistrations and deposits are paused
    19NotAllowlistedSeller not approved during an allowlist-only launch
    20StakeCapReachedPer-service or total stake limit
    21InvalidFeesNegative registration fee or more than 10,000 bps
    22InvalidLimitsNegative limit

    Events

    EventData
    ServiceRegisteredservice_id (topic), seller, terms_hash, min_stake
    TermsUpdatedservice_id (topic), terms_hash
    KeyRotatedservice_id (topic), key_version
    Depositedservice_id (topic), amount, stake
    UnbondRequestedservice_id (topic), amount, unlock_at, full_exit
    UnbondCancelledservice_id (topic)
    Withdrawnservice_id (topic), amount, stake
    Slashedservice_id (topic), buyer, amount, stake
    FeeCollectedservice_id (topic), amount, stake
    StateChangedservice_id (topic), from, to
    ClaimsContractSet, ClaimsChangeProposed, ClaimsChangeCancelledclaims, and eta on the proposal
    LimitsSet, LimitsChangeProposedlimits, and eta on the proposal
    FeesSet, FeesChangeProposedfees, and eta on the proposal
    PausedSetpaused
    SellerAllowedseller (topic), allowed

    Protocol floors

    Fixed at registry construction in ProtocolConfig. Values of the first deployment:

    ParameterValueEffect
    min_stake_floor100 USDC (1,000,000,000 in 7-decimal units)Lowest allowed min_stake
    max_claim_window86,400 s (24 h)Longest allowed claim window
    max_oracle_claim_window3,600 s (1 h)Longest claim window for services that offer P6, inside the oracle’s history retention
    unbond_margin3,600 s (1 h)Added to the largest claim window to form the unbonding delay and the slash epoch
    slash_epoch_cap_bps5,000 (50%)Share of epoch-start stake that may be slashed per epoch

    Safeguards

    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.

    Rate limit, as the registry applies it
    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 += payout

    The 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

    Text
    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 stake

    Examples: 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

    Service
    Withdrawal
    Fees

    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:

    FeeWhenHow
    registration_feeOnce, on register_serviceTransferred from the seller’s account to the treasury before the service is created
    stake_fee_bps_per_yearContinuously on stake heldAccrued per second; collected from stake whenever the service is touched (deposit, request_unbond, withdraw, slash) or when anyone calls collect_fee; emits FeeCollected
    Text
    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 USDC

    The 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

    ControlMeaningChange rule
    max_stake_per_serviceMost stake one service may hold (0 = no limit)Tightening immediate; loosening time-locked
    max_total_stakeMost stake the registry may hold across all servicesSame; backed by a running TotalStake counter
    max_payoutCeiling on any single claim, whatever the offer’s max_claimSame
    allowlist_onlyOnly sellers approved with set_seller_allowed may registerTurning on immediate; turning off time-locked
    set_pausedStops new registrations and deposits; never claims, unbonding or withdrawals, so funds are never trappedImmediate 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.

    ChangeProposeApply (anyone, after eta)Cancel (admin)
    Which contract may slashset_claims_contract → ClaimsChangeProposedapply_claims_contract → ClaimsContractSetcancel_claims_change
    Which validator decides a predicateset_validator (Claims contract) → ValidatorChangeProposedapply_validator → ValidatorSetcancel_validator_change
    Feesset_fees → FeesChangeProposedapply_fees → FeesSetcancel_fees_change
    Looser limitsset_limits → LimitsChangeProposedapply_limits → LimitsSetcancel_limits_change
    Text
    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 construction

    26 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.