Skip to main content

Gauge Controller

The source code of the GaugeController.vy contract can be found on GitHub. The contract is written with Vyper version 0.4.3.

Overview​

The GaugeController is the governance contract that controls liquidity gauges and distributes YB token emissions. It implements a voting system where users with locked YB tokens (veYB) can vote on gauge weights, which determine the proportion of emissions each gauge receives.

  • Voting Power: Users with locked YB tokens can vote on gauge weights
  • Time-Decay Voting: Voting power decreases over time based on lock duration
  • Emission Distribution: Automatically distributes YB emissions to gauges based on weights
  • Gauge Management: Add/remove gauges and control their status
  • Checkpoint System: Maintains historical weight data for accurate calculations
  • Instant vote effect: vote_for_gauge_weights updates point_weight in real time; gauge weights respond immediately. Scheduled slope changes (from lock expirations) and historical checkpoints align to weekly boundaries, but vote arrival itself is not delayed.

Related: VotingEscrow for voting power and YB Token for emission mechanics.

Constants​

  • WEEK (uint256) - 7 days in seconds (604,800)
  • WEIGHT_VOTE_DELAY (uint256) - Minimum time between votes on the same gauge (10 days = 864,000 seconds)

Public Variables​

Core Contracts

  • TOKEN (GovernanceToken) - YB token contract (immutable)
  • VOTING_ESCROW (VotingEscrow) - veYB contract (immutable)

Gauge Management

  • n_gauges() (uint256) - Total number of gauges
  • gauges(uint256) (address) - Get gauge address by index
  • is_killed(address) (bool) - Check if gauge is killed

Voting State

  • vote_user_slopes(address, address) (VotedSlope) - User's vote slope for a gauge
  • vote_user_power(address) (uint256) - Total voting power used by user
  • last_user_vote(address, address) (uint256) - Last vote timestamp for user-gauge pair

Weight Tracking

  • point_weight(address) (Point) - Current gauge weight point (bias + slope)
  • time_weight(address) (uint256) - Last weight update timestamp for gauge
  • gauge_weight(address) (uint256) - Current gauge weight
  • gauge_weight_sum() (uint256) - Sum of all gauge weights
  • adjusted_gauge_weight(address) (uint256) - Adjusted weight for gauge
  • adjusted_gauge_weight_sum() (uint256) - Sum of adjusted weights

Emission Tracking

  • specific_emissions() (uint256) - Total emissions distributed
  • specific_emissions_per_gauge(address) (uint256) - Emissions for specific gauge
  • weighted_emissions_per_gauge(address) (uint256) - Weighted emissions for gauge
  • sent_emissions_per_gauge(address) (uint256) - Emissions already sent to gauge

Data Structs​

Point: bias: uint256, slope: uint256.

VotedSlope: slope, bias, power, end.

Events​

VoteForGauge — time, user, gauge_addr, weight

NewGauge — addr

SetKilled — gauge, is_killed


Function Documentation​

add_gauge()​

Description: Adds a new gauge to the controller. Only callable by the owner.

Emits: NewGauge.

InputTypeDescription
gaugeaddressAddress of the gauge to add
📄 View Source Code
@external
def add_gauge(gauge: address):
ownable._check_owner()
assert self.time_weight[gauge] == 0, "Gauge already added"

n: uint256 = self.n_gauges
self.n_gauges = n + 1
self.gauges[n] = gauge
self.time_weight[gauge] = block.timestamp
self.specific_emissions_per_gauge[gauge] = self.specific_emissions

log NewGauge(addr=gauge)

vote_for_gauge_weights()​

Description: Allows users with locked YB tokens to vote on gauge weights. Voting power is based on locked token amount and lock duration. Votes apply instantly.

Emits: VoteForGauge — for each gauge voted on.

InputTypeDescription
_gauge_addrsDynArray[address, 50]Array of gauge addresses to vote for
_user_weightsDynArray[uint256, 50]Array of weights in basis points (0.01% units, max 10000 total)

Requirements:

  • User must have locked YB tokens (veYB)
  • Lock must not be expired
  • Vote delay period must have passed (10 days per gauge)
  • Total voting power cannot exceed 10,000 basis points (100%)
📄 View Source Code
@external
def vote_for_gauge_weights(_gauge_addrs: DynArray[address, 50], _user_weights: DynArray[uint256, 50]):
# Check if transfer_clearance_checker is set to GC
assert staticcall VOTING_ESCROW.transfer_clearance_checker() == self, "Vote checker not set"

n: uint256 = len(_gauge_addrs)
assert len(_user_weights) == n, "Mismatch in lengths"
pt: Point = staticcall VOTING_ESCROW.get_last_user_point(msg.sender)
slope: uint256 = pt.slope
bias: uint256 = pt.bias
lock_end: uint256 = staticcall VOTING_ESCROW.locked__end(msg.sender)
assert lock_end > block.timestamp, "Expired"

