/
LiquidityGauge.vy
312 lines (249 loc) · 10.6 KB
/
LiquidityGauge.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
# @version 0.2.4
"""
@title Liquidity Gauge
@author Curve Finance
@license MIT
@notice Used for measuring liquidity and insurance
"""
from vyper.interfaces import ERC20
interface CRV20:
def future_epoch_time_write() -> uint256: nonpayable
def rate() -> uint256: view
interface Controller:
def period() -> int128: view
def period_write() -> int128: nonpayable
def period_timestamp(p: int128) -> uint256: view
def gauge_relative_weight(addr: address, time: uint256) -> uint256: view
def voting_escrow() -> address: view
def checkpoint(): nonpayable
def checkpoint_gauge(addr: address): nonpayable
interface Minter:
def token() -> address: view
def controller() -> address: view
def minted(user: address, gauge: address) -> uint256: view
interface VotingEscrow:
def user_point_epoch(addr: address) -> uint256: view
def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view
event Deposit:
provider: indexed(address)
value: uint256
event Withdraw:
provider: indexed(address)
value: uint256
event UpdateLiquidityLimit:
user: address
original_balance: uint256
original_supply: uint256
working_balance: uint256
working_supply: uint256
TOKENLESS_PRODUCTION: constant(uint256) = 40
BOOST_WARMUP: constant(uint256) = 3600
WEEK: constant(uint256) = 604800
minter: public(address)
crv_token: public(address)
lp_token: public(address)
controller: public(address)
voting_escrow: public(address)
balanceOf: public(HashMap[address, uint256])
totalSupply: public(uint256)
future_epoch_time: public(uint256)
# caller -> recipient -> can deposit?
approved_to_deposit: public(HashMap[address, HashMap[address, bool]])
working_balances: public(HashMap[address, uint256])
working_supply: public(uint256)
# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint
# All values are kept in units of being multiplied by 1e18
period: public(int128)
period_timestamp: public(uint256[100000000000000000000000000000])
# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint
integrate_inv_supply: public(uint256[100000000000000000000000000000]) # bump epoch when rate() changes
# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint
integrate_inv_supply_of: public(HashMap[address, uint256])
integrate_checkpoint_of: public(HashMap[address, uint256])
# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint
# Units: rate * t = already number of coins per address to issue
integrate_fraction: public(HashMap[address, uint256])
inflation_rate: public(uint256)
@external
def __init__(lp_addr: address, _minter: address):
"""
@notice Contract constructor
@param lp_addr Liquidity Pool contract address
@param _minter Minter contract address
"""
assert lp_addr != ZERO_ADDRESS
assert _minter != ZERO_ADDRESS
self.lp_token = lp_addr
self.minter = _minter
crv_addr: address = Minter(_minter).token()
self.crv_token = crv_addr
controller_addr: address = Minter(_minter).controller()
self.controller = controller_addr
self.voting_escrow = Controller(controller_addr).voting_escrow()
self.period_timestamp[0] = block.timestamp
self.inflation_rate = CRV20(crv_addr).rate()
self.future_epoch_time = CRV20(crv_addr).future_epoch_time_write()
@internal
def _update_liquidity_limit(addr: address, l: uint256, L: uint256):
"""
@notice Calculate limits which depend on the amount of CRV token per-user.
Effectively it calculates working balances to apply amplification
of CRV production by CRV
@param addr User address
@param l User's amount of liquidity (LP tokens)
@param L Total amount of liquidity (LP tokens)
"""
# To be called after totalSupply is updated
_voting_escrow: address = self.voting_escrow
voting_balance: uint256 = ERC20(_voting_escrow).balanceOf(addr)
voting_total: uint256 = ERC20(_voting_escrow).totalSupply()
lim: uint256 = l * TOKENLESS_PRODUCTION / 100
if (voting_total > 0) and (block.timestamp > self.period_timestamp[0] + BOOST_WARMUP):
lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100
lim = min(l, lim)
old_bal: uint256 = self.working_balances[addr]
self.working_balances[addr] = lim
_working_supply: uint256 = self.working_supply + lim - old_bal
self.working_supply = _working_supply
log UpdateLiquidityLimit(addr, l, L, lim, _working_supply)
@internal
def _checkpoint(addr: address):
"""
@notice Checkpoint for a user
@param addr User address
"""
_token: address = self.crv_token
_controller: address = self.controller
_period: int128 = self.period
_period_time: uint256 = self.period_timestamp[_period]
_integrate_inv_supply: uint256 = self.integrate_inv_supply[_period]
rate: uint256 = self.inflation_rate
new_rate: uint256 = rate
prev_future_epoch: uint256 = self.future_epoch_time
if prev_future_epoch >= _period_time:
self.future_epoch_time = CRV20(_token).future_epoch_time_write()
new_rate = CRV20(_token).rate()
self.inflation_rate = new_rate
Controller(_controller).checkpoint_gauge(self)
_working_balance: uint256 = self.working_balances[addr]
_working_supply: uint256 = self.working_supply
# Update integral of 1/supply
if block.timestamp > _period_time:
prev_week_time: uint256 = _period_time
week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp)
for i in range(500):
dt: uint256 = week_time - prev_week_time
w: uint256 = Controller(_controller).gauge_relative_weight(self, prev_week_time / WEEK * WEEK)
if _working_supply > 0:
if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time:
# If we went across one or multiple epochs, apply the rate
# of the first epoch until it ends, and then the rate of
# the last epoch.
# If more than one epoch is crossed - the gauge gets less,
# but that'd mean it wasn't called for more than 2 weeks
_integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply
rate = new_rate
_integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply
else:
_integrate_inv_supply += rate * w * dt / _working_supply
# On precisions of the calculation
# rate ~= 10e18
# last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%)
# _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example)
# The largest loss is at dt = 1
# Loss is 1e-9 - acceptable
if week_time == block.timestamp:
break
prev_week_time = week_time
week_time = min(week_time + WEEK, block.timestamp)
_period += 1
self.period = _period
self.period_timestamp[_period] = block.timestamp
self.integrate_inv_supply[_period] = _integrate_inv_supply
# Update user-specific integrals
self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18
self.integrate_inv_supply_of[addr] = _integrate_inv_supply
self.integrate_checkpoint_of[addr] = block.timestamp
@external
def user_checkpoint(addr: address) -> bool:
"""
@notice Record a checkpoint for `addr`
@param addr User address
@return bool success
"""
assert (msg.sender == addr) or (msg.sender == self.minter) # dev: unauthorized
self._checkpoint(addr)
self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply)
return True
@external
def claimable_tokens(addr: address) -> uint256:
"""
@notice Get the number of claimable tokens per user
@dev This function should be manually changed to "view" in the ABI
@return uint256 number of claimable tokens per user
"""
self._checkpoint(addr)
return self.integrate_fraction[addr] - Minter(self.minter).minted(addr, self)
@external
def kick(addr: address):
"""
@notice Kick `addr` for abusing their boost
@dev Only if either they had another voting event, or their voting escrow lock expired
@param addr Address to kick
"""
_voting_escrow: address = self.voting_escrow
t_last: uint256 = self.integrate_checkpoint_of[addr]
t_ve: uint256 = VotingEscrow(_voting_escrow).user_point_history__ts(
addr, VotingEscrow(_voting_escrow).user_point_epoch(addr)
)
_balance: uint256 = self.balanceOf[addr]
assert ERC20(self.voting_escrow).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed
assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed
self._checkpoint(addr)
self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply)
@external
def set_approve_deposit(addr: address, can_deposit: bool):
"""
@notice Set whether `addr` can deposit tokens for `msg.sender`
@param addr Address to set approval on
@param can_deposit bool - can this account deposit for `msg.sender`?
"""
self.approved_to_deposit[addr][msg.sender] = can_deposit
@external
@nonreentrant('lock')
def deposit(_value: uint256, addr: address = msg.sender):
"""
@notice Deposit `_value` LP tokens
@param _value Number of tokens to deposit
@param addr Address to deposit for
"""
if addr != msg.sender:
assert self.approved_to_deposit[msg.sender][addr], "Not approved"
self._checkpoint(addr)
if _value != 0:
_balance: uint256 = self.balanceOf[addr] + _value
_supply: uint256 = self.totalSupply + _value
self.balanceOf[addr] = _balance
self.totalSupply = _supply
self._update_liquidity_limit(addr, _balance, _supply)
assert ERC20(self.lp_token).transferFrom(msg.sender, self, _value)
log Deposit(addr, _value)
@external
@nonreentrant('lock')
def withdraw(_value: uint256):
"""
@notice Withdraw `_value` LP tokens
@param _value Number of tokens to withdraw
"""
self._checkpoint(msg.sender)
_balance: uint256 = self.balanceOf[msg.sender] - _value
_supply: uint256 = self.totalSupply - _value
self.balanceOf[msg.sender] = _balance
self.totalSupply = _supply
self._update_liquidity_limit(msg.sender, _balance, _supply)
assert ERC20(self.lp_token).transfer(msg.sender, _value)
log Withdraw(msg.sender, _value)
@external
@view
def integrate_checkpoint() -> uint256:
return self.period_timestamp[self.period]