Skip to content
Protocol

Borrowing by epoch

Every borrowing is a request now and a settlement a minute later, at the price of that minute, and the fyUSD is minted only once a second implementation has recomputed the same debt, the same ratio and the same counter.

Spec v0.15, reviewed 2026-09-09

One contract creates fyUSD against a debt, and it is not the one that holds the positions. EpochMinter is small, has no owner, no setter and no key, and does one thing: it takes a borrowing as a request, settles it at the next epoch of 60 seconds, recomputes everything through a second implementation that shares no code with Branch, and mints only if both arrive at the same numbers.

There is no rate limit on borrowing anywhere: no ceiling per hour, per day or per transaction. A branch with a five-million ceiling fills at the speed of its borrowers. What bounds a flaw is the verification, not the quantity.

Rule R-3.37, decisions D213 to D215

Why a minute of waiting buys anything

The contract that writes positions is around two thousand lines. If it could also mint, a flaw in any of those lines would be a flaw in the money supply, and the only bound would be whoever noticed first.

So the mint sits behind a contract small enough to be proved, and the recomputation is done by a different implementation of the same arithmetic. A bug that makes Branch compute a debt of one dollar on collateral worth nothing has to be present, identically, in ShadowMath as well for the mint to happen. If the two disagree by more than 1 fyUSD, nothing is minted, the whole lot is refunded, and the branch closes for good.

The cost is stated: a minute of waiting, a second transaction, a bond in ether, and the gas of a settlement.

The first step: a request

request escrows the collateral, records the request in the lot of the current epoch, takes the bond, and mints nothing. It reads no price and writes nothing into the branch.

It refuses immediately if the branch could not mint at this moment — dormant, shut down, in the terminal mode, halted, mint-frozen, under an upgrade freeze, under a Closer freeze on minting, or inside the 24 hours before a corporate action — because a request that would certainly be refused at settlement is better refused now. It also refuses a second pending request from the same address on the same branch, a debt under 1,000 fyUSD on an opening, and a rate outside the branch's own floor and ceiling.

A request carries a minPrice: the lowest reference price the borrower accepts for the valuation. Below it the request is refunded rather than settled. Zero accepts any price the ratio tolerates.

A request cannot be cancelled, and that is the point

There is no cancel. Without that, a borrower could place a request and decide a minute later, once the settlement price is known, whether to let it through — a free option on sixty seconds of the market, paid for by everybody else. The only way out of a request is its settlement, as a mint or as a refund.


One residual option is stated rather than closed: on a position that already exists, the borrower can make their own request refundable by withdrawing or closing before it settles. Closing that would mean freezing a risk-reducing withdrawal while a request is pending, which contradicts a guarantee that matters more. It costs them the bond and the gas.

Rule R-3.37.1

The second step: a settlement

From the next epoch onward, settle is callable by anybody, with or without a set of fresh ordering hints — settle(branch, fresh) and settle(branch) are the same function with and without them — and it processes up to 10 requests of the lot at the head, in arrival order. It returns two figures: how many requests it processed, and the bonds of the ones that minted.

Each fresh hint names the request it is for, by epoch and by index in that epoch. A hint that names no request of this call is ignored rather than applied to whichever request happens to sit at that rank, so two settlers racing on the same lot cannot make each other write at a wrong position; an array longer than one epoch's worth of requests reverts TooManyHints. A lot is only settleable once its epoch has ended, so every request in it sees the same price and none can be settled inside the epoch it was born in.

The order of operations is fixed.

  1. Lock and price. The branch's lock is taken, the adapter is poked, the volatility oracle sampled, the interest accrued — so a settlement always starts on a debt that is up to date — and then the price, the regime, the confidence, the haircut, the limit of the day, the three caps, the recorded debt, the minted counter, the token balance, the total collateral and the total surplus are read once.
  2. Branch guard. If the branch cannot mint, or the token balance does not cover the accounting, or the minted counter already disagrees with the debt, every request in the lot is refunded without a single mint — the whole lot, in that one call, not 10 of it at a time, because a lot handed back is handed back entire and the state that made it so can change between two calls. In the last two cases the branch is frozen on the spot.
  3. The loop. For each request, a recomputation, then the tests, then the write. Each test that fails refunds that one request with its own reason and the loop carries on. The write into Branch uses the ordering hints the settler computed for that request, and falls back to the hints the request itself carried; a rejection of the fresh ones buys one second attempt with the request's own, which is 2 bounded searches per request at most. The write sits inside its own try/catch: hints that no longer hold on the second attempt either, a token the issuer has paused, a branch state that changed since the previous write — any revert at all refunds that request, records the selector in an event, and the loop carries on. None of that is an accounting drift and none of it freezes anything.
  4. The four equalities. After the lot is written and before anything is minted: the recorded debt moved by exactly the sum of the debts and fees written; a second, position-by-position recomputation gives the same sum; the branch's token balance covers its collateral and its surplus; and the branch's total collateral moved by exactly the collateral written. Three of those compare a counter with a counter, and the fourth is a one-way inequality on a balance.
  5. The mint. Only now. Each borrower's fyUSD, then the seven days of interest to the interest router, then — after the micro-fees start — the floor of the origination fee to the treasury. The minted counter has to land on the value computed in step 1, exactly.
  6. The queue, and the bonds. The head advances. The bond of a request that was minted goes to whoever made the call, returned to them as the second value of settle; the bond of a request that was refunded — for a reason of value, for a write that reverted, or because the whole lot was handed back — goes to the keeper vault instead. Neither is ever returned to the author of a refunded request.
  7. Unlock. The mint freeze is re-evaluated once for the whole lot, and the locks are released.