power_used: uint256 = self.vote_user_power[msg.sender]

for i: uint256 in range(50):
if i >= n:
break
_user_weight: uint256 = _user_weights[i]
_gauge_addr: address = _gauge_addrs[i]
assert _user_weight <= 10000, "Weight too large"
if _user_weight != 0:
assert not self.is_killed[_gauge_addr], "Killed"
assert self.time_weight[_gauge_addr] > 0, "Gauge not added"
assert block.timestamp >= self.last_user_vote[msg.sender][_gauge_addr] + WEIGHT_VOTE_DELAY, "Cannot vote so often"

# ... voting logic implementation

_checkpoint_gauge() (internal)​

Description: Updates gauge weights and distributes emissions. Called during voting and gauge operations.

Returns: Point - Updated gauge weight point.

InputTypeDescription
gaugeaddressAddress of the gauge to checkpoint
📄 View Source Code
@internal
def _checkpoint_gauge(gauge: address) -> Point:
assert self.time_weight[gauge] > 0, "Gauge not alive"

adjustment: uint256 = min(staticcall Gauge(gauge).get_adjustment(), 10**18)
t: uint256 = self.time_weight[gauge]

w: uint256 = self.gauge_weight[gauge]
aw: uint256 = self.adjusted_gauge_weight[gauge]
w_sum: uint256 = self.gauge_weight_sum
aw_sum: uint256 = self.adjusted_gauge_weight_sum

pt: Point = self._get_weight(gauge)
self.point_weight[gauge] = pt
w_new: uint256 = pt.bias
aw_new: uint256 = w_new * adjustment // 10**18

self.gauge_weight[gauge] = w_new
self.gauge_weight_sum = w_sum + w_new - w
self.adjusted_gauge_weight[gauge] = aw_new
self.adjusted_gauge_weight_sum = aw_sum + aw_new - aw

d_emissions: uint256 = extcall TOKEN.emit(self, unsafe_div(aw_sum * 10**18, w_sum))
self.time_weight[gauge] = block.timestamp

specific_emissions: uint256 = self.specific_emissions + unsafe_div(d_emissions * 10**18, aw_sum)
if d_emissions > 0:
self.specific_emissions = specific_emissions

if block.timestamp > t:
self.weighted_emissions_per_gauge[gauge] += (specific_emissions - self.specific_emissions_per_gauge[gauge]) * aw // 10**18
self.specific_emissions_per_gauge[gauge] = specific_emissions

return pt

get_gauge_weight()​

Description: Current weight of a gauge (raw, unadjusted).

Returns: uint256.

InputTypeDescription
addraddressGauge address
📄 View Source Code
@external
@view
def get_gauge_weight(addr: address) -> uint256:
return self._get_weight(addr).bias

gauge_relative_weight()​

Description: Relative weight of a gauge normalized to 1e18 (1.0 = 1e18) — proportion of total emissions the gauge receives.

Returns: uint256.

InputTypeDescription
gaugeaddressGauge address
📄 View Source Code
@external
@view
def gauge_relative_weight(gauge: address) -> uint256:
return unsafe_div(self.adjusted_gauge_weight[gauge] * 10**18, self.adjusted_gauge_weight_sum)

ve_transfer_allowed()​

Description: Checks if a user is allowed to transfer their veYB. Returns True iff vote_user_power[user] == 0 (no active gauge votes).

Returns: bool.

InputTypeDescription
useraddressUser address to check
📄 View Source Code
@external
@view
def ve_transfer_allowed(user: address) -> bool:
return self.vote_user_power[user] == 0

checkpoint()​

Description: Public wrapper over _checkpoint_gauge(). Updates gauge weights and distributes emissions.

InputTypeDescription
gaugeaddressAddress of the gauge to checkpoint
📄 View Source Code
@external
def checkpoint(gauge: address):
self._checkpoint_gauge(gauge)

preview_emissions()​

Description: Preview the emissions a gauge would receive at a specific time without state changes.

Returns: uint256 - Preview of emissions for the gauge.

InputTypeDescription
gaugeaddressGauge address
at_timeuint256Timestamp to preview emissions at
📄 View Source Code
@external
@view
def preview_emissions(gauge: address, at_time: uint256) -> uint256:
if self.time_weight[gauge] == 0:
return 0

w: uint256 = self.gauge_weight[gauge]
aw: uint256 = self.adjusted_gauge_weight[gauge]
w_sum: uint256 = self.gauge_weight_sum
aw_sum: uint256 = self.adjusted_gauge_weight_sum

