Skip to content
Protocol

The accounting guard

A permissionless contract with no address of its own that closes a branch when two of its internal counters stop agreeing, reads no price, pays nobody, and cannot be undone.

Spec v0.15, reviewed 2026-09-09

AccountingGuard answers one question, and anybody may ask it: does the fyUSD this branch has minted still equal the debt this branch has recorded? If it does not, by more than 1 fyUSD, the branch closes. For good.

It reads no price, no regime and no average. An oracle that lies can never close a branch through this contract.

Rule R-3.38, decision D218

The rule it is built on

A counter is only ever compared with a counter

No equality in this protocol rests on a token balance. Two internal counters, written by two different contracts, are compared for equality. A balance appears only in a one-way inequality: what the contract really holds must cover what its accounting claims. So somebody who sends tokens to a branch creates a surplus — swept to the treasury by a permissionless call — and triggers nothing at all.


Rule R-3.38.1, principle P15

That is why a donation cannot kill a branch, and why a rounding wei of USDG left in the peg module is a surplus rather than a drift.

What check does

check(branch) is permissionless and, on a healthy branch, is a no-op that emits an event.

It accrues the branch first. That is the whole of what changed in this version, and it matters: between two touches the accrued-but-not-yet-materialised interest is a real difference between the minted counter and the debt view. On five million dollars of debt at 4%, one fyUSD of it appears every two and a half minutes. Comparing the two without accruing first would have closed a perfectly healthy branch inside three minutes of quiet. So the guard calls the accrual, which materialises the interest through the epoch minter, and only then compares — on the recorded debt, never on the accrued view.

Then it freezes the branch if any of these holds.

ConditionShape
The minted counter differs from the recorded debt plus bad debt, less the pending interest, by more than 1 fyUSDequality between two internal counters, read at the same instant
The branch's stock token balance is under its total collateral plus its total surplusone-way inequality on a balance
The Stability Pool's fyUSD balance is under its deposits plus its undistributed yieldthe same shape
The epoch minter is already holding pending interest on that branchalready frozen; the call is idempotent

The accrual itself cannot make the call fail: if the recomputation of the interest disagrees, the minter records the amount as pending and freezes the branch on its own, without reverting. And check reverts BranchLocked when the branch's lock is held by somebody else — a settlement running on that branch — because reading two counters mid-write proves nothing.

The lock is an address rather than a flag, and what it admits is a list, not the holder. lockHolder names whoever took it; a public write on the branch takes the lock if it is free, and when it is held it runs under it without retaking it if and only if the caller is one of the branch's own modules — the closed list fixed in the branch's constructor, MODULES — and reverts BranchLocked for anybody else. lock() itself reverts the moment the lock is held, whoever asks. A function reserved to those modules never takes the lock at all.

That is a list rather than "the holder" for a reason that showed up in the first gesture of the protocol's first day: the peg module's deposit into a Stability Pool holds the branch lock, and the pool then calls the branch's own accrual with the pool as the caller, not the peg module. Read as "the holder", that reverted; read as the list, it goes through. The pool tightens it back where it matters: under a lock somebody is holding, its own deposit and withdrawal accept only the wrapper and the peg module and revert NotWrapperOrPsm for anything else, so a transfer hook cannot reach into it mid-liquidation.

That is also what lets a settlement call the branch's accrual and its mint-freeze re-evaluation under the lock it took itself, while the guard's permissionless call — which reaches the accrual from outside — is refused for as long as that settlement runs. unlock() accepts the holder and nobody else.

The epoch minter calls the same effect directly, through haltFromMinter, whenever it finds the drift itself — before a lot, after a lot, or on an interest recomputation.

The peg module has its own check

checkPsm() tests two things and closes the module's entrance if either fails: the reserve counter must equal exactly what the module has minted, and the USDG it really holds must cover that reserve. The first is a counter against a counter; the second is the one-way inequality, and it is what catches a transfer credited twice — the counters would still match, and only the balance would say otherwise.

The exit is never closed by this, or by anything

An accounting freeze on the peg module closes minting for ever and leaves swapOut untouched. Any amount inside the outbound window converts, in every state, under every freeze, in the terminal mode included.


Rule R-8.5.1, Rule R-3.38.1

What a freeze does

The branch reports a permanent halt, its last accepted price is frozen as its display price, its interest clock stops, and borrowing, withdrawing, liquidating, flagging, setting a rate and ordinary redemption close. Repaying, adding collateral, closing, claiming a surplus, withdrawing from the Stability Pool, claiming its collateral and claiming a refunded escrow all stay open.

Urgent redemptions open 7 days later, not immediately. That delay is the same one a shutdown ordered by the key carries, and for the same reason: the exit of a branch closed for an accounting drift is exactly the door a fyUSD created by that drift would take.

The branch then counts toward the 3 shutdowns that put the whole protocol into its terminal mode. See Sunset.

One way, with no key anywhere near it

There is no function to lift an accounting freeze. The Closer key does not know this contract, is not consulted, and could not reopen a branch if it wanted to. A drift does not resolve itself, and a reopening would be a power.


Rule R-3.38.2, Rule R-12.1.7

What it costs

An accrual plus a handful of reads and one conditional write: around a hundred thousand gas, most of it the accrual. Nobody is paid for it. No bounty exists inside the protocol, and no flow of the treasury cascade is reserved for one. What exists is the event, which names the address that proved the drift — enough for a reward funded outside the protocol to identify it.

Risk

A false positive kills a branch. A branch is not repaired in place — it is redeployed, which costs a differential audit and weeks — and the positions on it go through urgent redemption and settlement instead. With a tolerance of one whole fyUSD between two contracts that count in wei, and both read immediately after the same accrual, the difference needed is not a rounding error. But the possibility is real and it has no mitigation beyond that arithmetic. Read contract and immutability risk.

There is a precedent for the shape: a lending protocol that shuts a market from a mechanical condition, one way, with no key. What is added here is that the condition is an accounting one rather than an oracle one.

Rule R-3.38.3

What follows from this page