-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathRewardsHandler.vy
407 lines (304 loc) · 13.6 KB
/
RewardsHandler.vy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# pragma version ~=0.4
"""
@title Rewards Handler
@notice A contract that helps distributing rewards for scrvUSD, an ERC4626 vault
for crvUSD (yearn's vault v3 multi-vault implementaiton is used). Any crvUSD
token sent to this contract is considered donated as rewards for depositors and
will not be recoverable. This contract can receive funds to be distributed from
the FeeSplitter (crvUSD borrow rates revenues) and potentially other sources as
well. The amount of funds that this contract should receive from the fee
splitter is determined by computing the time-weighted average of the vault
balance over crvUSD circulating supply ratio. The contract handles the rewards
in a permissionless manner, anyone can take snapshots of the TVL and distribute
rewards. In case of manipulation of the time-weighted average, the contract
allows trusted contracts given the role of `RATE_MANGER` to correct the
distribution rate of the rewards.
@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved
@author curve.fi
@custom:security [email protected]
"""
################################################################
# INTERFACES #
################################################################
from ethereum.ercs import IERC20
from ethereum.ercs import IERC165
implements: IERC165
from contracts.interfaces import IDynamicWeight
implements: IDynamicWeight
from contracts.interfaces import IStablecoinLens
# yearn vault's interface
from interfaces import IVault
################################################################
# MODULES #
################################################################
# 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`s
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
# import custom modules that contain helper functions.
import TWA as twa
initializes: twa
exports: (
twa.compute_twa,
twa.snapshots,
twa.get_len_snapshots,
twa.twa_window,
twa.min_snapshot_dt_seconds,
twa.last_snapshot_timestamp,
)
################################################################
# EVENTS #
################################################################
event MinimumWeightUpdated:
new_minimum_weight: uint256
event ScalingFactorUpdated:
new_scaling_factor: uint256
event StablecoinLensUpdated:
new_stablecoin_lens: IStablecoinLens
################################################################
# CONSTANTS #
################################################################
RATE_MANAGER: public(constant(bytes32)) = keccak256("RATE_MANAGER")
RECOVERY_MANAGER: public(constant(bytes32)) = keccak256("RECOVERY_MANAGER")
LENS_MANAGER: public(constant(bytes32)) = keccak256("LENS_MANAGER")
WEEK: constant(uint256) = 86_400 * 7 # 7 days
MAX_BPS: constant(uint256) = 10**4 # 100%
_SUPPORTED_INTERFACES: constant(bytes4[1]) = [
0xA1AAB33F, # The ERC-165 identifier for the dynamic weight interface.
]
################################################################
# STORAGE #
################################################################
stablecoin: immutable(IERC20)
vault: public(immutable(IVault))
stablecoin_lens: public(IStablecoinLens)
# 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)
################################################################
# CONSTRUCTOR #
################################################################
@deploy
def __init__(
_stablecoin: IERC20,
_vault: IVault,
_lens: IStablecoinLens,
minimum_weight: uint256,
scaling_factor: uint256,
admin: address,
):
# initialize access control
access_control.__init__()
# admin (most likely the dao) controls who can be a rate manager
access_control._grant_role(access_control.DEFAULT_ADMIN_ROLE, admin)
# admin itself is a RATE_MANAGER and RECOVERY_MANAGER
access_control._grant_role(RATE_MANAGER, admin)
access_control._grant_role(RECOVERY_MANAGER, admin)
access_control._grant_role(LENS_MANAGER, admin)
# deployer does not control this contract
access_control._revoke_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
twa.__init__(
WEEK, # twa_window = 1 week
3_600, # min_snapshot_dt_seconds = 1 hour (3600 sec)
)
self._set_minimum_weight(minimum_weight)
self._set_scaling_factor(scaling_factor)
self._set_stablecoin_lens(_lens)
stablecoin = _stablecoin
vault = _vault
################################################################
# PERMISSIONLESS FUNCTIONS #
################################################################
@external
def take_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()
@internal
def _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' debt
circulating_supply: uint256 = staticcall self.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 = staticcall vault.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_supply
twa._take_snapshot(supply_ratio)
@external
def process_rewards(take_snapshot: bool = False):
"""
@notice Permissionless function that let anyone distribute rewards (if any) to
the crvUSD vault.
"""
# optional (advised) snapshot before distributing the rewards
if take_snapshot:
self._take_snapshot()
# prevent the rewards from being distributed untill the distribution rate
# has been set
assert (staticcall vault.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 = staticcall stablecoin.balanceOf(self)
assert available_balance > 0, "no rewards to distribute"
# we distribute funds in 2 steps:
# 1. transfer the actual funds
extcall stablecoin.transfer(vault.address, available_balance)
# 2. start streaming the rewards to users
extcall vault.process_report(vault.address)
################################################################
# VIEW FUNCTIONS #
################################################################
@external
@view
def supportsInterface(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_id in access_control._SUPPORTED_INTERFACES
or interface_id in _SUPPORTED_INTERFACES
)
@external
@view
def weight() -> 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_BPS
return max(raw_weight, self.minimum_weight)
################################################################
# ADMIN FUNCTIONS #
################################################################
@external
def set_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)
@external
def set_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)
@external
def set_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 vault
extcall vault.setProfitMaxUnlockTime(new_distribution_time)
# enact the changes
extcall vault.process_report(vault.address)
@view
@external
def distribution_time() -> uint256:
"""
@notice Getter for the distribution time of the rewards.
@return uint256 The time over which vault rewards will be distributed.
"""
return staticcall vault.profitMaxUnlockTime()
@external
def set_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)
@internal
def _set_minimum_weight(new_minimum_weight: uint256):
assert new_minimum_weight <= MAX_BPS, "minimum weight should be <= 100%"
self.minimum_weight = new_minimum_weight
log MinimumWeightUpdated(new_minimum_weight)
@external
def set_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)
@internal
def _set_scaling_factor(new_scaling_factor: uint256):
self.scaling_factor = new_scaling_factor
log ScalingFactorUpdated(new_scaling_factor)
@external
def set_stablecoin_lens(_lens: address):
"""
@notice Setter for the stablecoin lens that determines stablecoin circulating supply.
@param _lens The address of the new stablecoin lens.
"""
access_control._check_role(LENS_MANAGER, msg.sender)
self._set_stablecoin_lens(IStablecoinLens(_lens))
@internal
def _set_stablecoin_lens(_lens: IStablecoinLens):
assert _lens.address != empty(address), "no lens"
self.stablecoin_lens = _lens
log StablecoinLensUpdated(_lens)
@external
def recover_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.
assert token != stablecoin, "can't recover crvusd"
# when funds are recovered the whole balanced is sent to a trusted address.
balance_to_recover: uint256 = staticcall token.balanceOf(self)
assert extcall token.transfer(receiver, balance_to_recover, default_return_value=True)