Skip to main content

LiquidityGauge

Overview​

The LiquidityGauge is the staking contract of a market. This page calls it the gauge. It is an ERC-4626-style vault: you deposit yb-LP shares into it, receive gauge shares, and earn rewards. yb-LP shares are the share tokens that LT, the market's vault contract, mints. This page calls them LT shares. The gauge's integral-based reward system tracks deposits over time. It distributes multiple reward tokens in proportion to deposit duration and amount.

The contract has these features:

  • ERC-4626 vault: standard vault functions for deposits of LT shares.
  • Multi-reward system: 8 reward slots, with YB in slot 0 and up to 7 extra reward tokens.
  • Time-weighted rewards: the gauge distributes rewards by deposit duration.
  • YB integration: the primary YB rewards come from the GaugeController, the contract that distributes YB emissions to gauges by veYB vote.
  • Emergency controls: when the LT is killed, the emergency admin can redeem the shares of any holder. The LT shares go to that holder.
  • Checkpoint system: the gauge keeps historical reward data for reward distribution.

Approvals​

Before you call deposit() or mint(), approve the gauge to spend your LT shares. Before you call deposit_reward(), approve the gauge to spend the reward token.

Constants​

The contract has these constants:

  • MAX_REWARDS (uint256) - Number of reward slots (8, including YB in slot 0)
  • MIN_SHARES_DECIMALS (uint8) - Minimum shares decimals (12). MIN_SHARES() returns 10**12: a gauge supply above 0 and below this value reverts with erc4626: leave MIN_SHARES.
  • VERSION (String[8]) - Contract version ("v1.0.0")

Gauge shares use the decimals of the LT, which are 18.

Public variables​

The contract has these public variables:

Core contracts

  • GC() (GaugeController) - GaugeController contract (immutable)
  • YB() (IERC20) - YB token contract (immutable)
  • LP_TOKEN() (IERC20) - The staked token: the LT of the market (immutable)
  • FACTORY() (Factory) - Factory contract (immutable)

Reward management

  • reward_count() (uint256) - Number of active reward tokens
  • reward_tokens(uint256) (IERC20) - Reward token by index
  • rewards(IERC20) (Reward) - Reward info of a token
  • processed_rewards(IERC20) (uint256) - Amount of rewards processed for token

Checkpoint data

  • integral_inv_supply() (Integral) - Global inverse supply integral
  • integral_inv_supply_4_token(IERC20) (uint256) - Token-specific inverse supply integral
  • reward_rate_integral(IERC20) (Integral) - Reward rate integral per token
  • reward_rate_integral_4_user(address, IERC20) (uint256) - Reward rate integral of an address per token
  • user_rewards_integral(address, IERC20) (Integral) - Reward integral of an address per token
  • claimed_rewards(address, IERC20) (uint256) - Claimed rewards of an address per token

Data structs​

The contract uses these structs:

Reward

  • distributor (address) - Address that can distribute rewards
  • finish_time (uint256) - When reward distribution ends
  • total (uint256) - Total reward amount

Integral

  • v (uint256) - Value
  • t (uint256) - Timestamp

RewardIntegrals

  • integral_inv_supply (Integral) - Global inverse supply integral
  • reward_rate_integral (Integral) - Reward rate integral
  • user_rewards_integral (Integral) - User reward integral

Events​

The contract emits these reward events:

AddReward (the owner adds a new reward token)

  • token (address, indexed) - Address of the reward token
  • distributor (address) - Address of the reward distributor
  • id (uint256) - Reward token ID

ChangeRewardDistributor (the owner changes a reward distributor)

  • token (address, indexed) - Address of the reward token
  • distributor (address) - New distributor address

DepositRewards (a distributor or the owner deposits rewards)

  • token (address, indexed) - Address of the reward token
  • distributor (address) - Address of the distributor
  • amount (uint256) - Amount of rewards deposited
  • finish_time (uint256) - Time when the distribution of this reward ends

Owner​

The owner adds extra reward tokens and changes their distributors. At deployment, the gauge stores Factory.admin() as its owner(). The Factory admin changed over time, so the owner differs between gauges. To check the owner of a gauge, call owner() on it.

The owner of a gauge is one of these contracts:

  • The YB DAO (decentralized autonomous organization).
  • A HybridFactoryOwner contract.
  • A MigrationFactoryOwner contract.

