-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathMigratorZap.sol
470 lines (421 loc) · 15.5 KB
/
MigratorZap.sol
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
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
// SPDX-License-Identifier: MIT
pragma solidity =0.8.15;
import {TransferLib} from "@src/lib/TransferLib.sol";
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IWETH9} from "@uni-periphery/interfaces/external/IWETH9.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import {INFTXVaultV2} from "@src/v2/interfaces/INFTXVaultV2.sol";
import {INFTXVaultV3} from "@src/interfaces/INFTXVaultV3.sol";
import {IUniswapV2Pair} from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import {IUniswapV2Router02} from "@src/interfaces/external/IUniswapV2Router02.sol";
import {INFTXVaultFactoryV3} from "@src/interfaces/INFTXVaultFactoryV3.sol";
import {INFTXVaultFactoryV2} from "@src/v2/interfaces/INFTXVaultFactoryV2.sol";
import {INFTXInventoryStakingV2} from "@src/v2/interfaces/INFTXInventoryStakingV2.sol";
import {INFTXInventoryStakingV3} from "@src/interfaces/INFTXInventoryStakingV3.sol";
import {INonfungiblePositionManager} from "@uni-periphery/interfaces/INonfungiblePositionManager.sol";
/**
* @title NFTX Migrator Zap
* @author @apoorvlathey
* @notice Migrates positions from NFTX v2 to v3
* @dev This Zap must be excluded from vault fees in both NFTX v2 & v3.
*/
contract MigratorZap is Ownable {
using SafeERC20 for IERC20;
struct SushiToNFTXAMMParams {
// Sushiswap pool address for vTokenV2 <> WETH pair
address sushiPair;
// LP balance to withdraw from sushiswap
uint256 lpAmount;
// Vault address in NFTX v2
address vTokenV2;
// If underlying vault NFT is ERC1155
bool is1155;
// Encoded permit signature for sushiPair
bytes permitSig;
// Vault id in NFTX v3
uint256 vaultIdV3;
// Add liquidity params for NFTX AMM:
int24 tickLower;
int24 tickUpper;
uint24 fee;
// this price is used if new pool needs to be initialized
uint160 sqrtPriceX96;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
// =============================================================
// CONSTANTS
// =============================================================
uint256 private constant DEADLINE =
0xf000000000000000000000000000000000000000000000000000000000000000;
uint256 private constant DUST_THRESHOLD = 0.005 ether;
uint256 private constant V2_VTOKEN_DUST = 100; // 100 wei
IWETH9 public immutable WETH;
INFTXVaultFactoryV2 public immutable v2NFTXFactory;
INFTXInventoryStakingV2 public immutable v2Inventory;
IUniswapV2Router02 public immutable sushiRouter;
INonfungiblePositionManager public immutable positionManager;
INFTXVaultFactoryV3 public immutable v3NFTXFactory;
INFTXInventoryStakingV3 public immutable v3Inventory;
// =============================================================
// ERRORS
// =============================================================
error InvalidSignatureLength();
// =============================================================
// INIT
// =============================================================
constructor(
IWETH9 WETH_,
INFTXVaultFactoryV2 v2NFTXFactory_,
INFTXInventoryStakingV2 v2Inventory_,
IUniswapV2Router02 sushiRouter_,
INonfungiblePositionManager positionManager_,
INFTXVaultFactoryV3 v3NFTXFactory_,
INFTXInventoryStakingV3 v3Inventory_
) {
WETH = WETH_;
v2NFTXFactory = v2NFTXFactory_;
v2Inventory = v2Inventory_;
sushiRouter = sushiRouter_;
positionManager = positionManager_;
v3NFTXFactory = v3NFTXFactory_;
v3Inventory = v3Inventory_;
WETH_.approve(address(positionManager_), type(uint256).max);
}
// =============================================================
// PUBLIC / EXTERNAL WRITE
// =============================================================
/**
* @notice Migrates liquidity from Sushiswap to NFTX AMM
*/
function sushiToNFTXAMM(
SushiToNFTXAMMParams calldata params
) external returns (uint256 positionId) {
if (params.permitSig.length > 0) {
_permit(params.sushiPair, params.lpAmount, params.permitSig);
}
uint256 wethBalance;
address vTokenV3;
uint256 vTokenV3Balance;
{
// withdraw liquidity from Sushiswap
uint256 vTokenV2Balance;
(vTokenV2Balance, wethBalance) = _withdrawFromSushi(
params.sushiPair,
params.lpAmount,
params.vTokenV2
);
// convert v2 to v3 vault tokens
uint256 wethReceived;
(vTokenV3, vTokenV3Balance, wethReceived) = _v2ToV3Vault(
params.vTokenV2,
vTokenV2Balance,
params.vaultIdV3,
params.is1155,
0 // passing zero here as `positionManager.mint` takes this into account via `amount0Min` or `amount1Min`
);
wethBalance += wethReceived;
}
bool isVTokenV30 = vTokenV3 < address(WETH);
(
address token0,
address token1,
uint256 amount0,
uint256 amount1
) = isVTokenV30
? (vTokenV3, address(WETH), vTokenV3Balance, wethBalance)
: (address(WETH), vTokenV3, wethBalance, vTokenV3Balance);
// deploy new pool if it doesn't yet exist
positionManager.createAndInitializePoolIfNecessary(
token0,
token1,
params.fee,
params.sqrtPriceX96
);
// give vToken approval to positionManager
TransferLib.unSafeMaxApprove(
vTokenV3,
address(positionManager),
vTokenV3Balance
);
// provide liquidity to NFTX AMM
uint256 newAmount0;
uint256 newAmount1;
(positionId, , newAmount0, newAmount1) = positionManager.mint(
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: params.fee,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
amount0Desired: amount0,
amount1Desired: amount1,
amount0Min: params.amount0Min,
amount1Min: params.amount1Min,
recipient: msg.sender,
deadline: params.deadline
})
);
// refund any dust left
if (newAmount0 < amount0) {
IERC20(token0).transfer(msg.sender, amount0 - newAmount0);
}
if (newAmount1 < amount1) {
IERC20(token1).transfer(msg.sender, amount1 - newAmount1);
}
}
/**
* @notice Move vTokens from v2 Inventory to v3 Inventory (minting xNFT)
* @dev Must give xToken approval before calling this function
*/
function v2InventoryToXNFT(
uint256 vaultIdV2,
uint256 shares,
bool is1155,
uint256 vaultIdV3,
uint256 minWethToReceive
) external returns (uint256 xNFTId) {
address xToken = v2Inventory.vaultXToken(vaultIdV2);
IERC20(xToken).transferFrom(msg.sender, address(this), shares);
v2Inventory.withdraw(vaultIdV2, shares);
address vTokenV2 = v2NFTXFactory.vault(vaultIdV2);
uint256 vTokenV2Balance = IERC20(vTokenV2).balanceOf(address(this));
// to account for rounding down when withdrawing xTokens to vTokens
uint256 numNftsRedeemable = vTokenV2Balance / 1 ether;
uint256 numNftsRedeemableAfterDust = (vTokenV2Balance +
V2_VTOKEN_DUST) / 1 ether;
if (numNftsRedeemableAfterDust > numNftsRedeemable) {
// having few wei more of vTokens (100 wei at max) would result in redeeming one whole more NFT
uint256 vTokensToBuy = numNftsRedeemableAfterDust *
1 ether -
vTokenV2Balance;
// swapping ETH from this contract to get `vTokensToBuy`
address[] memory path = new address[](2);
path[0] = address(WETH);
path[1] = vTokenV2;
sushiRouter.swapETHForExactTokens{value: 1_000_000_000}(
vTokensToBuy,
path,
address(this),
block.timestamp
);
// update var to the latest balance
vTokenV2Balance = IERC20(vTokenV2).balanceOf(address(this));
}
(
address vTokenV3,
uint256 vTokenV3Balance,
uint256 wethReceived
) = _v2ToV3Vault(
vTokenV2,
vTokenV2Balance,
vaultIdV3,
is1155,
minWethToReceive
);
if (wethReceived > 0) {
WETH.transfer(msg.sender, wethReceived);
}
TransferLib.unSafeMaxApprove(
vTokenV3,
address(v3Inventory),
vTokenV3Balance
);
xNFTId = v3Inventory.deposit(
vaultIdV3,
vTokenV3Balance,
msg.sender,
"",
false,
false
);
}
/**
* @notice Move v2 vTokens to v3 Inventory (minting xNFT)
* @dev Must give v2 VToken approval before calling this function
*/
function v2VaultToXNFT(
address vTokenV2,
uint256 vTokenV2Balance,
bool is1155,
uint256 vaultIdV3,
uint256 minWethToReceive
) external returns (uint256 xNFTId) {
IERC20(vTokenV2).transferFrom(
msg.sender,
address(this),
vTokenV2Balance
);
(
address vTokenV3,
uint256 vTokenV3Balance,
uint256 wethReceived
) = _v2ToV3Vault(
vTokenV2,
vTokenV2Balance,
vaultIdV3,
is1155,
minWethToReceive
);
if (wethReceived > 0) {
WETH.transfer(msg.sender, wethReceived);
}
TransferLib.unSafeMaxApprove(
vTokenV3,
address(v3Inventory),
vTokenV3Balance
);
xNFTId = v3Inventory.deposit(
vaultIdV3,
vTokenV3Balance,
msg.sender,
"",
false,
false
);
}
// =============================================================
// ONLY OWNER WRITE
// =============================================================
function rescueTokens(address token) external onlyOwner {
if (token != address(0)) {
uint256 balance = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransfer(msg.sender, balance);
} else {
uint256 balance = address(this).balance;
TransferLib.transferETH(msg.sender, balance);
}
}
// =============================================================
// INTERNAL HELPERS
// =============================================================
function _permit(
address sushiPair,
uint256 lpAmount,
bytes memory permitSig
) internal {
if (permitSig.length != 65) revert InvalidSignatureLength();
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(permitSig, 32))
s := mload(add(permitSig, 64))
v := byte(0, mload(add(permitSig, 96)))
}
IUniswapV2Pair(sushiPair).permit(
msg.sender,
address(this),
lpAmount,
DEADLINE,
v,
r,
s
);
}
function _withdrawFromSushi(
address sushiPair,
uint256 lpAmount,
address vTokenV2
) internal returns (uint256 vTokenV2Balance, uint256 wethBalance) {
// burn sushi liquidity to this contract
IUniswapV2Pair(sushiPair).transferFrom(msg.sender, sushiPair, lpAmount);
(uint256 amount0, uint256 amount1) = IUniswapV2Pair(sushiPair).burn(
address(this)
);
bool isVTokenV20 = vTokenV2 < address(WETH);
(vTokenV2Balance, wethBalance) = isVTokenV20
? (amount0, amount1)
: (amount1, amount0);
}
function _v2ToV3Vault(
address vTokenV2,
uint256 vTokenV2Balance,
uint256 vaultIdV3,
bool is1155,
uint256 minWethToReceive
)
internal
returns (
address vTokenV3,
uint256 vTokenV3Balance,
uint256 wethReceived
)
{
vTokenV3 = v3NFTXFactory.vault(vaultIdV3);
address assetAddress = INFTXVaultV3(vTokenV3).assetAddress();
bool isCryptoPunk = (assetAddress == TransferLib.CRYPTO_PUNKS);
// random redeem v2 vTokens. Directly transferring to the v3 vault
uint256[] memory emptyArray;
uint256[] memory idsRedeemed = INFTXVaultV2(vTokenV2).redeemTo(
vTokenV2Balance / 1 ether,
emptyArray,
is1155 ? address(this) : (isCryptoPunk ? address(this) : vTokenV3)
);
if (isCryptoPunk) {
for (uint256 i; i < idsRedeemed.length; ) {
// from TransferLib._approveCryptoPunkERC721()
bytes memory data = abi.encodeWithSignature(
"offerPunkForSaleToAddress(uint256,uint256,address)",
idsRedeemed[i],
0,
vTokenV3 // to = v3 vault address
);
(bool success, bytes memory resultData) = TransferLib
.CRYPTO_PUNKS
.call(data);
require(success, string(resultData));
unchecked {
++i;
}
}
}
if (is1155) {
IERC1155(assetAddress).setApprovalForAll(vTokenV3, true);
}
// fractional portion of vToken would be left
vTokenV2Balance = vTokenV2Balance % 1 ether;
// sell fractional portion for WETH
if (vTokenV2Balance > DUST_THRESHOLD) {
address[] memory path = new address[](2);
path[0] = vTokenV2;
path[1] = address(WETH);
TransferLib.unSafeMaxApprove(
vTokenV2,
address(sushiRouter),
vTokenV2Balance
);
wethReceived = sushiRouter.swapExactTokensForTokens(
vTokenV2Balance,
minWethToReceive,
path,
address(this),
block.timestamp
)[path.length - 1];
} else if (vTokenV2Balance > 0) {
// send back the vTokens as not worth the swap gas fees
IERC20(vTokenV2).transfer(msg.sender, vTokenV2Balance);
}
// mint v3 vault tokens with the nfts received
uint256[] memory amounts;
if (is1155) {
amounts = new uint256[](idsRedeemed.length);
for (uint256 i; i < idsRedeemed.length; ) {
amounts[i] = 1;
unchecked {
++i;
}
}
}
vTokenV3Balance = INFTXVaultV3(vTokenV3).mint(
idsRedeemed,
amounts,
msg.sender,
address(this)
);
}
// To fund the zap with ETH to swap for missing vTokens during v2 redeem
receive() external payable {}
}