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

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 (10 days)

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) - Get user's vote slope for a gauge
  • vote_user_power(address) (uint256) - Get total voting power used by user
  • last_user_vote(address, address) (uint256) - Get last vote timestamp for user-gauge pair

Weight Tracking

  • point_weight(address) (Point) - Get current gauge weight point (bias + slope)
  • time_weight(address) (uint256) - Get last weight update timestamp for gauge
  • gauge_weight(address) (uint256) - Get current gauge weight
  • gauge_weight_sum() (uint256) - Sum of all gauge weights
  • adjusted_gauge_weight(address) (uint256) - Get 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) - Get emissions for specific gauge
  • weighted_emissions_per_gauge(address) (uint256) - Get weighted emissions for gauge
  • sent_emissions_per_gauge(address) (uint256) - Get emissions already sent to gauge

Data Structs

Point

  • bias (uint256) - Current weight value
  • slope (uint256) - Rate of weight change

VotedSlope

  • slope (uint256) - Voting power slope
  • bias (uint256) - Voting power bias (used if slope == 0)
  • power (uint256) - Total voting power
  • end (uint256) - Lock end time

Events

VoteForGauge (Emitted when a user votes for a gauge)

  • time (uint256) - Timestamp of the vote
  • user (address) - Address of the voter
  • gauge_addr (address) - Address of the gauge being voted for
  • weight (uint256) - Voting weight assigned to the gauge

NewGauge (Emitted when a new gauge is added)

  • addr (address) - Address of the new gauge

SetKilled (Emitted when a gauge is killed or unkilled)

  • gauge (address) - Address of the gauge
  • is_killed (bool) - New kill status

Function Documentation

add_gauge()

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

Emits:
NewGauge - When a new gauge is added.

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.

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)

Requirements:

  • User must have locked YB tokens (veYB)
  • Lock must not be expired
  • Vote delay period must have passed
  • 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()

Description:
Internal function that 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:
Returns the current weight of a gauge. This is the raw weight value without any adjustments applied.

Returns:
uint256 - Current gauge weight.

InputTypeDescription
addraddressGauge address

Requirements:

  • Gauge must be registered (time_weight[addr] > 0)
📄 View Source Code
@external
@view
def get_gauge_weight(addr: address) -> uint256:
return self._get_weight(addr).bias

gauge_relative_weight()

Description:
Returns the relative weight of a gauge normalized to 1e18 (1.0 = 1e18). This represents the proportion of total emissions the gauge will receive.

Returns:
uint256 - Relative weight normalized to 1e18.

InputTypeDescription
gaugeaddressGauge address

Requirements:

  • Gauge must be registered (time_weight[gauge] > 0)
📄 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 tokens. Users cannot transfer veYB tokens if they have active voting power.

Returns:
bool - True if transfer is allowed, false otherwise.

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 function to checkpoint a gauge. Updates gauge weights and distributes emissions. This is the same as the internal _checkpoint_gauge() function.

Returns:
None.

InputTypeDescription
gaugeaddressAddress of the gauge to checkpoint

Requirements:

  • Gauge must be registered (time_weight[gauge] > 0)
📄 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 causing 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.

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

Emits:
Transfer - When tokens are 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. Killed gauges cannot receive votes or emissions.

Emits:
SetKilled - When gauge kill status is changed.

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:
Internal function that fills historic gauge weights week-over-week for missed checkins and returns the total for the future week. This function handles the time-decay calculations for gauge weights.

Returns:
Point - Gauge weight point with bias and slope.

InputTypeDescription
gaugeaddressAddress of the gauge

Requirements:

  • Gauge must be registered (time_weight[gauge] > 0)
📄 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) # Correctly handles even slope=0
pt.slope -= min(self.changes_weight[gauge][t], pt.slope) # Value from non-week-boundary is 0
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%)
  3. Time Elapsed: Since last checkpoint

The checkpoint system ensures:

  • Accurate Weight Tracking: Historical weight changes are properly applied
  • Fair Distribution: Emissions are distributed based on current weights
  • Gas Efficiency: Updates only when necessary

Integration with YB Token

The GaugeController integrates with the YB token through:

  • Minting Authority: Can mint YB tokens via emit() function
  • 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()
  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()