The deployed HybridFactoryOwner and MigrationFactoryOwner contracts that own gauges have no function that calls add_reward or change_reward_distributor on a gauge. So a gauge that one of these contracts owns cannot get a new extra reward token or a new reward distributor through its owner.

Functions​

Each function lists its own revert strings. Error codes holds the shared table.

deposit()​

Deposits LT shares and mints gauge shares. The function updates the checkpoint of receiver and triggers YB emissions.

The function returns the number of gauge shares minted, a uint256.

The function emits Deposit for the deposit.

The function takes these inputs:

InputTypeDescription
assetsuint256Amount of LT shares to deposit
receiveraddressAddress to receive the gauge shares

deposit() reverts in these cases:

  • erc4626: deposit more than maximum - assets is above maxDeposit(receiver), which returns the uint256 maximum.
  • Without a message - The LT transferFrom reverts: the caller's LT allowance for the gauge, or its LT balance, is below assets.
  • erc4626: transferFrom operation did not succeed - The LT transferFrom returns False.
  • erc20: mint to the zero address - receiver is the zero address.
  • erc4626: leave MIN_SHARES - The gauge supply after the deposit is above 0 and below MIN_SHARES.
  • Gauge not alive - From GaugeController.emit(): the GaugeController has not added this gauge.
Contract source: deposit()

The contract source gives this function body:

@external
@nonreentrant
def deposit(assets: uint256, receiver: address) -> uint256:
extcall LT(LP_TOKEN.address).checkpoint_staker_rebase()
assert assets <= erc4626._max_deposit(receiver), "erc4626: deposit more than maximum"
shares: uint256 = erc4626._preview_deposit(assets)
self._checkpoint_user(receiver)
erc4626._deposit(msg.sender, receiver, assets, shares)
erc4626._check_min_shares()
extcall GC.emit()
return shares

mint()​

Deposits LT shares for an exact number of gauge shares: mint(shares, receiver). The function returns the LT amount taken. It runs the same checkpoints as deposit().

mint() can revert with these strings:

  • erc4626: mint more than maximum
  • erc4626: leave MIN_SHARES
  • Gauge not alive
  • The token strings of deposit()

withdraw()​

Withdraws LT shares by burning gauge shares. The function updates the checkpoint of owner and triggers YB emissions.

The function returns the number of gauge shares burned, a uint256.

The function emits Withdraw for the withdrawal.

The function takes these inputs:

InputTypeDescription
assetsuint256Amount of LT shares to withdraw
receiveraddressAddress to receive the LT shares
owneraddressAddress that owns the gauge shares

withdraw() reverts in these cases:

  • erc4626: withdraw more than maximum - assets is above maxWithdraw(owner).
  • erc20: insufficient allowance - The caller is not owner and its allowance is below the shares to burn.
  • erc20: burn amount exceeds balance - The shares to burn, rounded up, exceed the balance of owner.
  • erc4626: transfer operation did not succeed - The LT transfer returns False.
  • Without a message - The LT transfer reverts: receiver is the zero address or the LT.
  • erc4626: leave MIN_SHARES - The gauge supply after the withdrawal is above 0 and below MIN_SHARES.
  • Gauge not alive - From GaugeController.emit(): the GaugeController has not added this gauge.
Contract source: withdraw()

The contract source gives this function body:

@external
@nonreentrant
def withdraw(assets: uint256, receiver: address, owner: address) -> uint256:
extcall LT(LP_TOKEN.address).checkpoint_staker_rebase()
assert assets <= erc4626._max_withdraw(owner), "erc4626: withdraw more than maximum"
shares: uint256 = erc4626._preview_withdraw(assets)
self._checkpoint_user(owner)
erc4626._withdraw(msg.sender, receiver, owner, assets, shares)
erc4626._check_min_shares()
extcall GC.emit()
return shares

claim()​

Claims the accumulated rewards of user. The function updates checkpoints and transfers the reward tokens.

The function returns the amount of rewards claimed, a uint256.

The reward token emits Transfer for the claimed amount.

The function takes these inputs:

InputTypeDescription
rewardIERC20Reward token to claim (defaults to YB)
useraddressAddress to claim for (defaults to the caller)

