FeeDistributor
Fees used to be distributed to veCRV in the form of 3CRV tokens, the LP token of the threepool, which consists of USDT, USDC, and DAI. After the release of Curve's own stablecoin crvUSD and following a successful DAO vote to change the reward token to it, a new FeeDistributor contract was deployed to distribute fees in the form of crvUSD tokens. Fee claiming always takes place on Ethereum.
FeeDistributor.vylast_token_time
FeeDistributor.last_token_time() -> uint256: viewGetter for the timestamp of the last token checkpoint.
Returns: timestamp (uint256).
<>Source code▼
last_token_time: public(uint256)
▶Example▼
can_checkpoint_token
FeeDistributor.can_checkpoint_token() -> bool: viewFunction to check whether the checkpoint_token function can be called by anyone or only by the admin. The state of this variable can be changed using the toggle_allow_checkpoint_token function.
Returns: true or false (bool).
<>Source code▼
can_checkpoint_token: public(bool)
▶Example▼
toggle_allow_checkpoint_token
FeeDistributor.toggle_allow_checkpoint_token()This function is only callable by the admin of the contract.
Function to toggle permission for checkpointing by an account.
<>Source code▼
event ToggleAllowCheckpointToken:
toggle_flag: bool
@external
def toggle_allow_checkpoint_token():
"""
@notice Toggle permission for checkpointing by any account
"""
assert msg.sender == self.admin
flag: bool = not self.can_checkpoint_token
self.can_checkpoint_token = flag
log ToggleAllowCheckpointToken(flag)
▶Example▼
>>> FeeDistributor.toggle_allow_checkpoint_token()
ve-Supply Checkpoint
Checkpointing the ve-Supply is an essential process to ensure fair reward distribution. It involves periodically recording the total supply of veCRV for each epoch. This process is crucial for accurately distributing fees to veCRV holders based on their balances.
checkpoint_total_supply
FeeDistributor.checkpoint_total_supply()Function to update the total supply checkpoint of veCRV for each epoch. The checkpoint is also updated by the first claimant of each new epoch week. This function can be called independently of a claim to reduce claiming gas costs. It ensures that the contract maintains an accurate record of the total veCRV supply at the start of each week, which is essential for correctly distributing fees based on veCRV holdings.
<>Source code▼
@external
def checkpoint_total_supply():
"""
@notice Update the veCRV total supply checkpoint
@dev The checkpoint is also updated by the first claimant each
new epoch week. This function may be called independently
of a claim, to reduce claiming gas costs.
"""
self._checkpoint_total_supply()
@internal
def _checkpoint_total_supply():
ve: address = self.voting_escrow
t: uint256 = self.time_cursor
rounded_timestamp: uint256 = block.timestamp / WEEK * WEEK
VotingEscrow(ve).checkpoint()
for i in range(20):
if t > rounded_timestamp:
break
else:
epoch: uint256 = self._find_timestamp_epoch(ve, t)
pt: Point = VotingEscrow(ve).point_history(epoch)
dt: int128 = 0
if t > pt.ts:
# If the point is at 0 epoch, it can actually be earlier than the first deposit
# Then make dt 0
dt = convert(t - pt.ts, int128)
self.ve_supply[t] = convert(max(pt.bias - pt.slope * dt, 0), uint256)
t += WEEK
self.time_cursor = t
▶Example▼
This example checkpoints the total supply of veCRV.
>>> FeeDistributor.checkpoint_total_supply()
time_cursor
FeeDistributor.time_cursor() -> uint256: viewGetter for the timestamp of the last checkpoint_total_supply of veCRV.
Returns: timestamp (uint256).
<>Source code▼
time_cursor: public(uint256)
▶Example▼
time_cursor_of
FeeDistributor.time_cursor_of(arg0: address) -> uint256: viewGetter for the timestamp of the last checkpoint_total_supply of veCRV.
Returns: timestamp (uint256).
| Input | Type | Description |
|---|---|---|
arg0 | address | Address to check for |
<>Source code▼
time_cursor_of: public(HashMap[address, uint256])
▶Example▼
This example returns the time_cursor_of for a given address.
>>> FeeDistributor.time_cursor_of('0x7a16fF8270133F063aAb6C9977183D9e72835428')
1719446400
ve_for_at
FeeDistributor.ve_for_at(_user: address, _timestamp: uint256) -> uint256: viewGetter for the veCRV balance of a user at a certain timestamp.
Returns: veCRV balance (uint256).
| Input | Type | Description |
|---|---|---|
_user | address | Address to query the veCRV balance for |
_timestamp | uint256 | Timestamp |
<>Source code▼
@view
@external
def ve_for_at(_user: address, _timestamp: uint256) -> uint256:
"""
@notice Get the veCRV balance for `_user` at `_timestamp`
@param _user Address to query balance for
@param _timestamp Epoch time
@return uint256 veCRV balance
"""
ve: address = self.voting_escrow
max_user_epoch: uint256 = VotingEscrow(ve).user_point_epoch(_user)
epoch: uint256 = self._find_timestamp_user_epoch(ve, _user, _timestamp, max_user_epoch)
pt: Point = VotingEscrow(ve).user_point_history(_user, epoch)
return convert(max(pt.bias - pt.slope * convert(_timestamp - pt.ts, int128), 0), uint256)
▶Example▼
>>> FeeDistributor.ve_for_at("0x989AEb4d175e16225E39E87d0D97A3360524AD80", 1685972555)
290896146145001156884162140
ve_supply
FeeDistributor.ve_supply(arg0: uint256) -> uint256: viewGetter for the total supply of veCRV at the beginning of an epoch.
Returns: veCRV supply (uint256).
| Input | Type | Description |
|---|---|---|
arg0 | uint256 | Timestamp of the epoch start |
<>Source code▼
ve_supply: public(uint256[1000000000000000]) # VE total supply at week bounds
▶Example▼
>>> FeeDistributor.ve_supply(1718841600)
667140493408797243694521600
Killing The FeeDistributor
The FeeDistributor can be killed by the admin of the contract, which is the Curve DAO. Doing so, transfers the entire token balance to the emergency_return address and blocks the ability to claim or burn. The contract can not be unkilled.
A Google Colab notebook that simulates killing the FeeDistributor and its respective consequences can be found here: Google Colab Notebook.
is_killed
FeeDistributor.is_killed() -> bool: viewGetter method to check if the FeeDistributor contract is killed. When killed, the contract blocks claim and burn and the entire token balance is transferred to the emergency_return address.
Returns: true or false (bool).
<>Source code▼
is_killed: public(bool)
▶Example▼
kill_me
FeeDistributor.kill_me()By killing the FeeDistributor, the entire token balance is transferred to the emergency_return address, and the ability to further call the claim, claim_many, or burn functions is blocked.
This function is only callable by the admin of the contract.
Function to kill the FeeDistributor contract.
<>Source code▼
is_killed: public(bool)
@external
def kill_me():
"""
@notice Kill the contract
@dev Killing transfers the entire 3CRV balance to the emergency return address
and blocks the ability to claim or burn. The contract cannot be unkilled.
"""
assert msg.sender == self.admin
self.is_killed = True
token: address = self.token
assert ERC20(token).transfer(self.emergency_return, ERC20(token).balanceOf(self))
▶Example▼
>>> FeeDistributor.kill_me()
emergency_return
FeeDistributor.emergency_return() -> address: viewGetter for the emergency return address. This address can not be changed.
Returns: emergency return (address).
<>Source code▼
emergency_return: public(address)
▶Example▼
Due to the fact that the emergency return address can not be changed and Curve used a ownership agent back then when the distributor contract for 3CRV was deployed, this one was set as the emergency return address.
The second fee distributor contract (crvUSD) uses a 5 of 9 multisig, which replaced the ownership agent.
>>> FeeDistributor.emergency_return() # 3CRV distributor
'0x00669DF67E4827FCc0E48A1838a8d5AB79281909'
>>> FeeDistributor.emergency_return() # crvUSD distributor
'0x467947EE34aF926cF1DCac093870f613C96B1E0c'
recover_balance
FeeDistributor.recover_balance(_coin: address) -> boolFunction to recover ERC20 tokens from the contract. Tokens are sent to the emergency return address. This function only works for tokens other than the address set for token. E.g. this function on the 3CRV distributor contract can not be called to transfer 3CRV. The same applied to crvUSD distributor.
Returns: true (bool).
| Input | Type | Description |
|---|---|---|
_coin | address | Tokens to recover |
<>Source code▼
@external
def recover_balance(_coin: address) -> bool:
"""
@notice Recover ERC20 tokens from this contract
@dev Tokens are sent to the emergency return address.
@param _coin Token address
@return bool success
"""
assert msg.sender == self.admin
assert _coin != self.token
amount: uint256 = ERC20(_coin).balanceOf(self)
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("transfer(address,uint256)"),
convert(self.emergency_return, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
return True
▶Example▼
This example recovers the balance of a given token.
>>> FeeDistributor.recover_balance("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
true
Admin Ownership
admin
FeeDistributor.admin() -> address: viewGetter for the admin of the contract.
Returns: admin (address).
<>Source code▼
admin: public(address)
▶Example▼
future_admin
FeeDistributor.future_admin() -> address: viewGetter for the future admin of the contract.
Returns: future admin (address).
<>Source code▼
future_admin: public(address)
▶Example▼
commit_admin
FeeDistributor.commit_admin(_addr: address)This function is only callable by the admin of the contract.
Function to commit transfer of the ownership.
Emits: CommitAdmin
| Input | Type | Description |
|---|---|---|
_addr | address | Address to commit the ownership transfer to. |
<>Source code▼
event CommitAdmin:
admin: address
admin: public(address)
future_admin: public(address)
@external
def commit_admin(_addr: address):
"""
@notice Commit transfer of ownership
@param _addr New admin address
"""
assert msg.sender == self.admin # dev: access denied
self.future_admin = _addr
log CommitAdmin(_addr)
▶Example▼
This example commits the transfer of the ownership.
>>> FeeDistributor.commit_admin("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
apply_admin
FeeDistributor.apply_admin()This function is only callable by the admin of the contract.
Function to apply the transfer of the ownership.
Emits: ApplyAdmin
<>Source code▼
event ApplyAdmin:
admin: address
admin: public(address)
future_admin: public(address)
@external
def apply_admin():
"""
@notice Apply transfer of ownership
"""
assert msg.sender == self.admin
assert self.future_admin != ZERO_ADDRESS
future_admin: address = self.future_admin
self.admin = future_admin
log ApplyAdmin(future_admin)
▶Example▼
This example applies the transfer of the ownership.
>>> FeeDistributor.apply_admin()
Other Methods
start_time
FeeDistributor.start_time() -> uint256: viewGetter for the epoch time for fee distribution to start.
Returns: epoch time (uint256).
<>Source code▼
start_time: public(uint256)
@external
def __init__(
_voting_escrow: address,
_start_time: uint256,
_token: address,
_admin: address,
_emergency_return: address
):
"""
@notice Contract constructor
@param _voting_escrow VotingEscrow contract address
@param _start_time Epoch time for fee distribution to start
@param _token Fee token address (3CRV)
@param _admin Admin address
@param _emergency_return Address to transfer `_token` balance to
if this contract is killed
"""
t: uint256 = _start_time / WEEK * WEEK
self.start_time = t
self.last_token_time = t
self.time_cursor = t
self.token = _token
self.voting_escrow = _voting_escrow
self.admin = _admin
self.emergency_return = _emergency_return
▶Example▼
This example returns the start_time of the first distribution of rewards.
>>> FeeDistributor.start_time() # 3CRV Distributor
1600300800 # Thu Sep 17 2020 00:00:00 GMT+0000
>>> FeeDistributor.start_time() # crvUSD Distributor
1718841600 # Thu Jun 20 2024 00:00:00 GMT+0000
voting_escrow
FeeDistributor.voting_escrow() -> address: viewGetter for the voting escrow contract.
Returns: voting escrow (address).
<>Source code▼
voting_escrow: public(address)
@external
def __init__(
_voting_escrow: address,
_start_time: uint256,
_token: address,
_admin: address,
_emergency_return: address
):
"""
@notice Contract constructor
@param _voting_escrow VotingEscrow contract address
@param _start_time Epoch time for fee distribution to start
@param _token Fee token address (3CRV)
@param _admin Admin address
@param _emergency_return Address to transfer `_token` balance to
if this contract is killed
"""
t: uint256 = _start_time / WEEK * WEEK
self.start_time = t
self.last_token_time = t
self.time_cursor = t
self.token = _token
self.voting_escrow = _voting_escrow
self.admin = _admin
self.emergency_return = _emergency_return
▶Example▼
token
FeeDistributor.token() -> address: viewGetter for the token address in which the fees are distributed.
Returns: reward token (address).
<>Source code▼
token: public(address)
@external
def __init__(
_voting_escrow: address,
_start_time: uint256,
_token: address,
_admin: address,
_emergency_return: address
):
"""
@notice Contract constructor
@param _voting_escrow VotingEscrow contract address
@param _start_time Epoch time for fee distribution to start
@param _token Fee token address (crvUSD)
@param _admin Admin address
@param _emergency_return Address to transfer `_token` balance to
if this contract is killed
"""
t: uint256 = _start_time / WEEK * WEEK
self.start_time = t
self.last_token_time = t
self.time_cursor = t
self.token = _token
self.voting_escrow = _voting_escrow
self.admin = _admin
self.emergency_return = _emergency_return
▶Example▼
user_epoch_of
FeeDistributor.user_epoch_of(arg0: address) -> uint256: viewGetter for the user epoch of an address. This value increments by one each time rewards are claimed.
Returns: user epoch (uint256).
| Input | Type | Description |
|---|---|---|
arg0 | address | Address to get the user epoch for |
<>Source code▼
user_epoch_of: public(HashMap[address, uint256])
▶Example▼
This example returns the user epoch of a given address.
>>> FeeDistributor.user_epoch_of("0x989AEb4d175e16225E39E87d0D97A3360524AD80")
7739