If the fourth step's typed error is raised, the whole lot is refunded, the branch is frozen by the accounting guard and nothing was minted. If the batch reverts for any other reason — an out-of-gas in a sub-call, an unexpected error in the minter itself — the settlement reverts entirely, nothing is written, the lot stays at the head, and somebody retries with more gas. A revert nobody typed is never a reason to kill a branch.

Rule R-3.37.3, decisions D239 and D240

The twelve reasons a request comes back

A refund is not a revert. The collateral is credited to the borrower and claimed with claimRefund, which is never blocked, never charged, and never calls the oracle. claimRefundTo sends it to another address, which is what a borrower needs if the token itself has stopped accepting theirs.

12 reasons exist, and the event says which: the branch cannot mint; the price is under the minPrice; the resulting ratio is under the limit; the branch ceiling, the address cap or the position ceiling is reached; the rate is out of bounds; the position no longer exists; an accounting freeze; the write itself reverted; and the branch ratio rule below. One reason is reserved and never raised.

A mint can no longer create a mint freeze

A request whose write would push the branch ratio under the freeze threshold is refunded, unless the request is itself above that threshold. Since a weighted average of two ratios above the threshold is above the threshold, a lot can never take a healthy branch under it — and the freeze that used to be reachable by borrowing is now reachable only by a falling price.


The consequence is stated: on an empty branch the first borrower opens at 130% on tier 1 rather than at the grid's 125%, because there is no other position to average with.


Rule R-3.37.3, decision D239

The bond, and who settles

Each request deposits a settlement bond in ether, equal to the ceiling on what one settled request can be reimbursed (not fixed yet). Where it ends up follows the fyUSD: if the request mints, the bond goes to whoever settled it; if the request is refunded, for any of the reasons above, the bond goes to the keeper vault. It is never handed back to its author.

That makes a queue of empty requests pay for itself. Five hundred requests carrying a wei of collateral cost their author five hundred bonds plus five hundred lots of gas, and those five hundred bonds go to the keeper vault whoever clears them — the author included. The queue is still possible — a full lot always slides to the next epoch rather than reverting — but it has a price that nobody refunds.

A borrower settling their own epoch gets their own bond back if their request is minted. In the first 120 seconds that is the intended path, and the reference interface chains the two transactions. A refund keeps nothing back for them: that bond has gone to the vault. After the grace period, KeeperVault.settleAndRefund passes on the bonds of the requests that minted and reimburses the gas of the call in full, bounded per request processed. The vault has a plain receive() for exactly that: the bonds of the minted requests arrive there with no logic attached, and the vault forwards them to whoever made the call. If that forwarding fails — a caller that cannot take ether — the amount is credited inside the vault instead and pulled later with claimBond(); nothing is stranded and nothing is lost. The bonds of the refunded requests reached the vault directly, so it is never charged for a lot of empty requests.

If nobody ever settles, you settle

settle is permissionless with no condition on the caller. No keeper, no server and no watcher belonging to the authors exists, at any point, by design. If the keeper vault is empty and no third party turns up, the borrower calls settle themselves: two transactions, never blocked.


The cost is stated: the queue is first in, first out, so at worst they pay for the settlement of the requests ahead of theirs before their own — at most a handful of calls on a full lot of 50.


Rule R-3.37.6, decisions D225 and D258

What goes through an epoch, and what does not

Through an epoch: opening a position, borrowing more, and the debt-increasing part of an adjustment.

Instantaneous and unchanged: repaying, adding collateral, closing, withdrawing collateral, claiming a surplus, setting a rate, flagging, liquidating, redeeming, urgent redemption, post-shutdown settlement, every Stability Pool and wrapper operation, every vault operation, the whole peg module, the backstop, the treasury, the refund claim, and every maintenance call.

An adjustment that reduces risk and increases it at once is split: the reduction applies immediately in the branch, with no oracle and no epoch, and only the new fyUSD goes into a lot. A withdrawal of collateral mints nothing and stays instantaneous.

Rule R-3.37.2

Interest is minted the same way, and never by a branch

Interest is not batched: accrual has no price and no collateral to escrow, and deferring it would block repayment. So it goes through the same contract by a different door.

