Buyback Auction
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
| Parameter | Deploy-script value | Bounds |
|---|---|---|
buybackBps | 9,000 (90%) | ≤ 10,000 |
halfLifeSeconds | 6 hours | 5 minutes to 30 days |
maxPremiumBps | 10,000 (the round rises towards twice the floor) | ≤ MAX_PREMIUM_BPS (50,000, six times the floor) |
maxFillBps | 2,500 (25% of reserve) | 1 to 10,000 |
minFillBps | 10 (0.1% of reserve, DEFAULT_MIN_FILL_BPS) | 1 to maxFillBps |
minFillQuoteAbs | 1 USDG (DEFAULT_MIN_FILL_UNITS) until setMinFillQuote is called | 1 to MAX_MIN_FILL_UNITS (1,000,000) whole quote units |
minFillInterval | 30 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.
| Hours since the round opened | Share of the distance closed, 1 − 2^(−t/6h) | P(t) = 0.50 + 0.50 × share |
|---|---|---|
| 0 | 0.0000 | 0.5000 (floor) |
| 0.5 | 0.0561 | 0.5281 |
| 3 | 0.2929 | 0.6464 |
| 6 | 0.5000 | 0.7500 |
| 12 | 0.7500 | 0.8750 |
| 24 | 0.9375 | 0.9688 |
| 48 | 0.9961 | 0.9980 |
| ∞ | 1 | 1.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.
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 asredemptionFloorWad(). The auction price can exceed the floor, up tomaxPriceWad(). The floor is a lower reference bound, not a ceiling or an assurance that a buyback matches a redemption.previewRedeemrounds down to whole quote units, so the floor is read at WAD precision by quoting the redemption of1e18 × 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
| Function | Effect |
|---|---|
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,
sellNoteForQuotereverts withFillExceedsCap(requested, 0). Nothing is lost; the next fee sweep refills it. sellNoteForQuotechecks, in order:ZeroAmount,FillTooSoon(nextAllowedAt)when less thanminFillInterval()has passed since the last fill, then aftersyncZeroAmountagain when the quote out rounds to zero,SlippageExceeded(out, minOut),FillExceedsCap(requested, cap)andFillBelowMinimum(requested, minimum)whenquoteOutis underminFillQuote(). A successful fill emitsBuyback(seller, noteAmount, quoteOut, priceWad)andAuctionReset(anchorTime)for the round it opens.- If the Treasury’s
circulatingSupplyis zero,floorPriceWadreturns zero and so does the round price. This happens only before any NOTE circulates. notifyFeeemitsFeeNotifiedeven while paused, but the split is deferred untilsync()after unpausing, sosweepFeesonNoteCoreis 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
maxPremiumBpsor the market falls.resetAuction()only restarts the round at the floor; it does not change the cap.
Keep exploring, or revisit this topic from the navigation.