Buyback Auction

Documentation snapshot 18 October 2026. Deployment · Audit Status
On this page

Summary

fact RevenueRouter is the protocol feeSink. On notifyFee(USDG, amount) it splits buybackBps (9,000 in DeployToken.s.sol) into the buyback reserve and forwards the remainder (1,000) to the Treasury (FeesRouted). Anyone may call sellNoteForQuote(noteAmount, minQuoteOut) and be paid at the current round price from the buyback reserve, subject to a fill cap, a minimum fill size and a minimum interval between fills. Purchased NOTE is transferred to sNOTE and notifyReward is called in the same transaction.

Price

fact Price Calculation

floor(t) = Treasury.previewRedeem(1 NOTE) = reserveValueQuote / circulatingSupply × (1 − redemptionHaircut) (quote WAD per NOTE) max(t) = floor × (1e4 + maxPremiumBps) / 1e4 P(t) = floor + (max − floor) × (1 − 2^(−(t − anchorTime) / halfLifeSeconds)) fill cap: quoteOut ≤ maxFillQuote() = buybackReserve × maxFillBps / 1e4 fill min: quoteOut ≥ minFillQuote() = max(buybackReserve × minFillBps / 1e4, minFillQuoteAbs) after fill: anchorTime = t; lastFillTime = t (the next round starts at the floor) next fill: t ≥ lastFillTime + minFillInterval()
fact Price
ParameterDeploy-script valueBounds
buybackBps9,000 (90%)≤ 10,000
halfLifeSeconds6 hours5 minutes to 30 days
maxPremiumBps10,000 (the round rises towards twice the floor)≤ MAX_PREMIUM_BPS (50,000, six times the floor)
maxFillBps2,500 (25% of reserve)1 to 10,000
minFillBps10 (0.1% of reserve, DEFAULT_MIN_FILL_BPS)1 to maxFillBps
minFillQuoteAbs1 USDG (DEFAULT_MIN_FILL_UNITS) until setMinFillQuote is called1 to MAX_MIN_FILL_UNITS (1,000,000) whole quote units
minFillInterval30 minutes (DEFAULT_MIN_FILL_INTERVAL, capped at the half-life)MIN_FILL_INTERVAL (5 minutes) to halfLifeSeconds

A round opens at the floor at anchorTime and the price closes half of the remaining distance to the capped maximum every half-life. The first seller to accept sets the lowest price at which anyone will sell; the fill records lastFillTime, moves anchorTime to the fill and the next round starts at the floor again. resetAuction() and a parameter change open a round the same way.

The price is never below the floor and never above maxPriceWad(), and it is not a formula of past fills: a market that values NOTE above the cap leaves the reserve unspent until governance raises maxPremiumBps.

The minimum interval paces the reserve at no more than one cap-sized fill per interval. It is short against the half-life on purpose: the price a seller can first accept after a fill sits close to the floor, so the round, not the gate, sets the price. minFillInterval() never reads below MIN_FILL_INTERVAL, and setAuctionParams lowers minFillInterval to the new half-life and minFillBps to the new maxFillBps when either bound shrinks.

2^(−x) is computed by ExpMath.halfLifeDecayWad, a fixed-point exponential with bounded error tested against reference values.

Worked Example

illustrative Floor 0.50, maxPremiumBps 10,000, so the round rises towards 1.00.

illustrative Worked Example Reference
Hours since the round openedShare of the distance closed, 1 − 2^(−t/6h)P(t) = 0.50 + 0.50 × share
00.00000.5000 (floor)
0.50.05610.5281
30.29290.6464
60.50000.7500
120.75000.8750
240.93750.9688
480.99610.9980
∞11.0000 (cap)

A seller accepts at hour 6 for 0.75 and takes the whole cap. If the buyback reserve holds 200,000 USDG, one fill pays at most 50,000 USDG (25%), buying 66,666.67 NOTE at 0.75, and must pay at least 200 USDG (0.1%, above the 1 USDG absolute minimum).

The next round opens at the floor immediately. The next fill is accepted 30 minutes later at the earliest, when the new round has reached about 0.528; a seller who wants 0.75 again waits another six hours.

