Bound

Protocol

How it works

The certificate lifecycle: publish, accept, fund, attest, pay, halt, challenge, close_window and release.

One contract holds the whole protocol. Tokens enter only through NEP-141 ft_transfer_call and are credited to an internal balance; inside the protocol only internal balances move (reserve, float, stake, allocation, bonds, payouts); tokens leave only by withdraw. Amounts are mock USDC base units (6 decimals).

The lifecycle at a glance

  1. Register. Every account that holds a balance on Bound (operator, auditor, challenger, service) calls storage_deposit (0.01 NEAR). The agent does not need to.
  2. Stake. The auditor sends mock USDC with ft_transfer_call and msg {"stake":{}}.
  3. Publish. The operator calls publish with the agent, bound, reserve, expiry, auditor, float cap, optional allowlist and optional premium.
  4. Accept. The agent calls accept. This is its consent; a Pending certificate never blocks the agent.
  5. Fund. The operator funds the reserve and the float (by method or by ft_transfer_call msg).
  6. Attest. The named auditor locks an allocation of its free stake behind this one certificate. Status becomes Verified.
  7. Confine. The agent adds a pay-only function-call key and deletes its full-access key (see Key confinement).
  8. Pay. The agent spends from the float through pay, metered on every call.
  9. Challenge and settle if something goes wrong; otherwise release after the settlement deadline.

Publish and accept

publish needs 0.05 NEAR attached (excess is refunded) and a registered operator. The contract refuses an agent equal to the operator, an auditor equal to the operator or the agent, a zero bound or reserve, float_cap > bound, an expiry in the past, and an allowlist that is empty, longer than 16 or contains the agent. A premium, if given, is taken from the operator's balance now and held in escrow.

accept is called by the agent with 1 yoctoNEAR. An agent holds one live certificate at a time: accepting a new one fails with AgentHasLiveCert while the previous one is still Accepted or Verified and before its expiry (or frozen by a claim window). verify(agent) always reports the last certificate the agent accepted.

Fund the reserve and the float

  • Reserve. fund_reserve(cert_id, amount) from the operator's internal balance, or ft_transfer_call with msg {"reserve":{"cert_id":N}}. Allowed while the certificate is Pending, Accepted or Verified.
  • Float. fund_float(cert_id, amount) or msg {"float":{"cert_id":N}}. Allowed while Accepted or Verified, and never above float_cap (FloatCapExceeded). The float is what the agent can actually spend; the operator refills it.

Attest and stake

attest(cert_id, allocation) is called by the auditor named in the certificate. It needs the certificate Accepted and not expired, reserve_locked >= reserve_promised (else ReserveUnfunded), allocation >= min_stake and enough free stake. The allocation moves from the auditor's free stake to allocated stake. An escrowed premium is paid now: the treasury gets protocol_fee_bps of it (10% on testnet), the auditor the rest.

Pay: float, allowlist, halt

pay(cert_id, to, amount, memo?) takes no deposit, so a function-call key can call it. It is refused, in this order, when:

ErrorCondition
NotAgentthe caller is not the certificate's agent
CertNotVerifiedthe stored status is not Verified
CertFrozena claim window is open
SettlementDeadlinePassednow >= expires_at + post_expiry_secs
NotCurrentCertthe agent accepted a newer certificate
CertHaltedthe operator halted it
InvalidAmountamount is 0
InvalidRecipientto is the agent itself or the contract
RecipientNotAllowedan allowlist is set and to is not on it
RecipientNotRegisteredto has no storage_deposit on Bound
FloatExceededamount > float
MemoTooLongmemo over 64 bytes

A successful payment moves the amount from the float to the recipient's internal balance, adds it to spent, counts pay_count, adds it to post_expiry_spent when it lands later than expires_at + grace_secs, and emits a pay event. The operator can halt and resume a certificate, and clawback the unspent float, at any time.

Challenges

challenge(cert_id, proof, victim?, bond) needs 0.01 NEAR attached (excess refunded) and a bond of at least min_challenge_bond from the caller's balance. The operator, agent and auditor cannot challenge (ConflictedChallenger) or be named as the victim (ConflictedVictim). The victim defaults to the challenger and must be registered.

ProofKindTrue when
InsufficientReservetrustlessreserve_locked < reserve_promised. Structurally false after attest; kept for completeness.
BoundExceededtrustless, hygienespent > bound
ExpiredSpendtrustless, hygienepost_expiry_spent > de_minimis_bps of bound
Harm { claimed, evidence_uri }arbiterdecided by the arbiter; claimed > 0, evidence_uri up to 256 bytes
  • A trustless predicate that is false when filed is rejected in the same transaction, and its bond goes to the treasury.
  • Any other claim opens the certificate's claim window (or joins it) and freezes the certificate: no pay, no release.
  • A window holds at most 16 claims (WindowFull) and one claim per trustless predicate kind (DuplicateClaim).
  • A Verified certificate accepts every kind. A settled (Invalid) certificate accepts only Harm claims (NotChallengeable).

