文件 1 的 1:Gaugecontroller.vy
# @version 0.3.10
"""
@title Gauge controller
@author Yearn Finance
@license GNU AGPLv3
@notice
Controls gauge emissions as defined in YIP-73.
- Gauges can be whitelisted, making them eligible for emissions in future epochs
- Accounts with a vote weight can distribute their weight over whitelisted gauges every epoch
- At the end of the epoch the tokens are minted and distributed according to their received vote weights
- Governance can reserve a percentage of emissions to specific gauges, prior to any votes
- These reserved emissions are taken out of the pool of votable emissions
- Votes can be blank. Emissions due to blank votes are partially burned and partially
reallocated to next epoch
"""
from vyper.interfaces import ERC20
interface Minter:
def mint(_epoch: uint256) -> uint256: nonpayable
interface Burner:
def burn(_epoch: uint256, _amount: uint256): nonpayable
interface Measure:
def total_vote_weight() -> uint256: view
def vote_weight(_account: address) -> uint256: view
genesis: public(immutable(uint256))
token: public(immutable(ERC20))
management: public(address)
pending_management: public(address)
whitelister: public(address)
legacy_operator: public(address)
measure: public(Measure)
minter: public(Minter)
burner: public(Burner)
packed_emission: uint256 # (epoch, global cumulative emission, global current emission)
blank_emission: public(uint256)
packed_epoch_emission: HashMap[uint256, uint256] # epoch => (_, emission, reserved)
packed_gauge_emission: HashMap[address, uint256] # gauge => (epoch, cumulative emission, current emission)
gauge_claimed: public(HashMap[address, uint256]) # gauge => claimed
blank_burn_points: public(uint256)
reserved_points: public(uint256)
packed_gauge_reserved: HashMap[address, uint256] # gauge => (_, points, last global cumulative emission)
gauge_whitelisted: public(HashMap[address, bool]) # gauge => whitelisted?
legacy_gauge: public(HashMap[address, bool]) # gauge => legacy?
votes: public(HashMap[uint256, uint256]) # epoch => total votes
votes_user: public(HashMap[address, HashMap[uint256, uint256]]) # user => epoch => votes
gauge_votes: public(HashMap[uint256, HashMap[address, uint256]]) # epoch => gauge => votes
gauge_votes_user: public(HashMap[address, HashMap[uint256, HashMap[address, uint256]]]) # user => epoch => gauge => votes
event NewEpoch:
epoch: uint256
emission: uint256
reserved: uint256
burned: uint256
blank: uint256
event Claim:
gauge: indexed(address)
amount: uint256
event Vote:
epoch: uint256
account: indexed(address)
gauge: indexed(address)
votes: uint256
event Whitelist:
gauge: indexed(address)
whitelisted: bool
event SetReservedPoints:
gauge: indexed(address)
points: uint256
event SetBlankBurnPoints:
points: uint256
event SetLegacyGauge:
gauge: indexed(address)
legacy: bool
event SetWhitelister:
whitelister: address
event SetLegacyOperator:
operator: address
event SetMeasure:
measure: address
event SetMinter:
minter: address
event SetBurner:
burner: address
event PendingManagement:
management: indexed(address)
event SetManagement:
management: indexed(address)
WEEK_LENGTH: constant(uint256) = 7 * 24 * 60 * 60
EPOCH_LENGTH: constant(uint256) = 2 * WEEK_LENGTH
POINTS_SCALE: constant(uint256) = 10_000
MASK: constant(uint256) = 2**112 - 1
EPOCH_MASK: constant(uint256) = 2**32 - 1
@external
def __init__(_genesis: uint256, _token: address, _measure: address, _minter: address, _burner: address):
"""
@notice Constructor
@param _genesis Genesis timestamp
@param _token Reward token address
@param _measure Vote weight measure
@param _minter Reward token minter
@param _burner Reward token burner
@dev Genesis should be picked at least one full epoch in the past
"""
genesis = _genesis
token = ERC20(_token)
self.management = msg.sender
self.whitelister = msg.sender
self.legacy_operator = msg.sender
self.measure = Measure(_measure)
self.minter = Minter(_minter)
self.burner = Burner(_burner)
epoch: uint256 = self._epoch()
self.packed_emission = self._pack(epoch - 1, 0, 0)
@external
@view
def epoch() -> uint256:
"""
@notice Get the current epoch
@return Current epoch
"""
return self._epoch()
@internal
@view
def _epoch() -> uint256:
return (block.timestamp - genesis) / EPOCH_LENGTH
@external
@view
def vote_open() -> bool:
"""
@notice Check whether the vote is currently open
@return True: vote is open, False: vote is closed
"""
return self._vote_open()
@internal
@view
def _vote_open() -> bool:
return (block.timestamp - genesis) % EPOCH_LENGTH >= WEEK_LENGTH
@external
@view
def votes_available(_account: address) -> uint256:
"""
@notice Get amount of votes still available
@param _account Account to check for
@return Amount of votes still available
"""
total: uint256 = self.measure.vote_weight(_account)
epoch: uint256 = self._epoch()
return total - self.votes_user[_account][epoch]
@external
@view
def emission() -> (uint256, uint256, uint256):
"""
@notice Get overall emission information
@return Tuple with:
- Last finalized epoch. At most one behind the current epoch
- Cumulative emission for all gauges until the last finalized epoch
- Emission for all gauges in the last finalized epoch
"""
return self._unpack(self.packed_emission)
@external
@view
def epoch_emission(_epoch: uint256) -> (uint256, uint256):
"""
@notice Get emission information for a specific epoch
@param _epoch Epoch
@return Tuple with:
- Emission for all gauges in the epoch
- Reserved emission in the epoch
@dev The total emission is inclusive of the reserved emission
"""
return self._unpack_two(self.packed_epoch_emission[_epoch])
@external
@view
def gauge_emission(_gauge: address) -> (uint256, uint256, uint256):
"""
@notice Get emission information for a specific gauge
@param _gauge Gauge address
@return Tuple with:
- Last updated epoch. At most equal to the last finalized epoch
- Cumulative emission for this gauge until the last updated epoch
- Emission for this gauge in the last finalized epoch
"""
return self._unpack(self.packed_gauge_emission[_gauge])
@external
@view
def gauge_reserved_points(_gauge: address) -> uint256:
"""
@notice Get reserved points for a gauge
@param _gauge Gauge address
@return Reserved points (bps)
@dev Gauges with reserved emissions receive a fixed percentage
of all the emissions, which is subtracted from the available
emissions for votes
"""
return self._unpack_two(self.packed_gauge_reserved[_gauge])[0]
@external
@view
def gauge_reserved_last_cumulative(_gauge: address) -> uint256:
"""
@notice Get gauge's last known overall cumulative emissions
@param _gauge Gauge address
@return Last known overall cumulative emission
@dev Used to fast-forward gauge emissions without having to iterate
"""
return self._unpack_two(self.packed_gauge_reserved[_gauge])[1]
@external
def vote(_gauges: DynArray[address, 32], _votes: DynArray[uint256, 32]):
"""
@notice Vote for specific gauges
@param _gauges Gauge addresses
@param _votes Votes as a fraction of users total vote weight (bps)
@dev Can be called multiple times
@dev Votes are additive, they cant be undone
@dev Votes can be blank by using the zero address
"""
assert len(_gauges) == len(_votes)
assert self._vote_open()
assert self._update_emission()
available: uint256 = self.measure.vote_weight(msg.sender)
epoch: uint256 = self._epoch()
used: uint256 = 0
for i in range(32):
if i == len(_gauges):
break
gauge: address = _gauges[i]
votes: uint256 = available * _votes[i] / POINTS_SCALE
if gauge != empty(address):
assert self.gauge_whitelisted[gauge]
self._update_gauge_emission(gauge)
self.gauge_votes[epoch][gauge] += votes
self.gauge_votes_user[msg.sender][epoch][gauge] += votes
used += votes
log Vote(epoch, msg.sender, gauge, votes)
assert used > 0
self.votes[epoch] += used
used += self.votes_user[msg.sender][epoch]
assert used <= available
self.votes_user[msg.sender][epoch] = used
@external
def claim(_gauge: address = empty(address), _recipient: address = empty(address)) -> (uint256, uint256, uint256):
"""
@notice Claim rewards for distribution by a gauge
@param _gauge Gauge address to claim for
@param _recipient Recipient of legacy gauge rewards
@dev Certain gauges are considered legacy, for which claiming is permissioned
"""
gauge: address = _gauge
if _gauge == empty(address):
gauge = msg.sender
recipient: address = gauge
if self.legacy_gauge[gauge]:
assert msg.sender == self.legacy_operator
assert _recipient != empty(address)
recipient = _recipient
assert self._update_emission()
gauge_epoch: uint256 = 0
cumulative: uint256 = 0
current: uint256 = 0
gauge_epoch, cumulative, current = self._update_gauge_emission(gauge)
claimed: uint256 = self.gauge_claimed[gauge]
claim: uint256 = cumulative - claimed
if claim > 0:
self.gauge_claimed[gauge] = cumulative
assert token.transfer(recipient, claim, default_return_value=True)
log Claim(_gauge, claim)
epoch_start: uint256 = genesis + self._epoch() * EPOCH_LENGTH
return cumulative, current, epoch_start
@external
def update_emission():
"""
@notice Update overall emissions
@dev Should be called by anyone to catch-up overall emissions if no
calls to this contract have been made for more than a full epoch
"""
for _ in range(32):
if self._update_emission():
return
@external
def whitelist(_gauge: address, _whitelisted: bool):
"""
@notice Add or remove a gauge to the whitelist
@param _gauge Gauge address
@param _whitelisted True: add to whitelist, False: remove from whitelist
@dev Only callable by the whitelister
"""
assert msg.sender == self.whitelister
assert _gauge != empty(address)
assert self._update_emission()
if _whitelisted == self.gauge_whitelisted[_gauge]:
return
if not _whitelisted:
self._update_gauge_emission(_gauge)
points: uint256 = 0
last: uint256 = 0
points, last = self._unpack_two(self.packed_gauge_reserved[_gauge])
if points > 0:
self.reserved_points -= points
self.packed_gauge_reserved[_gauge] = 0
self.gauge_whitelisted[_gauge] = _whitelisted
log Whitelist(_gauge, _whitelisted)
@external
def set_reserved_points(_gauge: address, _points: uint256):
"""
@notice Set the fraction of reserved emissions for a gauge
@param _gauge Gauge address
@param _points Reserved emission fraction (bps)
@dev Only callable by management
"""
assert msg.sender == self.management
assert self.gauge_whitelisted[_gauge]
assert self._update_emission()
self._update_gauge_emission(_gauge)
prev_points: uint256 = 0
last: uint256 = 0
prev_points, last = self._unpack_two(self.packed_gauge_reserved[_gauge])
if prev_points == 0:
# to save gas the cumulative amount is never updated for gauges without
# reserved points, so we have to do it here
last = self._unpack_two(self.packed_emission)[0]
total_points: uint256 = self.reserved_points - prev_points + _points
assert total_points <= POINTS_SCALE
self.reserved_points = total_points
self.packed_gauge_reserved[_gauge] = self._pack(0, _points, last)
log SetReservedPoints(_gauge, _points)
@external
def set_blank_burn_points(_points: uint256):
"""
@notice Set fraction of blank emissions to be burned
@param _points Blank burn fraction (bps)
@dev Only callable by management
"""
assert msg.sender == self.management
assert _points <= POINTS_SCALE
assert self._update_emission()
self.blank_burn_points = _points
log SetBlankBurnPoints(_points)
@external
def set_legacy_gauge(_gauge: address, _legacy: bool):
"""
@notice Set legacy status for a gauge
@param _gauge Gauge address
@param _legacy True: legacy, False: no legacy
@dev Only callable by management
"""
assert msg.sender == self.management
assert self.gauge_whitelisted[_gauge]
self.legacy_gauge[_gauge] = _legacy
log SetLegacyGauge(_gauge, _legacy)
@external
def set_whitelister(_whitelister: address):
"""
@notice Set new whitelister address
@param _whitelister New whitelister address
@dev Only callable by management
"""
assert msg.sender == self.management
self.whitelister = _whitelister
log SetWhitelister(_whitelister)
@external
def set_legacy_operator(_operator: address):
"""
@notice Set new legacy operator
@param _operator New operator address
@dev Only callable by management
"""
assert msg.sender == self.management
self.legacy_operator = _operator
log SetLegacyOperator(_operator)
@external
def set_measure(_measure: address):
"""
@notice Set vote weight measure
@param _measure Measure address
@dev Only callable by management
"""
assert msg.sender == self.management
assert _measure != empty(address)
assert not self._vote_open()
self.measure = Measure(_measure)
log SetMeasure(_measure)
@external
def set_minter(_minter: address):
"""
@notice Set new reward minter
@param _minter New minter address
@dev Only callable by management
"""
assert msg.sender == self.management
assert _minter != empty(address)
assert self._update_emission()
self.minter = Minter(_minter)
log SetMinter(_minter)
@external
def set_burner(_burner: address):
"""
@notice Set new reward burner
@param _burner New burner address
@dev Only callable by management
"""
assert msg.sender == self.management
assert _burner != empty(address)
assert self._update_emission()
self.burner = Burner(_burner)
log SetBurner(_burner)
@external
def set_management(_management: address):
"""
@notice
Set the pending management address.
Needs to be accepted by that account separately to transfer management over
@param _management New pending management address
"""
assert msg.sender == self.management
self.pending_management = _management
log PendingManagement(_management)
@external
def accept_management():
"""
@notice
Accept management role.
Can only be called by account previously marked as pending management by current management
"""
assert msg.sender == self.pending_management
self.pending_management = empty(address)
self.management = msg.sender
log SetManagement(msg.sender)
@internal
def _update_emission() -> bool:
"""
@notice
Update global emission. Must be called before:
- Any gauge is updated
- Any gauge receives votes
- Any gauge reserved points is changed
- Blank vote burn points is changed
"""
last_epoch: uint256 = self._epoch() - 1
global_epoch: uint256 = 0
global_cumulative: uint256 = 0
global_current: uint256 = 0
global_epoch, global_cumulative, global_current = self._unpack(self.packed_emission)
if global_epoch == last_epoch:
return True
global_epoch += 1
minted: uint256 = self.minter.mint(global_epoch) + self.blank_emission
reserved: uint256 = minted * self.reserved_points / POINTS_SCALE
blank_votes: uint256 = self.gauge_votes[global_epoch][empty(address)]
total_votes: uint256 = self.votes[global_epoch]
blank_emission: uint256 = 0
if blank_votes > 0:
blank_emission = (minted - reserved) * blank_votes / total_votes
elif total_votes == 0:
# no votes: all non-reserved emissions are considered blank
blank_emission = minted - reserved
burn_amount: uint256 = 0
if blank_emission > 0:
# blank emission is partially burned and partially added to next epoch
burn_amount = blank_emission * self.blank_burn_points / POINTS_SCALE
if burn_amount > 0:
blank_emission -= burn_amount
assert token.approve(self.burner.address, burn_amount, default_return_value=True)
self.burner.burn(global_epoch, burn_amount)
global_cumulative += minted
global_current = minted
self.packed_emission = self._pack(global_epoch, global_cumulative, global_current)
self.blank_emission = blank_emission
self.packed_epoch_emission[global_epoch] = self._pack(0, minted, reserved)
log NewEpoch(global_epoch, minted, reserved, burn_amount, blank_emission)
return global_epoch == last_epoch
@internal
def _update_gauge_emission(_gauge: address) -> (uint256, uint256, uint256):
"""
@notice
Update gauge emission. Must be called before:
- Gauge receives votes
- Gauge claim
- Gauge reserved points changes
Assumes global emission is up to date
"""
last_epoch: uint256 = self._epoch() - 1
gauge_epoch: uint256 = 0
cumulative: uint256 = 0
current: uint256 = 0
gauge_epoch, cumulative, current = self._unpack(self.packed_gauge_emission[_gauge])
if gauge_epoch == last_epoch:
return gauge_epoch, cumulative, current
# emission from last updated epoch
last_updated_epoch: uint256 = gauge_epoch + 1
current = self.gauge_votes[last_updated_epoch][_gauge]
if current > 0:
epoch_emission: uint256 = 0
epoch_reserved: uint256 = 0
epoch_emission, epoch_reserved = self._unpack_two(self.packed_epoch_emission[last_updated_epoch])
current = (epoch_emission - epoch_reserved) * current / self.votes[last_updated_epoch]
cumulative += current
if last_updated_epoch != last_epoch:
# last update was more than 1 epoch ago. the full amount is immediately available.
# we know the other missing epochs did not have any votes for this gauge because
# the gauge emissions would have been updated
current = 0
# fast-forward reserved emission
points: uint256 = 0
last: uint256 = 0
points, last = self._unpack_two(self.packed_gauge_reserved[_gauge])
if points > 0:
# we know that reserved points for this gauge has remained unchanged since the last
# gauge update, so we can safely get reserved rewards from potentially multiple epochs
# by applying the points to the change in global cumulative emission
global_cumulative: uint256 = 0
global_current: uint256 = 0
global_cumulative, global_current = self._unpack_two(self.packed_emission)
current += global_current * points / POINTS_SCALE
cumulative += (global_cumulative - last) * points / POINTS_SCALE
self.packed_gauge_reserved[_gauge] = self._pack(0, points, global_cumulative)
self.packed_gauge_emission[_gauge] = self._pack(last_epoch, cumulative, current)
return last_epoch, cumulative, current
@internal
@pure
def _pack(_epoch: uint256, _a: uint256, _b: uint256) -> uint256:
"""
@notice Pack a 32 bit number with two 112 bit numbers
"""
assert _epoch <= EPOCH_MASK and _a <= MASK and _b <= MASK
return (_epoch << 224) | (_a << 112) | _b
@internal
@pure
def _unpack(_packed: uint256) -> (uint256, uint256, uint256):
"""
@notice Unpack a 32 bit number followed by two 112 bit numbers
"""
return _packed >> 224, (_packed >> 112) & MASK, _packed & MASK
@internal
@pure
def _unpack_two(_packed: uint256) -> (uint256, uint256):
"""
@notice Unpack last two 112 bit numbers
"""
return (_packed >> 112) & MASK, _packed & MASK