claim() reverts in these cases:

  • No reward - reward is not YB and the owner has not added it. YB is always registered.
  • Gauge not alive - From GaugeController.emit(): the GaugeController has not added this gauge. Raised only when reward is YB.
  • Without a message - The reward token transfer returns False.

If user has nothing to claim, the call transfers 0 and does not revert.

Contract source: claim()

The contract source gives this function body:

@external
@nonreentrant
def claim(reward: IERC20 = YB, user: address = msg.sender) -> uint256:
d_reward: uint256 = self._vest_rewards(reward, False)
r: RewardIntegrals = self._checkpoint(reward, d_reward, user)

self.integral_inv_supply = r.integral_inv_supply
self.integral_inv_supply_4_token[reward] = r.integral_inv_supply.v
self.reward_rate_integral[reward] = r.reward_rate_integral
self.reward_rate_integral_4_user[user][reward] = r.reward_rate_integral.v
self.user_rewards_integral[user][reward] = r.user_rewards_integral

d_reward = r.user_rewards_integral.v - self.claimed_rewards[user][reward]
self.claimed_rewards[user][reward] = r.user_rewards_integral.v

assert extcall reward.transfer(user, d_reward, default_return_value=True)
return d_reward

preview_claim()​

Previews the amount of rewards that user can claim, without running the claim.

The function returns the preview of claimable rewards, a uint256 in raw units of the reward token (YB uses 18 decimals).

The function takes these inputs:

InputTypeDescription
rewardIERC20Reward token to preview
useraddressAddress to preview for

preview_claim() reverts with No reward when reward is not YB and the owner has not added it.

Contract source: preview_claim()

The contract source gives this function body:

@external
@view
def preview_claim(reward: IERC20, user: address) -> uint256:
d_reward: uint256 = 0
if reward == YB:
d_reward = staticcall GC.preview_emissions(self, block.timestamp)
else:
d_reward = self._get_vested_rewards(reward)

r: RewardIntegrals = self._checkpoint(reward, d_reward, user)
return r.user_rewards_integral.v - self.claimed_rewards[user][reward]

add_reward()​

Adds a new reward token to the gauge. Only the owner can call it; Owner lists the possible owners. The gauge holds at most 7 extra reward tokens.

The function emits AddReward.

The function takes these inputs:

InputTypeDescription
tokenIERC20Reward token to add
distributoraddressAddress that can distribute rewards

add_reward() reverts in these cases:

  • YB - token is YB.
  • LP_TOKEN - token is the LT.
  • Without a message - distributor is the zero address, or all 8 reward slots are in use.
  • Already added - The token is in the reward list already.
  • ownable: caller is not the owner - The caller is not the owner.
Contract source: add_reward()

The contract source gives this function body:

@external
@nonreentrant
def add_reward(token: IERC20, distributor: address):
assert token != YB, "YB"
assert token != LP_TOKEN, "LP_TOKEN"
assert distributor != empty(address)
assert self.rewards[token].distributor == empty(address), "Already added"
erc4626.ownable._check_owner()
self.rewards[token].distributor = distributor
reward_id: uint256 = self.reward_count
self.reward_tokens[reward_id] = token
self.reward_count = reward_id + 1
log AddReward(token=token.address, distributor=distributor, id=reward_id)

change_reward_distributor()​

Changes the distributor of an extra (non-YB) reward token: change_reward_distributor(token, distributor). Only the owner can call it. The token must already be added.

The function emits ChangeRewardDistributor.

change_reward_distributor() reverts in these cases:

  • YB - token is YB.
  • Not added - The owner has not added token.
  • ownable: caller is not the owner - The caller is not the owner.
  • Without a message - distributor is the zero address.

deposit_reward()​

Deposits reward tokens into the gauge for distribution. The distributor of token or the owner can call it.

The function emits DepositRewards.

The function takes these inputs:

InputTypeDescription
tokenIERC20Reward token to deposit
amountuint256Amount of tokens to deposit
finish_timeuint256When reward distribution ends. If rewards are left, a value of 0 keeps the current rate and extends the end time.

deposit_reward() reverts in these cases:

  • YB - token is YB.
  • No rewards - amount is 0.
  • ownable: caller is not the owner - The caller is neither the distributor of token nor the owner.
  • No reward - The owner calls it for a token that the owner has not added.
  • Finishes in the past - finish_time is not in the future, and either finish_time is not 0 or no rewards are left.
  • Rate unknown - finish_time is 0 and rewards are left, but the current distribution has ended.
  • Without a message - The token transferFrom returns False.
