The RewardsHandler contract manages the distribution of crvUSD rewards to Savings crvUSD (scrvUSD). The contract takes snapshots of the ratio of crvUSD deposited into the Vault relative to the total circulating supply of crvUSD to calculate a time-weighted average of this ratio to determine the amount of rewards to request from the FeeSplitter.
RewardsHandler.vy
The source code for the RewardsHandler.vy contract is available on GitHub. The contract is written in Vyper version ~=0.4.
The weight allocated to the RewardsHandler in the FeeSplitter is determined by the time-weighted average of the ratio of crvUSD deposited into the Vault compared to the total circulating supply of crvUSD. The weight allocated to the RewardsHandler can be permissionlessly distributed as rewards to the Savings Vault (scrvUSD) by anyone calling the process_rewards function.
To calculate this time-weighted average, the RewardsHandler uses a TWA module that takes snapshots of the deposited supply ratio and stores them in a Snapshot struct. All structs are stored in a dynamic array called snapshots. Each snapshot includes a ratio value and the timestamp at which it was taken.
Source code for snapshot calculation and storage
fromcontracts.interfacesimportIStablecoinLens@externaldeftake_snapshot():""" @notice Function that anyone can call to take a snapshot of the current deposited supply ratio in the vault. This is used to compute the time-weighted average of the TVL to decide on the amount of rewards to ask for (weight). @dev There's no point in MEVing this snapshot as the rewards distribution rate can always be reduced (if a malicious actor inflates the value of the snapshot) or the minimum amount of rewards can always be increased (if a malicious actor deflates the value of the snapshot). """self._take_snapshot()@internaldef_take_snapshot():""" @notice Internal function to take a snapshot of the current deposited supply ratio in the vault. """# get the circulating supply from a helper contract.# supply in circulation = controllers' debt + peg keppers' debtcirculating_supply:uint256=staticcallself.stablecoin_lens.circulating_supply()# obtain the supply of crvUSD contained in the vault by checking its totalAssets.# This will not take into account rewards that are not yet distributed.supply_in_vault:uint256=staticcallvault.totalAssets()# here we intentionally reduce the precision of the ratio because the# dynamic weight interface expects a percentage in BPS.supply_ratio:uint256=supply_in_vault*MAX_BPS//circulating_supplytwa._take_snapshot(supply_ratio)
# bound from factoryMAX_CONTROLLERS:constant(uint256)=50000# bound from monetary policyMAX_PEG_KEEPERS:constant(uint256)=1001# could have been any other controllerWETH_CONTROLLER_IDX:constant(uint256)=3# the crvusd controller factoryfactory:immutable(IControllerFactory)@view@internaldef_circulating_supply()->uint256:""" @notice Compute the circulating supply for crvUSD, `totalSupply` is incorrect since it takes into account all minted crvUSD (i.e. flashloans) @dev This function sacrifices some gas to fetch peg keepers from a unique source of truth to avoid having to manually maintain multiple lists across several contracts. For this reason we read the list of peg keepers contained in the monetary policy returned by a controller in the factory. factory -> weth controller -> monetary policy -> peg keepers This function is not exposed as external as it can be easily manipulated and should not be used by third party contracts. """circulating_supply:uint256=0# Fetch the weth controller (index 3) under the assumption that# weth will always be a valid collateral for crvUSD, therefore its# monetary policy should always be up to date.controller:IController=staticcallfactory.controllers(WETH_CONTROLLER_IDX)# We obtain the address of the current monetary policy used by the# weth controller because it contains a list of all the peg keepers.monetary_policy:IMonetaryPolicy=staticcallcontroller.monetary_policy()# Iterate over the peg keepers (since it's a fixed size array we# wait for a zero address to stop iterating).fori:uint256inrange(MAX_PEG_KEEPERS):pk:IPegKeeper=staticcallmonetary_policy.peg_keepers(i)ifpk.address==empty(address):# end of arraybreakcirculating_supply+=staticcallpk.debt()n_controllers:uint256=staticcallfactory.n_collaterals()fori:uint256inrange(n_controllers,bound=MAX_CONTROLLERS):controller=staticcallfactory.controllers(i)# add crvUSD minted by controllercirculating_supply+=staticcallcontroller.total_debt()returncirculating_supply
eventSnapshotTaken:value:uint256timestamp:uint256snapshots:public(DynArray[Snapshot,MAX_SNAPSHOTS])min_snapshot_dt_seconds:public(uint256)# Minimum time between snapshots in secondstwa_window:public(uint256)# Time window in seconds for TWA calculationlast_snapshot_timestamp:public(uint256)# Timestamp of the last snapshot@internaldef_take_snapshot(_value:uint256):""" @notice Stores a snapshot of the tracked value. @param _value The value to store. """if(len(self.snapshots)==0)or(# First snapshotself.last_snapshot_timestamp+self.min_snapshot_dt_seconds<=block.timestamp# after dt):self.last_snapshot_timestamp=block.timestampself.snapshots.append(Snapshot(tracked_value=_value,timestamp=block.timestamp))# store the snapshot into the DynArraylogSnapshotTaken(_value,block.timestamp)
Snapshots are used to calculate the time-weighted average (TWA) of the ratio between crvUSD deposited into the Vault and the total circulating supply of crvUSD. Each snapshot stores the ratio of crvUSD deposited in the Vault to the circulating supply of crvUSD, along with the timestamp when the snapshot was taken. Taking a snapshot is fully permissionless—anyone can take one by calling the take_snapshot function. The snapshot values are stored in a Snapshot struct, and each struct is saved in a dynamic array called snapshots.
MAX_SNAPSHOTS:constant(uint256)=10**18# 31.7 billion years if snapshot every secondsnapshots:public(DynArray[Snapshot,MAX_SNAPSHOTS])structSnapshot:tracked_value:uint256timestamp:uint256
Snapshots can only be taken once a minimum time interval (min_snapshot_dt_seconds) has passed since the last one. The TWA is then computed using the trapezoidal rule, iterating over the stored snapshots in reverse chronological order to calculate the weighted average of the tracked value over the specified time window (twa_window).
Snapshots are taken by calling the take_snapshot function. When this function is called, the snapshot value is computed and stored as follows:
Determine the circulating supply of crvUSD. Directly calling crvUSD.totalSupply() is not feasible because some crvUSD is minted to specific contracts and is not part of the circulating supply (e.g., unborrowed crvUSD held by Controllers, crvUSD allocated to PegKeepers, or crvUSD assigned to the FlashLender). Therefore, the StablecoinLens contract is used to obtain the actual circulating supply of crvUSD.
Obtain the amount of crvUSD held in the Vault by calling Vault.totalAssets(), which excludes rewards that have not yet been distributed.
Calculate the supply ratio as follows:
Store the calculated supply ratio and the timestamp at which the snapshot was taken in the dynamic array.
There's no point in MEVing this snapshot as the rewards distribution rate can always be reduced (if a malicious actor inflates the value of the snapshot) or the minimum amount of rewards can always be increased (if a malicious actor deflates the value of the snapshot).
Function to take a snapshot of the current deposited supply ratio in the Vault. This function is fully permissionless and can be called by anyone. Snapshots are used to compute the time-weighted average of the TVL to decide on the amount of rewards to ask for (weight).
Minimum time inbetween snapshots is defined by min_snapshot_dt_seconds. The maximum number of snapshots is set to 10^18, which is equivalent to 31.7 billion years if a snapshot were to be taken every second.
Emits: SnapshotTaken event.
Source code
@externaldeftake_snapshot():""" @notice Function that anyone can call to take a snapshot of the current deposited supply ratio in the vault. This is used to compute the time-weighted average of the TVL to decide on the amount of rewards to ask for (weight). @dev There's no point in MEVing this snapshot as the rewards distribution rate can always be reduced (if a malicious actor inflates the value of the snapshot) or the minimum amount of rewards can always be increased (if a malicious actor deflates the value of the snapshot). """self._take_snapshot()@internaldef_take_snapshot():""" @notice Internal function to take a snapshot of the current deposited supply ratio in the vault. """# get the circulating supply from a helper contract.# supply in circulation = controllers' debt + peg keppers' debtcirculating_supply:uint256=staticcallself.stablecoin_lens.circulating_supply()# obtain the supply of crvUSD contained in the vault by checking its totalAssets.# This will not take into account rewards that are not yet distributed.supply_in_vault:uint256=staticcallvault.totalAssets()# here we intentionally reduce the precision of the ratio because the# dynamic weight interface expects a percentage in BPS.supply_ratio:uint256=supply_in_vault*MAX_BPS//circulating_supplytwa._take_snapshot(supply_ratio)
# bound from factoryMAX_CONTROLLERS:constant(uint256)=50000# bound from monetary policyMAX_PEG_KEEPERS:constant(uint256)=1001# could have been any other controllerWETH_CONTROLLER_IDX:constant(uint256)=3# the crvusd controller factoryfactory:immutable(IControllerFactory)@view@internaldef_circulating_supply()->uint256:""" @notice Compute the circulating supply for crvUSD, `totalSupply` is incorrect since it takes into account all minted crvUSD (i.e. flashloans) @dev This function sacrifices some gas to fetch peg keepers from a unique source of truth to avoid having to manually maintain multiple lists across several contracts. For this reason we read the list of peg keepers contained in the monetary policy returned by a controller in the factory. factory -> weth controller -> monetary policy -> peg keepers This function is not exposed as external as it can be easily manipulated and should not be used by third party contracts. """circulating_supply:uint256=0# Fetch the weth controller (index 3) under the assumption that# weth will always be a valid collateral for crvUSD, therefore its# monetary policy should always be up to date.controller:IController=staticcallfactory.controllers(WETH_CONTROLLER_IDX)# We obtain the address of the current monetary policy used by the# weth controller because it contains a list of all the peg keepers.monetary_policy:IMonetaryPolicy=staticcallcontroller.monetary_policy()# Iterate over the peg keepers (since it's a fixed size array we# wait for a zero address to stop iterating).fori:uint256inrange(MAX_PEG_KEEPERS):pk:IPegKeeper=staticcallmonetary_policy.peg_keepers(i)ifpk.address==empty(address):# end of arraybreakcirculating_supply+=staticcallpk.debt()n_controllers:uint256=staticcallfactory.n_collaterals()fori:uint256inrange(n_controllers,bound=MAX_CONTROLLERS):controller=staticcallfactory.controllers(i)# add crvUSD minted by controllercirculating_supply+=staticcallcontroller.total_debt()returncirculating_supply
eventSnapshotTaken:value:uint256timestamp:uint256MAX_SNAPSHOTS:constant(uint256)=10**18# 31.7 billion years if snapshot every secondsnapshots:public(DynArray[Snapshot,MAX_SNAPSHOTS])min_snapshot_dt_seconds:public(uint256)# Minimum time between snapshots in secondstwa_window:public(uint256)# Time window in seconds for TWA calculationlast_snapshot_timestamp:public(uint256)# Timestamp of the last snapshotstructSnapshot:tracked_value:uint256timestamp:uint256@internaldef_take_snapshot(_value:uint256):""" @notice Stores a snapshot of the tracked value. @param _value The value to store. """if(len(self.snapshots)==0)or(# First snapshotself.last_snapshot_timestamp+self.min_snapshot_dt_seconds<=block.timestamp# after dt):self.last_snapshot_timestamp=block.timestampself.snapshots.append(Snapshot(tracked_value=_value,timestamp=block.timestamp))# store the snapshot into the DynArraylogSnapshotTaken(_value,block.timestamp)
Getter for a Snapshot struct at a specific index. First snapshot is at index 0, second at index 1, etc.
Returns: Snapshot struct containing the ratio of deposited crvUSD into the Vault to the total circulating supply of crvUSD (uint256) and the timestamp (uint256).
Input
Type
Description
arg
uint256
Index of the snapshot to return
Source code
eventSnapshotTaken:value:uint256timestamp:uint256MAX_SNAPSHOTS:constant(uint256)=10**18# 31.7 billion years if snapshot every secondsnapshots:public(DynArray[Snapshot,MAX_SNAPSHOTS])structSnapshot:tracked_value:uint256timestamp:uint256@internaldef_take_snapshot(_value:uint256):""" @notice Stores a snapshot of the tracked value. @param _value The value to store. """ifself.last_snapshot_timestamp+self.min_snapshot_dt_seconds<=block.timestamp:self.last_snapshot_timestamp=block.timestampself.snapshots.append(Snapshot(tracked_value=_value,timestamp=block.timestamp))# store the snapshot into the DynArraylogSnapshotTaken(_value,block.timestamp)
In this example, the address and weight of a receiver at a specific index is returned.
Getter for the minimum time between snapshots in seconds. This value can be changed using the set_twa_snapshot_dt function.
Returns: minimum time between snapshots in seconds (uint256).
Source code
min_snapshot_dt_seconds:public(uint256)# Minimum time between snapshots in seconds@deploydef__init__(_twa_window:uint256,_min_snapshot_dt_seconds:uint256):self._set_twa_window(_twa_window)self._set_snapshot_dt(max(1,_min_snapshot_dt_seconds))
This example returns the minimum time between snapshots in seconds.
Getter for the timestamp of the last snapshot taken. This variable is adjusted each time a snapshot is taken.
Returns: timestamp of the last snapshot taken (uint256).
Source code
last_snapshot_timestamp:public(uint256)# Timestamp of the last snapshot@internaldef_take_snapshot(_value:uint256):""" @notice Stores a snapshot of the tracked value. @param _value The value to store. """ifself.last_snapshot_timestamp+self.min_snapshot_dt_seconds<=block.timestamp:self.last_snapshot_timestamp=block.timestampself.snapshots.append(Snapshot(tracked_value=_value,timestamp=block.timestamp))# store the snapshot into the DynArraylogSnapshotTaken(_value,block.timestamp)
This example returns the timestamp of the last snapshot taken.
Getter for the total number of snapshots taken and stored. Increments by one each time a snapshot is taken.
Returns: total number of snapshots stored (uint256).
Source code
snapshots:public(DynArray[Snapshot,MAX_SNAPSHOTS])@external@viewdefget_len_snapshots()->uint256:""" @notice Returns the number of snapshots stored. """returnlen(self.snapshots)
This example returns the total number of snapshots stored.
The weight represents the percentage of the total rewards requested from the FeeSplitter. This value is denominated in 10000 BPS (100%). E.g. if the weight is 500, then RewardsHandler will request 5% of the total rewards from the FeeSplitter.
The weight is computed as a time-weighted average (TWA) of the ratio between deposited crvUSD in the Vault and total circulating supply of crvUSD.
Weight calculation is handled using a time-weighted average (TWA) module. While this module can be used to calculate any kind of time-weighted value, the scrvUSD system uses it to compute the time-weighted average of the deposited crvUSD in the Vault compared to the total circulating crvUSD supply.
The value is calculated over a specified time window defined by twa_window by iterating backwards over the snapshots stored in the snapshots dynamic array.
Function to compute the time-weighted average of the ratio between deposited crvUSD in the Vault and total circulating supply of crvUSD by iterating over the stored snapshots in reverse chronological order.
Returns: time-weighted average of the ratio between deposited crvUSD and total circulating supply of crvUSD (uint256).
Source code
snapshots:public(DynArray[Snapshot,MAX_SNAPSHOTS])min_snapshot_dt_seconds:public(uint256)# Minimum time between snapshots in secondstwa_window:public(uint256)# Time window in seconds for TWA calculationlast_snapshot_timestamp:public(uint256)# Timestamp of the last snapshotstructSnapshot:tracked_value:uint256timestamp:uint256@external@viewdefcompute_twa()->uint256:""" @notice External endpoint for _compute() function. """returnself._compute()@internal@viewdef_compute()->uint256:""" @notice Computes the TWA over the specified time window by iterating backwards over the snapshots. @return The TWA for tracked value over the self.twa_window. """num_snapshots:uint256=len(self.snapshots)ifnum_snapshots==0:return0time_window_start:uint256=block.timestamp-self.twa_windowtotal_weighted_tracked_value:uint256=0total_time:uint256=0# Iterate backwards over all snapshotsindex_array_end:uint256=num_snapshots-1fori:uint256inrange(0,num_snapshots,bound=MAX_SNAPSHOTS):# i from 0 to (num_snapshots-1)i_backwards:uint256=index_array_end-icurrent_snapshot:Snapshot=self.snapshots[i_backwards]next_snapshot:Snapshot=current_snapshotifi!=0:# If not the first iteration (last snapshot), get the next snapshotnext_snapshot=self.snapshots[i_backwards+1]# Time Axis (Increasing to the Right) ---># SNAPSHOT# |---------|---------|---------|------------------------|---------|---------|# t0 time_window_start interval_start interval_end block.timestamp (Now)interval_start:uint256=current_snapshot.timestamp# Adjust interval start if it is before the time window startifinterval_start<time_window_start:interval_start=time_window_startinterval_end:uint256=interval_startifi==0:# First iteration - we are on the last snapshot (i_backwards = num_snapshots - 1)# For the last snapshot, interval end is block.timestampinterval_end=block.timestampelse:# For other snapshots, interval end is the timestamp of the next snapshotinterval_end=next_snapshot.timestampifinterval_end<=time_window_start:breaktime_delta:uint256=interval_end-interval_start# Interpolation using the trapezoidal ruleaveraged_tracked_value:uint256=(current_snapshot.tracked_value+next_snapshot.tracked_value)//2# Accumulate weighted rate and timetotal_weighted_tracked_value+=averaged_tracked_value*time_deltatotal_time+=time_deltaiftotal_time==0andlen(self.snapshots)==1:# case when only snapshot is taken in the block where computation is calledreturnself.snapshots[0].tracked_valueasserttotal_time>0,"Zero total time!"twa:uint256=total_weighted_tracked_value//total_timereturntwa
This example returns the time-weighted average of the ratio between staked supply and total supply of crvUSD.
Getter for the time window in seconds which is applied to the TWA calculation, essentially the length of the time window over which the TWA is computed. This value can be changed using the set_twa_window function.
Returns: time window in seconds for TWA calculation (uint256).
Source code
twa_window:public(uint256)# Time window in seconds for TWA calculation@deploydef__init__(_twa_window:uint256,_min_snapshot_dt_seconds:uint256):self._set_twa_window(_twa_window)self._set_snapshot_dt(max(1,_min_snapshot_dt_seconds))
This example returns the time window in seconds for TWA calculation.
Getter for the weight of the rewards. This is the time-weighted average of the ratio between deposited crvUSD in the Vault and total circulating supply of crvUSD. This function is part of the dynamic weight interface expected by the FeeSplitter to know what percentage of funds should be sent for rewards distribution. Weight value is denominated in 10000 BPS (100%). E.g. if the weight is 2000, then RewardsHandler will request 20% of the total rewards from the FeeSplitter.
Returns: requested weight (uint256).
Source code
MAX_BPS:constant(uint256)=10**4# 100%# scaling factor for the deposited token / circulating supply ratio.scaling_factor:public(uint256)# the minimum amount of rewards requested to the FeeSplitter.minimum_weight:public(uint256)@external@viewdefweight()->uint256:""" @notice this function is part of the dynamic weight interface expected by the FeeSplitter to know what percentage of funds should be sent for rewards distribution to scrvUSD vault depositors. @dev `minimum_weight` acts as a lower bound for the percentage of rewards that should be distributed to depositors. This is useful to bootstrapping TVL by asking for more at the beginning and can also be increased in the future if someone tries to manipulate the time-weighted average of the tvl ratio. """raw_weight:uint256=twa._compute()*self.scaling_factor//MAX_BPSreturnmax(raw_weight,self.minimum_weight)
MAX_SNAPSHOTS:constant(uint256)=10**18# 31.7 billion years if snapshot every secondsnapshots:public(DynArray[Snapshot,MAX_SNAPSHOTS])min_snapshot_dt_seconds:public(uint256)# Minimum time between snapshots in secondstwa_window:public(uint256)# Time window in seconds for TWA calculationlast_snapshot_timestamp:public(uint256)# Timestamp of the last snapshotstructSnapshot:tracked_value:uint256# In 1e18 precisiontimestamp:uint256@internal@viewdef_compute()->uint256:""" @notice Computes the TWA over the specified time window by iterating backwards over the snapshots. @return The TWA for tracked value over the self.twa_window. """num_snapshots:uint256=len(self.snapshots)ifnum_snapshots==0:return0time_window_start:uint256=block.timestamp-self.twa_windowtotal_weighted_tracked_value:uint256=0total_time:uint256=0# Iterate backwards over all snapshotsindex_array_end:uint256=num_snapshots-1fori:uint256inrange(0,num_snapshots,bound=MAX_SNAPSHOTS):# i from 0 to (num_snapshots-1)i_backwards:uint256=index_array_end-icurrent_snapshot:Snapshot=self.snapshots[i_backwards]next_snapshot:Snapshot=current_snapshotifi!=0:# If not the first iteration (last snapshot), get the next snapshotnext_snapshot=self.snapshots[i_backwards+1]# Time Axis (Increasing to the Right) ---># SNAPSHOT# |---------|---------|---------|------------------------|---------|---------|# t0 time_window_start interval_start interval_end block.timestamp (Now)interval_start:uint256=current_snapshot.timestamp# Adjust interval start if it is before the time window startifinterval_start<time_window_start:interval_start=time_window_startinterval_end:uint256=interval_startifi==0:# First iteration - we are on the last snapshot (i_backwards = num_snapshots - 1)# For the last snapshot, interval end is block.timestampinterval_end=block.timestampelse:# For other snapshots, interval end is the timestamp of the next snapshotinterval_end=next_snapshot.timestampifinterval_end<=time_window_start:breaktime_delta:uint256=interval_end-interval_start# Interpolation using the trapezoidal ruleaveraged_tracked_value:uint256=(current_snapshot.tracked_value+next_snapshot.tracked_value)//2# Accumulate weighted rate and timetotal_weighted_tracked_value+=averaged_tracked_value*time_deltatotal_time+=time_deltaiftotal_time==0andlen(self.snapshots)==1:# case when only snapshot is taken in the block where computation is calledreturnself.snapshots[0].tracked_valueasserttotal_time>0,"Zero total time!"twa:uint256=total_weighted_tracked_value//total_timereturntwa
This example returns the weight the RewardsHandler will request from the FeeSplitter.
Getter for the minimum weight. This is the minimum weight requested from the FeeSplitter. Value is set at initialization and can be changed by the set_minimum_weight function.
Returns: minimum weight (uint256).
Source code
# the minimum amount of rewards requested to the FeeSplitter.minimum_weight:public(uint256)@deploydef__init__(_stablecoin:IERC20,_vault:IVault,minimum_weight:uint256,scaling_factor:uint256,controller_factory:lens.IControllerFactory,admin:address,):...self._set_minimum_weight(minimum_weight)...
This example returns the weight the RewardsHandler will request from the FeeSplitter.
Rewards are distributed to the Vault thought the RewardsHandler contract using a simple process_rewards function. This function permnissionlessly lets anyone distribute rewards to the Savings Vault.
Function to process the crvUSD rewards by transferring the available balance to the Vault and then calling the process_report function to start streaming the rewards to scrvUSD. This function is permissionless and can be called by anyone. When calling this function, the contracts entire crvUSD balance will be transferred and used as rewards for the stakers.
Source code
# the time over which rewards will be distributed mirror of the private# `profit_max_unlock_time` variable from yearn vaults.distribution_time:public(uint256)@externaldefprocess_rewards(take_snapshot:bool=True):""" @notice Permissionless function that let anyone distribute rewards (if any) to the crvUSD vault. """# optional (advised) snapshot before distributing the rewardsiftake_snapshot:self._take_snapshot()# prevent the rewards from being distributed untill the distribution rate# has been setassert(staticcallvault.profitMaxUnlockTime()!=0),"rewards should be distributed over time"# any crvUSD sent to this contract (usually through the fee splitter, but# could also come from other sources) will be used as a reward for scrvUSD# vault depositors.available_balance:uint256=staticcallstablecoin.balanceOf(self)assertavailable_balance>0,"no rewards to distribute"# we distribute funds in 2 steps:# 1. transfer the actual fundsextcallstablecoin.transfer(vault.address,available_balance)# 2. start streaming the rewards to usersextcallvault.process_report(vault.address)
# The amount of time profits will unlock over.profit_max_unlock_time:uint256@view@externaldefprofitMaxUnlockTime()->uint256:""" @notice Gets the current time profits are set to unlock over. @return The current profit max unlock time. """returnself.profit_max_unlock_time@external@nonreentrant("lock")defprocess_report(strategy:address)->(uint256,uint256):""" @notice Process the report of a strategy. @param strategy The strategy to process the report for. @return The gain and loss of the strategy. """self._enforce_role(msg.sender,Roles.REPORTING_MANAGER)returnself._process_report(strategy)@internaldef_process_report(strategy:address)->(uint256,uint256):""" Processing a report means comparing the debt that the strategy has taken with the current amount of funds it is reporting. If the strategy owes less than it currently has, it means it has had a profit, else (assets < debt) it has had a loss. Different strategies might choose different reporting strategies: pessimistic, only realised P&L, ... The best way to report depends on the strategy. The profit will be distributed following a smooth curve over the vaults profit_max_unlock_time seconds. Losses will be taken immediately, first from the profit buffer (avoiding an impact in pps), then will reduce pps. Any applicable fees are charged and distributed during the report as well to the specified recipients. Can update the vaults `totalIdle` to account for any airdropped tokens by passing the vaults address in as the parameter. """# Cache `asset` for repeated use._asset:address=self.assettotal_assets:uint256=0current_debt:uint256=0ifstrategy!=self:# Make sure we have a valid strategy.assertself.strategies[strategy].activation!=0,"inactive strategy"# Vault assesses profits using 4626 compliant interface. # NOTE: It is important that a strategies `convertToAssets` implementation# cannot be manipulated or else the vault could report incorrect gains/losses.strategy_shares:uint256=IStrategy(strategy).balanceOf(self)# How much the vaults position is worth.total_assets=IStrategy(strategy).convertToAssets(strategy_shares)# How much the vault had deposited to the strategy.current_debt=self.strategies[strategy].current_debtelse:# Accrue any airdropped `asset` into `total_idle`total_assets=ERC20(_asset).balanceOf(self)current_debt=self.total_idlegain:uint256=0loss:uint256=0### Asses Gain or Loss #### Compare reported assets vs. the current debt.iftotal_assets>current_debt:# We have a gain.gain=unsafe_sub(total_assets,current_debt)else:# We have a loss.loss=unsafe_sub(current_debt,total_assets)### Asses Fees and Refunds #### For Accountant fee assessment.total_fees:uint256=0total_refunds:uint256=0# If accountant is not set, fees and refunds remain unchanged.accountant:address=self.accountantifaccountant!=empty(address):total_fees,total_refunds=IAccountant(accountant).report(strategy,gain,loss)iftotal_refunds>0:# Make sure we have enough approval and enough asset to pull.total_refunds=min(total_refunds,min(ERC20(_asset).balanceOf(accountant),ERC20(_asset).allowance(accountant,self)))# Total fees to charge in shares.total_fees_shares:uint256=0# For Protocol fee assessment.protocol_fee_bps:uint16=0protocol_fees_shares:uint256=0protocol_fee_recipient:address=empty(address)# `shares_to_burn` is derived from amounts that would reduce the vaults PPS.# NOTE: this needs to be done before any pps changesshares_to_burn:uint256=0# Only need to burn shares if there is a loss or fees.ifloss+total_fees>0:# The amount of shares we will want to burn to offset losses and fees.shares_to_burn=self._convert_to_shares(loss+total_fees,Rounding.ROUND_UP)# If we have fees then get the proportional amount of shares to issue.iftotal_fees>0:# Get the total amount shares to issue for the fees.total_fees_shares=shares_to_burn*total_fees/(loss+total_fees)# Get the protocol fee config for this vault.protocol_fee_bps,protocol_fee_recipient=IFactory(self.factory).protocol_fee_config()# If there is a protocol fee.ifprotocol_fee_bps>0:# Get the percent of fees to go to protocol fees.protocol_fees_shares=total_fees_shares*convert(protocol_fee_bps,uint256)/MAX_BPS# Shares to lock is any amount that would otherwise increase the vaults PPS.shares_to_lock:uint256=0profit_max_unlock_time:uint256=self.profit_max_unlock_time# Get the amount we will lock to avoid a PPS increase.ifgain+total_refunds>0andprofit_max_unlock_time!=0:shares_to_lock=self._convert_to_shares(gain+total_refunds,Rounding.ROUND_DOWN)# The total current supply including locked shares.total_supply:uint256=self.total_supply# The total shares the vault currently owns. Both locked and unlocked.total_locked_shares:uint256=self.balance_of[self]# Get the desired end amount of shares after all accounting.ending_supply:uint256=total_supply+shares_to_lock-shares_to_burn-self._unlocked_shares()# If we will end with more shares than we have now.ifending_supply>total_supply:# Issue the difference.self._issue_shares(unsafe_sub(ending_supply,total_supply),self)# Else we need to burn shares.eliftotal_supply>ending_supply:# Can't burn more than the vault owns.to_burn:uint256=min(unsafe_sub(total_supply,ending_supply),total_locked_shares)self._burn_shares(to_burn,self)# Adjust the amount to lock for this period.ifshares_to_lock>shares_to_burn:# Don't lock fees or losses.shares_to_lock=unsafe_sub(shares_to_lock,shares_to_burn)else:shares_to_lock=0# Pull refundsiftotal_refunds>0:# Transfer the refunded amount of asset to the vault.self._erc20_safe_transfer_from(_asset,accountant,self,total_refunds)# Update storage to increase total assets.self.total_idle+=total_refunds# Record any reported gains.ifgain>0:# NOTE: this will increase total_assetscurrent_debt=unsafe_add(current_debt,gain)ifstrategy!=self:self.strategies[strategy].current_debt=current_debtself.total_debt+=gainelse:# Add in any refunds since it is now idle.current_debt=unsafe_add(current_debt,total_refunds)self.total_idle=current_debt# Or record any reported losselifloss>0:current_debt=unsafe_sub(current_debt,loss)ifstrategy!=self:self.strategies[strategy].current_debt=current_debtself.total_debt-=losselse:# Add in any refunds since it is now idle.current_debt=unsafe_add(current_debt,total_refunds)self.total_idle=current_debt# Issue shares for fees that were calculated above if applicable.iftotal_fees_shares>0:# Accountant fees are (total_fees - protocol_fees).self._issue_shares(total_fees_shares-protocol_fees_shares,accountant)# If we also have protocol fees.ifprotocol_fees_shares>0:self._issue_shares(protocol_fees_shares,protocol_fee_recipient)# Update unlocking rate and time to fully unlocked.total_locked_shares=self.balance_of[self]iftotal_locked_shares>0:previously_locked_time:uint256=0_full_profit_unlock_date:uint256=self.full_profit_unlock_date# Check if we need to account for shares still unlocking.if_full_profit_unlock_date>block.timestamp:# There will only be previously locked shares if time remains.# We calculate this here since it will not occur every time we lock shares.previously_locked_time=(total_locked_shares-shares_to_lock)*(_full_profit_unlock_date-block.timestamp)# new_profit_locking_period is a weighted average between the remaining time of the previously locked shares and the profit_max_unlock_timenew_profit_locking_period:uint256=(previously_locked_time+shares_to_lock*profit_max_unlock_time)/total_locked_shares# Calculate how many shares unlock per second.self.profit_unlocking_rate=total_locked_shares*MAX_BPS_EXTENDED/new_profit_locking_period# Calculate how long until the full amount of shares is unlocked.self.full_profit_unlock_date=block.timestamp+new_profit_locking_period# Update the last profitable report timestamp.self.last_profit_update=block.timestampelse:# NOTE: only setting this to the 0 will turn in the desired effect, # no need to update profit_unlocking_rateself.full_profit_unlock_date=0# Record the report of profit timestamp.self.strategies[strategy].last_report=block.timestamp# We have to recalculate the fees paid for cases with an overall loss or no profit lockingifloss+total_fees>gain+total_refundsorprofit_max_unlock_time==0:total_fees=self._convert_to_assets(total_fees_shares,Rounding.ROUND_DOWN)logStrategyReported(strategy,gain,loss,current_debt,total_fees*convert(protocol_fee_bps,uint256)/MAX_BPS,# Protocol Feestotal_fees,total_refunds)return(gain,loss)
Getter for the distribution time. This is the time it takes to stream the rewards.
Returns: distribution time (uint256).
Source code
@view@externaldefdistribution_time()->uint256:""" @notice Getter for the distribution time of the rewards. @return uint256 The time over which vault rewards will be distributed. """returnstaticcallvault.profitMaxUnlockTime()
# The amount of time profits will unlock over.profit_max_unlock_time:uint256@view@externaldefprofitMaxUnlockTime()->uint256:""" @notice Gets the current time profits are set to unlock over. @return The current profit max unlock time. """returnself.profit_max_unlock_time
This example returns the distribution time of the rewards.
The contract uses the Multi-Role-Based Access Control Module from Snekmate to manage roles and permissions. This module ensures that only specific addresses assigned the RATE_MANAGER role can modify key parameters such as the Time-Weighted Average (TWA) window, the minimum time between snapshots, and the distribution time. Roles can only be granted or revoked by the DEFAULT_ADMIN_ROLE defined in the access module.
For a detailed explanation of how to use the access control module, please refer to the source code where its mechanics are explained in detail: Snekmate access_control.vy.
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the RATE_MANAGER role.
Function to set a new value for the twa_window variable in the TWA module. This value represents the time window over which the time-weighted average (TWA) is calculated.
Emits: TWAWindowUpdated event.
Input
Type
Description
_twa_window
uint256
New value for the TWA window
Source code
fromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)RATE_MANAGER:public(constant(bytes32))=keccak256("RATE_MANAGER")@externaldefset_twa_window(_twa_window:uint256):""" @notice Setter for the time-weighted average window @param _twa_window The time window used to compute the TWA value of the balance/supply ratio. """access_control._check_role(RATE_MANAGER,msg.sender)twa._set_twa_window(_twa_window)
eventTWAWindowUpdated:new_window:uint256twa_window:public(uint256)# Time window in seconds for TWA calculation@internaldef_set_twa_window(_new_window:uint256):""" @notice Adjusts the TWA window. @param _new_window The new TWA window in seconds. @dev Only callable by the importing contract. """self.twa_window=_new_windowlogTWAWindowUpdated(_new_window)
This example sets the TWA window from 604800 seconds (1 week) to 302400 seconds (½ week).
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the RATE_MANAGER role.
Function to set a new value for the min_snapshot_dt_seconds variable in the TWA module. This value represents the minimum time between snapshots.
Emits: SnapshotIntervalUpdated event.
Input
Type
Description
_min_snapshot_dt_seconds
uint256
New value for the minimum time between snapshots
Source code
fromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)RATE_MANAGER:public(constant(bytes32))=keccak256("RATE_MANAGER")@externaldefset_twa_snapshot_dt(_min_snapshot_dt_seconds:uint256):""" @notice Setter for the time-weighted average minimal frequency. @param _min_snapshot_dt_seconds The minimum amount of time that should pass between two snapshots. """access_control._check_role(RATE_MANAGER,msg.sender)twa._set_snapshot_dt(_min_snapshot_dt_seconds)
eventSnapshotIntervalUpdated:new_dt_seconds:uint256min_snapshot_dt_seconds:public(uint256)# Minimum time between snapshots in seconds@internaldef_set_snapshot_dt(_new_dt_seconds:uint256):""" @notice Adjusts the minimum snapshot time interval. @param _new_dt_seconds The new minimum snapshot time interval in seconds. @dev Only callable by the importing contract. """self.min_snapshot_dt_seconds=_new_dt_secondslogSnapshotIntervalUpdated(_new_dt_seconds)
This example sets the minimum time between snapshots from 3600 seconds (1 hour) to 7200 seconds (2 hours).
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the RATE_MANAGER role.
Function to set the distribution time for the rewards. This is the time it takes to stream the rewards. Setting this value to 0 will immediately distribute all the rewards. If the value is set to a number greater than 0, the rewards will be distributed over the specified number of seconds.
Emits: UpdateProfitMaxUnlockTime and StrategyReported events from the Vault contract.
Input
Type
Description
new_distribution_time
uint256
New distribution time
Source code
fromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)RATE_MANAGER:public(constant(bytes32))=keccak256("RATE_MANAGER")@externaldefset_distribution_time(new_distribution_time:uint256):""" @notice Admin function to correct the distribution rate of the rewards. Making this value lower will reduce the time it takes to stream the rewards, making it longer will do the opposite. Setting it to 0 will immediately distribute all the rewards. @dev This function can be used to prevent the rewards distribution from being manipulated (i.e. MEV twa snapshots to obtain higher APR for the vault). Setting this value to zero can be used to pause `process_rewards`. """access_control._check_role(RATE_MANAGER,msg.sender)# change the distribution time of the rewards in the vaultextcallvault.setProfitMaxUnlockTime(new_distribution_time)# enact the changesextcallvault.process_report(vault.address)
eventStrategyReported:strategy:indexed(address)gain:uint256loss:uint256current_debt:uint256protocol_fees:uint256total_fees:uint256total_refunds:uint256eventUpdateProfitMaxUnlockTime:profit_max_unlock_time:uint256@externaldefsetProfitMaxUnlockTime(new_profit_max_unlock_time:uint256):""" @notice Set the new profit max unlock time. @dev The time is denominated in seconds and must be less than 1 year. We only need to update locking period if setting to 0, since the current period will use the old rate and on the next report it will be reset with the new unlocking time. Setting to 0 will cause any currently locked profit to instantly unlock and an immediate increase in the vaults Price Per Share. @param new_profit_max_unlock_time The new profit max unlock time. """self._enforce_role(msg.sender,Roles.PROFIT_UNLOCK_MANAGER)# Must be less than one year for report cyclesassertnew_profit_max_unlock_time<=31_556_952,"profit unlock time too long"# If setting to 0 we need to reset any locked values.if(new_profit_max_unlock_time==0):share_balance:uint256=self.balance_of[self]ifshare_balance>0:# Burn any shares the vault still has.self._burn_shares(share_balance,self)# Reset unlocking variables to 0.self.profit_unlocking_rate=0self.full_profit_unlock_date=0self.profit_max_unlock_time=new_profit_max_unlock_timelogUpdateProfitMaxUnlockTime(new_profit_max_unlock_time)@external@nonreentrant("lock")defprocess_report(strategy:address)->(uint256,uint256):""" @notice Process the report of a strategy. @param strategy The strategy to process the report for. @return The gain and loss of the strategy. """self._enforce_role(msg.sender,Roles.REPORTING_MANAGER)returnself._process_report(strategy)@internaldef_process_report(strategy:address)->(uint256,uint256):""" Processing a report means comparing the debt that the strategy has taken with the current amount of funds it is reporting. If the strategy owes less than it currently has, it means it has had a profit, else (assets < debt) it has had a loss. Different strategies might choose different reporting strategies: pessimistic, only realised P&L, ... The best way to report depends on the strategy. The profit will be distributed following a smooth curve over the vaults profit_max_unlock_time seconds. Losses will be taken immediately, first from the profit buffer (avoiding an impact in pps), then will reduce pps. Any applicable fees are charged and distributed during the report as well to the specified recipients. Can update the vaults `totalIdle` to account for any airdropped tokens by passing the vaults address in as the parameter. """# Cache `asset` for repeated use._asset:address=self.assettotal_assets:uint256=0current_debt:uint256=0ifstrategy!=self:# Make sure we have a valid strategy.assertself.strategies[strategy].activation!=0,"inactive strategy"# Vault assesses profits using 4626 compliant interface. # NOTE: It is important that a strategies `convertToAssets` implementation# cannot be manipulated or else the vault could report incorrect gains/losses.strategy_shares:uint256=IStrategy(strategy).balanceOf(self)# How much the vaults position is worth.total_assets=IStrategy(strategy).convertToAssets(strategy_shares)# How much the vault had deposited to the strategy.current_debt=self.strategies[strategy].current_debtelse:# Accrue any airdropped `asset` into `total_idle`total_assets=ERC20(_asset).balanceOf(self)current_debt=self.total_idlegain:uint256=0loss:uint256=0### Asses Gain or Loss #### Compare reported assets vs. the current debt.iftotal_assets>current_debt:# We have a gain.gain=unsafe_sub(total_assets,current_debt)else:# We have a loss.loss=unsafe_sub(current_debt,total_assets)### Asses Fees and Refunds #### For Accountant fee assessment.total_fees:uint256=0total_refunds:uint256=0# If accountant is not set, fees and refunds remain unchanged.accountant:address=self.accountantifaccountant!=empty(address):total_fees,total_refunds=IAccountant(accountant).report(strategy,gain,loss)iftotal_refunds>0:# Make sure we have enough approval and enough asset to pull.total_refunds=min(total_refunds,min(ERC20(_asset).balanceOf(accountant),ERC20(_asset).allowance(accountant,self)))# Total fees to charge in shares.total_fees_shares:uint256=0# For Protocol fee assessment.protocol_fee_bps:uint16=0protocol_fees_shares:uint256=0protocol_fee_recipient:address=empty(address)# `shares_to_burn` is derived from amounts that would reduce the vaults PPS.# NOTE: this needs to be done before any pps changesshares_to_burn:uint256=0# Only need to burn shares if there is a loss or fees.ifloss+total_fees>0:# The amount of shares we will want to burn to offset losses and fees.shares_to_burn=self._convert_to_shares(loss+total_fees,Rounding.ROUND_UP)# If we have fees then get the proportional amount of shares to issue.iftotal_fees>0:# Get the total amount shares to issue for the fees.total_fees_shares=shares_to_burn*total_fees/(loss+total_fees)# Get the protocol fee config for this vault.protocol_fee_bps,protocol_fee_recipient=IFactory(self.factory).protocol_fee_config()# If there is a protocol fee.ifprotocol_fee_bps>0:# Get the percent of fees to go to protocol fees.protocol_fees_shares=total_fees_shares*convert(protocol_fee_bps,uint256)/MAX_BPS# Shares to lock is any amount that would otherwise increase the vaults PPS.shares_to_lock:uint256=0profit_max_unlock_time:uint256=self.profit_max_unlock_time# Get the amount we will lock to avoid a PPS increase.ifgain+total_refunds>0andprofit_max_unlock_time!=0:shares_to_lock=self._convert_to_shares(gain+total_refunds,Rounding.ROUND_DOWN)# The total current supply including locked shares.total_supply:uint256=self.total_supply# The total shares the vault currently owns. Both locked and unlocked.total_locked_shares:uint256=self.balance_of[self]# Get the desired end amount of shares after all accounting.ending_supply:uint256=total_supply+shares_to_lock-shares_to_burn-self._unlocked_shares()# If we will end with more shares than we have now.ifending_supply>total_supply:# Issue the difference.self._issue_shares(unsafe_sub(ending_supply,total_supply),self)# Else we need to burn shares.eliftotal_supply>ending_supply:# Can't burn more than the vault owns.to_burn:uint256=min(unsafe_sub(total_supply,ending_supply),total_locked_shares)self._burn_shares(to_burn,self)# Adjust the amount to lock for this period.ifshares_to_lock>shares_to_burn:# Don't lock fees or losses.shares_to_lock=unsafe_sub(shares_to_lock,shares_to_burn)else:shares_to_lock=0# Pull refundsiftotal_refunds>0:# Transfer the refunded amount of asset to the vault.self._erc20_safe_transfer_from(_asset,accountant,self,total_refunds)# Update storage to increase total assets.self.total_idle+=total_refunds# Record any reported gains.ifgain>0:# NOTE: this will increase total_assetscurrent_debt=unsafe_add(current_debt,gain)ifstrategy!=self:self.strategies[strategy].current_debt=current_debtself.total_debt+=gainelse:# Add in any refunds since it is now idle.current_debt=unsafe_add(current_debt,total_refunds)self.total_idle=current_debt# Or record any reported losselifloss>0:current_debt=unsafe_sub(current_debt,loss)ifstrategy!=self:self.strategies[strategy].current_debt=current_debtself.total_debt-=losselse:# Add in any refunds since it is now idle.current_debt=unsafe_add(current_debt,total_refunds)self.total_idle=current_debt# Issue shares for fees that were calculated above if applicable.iftotal_fees_shares>0:# Accountant fees are (total_fees - protocol_fees).self._issue_shares(total_fees_shares-protocol_fees_shares,accountant)# If we also have protocol fees.ifprotocol_fees_shares>0:self._issue_shares(protocol_fees_shares,protocol_fee_recipient)# Update unlocking rate and time to fully unlocked.total_locked_shares=self.balance_of[self]iftotal_locked_shares>0:previously_locked_time:uint256=0_full_profit_unlock_date:uint256=self.full_profit_unlock_date# Check if we need to account for shares still unlocking.if_full_profit_unlock_date>block.timestamp:# There will only be previously locked shares if time remains.# We calculate this here since it will not occur every time we lock shares.previously_locked_time=(total_locked_shares-shares_to_lock)*(_full_profit_unlock_date-block.timestamp)# new_profit_locking_period is a weighted average between the remaining time of the previously locked shares and the profit_max_unlock_timenew_profit_locking_period:uint256=(previously_locked_time+shares_to_lock*profit_max_unlock_time)/total_locked_shares# Calculate how many shares unlock per second.self.profit_unlocking_rate=total_locked_shares*MAX_BPS_EXTENDED/new_profit_locking_period# Calculate how long until the full amount of shares is unlocked.self.full_profit_unlock_date=block.timestamp+new_profit_locking_period# Update the last profitable report timestamp.self.last_profit_update=block.timestampelse:# NOTE: only setting this to the 0 will turn in the desired effect, # no need to update profit_unlocking_rateself.full_profit_unlock_date=0# Record the report of profit timestamp.self.strategies[strategy].last_report=block.timestamp# We have to recalculate the fees paid for cases with an overall loss or no profit lockingifloss+total_fees>gain+total_refundsorprofit_max_unlock_time==0:total_fees=self._convert_to_assets(total_fees_shares,Rounding.ROUND_DOWN)logStrategyReported(strategy,gain,loss,current_debt,total_fees*convert(protocol_fee_bps,uint256)/MAX_BPS,# Protocol Feestotal_fees,total_refunds)return(gain,loss)
This example sets the distribution time from 1 week to ½ week.
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the LENS_MANAGER role.
Function to set a new stablecoin_lens address.
Emits: StablecoinLensUpdated event.
Input
Type
Description
_lens
address
New stablecoin_lens address
Source code
fromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)LENS_MANAGER:public(constant(bytes32))=keccak256("LENS_MANAGER")eventStablecoinLensUpdated:new_stablecoin_lens:IStablecoinLensstablecoin_lens:public(IStablecoinLens)@internaldef_set_stablecoin_lens(_lens:IStablecoinLens):assert_lens.address!=empty(address),"no lens"self.stablecoin_lens=_lenslogStablecoinLensUpdated(_lens)
This example sets the stablecoin_lens address to ZERO_ADDRESS. This is just an example but would not make sense in practice.
Function to set the minimum weight that the vault will ask for.
Input
Type
Description
new_minimum_weight
uint256
New minimum weight
Source code
fromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)eventMinimumWeightUpdated:new_minimum_weight:uint256MAX_BPS:constant(uint256)=10**4# 100%@externaldefset_minimum_weight(new_minimum_weight:uint256):""" @notice Update the minimum weight that the the vault will ask for. @dev This function can be used to prevent the rewards requested from being manipulated (i.e. MEV twa snapshots to obtain lower APR for the vault). Setting this value to zero makes the amount of rewards requested fully determined by the twa of the deposited supply ratio. """access_control._check_role(RATE_MANAGER,msg.sender)self._set_minimum_weight(new_minimum_weight)@internaldef_set_minimum_weight(new_minimum_weight:uint256):assertnew_minimum_weight<=MAX_BPS,"minimum weight should be <= 100%"self.minimum_weight=new_minimum_weightlogMinimumWeightUpdated(new_minimum_weight)
This example sets the minimum weight the RewardsHandler will ask for from 5% to 10%.
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the RATE_MANAGER role.
Function to change the scaling factor value.
Emits: ScalingFactorUpdated event.
Source code
eventScalingFactorUpdated:new_scaling_factor:uint256# scaling factor for the deposited token / circulating supply ratio.scaling_factor:public(uint256)@externaldefset_scaling_factor(new_scaling_factor:uint256):""" @notice Update the scaling factor that is used in the weight calculation. This factor can be used to adjust the rewards distribution rate. """access_control._check_role(RATE_MANAGER,msg.sender)self._set_scaling_factor(new_scaling_factor)@internaldef_set_scaling_factor(new_scaling_factor:uint256):self.scaling_factor=new_scaling_factorlogScalingFactorUpdated(new_scaling_factor)
This example sets the scaling factor from 10000 to 15000.
# pragma version ~=0.4@externaldefsetProfitMaxUnlockTime(new_profit_max_unlock_time:uint256):...@externaldefprocess_report(strategy:address)->(uint256,uint256):...@view@externaldeftotalAssets()->uint256:...@view@externaldefprofitMaxUnlockTime()->uint256:...
This example returns the scrvUSD YearnV3 vault address.
Function to check if the contract implements a specific interface.
Returns: True if the contract implements the interface, False otherwise.
Input
Type
Description
interface_id
bytes4
Interface ID to check
Source code
_SUPPORTED_INTERFACES:constant(bytes4[1])=[0xA1AAB33F,# The ERC-165 identifier for the dynamic weight interface.]fromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)@external@viewdefsupportsInterface(interface_id:bytes4)->bool:""" @dev Returns `True` if this contract implements the interface defined by `interface_id`. @param interface_id The 4-byte interface identifier. @return bool The verification whether the contract implements the interface or not. """return(interface_idinaccess_control._SUPPORTED_INTERFACESorinterface_idin_SUPPORTED_INTERFACES)
In this example, the address and weight of a receiver at a specific index is returned.
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the RECOVERY_MANAGER role.
Function to recover funds accidently sent to the contract. This function can not recover crvUSD tokens as any crvUSD tokens sent to the contract are considered as donations and will be distributed to stakers.
Input
Type
Description
token
IERC20
Address of the token to recover
receiver
address
Receier address of the recovered funds
Source code
fromethereum.ercsimportIERC20fromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)RECOVERY_MANAGER:public(constant(bytes32))=keccak256("RECOVERY_MANAGER")@externaldefrecover_erc20(token:IERC20,receiver:address):""" @notice This is a helper function to let an admin rescue funds sent by mistake to this contract. crvUSD cannot be recovered as it's part of the core logic of this contract. """access_control._check_role(RECOVERY_MANAGER,msg.sender)# if crvUSD was sent by accident to the contract the funds are lost and will# be distributed as rewards on the next `process_rewards` call.asserttoken!=stablecoin,"can't recover crvusd"# when funds are recovered the whole balanced is sent to a trusted address.balance_to_recover:uint256=staticcalltoken.balanceOf(self)assertextcalltoken.transfer(receiver,balance_to_recover,default_return_value=True)
In this example, all wETH tokens sent to the contract are recovered and sent to Curve Fee Collector.
Getter for the DEFAULT_ADMIN_ROLE role which is the keccak256 hash of the string "DEFAULT_ADMIN_ROLE". This variable is needed for compatibility with the access control module.
Returns: DEFAULT_ADMIN_ROLE (bytes32).
Source code
# we use access control because we want to have multiple addresses being able# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)# can appoint `RATE_MANAGER`sfromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)
# @dev The default 32-byte admin role.# @notice If you declare a variable as `public`,# Vyper automatically generates an `external`# getter function for the variable.DEFAULT_ADMIN_ROLE:public(constant(bytes32))=empty(bytes32)
Getter for the RATE_MANAGER role which is the keccak256 hash of the string "RATE_MANAGER". This variable is needed for compatibility with the access control module.
Getter for the RECOVERY_MANAGER role which is the keccak256 hash of the string "RECOVERY_MANAGER". This variable is needed for compatibility with the access control module.
Getter for the LENS_MANAGER role which is the keccak256 hash of the string "LENS_MANAGER". This variable is needed for compatibility with the access control module.
Getter to check if an account has a specific role.
Input
Type
Description
role
bytes32
Role to check
account
address
Account to check the role for
Source code
# we use access control because we want to have multiple addresses being able# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)# can appoint `RATE_MANAGER`sfromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)
# @dev Returns `True` if `account` has been granted `role`.hasRole:public(HashMap[bytes32,HashMap[address,bool]])
This example checks if 0x40907540d8a6C65c637785e8f8B742ae6b0b9968 has the DEFAULT_ADMIN_ROLE role.
# we use access control because we want to have multiple addresses being able# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)# can appoint `RATE_MANAGER`sfromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)
# @dev Returns the admin role that controls `role`.getRoleAdmin:public(HashMap[bytes32,bytes32])
This example returns the admin role for the RATE_MANAGER role.
# we use access control because we want to have multiple addresses being able# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)# can appoint `RATE_MANAGER`sfromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)
# @dev Returns the admin role that controls `role`.getRoleAdmin:public(HashMap[bytes32,bytes32])@externaldefgrantRole(role:bytes32,account:address):""" @dev Grants `role` to `account`. @notice If `account` had not been already granted `role`, emits a `RoleGranted` event. Note that the caller must have `role`'s admin role. @param role The 32-byte role definition. @param account The 20-byte address of the account. """self._check_role(self.getRoleAdmin[role],msg.sender)self._grant_role(role,account)@internaldef_grant_role(role:bytes32,account:address):""" @dev Grants `role` to `account`. @notice This is an `internal` function without access restriction. @param role The 32-byte role definition. @param account The 20-byte address of the account. """if(not(self.hasRole[role][account])):self.hasRole[role][account]=TruelogIAccessControl.RoleGranted(role=role,account=account,sender=msg.sender)
This example grants the RATE_MANAGER role to 0x40907540d8a6C65c637785e8f8B742ae6b0b9968.
# we use access control because we want to have multiple addresses being able# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)# can appoint `RATE_MANAGER`sfromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)
# @dev Returns the admin role that controls `role`.getRoleAdmin:public(HashMap[bytes32,bytes32])@externaldefrevokeRole(role:bytes32,account:address):""" @dev Revokes `role` from `account`. @notice If `account` had been granted `role`, emits a `RoleRevoked` event. Note that the caller must have `role`'s admin role. @param role The 32-byte role definition. @param account The 20-byte address of the account. """self._check_role(self.getRoleAdmin[role],msg.sender)self._revoke_role(role,account)@internaldef_revoke_role(role:bytes32,account:address):""" @dev Revokes `role` from `account`. @notice This is an `internal` function without access restriction. @param role The 32-byte role definition. @param account The 20-byte address of the account. """if(self.hasRole[role][account]):self.hasRole[role][account]=FalselogIAccessControl.RoleRevoked(role=role,account=account,sender=msg.sender)
This example revokes the RATE_MANAGER role from 0x40907540d8a6C65c637785e8f8B742ae6b0b9968.
# we use access control because we want to have multiple addresses being able# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)# can appoint `RATE_MANAGER`sfromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)
# @dev Returns the admin role that controls `role`.getRoleAdmin:public(HashMap[bytes32,bytes32])@externaldefrenounceRole(role:bytes32,account:address):""" @dev Revokes `role` from the calling account. @notice Roles are often managed via `grantRole` and `revokeRole`. This function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been granted `role`, emits a `RoleRevoked` event. Note that the caller must be `account`. @param role The 32-byte role definition. @param account The 20-byte address of the account. """assertaccount==msg.sender,"access_control: can only renounce roles for itself"self._revoke_role(role,account)@internaldef_revoke_role(role:bytes32,account:address):""" @dev Revokes `role` from `account`. @notice This is an `internal` function without access restriction. @param role The 32-byte role definition. @param account The 20-byte address of the account. """if(self.hasRole[role][account]):self.hasRole[role][account]=FalselogIAccessControl.RoleRevoked(role=role,account=account,sender=msg.sender)
This example renounces the RATE_MANAGER role from 0x40907540d8a6C65c637785e8f8B742ae6b0b9968.
# we use access control because we want to have multiple addresses being able# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)# can appoint `RATE_MANAGER`sfromsnekmate.authimportaccess_controlinitializes:access_controlexports:(# we don't expose `supportsInterface` from access controlaccess_control.grantRole,access_control.revokeRole,access_control.renounceRole,access_control.set_role_admin,access_control.DEFAULT_ADMIN_ROLE,access_control.hasRole,access_control.getRoleAdmin,)
# @dev Returns the admin role that controls `role`.getRoleAdmin:public(HashMap[bytes32,bytes32])@externaldefset_role_admin(role:bytes32,admin_role:bytes32):""" @dev Sets `admin_role` as `role`'s admin role. @notice Note that the caller must have `role`'s admin role. @param role The 32-byte role definition. @param admin_role The new 32-byte admin role definition. """self._check_role(self.getRoleAdmin[role],msg.sender)self._set_role_admin(role,admin_role)@internaldef_set_role_admin(role:bytes32,admin_role:bytes32):""" @dev Sets `admin_role` as `role`'s admin role. @notice This is an `internal` function without access restriction. @param role The 32-byte role definition. @param admin_role The new 32-byte admin role definition. """previous_admin_role:bytes32=self.getRoleAdmin[role]self.getRoleAdmin[role]=admin_rolelogIAccessControl.RoleAdminChanged(role=role,previousAdminRole=previous_admin_role,newAdminRole=admin_role)
This example sets the admin role for the RATE_MANAGER role to the DEFAULT_ADMIN_ROLE.