d_emissions: uint256 = 0
if at_time > self.time_weight[gauge]:
d_emissions = staticcall TOKEN.preview_emissions(at_time, unsafe_div(aw_sum * 10**18, w_sum))
specific_emissions: uint256 = self.specific_emissions + unsafe_div(d_emissions * 10**18, aw_sum)
weighted_emissions_per_gauge: uint256 = self.weighted_emissions_per_gauge[gauge] + (specific_emissions - self.specific_emissions_per_gauge[gauge]) * aw // 10**18

return weighted_emissions_per_gauge - self.sent_emissions_per_gauge[gauge]

emit()​

Description: Allows gauges to claim their allocated emissions. Only callable by registered gauges (identified by msg.sender).

Returns: uint256 - Amount of tokens transferred to the gauge.

📄 View Source Code
@external
def emit() -> uint256:
self._checkpoint_gauge(msg.sender)
emissions: uint256 = self.weighted_emissions_per_gauge[msg.sender]
to_send: uint256 = emissions - self.sent_emissions_per_gauge[msg.sender]
self.sent_emissions_per_gauge[msg.sender] = emissions
if to_send > 0:
extcall TOKEN.transfer(msg.sender, to_send)
return to_send

set_killed()​

Description: Sets the kill status of a gauge. While killed, the gauge cannot receive new positive vote weight — vote_for_gauge_weights reverts with "Killed" for any weight above zero. A weight of 0 is still permitted, so existing voters can remove weight from a killed gauge. Killing does not by itself zero existing votes, remove the gauge's weight from gauge_weight_sum, or stop emit(); in practice a killed gauge stops drawing emissions once its stakers leave and its get_adjustment() falls to zero.

Emits: SetKilled.

InputTypeDescription
gaugeaddressGauge address
is_killedboolWhether to kill the gauge
📄 View Source Code
@external
def set_killed(gauge: address, is_killed: bool):
ownable._check_owner()
assert self.time_weight[gauge] > 0, "Gauge not added"
self.is_killed[gauge] = is_killed
log SetKilled(gauge=gauge, is_killed=is_killed)

_get_weight() (internal)​

Description: Fills historic gauge weights week-over-week for missed checkins and returns the total for the future week. Handles time-decay of gauge weights.

Returns: Point.

InputTypeDescription
gaugeaddressAddress of the gauge
📄 View Source Code
@internal
@view
def _get_weight(gauge: address) -> Point:
t: uint256 = self.time_weight[gauge]
current_week: uint256 = block.timestamp // WEEK * WEEK
dt: uint256 = 0
if t > 0:
pt: Point = self.point_weight[gauge]
for i: uint256 in range(500):
if t >= current_week:
dt = block.timestamp - t
if dt == 0:
break
else:
dt = (t + WEEK) // WEEK * WEEK - t
t += dt
pt.bias -= min(pt.slope * dt, pt.bias)
pt.slope -= min(self.changes_weight[gauge][t], pt.slope)
if pt.bias == 0:
pt.slope = 0
return pt
else:
return empty(Point)

Voting Mechanics​

Voting power is based on:

  • Locked YB amount - More tokens = more voting power
  • Lock duration - Longer locks = more voting power
  • Time decay - Power decreases over time

Weight Distribution is based on:

  • Basis Points: Weights are in basis points (0.01% units)
  • Maximum: 10,000 basis points (100%) total voting power
  • Minimum: 1 basis point (0.01%) per gauge
  • Vote Delay: 10 days between votes on the same gauge

The voting system uses a time-decay mechanism:

  • Slope: Rate of voting power decrease
  • Bias: Current voting power value
  • End Time: When voting power expires

Emission Distribution​

The rate factor for YB emissions is calculated as:

rate_factor = (adjusted_gauge_weight_sum * 10^18) / gauge_weight_sum

Emissions are distributed proportionally based on:

  1. Gauge Weight: Voting power allocated to each gauge
  2. Adjustment Factor: Gauge-specific adjustment (0-100%) — typically sqrt(staked/total) from the gauge
  3. Time Elapsed: Since last checkpoint

Checkpoints apply pending historical weight changes (vote decay, expired locks) before each distribution, so emissions are always computed against current weights without per-block updates.


Integration with YB Token​

The GaugeController integrates with the YB token through:

  • Minting Authority: Can mint YB tokens via emit() function (on the TOKEN contract)
  • Rate Control: Controls emission rate via rate_factor parameter
  • Distribution: Transfers minted tokens to gauges

Governance Flow​

  1. Gauge Addition: Owner adds gauges via add_gauge()
  2. Voting: Users vote on gauge weights via vote_for_gauge_weights() (instant effect)
  3. Weight Updates: Weights are updated during checkpoints
  4. Emission Distribution: Gauges claim emissions via emit()
  5. Governance: Owner can kill gauges via set_killed()