Skip to main content

Liquidity Gauge

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

Overview

The LiquidityGauge contract is an ERC4626 vault that allows users to deposit LP tokens and earn rewards. It implements a sophisticated reward distribution system that tracks user deposits over time and distributes multiple reward tokens proportionally based on deposit duration and amount.

  • ERC4626 Vault: Standard vault functionality for LP token deposits
  • Multi-Reward System: Supports up to 8 different reward tokens
  • Time-Weighted Rewards: Rewards distributed based on deposit duration
  • YB Integration: Primary YB token rewards from GaugeController
  • Emergency Controls: Emergency admin can withdraw user funds if LT is killed
  • Checkpoint System: Maintains historical reward data for accurate distribution

Constants

  • MAX_REWARDS (uint256) - Maximum number of reward tokens (8)
  • MIN_SHARES_DECIMALS (uint8) - Minimum shares decimals (12)
  • VERSION (String[8]) - Contract version ("v1.0.0")

Public Variables

Core Contracts

  • GC() (GaugeController) - GaugeController contract (immutable)
  • YB() (IERC20) - YB token contract (immutable)
  • LP_TOKEN() (IERC20) - LP token being staked (immutable)
  • FACTORY() (Factory) - Factory contract (immutable)

Reward Management

  • reward_count() (uint256) - Number of active reward tokens
  • reward_tokens(uint256) (IERC20) - Get reward token by index
  • rewards(IERC20) (Reward) - Get reward info for 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) - User's reward rate integral per token
  • user_rewards_integral(address, IERC20) (Integral) - User's reward integral per token
  • claimed_rewards(address, IERC20) (uint256) - User's claimed rewards per token

Data 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

AddReward (Emitted when a new reward token is added)

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

ChangeRewardDistributor (Emitted when a reward distributor is changed)

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

DepositRewards (Emitted when rewards are deposited)

  • token (address) - Address of the reward token
  • distributor (address) - Address of the distributor
  • amount (uint256) - Amount of rewards deposited

Function Documentation

deposit()

Description:
Deposits LP tokens and mints gauge shares. Updates user checkpoints and triggers YB emissions.

Returns:
uint256 - Number of shares minted.

Emits:
Deposit - When tokens are deposited.

InputTypeDescription
assetsuint256Amount of LP tokens to deposit
receiveraddressAddress to receive shares

Requirements:

  • Assets must not exceed maximum deposit limit
  • LP token must not be killed
📄 View Source Code
@external
@nonreentrant
def deposit(assets: uint256, receiver: address) -> uint256:
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

withdraw()

Description:
Withdraws LP tokens by burning gauge shares. Updates user checkpoints and triggers YB emissions.

Returns:
uint256 - Number of shares burned.

Emits:
Withdraw - When tokens are withdrawn.

InputTypeDescription
assetsuint256Amount of LP tokens to withdraw
receiveraddressAddress to receive tokens
owneraddressAddress that owns the shares

Requirements:

  • Assets must not exceed maximum withdraw limit
  • Caller must be approved or owner
📄 View Source Code
@external
@nonreentrant
def withdraw(assets: uint256, receiver: address, owner: address) -> uint256:
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()

Description:
Claims accumulated rewards for a user. Updates checkpoints and transfers reward tokens.

Returns:
uint256 - Amount of rewards claimed.

Emits:
Transfer - When reward tokens are transferred.

InputTypeDescription
rewardIERC20Reward token to claim (defaults to YB)
useraddressUser to claim for (defaults to msg.sender)

Requirements:

  • Reward token must be registered
  • User must have unclaimed rewards
📄 View Source Code
@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()

Description:
Preview the amount of rewards a user can claim without executing the claim.

Returns:
uint256 - Preview of claimable rewards.

InputTypeDescription
rewardIERC20Reward token to preview
useraddressUser to preview for
📄 View Source Code
@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()

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

Emits:
AddReward - When a new reward token is added.

InputTypeDescription
tokenIERC20Reward token to add
distributoraddressAddress that can distribute rewards

Requirements:

  • Token must not be YB or LP_TOKEN
  • Token must not already be added
  • Distributor must not be zero address
📄 View Source Code
@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)

deposit_reward()

Description:
Deposits reward tokens into the gauge for distribution. Can be called by distributor or owner.

Emits:
DepositRewards - When rewards are deposited.

InputTypeDescription
tokenIERC20Reward token to deposit
amountuint256Amount of tokens to deposit
finish_timeuint256When reward distribution ends

Requirements:

  • Token must not be YB
  • Amount must be greater than 0
  • Caller must be distributor or owner
  • Finish time must be in the future
📄 View Source Code
@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()

Description:
Returns the adjustment factor based on the gauge's share of total LP supply. Used by GaugeController for weight calculations.

Returns:
uint256 - Adjustment factor (square root of gauge's LP share).

📄 View Source Code
@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()

Description:
Redeems shares for LP tokens. Includes emergency admin functionality for killed LT tokens.

Returns:
uint256 - Amount of LP tokens received.

Emits:
Withdraw - When tokens are withdrawn.

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

Requirements:

  • Shares must not exceed maximum redeem limit
  • If LT is killed, only emergency admin can withdraw for others
📄 View Source Code
@external
@nonreentrant
def redeem(shares: uint256, receiver: address, owner: address) -> uint256:
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()

Description:
Transfer gauge shares between users. Updates checkpoints for both sender and receiver.

Returns:
bool - True on success.

Emits:
Transfer - When shares are transferred.

InputTypeDescription
toaddressRecipient address
amountuint256Amount of shares to transfer

Requirements:

  • Sender must have sufficient balance
  • For transferFrom: sufficient allowance must be approved
📄 View Source Code
@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

Reward Distribution Mechanics

The LiquidityGauge uses a sophisticated integral-based reward distribution system that tracks user deposits over time through the _checkpoint() function, which updates three key integrals: global inverse supply integral (tracks total deposit changes), reward rate integral (tracks reward distribution rate per token), and user reward integral (tracks individual user's accumulated rewards). YB tokens are distributed as the primary reward through GaugeController integration with GC.emit() called during deposits/withdrawals, GC.preview_emissions() for claim previews, and get_adjustment() providing weight adjustment. The gauge supports up to 8 additional reward tokens beyond YB, added via add_reward() by the owner, with each reward having a designated distributor and time-based distribution over specified periods with dynamic rates based on remaining rewards and time.

Emergency Controls

The gauge includes emergency functionality for when the underlying LT token is killed, allowing the emergency admin to withdraw user funds to their own wallet. Emergency mode is determined by LT.is_killed(), with restricted access ensuring only the emergency admin can withdraw for others, and emergency withdrawals must go to the emergency admin's wallet.

Integration with GaugeController

The LiquidityGauge integrates with the GaugeController through weight calculations via get_adjustment(), YB emissions triggered by GC.emit(), emission previews via GC.preview_emissions() for claim calculations, and checkpoint triggers during deposits/withdrawals that update gauge checkpoints.

ERC4626 Compliance

The contract implements the ERC4626 vault standard:

  • Standard functions - deposit(), withdraw(), mint(), redeem()
  • Preview functions - previewDeposit(), previewWithdraw(), etc.
  • Transfer functions - transfer(), transferFrom() with checkpoint updates
  • Share calculations - Proper asset/share conversion with minimum shares