DocumentationGovernance

Gauges

Documentation snapshot 18 October 2026. Deployment · Audit Status
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

Weekly gauge cycle: votes during week W minus one, checkpoint at the Thursday boundary, then mintWeekly and syncCaps for week W Thu 00:00 UTC Thu 00:00 UTC Thu 00:00 UTC week W − 1: voteForGaugeWeights changes recorded for the next boundary week W: checkpoint, mintWeekly, syncCaps emissions stream over 7 days caps pushed to the Desk once per week
fact Weeks run Thursday 00:00 UTC to Thursday 00:00 UTC. A vote cast at any point in week W − 1 affects the weights read for week W. Nothing changes mid-week.
  1. 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).
  2. Boundary. At Thursday 00:00 UTC every gauge’s bias for the new week is fixed. checkpoint() and checkpointGauge(gauge) are permissionless and fill the histories; gaugeRelativeWeight(gauge, ts) returns the WAD share once the week is checkpointed (gaugeRelativeWeightWrite checkpoints 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 reverts CheckpointIncomplete(reachedWeek) instead of building on a stale point.
  3. Mint. Anyone calls Minter.mintWeekly(). The budget is min(EmissionSchedule.weeklyEmission(week) + emissionShortfall, NOTE.epochMintRemaining()) (the shortfall is emission an earlier week could not mint under the epoch cap); each gauge receives budget × relativeWeight / 1e18, is minted to directly and notified in the same call.
  4. 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 with Desk.setPerUnderlyingCapFromGauge(underlying, bps) and evented as CapSynced(underlying, relativeWeight, bps, week).
  5. Stream. Each gauge streams its NOTE over DURATION = 7 days to stakers, Synthetix-style (rewardRate = amount × 1e18 / 604,800 per 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.

fact Gauge Types
Type idGaugeStakeWhat a vote here does
0SeriesGauge (one per COUPON series)COUPON units, LEG_ID = seriesId << 1, pulled by safeTransferFromDirects NOTE to holders who stake that series' COUPON leg, and raises the Desk cap for the series' underlying
1DeskGaugedNOTE (Desk shares)Directs NOTE to Desk depositors
2StakingGaugesNOTEDirects 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

  • voteForGaugeWeights records voteUserSlopes[user][gauge] = {slope, power, end}; your total voteUserPower may 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.
  • WeightTooHigh if a single allocation exceeds 10,000 bps; UnknownGauge for 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:

fact From Weights to Desk Caps Reference
StepRule
Series sharew_u / Σ w_series where w are the series-gauge relative weights for the week, summed per underlying
Raw captotalDeployedCapBps × share
Clampat least CAP_FLOOR_BPS = 100 (1%), at most CAP_CEILING_BPS = 4,000 (40%)
No votesevery underlying gets the 100 bps floor
Desk sidesetPerUnderlyingCapFromGauge 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 capeffectivePerUnderlyingCapBps 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.

illustrative Worked Weekly Example Reference
GaugeSlope from WBias at W (veNOTE units)
S-A3.96e11≈ 50,000
S-B4.76e11≈ 30,000
D2.38e11≈ 30,000
K1.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:

illustrative Worked Weekly Example Reference
GaugeNOTE mintedrewardRate (per second)
S-A38,461.538,461.5e18 / 604,800
S-B23,076.923,076.9e18 / 604,800
D23,076.923,076.9e18 / 604,800
K15,384.615,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 in rewardRate = 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 − remainingRewards is 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.

A little more clarity?

Keep exploring, or revisit this topic from the navigation.