/
NiftyswapExchange.sol
885 lines (749 loc) · 38.6 KB
/
NiftyswapExchange.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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.4;
import {INiftyswapExchange} from "../interfaces/INiftyswapExchange.sol";
import {ReentrancyGuard} from "../utils/ReentrancyGuard.sol";
import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol";
import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol";
import {IERC1155TokenReceiver} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155TokenReceiver.sol";
import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC1155MintBurn.sol";
/**
* This Uniswap-like implementation supports ERC-1155 standard tokens
* with an ERC-1155 based token used as a currency instead of Ether.
*
* See https://github.com/0xsequence/erc20-meta-token for a generalized
* ERC-20 => ERC-1155 token wrapper
*
* Liquidity tokens are also ERC-1155 tokens you can find the ERC-1155
* implementation used here:
* https://github.com/horizon-games/multi-token-standard/tree/master/contracts/tokens/ERC1155
*
* @dev Like Uniswap, tokens with 0 decimals and low supply are susceptible to significant rounding
* errors when it comes to removing liquidity, possibly preventing them to be withdrawn without
* some collaboration between liquidity providers.
*/
contract NiftyswapExchange is ReentrancyGuard, ERC1155MintBurn, INiftyswapExchange {
// Variables
IERC1155 internal token; // address of the ERC-1155 token contract
IERC1155 internal currency; // address of the ERC-1155 currency used for exchange
bool internal currencyPoolBanned; // Whether the currency token ID can have a pool or not
address internal factory; // address for the factory that created this contract
uint256 internal currencyID; // ID of currency token in ERC-1155 currency contract
uint256 internal constant FEE_MULTIPLIER = 995; // Multiplier that calculates the fee (1.0%)
// Mapping variables
mapping(uint256 => uint256) internal totalSupplies; // Liquidity pool token supply per Token id
mapping(uint256 => uint256) internal currencyReserves; // currency Token reserve per Token id
//
// Constructor
//
/**
* @notice Create instance of exchange contract with respective token and currency token
* @param _tokenAddr The address of the ERC-1155 Token
* @param _currencyAddr The address of the ERC-1155 currency Token
* @param _currencyID The ID of the ERC-1155 currency Token
*/
constructor(address _tokenAddr, address _currencyAddr, uint256 _currencyID) {
require(
address(_tokenAddr) != address(0) && _currencyAddr != address(0),
"NE#01" // NiftyswapExchange#constructor:INVALID_INPUT
);
factory = msg.sender;
token = IERC1155(_tokenAddr);
currency = IERC1155(_currencyAddr);
currencyID = _currencyID;
// If token and currency are the same contract,
// need to prevent currency/currency pool to be created.
currencyPoolBanned = _currencyAddr == _tokenAddr ? true : false;
}
//
// Exchange Functions
//
/**
* @notice Convert currency tokens to Tokens _id and transfers Tokens to recipient.
* @dev User specifies MAXIMUM inputs (_maxCurrency) and EXACT outputs.
* @dev Assumes that all trades will be successful, or revert the whole tx
* @dev Exceeding currency tokens sent will be refunded to recipient
* @dev Sorting IDs is mandatory for efficient way of preventing duplicated IDs (which would lead to exploit)
* @param _tokenIds Array of Tokens ID that are bought
* @param _tokensBoughtAmounts Amount of Tokens id bought for each corresponding Token id in _tokenIds
* @param _maxCurrency Total maximum amount of currency tokens to spend for all Token ids
* @param _deadline Timestamp after which this transaction will be reverted
* @param _recipient The address that receives output Tokens and refund
* @return currencySold How much currency was actually sold.
*/
function _currencyToToken(
uint256[] memory _tokenIds,
uint256[] memory _tokensBoughtAmounts,
uint256 _maxCurrency,
uint256 _deadline,
address _recipient
) internal nonReentrant returns (uint256[] memory currencySold) {
// Input validation
// solhint-disable-next-line not-rely-on-time
require(_deadline >= block.timestamp, "NE#02"); // NiftyswapExchange#_currencyToToken: DEADLINE_EXCEEDED
// Number of Token IDs to deposit
uint256 nTokens = _tokenIds.length;
uint256 totalRefundCurrency = _maxCurrency;
// Initialize variables
currencySold = new uint256[](nTokens); // Amount of currency tokens sold per ID
uint256[] memory tokenReserves = new uint256[](nTokens); // Amount of tokens in reserve for each Token id
// Get token reserves
tokenReserves = _getTokenReserves(_tokenIds);
// Assumes he currency Tokens are already received by contract, but not
// the Tokens Ids
// Remove liquidity for each Token ID in _tokenIds
for (uint256 i = 0; i < nTokens; i++) {
// Store current id and amount from argument arrays
uint256 idBought = _tokenIds[i];
uint256 amountBought = _tokensBoughtAmounts[i];
uint256 tokenReserve = tokenReserves[i];
require(amountBought > 0, "NE#03"); // NiftyswapExchange#_currencyToToken: NULL_TOKENS_BOUGHT
// Load currency token and Token _id reserves
uint256 currencyReserve = currencyReserves[idBought];
// Get amount of currency tokens to send for purchase
// Neither reserves amount have been changed so far in this transaction, so
// no adjustment to the inputs is needed
uint256 currencyAmount = getBuyPrice(amountBought, currencyReserve, tokenReserve);
// Calculate currency token amount to refund (if any) where whatever is not used will be returned
// Will throw if total cost exceeds _maxCurrency
totalRefundCurrency -= currencyAmount;
// Append Token id, Token id amount and currency token amount to tracking arrays
currencySold[i] = currencyAmount;
// Update individual currency reseve amount
currencyReserves[idBought] = currencyReserve + currencyAmount;
}
// Refund currency token if any
if (totalRefundCurrency > 0) {
currency.safeTransferFrom(address(this), _recipient, currencyID, totalRefundCurrency, "");
}
// Send Tokens all tokens purchased
token.safeBatchTransferFrom(address(this), _recipient, _tokenIds, _tokensBoughtAmounts, "");
return currencySold;
}
/**
* @dev Pricing function used for converting between currency token to Tokens.
* @param _assetBoughtAmount Amount of Tokens being bought.
* @param _assetSoldReserve Amount of currency tokens in exchange reserves.
* @param _assetBoughtReserve Amount of Tokens (output type) in exchange reserves.
* @return price Amount of currency tokens to send to Niftyswap.
*/
function getBuyPrice(uint256 _assetBoughtAmount, uint256 _assetSoldReserve, uint256 _assetBoughtReserve)
public
pure
override
returns (uint256 price)
{
// Reserves must not be empty
require(_assetSoldReserve > 0 && _assetBoughtReserve > 0, "NE#04"); // NiftyswapExchange#getBuyPrice: EMPTY_RESERVE
// Calculate price with fee
uint256 numerator = _assetSoldReserve * _assetBoughtAmount * 1000;
uint256 denominator = (_assetBoughtReserve - _assetBoughtAmount) * FEE_MULTIPLIER;
(price,) = divRound(numerator, denominator);
return price; // Will add 1 if rounding error
}
/**
* @notice Convert Tokens _id to currency tokens and transfers Tokens to recipient.
* @dev User specifies EXACT Tokens _id sold and MINIMUM currency tokens received.
* @dev Assumes that all trades will be valid, or the whole tx will fail
* @dev Sorting _tokenIds is mandatory for efficient way of preventing duplicated IDs (which would lead to errors)
* @param _tokenIds Array of Token IDs that are sold
* @param _tokensSoldAmounts Array of Amount of Tokens sold for each id in _tokenIds.
* @param _minCurrency Minimum amount of currency tokens to receive
* @param _deadline Timestamp after which this transaction will be reverted
* @param _recipient The address that receives output currency tokens.
* @return currencyBought How much currency was actually purchased.
*/
function _tokenToCurrency(
uint256[] memory _tokenIds,
uint256[] memory _tokensSoldAmounts,
uint256 _minCurrency,
uint256 _deadline,
address _recipient
) internal nonReentrant returns (uint256[] memory currencyBought) {
// Number of Token IDs to deposit
uint256 nTokens = _tokenIds.length;
// Input validation
// solhint-disable-next-line not-rely-on-time
require(_deadline >= block.timestamp, "NE#05"); // NiftyswapExchange#_tokenToCurrency: DEADLINE_EXCEEDED
// Initialize variables
uint256 totalCurrency = 0; // Total amount of currency tokens to transfer
currencyBought = new uint256[](nTokens);
uint256[] memory tokenReserves = new uint256[](nTokens);
// Get token reserves
tokenReserves = _getTokenReserves(_tokenIds);
// Assumes the Tokens ids are already received by contract, but not
// the Tokens Ids. Will return cards not sold if invalid price.
// Remove liquidity for each Token ID in _tokenIds
for (uint256 i = 0; i < nTokens; i++) {
// Store current id and amount from argument arrays
uint256 idSold = _tokenIds[i];
uint256 amountSold = _tokensSoldAmounts[i];
uint256 tokenReserve = tokenReserves[i];
// If 0 tokens send for this ID, revert
require(amountSold > 0, "NE#06"); // NiftyswapExchange#_tokenToCurrency: NULL_TOKENS_SOLD
// Load currency token and Token _id reserves
uint256 currencyReserve = currencyReserves[idSold];
// Get amount of currency that will be received
// Need to sub amountSold because tokens already added in reserve, which would bias the calculation
// Don't need to add it for currencyReserve because the amount is added after this calculation
uint256 currencyAmount = getSellPrice(amountSold, tokenReserve - amountSold, currencyReserve);
// Increase cost of transaction
totalCurrency += currencyAmount;
// Update individual currency reseve amount
currencyReserves[idSold] = currencyReserve - currencyAmount;
// Append Token id, Token id amount and currency token amount to tracking arrays
currencyBought[i] = currencyAmount;
}
// If minCurrency is not met
require(totalCurrency >= _minCurrency, "NE#07"); // NiftyswapExchange#_tokenToCurrency: INSUFFICIENT_CURRENCY_AMOUNT
// Transfer currency here
currency.safeTransferFrom(address(this), _recipient, currencyID, totalCurrency, "");
return currencyBought;
}
/**
* @dev Pricing function used for converting Tokens to currency token.
* @param _assetSoldAmount Amount of Tokens being sold.
* @param _assetSoldReserve Amount of Tokens in exchange reserves.
* @param _assetBoughtReserve Amount of currency tokens in exchange reserves.
* @return price Amount of currency tokens to receive from Niftyswap.
*/
function getSellPrice(uint256 _assetSoldAmount, uint256 _assetSoldReserve, uint256 _assetBoughtReserve)
public
pure
override
returns (uint256 price)
{
//Reserves must not be empty
require(_assetSoldReserve > 0 && _assetBoughtReserve > 0, "NE#08"); // NiftyswapExchange#getSellPrice: EMPTY_RESERVE
// Calculate amount to receive (with fee)
uint256 _assetSoldAmount_withFee = _assetSoldAmount * FEE_MULTIPLIER;
uint256 numerator = _assetSoldAmount_withFee * _assetBoughtReserve;
uint256 denominator = (_assetSoldReserve * 1000) + _assetSoldAmount_withFee;
return numerator / denominator; //Rounding errors will favor Niftyswap, so nothing to do
}
//
// Liquidity Functions
//
/**
* @notice Deposit less than max currency tokens && exact Tokens (token ID) at current ratio to mint liquidity pool tokens.
* @dev min_liquidity does nothing when total liquidity pool token supply is 0.
* @dev Assumes that sender approved this contract on the currency
* @dev Sorting _tokenIds is mandatory for efficient way of preventing duplicated IDs (which would lead to errors)
* @param _provider Address that provides liquidity to the reserve
* @param _tokenIds Array of Token IDs where liquidity is added
* @param _tokenAmounts Array of amount of Tokens deposited corresponding to each ID provided in _tokenIds
* @param _maxCurrency Array of maximum number of tokens deposited for each ID provided in _tokenIds.
* Deposits max amount if total liquidity pool token supply is 0.
* @param _deadline Timestamp after which this transaction will be reverted
*/
function _addLiquidity(
address _provider,
uint256[] memory _tokenIds,
uint256[] memory _tokenAmounts,
uint256[] memory _maxCurrency,
uint256 _deadline
) internal nonReentrant {
// Requirements
// solhint-disable-next-line not-rely-on-time
require(_deadline >= block.timestamp, "NE#09"); // NiftyswapExchange#_addLiquidity: DEADLINE_EXCEEDED
// Initialize variables
uint256 nTokens = _tokenIds.length; // Number of Token IDs to deposit
uint256 totalCurrency = 0; // Total amount of currency tokens to transfer
// Initialize arrays
uint256[] memory liquiditiesToMint = new uint256[](nTokens);
uint256[] memory currencyAmounts = new uint256[](nTokens);
uint256[] memory tokenReserves = new uint256[](nTokens);
// Get token reserves
tokenReserves = _getTokenReserves(_tokenIds);
// Assumes tokens _ids are deposited already, but not currency tokens
// as this is calculated and executed below.
// Loop over all Token IDs to deposit
for (uint256 i = 0; i < nTokens; i++) {
// Store current id and amount from argument arrays
uint256 tokenId = _tokenIds[i];
uint256 amount = _tokenAmounts[i];
// Check if input values are acceptable
require(_maxCurrency[i] > 0, "NE#10"); // NiftyswapExchange#_addLiquidity: NULL_MAX_CURRENCY
require(amount > 0, "NE#11"); // NiftyswapExchange#_addLiquidity: NULL_TOKENS_AMOUNT
// If the token contract and currency contract are the same, prevent the creation
// of a currency pool.
if (currencyPoolBanned) {
require(tokenId != currencyID, "NE#12"); // NiftyswapExchange#_addLiquidity: CURRENCY_POOL_FORBIDDEN
}
// Current total liquidity calculated in currency token
uint256 totalLiquidity = totalSupplies[tokenId];
// When reserve for this token already exists
if (totalLiquidity > 0) {
// Load currency token and Token reserve's supply of Token id
uint256 currencyReserve = currencyReserves[tokenId]; // Amount not yet in reserve
uint256 tokenReserve = tokenReserves[i];
/**
* Amount of currency tokens to send to token id reserve:
* X/Y = dx/dy
* dx = X*dy/Y
* where
* X: currency total liquidity
* Y: Token _id total liquidity (before tokens were received)
* dy: Amount of token _id deposited
* dx: Amount of currency to deposit
*
* Adding 1 if rounding errors so to not favor users incorrectly
*/
(uint256 currencyAmount, bool rounded) = divRound(amount * currencyReserve, tokenReserve - amount);
require(
_maxCurrency[i] >= currencyAmount,
"NE#13" // NiftyswapExchange#_addLiquidity: MAX_CURRENCY_AMOUNT_EXCEEDED
);
// Update currency reserve size for Token id before transfer
currencyReserves[tokenId] = currencyReserve + currencyAmount;
// Update totalCurrency
totalCurrency += currencyAmount;
// Proportion of the liquidity pool to give to current liquidity provider
// If rounding error occured, round down to favor previous liquidity providers
// See https://github.com/0xsequence/niftyswap/issues/19
liquiditiesToMint[i] = (currencyAmount - (rounded ? 1 : 0)) * totalLiquidity / currencyReserve;
currencyAmounts[i] = currencyAmount;
// Mint liquidity ownership tokens and increase liquidity supply accordingly
totalSupplies[tokenId] = totalLiquidity + liquiditiesToMint[i];
} else {
uint256 maxCurrency = _maxCurrency[i];
// Otherwise rounding error could end up being significant on second deposit
require(maxCurrency >= 1000000000, "NE#14"); // NiftyswapExchange#_addLiquidity: INVALID_CURRENCY_AMOUNT
// Update currency reserve size for Token id before transfer
currencyReserves[tokenId] = maxCurrency;
// Update totalCurrency
totalCurrency += maxCurrency;
// Initial liquidity is amount deposited (Incorrect pricing will be arbitraged)
// uint256 initialLiquidity = _maxCurrency;
totalSupplies[tokenId] = maxCurrency;
// Liquidity to mints
liquiditiesToMint[i] = maxCurrency;
currencyAmounts[i] = maxCurrency;
}
}
// Mint liquidity pool tokens
_batchMint(_provider, _tokenIds, liquiditiesToMint, "");
// Transfer all currency to this contract
currency.safeTransferFrom(_provider, address(this), currencyID, totalCurrency, abi.encode(DEPOSIT_SIG));
// Emit event
emit LiquidityAdded(_provider, _tokenIds, _tokenAmounts, currencyAmounts);
}
/**
* @dev Convert pool participation into amounts of token and currency.
* @dev Rounding error of the asset with lower resolution is traded for the other asset.
* @param _amountPool Participation to be converted to tokens and currency.
* @param _tokenReserve Amount of tokens on the AMM reserve.
* @param _currencyReserve Amount of currency on the AMM reserve.
* @param _totalLiquidity Total liquidity on the pool.
*
* @return currencyAmount Currency corresponding to pool amount plus rounded tokens.
* @return tokenAmount Token corresponding to pool amount plus rounded currency.
*/
function _toRoundedLiquidity(
uint256 _amountPool,
uint256 _tokenReserve,
uint256 _currencyReserve,
uint256 _totalLiquidity
)
internal
pure
returns (
uint256 currencyAmount,
uint256 tokenAmount,
uint256 soldTokenNumerator,
uint256 boughtCurrencyNumerator
)
{
uint256 currencyNumerator = _amountPool * _currencyReserve;
uint256 tokenNumerator = _amountPool * _tokenReserve;
// Convert all tokenProduct rest to currency
soldTokenNumerator = tokenNumerator % _totalLiquidity;
if (soldTokenNumerator != 0) {
// The trade happens "after" funds are out of the pool
// so we need to remove these funds before computing the rate
uint256 virtualTokenReserve = (_tokenReserve - (tokenNumerator / _totalLiquidity)) * _totalLiquidity;
uint256 virtualCurrencyReserve =
(_currencyReserve - (currencyNumerator / _totalLiquidity)) * _totalLiquidity;
// Skip process if any of the two reserves is left empty
// this step is important to avoid an error withdrawing all left liquidity
if (virtualCurrencyReserve != 0 && virtualTokenReserve != 0) {
boughtCurrencyNumerator = getSellPrice(soldTokenNumerator, virtualTokenReserve, virtualCurrencyReserve);
currencyNumerator += boughtCurrencyNumerator;
}
}
// Calculate amounts
currencyAmount = currencyNumerator / _totalLiquidity;
tokenAmount = tokenNumerator / _totalLiquidity;
}
/**
* @dev Burn liquidity pool tokens to withdraw currency && Tokens at current ratio.
* @dev Sorting _tokenIds is mandatory for efficient way of preventing duplicated IDs (which would lead to errors)
* @param _provider Address that removes liquidity to the reserve
* @param _tokenIds Array of Token IDs where liquidity is removed
* @param _poolTokenAmounts Array of Amount of liquidity pool tokens burned for each Token id in _tokenIds.
* @param _minCurrency Minimum currency withdrawn for each Token id in _tokenIds.
* @param _minTokens Minimum Tokens id withdrawn for each Token id in _tokenIds.
* @param _deadline Timestamp after which this transaction will be reverted
*/
function _removeLiquidity(
address _provider,
uint256[] memory _tokenIds,
uint256[] memory _poolTokenAmounts,
uint256[] memory _minCurrency,
uint256[] memory _minTokens,
uint256 _deadline
) internal nonReentrant {
// Input validation
// solhint-disable-next-line not-rely-on-time
require(_deadline > block.timestamp, "NE#15"); // NiftyswapExchange#_removeLiquidity: DEADLINE_EXCEEDED
// Initialize variables
uint256 nTokens = _tokenIds.length; // Number of Token IDs to deposit
uint256 totalCurrency = 0; // Total amount of currency to transfer
uint256[] memory tokenAmounts = new uint256[](nTokens); // Amount of Tokens to transfer for each id
// Structs contain most information for the event
// notice: tokenAmounts and tokenIds are absent because we already
// either have those arrays constructed or we need to construct them for other reasons
LiquidityRemovedEventObj[] memory eventObjs = new LiquidityRemovedEventObj[](nTokens);
// Get token reserves
uint256[] memory tokenReserves = _getTokenReserves(_tokenIds);
// Assumes NIFTY liquidity tokens are already received by contract, but not
// the currency nor the Tokens Ids
// Remove liquidity for each Token ID in _tokenIds
for (uint256 i = 0; i < nTokens; i++) {
// Store current id and amount from argument arrays
uint256 id = _tokenIds[i];
uint256 amountPool = _poolTokenAmounts[i];
// Load total liquidity pool token supply for Token _id
uint256 totalLiquidity = totalSupplies[id];
require(totalLiquidity > 0, "NE#16"); // NiftyswapExchange#_removeLiquidity: NULL_TOTAL_LIQUIDITY
// Load currency and Token reserve's supply of Token id
uint256 currencyReserve = currencyReserves[id];
// Calculate amount to withdraw for currency and Token _id
uint256 currencyAmount;
uint256 tokenAmount;
{
uint256 tokenReserve = tokenReserves[i];
uint256 soldTokenNumerator;
uint256 boughtCurrencyNumerator;
(currencyAmount, tokenAmount, soldTokenNumerator, boughtCurrencyNumerator) =
_toRoundedLiquidity(amountPool, tokenReserve, currencyReserve, totalLiquidity);
// Add trade info to event
eventObjs[i].soldTokenNumerator = soldTokenNumerator;
eventObjs[i].boughtCurrencyNumerator = boughtCurrencyNumerator;
eventObjs[i].totalSupply = totalLiquidity;
}
// Verify if amounts to withdraw respect minimums specified
require(
currencyAmount >= _minCurrency[i],
"NE#17" // NiftyswapExchange#_removeLiquidity: INSUFFICIENT_CURRENCY_AMOUNT
);
require(tokenAmount >= _minTokens[i], "NE#18"); // NiftyswapExchange#_removeLiquidity: INSUFFICIENT_TOKENS
// Update total liquidity pool token supply of Token _id
totalSupplies[id] = totalLiquidity - amountPool;
// Update currency reserve size for Token id
currencyReserves[id] = currencyReserve - currencyAmount;
// Update totalCurrency and tokenAmounts
totalCurrency += currencyAmount;
tokenAmounts[i] = tokenAmount;
eventObjs[i].currencyAmount = currencyAmount;
}
// Burn liquidity pool tokens for offchain supplies
_batchBurn(address(this), _tokenIds, _poolTokenAmounts);
// Transfer total currency and all Tokens ids
currency.safeTransferFrom(address(this), _provider, currencyID, totalCurrency, "");
token.safeBatchTransferFrom(address(this), _provider, _tokenIds, tokenAmounts, "");
// Emit event
emit LiquidityRemoved(_provider, _tokenIds, tokenAmounts, eventObjs);
}
//
// Receiver Method Handlers
//
// Method signatures for onReceive control logic
// bytes4(keccak256(
// "_currencyToToken(uint256[],uint256[],uint256,uint256,address)"
// ));
bytes4 internal constant BUYTOKENS_SIG = 0xb2d81047;
// bytes4(keccak256(
// "_tokenToCurrency(uint256[],uint256[],uint256,uint256,address)"
// ));
bytes4 internal constant SELLTOKENS_SIG = 0xdb08ec97;
// bytes4(keccak256(
// "_addLiquidity(address,uint256[],uint256[],uint256[],uint256)"
// ));
bytes4 internal constant ADDLIQUIDITY_SIG = 0x82da2b73;
// bytes4(keccak256(
// "_removeLiquidity(address,uint256[],uint256[],uint256[],uint256[],uint256)"
// ));
bytes4 internal constant REMOVELIQUIDITY_SIG = 0x5c0bf259;
// bytes4(keccak256(
// "DepositTokens()"
// ));
bytes4 internal constant DEPOSIT_SIG = 0xc8c323f9;
/**
* @notice Handle which method is being called on transfer
* @dev `_data` must be encoded as follow: abi.encode(bytes4, MethodObj)
* where bytes4 argument is the MethodObj object signature passed as defined
* in the `Signatures for onReceive control logic` section above
* @param _from The address which previously owned the Token
* @param _ids An array containing ids of each Token being transferred
* @param _amounts An array containing amounts of each Token being transferred
* @param _data Method signature and corresponding encoded arguments for method to call on *this* contract
* @return bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")
*/
function onERC1155BatchReceived(
address, // _operator,
address _from,
uint256[] memory _ids,
uint256[] memory _amounts,
bytes memory _data
) public override returns (bytes4) {
// This function assumes that the ERC-1155 token contract can
// only call `onERC1155BatchReceived()` via a valid token transfer.
// Users must be responsible and only use this Niftyswap exchange
// contract with ERC-1155 compliant token contracts.
// Obtain method to call via object signature
bytes4 functionSignature = abi.decode(_data, (bytes4));
//
// Buying Tokens
//
if (functionSignature == BUYTOKENS_SIG) {
// Tokens received need to be currency contract
require(
msg.sender == address(currency),
"NE#19" // NiftyswapExchange#onERC1155BatchReceived: INVALID_CURRENCY_TRANSFERRED
);
require(_ids.length == 1, "NE#20"); // NiftyswapExchange#onERC1155BatchReceived: INVALID_CURRENCY_IDS_AMOUNT
require(_ids[0] == currencyID, "NE#21"); // NiftyswapExchange#onERC1155BatchReceived: INVALID_CURRENCY_ID
// Decode BuyTokensObj from _data to call _currencyToToken()
BuyTokensObj memory obj;
(, obj) = abi.decode(_data, (bytes4, BuyTokensObj));
address recipient = obj.recipient == address(0x0) ? _from : obj.recipient;
// Execute trade and retrieve amount of currency spent
uint256[] memory currencySold =
_currencyToToken(obj.tokensBoughtIDs, obj.tokensBoughtAmounts, _amounts[0], obj.deadline, recipient);
emit TokensPurchase(_from, recipient, obj.tokensBoughtIDs, obj.tokensBoughtAmounts, currencySold);
//
// Selling Tokens
//
} else if (functionSignature == SELLTOKENS_SIG) {
// Tokens received need to be Token contract
require(
msg.sender == address(token),
"NE#22" // NiftyswapExchange#onERC1155BatchReceived: INVALID_TOKENS_TRANSFERRED
);
// Decode SellTokensObj from _data to call _tokenToCurrency()
SellTokensObj memory obj;
(, obj) = abi.decode(_data, (bytes4, SellTokensObj));
address recipient = obj.recipient == address(0x0) ? _from : obj.recipient;
// Execute trade and retrieve amount of currency received
uint256[] memory currencyBought = _tokenToCurrency(_ids, _amounts, obj.minCurrency, obj.deadline, recipient);
emit CurrencyPurchase(_from, recipient, _ids, _amounts, currencyBought);
//
// Adding Liquidity Tokens
//
} else if (functionSignature == ADDLIQUIDITY_SIG) {
// Only allow to receive ERC-1155 tokens from `token` contract
require(msg.sender == address(token), "NE#23"); // NiftyswapExchange#onERC1155BatchReceived: INVALID_TOKEN_TRANSFERRED
// Decode AddLiquidityObj from _data to call _addLiquidity()
AddLiquidityObj memory obj;
(, obj) = abi.decode(_data, (bytes4, AddLiquidityObj));
_addLiquidity(_from, _ids, _amounts, obj.maxCurrency, obj.deadline);
//
// Removing Liquidity Tokens
//
} else if (functionSignature == REMOVELIQUIDITY_SIG) {
// Tokens received need to be NIFTY-1155 tokens
require(
msg.sender == address(this),
"NE#24" // NiftyswapExchange#onERC1155BatchReceived: INVALID_NIFTY_TOKENS_TRANSFERRED
);
// Decode RemoveLiquidityObj from _data to call _removeLiquidity()
RemoveLiquidityObj memory obj;
(, obj) = abi.decode(_data, (bytes4, RemoveLiquidityObj));
_removeLiquidity(_from, _ids, _amounts, obj.minCurrency, obj.minTokens, obj.deadline);
//
// Deposits & Invalid Calls
//
} else if (functionSignature == DEPOSIT_SIG) {
// Do nothing for when contract is self depositing
// This could be use to deposit currency "by accident", which would be locked
require(
msg.sender == address(currency),
"NE#25" // NiftyswapExchange#onERC1155BatchReceived: INVALID_TOKENS_DEPOSITED
);
require(_ids[0] == currencyID, "NE#26"); // NiftyswapExchange#onERC1155BatchReceived: INVALID_CURRENCY_ID
} else {
revert("NiftyswapExchange#onERC1155BatchReceived: INVALID_METHOD");
}
return ERC1155_BATCH_RECEIVED_VALUE;
}
/**
* @dev Will pass to onERC115Batch5Received
*/
function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _amount, bytes memory _data)
public
override
returns (bytes4)
{
uint256[] memory ids = new uint256[](1);
uint256[] memory amounts = new uint256[](1);
ids[0] = _id;
amounts[0] = _amount;
require(
ERC1155_BATCH_RECEIVED_VALUE == onERC1155BatchReceived(_operator, _from, ids, amounts, _data),
"NE#27" // NiftyswapExchange#onERC1155Received: INVALID_ONRECEIVED_MESSAGE
);
return ERC1155_RECEIVED_VALUE;
}
/**
* @notice Prevents receiving Ether or calls to unsuported methods
*/
fallback() external {
revert("NE#28"); // NiftyswapExchange:UNSUPPORTED_METHOD
}
//
// Getter Functions
//
/**
* @notice Get amount of currency in reserve for each Token _id in _ids
* @param _ids Array of ID sto query currency reserve of
* @return amount of currency in reserve for each Token _id
*/
function getCurrencyReserves(uint256[] calldata _ids) external view override returns (uint256[] memory) {
uint256 nIds = _ids.length;
uint256[] memory currencyReservesReturn = new uint256[](nIds);
for (uint256 i = 0; i < nIds; i++) {
currencyReservesReturn[i] = currencyReserves[_ids[i]];
}
return currencyReservesReturn;
}
/**
* @notice Return price for `currency => Token _id` trades with an exact token amount.
* @param _ids Array of ID of tokens bought.
* @param _tokensBought Amount of Tokens bought.
* @return Amount of currency needed to buy Tokens in _ids for amounts in _tokensBought
*/
function getPrice_currencyToToken(uint256[] calldata _ids, uint256[] calldata _tokensBought)
external
view
override
returns (uint256[] memory)
{
uint256 nIds = _ids.length;
uint256[] memory prices = new uint256[](nIds);
for (uint256 i = 0; i < nIds; i++) {
// Load Token id reserve
uint256 tokenReserve = token.balanceOf(address(this), _ids[i]);
prices[i] = getBuyPrice(_tokensBought[i], currencyReserves[_ids[i]], tokenReserve);
}
// Return prices
return prices;
}
/**
* @notice Return price for `Token _id => currency` trades with an exact token amount.
* @param _ids Array of IDs token sold.
* @param _tokensSold Array of amount of each Token sold.
* @return Amount of currency that can be bought for Tokens in _ids for amounts in _tokensSold
*/
function getPrice_tokenToCurrency(uint256[] calldata _ids, uint256[] calldata _tokensSold)
external
view
override
returns (uint256[] memory)
{
uint256 nIds = _ids.length;
uint256[] memory prices = new uint256[](nIds);
for (uint256 i = 0; i < nIds; i++) {
// Load Token id reserve
uint256 tokenReserve = token.balanceOf(address(this), _ids[i]);
prices[i] = getSellPrice(_tokensSold[i], tokenReserve, currencyReserves[_ids[i]]);
}
// Return price
return prices;
}
/**
* @return Address of Token that is sold on this exchange.
*/
function getTokenAddress() external view override returns (address) {
return address(token);
}
/**
* @return Address of the currency contract that is used as currency and its corresponding id
*/
function getCurrencyInfo() external view override returns (address, uint256) {
return (address(currency), currencyID);
}
/**
* @notice Get total supply of liquidity tokens
* @param _ids ID of the Tokens
* @return The total supply of each liquidity token id provided in _ids
*/
function getTotalSupply(uint256[] calldata _ids) external view override returns (uint256[] memory) {
// Number of ids
uint256 nIds = _ids.length;
// Variables
uint256[] memory batchTotalSupplies = new uint256[](nIds);
// Iterate over each owner and token ID
for (uint256 i = 0; i < nIds; i++) {
batchTotalSupplies[i] = totalSupplies[_ids[i]];
}
return batchTotalSupplies;
}
/**
* @return Address of factory that created this exchange.
*/
function getFactoryAddress() external view override returns (address) {
return factory;
}
//
// Utility Functions
//
/**
* @notice Divides two numbers and add 1 if there is a rounding error
* @param a Numerator
* @param b Denominator
*/
function divRound(uint256 a, uint256 b) internal pure returns (uint256, bool) {
return a % b == 0 ? (a / b, false) : ((a / b) + 1, true);
}
/**
* @notice Return Token reserves for given Token ids
* @dev Assumes that ids are sorted from lowest to highest with no duplicates.
* This assumption allows for checking the token reserves only once, otherwise
* token reserves need to be re-checked individually or would have to do more expensive
* duplication checks.
* @param _tokenIds Array of IDs to query their Reserve balance.
* @return Array of Token ids' reserves
*/
function _getTokenReserves(uint256[] memory _tokenIds) internal view returns (uint256[] memory) {
uint256 nTokens = _tokenIds.length;
// Regular balance query if only 1 token, otherwise batch query
if (nTokens == 1) {
uint256[] memory tokenReserves = new uint256[](1);
tokenReserves[0] = token.balanceOf(address(this), _tokenIds[0]);
return tokenReserves;
} else {
// Lazy check preventing duplicates & build address array for query
address[] memory thisAddressArray = new address[](nTokens);
thisAddressArray[0] = address(this);
for (uint256 i = 1; i < nTokens; i++) {
require(
_tokenIds[i - 1] < _tokenIds[i],
"NE#29" // NiftyswapExchange#_getTokenReserves: UNSORTED_OR_DUPLICATE_TOKEN_IDS
);
thisAddressArray[i] = address(this);
}
return token.balanceOfBatch(thisAddressArray, _tokenIds);
}
}
/**
* @notice Indicates whether a contract implements the `ERC1155TokenReceiver` functions and so can accept ERC1155 token types.
* @param interfaceID The ERC-165 interface ID that is queried for support.s
* @dev This function MUST return true if it implements the ERC1155TokenReceiver interface and ERC-165 interface.
* This function MUST NOT consume more thsan 5,000 gas.
* @return Whether a given interface is supported
*/
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IERC165).interfaceId || interfaceID == type(IERC1155).interfaceId
|| interfaceID == type(IERC1155TokenReceiver).interfaceId;
}
}