Run the circuit breaker
This demo shows the guard doing its job. A tiny lending pool asks the guard before every borrow; the reserve reports change, and the borrows follow. It takes about five minutes, most of it waiting for a report to go stale.
Before you start
Section titled “Before you start”- The repo builds: see the quickstart.
- The identities
assay-admin,assay-issuer,assay-attestor-a,assay-witnessandassay-borrowerexist and are funded: see testnet identities.
Run it
Section titled “Run it”pnpm --filter @garkuwa/assay-scripts demo:circuit-breakerThe script deploys a fresh registry, the guard and the demo pool, and gets everything ready:
- Deploy the registry and the guard (pointed at that registry).
- Use the
ADEMOtoken contract as collateral, and deploy a demo stablecoin,DUSD, as the asset being lent. - Deploy the demo pool with the policy below and a 50% borrow limit.
- Register
ADEMOin the registry, add one attestor, set the supply witness, and post a supply of 1,000,000ADEMO. - Give the borrower 1,000
ADEMO, fund the pool with 100,000DUSD, and deposit the borrower’s 1,000ADEMOas collateral.
The demo policy (one Policy value the pool passes to the guard):
| Field | Value | Meaning |
|---|---|---|
min_coverage_bps |
10000 | At least 100% coverage |
max_age_secs |
90 | Reports older than 90 seconds don’t count (short, so the stale step happens live) |
min_attestors |
1 | One fresh attestor is enough |
max_disagreement_bps |
500 | Attestors may differ by up to 5 points |
max_supply_mismatch_bps |
50 | Supply may exceed stated liabilities by up to 0.5% |
The story
Section titled “The story”Each step posts a report (except the last) and then borrows 100 DUSD:
| Step | Report | Guard says | Result |
|---|---|---|---|
| 1 | 101% | (true, Ok) |
Borrow succeeds; DUSD is transferred |
| 2 | 92% | (false, Undercovered) |
Rejected; nothing moves |
| 3 | 100.4% | (true, Ok) |
Borrow succeeds |
| 4 | none for 90 s | (false, Stale) |
Rejected; nothing moves |
A rejected borrow is still a successful transaction: the pool returns
Rejected(reason) and emits a borrow_rejected event with the reason. That
way every step has a transaction anyone can open.
Output from a run on 14 October 2026:
✓ Borrower deposits 1,000 ADEMO as collateralThe story:✓ 1. Fresh report at 101% → borrow succeeded✓ 2. Attestor posts 92% → borrow rejected (UNDERCOVERED)✓ 3. Fresh report at 100.4% → borrow succeeded waiting 74 s for the last report to pass the 90 s max age…✓ 4. No new report past the 90-second max age → borrow rejected (STALE)wrote docs/DEMO-RUN.md and docs/audit/e2e-receipts/circuit-breaker-2026-10-14.jsonCheck it on chain
Section titled “Check it on chain”Open a rejected borrow on stellar.expert and expand its effects. In the
committed run, step 2 is
84d31e35…
and step 4 is
ec2c5847….
You’ll see:
borrow(borrower, 1000000000)on the pool returns["Rejected", ["Undercovered"]].- The pool’s call to the guard:
is_healthy(ADEMO, policy)→[false, ["Undercovered"]]. - The guard’s call to the registry:
status_with(ADEMO, 90, 50)with the coverage it found. - One event,
["testnet_demo", "borrow_rejected", borrower], with the amount and the reason. - No token transfer.
The successful borrows show a DUSD transfer instead. After the run, the
borrower’s position is 1,000 ADEMO collateral and 200 DUSD debt.
Ask the guard yourself
Section titled “Ask the guard yourself”The guard from the committed run is
CCVEIPYTY6RRVJZONFZL4WVGN66H5IU77H3HSSQJKH7I7WSDL6DIEKDV. Ask it about
ADEMO with your own policy (here, a 35-day max age):
stellar contract invoke --network testnet --source-account assay-admin \ --id CCVEIPYTY6RRVJZONFZL4WVGN66H5IU77H3HSSQJKH7I7WSDL6DIEKDV \ -- is_healthy --asset CC22EDRUYLGMSF4YOP77GKP6O6SQQT6WY2VP6VHNHALTOBPGXNIKRWTB \ --policy '{"min_coverage_bps":10000,"max_age_secs":3024000,"min_attestors":1,"max_disagreement_bps":500,"max_supply_mismatch_bps":50}'On 14 October 2026 this returned [true,"Ok"]. Once the last report is
older than 35 days, the same call returns [false,"Stale"].
The full run, with all 22 transactions, is in the demo run.