Integrate sfyUSD
A plain ERC-4626 vault per Stability Pool. What its share price means, when a redemption returns something other than fyUSD, and what you must not assume.
sfyUSD-<TICKER> is a standard ERC-4626 vault whose asset is fyUSD and whose strategy is a single Stability Pool. There is one per branch — sfyUSD-SPY, sfyUSD-QQQ, and so on. There is no aggregator across branches in the core protocol; if you want one, build it as a layer on top.
It has no management fee, no performance fee, no lock, no deposit queue and no withdrawal queue.
The standard surface
You can treat it as any other ERC-4626 vault:
function asset() external view returns (address); // fyUSD
function totalAssets() external view returns (uint256);
function convertToShares(uint256 assets) external view returns (uint256);
function convertToAssets(uint256 shares) external view returns (uint256);
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
function mint(uint256 shares, address receiver) external returns (uint256 assets);
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
function maxDeposit(address) external view returns (uint256);
function maxRedeem(address owner) external view returns (uint256);Two additions:
function redeemInKind(uint256 shares, address receiver) external; // exact pro-rata: fyUSD plus stock token
function harvest() external; // permissionless, also implicit in every deposit and redeemPrice per share
There is no pricePerShare() function; use the ERC-4626 primitive:
uint256 pricePerShare = vault.convertToAssets(1e18); // fyUSD per share, WADThis is the number your integration should track. It only goes up in normal operation — yield compounds into it — and it goes down when the pool takes a loss absorbing an underwater liquidation.
totalAssets() is the sum of three things (Rule R-9.5.1):
How it is calculated
totalAssets() = SP.getDeposit(wrapper) + pending fyUSD yield + CollateralSale inventory × p_val,
where p_val = min(view.pRef, view.pLiq) × (1 − discount) and the
discount equals the tier's base liquidation bonus — 5% for Tier 1, 7% for Tier 2.
The discount on inventory is deliberate. It means the NAV understates the collateral the vault is holding, so a redemption during a sale never takes value from the shareholders who stay. This is a fuzzed invariant: totalAssets() never exceeds the undiscounted sum (Rule R-16.3 item 23).
Do not compute a yield from pricePerShare over a short window. A liquidation moves it down and then a sale moves it back up over the following hours. The realised figure the protocol publishes uses a 30-day window:
How it is calculated
realised yield = (pricePerShare_t / pricePerShare_{t−30d} − 1) × 365 / 30
Collateral claims
The wrapper is a single depositor in the Stability Pool. When the pool absorbs a liquidation, the wrapper receives stock token like any depositor. It does not hold it: harvest() hands it to CollateralSale, which sells it for fyUSD at a discount ramping from 0 to 3% over six hours and then holding at 3%, during live sessions only, limited to 1.0 × d2_eff per hour and 0.25 × d2_eff per transaction. The proceeds are redeposited (Rule R-9.5.3, R-9.5.4).
harvest() is permissionless and also runs at the head of every deposit, withdrawal and redemption, so an integration never needs to call it explicitly. It is not rewarded.
To see how much collateral is currently in flight:
uint256 inFlight = collateralSale.inventory(); // in stock token
uint256 currentDiscount = collateralSale.discount(); // WAD, 0 to 3%A front end should show this as a fraction of NAV, with the current discount — for example, "3% of assets in SPY being sold, current discount 1.2%" (Rule R-15.1.9).
When redemption returns something other than fyUSD
This is the one place where the vault departs from what a naive integration expects (Rule R-9.5.6):
- While the
CollateralSaleinventory is at most 20% oftotalAssets(),redeemandwithdrawdeliver fyUSD only. - Above 20%, the excess portion is delivered in kind, pro rata: fyUSD plus stock token, valued at
p_val. redeemInKind(shares, receiver)always delivers the exact pro-rata mixture, for anyone who wants the collateral.
Guarantee
Redemption is never blocked. In the worst case it delivers a mixture rather than fyUSD alone, but it always delivers, in every market state and under every freeze (Rule R-4.4.1).
If your integration cannot handle receiving an ERC-20 it did not expect, check the inventory ratio before redeeming, or route through Router.withdrawToUSDG and handle the residue.
USDG in and out, in one transaction
router.depositUSDG(branch, usdgAmount, receiver, minShares);
router.withdrawToUSDG(branch, shares, receiver, minUsdg);depositUSDG runs PSM.swapIn at 0.05% and then deposit. It reverts when the PSM is at its 30% intake cap; there is no DEX fallback and no hidden slippage. Check psm.intakeCapacity() first.
withdrawToUSDG runs redeem and then PSM.swapOut at 0.20%. If the reserve is short, it converts what it can and returns the remaining fyUSD to the receiver, along with any stock token from an in-kind delivery. It never reverts for lack of reserve.
The round trip costs 0.25%. The router is stateless, takes no fee of its own, and has no role.
Caps
uint256 branchCap = pool.cap(); // max(500k, 1.0 × branch debt)
uint256 addressCap = pool.capPerAddress(); // 100k until day 90, unlimited afterThe wrapper is exempt from the per-address pool cap, but until day 90 an equivalent 100 000 USD limit is applied at the share level by the same dated formula (Rule R-9.5.2). maxDeposit reflects whichever binds.
Compounded yield can carry the pool above its cap; only new deposits are refused, never withdrawals.
Same-block guard
provide and withdraw on the pool — and therefore deposit, withdraw and redeem on the wrapper — revert if called in the same block as an offset on that pool (Rule R-9.2.4, R-9.5.5). Retry in the next block. On a chain with sub-second blocks this is not a practical constraint, but a contract that must not revert should handle it.
Dormant branches
Three of the five wrappers belong to branches that are dormant at genesis. They accept deposits, issue shares, and are fully functional — but the underlying pool earns nothing until the branch activates, because there are no borrowers. pricePerShare sits flat at one.
Check branch.isActive() before presenting a dormant wrapper as a yield product, and branch.canActivate() to show what is still missing (Rule R-12.3.3).
What not to assume
Risk
The share price can fall. When the pool absorbs a position whose collateral is worth less than its debt plus the bonus, depositors take the shortfall. Below 100% collateralisation, that is an outright loss. This is the risk the yield pays for, and no mechanism absorbs it before the depositors do.
- Do not treat the yield as a rate. It is
pool share × average borrower rate × debt / pool, and every input moves. What sits underneath it is interest borrowers actually paid, and nothing is promised. - Do not assume the vault holds only fyUSD. It holds a stock token whenever a liquidation has just happened.
- Do not assume liquidity. A withdrawal is always possible, but the fyUSD you receive comes from the pool's balance; if a large fraction of assets is mid-sale, part of your redemption arrives in kind.
- Do not aggregate the five wrappers as if they were one asset. They carry different collateral, different tiers, different thresholds and different depths. A meta-vault across them is a third-party layer and it inherits every branch's risk (Rule R-2.2.1).
- Do not expect an upgrade. The vault is immutable. If it is wrong, it stays wrong, and a corrected one is a different address in a different version.
Listing checklist
If you are listing sfyUSD as a yield product, you should be able to show, from on-chain reads alone: the share price and its 30-day change; the fraction of NAV currently in collateral being sold and the current discount; the underlying branch, its tier and its activation status; the pool cap and remaining capacity; the branch's debt-to-pool ratio, which is the main driver of the yield; and a plain statement that the yield is paid by borrowers on that branch and is not promised by anybody.
Last reviewed: 2026-09-07 · Spec v0.4