Skip to main content

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.vy

The source code for the FeeDistributor.vy contract can be found on GitHub. The contract is written using Vyper version 0.2.7 and 0.3.7.

There are two different FeeDistributor contracts deployed on Ethereum, depending on the reward token:


last_token_time

FeeDistributor.last_token_time() -> uint256: view

Getter 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: view

Function 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()
Guarded Method

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: view

Getter 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: view

Getter for the timestamp of the last checkpoint_total_supply of veCRV.

Returns: timestamp (uint256).

InputTypeDescription
arg0addressAddress 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: view

Getter for the veCRV balance of a user at a certain timestamp.

Returns: veCRV balance (uint256).

InputTypeDescription
_useraddressAddress to query the veCRV balance for
_timestampuint256Timestamp
<>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: view

Getter for the total supply of veCRV at the beginning of an epoch.

Returns: veCRV supply (uint256).

InputTypeDescription
arg0uint256Timestamp 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.

Google Colab Notebook

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: view

Getter 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.

Guarded Method

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: view

Getter 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) -> bool

Function 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).

InputTypeDescription
_coinaddressTokens 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: view

Getter for the admin of the contract.

Returns: admin (address).

<>Source code
admin: public(address)
Example

future_admin

FeeDistributor.future_admin() -> address: view

Getter 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)
Guarded Method

This function is only callable by the admin of the contract.

Function to commit transfer of the ownership.

Emits: CommitAdmin

InputTypeDescription
_addraddressAddress 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()
Guarded Method

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: view

Getter 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: view

Getter 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: view

Getter 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: view

Getter for the user epoch of an address. This value increments by one each time rewards are claimed.

Returns: user epoch (uint256).

InputTypeDescription
arg0addressAddress 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