Contract source: deposit_reward()

The contract source gives this function body:

@external
@nonreentrant
def deposit_reward(token: IERC20, amount: uint256, finish_time: uint256):
assert token != YB, "YB"
assert amount > 0, "No rewards"
r: Reward = self.rewards[token]

if msg.sender != r.distributor:
erc4626.ownable._check_owner()

d_reward: uint256 = self._vest_rewards(token, False)
ri: RewardIntegrals = self._checkpoint(token, d_reward, empty(address))
self.integral_inv_supply = ri.integral_inv_supply
self.integral_inv_supply_4_token[token] = ri.integral_inv_supply.v
self.reward_rate_integral[token] = ri.reward_rate_integral

unused_rewards: uint256 = r.total - self.processed_rewards[token]

if finish_time > 0 or unused_rewards == 0:
assert finish_time > block.timestamp, "Finishes in the past"
r.finish_time = finish_time
else:
assert r.finish_time > block.timestamp, "Rate unknown"
r.finish_time = block.timestamp + (r.finish_time - block.timestamp) * (unused_rewards + amount) // unused_rewards
r.total += amount

self.rewards[token] = r
assert extcall token.transferFrom(msg.sender, self, amount, default_return_value=True)
log DepositRewards(token=token.address, distributor=msg.sender, amount=amount, finish_time=r.finish_time)

get_adjustment()​

Returns the adjustment factor of the gauge, based on the gauge's share of the total LT supply. The GaugeController uses it for weight calculations.

The function returns the adjustment factor, a uint256 where 1e18 = 1.0: the square root of the gauge's share of the LT supply. For example, when the gauge holds 25% of the LT supply, the call returns isqrt(25 * 10**34) = 5 × 10^17, or 0.5. It returns 0 when the LT supply is 0.

The function has no revert strings.

Contract source: get_adjustment()

The contract source gives this function body:

@external
@view
def get_adjustment() -> uint256:
staked: uint256 = staticcall LP_TOKEN.balanceOf(self)
supply: uint256 = staticcall LP_TOKEN.totalSupply()
return isqrt(unsafe_div(staked * 10**36, supply))

redeem()​

Redeems gauge shares for LT shares. The function includes an emergency-admin path for a killed LT. The emergency admin can redeem the shares of any holder. The LT shares go to that holder, because receiver must equal owner.

The function returns the amount of LT shares received, a uint256.

The function emits Withdraw for the redemption.

The function takes these inputs:

InputTypeDescription
sharesuint256Number of gauge shares to redeem
receiveraddressAddress to receive the LT shares
owneraddressAddress that owns the gauge shares

redeem() reverts in these cases:

  • erc4626: redeem more than maximum - shares is above the share balance of owner.
  • receiver - The LT is killed, the caller is the emergency admin, and receiver is not owner.
  • erc20: insufficient allowance - The caller is not owner, the emergency-admin path does not apply, and the allowance is below shares.
  • erc4626: transfer operation did not succeed - The LT transfer returns False.
  • Without a message - The LT transfer reverts: receiver is the zero address or the LT.
  • erc4626: leave MIN_SHARES - The gauge supply after the redeem is above 0 and below MIN_SHARES.
  • Gauge not alive - From GaugeController.emit(): the GaugeController has not added this gauge.
Contract source: redeem()

The contract source gives this function body:

@external
@nonreentrant
def redeem(shares: uint256, receiver: address, owner: address) -> uint256:
extcall LT(LP_TOKEN.address).checkpoint_staker_rebase()
assert shares <= erc4626._max_redeem(owner), "erc4626: redeem more than maximum"

sender: address = msg.sender
if staticcall LT(LP_TOKEN.address).is_killed():
if msg.sender == staticcall FACTORY.emergency_admin():
assert receiver == owner, "receiver"
sender = owner

assets: uint256 = erc4626._preview_redeem(shares)
self._checkpoint_user(owner)
erc4626._withdraw(sender, receiver, owner, assets, shares)
erc4626._check_min_shares()
extcall GC.emit()
return assets

transfer() and transferFrom()​

The functions transfer gauge shares between addresses. Both functions update the checkpoints of the sender and the receiver before the ERC-20 transfer, then trigger a GaugeController emit().

