-
Notifications
You must be signed in to change notification settings - Fork 3
/
FundraisingGaugeV1.vy
235 lines (179 loc) · 7.47 KB
/
FundraisingGaugeV1.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
# @version 0.3.1
"""
@title Fundraising Gauge
@license MIT
@author veFunder
@notice Custom gauge directing emissions entirely to a specific address up to a maximum
"""
interface CRV20:
def rate() -> uint256: view
def future_epoch_time_write() -> uint256: nonpayable
interface GaugeController:
def checkpoint_gauge(_gauge: address): nonpayable
def gauge_relative_weight(_gauge: address, _time: uint256) -> uint256: view
interface Minter:
def minted(_user: address, _gauge: address) -> uint256: view
event Checkpoint:
_timestamp: uint256
_new_emissions: uint256
ADMIN: immutable(address)
CRV: constant(address) = 0xD533a949740bb3306d119CC777fa900bA034cd52
GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB
MINTER: constant(address) = 0xd061D61a4d941c39E5453435B6345Dc261C2fcE0
WEEK: constant(uint256) = 604800
YEAR: constant(uint256) = 86400 * 365
# taken from CRV20 to allow calculating locally
RATE_DENOMINATOR: constant(uint256) = 10 ** 18
RATE_REDUCTION_COEFFICIENT: constant(uint256) = 1189207115002721024 # 2 ** (1/4) * 1e18
RATE_REDUCTION_TIME: constant(uint256) = YEAR
# [uint216 inflation_rate][uint40 future_epoch_time]
inflation_params: uint256
_is_killed: bool
# _user => accumulated CRV
integrate_fraction: public(HashMap[address, uint256])
last_checkpoint: public(uint256)
receiver: public(address)
max_emissions: public(uint256)
@external
def __init__(_admin: address):
ADMIN = _admin
# prevent initialization of the implementation contract
self.last_checkpoint = MAX_UINT256
@internal
def _user_checkpoint(_user: address) -> bool:
# timestamp of the last checkpoint and start point for calculating new emissions
prev_week_time: uint256 = self.last_checkpoint
# if time has not advanced since the last checkpoint
if block.timestamp == prev_week_time:
return True
# load the receiver
receiver: address = self.receiver
max_emissions: uint256 = self.max_emissions
# initialize emission tracking variables
receiver_emissions: uint256 = self.integrate_fraction[receiver]
# if the maximum emissions has already been reached return early
if receiver_emissions == max_emissions:
return True
# cache the receiver emissions var
cached_receiver_emissions: uint256 = receiver_emissions
# load and unpack inflation params
inflation_params: uint256 = self.inflation_params
rate: uint256 = shift(inflation_params, -40)
future_epoch_time: uint256 = bitwise_and(inflation_params, 2 ** 40 - 1)
# checkpoint the gauge filling in any missing gauge data across weeks
GaugeController(GAUGE_CONTROLLER).checkpoint_gauge(self)
# either the start of the next week or the current timestamp
week_time: uint256 = min((prev_week_time + WEEK) / WEEK * WEEK, block.timestamp)
# iterate 512 times at maximum
for i in range(512):
dt: uint256 = week_time - prev_week_time
w: uint256 = GaugeController(GAUGE_CONTROLLER).gauge_relative_weight(self, prev_week_time / WEEK * WEEK)
period_emissions: uint256 = 0
# if we cross over an inflation epoch, calculate the emissions using old and new rate
if prev_week_time <= future_epoch_time and future_epoch_time < week_time:
# calculate up to the epoch using the old rate
period_emissions = rate * w * (future_epoch_time - prev_week_time) / 10 ** 18
# update the rate in memory
rate = rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT
# calculate using the new rate for the rest of the time period
period_emissions += rate * w * (week_time - future_epoch_time) / 10 ** 18
# update the new future epoch time
future_epoch_time += RATE_REDUCTION_TIME
# update storage
self.inflation_params = shift(rate, 40) + future_epoch_time
else:
period_emissions = rate * w * dt / 10 ** 18
# if adding period emissions is still below max emissions add to receiver
if receiver_emissions + period_emissions <= max_emissions:
receiver_emissions += period_emissions
# else set received emissions at max and break
else:
receiver_emissions = max_emissions
break
if week_time == block.timestamp:
break
# update timestamps for tracking timedelta
prev_week_time = week_time
week_time = min(week_time + WEEK, block.timestamp)
# this will only be the case if receiver got emissions
if receiver_emissions != cached_receiver_emissions:
self.integrate_fraction[receiver] = receiver_emissions
self.last_checkpoint = block.timestamp
log Checkpoint(block.timestamp, (receiver_emissions - cached_receiver_emissions))
return True
@external
def user_checkpoint(_user: address) -> bool:
"""
@notice Checkpoint the gauge updating total emissions
@param _user The user to checkpoint and update accumulated emissions for
"""
return self._user_checkpoint(_user)
@external
def claimable_tokens_write(_user: address) -> uint256:
"""
@notice Get the number of claimable tokens per user
@dev This function should be manually changed to "view" in the ABI
@param _user The user to check claimable emissions of
@return uint256 number of claimable tokens per user
"""
self._user_checkpoint(_user)
return self.integrate_fraction[_user] - Minter(MINTER).minted(_user, self)
@external
def set_killed(_is_killed: bool):
"""
@notice Set the gauge status
@dev Inflation params are modified accordingly to disable/enable emissions
"""
assert msg.sender == ADMIN
if _is_killed:
self._is_killed = True
self.inflation_params = 0
else:
self._is_killed = False
self.inflation_params = shift(CRV20(CRV).rate(), 40) + CRV20(CRV).future_epoch_time_write()
@view
@external
def is_killed() -> bool:
"""
@notice Get whether this gauge is killed and not receiving anymore emissions
@dev This will return True if the max emissions has been reached or if set to killed by
the ADMIN.
"""
return self.integrate_fraction[self.receiver] == self.max_emissions or self._is_killed
@view
@external
def inflation_rate() -> uint256:
"""
@notice Get the locally stored inflation rate
"""
return shift(self.inflation_params, -40)
@view
@external
def future_epoch_time() -> uint256:
"""
@notice Get the locally stored timestamp of the inflation rate epoch end
"""
return bitwise_and(self.inflation_params, 2 ** 40 - 1)
@pure
@external
def admin() -> address:
"""
@notice Get the address of the admin which can kill this gauge
"""
return ADMIN
@external
def initialize(_receiver: address, _max_emissions: uint256):
"""
@notice Proxy initializer method
@dev Placed last in the source file to save some gas, this fn is called only once.
Additional checks should be made by the DAO before voting in this gauge, specifically
to make sure that `_fund_recipient` is capable of collecting emissions.
@param _receiver The address which will receive CRV emissions
@param _max_emissions The maximum amount of emissions which `_receiver` will
receive
"""
assert self.last_checkpoint == 0 # dev: already initialized
self.receiver = _receiver
self.max_emissions = _max_emissions
self.last_checkpoint = block.timestamp
self.inflation_params = shift(CRV20(CRV).rate(), 40) + CRV20(CRV).future_epoch_time_write()