Modules
The nineteen immutable contracts of Fyber — responsibility, key function signatures, and the invariants each one holds.
Every module is immutable: no proxy, no owner, no administration role, no selfdestruct, no migration function. The only access modifiers are inter-module (onlyBranch, onlyLiquidationEngine, onlyInterestRouter, onlyWrapper, onlyRegistry) and onlyCloser on the closing functions (Rule R-3.0.1, Rule R-12.2.1).
The "Constants" column of the summary says how each module's parameters are set: at construction, by formula read on-chain, or as a dated tier.
| Module | Instances | Constants | Responsibility |
|---|---|---|---|
FYUSD | 1 | minters at constructor | Stablecoin, six frozen minters |
MarketCalendar | 1 (pure, shared) | NYSE + DST rules, exception table frozen to 2036-12-31 | Sessions, holidays, half days, and weighted market time |
OracleAdapter | 1 per branch | feed proxy, token, thresholds, deviation bounds | Validated price and market state |
LiquidityOracle | 1 per branch | reference Uniswap pools | On-chain d2, 7-day buffer, ratchet |
RateFloor | 1 | reference Morpho market, k and fixed floors per tier | Indexed rate floor per tier |
Branch | 1 per stock token (5) | tier, thresholds, C_abs tiers, activation criteria | Positions, interest clock, mint and burn, per-state rules, shutdown |
SortedTroves | 1 per branch | — | Rate-ordered list for redemptions |
StabilityPool | 1 per branch | cap formula | Liquidation absorption, product-sum accounting |
LiquidationEngine | 1 + per-branch flag and bucket state | bonus, buckets, delays | Flag, liquidate, open sale, hourly throughput |
RedemptionRouter | 1 | fee floor, half-life | Cross-branch routing, baseRate, urgent redemptions |
InterestRouter | 1 | recipients at constructor, shares by formula | Split between pool, treasury, incentives, backstop |
Backstop | 1 | — | fyUSD reserve for bad debt, permissionless redistribution |
PoolIncentive | 1 | at most 2 canonical pools | Distributes the incentive share to in-range liquidity providers |
PSM | 1 | fees, capShare | USDG to fyUSD gate, repayWithUSDG, Sunset |
BranchRegistry | 1 | 10 immutable slots (5 filled, 5 empty forever) | Branch list, activation, Sunset, aggregate TCR |
sfyUSD | 1 per pool (5) | — | ERC-4626 wrapper, NAV including collateral being sold |
CollateralSale | 1 per wrapper (5) | max discount, ramp, bucket | Sells the wrapper's seized collateral for fyUSD |
Router | 1 | — | USDG to sfyUSD in one transaction, no fee of its own |
Closer | 1 | Safe address, EXPIRES_AT | Bounded freezes, shutdown, Sunset, renunciation |
Removed relative to earlier designs: Guardian, three TimelockController instances, DepthFeed (a keeper quorum), RateManagerRegistry, and every set*, add*, remove*, delist, sweep* and acknowledge* function on every module.
FYUSD
The stablecoin. ERC-20, 18 decimals, ERC20Permit (EIP-2612), name "Fyber USD", symbol fyUSD. No rebase, no transfer fee, no blocklist, no flash mint, no omnichain extension, no owner (Rule R-2.1.1).
constructor(string name, string symbol, address[6] minters); // 5 branches + PSM; reverts on duplicate or zero
function mint(address to, uint256 amount) external; // onlyMinter; reverts NotMinter
function burn(uint256 amount) external; // burns msg.sender
function burnFrom(address from, uint256 amount) external; // standard allowance
function minted(address) external view returns (uint256);
function isMinter(address) external view returns (bool);There is no MinterSet event, because the list is in the bytecode. Each minter's per-minter cap is its own ceiling: D_max(t) for a branch, capShare for the PSM. A dormant branch is a minter from day zero but its ceiling is zero until activation.
Invariants (Rule R-2.1.3). totalSupply == Σ getEntireDebt + Σ badDebt + minted[PSM] within n wei, n being the number of accruals; minted[branch] == getEntireDebt + badDebt for each branch; no transfer or burn without a standard allowance; the minter set equals exactly the deployed bytecode's set.
MarketCalendar
A pure, stateless, shared contract that answers three questions about a timestamp: which session, how much weighted market time separates two instants, is it a holiday or half day.
function sessionAt(uint64 ts) external view returns (Session, uint64 sessionStart, uint64 nextBoundary);
function marketTime(uint64 t0, uint64 t1) external view returns (uint256);
function isHalfDay(uint32 yyyymmdd) external view returns (bool);
function isHoliday(uint32 yyyymmdd) external view returns (bool);
function etOffset(uint64 ts) external view returns (int32); // -4h or -5h by DST ruleThe calendar is algorithmic, not a table of epochs with an expiry (Rule R-4.3.1): DST by the US rule in force since 2007, holidays by rule including Good Friday from the anonymous Gregorian algorithm, plus a frozen exception table of roughly forty entries covering announced half days and exceptional closures through 2036-12-31. After 2036 the rules keep working; only unlisted half days are missing, and their absence degrades rather than breaks — the feed goes quiet at 13:00 ET and the branch reads DRIFT a few hours later, at a haircut of a fraction of a point.
The calendar announces, the feed confirms: no permission depends on the calendar alone.
OracleAdapter
One per branch. Reads exactly one Chainlink proxy address, fixed at construction, and returns a full quote. See Oracle for the validation chain.
function quote() external view returns (Quote memory);
function poke() external; // permissionless snapshot refresh
function markShutdown(uint256 lastGood) external; // onlyBranch; DELISTED, final
function longestDegraded() external view returns (uint64);
function deployedAt() external view returns (uint64);The PriceView struct carries pOfficial, pDex, dDex, dispBps, uBps, confBps, bandBps, the three execution prices pRef, pLiq and pRedeem, the regime and session enums, plus preAction, sessionEdge, rampEndsAt and degradedSince.
Invariants (Rule R-3.2.11). pLiq ≤ pRef ≤ pOfficial ≤ pRedeem; the band never exceeds BAND_MAX; confidence is monotone when a source disappears; degradedSince != 0 exactly when the regime is BLIND; pRef never depends on caller-modifiable state; no adapter function writes a constant.
LiquidityOracle
One per branch. Measures d2 — the depth of the token against USDG at 2% slippage — directly on the reference Uniswap pools whose addresses are immutable, and maintains a 168-slot hourly buffer.
function poke() external; // permissionless; fills the current hourly slot if empty
function d2Now() external view returns (uint256);
function d2Eff() external view returns (uint256);
function d2P25() external view returns (uint256);
function bufferFill() external view returns (uint16);
function poolDepths() external view returns (uint256[] memory);Uniswap v2 depth is a closed formula over the reserves; v3 uses slot0 plus iteration over initialised ticks through tickBitmap across roughly ±198 ticks; v4 uses StateView. The result is converted to USD at the oracle's last regular close, never at the DEX spot. Details and the ratchet rules: Caps and liquidity.
Invariant (fuzzed, Rule R-16.3 item 20). d2_eff can only fall immediately; any rise is at most 20% per 24 hours and never within 24 hours of the first observed rise of the 25th percentile.
RateFloor
One global contract that turns a reference borrow rate into a per-tier interest rate floor.
function poke() external; // permissionless daily sample
function rRef() external view returns (uint256); // 30-day TWA clamped to [2%, 6%]; 0 if unavailable
function floor(uint8 tier) external view returns (uint256);// max(floorFixed[tier], k[tier] * rRef())The reference is a Morpho Blue market whose loan token is USDG, chosen seven days before deployment as the one with the largest borrowed assets, then frozen. The reference is treated as unavailable — and rRef returns zero, so the fixed floors apply — if the call reverts, if the market's supplied assets fall below 10 M USD, if its last update is more than 30 days old, or if fewer than 15 of the 30 daily slots are filled (Rule R-5.7.2).
Failure is benign in both directions: an unavailable reference lowers the floor to its fixed value, and pushing the reference market's utilisation for thirty days only raises a bounded floor, at real capital cost, with no gain to the attacker.
Branch
The core contract. One per stock token, five deployed at genesis, two active. It owns positions, the interest clock, minting and burning, per-state permissions, the mint freeze, activation and shutdown.
function open(uint256 coll, uint256 debt, uint256 annualRate, Hints calldata h) external;
function adjust(int256 collDelta, int256 debtDelta, Hints calldata h) external;
function addCollateral(address onBehalfOf, uint256 amount) external; // never blocked, no oracle call
function repay(address onBehalfOf, uint256 amount) external; // never blocked, no oracle call
function withdraw(uint256 amount, address to) external;
function borrow(uint256 amount, address to) external;
function close() external;
function setRate(uint256 annualRate, Hints calldata h) external;
function setRateFor(address user, uint256 annualRate, Hints calldata h) external; // onlyDelegate(user)
function setDelegate(address manager, uint256 minRate, uint256 maxRate) external;
function clearDelegate() external;
function claimSurplus() external;
function accrue() external; // permissionless
function updateMintFreeze() external; // permissionless
function activate() external; // permissionless when criteria are met
function flagShutdown() external; // permissionless when TCR < SCR
function shutdown() external; // permissionless when conditions hold
function settleAfterShutdown(address user) external; // permissionless 30 days after urgentFrom
function checkUpgrade() external; // permissionless token-implementation check
function forceShutdown() external; // onlyCloser
function freeze(uint8 mask, uint64 until) external; // onlyCloser
function freezeLiquidation(uint64 until) external; // onlyCloser
function unfreeze() external; // onlyCloserViews: getDebt, getICR, getTCR, getEntireDebt, debtCeiling, cAbs, addressCap, isActive, canActivate.
Typed reverts include StateForbids(op, state), ICRBelowMint, TCRBelowCCR, DebtBelowMin, CeilingExceeded, AddressCapExceeded, PositionMaxExceeded, SameBlockTouch, RateOutOfBounds(floor, max), NotDelegate, Shutdown, NotActivated, ActivationCriterionFailed(uint8), FrozenByCloser(op, until), UpgradeFreeze(until) and CloserExpired.
Invariants (Rule R-3.3.2). The sum of per-position collateral equals totalColl, and totalColl plus unclaimed surplus plus unclaimed pool gains never exceeds the branch's token balance; the sum of position debts equals getEntireDebt() within n wei; a position's debt is zero, at least minDebt, or flagged as reduced; after any voluntary owner operation the position's ICR is at least the mint threshold of the current state; getEntireDebt() never exceeds debtCeiling() after an operation that increases debt; FYUSD.minted[branch] == getEntireDebt() + badDebt; no function reduces a position's collateral except withdrawal or closure by its owner, liquidation, redemption, or post-shutdown settlement; a dormant branch has zero debt; badDebtCumul is monotone; cAbs() is non-decreasing while badDebtCumul is zero and frozen at the reached tier afterwards; interestClock is monotone and never exceeds elapsed time since deployment.
SortedTroves
One per branch. A doubly linked list ordered by ascending annualRate, ties broken by insertion order, with hint-based insertion so that gas stays bounded.
function insert(address user, uint256 rate, address prevHint, address nextHint) external;
function remove(address user) external;
function reInsert(address user, uint256 rate, address prevHint, address nextHint) external;
function getFirst() external view returns (address);
function getNext(address) external view returns (address);
function findInsertPosition(uint256 rate, address prevHint, address nextHint) external view returns (address, address);Reduced positions — those pushed below minDebt by a redemption — are removed from the list and reinserted when their debt returns to minDebt (Rule R-7.4.3). The rate floor never reorders the list (Rule R-5.7.3).
Invariant. The list is sorted and its length equals the number of non-reduced positions.
StabilityPool
One per branch. Absorbs liquidated debt against seized collateral, using Liquity-style product-sum accounting with epochs and scales.
function provide(uint256 amount) external;
function withdraw(uint256 amount) external; // never pausable
function claimCollateral() external;
function offset(uint256 debt, uint256 coll) external; // onlyLiquidationEngine or onlyBranch
function triggerYield(uint256 amount) external; // onlyInterestRouter
function getDeposit(address) external view returns (uint256);
function getCollateralGain(address) external view returns (uint256);
function totalDeposits() external view returns (uint256);
function cap() external view returns (uint256); // max(spSeed, 1.0 * branch.getEntireDebt())
function capPerAddress() external view returns (uint256);Mechanics and the wrapper on top of it: Stability Pool.
Invariants (Rule R-9.2.6). fyUSD.balanceOf(pool) is at least totalDeposits plus pending yield; the pool's token balance is at least the sum of unclaimed collateral gains; an offset reduces branch debt and branch collateral by exactly the amounts burned and transferred.
LiquidationEngine
Stateless with respect to parameters, onlyBranch for its effects, with per-branch flag and throughput-bucket state.
function flag(address branch, address user) external;
function liquidate(address branch, address user, uint256 maxDebtToRepay)
external returns (uint256 debtRepaid, uint256 collSeized);
function bucketAvailable(address branch) external view returns (uint256);
function previewLiquidation(address branch, address user)
external view returns (uint256 debtLiq, uint256 coll, uint256 bonus, uint256 price, uint8 mode);Reverts: NotFlagged, FlagTooRecent, FlagExpired, NotLiquidatable(icr, threshold), LiqForbidden(state), BucketEmpty, DropNotConfirmed, FrozenByCloser. Full mechanics: Liquidations.
RedemptionRouter
One instance. Routes a redemption across eligible branches in proportion to uncovered debt, walks each branch's sorted list, and maintains the decaying baseRate.
function redeem(uint256 amount, uint256 maxIterationsPerBranch, uint256[] calldata minCollOut, uint256 maxFeeWad)
external returns (uint256 burned);
function urgentRedeem(address branch, uint256 amount, address[] calldata users, uint256 minCollOut) external;
function baseRate() external view returns (uint256);
function getRedemptionFee(uint256 amount) external view returns (uint256 feeWad);
function eligibleBranches() external view returns (address[] memory, uint256[] memory unbackedDebt);Invariant (Rule R-7.7.1). For burned fyUSD burned, total debt falls by exactly burned; each sorted list keeps its ordering; no position below the open-market liquidation threshold is ever touched.
InterestRouter
Receives every fyUSD minted outside principal — accrued interest, mint fees, rate-change fees, PSM fees — and splits it in a single call.
constructor(address fyusd, address registry, address psm, address endowment, address poolIncentive, address backstop);
function route(uint256 amount) external; // onlyBranch or onlyPSM; fyUSD already minted to the router
function latchFeeSwitch() external; // permissionless, one-way, when total debt exceeds 25 M
function shares() external view returns (uint256 sp, uint256 treasury, uint256 pil, uint256 backstop);Recipients are immutable, shares are a formula of protocol state. There is no setter. Details: Interest distribution.
Invariant (Rule R-16.3 item 24). The four shares sum to one at every route; the treasury share never exceeds 20%; the backstop share is non-zero exactly when the backstop balance is below 2% of total debt.
Backstop
A fyUSD reserve that absorbs bad debt before it reaches remaining borrowers.
function fund(uint256 amount) external; // anyone
function coverBadDebt(address branch, uint256 amount) external; // permissionless; burns fyUSD, reduces badDebt
function redistribute(address branch) external; // permissionless, last resort
function receiveCollateral(address branch, uint256 amount) external; // onlyLiquidationEngine
function sellCollateral(address branch, uint256 amount, uint256 minFy) external; // permissionless, oracle price minus 3%Funding is by formula, not by decision: 5 percentage points of every routed amount while the balance is below 2% of total debt, taken from the pool share in regime 1 and from the treasury share in regime 2 (Rule R-10.8). Redistribution is permissionless once bad debt has stood for 72 hours with a backstop below minDebt, and is confined to the branch that produced it (Rule R-6.8.4).
PoolIncentive
An immutable contract that pays the incentive share to in-range liquidity providers on at most two canonical pools whose addresses are set at construction: a fyUSD/USDG pool at ±0.5% is mandatory; a rate-provider sfyUSD pool is optional at genesis and otherwise belongs to a future version.
function stake(uint256 tokenId) external;
function unstake(uint256 tokenId) external;
function checkpoint() external; // permissionless; accumulates in-range liquidity times time
function claim() external;
function purge() external; // after 180 days with no staked position, routes the balance to active poolsNo fyUSD/ETH pool ever (Rule R-10.5).
PSM
The USDG gate. Bounds the fyUSD price in both directions and provides the exit that does not depend on a DEX.
function swapIn(uint256 usdgAmount, uint256 minFyOut) external returns (uint256);
function swapOut(uint256 fyAmount, uint256 minUsdgOut) external returns (uint256);
function repayWithUSDG(address branch, address onBehalfOf, uint256 usdgAmount, uint256 minDebtRepaid) external;
function swapInToSP(address branch, uint256 usdgAmount, uint256 minDeposit) external;
function reserve() external view returns (uint256);
function intakeCapacity() external view returns (uint256);
function feeIn() external view returns (uint256); // 0.05%; 0 in Sunset
function feeOut() external view returns (uint256); // 0.20%; 0 in Sunset
function capShare() external view returns (uint256); // 30%; 100% in Sunset
function freezeIntake(uint64 until) external; // onlyCloser
function unfreezeIntake() external; // onlyCloserSunset is read from BranchRegistry.sunsetAt(); the PSM has no enterSunset of its own. Details: PSM.
Invariant (Rule R-8.5.1). reserve == FYUSD.minted[PSM] up to rounding, in every mode, because each fee is minted in fyUSD against USDG that stays in reserve.
BranchRegistry
Ten immutable slots, five filled at genesis, five empty forever in version 1.
constructor(address[10] slots, address closer); // 5 branches, 5 zeros; never modified
function branches() external view returns (address[] memory); // activated branches only
function allBranches() external view returns (address[5] memory);
function isBranch(address) external view returns (bool);
function aggregateTCR() external view returns (uint256);
function flagSunset() external; // permissionless when conditions hold
function enterSunset() external; // permissionless one hour later, or onlyCloser
function sunsetAt() external view returns (uint64);Invariants. No two branches share a token, asserted at construction; a branch is never removed, because the function does not exist; sunsetAt is one-way.
sfyUSD
One ERC-4626 wrapper per Stability Pool, deployed at genesis for all five branches, symbol sfyUSD-<TICKER>. The underlying asset is fyUSD. Yield compounds into the share price; the wrapper's share of seized collateral is sold by CollateralSale and redeposited, so a shareholder never handles a stock token unless they ask for one.
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
function redeemInKind(uint256 shares, address receiver) external; // exact pro-rata: fyUSD plus tokens
function harvest() external; // permissionless
function totalAssets() external view returns (uint256); // NAV with a discount on inventoryNo management fee, no performance fee, no lock (Rule R-2.2.1, R-9.5.1).
Invariant (Rule R-16.3 item 23). totalAssets() never exceeds the pool deposit plus pending yield plus inventory valued at the oracle price, so the discount can never overstate the NAV; redeemInKind delivers exactly the pro-rata share.
CollateralSale
One per wrapper. Sells the wrapper's seized collateral for fyUSD at an increasing discount, with no DEX fallback.
function start(uint256 amount) external; // onlyWrapper
function buy(uint256 collAmount, uint256 maxFyIn) external; // permissionless, open market only
function discount() external view returns (uint256); // 0 to 3% linearly over 6 hours, then 3%
function inventory() external view returns (uint256);A new contribution to a non-empty lot does not restart the ramp. Sales are limited to 1.0 × d2_eff per hour in a bucket distinct from the liquidation bucket, and 0.25 × d2_eff per transaction. The proceeds are supply-neutral: no selling pressure on the token beyond the buyer's own (Rule R-9.5.4).
Router
Stateless, feeless, roleless. Turns USDG into wrapper shares and back in a single transaction.
function depositUSDG(address branch, uint256 usdg, address receiver, uint256 minShares)
external returns (uint256 shares);
function withdrawToUSDG(address branch, uint256 shares, address receiver, uint256 minUsdg)
external returns (uint256 usdg, uint256 fyLeft);depositUSDG reverts with a clear message when the PSM is at its cap; there is no DEX fallback. withdrawToUSDG converts what the reserve allows and returns the remaining fyUSD to the receiver, so the exit is never blocked (Rule R-9.6).
Closer
The only human power in the system, and the only one that expires.
constructor(address safe, address registry, address psm, uint64 expiresAt); // DEPLOY_TS + 365 days
function freeze(address branchOrAll, uint8 mask, uint64 duration) external; // mask subset of MINT, WITHDRAW, REDEEM
function freezeLiquidation(address branch, uint64 duration) external;
function unfreeze(address branchOrAll) external;
function shutdown(address branch) external; // irreversible
function shutdownAll() external; // irreversible, equals Sunset
function renounce() external; // sets expiresAt to now
function expiresAt() external view returns (uint64);
function isAlive() external view returns (bool);Every function reverts CloserExpired once the expiry has passed. Bounds, doctrine and what the key can never do: Immutability and the Closer.
Last reviewed: 2026-09-07 · Spec v0.4
Architecture
How the nineteen immutable modules of Fyber fit together, what each layer owns, and which invariants hold the system in one piece.
Price regimes
Three regimes replace the open/closed state machine. How confidence is computed, what every rule does with it, and what still counts as having no price at all.