Rising-price round opening at the floor, approaching the cap and restarting at the floor after a fill cap 1.00 (floor × 2) floor 0.50 (reserve / circulating) fill at 0.75 next round opens at the floor 0 h 6 h 12 h 18 h 1.00 0.75 0.50 P(t) = floor + (cap − floor) · (1 − 2^(−t / 6 h)) half-life 6 h · cap 2 × floor · fill cap 25% of reserve
fact The auction never pays below the Treasury floor or above the capped maximum; a fill ends the round, and the next seller starts again from the floor.

Properties

  • No DEX dependency. Buybacks do not route through an AMM, so there is no pool to sandwich and no price impact fee paid to LPs.
  • Floor-aware. Because floor(t) is read live from the Treasury, the round and its cap adjust automatically as reserves grow.
  • One floor. floorPriceWad() is the Treasury’s redemption value of one NOTE after the redemption haircut, the same figure the bond market uses as redemptionFloorWad(). The auction price can exceed the floor, up to maxPriceWad(). The floor is a lower reference bound, not a ceiling or an assurance that a buyback matches a redemption. previewRedeem rounds down to whole quote units, so the floor is read at WAD precision by quoting the redemption of 1e18 × 10^(18 − quoteDecimals) NOTE.
  • Rate-limited. The fill cap, the minimum interval between fills and the restart at the floor limit how fast a single seller can drain the reserve in a volatile market; the minimum fill size stops the interval being consumed by a dust sale. Reserve spending depends on accepted fills; no completion time is assured.
  • Reward routing. Every fill immediately becomes an sNOTE reward stream; there is no manual distribution step.

Governance Surface

fact Governance Surface Reference
FunctionEffect
setSplit(buybackBps)Fee split; quote already received is split at the rate in force when it arrived
setAuctionParams(halfLife, maxPremiumBps, maxFillBps)Round shape and cap (AuctionParamsUpdated); also clamps minFillInterval and minFillBps to the new bounds
setMinFillInterval(seconds)Minimum time between fills, within [MIN_FILL_INTERVAL, halfLifeSeconds] (MinFillIntervalUpdated, else InvalidParams)
setMinFillBps(bps)Minimum fill as a share of the reserve, from 1 to maxFillBps (MinFillBpsUpdated, else InvalidParams)
setMinFillQuote(amount)Absolute minimum fill, 1 to 1,000,000 whole quote units (MinFillQuoteUpdated, else InvalidParams)
resetAuction()Opens a new round at the floor (AuctionReset), for example after a long idle period at the cap
sweep(token, to, amount)Recover a non-quote token sent by mistake (Swept); the quote token cannot be swept
sync()Permissionless: splits any quote balance not yet routed (FeesRouted), used after a paused period

Treasury and sNOTE addresses are immutable constructor arguments; there is no setter for either destination.

Failure Modes

  • If the buyback reserve is empty, sellNoteForQuote reverts with FillExceedsCap(requested, 0). Nothing is lost; the next fee sweep refills it.
  • sellNoteForQuote checks, in order: ZeroAmount, FillTooSoon(nextAllowedAt) when less than minFillInterval() has passed since the last fill, then after sync ZeroAmount again when the quote out rounds to zero, SlippageExceeded(out, minOut), FillExceedsCap(requested, cap) and FillBelowMinimum(requested, minimum) when quoteOut is under minFillQuote(). A successful fill emits Buyback(seller, noteAmount, quoteOut, priceWad) and AuctionReset(anchorTime) for the round it opens.
  • If the Treasury’s circulatingSupply is zero, floorPriceWad returns zero and so does the round price. This happens only before any NOTE circulates.
  • notifyFee emits FeeNotified even while paused, but the split is deferred until sync() after unpausing, so sweepFees on NoteCore is never blocked by a router pause.
  • If the round has reached the cap and NOTE trades above it, no seller appears and the reserve accumulates until governance raises maxPremiumBps or the market falls. resetAuction() only restarts the round at the floor; it does not change the cap.
A little more clarity?

Keep exploring, or revisit this topic from the navigation.