Skip to main content

FeeAllocator

The FeeAllocator is a contract that allocates protocol fees between different receivers based on configurable weights. It sits between the Hooker and the FeeDistributor, splitting the accumulated crvUSD fees among a set of receivers before forwarding the remainder to the FeeDistributor for distribution to veCRV holders.

Receivers can be assigned weights in basis points (bps), with a maximum total weight of 5,000 bps (50%). The remaining portion (at least 50%) always flows to the FeeDistributor.

FeeAllocator.vy

The source code for the FeeAllocator.vy contract can be found on GitHub. The contract is written using Vyper version 0.4.1 and utilizes a Snekmate module to handle contract ownership.

The contract is deployed on Ethereum at 0x22530d384cd9915e096ead2db7f82ee81f8eb468.

{ }Contract ABI
[{"anonymous":false,"inputs":[{"indexed":true,"name":"receiver","type":"address"},{"indexed":false,"name":"old_weight","type":"uint256"},{"indexed":false,"name":"new_weight","type":"uint256"}],"name":"ReceiverSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"receiver","type":"address"}],"name":"ReceiverRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"total_amount","type":"uint256"},{"indexed":false,"name":"distributor_share","type":"uint256"}],"name":"FeesDistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previous_owner","type":"address"},{"indexed":true,"name":"new_owner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[{"name":"new_owner","type":"address"}],"name":"transfer_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_receiver","type":"address"},{"name":"_weight","type":"uint256"}],"name":"set_receiver","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"receiver","type":"address"},{"name":"weight","type":"uint256"}],"name":"_configs","type":"tuple[]"}],"name":"set_multiple_receivers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_receiver","type":"address"}],"name":"remove_receiver","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"distribute_fees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"n_receivers","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"distributor_weight","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_RECEIVERS","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_TOTAL_WEIGHT","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee_distributor","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee_collector","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee_token","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"receiver_weights","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"receivers","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"total_weight","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VERSION","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_fee_distributor","type":"address"},{"name":"_fee_collector","type":"address"},{"name":"_owner","type":"address"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]

Distributing Fees

distribute_fees

FeeAllocator.distribute_fees()
Guarded Method

This function can only be called by the hooker address of the FeeCollector contract. The hooker is the contract responsible for orchestrating the fee forwarding process.

Function to distribute accumulated crvUSD fees to receivers based on their weights. The function first transfers the fee token balance from the caller (the hooker) to this contract, then distributes proportional amounts to each receiver based on their weight. The remaining balance is forwarded to the FeeDistributor for distribution to veCRV holders.

Emits: FeesDistributed event.

<>Source code
@external
@nonreentrant
def distribute_fees():
"""
@notice Distribute accumulated crvUSD fees to receivers based on their weights
"""
assert (msg.sender == staticcall fee_collector.hooker()), "distribute: hooker only"

amount_receivable: uint256 = staticcall fee_token.balanceOf(msg.sender)
extcall fee_token.transferFrom(msg.sender, self, amount_receivable)
balance: uint256 = staticcall fee_token.balanceOf(self)
assert balance > 0, "receivers: no fees to distribute"

remaining_balance: uint256 = balance

for receiver: address in self.receivers:
weight: uint256 = self.receiver_weights[receiver]
amount: uint256 = balance * weight // MAX_BPS
if amount > 0:
extcall fee_token.transfer(receiver, amount, default_return_value=True)
remaining_balance -= amount
extcall fee_distributor.burn(fee_token.address)
log FeesDistributed(total_amount=balance, distributor_share=remaining_balance)
Example
>>> FeeAllocator.distribute_fees()

Managing Receivers

set_receiver

FeeAllocator.set_receiver(_receiver: address, _weight: uint256)
Guarded Method by Snekmate 🐍

This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current owner of the contract.

Function to add a new receiver or update the weight of an existing receiver. Weights are specified in basis points (bps) where 10,000 = 100%. The total weight of all receivers cannot exceed MAX_TOTAL_WEIGHT (5,000 bps = 50%). To remove a receiver, use remove_receiver instead.

InputTypeDescription
_receiveraddressAddress of the receiver
_weightuint256Weight assigned to the receiver in basis points

Emits: ReceiverSet event.

<>Source code
@internal
def _set_receiver(_receiver: address, _weight: uint256):
"""
@notice Add or update a receiver with a specified weight
@param _receiver The address of the receiver
@param _weight The weight assigned to the receiver
"""
ownable._check_owner()
assert _receiver != empty(address), "zeroaddr: receiver"
assert _weight > 0, "receivers: invalid weight, use remove_receiver"

old_weight: uint256 = self.receiver_weights[_receiver]
new_total_weight: uint256 = self.total_weight

if old_weight > 0:
new_total_weight = new_total_weight - old_weight + _weight
else:
assert (len(self.receivers) < MAX_RECEIVERS), "receivers: max limit reached"
new_total_weight += _weight