The functions return True on success, a bool.

The functions emit Transfer for the shares.

The functions take these inputs:

InputTypeDescription
toaddressRecipient address
amountuint256Amount of shares to transfer

The functions revert in these cases:

  • erc20: transfer amount exceeds balance - The sender's share balance is below amount.
  • erc20: transfer to the zero address - to is the zero address.
  • erc20: insufficient allowance - transferFrom only: the caller's allowance is below amount.
  • Gauge not alive - From GaugeController.emit(): the GaugeController has not added this gauge.
Contract source: transfer() and transferFrom()

The contract source gives these function bodies:

@external
@nonreentrant
def transfer(to: address, amount: uint256) -> bool:
self._checkpoint_user(msg.sender)
self._checkpoint_user(to)
erc4626.erc20._transfer(msg.sender, to, amount)
extcall GC.emit()
return True

@external
@nonreentrant
def transferFrom(owner: address, to: address, amount: uint256) -> bool:
self._checkpoint_user(owner)
self._checkpoint_user(to)
erc4626.erc20._spend_allowance(owner, msg.sender, amount)
erc4626.erc20._transfer(owner, to, amount)
extcall GC.emit()
return True

Share conversion​

convertToAssets(shares) and previewRedeem(shares) return the amount of LT shares for a number of gauge shares. convertToShares(assets) and previewDeposit(assets) go the other way. All four round down; inputs and outputs use 18 decimals.

The conversion uses these views:

  • convertToAssets(shares: uint256) -> uint256
  • convertToShares(assets: uint256) -> uint256
  • totalAssets() -> uint256: the staked LT balance after the pending rebase that LT.checkpoint_staker_rebase() applies, LT.updated_balances()[1].

While totalSupply() is above 0, the conversion is shares × (totalAssets() + 1) // (totalSupply() + 1). On the gauges of markets 0 to 2, ERC-4626 compliance gives the difference.

In this example, totalSupply() is 1,000 × 10^18 gauge shares and totalAssets() is 990 × 10^18 LT shares. convertToAssets(10**18) returns 990,000,000,000,000,000. One gauge share redeems for 0.99 LT shares.

Reward distribution​

The LiquidityGauge distributes rewards through integrals that _checkpoint() updates. Three integrals track state:

  • The global inverse-supply integral (total deposit changes).
  • The reward-rate integral (distribution rate per token).
  • The user reward integral (each address's accumulated rewards).

YB is the primary reward. The GaugeController supplies it in three ways:

  • GC.emit() runs on deposits, withdrawals, and transfers.
  • GC.preview_emissions() previews claims.
  • get_adjustment() supplies the weight adjustment.

Deposits and withdrawals also update the gauge checkpoint in the GaugeController.

The gauge supports up to 7 extra reward tokens beyond YB. The owner adds them with add_reward(). Each has a designated distributor and streams over a set period. The gauge recomputes the rate from the remaining rewards and time.

Emergency controls​

When the underlying LT is killed (LT.is_killed()), the emergency admin (Factory.emergency_admin()) can call redeem for any holder. The receiver must equal the share owner, so the LT shares go to the holder, not to the emergency admin.

While the LT is not killed, deposit, mint, withdraw, and redeem first call LT.checkpoint_staker_rebase(), which reads AMM.value_oracle(). If value_oracle() reverts, these calls revert, so holders cannot unstake until the oracle works again or the LT is killed. After a kill, checkpoint_staker_rebase() does nothing, and withdraw and redeem work without the oracle. For the calls that work in each market state, read Market states and allowed calls in LT.

ERC-4626 compliance​

The contract implements these parts of the ERC-4626 vault standard:

  • Standard functions: deposit(), withdraw(), mint(), redeem()
  • Preview functions: previewDeposit(), previewWithdraw(), previewRedeem(). On the gauges of markets 3 and later, previewMint() is not callable, because the source has no @external decorator on it. Use previewDeposit() or previewRedeem().
  • Transfer functions: transfer(), transferFrom() with checkpoint updates
  • Share calculations: asset/share conversion with minimum shares, as in Share conversion

The gauges of markets 0 to 2 run an older version. On them, previewMint() is callable. Their ERC-4626 views use the stored LT balance of the gauge and do not apply the pending rebase.