The claim window

A new window closes claim_window_secs after it opens (90 s on testnet). A claim that arrives with less than a third of a window left extends closes_at to now plus a third of a window, so a late victim and the arbiter always get time. The extension never moves closes_at past deadline + claim_window_secs.

New claims are accepted while now < deadline (the settlement deadline, expires_at + post_expiry_secs), or after it only into a window that opened before the deadline and is still open.

The arbiter decides Harm claims with resolve_by_arbiter(challenge_id, proven, harm). A proven harm must satisfy 0 < harm <= claimed and is capped at the certificate's coverage. A rejected claim is settled at once: its bond goes to the treasury and its slot in the window is freed.

close_window and the waterfall

Anyone may call close_window(cert_id) once now >= closes_at (else WindowOpen). While a Harm claim is still undecided, it also waits for the arbiter grace of a third of a window after closes_at (ArbiterGrace). Then, in one state change:

  • An undecided claim is Unadjudicated: half its bond returns to the challenger, half goes to the treasury.
  • A trustless claim whose predicate is no longer true is Cured: its bond returns.
  • Every other claim is admitted. With no admitted claim, the window simply closes and the certificate is payable again (close_window returns null).

With arbitrated harm (fraud mode), the settlement waterfall in settle.rs runs as one pure function:

settlement waterfall
payable      = min(total_harm, reserve + allocation)
reserve_draw = min(payable, reserve)
victim_pool  = min(reserve_draw, arbitrated_harm)  -> victims pro rata; rest -> treasury
fee_pool     = min(fee_bps x total_harm, reserve - reserve_draw) -> challengers pro rata
slash        = min(payable - reserve_draw, allocation)
               -> unpaid arbitrated harm first (pro rata), rest -> treasury
allocation - slash stays allocated until the deadline; cert Invalid (halted);
               admitted bonds returned

The auditor is slashed only for harm the operator's own reserve could not cover, and the slash first tops up arbitrated victims who were not paid in full; it never goes to a predicate claimant or a challenger fee. In hygiene mode (only true predicates, nobody proven harmed) one flat hygiene_bounty, at most the reserve, is split equally between the admitted challengers, and the allocation is not slashed.

Either way, admitted bonds are returned, the unspent float goes back to the operator, and the certificate becomes Invalid and halted. The result is stored as the certificate's settlement and emitted as a settle event.

In the recorded demo

Cert 0 had reserve 100 and allocation 200. The arbiter upheld a Harm of 120. Payable was 120: the victim got 100 from the reserve and 20 from the auditor's slash. The challenger fee was 0 because the reserve was exhausted, the treasury got 0, and the unslashed 180 stayed allocated to the certificate.

After a settlement

  • Settled certificates stay liable until the deadline. A settlement halts the certificate for spending but leaves it open to Harm claims until expires_at + post_expiry_secs. Later windows pay from what is left of the reserve and the allocation. release_reserve on an Invalid certificate waits for the deadline, and close_window no longer frees the allocation.
  • Rejected claims free their slots. An arbiter-rejected claim leaves the window immediately, so 16 junk claims cannot keep a real victim out as long as the arbiter acts.
  • Harm is capped at coverage. A proven harm above reserve_locked + allocation is capped at it, and the waterfall uses a 256-bit mul_div, so a huge harm cannot make close_window panic.
  • A later window that moves no funds keeps the earlier settlement record.

Release after the deadline

  • release_reserve(cert_id) (operator): before attestation, at any time, returning the reserve, the float and an escrowed premium. After attestation, only from the settlement deadline on and with no open window (Locked, CertFrozen). The certificate becomes Released; if a settlement drew everything it still becomes Released and returns 0.
  • release_allocation(cert_id) (auditor): from the deadline on, with no open window, the remaining allocation returns to free stake. Then unstake moves free stake to the internal balance and withdraw sends it to the token account.

Statuses in the view

verify and get_cert compute the status at read time. Act only on Verified, and on deal: true if you expect the agent to pay you.

StatusMeaning
Pendingpublished, waiting for the agent
Acceptedaccepted, waiting for reserve and attestation
Verifiedattested, current, not expired, not halted, no open window
Haltedattested but halted by the operator
Expiredpast expires_at; pay still works until the deadline, but deal is false
Supersededthe agent accepted a newer certificate; this one cannot pay, but stays challengeable until its deadline
Challengeda claim window is open
Invalidsettled against; still answers Harm claims until the deadline
Releasedcollateral returned

deal is true only when the status is Verified and the float is above 0. It says nothing about the agent's keys: check those separately (Key confinement).