assert (new_total_weight <= MAX_TOTAL_WEIGHT), "receivers: exceeds max total weight"

if old_weight == 0:
self.receiver_indices[_receiver] = (
len(self.receivers) + 1
) # offset by 1, 0 is for deleted receivers
self.receivers.append(_receiver)

self.receiver_weights[_receiver] = _weight
self.total_weight = new_total_weight

log ReceiverSet(receiver=_receiver, old_weight=old_weight, new_weight=_weight)


@external
def set_receiver(_receiver: address, _weight: uint256):
"""
@notice Add or update a receiver with a specified weight
@param _receiver The address of the receiver
@param _weight The weight assigned to the receiver
"""
self._set_receiver(_receiver, _weight)
Example
>>> FeeAllocator.set_receiver("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 1000)

set_multiple_receivers

FeeAllocator.set_multiple_receivers(_configs: DynArray[ReceiverConfig, MAX_RECEIVERS])
Guarded Method by Snekmate 🐍

This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current owner of the contract.

Function to add or update multiple receivers at once. Each configuration in the array specifies a receiver address and its weight. When adding new receivers whose combined weight might temporarily exceed MAX_TOTAL_WEIGHT, place receivers being updated with lower weights first in the array.

InputTypeDescription
_configsDynArray[ReceiverConfig, MAX_RECEIVERS]Array of receiver configurations (address, weight)

Emits: ReceiverSet event for each receiver.

<>Source code
@external
def set_multiple_receivers(_configs: DynArray[ReceiverConfig, MAX_RECEIVERS]):
"""
@notice Add or update multiple receivers with specified weights
@param _configs Array of receiver configurations (address, weight)
@dev When adding new receivers, if total weight might exceed MAX_TOTAL_WEIGHT,
place receivers being updated with lower weights first in the array
"""
assert len(_configs) > 0, "receivers: empty array"

for i: uint256 in range(MAX_RECEIVERS):
if i >= len(_configs):
break

config: ReceiverConfig = _configs[i]
self._set_receiver(config.receiver, config.weight)


@internal
def _set_receiver(_receiver: address, _weight: uint256):
"""
@notice Add or update a receiver with a specified weight
@param _receiver The address of the receiver
@param _weight The weight assigned to the receiver
"""
ownable._check_owner()
assert _receiver != empty(address), "zeroaddr: receiver"
assert _weight > 0, "receivers: invalid weight, use remove_receiver"

old_weight: uint256 = self.receiver_weights[_receiver]
new_total_weight: uint256 = self.total_weight

if old_weight > 0:
new_total_weight = new_total_weight - old_weight + _weight
else:
assert (len(self.receivers) < MAX_RECEIVERS), "receivers: max limit reached"
new_total_weight += _weight

assert (new_total_weight <= MAX_TOTAL_WEIGHT), "receivers: exceeds max total weight"

if old_weight == 0:
self.receiver_indices[_receiver] = (
len(self.receivers) + 1
) # offset by 1, 0 is for deleted receivers
self.receivers.append(_receiver)

self.receiver_weights[_receiver] = _weight
self.total_weight = new_total_weight

log ReceiverSet(receiver=_receiver, old_weight=old_weight, new_weight=_weight)
Example
>>> FeeAllocator.set_multiple_receivers([("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 1000), ("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", 2000)])

remove_receiver

FeeAllocator.remove_receiver(_receiver: address)
Guarded Method by Snekmate 🐍

This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current owner of the contract.

Function to remove a receiver from the list. The receiver's weight is subtracted from the total weight, effectively increasing the share that flows to the FeeDistributor.

InputTypeDescription
_receiveraddressAddress of the receiver to remove

Emits: ReceiverRemoved event.

<>Source code
@external
def remove_receiver(_receiver: address):
"""
@notice Remove a receiver from the list
@param _receiver The address of the receiver to remove
"""
ownable._check_owner()
weight: uint256 = self.receiver_weights[_receiver]
assert weight > 0, "receivers: does not exist"

index_to_remove: uint256 = self.receiver_indices[_receiver] - 1
last_index: uint256 = len(self.receivers) - 1
assert self.receivers[index_to_remove] == _receiver
if index_to_remove < last_index:
last_receiver: address = self.receivers[last_index]
self.receivers[index_to_remove] = last_receiver
self.receiver_indices[last_receiver] = index_to_remove + 1

self.receivers.pop()

self.receiver_weights[_receiver] = 0
self.receiver_indices[_receiver] = 0

self.total_weight -= weight

log ReceiverRemoved(receiver=_receiver)
Example
>>> FeeAllocator.remove_receiver("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")

receivers

FeeAllocator.receivers(arg0: uint256) -> address: view

Getter for the receiver address at a given index.

InputTypeDescription
arg0uint256Index of the receiver

Returns: receiver address (address).

<>Source code
receivers: public(DynArray[address, MAX_RECEIVERS])
Example

receiver_weights

FeeAllocator.receiver_weights(arg0: address) -> uint256: view

Getter for the weight of a specific receiver in basis points.

InputTypeDescription
arg0addressAddress of the receiver

Returns: receiver weight in bps (uint256).

<>Source code
receiver_weights: public(HashMap[address, uint256])
Example

n_receivers

FeeAllocator.n_receivers() -> uint256: view

Getter for the number of receivers currently registered.

Returns: number of receivers (uint256).

<>Source code
@external
@view
def n_receivers() -> uint256:
"""
@notice Get the number of receivers
@return The number of receivers
"""
return len(self.receivers)
Example

total_weight

FeeAllocator.total_weight() -> uint256: view

Getter for the sum of all receiver weights in basis points.

Returns: total weight in bps (uint256).

<>Source code
total_weight: public(uint256)
Example

distributor_weight

FeeAllocator.distributor_weight() -> uint256: view

Getter for the portion of fees that flows to the FeeDistributor for veCRV holders. This is calculated as MAX_BPS - total_weight, meaning it is the complement of all receiver weights combined.

Returns: distributor weight in bps (uint256).

<>Source code
@external
@view
def distributor_weight() -> uint256:
"""
@notice Get the portion of fees going to the fee distributor for veCRV
@return The distributors' weight
"""
return MAX_BPS - self.total_weight
Example

Contract Ownership

Ownership of the contract is managed using the ownable.vy module from Snekmate which implements a basic access control mechanism, where there is an owner that can be granted exclusive access to specific functions.

owner

FeeAllocator.owner() -> address: view

Getter for the owner of the contract. The owner can manage receivers and their weights via set_receiver, set_multiple_receivers, and remove_receiver.

Returns: contract owner (address).

<>Source code
from snekmate.auth import ownable
initializes: ownable
exports: (ownable.transfer_ownership, ownable.owner)

@deploy
def __init__(
_fee_distributor: FeeDistributor,
_fee_collector: FeeCollector,
_owner: address,
):
assert _owner != empty(address), "zeroaddr: owner"
assert _fee_distributor.address != empty(address), "zeroaddr: fee_distributor"
assert _fee_collector.address != empty(address), "zeroaddr: fee_collector"

ownable.__init__()
ownable._transfer_ownership(_owner)

fee_distributor = _fee_distributor
fee_collector = _fee_collector
fee_token = IERC20(staticcall fee_collector.target())
extcall fee_token.approve(
fee_distributor.address, max_value(uint256), default_return_value=True
)
Example

transfer_ownership

FeeAllocator.transfer_ownership(new_owner: address)
Guarded Method by Snekmate 🐍

This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current owner of the contract.

Function to transfer the ownership of the contract to a new address.

InputTypeDescription
new_owneraddressNew owner of the contract

Emits: OwnershipTransferred event.

<>Source code
from snekmate.auth import ownable
initializes: ownable
exports: (ownable.transfer_ownership, ownable.owner)
Example
>>> FeeAllocator.transfer_ownership("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")

Other Methods

fee_distributor

FeeAllocator.fee_distributor() -> address: view

Getter for the immutable address of the FeeDistributor contract that receives the remaining fees after receiver allocations.

Returns: fee distributor address (address).

<>Source code
fee_distributor: public(immutable(FeeDistributor))
Example

fee_collector

FeeAllocator.fee_collector() -> address: view

Getter for the immutable address of the FeeCollector contract. The hooker of this contract is authorized to call distribute_fees.

Returns: fee collector address (address).

<>Source code
fee_collector: public(immutable(FeeCollector))
Example

fee_token

FeeAllocator.fee_token() -> address: view

Getter for the immutable address of the fee token (crvUSD). This is set at deployment by reading the target from the FeeCollector.

Returns: fee token address (address).

<>Source code
fee_token: public(immutable(IERC20))
Example

MAX_RECEIVERS

FeeAllocator.MAX_RECEIVERS() -> uint256: view

Getter for the maximum number of receivers that can be registered. This is a constant set to 10.

Returns: maximum number of receivers (uint256).

<>Source code
MAX_RECEIVERS: public(constant(uint256)) = 10
Example

MAX_TOTAL_WEIGHT

FeeAllocator.MAX_TOTAL_WEIGHT() -> uint256: view

Getter for the maximum total weight that can be assigned to all receivers combined. This is a constant set to 5,000 bps (50%), ensuring at least half of all fees always go to the FeeDistributor.

Returns: maximum total weight in bps (uint256).

<>Source code
MAX_TOTAL_WEIGHT: public(constant(uint256)) = 5_000  # in bps
Example

VERSION

FeeAllocator.VERSION() -> String[8]: view

Getter for the version of the contract.

Returns: contract version (String[8]).

<>Source code
VERSION: public(constant(String[8])) = "0.1.0"
Example