Skip to content

Buyback auction

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 auction price from the buyback reserve. Purchased NOTE is transferred to sNOTE and notifyReward is called in the same transaction.

floor(t) = Treasury.reserveValueQuote / Treasury.circulatingSupply (quote WAD per NOTE) P(t) = floor + (startPriceWad − floor) × 2^(−(t − lastFillTime) / halfLifeSeconds) after fill: startPriceWad = P(t) × (1e4 + bumpBps) / 1e4; lastFillTime = t fill cap: quoteOut ≤ buybackReserve × maxFillBps / 1e4
ParameterDeploy-script valueBounds
buybackBps9,000 (90%)≤ 10,000
halfLifeSeconds6 hours5 minutes to 30 days
bumpBps2,000 (20%)≤ 10,000
maxFillBps2,500 (25% of reserve)≤ 10,000

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

illustrative Floor 0.50, startPriceWad 1.20 after the last fill.

Hours since last fillDecay factor 2^(−t/6h)P(t) = 0.50 + 0.70 × factor
01.00001.2000
30.70710.9950
60.50000.8500
120.25000.6750
240.06250.5438
480.00390.5027
00.5000 (floor)

A seller fills at hour 6 for 0.85. The start price bumps to 0.85 × 1.20 = 1.02 and decay restarts from there. If the buyback reserve holds 200,000 USDG, one fill pays at most 50,000 USDG (25%), buying 58,823.5 NOTE at 0.85.

floor 0.50 (reserve / circulating) fill at 0.85 start × 1.20 after each fill 0 h6 h12 h18 h 1.200.850.50 P(t) = floor + (start − floor) · 2^(−t / 6 h). Half-life 6 h, bump 20%, fill cap 25% of reserve.
The auction never pays below the Treasury floor, and each fill raises the start price so the next seller must wait for decay or accept the reset price.
  • 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 auction adjusts automatically as reserves grow.
  • Rate-limited. The fill cap and bump limit how fast a single seller can drain the reserve in a volatile market; the exponential decay ensures the reserve is still spent within hours or days rather than sitting idle.
  • Reward routing. Every fill immediately becomes an sNOTE reward stream; there is no manual distribution step.
FunctionEffect
setSplit(buybackBps)Fee split
setAuctionParams(halfLife, bumpBps, maxFillBps)Auction shape
resetAuction(newStartPriceWad)Reset start price (AuctionReset), for example after a long idle period
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.

  • If the buyback reserve is empty, sellNoteForQuote reverts with FillExceedsCap(requested, 0). Nothing is lost; the next fee sweep refills it.
  • If the Treasury’s circulatingSupply is zero, floorPriceWad returns zero and the auction decays towards zero. This happens only before any NOTE circulates. If startPriceWad is at or below the floor, currentPriceWad returns the floor.
  • 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 start price has decayed to the floor and NOTE trades above the floor, no seller appears and the reserve accumulates until governance resets the start price or the market falls.