Gauges
On this page
fact Sources contracts/src/governance/GaugeController.sol and contracts/src/governance/gauges/. The controller is a Solidity 0.8 port of Curve’s GaugeController with custom errors and one addition: a weekly syncCaps() that turns series-gauge weights into per-underlying Desk caps.
Weekly Cycle
- Vote. Anyone with a live lock calls
voteForGaugeWeights(gauge, userWeightBps)once per gauge. Your veNOTE slope is allocated across gauges in proportion to the bps you give each;Σ userWeightBps ≤ 10,000(PowerExceeded(used)). The lock must end after the next week boundary (LockExpiresTooSoon). - Boundary. At Thursday 00:00 UTC every gauge’s bias for the new week is fixed.
checkpoint()andcheckpointGauge(gauge)are permissionless and fill the histories;gaugeRelativeWeight(gauge, ts)returns the WAD share once the week is checkpointed (gaugeRelativeWeightWritecheckpoints first). Each walk covers at most 500 weeks per call and records how far it got, so after a long idle period repeated calls catch up; a vote or weight change made while a walk is still behind the current week revertsCheckpointIncomplete(reachedWeek)instead of building on a stale point. - Mint. Anyone calls
Minter.mintWeekly(). The budget ismin(EmissionSchedule.weeklyEmission(week) + emissionShortfall, NOTE.epochMintRemaining())(the shortfall is emission an earlier week could not mint under the epoch cap); each gauge receivesbudget × relativeWeight / 1e18, is minted to directly and notified in the same call. - Caps. Anyone calls
GaugeController.syncCaps()once per week (AlreadySynced(week)otherwise). Per underlying:bps = totalDeployedCapBps × weight_u / Σ series-gauge weights, clamped to[CAP_FLOOR_BPS = 100, CAP_CEILING_BPS = 4,000], pushed withDesk.setPerUnderlyingCapFromGauge(underlying, bps)and evented asCapSynced(underlying, relativeWeight, bps, week). - Stream. Each gauge streams its NOTE over
DURATION = 7 daysto stakers, Synthetix-style (rewardRate = amount × 1e18 / 604,800per second).
Gauge Types
fact Three types are registered by the constructor, each with type weight 1e18, so a gauge’s relative weight is simply its bias over the total bias.
| Type id | Gauge | Stake | What a vote here does |
|---|---|---|---|
| 0 | SeriesGauge (one per COUPON series) | COUPON units, LEG_ID = seriesId << 1, pulled by safeTransferFrom | Directs NOTE to holders who stake that series' COUPON leg, and raises the Desk cap for the series' underlying |
| 1 | DeskGauge | dNOTE (Desk shares) | Directs NOTE to Desk depositors |
| 2 | StakingGauge | sNOTE | Directs NOTE to stakers |
Gauges are added by governance proposal: addGauge(gauge, type, weight, underlying) with underlying non-zero only for series gauges. A registered gauge can be switched off with killGauge(gauge) (owner or guardian) and switched back on with reviveGauge(gauge) (owner only); both revert UnknownGauge for an unregistered address and emit GaugeKilled(gauge, killed), and isKilled(gauge) exposes the state.
While killed the gauge’s relative weight reads 0, so its share of the weekly budget is not minted and syncCaps gives its underlying no weight; new votes for it revert GaugeIsKilled(gauge), while setting an existing vote to 0 bps stays allowed so voters can move their power elsewhere. Withdrawing and claiming on the gauge contract itself are unaffected; stake reverts GaugeRetired while the gauge is killed and again accepts stake after reviveGauge.
An admin weight (changeGaugeWeight, exposed as adminWeight(gauge)) is a separate, non-decaying component added to the gauge’s voted bias: changing it replaces only that component and shifts the gauge, type sum and total by the same delta, so the voted bias and slope are untouched and Σ relative weights stays within 1e18. It can be raised, lowered or cleared at any time, with or without live votes.
Staked COUPON units belong to the gauge for NoteLegs’ purposes, so the series coupons they earn accrue to the gauge contract.
The SeriesGauge passes them through to stakers: on every stake, withdraw, exit and claimCoupons it reads couponsPulled + NoteCore.accruedCoupon(SERIES_ID, gauge) and folds any increase since couponsSeen into couponPerUnitStored, so each staker’s share is proportional to units staked over the period. earnedCoupons(account) shows the claimable amount and claimCoupons() pays it in the series quote token (COUPON_TOKEN, USDG), pulling from NoteCore via claim(SERIES_ID) when the gauge’s balance is short and emitting CouponPaid(account, amount) (NothingToClaim when zero). exit and withdraw leave coupons claimable; they are not forfeited.
Coupons accrued while nothing is staked go to couponsUnallocated, which the owner can move with sweepUnallocatedCoupons(to) (UnallocatedCouponsSwept). The constructor rejects a series whose quote token is the reward token (CouponEqualsReward). withdraw, exit, getReward and claimCoupons are never pausable; a paused gauge blocks stake only.
Your Allocation and the Cooldown
voteForGaugeWeightsrecordsvoteUserSlopes[user][gauge] = {slope, power, end}; your totalvoteUserPowermay not exceed 10,000 bps. Setting a gauge to 0 bps releases that power.- 10-day cooldown per gauge.
WEIGHT_VOTE_DELAY = 10 days: after voting on a gauge you cannot change that gauge’s allocation until 10 days have passed (VoteCooldown(availableAt)). Other gauges are unaffected. Because the cooldown exceeds a week, a vote remains unchanged through at least one weekly boundary, subject to the gauge’s active status. - Votes persist. Your allocation stays in force and decays with your lock; you do not need to re-vote weekly. As your lock decays the gauge bias contributed by your vote decays with it; when the lock ends the slope is removed from the sums at
end. WeightTooHighif a single allocation exceeds 10,000 bps;UnknownGaugefor an unregistered address.
Events: VoteForGauge(time, user, gauge, weight), NewGauge, NewGaugeWeight, AddType, NewTypeWeight, DeskUpdated, CapSynced.
From Weights to Desk Caps
The Desk holds three caps as bps of totalAssets: perSeriesCapBps, perUnderlyingCapBps and totalDeployedCapBps (launch values 1,000 / 2,500 / 6,000). syncCaps() replaces the flat per-underlying figure with a per-underlying value derived from votes:
| Step | Rule |
|---|---|
| Series share | w_u / Σ w_series where w are the series-gauge relative weights for the week, summed per underlying |
| Raw cap | totalDeployedCapBps × share |
| Clamp | at least CAP_FLOOR_BPS = 100 (1%), at most CAP_CEILING_BPS = 4,000 (40%) |
| No votes | every underlying gets the 100 bps floor |
| Desk side | setPerUnderlyingCapFromGauge accepts only the gauge controller (NotGaugeController) and only values in 100 to 4,000 bps (MIN_UNDERLYING_CAP_BPS, MAX_UNDERLYING_CAP_BPS, else CapOutOfBounds); emits PerUnderlyingCapSet |
| Binding cap | effectivePerUnderlyingCapBps is the lower of the governed perUnderlyingCapBps and the gauge value. Votes can only tighten the governed default (2,500 bps at launch); loosening an underlying above it needs a governance change to setCaps. totalDeployedCapBps still bounds the sum |
Clamping means the pushed caps need not sum to totalDeployedCapBps; the Desk’s global cap remains the binding total. Vote weight redirects Desk capital between underlyings within governance’s ceilings, it does not increase the Desk’s total exposure.
Worked Weekly Example
illustrative Taken from the contract documentation (contracts/docs/governance/Overview.md). Week W starts Thursday 00:00 UTC; Desk totalDeployedCapBps = 6,000; four gauges: S-A and S-B (series gauges on underlyings A and B), D (DeskGauge), K (StakingGauge).
1. Locks (week W − 1). Holder A locks 100,000 NOTE for 4 years, weight ≈ 100,000 veNOTE (slope 100,000e18 / 126,144,000 ≈ 7.93e11). Holder B locks 60,000 NOTE for 2 years, weight ≈ 30,000 veNOTE (slope ≈ 4.76e11).
2. Votes (week W − 1). Holder A: 50% S-A, 30% D, 20% K. Holder B: 100% S-B.
| Gauge | Slope from W | Bias at W (veNOTE units) |
|---|---|---|
S-A | 3.96e11 | ≈ 50,000 |
S-B | 4.76e11 | ≈ 30,000 |
D | 2.38e11 | ≈ 30,000 |
K | 1.59e11 | ≈ 20,000 |
3. Week W opens. checkpoint() fills the points for W. Total 130,000. Relative weights: S-A 0.3846, S-B 0.2308, D 0.2308, K 0.1538 (sum 1e18 less at most 4 wei of truncation).
4. Emission. Week W is in the first 4-week period, so weeklyEmission = 100,000 NOTE; the NOTE epoch cap is 1,000,000 with nothing else minted, so budget = 100,000. mintWeekly() mints and notifies:
| Gauge | NOTE minted | rewardRate (per second) |
|---|---|---|
S-A | 38,461.5 | 38,461.5e18 / 604,800 |
S-B | 23,076.9 | 23,076.9e18 / 604,800 |
D | 23,076.9 | 23,076.9e18 / 604,800 |
K | 15,384.6 | 15,384.6e18 / 604,800 |
5. Caps. syncCaps(): series weights sum to 0.6154. A’s share is 0.3846 / 0.6154 = 62.5%, so 6,000 × 0.625 = 3,750 bps; B gets 2,250 bps. Both inside [100, 4,000], so the gauge values are 37.5% for A and 22.5% for B. With the launch perUnderlyingCapBps of 2,500, the Desk’s effective cap is min(2,500, 3,750) = 2,500 bps for A and 2,250 bps for B: votes can shift the Desk toward A but not past the governed 25% ceiling.
6. During the week. Holder C stakes 40% of all S-A COUPON units in the S-A gauge on Monday. From then she earns 40% of the remaining stream. Rewards streamed while nobody was staked (Thursday to Monday) accumulate in unallocated and are folded into next week’s stream, so nothing is stranded.
Once a gauge is retired (killed in the controller, or a series gauge whose series has reached a terminal state) the remaining unallocated NOTE can be swept out through a permissioned setting (sweepUnallocated(to), event UnallocatedSwept; GaugeActive while the gauge is still live), so a gauge that no longer streams cannot lock NOTE forever.
A retired gauge (killed, or a series gauge whose series is Autocalled, Matured or Unwound) rejects new stake with GaugeRetired, so nobody can capture a stream after retirement; the full stream that nobody staked for is sweepable.
One residue is accepted as dust: the per-second reward index is floored (rewardRate × dt / totalStaked, and again per account in the user update), and the flooring remainder stays in the gauge’s balance without being attributed to any account or to unallocated; it is below 1e-16 of the distributed rewards in the reference scenario and is not recoverable through sweepUnallocated.
7. Week W + 1. Biases have decayed by one week of slope (Holder A’s by 7.93e11 × 604,800 ≈ 480 veNOTE, Holder B’s by ≈ 288). Relative weights shift slightly toward Holder A’s gauges because Holder B’s shorter lock decays faster in relative terms. In week W + 4 the schedule steps down to 97,000 NOTE. A proposal to change the schedule takes about 8 days end to end; see Emissions.
Invariants the Tests Hold
- For the current and next week,
Σ gaugeRelativeWeightWrite(g) ∈ [1e18 − nGauges, 1e18], or exactly 0 when the total is 0. voteUserPower[u] == Σ voteUserSlopes[u][g].power ≤ 10,000.- Per gauge:
rewardsPaid ≤ rewardsNotified,REWARD.balanceOf(gauge) == rewardsNotified − rewardsPaid,Σ balanceOf == totalStaked == STAKE.balanceOf(gauge). The balance identity includes the flooring residue described above;Σ earned + unallocated ≤ rewardsNotified − rewardsPaid, with the difference being that residue. Reward streams settle inrewardRate = amount / DURATION(integer) and per-token accounting rounds down, so up to a few wei per second of stream stays in the gauge as residue:rewardsNotified − rewardsPaid − unallocated − remainingRewardsis a small non-negative dust amount, never paid twice and never negative. - Per week:
Minter.mintedForWeek ≤ weeklyEmission(week)and≤the NOTE epoch headroom; every minted wei sits in exactly one gauge and was notified to it.
Out of Scope in V1
Bribe or incentive markets on gauge votes, a veNOTE boost on sNOTE rewards, and vote delegation. All are listed as future work in the contract documentation.
Keep exploring, or revisit this topic from the navigation.