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 gaugesgauges(uint256)
(address
) - Get gauge address by indexis_killed(address)
(bool
) - Check if gauge is killed
Voting State
vote_user_slopes(address, address)
(VotedSlope
) - Get user's vote slope for a gaugevote_user_power(address)
(uint256
) - Get total voting power used by userlast_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 gaugegauge_weight(address)
(uint256
) - Get current gauge weightgauge_weight_sum()
(uint256
) - Sum of all gauge weightsadjusted_gauge_weight(address)
(uint256
) - Get adjusted weight for gaugeadjusted_gauge_weight_sum()
(uint256
) - Sum of adjusted weights
Emission Tracking
specific_emissions()
(uint256
) - Total emissions distributedspecific_emissions_per_gauge(address)
(uint256
) - Get emissions for specific gaugeweighted_emissions_per_gauge(address)
(uint256
) - Get weighted emissions for gaugesent_emissions_per_gauge(address)
(uint256
) - Get emissions already sent to gauge
Data Structs
Point
bias
(uint256
) - Current weight valueslope
(uint256
) - Rate of weight change
VotedSlope
slope
(uint256
) - Voting power slopebias
(uint256
) - Voting power bias (used if slope == 0)power
(uint256
) - Total voting powerend
(uint256
) - Lock end time
Events
VoteForGauge (Emitted when a user votes for a gauge)
time
(uint256
) - Timestamp of the voteuser
(address
) - Address of the votergauge_addr
(address
) - Address of the gauge being voted forweight
(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 gaugeis_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.
Input | Type | Description |
---|---|---|
gauge | address | Address 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.
Input | Type | Description |
---|---|---|
_gauge_addrs | DynArray[address, 50] | Array of gauge addresses to vote for |
_user_weights | DynArray[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.
Input | Type | Description |
---|---|---|
gauge | address | Address 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.
Input | Type | Description |
---|---|---|
addr | address | Gauge 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.
Input | Type | Description |
---|---|---|
gauge | address | Gauge 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.
Input | Type | Description |
---|---|---|
user | address | User 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.
Input | Type | Description |
---|---|---|
gauge | address | Address 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.
Input | Type | Description |
---|---|---|
gauge | address | Gauge address |
at_time | uint256 | Timestamp 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.
Input | Type | Description |
---|---|---|
gauge | address | Gauge address |
is_killed | bool | Whether 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.
Input | Type | Description |
---|---|---|
gauge | address | Address 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:
- Gauge Weight: Voting power allocated to each gauge
- Adjustment Factor: Gauge-specific adjustment (0-100%)
- 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
- Gauge Addition: Owner adds gauges via
add_gauge()
- Voting: Users vote on gauge weights via
vote_for_gauge_weights()
- Weight Updates: Weights are updated during checkpoints
- Emission Distribution: Gauges claim emissions via
emit()
- Governance: Owner can kill gauges via
set_killed()