# Bond Registry

> The LOPW Bond Registry: every function, event and error, the five service states with an animated state machine, protocol floors, launch limits, fees, time locks, the slash rate limit and the unbonding maths with a calculator.

*LOPW documentation · Protocol reference.* Canonical page: https://lopw.com/docs/protocol/bond-registry.html · This Markdown twin: https://lopw.com/docs/protocol/bond-registry.md · Index of all pages: https://lopw.com/llms.txt · Everything in one file: https://lopw.com/llms-full.txt

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

| From | Trigger | To |
| --- | --- | --- |
| `Pending` | deposit reaches min_stake | `Active` |
| `Pending` | withdraw (no delay) | `Closed` |
| `Active` | slash or fee takes stake below min_stake | `Suspended` |
| `Suspended` | deposit restores min_stake | `Active` |
| `Active` | request_unbond that would leave stake below min_stake | `Unbonding` |
| `Suspended` | request_unbond (full exit) | `Unbonding` |
| `Unbonding` | cancel_unbond, stake at or above min_stake | `Active` |
| `Unbonding` | cancel_unbond, stake below min_stake | `Suspended` |
| `Unbonding` | withdraw after the unbonding delay | `Closed` |
| `Active` | request_unbond of stake above min_stake (partial, stays Active) | `Active` |

| 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

*Bond Registry interface*

```text
// 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 than `max_payout` when 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 `Suspended` during 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.

*Rate limit, as the registry applies it*

```text
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

```text
unbonding_delay  = max_claim_window_ever + unbond_margin            (unbond_margin = 3600 s)
full_exit        = stake - amount < min_stake                       (then the whole stake exits)
unlock_at        = now + unbonding_delay
slash_epoch      = unbonding_delay; cap = epoch_base_stake * 5000 / 10000
payout           = min(max_claim, stake, max_payout if set); first slash of an epoch always allowed
stake_fee        = stake * bps_per_year * elapsed_seconds / (10000 * 31536000), capped at stake
admin_time_lock  = max_claim_window + 2 * unbond_margin = 93600 s (26 h)
```

## 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` |

```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

| 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` |

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