LiquidityGaugeV2
The V2 liquidity gauge adds a full ERC20 interface to the gauge, tokenizing deposits so they can be directly transferred between accounts without having to withdraw and redeposit. It also improves flexibility for onward staking, allowing staking to be enabled or disabled at any time and handling up to eight reward tokens at once.
Source Code
Source code of the LiquidityGaugeV2 can be found on Github. The following view methods and functions are using the AAVE liquidity gauge.
Admin Ownership¶
Liquidity Gauge V2 also added admin ownership. admin
is able to kill a gauge and add a reward contract.
admin
¶
LiquidityGaugeV2.admin() -> address: view
Getter for the admin of the gauge contract.
Returns: admin (address
).
Note
Admin can be changed by calling commit_transfer_ownership
.
Source code
admin: public(address)
@external
def __init__(_lp_token: address, _minter: address, _admin: address):
"""
@notice Contract constructor
@param _lp_token Liquidity Pool contract address
@param _minter Minter contract address
@param _admin Admin who can kill the gauge
"""
symbol: String[26] = ERC20Extended(_lp_token).symbol()
self.name = concat("Curve.fi ", symbol, " Gauge Deposit")
self.symbol = concat(symbol, "-gauge")
crv_token: address = Minter(_minter).token()
controller: address = Minter(_minter).controller()
self.lp_token = _lp_token
self.minter = _minter
self.admin = _admin
self.crv_token = crv_token
self.controller = controller
self.voting_escrow = Controller(controller).voting_escrow()
self.period_timestamp[0] = block.timestamp
self.inflation_rate = CRV20(crv_token).rate()
self.future_epoch_time = CRV20(crv_token).future_epoch_time_write()
@external
def accept_transfer_ownership():
"""
@notice Accept a pending ownership transfer
"""
_admin: address = self.future_admin
assert msg.sender == _admin # dev: future admin only
self.admin = _admin
log ApplyOwnership(_admin)
future_admin
¶
LiquidityGaugeV2.future_admin() -> address: view
Getter for the future admin of the gauge contract.
Returns: future admin (address
).
Note
Future admin is set by calling commit_transfer_ownership
. New admin ownership then needs to be applied via accept_transfer_ownership
.
Source code
future_admin: public(address) # Can and will be a smart contract
@external
def commit_transfer_ownership(addr: address):
"""
@notice Transfer ownership of GaugeController to `addr`
@param addr Address to have ownership transferred to
"""
assert msg.sender == self.admin # dev: admin only
self.future_admin = addr
log CommitOwnership(addr)
@external
def accept_transfer_ownership():
"""
@notice Accept a pending ownership transfer
"""
_admin: address = self.future_admin
assert msg.sender == _admin # dev: future admin only
self.admin = _admin
log ApplyOwnership(_admin)
commit_transfer_ownership
¶
LiquidityGaugeV2.commit_transfer_ownership(addr: address):
Function to commit transfer ownership.
Emits: CommitOwnership
Input | Type | Description |
---|---|---|
addr | address | Address to transfer ownership to |
Source code
event CommitOwnership:
admin: address
admin: public(address)
future_admin: public(address) # Can and will be a smart contract
@external
def commit_transfer_ownership(addr: address):
"""
@notice Transfer ownership of GaugeController to `addr`
@param addr Address to have ownership transferred to
"""
assert msg.sender == self.admin # dev: admin only
self.future_admin = addr
log CommitOwnership(addr)
Note
Only callable by admin
.
accept_transfer_ownership
¶
LiquidityGaugeV2.accept_transfer_ownership(addr: address):
Function to apply the admin changes.
Emits: ApplyOwnership
Input | Type | Description |
---|---|---|
addr | address | Address to accept the admin changes |
Note
This function can only be called by future_admin
.
Source code
event ApplyOwnership:
admin: address
admin: public(address)
future_admin: public(address) # Can and will be a smart contract
@external
def accept_transfer_ownership():
"""
@notice Accept a pending ownership transfer
"""
_admin: address = self.future_admin
assert msg.sender == _admin # dev: future admin only
self.admin = _admin
log ApplyOwnership(_admin)
Checking and Claiming Rewards¶
Note
Rewards are claimed automatically each time a user deposits or withdraws from the gauge, and on gauge token transfers.
claimable_rewards
¶
LiquidityGaugeV2.claimable_reward(_addr: address, _token: address) -> uint256:
Getter for the number of claimable reward token _token
for user addr
.
Returns: claimable reward amount (uint256
).
Input | Type | Description |
---|---|---|
_addr | address | Address to get reward amount for |
_token | address | Token to get reward amount for |
Warning
This function determines the claimable reward by actually claiming and then returning the received amount. As such, it is state changing and only of use to off-chain integrators. The mutability should be manually changed to view
within the ABI.
Source code
@external
@nonreentrant('lock')
def claimable_reward(_addr: address, _token: address) -> uint256:
"""
@notice Get the number of claimable reward tokens for a user
@dev This function should be manually changed to "view" in the ABI
Calling it via a transaction will claim available reward tokens
@param _addr Account to get reward amount for
@param _token Token to get reward amount for
@return uint256 Claimable reward token amount
"""
claimable: uint256 = ERC20(_token).balanceOf(_addr)
if self.reward_contract != ZERO_ADDRESS:
self._checkpoint_rewards(_addr, self.totalSupply)
claimable = ERC20(_token).balanceOf(_addr) - claimable
integral: uint256 = self.reward_integral[_token]
integral_for: uint256 = self.reward_integral_for[_token][_addr]
if integral_for < integral:
claimable += self.balanceOf[_addr] * (integral - integral_for) / 10**18
return claimable
claim_rewards
¶
LiquidityGaugeV2.claim_rewards(_addr: address = msg.sender):
Function to claim reward tokens for addr
.
Input | Type | Description |
---|---|---|
_addr | address | Address to claim for (defaulted to msg.sender (caller) if no input) |
Source code
claim_historic_rewards
¶
LiquidityGaugeV2.claim_historic_rewards(_reward_tokens: address[MAX_REWARDS], _addr: address = msg.sender):
Function to claim reward tokens available from a previously-set staking contract.
Input | Type | Description |
---|---|---|
_reward_tokens | address | Array of reward token addresses to claim |
_addr | address | Address to claim for (defaulted to msg.sender (caller) if no input) |
Source code
# reward token -> integral
reward_integral: public(HashMap[address, uint256])
# reward token -> claiming address -> integral
reward_integral_for: public(HashMap[address, HashMap[address, uint256]])
@external
@nonreentrant('lock')
def claim_historic_rewards(_reward_tokens: address[MAX_REWARDS], _addr: address = msg.sender):
"""
@notice Claim reward tokens available from a previously-set staking contract
@param _reward_tokens Array of reward token addresses to claim
@param _addr Address to claim for
"""
for token in _reward_tokens:
if token == ZERO_ADDRESS:
break
integral: uint256 = self.reward_integral[token]
integral_for: uint256 = self.reward_integral_for[token][_addr]
if integral_for < integral:
claimable: uint256 = self.balanceOf[_addr] * (integral - integral_for) / 10**18
self.reward_integral_for[token][_addr] = integral
response: Bytes[32] = raw_call(
token,
concat(
method_id("transfer(address,uint256)"),
convert(_addr, bytes32),
convert(claimable, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
Setting the Rewards Contract¶
set_rewards
¶
LiquidityGaugeV2.set_rewards(_reward_tokens: address[MAX_REWARDS], _addr: address = msg.sender):
Function to set the active reward contract.
Input | Type | Description |
---|---|---|
_reward_contract | address | Address of staking contract. Set to ZERO_ADDRESS if staking rewards are being removed |
_sigs | bytes32 | A concatenation of three four-byte function signatures: stake , withdraw and getReward . The signatures are then right padded with empty bytes. |
_reward_tokens | address | Array of reward tokens received from the staking contract |
Note
This action is only possible via the contract admin. It cannot be called when the gauge has no deposits. As a safety precaution, this call validates all the signatures with the following sequence of actions:
1. LP tokens are deposited into the new staking contract, verifying that the deposit signature is correct.
2. balanceOf
is called on the LP token to confirm that the gauge’s token balance is not zero.
3. The LP tokens are withdrawn, verifying that the withdraw function signature is correct.
4. balanceOf
is called on the LP token again, to confirm that the gauge has successfully withdrawn it’s entire balance.
5. A call to claim rewards is made to confirm that it does not revert.
These checks are required to protect against an incorrectly designed staking contract or incorrectly structured input arguments.
Note
It is also possible to claim from a reward contract that does not require onward staking. In this case, use 00000000
for the function selectors for both staking and withdrawing.
Source code
@external
@nonreentrant('lock')
def set_rewards(_reward_contract: address, _sigs: bytes32, _reward_tokens: address[MAX_REWARDS]):
"""
@notice Set the active reward contract
@dev A reward contract cannot be set while this contract has no deposits
@param _reward_contract Reward contract address. Set to ZERO_ADDRESS to
disable staking.
@param _sigs Four byte selectors for staking, withdrawing and claiming,
right padded with zero bytes. If the reward contract can
be claimed from but does not require staking, the staking
and withdraw selectors should be set to 0x00
@param _reward_tokens List of claimable tokens for this reward contract
"""
assert msg.sender == self.admin
lp_token: address = self.lp_token
current_reward_contract: address = self.reward_contract
total_supply: uint256 = self.totalSupply
if current_reward_contract != ZERO_ADDRESS:
self._checkpoint_rewards(ZERO_ADDRESS, total_supply)
withdraw_sig: Bytes[4] = slice(self.reward_sigs, 4, 4)
if convert(withdraw_sig, uint256) != 0:
if total_supply != 0:
raw_call(
current_reward_contract,
concat(withdraw_sig, convert(total_supply, bytes32))
)
ERC20(lp_token).approve(current_reward_contract, 0)
if _reward_contract != ZERO_ADDRESS:
assert _reward_contract.is_contract # dev: not a contract
sigs: bytes32 = _sigs
deposit_sig: Bytes[4] = slice(sigs, 0, 4)
withdraw_sig: Bytes[4] = slice(sigs, 4, 4)
if convert(deposit_sig, uint256) != 0:
# need a non-zero total supply to verify the sigs
assert total_supply != 0 # dev: zero total supply
ERC20(lp_token).approve(_reward_contract, MAX_UINT256)
# it would be Very Bad if we get the signatures wrong here, so
# we do a test deposit and withdrawal prior to setting them
raw_call(
_reward_contract,
concat(deposit_sig, convert(total_supply, bytes32))
) # dev: failed deposit
assert ERC20(lp_token).balanceOf(self) == 0
raw_call(
_reward_contract,
concat(withdraw_sig, convert(total_supply, bytes32))
) # dev: failed withdraw
assert ERC20(lp_token).balanceOf(self) == total_supply
# deposit and withdraw are good, time to make the actual deposit
raw_call(
_reward_contract,
concat(deposit_sig, convert(total_supply, bytes32))
)
else:
assert convert(withdraw_sig, uint256) == 0 # dev: withdraw without deposit
self.reward_contract = _reward_contract
self.reward_sigs = _sigs
for i in range(MAX_REWARDS):
if _reward_tokens[i] != ZERO_ADDRESS:
self.reward_tokens[i] = _reward_tokens[i]
elif self.reward_tokens[i] != ZERO_ADDRESS:
self.reward_tokens[i] = ZERO_ADDRESS
else:
assert i != 0 # dev: no reward token
break
if _reward_contract != ZERO_ADDRESS:
# do an initial checkpoint to verify that claims are working
self._checkpoint_rewards(ZERO_ADDRESS, total_supply)
reward_contract
¶
LiquidityGaugeV2.reward_contract() -> address: view
Getter for the reward contract.
Returns: reward contract (address
).
reward_tokens
¶
LiquidityGaugeV2.reward_tokens(arg0: uint256) -> address: view
Getter for the reward contract.
Returns: reward contract (address
).
Input | Type | Description |
---|---|---|
arg0 | uint256 | Index of reward token |
Killing Gauges¶
V2 Liquidity Gauges introduced the possibility to kill gauges. Killing a gauge sets the rate
in _checkpoint
to 0 and therefore stopping inflation.
is_killed
¶
LiquidityGaugeV2.is_killed(addr: address):
Getter for the killed status for the gauge.
Returns: true of false (bool
).
Source code
set_killed
¶
LiquidityGaugeV2.set_killed(_is_killed: bool):
Function to set the killed status of a gauge.
Input | Type | Description |
---|---|---|
_is_killed | bool | true of flase |
Source code
Note
Only callable by admin
.
Querying Gauge Information¶
decimals
¶
LiquidityGaugeV2.decimals() -> uint256:
Getter for the decimals of the liquidity gauge token.
Returns: decimals (uint256
).
Source code
name
¶
LiquidityGaugeV2.name() -> String[64]: view
Getter for the name of the lp gauge token.
Returns: token name (String[64]
).
Source code
name: public(String[64])
@external
def __init__(_lp_token: address, _minter: address, _admin: address):
"""
@notice Contract constructor
@param _lp_token Liquidity Pool contract address
@param _minter Minter contract address
@param _admin Admin who can kill the gauge
"""
symbol: String[26] = ERC20Extended(_lp_token).symbol()
self.name = concat("Curve.fi ", symbol, " Gauge Deposit")
self.symbol = concat(symbol, "-gauge")
crv_token: address = Minter(_minter).token()
controller: address = Minter(_minter).controller()
self.lp_token = _lp_token
self.minter = _minter
self.admin = _admin
self.crv_token = crv_token
self.controller = controller
self.voting_escrow = Controller(controller).voting_escrow()
self.period_timestamp[0] = block.timestamp
self.inflation_rate = CRV20(crv_token).rate()
self.future_epoch_time = CRV20(crv_token).future_epoch_time_write()
symbol
¶
LiquidityGaugeV2.symbol() -> String[32]: view
Getter for the symbol of the lp gauge token.
Returns: symbol (String[32]
).
Source code
symbol: public(String[32])
@external
def __init__(_lp_token: address, _minter: address, _admin: address):
"""
@notice Contract constructor
@param _lp_token Liquidity Pool contract address
@param _minter Minter contract address
@param _admin Admin who can kill the gauge
"""
symbol: String[26] = ERC20Extended(_lp_token).symbol()
self.name = concat("Curve.fi ", symbol, " Gauge Deposit")
self.symbol = concat(symbol, "-gauge")
crv_token: address = Minter(_minter).token()
controller: address = Minter(_minter).controller()
self.lp_token = _lp_token
self.minter = _minter
self.admin = _admin
self.crv_token = crv_token
self.controller = controller
self.voting_escrow = Controller(controller).voting_escrow()
self.period_timestamp[0] = block.timestamp
self.inflation_rate = CRV20(crv_token).rate()
self.future_epoch_time = CRV20(crv_token).future_epoch_time_write()
reward_integral
¶
LiquidityGaugeV2.reward_integral(arg0: uint256) -> uint256: view
Getter for the reward integral.
Returns: reward integral (uint256
).
Input | Type | Description |
---|---|---|
arg0 | uint256 | Index of reward token |
reward_integral_for
(todo)¶
LiquidityGaugeV2.reward_integral_for(arg0: uint256, arg1: uint256) -> uint256: view
todo
Returns: todo
Input | Type | Description |
---|---|---|
arg0 | uint256 | todo |
arg1 | uint256 | todo |