At every touch, the branch computes the pending interest and adds it to the recorded debt — the debt is correct from that instant, exactly as before — and then calls mintInterest, unless there is nothing pending, in which case it does not call it at all: two touches in one block, or a branch with no debt, mint nothing and go through the door not at all rather than through it with a zero. The minter recomputes the same amount from values the branch exposes as they were before the touch, and checks one formula bound: the pending amount cannot exceed what the debt could accrue at the maximum rate over the elapsed clock. That is not a rate limit; it is the largest accrual arithmetic allows, and a clock bug that tries to accrue years in one call does not pass it.

If the two agree, the interest is minted to the interest router. If they do not, nothing is minted, nothing reverts, the amount is recorded as pending and the branch is frozen. The operation that carried the accrual — a repayment, a deposit, a closure, a liquidation — succeeds either way, because those must never fail.

That is the one case where the fyUSD supply lags the debt, and the invariant says so: supply equals debt plus bad debt plus what the peg module minted, minus the pending interest, which is zero on every branch that is not already frozen.

mintInterest carries the interest of elapsed time and nothing else. The fee a borrower pays for changing their rate inside its cooldown has a door of its own, mintRateFee, because that fee arrives after the aggregate accrual has already consumed the clock and the formula bound above would be zero for it. The shape is the same: the minter recomputes the fee from the position as it stands before the touch, checks its own formula bound — the most the rate-change rule can produce at the maximum rate — then mints the seven days to the interest router and the floor, once the micro-fees start, to the treasury. A disagreement mints nothing, records the amount as pending, freezes the branch, and never reverts the rate change.

Both of those fees are dated, and the date is an immutable of the minter rather than something it reads: the minter carries the day the micro-fees start and the two floors as constructor constants, and finalize() refuses the deployment unless they equal what every branch, the peg module and the liquidation engine say. Two implementations of one fee can no longer disagree on a date by a second.

Rules R-3.37.5 and R-3.37.5 bis, decision D256

What happens if something moves in between

Between the request and the settlementAt settlement
The price movedThe lot is valued at the settlement price, with the haircut of the regime of that moment. A request made at full quality and settled while frozen is tested at the frozen haircut: it passes, or it is refunded
The branch haltedRefunded. A request never waits for a price to come back
The position was partially liquidated or redeemedSettled on the state of that moment, recomputed
The position was fully liquidated, closed or redeemed to zeroRefunded
The Closer froze minting, or shut the branch downRefunded. Yes, the key can close while a lot is pending: nothing is stuck and the collateral comes back
The issuer paused the tokenEach request whose escrow transfer reverts is refunded one by one. The claim itself reverts while the pause lasts and succeeds after it. The protocol reads no pause flag: the token says it, not Fyber
The terminal mode startedMinting is closed everywhere, so every pending lot is refunded
A ten-minute or opening edge is runningNothing. Those bound liquidation, not settlement

openedAt and the redemption exemption run from the settlement, not from the request. No fee is ever charged on a refunded request.

Rule R-3.37.8

Reentrancy, and the closed list of calls

Checks, effects, interactions, strictly. A per-branch lock is taken by both request and settle, so a transfer hook on a stock token cannot reenter either of them, cannot trigger a settlement, and cannot touch the branch, which is locked with its own lock as well.

No arbitrary address is called during a settlement. The list is closed: the branch, the adapter, the volatility oracle, the stablecoin, the interest router, the treasury, the guard, KeeperVault.receiveBonds for the bonds of the refunded requests, the escrow transfer into the branch, the minter's own batch function, and the bond payment to the caller — placed after every write of the lot and before the lock is released, rather than after the unlock. Refunds are pull, never push. The minter reads no balance to decide an amount: the escrow, the refunds and the amount to mint are all internal counters. A gift of stock tokens to the contract is a surplus, and a permissionless sweep sends it to the treasury, never to a user.

Rule R-3.37.7

What it costs, in figures

A settled request costs roughly 250,000 to 400,000 gas for the write and 60,000 to 120,000 more for the recomputation, so a call of 10 requests is in the range of three to five million gas — under the per-transaction limit of this chain. A full lot of 50 clears in five calls at most, and the order is preserved between them. The lot at the head has to be cleared before the next epoch of the same branch starts settling; requests behind it wait, and their price will be the price of their own settlement.

The tolerance is 1 fyUSD. Both implementations round debt upward, and their expected difference is on the order of one wei per position — at fifty positions, twenty quintillionths of the tolerance. A false freeze needs a disagreement of a whole fyUSD between two contracts that count in wei. That is not a rounding error; it is a flaw, in one implementation or the other, and in both cases the branch has to stop.

Risk

What this design does not remove is a flaw in EpochMinter itself, or the same flaw present in both implementations. Nothing bounds that except the proof work: the contract is written to be small enough for symbolic execution and invariant fuzzing before any public review, and that is the whole of the mitigation. Read contract and immutability risk.

What follows from this page