Bonds
On this page
A bond exchanges an accepted reserve asset for NOTE that vests over time. Check the current price, vesting term and available market capacity before depositing.
Summary
fact BondDepository runs OHM-v2-style bond markets. Each market accepts one asset kind, has a payout capacity, a maxDebt, a vestingSeconds, a controlVariable and a minPriceWad. Purchases mint NOTE into a linearly vesting per-user position; the asset paid goes to the Treasury.
| Accepted asset | Valuation |
|---|---|
| USDG | Par, normalised to 18-decimal WAD |
| Whitelisted Stock Tokens | Treasury feed value, live prints only: while the market is shut and the Treasury values the token at the adapter's reference close (Treasury.isReferencePriced(token)), deposit reverts QuoteNotLive(token), the same rule COUPON-leg markets apply to a stale print |
| COUPON legs (ERC-1155) | Par (1 USDG per unit), only while the series is Live and the barrier is holding |
Price
fact Price Calculation
Buying adds payout to the market’s debt, which raises the price for the next buyer; debt decays over the vesting period, lowering the price again. This is the usual bond-market feedback: heavy demand pushes price up towards market, quiet periods let it fall towards the discount governance intended.
Utilisation Discount
The discount links NOTE emission to the Desk’s need for capital. When the Desk has committed a large share of its COUPON caps (high utilisationBps), bonds get cheaper, raising reserves that fund the Desk.
When the Desk is idle the discount is zero, so NOTE is not sold cheaply for no reason. utilisationK is the slope (bps of bps) and maxDiscountBps the cap (hard maximum MAX_DISCOUNT_CAP_BPS = 5,000). DeployToken.s.sol sets k = 2,000 and maxDiscountBps = 1,500. Setting desk to the zero address disables the discount. The discount never takes marketPrice below minPriceWad.
Worked Example
illustrative Market 0 accepts USDG. controlVariable = 8.0e18, minPriceWad = 0.40e18, vestingSeconds = 5 days. NOTE total supply 10,000,000; current debt 300,000 NOTE. Desk utilisation 60% (6,000 bps), k = 500, maxDiscountBps = 2,000.
| Step | Calculation | Result |
|---|---|---|
| debtRatio | 300,000 / 10,000,000 | 0.03 |
| basePrice | max(8.0 × 0.03, 0.40) | 0.40 (floored) |
| discount | min(6,000 × 500 / 10,000, 2,000) | 300 bps |
| marketPrice | max(0.40 × (1 − 0.03), 0.40) | 0.40 USDG per NOTE (the discount cannot take the price below priceFloorWad, here equal to minPriceWad) |
| payout for 10,000 USDG | 10,000 / 0.40 | 25,000 NOTE, vesting linearly over 5 days |
| new debt | 300,000 + 25,000 | 325,000 → debtRatio 0.0325, basePrice 0.40 (still floored) |
With controlVariable = 16.0e18 instead, basePrice would be 0.52, and the 300 bps discount would price the bond at 0.5044. If the reference market price of NOTE is 0.55, that is an 8.3% discount to market.
If the market price is 0.35, the bond is more expensive than the market and nobody buys. minPriceWad is set near the Treasury floor, and the effective floor is the higher of minPriceWad and the current Treasury redemption value per NOTE, so bonds never sell NOTE below reserve value even when the floor has risen since the market was created.
Vesting and Redemption
deposit(id, amount, maxPriceWad, recipient) transfers the quote asset to the Treasury, mints payout NOTE to the depository and opens a Bond{marketId, payout, claimed, vestStart, vestEnd} for recipient (Bonded(id, depositor, recipient, amount, payout, priceWad, bondIndex)). It reverts with PriceTooHigh(price, maxPrice) if the price moved above the caller’s limit, ExceedsCapacity, ExceedsMaxDebt, MarketInactive or MarketConcluded. For ERC-20 markets the Treasury’s balance change is compared with amount and a mismatch reverts TransferShortfall(expected, received).
Every deposit also updates the smoothed utilisation (below). depositWithLimits(id, amount, maxPriceWad, minPayout, deadline, recipient) is the same deposit with two further guards for a transaction that waits in the mempool: it reverts Expired(deadline) once block.timestamp > deadline and PayoutTooLow(payout, minPayout) when the NOTE payout computed at execution, after the price, the utilisation discount and the leg valuation have moved, is below minPayout. deposit behaves as depositWithLimits with minPayout = 0 and no deadline. redeem(uint256[] indexes) pays the vested-but-unclaimed NOTE on each listed position (pendingPayout(user, index)) and emits Redeemed(recipient, bondIndex, amount); the rest remains.
Multiple positions per user are supported. The NOTE minted is bounded by the market capacity, the maxDebt, and the NOTE epoch cap, whichever binds first. closeMarket(id) sets active = false and capacity = 0.
COUPON-leg Bonds
A COUPON holder may bond units of a Live series with the barrier holding.
The Treasury registers the series (LegSeriesRegistered) and thereafter values the units at its live mark: par once the series has autocalled or matured unbreached, a mark-down towards liveMark / s0 while the barrier holds but the price sits within legBufferBps (default 15%) of it, min(1, liveMark / s0) after a live breach, and min(1, lastPrice / s0) when matured breached; it harvests the coupons (SeriesHarvested, whose quote and stock figures are the reserve’s measured balance deltas around the core call, not the core’s reported amounts) and the unwound principal when a series is unwound.
The Treasury accepts legs of one series only up to maxLegShareBps (default 50%) of that series’ matched notional (ExceedsLegShare). This converts a live note position into vesting NOTE and lets the Treasury accumulate yield-bearing reserves.
Each purchase extends the decay horizon to the size-weighted average of the residual debt’s remaining time and the new payout’s full vestingSeconds, so dust purchases barely move it while a purchase the size of the outstanding debt moves it about halfway to a full vesting period. debtDecayEnd(id) exposes the horizon; a market that has never been bought since the change decays over vestingSeconds from lastDecay.
Markets quoted in a stock token are closed while Treasury.isReferencePriced(quoteToken) is true (no print under maxFeedStaleness): deposit, payoutFor and quoteValueWad revert QuoteNotLive(quoteToken). USDG and COUPON-leg quoted markets are not affected.
Governance Surface
| Function | Effect |
|---|---|
createMarket(MarketParams) | New market (MarketCreated) |
closeMarket(id) | Sets capacity to zero (MarketClosed) |
setControlVariable(id, cv) | Tunes price slope (ControlVariableUpdated) |
setDiscountParams(k, maxDiscountBps) | Utilisation discount (DiscountParamsUpdated) |
setDesk(addr) | Utilisation source (DeskUpdated) |
pokeUtilisation() is permissionless and only refreshes the smoothed utilisation (UtilisationUpdated(snapshotBps, averageBps)).
Risk Notes
- Bonds dilute existing holders by design. The epoch cap bounds the pace.
- A mispriced
controlVariablecan sell NOTE too cheaply for one vesting period; the Treasury still receives full value at feed prices, so the floor rises even then. - Stock Token bonds add Stock Token issuer and oracle risk to the Treasury. See Issuer risk.
- Market-creation checklist for stock markets. Outside US trading hours a Stock Token’s live print ages past
maxFeedStalenessand the Treasury falls back to the last official close, which can be up to seven days old over a holiday weekend. Bond purchases are refused in that state (QuoteNotLive), butcapacityandmaxDebtshould still be sized as the overnight exposure of the market: the value the Treasury may take in at the last close before the next session can reprice it.purchaseStockon the Treasury is unchanged; it is Desk-only and reached through a governance call.
Floors and Lags
fact createMarket rejects a minPriceWad below redemptionFloorWad(), the Treasury’s current redemption value of one NOTE (MinPriceBelowFloor), so bonds cannot mint NOTE for less than the Treasury would pay to buy it back. marketPrice applies the same rule on every quote: the effective floor priceFloorWad(id) = max(minPriceWad, redemptionFloorWad()) is re-read at the time of the read, so a redemption value that has risen since the market was created, or a later setControlVariable / setDiscountParams, cannot produce a price below what the Treasury pays to redeem the NOTE sold. redemptionFloorWad() is zero while no NOTE circulates.
The utilisation input is min(laggedUtilisationBps, smoothedUtilisationBps). The lagged value is the Desk’s utilisation as of the previous block, so a deposit or withdrawal in the same block cannot move the bond price.
The smoothed value (utilisationAvgBps, last updated at utilisationAvgAt) is slew-limited toward the lagged snapshot: one update moves it by at most 1e4 × min(elapsed, UTILISATION_MAX_STEP) / UTILISATION_WINDOW bps with UTILISATION_WINDOW = 1 day and UTILISATION_MAX_STEP = 6 hours, i.e. at most 2,500 bps per update, and it is refreshed on every deposit and by the permissionless pokeUtilisation().
Taking the lower of the two prices a fall in utilisation immediately while a rise has to persist for about a day before it earns the full discount. Utilisation readings above 100% are treated as 100%, and when the Desk cannot report utilisation the discount is zero. COUPON-leg markets refuse deposits when the live price print is stale, the live barrier is breached, or an observation closes within one hour.
Keep exploring, or revisit this topic from the navigation.