Skip to content
Protocol

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.

ModuleInstancesConstantsResponsibility
FYUSD1minters at constructorStablecoin, six frozen minters
MarketCalendar1 (pure, shared)NYSE + DST rules, exception table frozen to 2036-12-31Sessions, holidays, half days, and weighted market time
OracleAdapter1 per branchfeed proxy, token, thresholds, deviation boundsValidated price and market state
LiquidityOracle1 per branchreference Uniswap poolsOn-chain d2, 7-day buffer, ratchet
RateFloor1reference Morpho market, k and fixed floors per tierIndexed rate floor per tier
Branch1 per stock token (5)tier, thresholds, C_abs tiers, activation criteriaPositions, interest clock, mint and burn, per-state rules, shutdown
SortedTroves1 per branchRate-ordered list for redemptions
StabilityPool1 per branchcap formulaLiquidation absorption, product-sum accounting
LiquidationEngine1 + per-branch flag and bucket statebonus, buckets, delaysFlag, liquidate, open sale, hourly throughput
RedemptionRouter1fee floor, half-lifeCross-branch routing, baseRate, urgent redemptions
InterestRouter1recipients at constructor, shares by formulaSplit between pool, treasury, incentives, backstop
Backstop1fyUSD reserve for bad debt, permissionless redistribution
PoolIncentive1at most 2 canonical poolsDistributes the incentive share to in-range liquidity providers
PSM1fees, capShareUSDG to fyUSD gate, repayWithUSDG, Sunset
BranchRegistry110 immutable slots (5 filled, 5 empty forever)Branch list, activation, Sunset, aggregate TCR
sfyUSD1 per pool (5)ERC-4626 wrapper, NAV including collateral being sold
CollateralSale1 per wrapper (5)max discount, ramp, bucketSells the wrapper's seized collateral for fyUSD
Router1USDG to sfyUSD in one transaction, no fee of its own
Closer1Safe address, EXPIRES_ATBounded 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 rule

The 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;                        // onlyCloser

Views: 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 pools

No 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;                   // onlyCloser

Sunset 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 inventory

No 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