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
- 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. - Stake. The auditor sends mock USDC with
ft_transfer_calland msg{"stake":{}}. - Publish. The operator calls
publishwith the agent, bound, reserve, expiry, auditor, float cap, optional allowlist and optional premium. - Accept. The agent calls
accept. This is its consent; a Pending certificate never blocks the agent. - Fund. The operator funds the reserve and the float (by method or by
ft_transfer_callmsg). - Attest. The named auditor locks an allocation of its free stake behind this one certificate. Status becomes
Verified. - Confine. The agent adds a pay-only function-call key and deletes its full-access key (see Key confinement).
- Pay. The agent spends from the float through
pay, metered on every call. - 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, orft_transfer_callwith 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 abovefloat_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:
| Error | Condition |
|---|---|
NotAgent | the caller is not the certificate's agent |
CertNotVerified | the stored status is not Verified |
CertFrozen | a claim window is open |
SettlementDeadlinePassed | now >= expires_at + post_expiry_secs |
NotCurrentCert | the agent accepted a newer certificate |
CertHalted | the operator halted it |
InvalidAmount | amount is 0 |
InvalidRecipient | to is the agent itself or the contract |
RecipientNotAllowed | an allowlist is set and to is not on it |
RecipientNotRegistered | to has no storage_deposit on Bound |
FloatExceeded | amount > float |
MemoTooLong | memo 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.
| Proof | Kind | True when |
|---|---|---|
InsufficientReserve | trustless | reserve_locked < reserve_promised. Structurally false after attest; kept for completeness. |
BoundExceeded | trustless, hygiene | spent > bound |
ExpiredSpend | trustless, hygiene | post_expiry_spent > de_minimis_bps of bound |
Harm { claimed, evidence_uri } | arbiter | decided 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_windowreturns null).
With arbitrated harm (fraud mode), the settlement waterfall in settle.rs runs as one pure function:
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 returnedThe 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_reserveon an Invalid certificate waits for the deadline, andclose_windowno 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 + allocationis capped at it, and the waterfall uses a 256-bitmul_div, so a huge harm cannot makeclose_windowpanic. - A later window that moves no funds keeps the earlier
settlementrecord.
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 becomesReleased; 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. Thenunstakemoves free stake to the internal balance andwithdrawsends 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.
| Status | Meaning |
|---|---|
Pending | published, waiting for the agent |
Accepted | accepted, waiting for reserve and attestation |
Verified | attested, current, not expired, not halted, no open window |
Halted | attested but halted by the operator |
Expired | past expires_at; pay still works until the deadline, but deal is false |
Superseded | the agent accepted a newer certificate; this one cannot pay, but stays challengeable until its deadline |
Challenged | a claim window is open |
Invalid | settled against; still answers Harm claims until the deadline |
Released | collateral 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).