Gauges¶
Overview¶
The gauge system is how RAAC decides where emissions go. veRAAC holders vote weights onto gauges; each gauge's weight determines its share of the reward stream; stakers in a gauge earn that share, amplified by their own veRAAC boost.
The gauge machinery is one implementation deployed twice. GaugeController, GaugeRewardsDistributor and BaseGauge are shared code; a gauge system is one controller, one distributor and one or more gauges, all denominated in a single reward token. RAAC runs two such systems side by side. They share the veRAAC contract, so the same voters and the same boost apply to both, but nothing else: a gauge in one system never touches the other system's rewards.
| RAAC gauge system | RWA gauge system | |
|---|---|---|
| Reward token | RAAC | USDC |
| Reward source | RAACMinter emissions, pushed on every tick() |
Monthly rental income from real estate properties, deposited by an external distributor |
| Controller | RAACGaugeController |
RWAGaugeController |
| Distributor | RAACGaugeRewardsDistributor |
RWAGaugeRewardsDistributor |
| Gauges | RAACGauge, weekly period |
RWAGauge, longer period |
| Where rewards land | Autolocked into the Liquid Locker as $leRAAC | Paid out to stakers as USDC |
Two gauge flavours, one implementation
BaseGauge is abstract and holds all the logic. The concrete gauges differ only in their period length and start time:
RAACGauge— periodWEEK(7 days)RWAGauge— periodPERIOD(14 days)
Both anchor their first period to the next Thursday midnight, which keeps every cycle aligned with veRAAC's epoch boundaries.
RWAGauge's period differs between branches
On develop the constant is PERIOD = 2 * WEEK (14 days). A working branch changes it to MONTH = 28 days, which matches the contract's own "Monthly gauge" title. This page documents develop. Confirm which value ships before treating the 14-day figure as final.
Architecture¶
Shared architecture¶
Every gauge system has the same shape. A reward source deposits its token into the distributor once per epoch; veRAAC holders vote weights on the controller; each gauge pulls its share, sized by its relative weight, and streams it to stakers at their boosted working balance. The only things that change between deployments are the reward source, the reward token and where a claim ends up.
RAAC gauge system¶
The RAAC system directs protocol emissions. RAACMinter registers RAACGaugeRewardsDistributor as a distribution pool and pushes newly minted RAAC into it on every tick(). Each RAACGauge pulls its voted share and, because depositRAACOnClaim is on, a claim above raacAutolockMin is not transferred out but locked into the Liquid Locker on the claimer's behalf, so emissions arrive as $leRAAC rather than as liquid RAAC.
RWA gauge system¶
The RWA system directs real-world yield. Rental income from the protocol's real estate properties is collected off-chain, converted to USDC and deposited once a month into RWAGaugeRewardsDistributor by an external distributor. The rest is identical: veRAAC holders vote on RWAGaugeController, each RWAGauge pulls its share, and stakers claim. Because the reward token is USDC rather than RAAC, the autolock branch never fires and a claim is paid out directly to the staker.
Purpose¶
- Let veRAAC holders direct each gauge system's rewards by voting weights onto gauges
- Decay those votes automatically as the underlying veRAAC locks expire, without anyone re-voting
- Hold each gauge's share in the distributor until the gauge pulls it, so nothing is stranded
- Amplify a staker's reward share by their veRAAC boost, up to 2.5×
- Provide reversible, non-destructive emergency controls at both gauge and system level
Gauge States¶
There is no "deactivated" state. A gauge is in exactly one of three states, and "active" simply means registered.
| State | Entered by | Left by | Votes | Emissions |
|---|---|---|---|---|
| Active | addGauge (GAUGE_ADMIN) |
pause or removal | kept | earns its voted share |
| Paused | setEmergencyGaugePause / batch (EMERGENCY_ADMIN) |
the same functions | kept; only vote(g, 0) allowed |
share held in the distributor |
| Removed | removeGauge (GAUGE_ADMIN) |
re-add via addGauge |
kicked from the aggregate | none; unpaid share goes to a recipient |
A gauge operates only when both switches are off — the controller's global switch and the gauge's own. They never touch each other, so lifting the global pause cannot reopen a gauge that was paused individually.
The global pause is O(1) and fully reversible
setEmergencyPause flips one flag. It runs no loop, kicks no gauge and destroys no vote, so unpausing restores the previous state exactly — every vote and weight intact. withdraw and removeGauge stay open throughout, and deposits keep accumulating in the distributor.
withdraw is deliberately never gated
Every user entry point on a gauge carries whenGaugeLive, which requires both switches off — except withdraw. Principal can never be trapped by a pause. withdraw settles rewards into claimable without pulling from the distributor, so it only touches ungated views.
Voting¶
Casting a vote¶
vote(gauge, weight) allocates weight basis points of the caller's veRAAC power to a gauge. batchVote does several at once, reading the caller's lock set only once.
| Parameter | Value |
|---|---|
MIN_VOTE_WEIGHT |
100 (1 %) |
MAX_VOTE_WEIGHT |
10,000 (100 %) |
WEIGHT_VOTE_DELAY |
10 days, per (user, gauge) |
The delay exists to stop bribe gaming: once you vote on a gauge you cannot change that allocation for ten days. In a batch, the cooldown is checked per gauge but the total allocated across the batch must not exceed 100 %.
Voting a non-zero weight on a paused gauge reverts with GaugePaused. A zero weight is always allowed, so voters can always leave.
Weights decay with the underlying locks¶
Gauge weights are kept Curve-style: a bias per gauge (pointsWeight) and a global sum (pointsSum), each with scheduled reductions (changesWeight, changesSum) that fire as the voter's veRAAC locks expire. Nobody has to re-vote — a vote decays on its own schedule.
Because a veRAAC holder has many locks with different expiries, the controller snapshots each lock's contribution separately in voteUserLockEntries, so a later revote or kick can cancel exactly what was scheduled even if the locks have since changed.
maxVoteBuckets bounds gas for heavy multi-lock voters
With maxVoteBuckets = 0 the controller schedules one decay entry per lock — exact, but O(locks). Set to K (2–64), locks are grouped into K equal-width time buckets and one entry is scheduled per non-empty bucket at its amount-weighted average expiry. That bounds gas at O(K) for voters like the Liquid Locker, at the cost of a small approximation. K = 1 is rejected.
Clearing stale votes¶
| Function | Who | When |
|---|---|---|
kick(user) |
anyone | the user has an open veRAAC ragequit request, so their power is zero but their votes still decay the aggregate |
removePowerFromGauge(gauge) |
the voter | the gauge has been removed; frees their allocation for reuse |
vote(gauge, 0) |
the voter | any time, including on a paused gauge |
kick is permissionless by design: other voters gain from removing a ragequitter's phantom weight, and the 7-day ragequit cooldown gives keepers and users ample time to act.
In practice, an AWS keeper kicks on every ragequit
RAAC runs an off-chain keeper on AWS that watches veRAAC for the RagequitLockInitiated and RagequitInitiatedAll events. Whenever a user emits one, the keeper calls kick(user) on both gauge controllers, so a ragequitter's votes are cleared from the aggregate within moments of the request rather than lingering until someone notices. The function stays open to anyone, so the keeper is a convenience, not a dependency.
Boost & Working Balance¶
A staker does not earn on their raw stake but on a working balance, which blends the stake with their share of veRAAC.
| Parameter | Value |
|---|---|
MIN_BOOST |
10,000 (1.0×) |
MAX_BOOST |
25,000 (2.5×) |
WEIGHT_PRECISION |
10,000 |
boostWindow |
7 days |
Per the protocol spec, the working balance is the base component plus the boost component, capped at the raw stake:
where s is the user's stake and S the gauge's total stake. A staker with no veRAAC earns on 40 % of their stake; enough veRAAC lifts them to 100 %.
The working balance is a snapshot, not a live value
veRAAC power decays continuously, but a user's working balance is only recomputed when they are touched. Between updates, rewards accrue against a stale boost. updateUserRewards(user) is permissionless precisely so anyone can refresh — and in practice kick down — a staker whose lock has expired. See Design Trade-offs.
The Liquid Locker cannot be kicked
updateUserRewards reverts with NotKickable for the RAACLiquidLocker, whose pooled position is refreshed through its own stake, withdraw and claimOnBehalfOf paths.
Reward Flow¶
sequenceDiagram
participant S as RAACMinter / rental income distributor
participant D as GaugeRewardsDistributor
participant C as GaugeController
participant G as BaseGauge
participant U as Staker
S->>D: deposit(token, amount) — recorded at the current epoch
G->>D: claimForGauge() — walks every unclaimed epoch
D->>C: getGaugeRelativeWeight(gauge, epoch)
D-->>G: entitled = deposited × relWeight / 1e18
G->>G: roll into the period rate, accrue integral / workingSupply
U->>G: claimRewardToken()
G-->>U: reward · RAAC autolocks into the Liquid Locker, USDC is paid out
The distributor is pull-based: it records one number per epoch and never splits across gauges. Each gauge computes its own share when it claims, so an idle or paused gauge's entitlement is simply held until it next claims — the walk covers every missed epoch.
Inside the gauge¶
Rewards pulled into a gauge are spread across the current period as a rate, and accrue into a per-token integral divided by workingSupply. When workingSupply is zero, the accrual has no one to go to and is banked in leftoverRewards instead.
MAX_EXTRA_REWARDS (8) additional reward tokens can be registered per gauge via addRewardToken and funded with depositRewardToken. All token amounts are normalised to 18 decimals internally and denormalised on transfer.
RAAC rewards can autolock
While depositRAACOnClaim is set, a RAAC reward above raacAutolockMin (default 1e16) is deposited straight into the Liquid Locker on the claimer's behalf rather than transferred out — which is how ecosystem emissions arrive as $leRAAC.
Parameters¶
| Parameter | Value | Where | Setter |
|---|---|---|---|
MAX_BOOST / MIN_BOOST |
25,000 / 10,000 | GaugeController | constants |
WEIGHT_VOTE_DELAY |
10 days | GaugeController | constant |
MIN_VOTE_WEIGHT / MAX_VOTE_WEIGHT |
100 / 10,000 | GaugeController | constants |
maxVoteBuckets |
0 (exact) | GaugeController | setMaxVoteBuckets (0, or 2–64) |
boostWindow |
7 days | GaugeController | set in constructor |
| period duration | 7 days / 14 days | RAACGauge / RWAGauge | constructor |
MAX_EXTRA_REWARDS |
8 | BaseGauge | constant |
raacAutolockMin |
1e16 | BaseGauge | setRaacAutolockMin |
depositRAACOnClaim |
true | BaseGauge | setDepositRAACOnClaim |
MAX_EPOCHS |
52 | Distributor | constant, bounds the claim walk |
enableMaxClaimBound |
off | Distributor | setMaxClaimBound |
The controller pins veRAAC's lock length
The GaugeController constructor reverts with InvalidVeToken unless veRAAC's maxLockEpochs × epochDuration equals exactly 52 weeks. The boost and decay maths assume that horizon.
Access Control¶
| Role | Contract | Powers |
|---|---|---|
GAUGE_ADMIN |
GaugeController | addGauge, removeGauge, setDistributor, setMaxVoteBuckets, boost amplification |
EMERGENCY_ADMIN |
GaugeController | global pause, per-gauge pause, batch pause |
GAUGE_ADMIN |
BaseGauge | addRewardToken, manageDistributor, setController |
DEFAULT_ADMIN_ROLE |
BaseGauge | setDepositRAACOnClaim, setRaacAutolockMin, setGaugeRewardDistributor |
CONTROLLER_ROLE |
BaseGauge | setEmergencyPaused — held by the controller |
DISTRIBUTOR_ROLE |
BaseGauge | claimRewardsFor |
CLAIM_FOR_ROLE |
BaseGauge | claimOnBehalfOf — held by the Liquid Locker |
DISTRIBUTOR_ADMIN |
Distributor | setMaxClaimBound |
DEFAULT_ADMIN_ROLE |
Distributor | setController, withdrawDust |
Permissionless by design: deposit, pullRewardsFromDistributor, claimForGauge (callable only by the gauge itself), kick, updateUserRewards, forceCompleteCheckpoint.
Functions¶
Every external function across the three contracts — voting, staking, claiming, distribution, administration and views — is documented with signatures, parameters, errors and source on the dedicated page.
Design Trade-offs¶
Behaviours that a security review raised and the protocol accepted as design — rather than fixed — are documented separately, so that reviewers can tell a deliberate trade-off from a defect.
Additional Features & Notes¶
- System isolation — a controller, a distributor and its gauges share one reward token; no reward path exists between the RAAC and RWA systems.
- Lazy checkpoints — gauge and global weights are walked forward only when touched, so an idle gauge costs nothing until someone uses it.
- Boost fallback — if
calculateBoostreverts (a removed gauge),_applyBoostfalls back toMIN_BOOSTso stakers can always exit. - Double-claim guard — the distributor snapshots
depositedAtEpochat each claim, so a gauge cannot re-claim a past epoch whose deposit has not grown. - Removal pays out in the same transaction —
removeGaugeredirects the gauge's unclaimed entitlement to a recipient rather than leaving an admin follow-up. - Decimal normalisation — every reward token is normalised to 18 decimals internally, whatever its own precision.