Skip to content

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 — period WEEK (7 days)
  • RWAGauge — period PERIOD (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.

SHARED ACROSS SYSTEMS REWARD SOURCE ONE GAUGE SYSTEM PARTICIPANTS veRAACToken voting power · boost · locks Reward source any caller · one token per system GaugeController vote · kick · weights boost params · pause GaugeRewardsDistributor per-epoch deposits pull-based claimForGauge BaseGauge RAACGauge · RWAGauge working balance · reward integral Stakers stake · withdraw · claim RAACLiquidLocker CLAIM_FOR_ROLE · autolock Keepers pull · kick · updateUserRewards locks · bias deposit(token, amount) getGaugeRelativeWeight(gauge, epoch) boost · isPaused · isGaugeActive claimForGauge() stake · rewards permissionless upkeep

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.

REWARD SOURCE · RAAC EMISSIONS VOTERS RAAC GAUGE SYSTEM RAAC GAUGES WHERE REWARDS LAND RAACMinter tick() · mints RAAC per block distribution pool weights veRAACToken voting power · boost · locks RAACGaugeRewardsDistributor reward token: RAAC per-epoch deposits · pull-based RAACGaugeController vote · kick · weights boost params · pause RAACGauge period WEEK claimForGauge() · integral RAACGauge period WEEK claimForGauge() · integral RAACGauge period WEEK claimForGauge() · integral Stakers stake · withdraw · claimRewardToken() receive leRAAC · small claims as RAAC RAACLiquidLocker depositFor(claimer, RAAC) locks into veRAAC · mints leRAAC Keepers tick · pull · kick · updateUserRewards deposit(RAAC, amount) · every tick() locks · bias getGaugeRelativeWeight(gauge, epoch) claimForGauge() · each gauge pulls its RAAC share boost · isPaused permissionless upkeep stake · RAAC below raacAutolockMin autolock · depositRAACOnClaim leRAAC

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.

REWARD SOURCE · RENTAL INCOME VOTERS RWA GAUGE SYSTEM RWA GAUGES WHERE REWARDS LAND Real estate properties off-chain · monthly rent collected by the protocol External RWA distributor converts rent to USDC deposit() once per month veRAACToken voting power · boost · locks RWAGaugeRewardsDistributor reward token: USDC per-epoch deposits · pull-based RWAGaugeController vote · kick · weights boost params · pause RWAGauge longer period · same BaseGauge claimForGauge() · integral RWAGauge longer period · same BaseGauge claimForGauge() · integral RWAGauge longer period · same BaseGauge claimForGauge() · integral Stakers stake · withdraw · claimRewardToken() receive USDC directly · no autolock Keepers pull · kick · updateUserRewards rent deposit(USDC, amount) · monthly locks · bias getGaugeRelativeWeight(gauge, epoch) claimForGauge() · each gauge pulls its USDC share boost · isPaused permissionless upkeep stake · withdraw claimRewardToken() · USDC straight 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:

\[w = \min\left(0.4 \times s + 0.6 \times S \times \frac{\text{veBal}}{\text{veSupply}},\; s\right)\]

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.

→ Gauges — Functions


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.

→ Gauges — Design Trade-offs


Additional Features & Notes

  1. System isolation — a controller, a distributor and its gauges share one reward token; no reward path exists between the RAAC and RWA systems.
  2. Lazy checkpoints — gauge and global weights are walked forward only when touched, so an idle gauge costs nothing until someone uses it.
  3. Boost fallback — if calculateBoost reverts (a removed gauge), _applyBoost falls back to MIN_BOOST so stakers can always exit.
  4. Double-claim guard — the distributor snapshots depositedAtEpoch at each claim, so a gauge cannot re-claim a past epoch whose deposit has not grown.
  5. Removal pays out in the same transaction — removeGauge redirects the gauge's unclaimed entitlement to a recipient rather than leaving an admin follow-up.
  6. Decimal normalisation — every reward token is normalised to 18 decimals internally, whatever its own precision.