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 tokensreward_tokens(uint256)
(IERC20
) - Get reward token by indexrewards(IERC20)
(Reward
) - Get reward info for tokenprocessed_rewards(IERC20)
(uint256
) - Amount of rewards processed for token
Checkpoint Data
integral_inv_supply()
(Integral
) - Global inverse supply integralintegral_inv_supply_4_token(IERC20)
(uint256
) - Token-specific inverse supply integralreward_rate_integral(IERC20)
(Integral
) - Reward rate integral per tokenreward_rate_integral_4_user(address, IERC20)
(uint256
) - User's reward rate integral per tokenuser_rewards_integral(address, IERC20)
(Integral
) - User's reward integral per tokenclaimed_rewards(address, IERC20)
(uint256
) - User's claimed rewards per token
Data Structs
Reward
distributor
(address
) - Address that can distribute rewardsfinish_time
(uint256
) - When reward distribution endstotal
(uint256
) - Total reward amount
Integral
v
(uint256
) - Valuet
(uint256
) - Timestamp
RewardIntegrals
integral_inv_supply
(Integral
) - Global inverse supply integralreward_rate_integral
(Integral
) - Reward rate integraluser_rewards_integral
(Integral
) - User reward integral
Events
AddReward (Emitted when a new reward token is added)
token
(address
) - Address of the reward tokendistributor
(address
) - Address of the reward distributorid
(uint256
) - Reward token ID
ChangeRewardDistributor (Emitted when a reward distributor is changed)
token
(address
) - Address of the reward tokendistributor
(address
) - New distributor address
DepositRewards (Emitted when rewards are deposited)
token
(address
) - Address of the reward tokendistributor
(address
) - Address of the distributoramount
(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.
Input | Type | Description |
---|---|---|
assets | uint256 | Amount of LP tokens to deposit |
receiver | address | Address 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.
Input | Type | Description |
---|---|---|
assets | uint256 | Amount of LP tokens to withdraw |
receiver | address | Address to receive tokens |
owner | address | Address 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.
Input | Type | Description |
---|---|---|
reward | IERC20 | Reward token to claim (defaults to YB) |
user | address | User 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.
Input | Type | Description |
---|---|---|
reward | IERC20 | Reward token to preview |
user | address | User 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.
Input | Type | Description |
---|---|---|
token | IERC20 | Reward token to add |
distributor | address | Address 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.
Input | Type | Description |
---|---|---|
token | IERC20 | Reward token to deposit |
amount | uint256 | Amount of tokens to deposit |
finish_time | uint256 | When 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.
Input | Type | Description |
---|---|---|
shares | uint256 | Number of shares to redeem |
receiver | address | Address to receive tokens |
owner | address | Address 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.
Input | Type | Description |
---|---|---|
to | address | Recipient address |
amount | uint256 | Amount 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