-
Notifications
You must be signed in to change notification settings - Fork 387
/
DepositMUSD.vy
376 lines (322 loc) · 14 KB
/
DepositMUSD.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
# @version 0.2.7
"""
@title "Zap" Depositer for Curve MUSD pool
@author Curve.Fi
@license Copyright (c) Curve.Fi, 2020 - all rights reserved
"""
from vyper.interfaces import ERC20
interface CurveMeta:
def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint256: nonpayable
def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) -> uint256[N_COINS]: nonpayable
def remove_liquidity_one_coin(_token_amount: uint256, i: int128, min_amount: uint256) -> uint256: nonpayable
def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) -> uint256: nonpayable
def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: view
def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256: view
def base_pool() -> address: view
def coins(i: uint256) -> address: view
interface CurveBase:
def add_liquidity(amounts: uint256[BASE_N_COINS], min_mint_amount: uint256): nonpayable
def remove_liquidity(_amount: uint256, min_amounts: uint256[BASE_N_COINS]): nonpayable
def remove_liquidity_one_coin(_token_amount: uint256, i: int128, min_amount: uint256): nonpayable
def remove_liquidity_imbalance(amounts: uint256[BASE_N_COINS], max_burn_amount: uint256): nonpayable
def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: view
def calc_token_amount(amounts: uint256[BASE_N_COINS], deposit: bool) -> uint256: view
def coins(i: uint256) -> address: view
def fee() -> uint256: view
N_COINS: constant(int128) = 2
MAX_COIN: constant(int128) = N_COINS-1
BASE_N_COINS: constant(int128) = 3
N_ALL_COINS: constant(int128) = N_COINS + BASE_N_COINS - 1
# An asset which may have a transfer fee (USDT)
FEE_ASSET: constant(address) = 0xdAC17F958D2ee523a2206206994597C13D831ec7
FEE_DENOMINATOR: constant(uint256) = 10 ** 10
FEE_IMPRECISION: constant(uint256) = 100 * 10 ** 8 # % of the fee
pool: public(address)
token: public(address)
base_pool: public(address)
coins: public(address[N_COINS])
base_coins: public(address[BASE_N_COINS])
@external
def __init__(_pool: address, _token: address):
"""
@notice Contract constructor
@param _pool Metapool address
@param _token Pool LP token address
"""
self.pool = _pool
self.token = _token
_base_pool: address = CurveMeta(_pool).base_pool()
self.base_pool = _base_pool
for i in range(N_COINS):
coin: address = CurveMeta(_pool).coins(convert(i, uint256))
self.coins[i] = coin
# approve coins for infinite transfers
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("approve(address,uint256)"),
convert(_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
for i in range(BASE_N_COINS):
coin: address = CurveBase(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
@external
def add_liquidity(amounts: uint256[N_ALL_COINS], min_mint_amount: uint256) -> uint256:
"""
@notice Wrap underlying coins and deposit them in the pool
@param amounts List of amounts of underlying coins to deposit
@param min_mint_amount Minimum amount of LP tokens to mint from the deposit
@return Amount of LP tokens received by depositing
"""
meta_amounts: uint256[N_COINS] = empty(uint256[N_COINS])
base_amounts: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
deposit_base: bool = False
# Transfer all coins in
for i in range(N_ALL_COINS):
amount: uint256 = amounts[i]
if amount == 0:
continue
coin: address = ZERO_ADDRESS
if i < MAX_COIN:
coin = self.coins[i]
meta_amounts[i] = amount
else:
x: int128 = i - MAX_COIN
coin = self.base_coins[x]
base_amounts[x] = amount
deposit_base = True
# "safeTransferFrom" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransferFrom"
# Handle potential Tether fees
if coin == FEE_ASSET:
amount = ERC20(FEE_ASSET).balanceOf(self)
if i < MAX_COIN:
meta_amounts[i] = amount
else:
base_amounts[i - MAX_COIN] = amount
# Deposit to the base pool
if deposit_base:
CurveBase(self.base_pool).add_liquidity(base_amounts, 0)
meta_amounts[MAX_COIN] = ERC20(self.coins[MAX_COIN]).balanceOf(self)
# Deposit to the meta pool
CurveMeta(self.pool).add_liquidity(meta_amounts, min_mint_amount)
# Transfer meta token back
_lp_token: address = self.token
_lp_amount: uint256 = ERC20(_lp_token).balanceOf(self)
assert ERC20(_lp_token).transfer(msg.sender, _lp_amount)
return _lp_amount
@external
def remove_liquidity(_amount: uint256, min_amounts: uint256[N_ALL_COINS]) -> uint256[N_ALL_COINS]:
"""
@notice Withdraw and unwrap coins from the pool
@dev Withdrawal amounts are based on current deposit ratios
@param _amount Quantity of LP tokens to burn in the withdrawal
@param min_amounts Minimum amounts of underlying coins to receive
@return List of amounts of underlying coins that were withdrawn
"""
_token: address = self.token
assert ERC20(_token).transferFrom(msg.sender, self, _amount)
min_amounts_meta: uint256[N_COINS] = empty(uint256[N_COINS])
min_amounts_base: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
amounts: uint256[N_ALL_COINS] = empty(uint256[N_ALL_COINS])
# Withdraw from meta
for i in range(MAX_COIN):
min_amounts_meta[i] = min_amounts[i]
CurveMeta(self.pool).remove_liquidity(_amount, min_amounts_meta)
# Withdraw from base
_base_amount: uint256 = ERC20(self.coins[MAX_COIN]).balanceOf(self)
for i in range(BASE_N_COINS):
min_amounts_base[i] = min_amounts[MAX_COIN+i]
CurveBase(self.base_pool).remove_liquidity(_base_amount, min_amounts_base)
# Transfer all coins out
for i in range(N_ALL_COINS):
coin: address = ZERO_ADDRESS
if i < MAX_COIN:
coin = self.coins[i]
else:
coin = self.base_coins[i - MAX_COIN]
amounts[i] = ERC20(coin).balanceOf(self)
# "safeTransfer" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(amounts[i], bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransfer"
return amounts
@external
def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) -> uint256:
"""
@notice Withdraw and unwrap a single coin from the pool
@param _token_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@param _min_amount Minimum amount of underlying coin to receive
@return Amount of underlying coin received
"""
assert ERC20(self.token).transferFrom(msg.sender, self, _token_amount)
coin: address = ZERO_ADDRESS
if i < MAX_COIN:
coin = self.coins[i]
# Withdraw a metapool coin
CurveMeta(self.pool).remove_liquidity_one_coin(_token_amount, i, _min_amount)
else:
coin = self.base_coins[i - MAX_COIN]
# Withdraw a base pool coin
CurveMeta(self.pool).remove_liquidity_one_coin(_token_amount, MAX_COIN, 0)
CurveBase(self.base_pool).remove_liquidity_one_coin(
ERC20(self.coins[MAX_COIN]).balanceOf(self), i-MAX_COIN, _min_amount
)
# Tranfer the coin out
coin_amount: uint256 = ERC20(coin).balanceOf(self)
# "safeTransfer" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(coin_amount, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransfer"
return coin_amount
@external
def remove_liquidity_imbalance(amounts: uint256[N_ALL_COINS], max_burn_amount: uint256) -> uint256:
"""
@notice Withdraw coins from the pool in an imbalanced amount
@param amounts List of amounts of underlying coins to withdraw
@param max_burn_amount Maximum amount of LP token to burn in the withdrawal.
This value cannot exceed the caller's LP token balance.
@return Actual amount of the LP token burned in the withdrawal
"""
_base_pool: address = self.base_pool
_meta_pool: address = self.pool
_base_coins: address[BASE_N_COINS] = self.base_coins
_meta_coins: address[N_COINS] = self.coins
_lp_token: address = self.token
fee: uint256 = CurveBase(_base_pool).fee() * BASE_N_COINS / (4 * (BASE_N_COINS - 1))
fee += fee * FEE_IMPRECISION / FEE_DENOMINATOR # Overcharge to account for imprecision
# Transfer the LP token in
assert ERC20(_lp_token).transferFrom(msg.sender, self, max_burn_amount)
withdraw_base: bool = False
amounts_base: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
amounts_meta: uint256[N_COINS] = empty(uint256[N_COINS])
leftover_amounts: uint256[N_COINS] = empty(uint256[N_COINS])
# Prepare quantities
for i in range(MAX_COIN):
amounts_meta[i] = amounts[i]
for i in range(BASE_N_COINS):
amount: uint256 = amounts[MAX_COIN + i]
if amount != 0:
amounts_base[i] = amount
withdraw_base = True
if withdraw_base:
amounts_meta[MAX_COIN] = CurveBase(self.base_pool).calc_token_amount(amounts_base, False)
amounts_meta[MAX_COIN] += amounts_meta[MAX_COIN] * fee / FEE_DENOMINATOR + 1
# Remove liquidity and deposit leftovers back
CurveMeta(_meta_pool).remove_liquidity_imbalance(amounts_meta, max_burn_amount)
if withdraw_base:
CurveBase(_base_pool).remove_liquidity_imbalance(amounts_base, amounts_meta[MAX_COIN])
leftover_amounts[MAX_COIN] = ERC20(_meta_coins[MAX_COIN]).balanceOf(self)
if leftover_amounts[MAX_COIN] > 0:
CurveMeta(_meta_pool).add_liquidity(leftover_amounts, 0)
# Transfer all coins out
for i in range(N_ALL_COINS):
coin: address = ZERO_ADDRESS
amount: uint256 = 0
if i < MAX_COIN:
coin = _meta_coins[i]
amount = amounts_meta[i]
else:
coin = _base_coins[i - MAX_COIN]
amount = amounts_base[i - MAX_COIN]
# "safeTransfer" which works for ERC20s which return bool or not
if amount > 0:
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransfer"
# Transfer the leftover LP token out
leftover: uint256 = ERC20(_lp_token).balanceOf(self)
if leftover > 0:
assert ERC20(_lp_token).transfer(msg.sender, leftover)
return max_burn_amount - leftover
@view
@external
def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256:
"""
@notice Calculate the amount received when withdrawing and unwrapping a single coin
@param _token_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the underlying coin to withdraw
@return Amount of coin received
"""
if i < MAX_COIN:
return CurveMeta(self.pool).calc_withdraw_one_coin(_token_amount, i)
else:
_base_tokens: uint256 = CurveMeta(self.pool).calc_withdraw_one_coin(_token_amount, MAX_COIN)
return CurveBase(self.base_pool).calc_withdraw_one_coin(_base_tokens, i-MAX_COIN)
@view
@external
def calc_token_amount(amounts: uint256[N_ALL_COINS], is_deposit: bool) -> uint256:
"""
@notice Calculate addition or reduction in token supply from a deposit or withdrawal
@dev This calculation accounts for slippage, but not fees.
Needed to prevent front-running, not for precise calculations!
@param amounts Amount of each underlying coin being deposited
@param is_deposit set True for deposits, False for withdrawals
@return Expected amount of LP tokens received
"""
meta_amounts: uint256[N_COINS] = empty(uint256[N_COINS])
base_amounts: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
for i in range(MAX_COIN):
meta_amounts[i] = amounts[i]
for i in range(BASE_N_COINS):
base_amounts[i] = amounts[i + MAX_COIN]
_base_tokens: uint256 = CurveBase(self.base_pool).calc_token_amount(base_amounts, is_deposit)
meta_amounts[MAX_COIN] = _base_tokens
return CurveMeta(self.pool).calc_token_amount(meta_amounts, is_deposit)