-
Notifications
You must be signed in to change notification settings - Fork 575
Expand file tree
/
Copy pathVault.sol
More file actions
1236 lines (1000 loc) · 52.3 KB
/
Vault.sol
File metadata and controls
1236 lines (1000 loc) · 52.3 KB
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
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
import "../libraries/math/SafeMath.sol";
import "../libraries/token/IERC20.sol";
import "../libraries/token/SafeERC20.sol";
import "../libraries/utils/ReentrancyGuard.sol";
import "../tokens/interfaces/IUSDG.sol";
import "./interfaces/IVault.sol";
import "./interfaces/IVaultUtils.sol";
import "./interfaces/IVaultPriceFeed.sol";
contract Vault is ReentrancyGuard, IVault {
using SafeMath for uint256;
using SafeERC20 for IERC20;
struct Position {
uint256 size;
uint256 collateral;
uint256 averagePrice;
uint256 entryFundingRate;
uint256 reserveAmount;
int256 realisedPnl;
uint256 lastIncreasedTime;
}
uint256 public constant BASIS_POINTS_DIVISOR = 10000;
uint256 public constant FUNDING_RATE_PRECISION = 1000000;
uint256 public constant PRICE_PRECISION = 10 ** 30;
uint256 public constant MIN_LEVERAGE = 10000; // 1x
uint256 public constant USDG_DECIMALS = 18;
uint256 public constant MAX_FEE_BASIS_POINTS = 500; // 5%
uint256 public constant MAX_LIQUIDATION_FEE_USD = 100 * PRICE_PRECISION; // 100 USD
uint256 public constant MIN_FUNDING_RATE_INTERVAL = 1 hours;
uint256 public constant MAX_FUNDING_RATE_FACTOR = 10000; // 1%
bool public override isInitialized;
bool public override isSwapEnabled = true;
bool public override isLeverageEnabled = true;
IVaultUtils public vaultUtils;
address public errorController;
address public override router;
address public override priceFeed;
address public override usdg;
address public override gov;
uint256 public override whitelistedTokenCount;
uint256 public override maxLeverage = 50 * 10000; // 50x
uint256 public override liquidationFeeUsd;
uint256 public override taxBasisPoints = 50; // 0.5%
uint256 public override stableTaxBasisPoints = 20; // 0.2%
uint256 public override mintBurnFeeBasisPoints = 30; // 0.3%
uint256 public override swapFeeBasisPoints = 30; // 0.3%
uint256 public override stableSwapFeeBasisPoints = 4; // 0.04%
uint256 public override marginFeeBasisPoints = 10; // 0.1%
uint256 public override minProfitTime;
bool public override hasDynamicFees = false;
uint256 public override fundingInterval = 8 hours;
uint256 public override fundingRateFactor;
uint256 public override stableFundingRateFactor;
uint256 public override totalTokenWeights;
bool public includeAmmPrice = true;
bool public useSwapPricing = false;
bool public override inManagerMode = false;
bool public override inPrivateLiquidationMode = false;
uint256 public override maxGasPrice;
mapping (address => mapping (address => bool)) public override approvedRouters;
mapping (address => bool) public override isLiquidator;
mapping (address => bool) public override isManager;
address[] public override allWhitelistedTokens;
mapping (address => bool) public override whitelistedTokens;
mapping (address => uint256) public override tokenDecimals;
mapping (address => uint256) public override minProfitBasisPoints;
mapping (address => bool) public override stableTokens;
mapping (address => bool) public override shortableTokens;
// tokenBalances is used only to determine _transferIn values
mapping (address => uint256) public override tokenBalances;
// tokenWeights allows customisation of index composition
mapping (address => uint256) public override tokenWeights;
// usdgAmounts tracks the amount of USDG debt for each whitelisted token
mapping (address => uint256) public override usdgAmounts;
// maxUsdgAmounts allows setting a max amount of USDG debt for a token
mapping (address => uint256) public override maxUsdgAmounts;
// poolAmounts tracks the number of received tokens that can be used for leverage
// this is tracked separately from tokenBalances to exclude funds that are deposited as margin collateral
mapping (address => uint256) public override poolAmounts;
// reservedAmounts tracks the number of tokens reserved for open leverage positions
mapping (address => uint256) public override reservedAmounts;
// bufferAmounts allows specification of an amount to exclude from swaps
// this can be used to ensure a certain amount of liquidity is available for leverage positions
mapping (address => uint256) public override bufferAmounts;
// guaranteedUsd tracks the amount of USD that is "guaranteed" by opened leverage positions
// this value is used to calculate the redemption values for selling of USDG
// this is an estimated amount, it is possible for the actual guaranteed value to be lower
// in the case of sudden price decreases, the guaranteed value should be corrected
// after liquidations are carried out
mapping (address => uint256) public override guaranteedUsd;
// cumulativeFundingRates tracks the funding rates based on utilization
mapping (address => uint256) public override cumulativeFundingRates;
// lastFundingTimes tracks the last time funding was updated for a token
mapping (address => uint256) public override lastFundingTimes;
// positions tracks all open positions
mapping (bytes32 => Position) public positions;
// feeReserves tracks the amount of fees per token
mapping (address => uint256) public override feeReserves;
mapping (address => uint256) public override globalShortSizes;
mapping (address => uint256) public override globalShortAveragePrices;
mapping (address => uint256) public override maxGlobalShortSizes;
mapping (uint256 => string) public errors;
event BuyUSDG(address account, address token, uint256 tokenAmount, uint256 usdgAmount, uint256 feeBasisPoints);
event SellUSDG(address account, address token, uint256 usdgAmount, uint256 tokenAmount, uint256 feeBasisPoints);
event Swap(address account, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, uint256 amountOutAfterFees, uint256 feeBasisPoints);
event IncreasePosition(
bytes32 key,
address account,
address collateralToken,
address indexToken,
uint256 collateralDelta,
uint256 sizeDelta,
bool isLong,
uint256 price,
uint256 fee
);
event DecreasePosition(
bytes32 key,
address account,
address collateralToken,
address indexToken,
uint256 collateralDelta,
uint256 sizeDelta,
bool isLong,
uint256 price,
uint256 fee
);
event LiquidatePosition(
bytes32 key,
address account,
address collateralToken,
address indexToken,
bool isLong,
uint256 size,
uint256 collateral,
uint256 reserveAmount,
int256 realisedPnl,
uint256 markPrice
);
event UpdatePosition(
bytes32 key,
uint256 size,
uint256 collateral,
uint256 averagePrice,
uint256 entryFundingRate,
uint256 reserveAmount,
int256 realisedPnl,
uint256 markPrice
);
event ClosePosition(
bytes32 key,
uint256 size,
uint256 collateral,
uint256 averagePrice,
uint256 entryFundingRate,
uint256 reserveAmount,
int256 realisedPnl
);
event UpdateFundingRate(address token, uint256 fundingRate);
event UpdatePnl(bytes32 key, bool hasProfit, uint256 delta);
event CollectSwapFees(address token, uint256 feeUsd, uint256 feeTokens);
event CollectMarginFees(address token, uint256 feeUsd, uint256 feeTokens);
event DirectPoolDeposit(address token, uint256 amount);
event IncreasePoolAmount(address token, uint256 amount);
event DecreasePoolAmount(address token, uint256 amount);
event IncreaseUsdgAmount(address token, uint256 amount);
event DecreaseUsdgAmount(address token, uint256 amount);
event IncreaseReservedAmount(address token, uint256 amount);
event DecreaseReservedAmount(address token, uint256 amount);
event IncreaseGuaranteedUsd(address token, uint256 amount);
event DecreaseGuaranteedUsd(address token, uint256 amount);
// once the parameters are verified to be working correctly,
// gov should be set to a timelock contract or a governance contract
constructor() public {
gov = msg.sender;
}
function initialize(
address _router,
address _usdg,
address _priceFeed,
uint256 _liquidationFeeUsd,
uint256 _fundingRateFactor,
uint256 _stableFundingRateFactor
) external {
_onlyGov();
_validate(!isInitialized, 1);
isInitialized = true;
router = _router;
usdg = _usdg;
priceFeed = _priceFeed;
liquidationFeeUsd = _liquidationFeeUsd;
fundingRateFactor = _fundingRateFactor;
stableFundingRateFactor = _stableFundingRateFactor;
}
function setVaultUtils(IVaultUtils _vaultUtils) external override {
_onlyGov();
vaultUtils = _vaultUtils;
}
function setErrorController(address _errorController) external {
_onlyGov();
errorController = _errorController;
}
function setError(uint256 _errorCode, string calldata _error) external override {
require(msg.sender == errorController, "Vault: invalid errorController");
errors[_errorCode] = _error;
}
function allWhitelistedTokensLength() external override view returns (uint256) {
return allWhitelistedTokens.length;
}
function setInManagerMode(bool _inManagerMode) external override {
_onlyGov();
inManagerMode = _inManagerMode;
}
function setManager(address _manager, bool _isManager) external override {
_onlyGov();
isManager[_manager] = _isManager;
}
function setInPrivateLiquidationMode(bool _inPrivateLiquidationMode) external override {
_onlyGov();
inPrivateLiquidationMode = _inPrivateLiquidationMode;
}
function setLiquidator(address _liquidator, bool _isActive) external override {
_onlyGov();
isLiquidator[_liquidator] = _isActive;
}
function setIsSwapEnabled(bool _isSwapEnabled) external override {
_onlyGov();
isSwapEnabled = _isSwapEnabled;
}
function setIsLeverageEnabled(bool _isLeverageEnabled) external override {
_onlyGov();
isLeverageEnabled = _isLeverageEnabled;
}
function setMaxGasPrice(uint256 _maxGasPrice) external override {
_onlyGov();
maxGasPrice = _maxGasPrice;
}
function setGov(address _gov) external {
_onlyGov();
gov = _gov;
}
function setPriceFeed(address _priceFeed) external override {
_onlyGov();
priceFeed = _priceFeed;
}
function setMaxLeverage(uint256 _maxLeverage) external override {
_onlyGov();
_validate(_maxLeverage > MIN_LEVERAGE, 2);
maxLeverage = _maxLeverage;
}
function setBufferAmount(address _token, uint256 _amount) external override {
_onlyGov();
bufferAmounts[_token] = _amount;
}
function setMaxGlobalShortSize(address _token, uint256 _amount) external override {
_onlyGov();
maxGlobalShortSizes[_token] = _amount;
}
function setFees(
uint256 _taxBasisPoints,
uint256 _stableTaxBasisPoints,
uint256 _mintBurnFeeBasisPoints,
uint256 _swapFeeBasisPoints,
uint256 _stableSwapFeeBasisPoints,
uint256 _marginFeeBasisPoints,
uint256 _liquidationFeeUsd,
uint256 _minProfitTime,
bool _hasDynamicFees
) external override {
_onlyGov();
_validate(_taxBasisPoints <= MAX_FEE_BASIS_POINTS, 3);
_validate(_stableTaxBasisPoints <= MAX_FEE_BASIS_POINTS, 4);
_validate(_mintBurnFeeBasisPoints <= MAX_FEE_BASIS_POINTS, 5);
_validate(_swapFeeBasisPoints <= MAX_FEE_BASIS_POINTS, 6);
_validate(_stableSwapFeeBasisPoints <= MAX_FEE_BASIS_POINTS, 7);
_validate(_marginFeeBasisPoints <= MAX_FEE_BASIS_POINTS, 8);
_validate(_liquidationFeeUsd <= MAX_LIQUIDATION_FEE_USD, 9);
taxBasisPoints = _taxBasisPoints;
stableTaxBasisPoints = _stableTaxBasisPoints;
mintBurnFeeBasisPoints = _mintBurnFeeBasisPoints;
swapFeeBasisPoints = _swapFeeBasisPoints;
stableSwapFeeBasisPoints = _stableSwapFeeBasisPoints;
marginFeeBasisPoints = _marginFeeBasisPoints;
liquidationFeeUsd = _liquidationFeeUsd;
minProfitTime = _minProfitTime;
hasDynamicFees = _hasDynamicFees;
}
function setFundingRate(uint256 _fundingInterval, uint256 _fundingRateFactor, uint256 _stableFundingRateFactor) external override {
_onlyGov();
_validate(_fundingInterval >= MIN_FUNDING_RATE_INTERVAL, 10);
_validate(_fundingRateFactor <= MAX_FUNDING_RATE_FACTOR, 11);
_validate(_stableFundingRateFactor <= MAX_FUNDING_RATE_FACTOR, 12);
fundingInterval = _fundingInterval;
fundingRateFactor = _fundingRateFactor;
stableFundingRateFactor = _stableFundingRateFactor;
}
function setTokenConfig(
address _token,
uint256 _tokenDecimals,
uint256 _tokenWeight,
uint256 _minProfitBps,
uint256 _maxUsdgAmount,
bool _isStable,
bool _isShortable
) external override {
_onlyGov();
// increment token count for the first time
if (!whitelistedTokens[_token]) {
whitelistedTokenCount = whitelistedTokenCount.add(1);
allWhitelistedTokens.push(_token);
}
uint256 _totalTokenWeights = totalTokenWeights;
_totalTokenWeights = _totalTokenWeights.sub(tokenWeights[_token]);
whitelistedTokens[_token] = true;
tokenDecimals[_token] = _tokenDecimals;
tokenWeights[_token] = _tokenWeight;
minProfitBasisPoints[_token] = _minProfitBps;
maxUsdgAmounts[_token] = _maxUsdgAmount;
stableTokens[_token] = _isStable;
shortableTokens[_token] = _isShortable;
totalTokenWeights = _totalTokenWeights.add(_tokenWeight);
// validate price feed
getMaxPrice(_token);
}
function clearTokenConfig(address _token) external {
_onlyGov();
_validate(whitelistedTokens[_token], 13);
totalTokenWeights = totalTokenWeights.sub(tokenWeights[_token]);
delete whitelistedTokens[_token];
delete tokenDecimals[_token];
delete tokenWeights[_token];
delete minProfitBasisPoints[_token];
delete maxUsdgAmounts[_token];
delete stableTokens[_token];
delete shortableTokens[_token];
whitelistedTokenCount = whitelistedTokenCount.sub(1);
}
function withdrawFees(address _token, address _receiver) external override returns (uint256) {
_onlyGov();
uint256 amount = feeReserves[_token];
if(amount == 0) { return 0; }
feeReserves[_token] = 0;
_transferOut(_token, amount, _receiver);
return amount;
}
function addRouter(address _router) external {
approvedRouters[msg.sender][_router] = true;
}
function removeRouter(address _router) external {
approvedRouters[msg.sender][_router] = false;
}
function setUsdgAmount(address _token, uint256 _amount) external override {
_onlyGov();
uint256 usdgAmount = usdgAmounts[_token];
if (_amount > usdgAmount) {
_increaseUsdgAmount(_token, _amount.sub(usdgAmount));
return;
}
_decreaseUsdgAmount(_token, usdgAmount.sub(_amount));
}
// the governance controlling this function should have a timelock
function upgradeVault(address _newVault, address _token, uint256 _amount) external {
_onlyGov();
IERC20(_token).safeTransfer(_newVault, _amount);
}
// deposit into the pool without minting USDG tokens
// useful in allowing the pool to become over-collaterised
function directPoolDeposit(address _token) external override nonReentrant {
_validate(whitelistedTokens[_token], 14);
uint256 tokenAmount = _transferIn(_token);
_validate(tokenAmount > 0, 15);
_increasePoolAmount(_token, tokenAmount);
emit DirectPoolDeposit(_token, tokenAmount);
}
function buyUSDG(address _token, address _receiver) external override nonReentrant returns (uint256) {
_validateManager();
_validate(whitelistedTokens[_token], 16);
useSwapPricing = true;
uint256 tokenAmount = _transferIn(_token);
_validate(tokenAmount > 0, 17);
updateCumulativeFundingRate(_token, _token);
uint256 price = getMinPrice(_token);
uint256 usdgAmount = tokenAmount.mul(price).div(PRICE_PRECISION);
usdgAmount = adjustForDecimals(usdgAmount, _token, usdg);
_validate(usdgAmount > 0, 18);
uint256 feeBasisPoints = vaultUtils.getBuyUsdgFeeBasisPoints(_token, usdgAmount);
uint256 amountAfterFees = _collectSwapFees(_token, tokenAmount, feeBasisPoints);
uint256 mintAmount = amountAfterFees.mul(price).div(PRICE_PRECISION);
mintAmount = adjustForDecimals(mintAmount, _token, usdg);
_increaseUsdgAmount(_token, mintAmount);
_increasePoolAmount(_token, amountAfterFees);
IUSDG(usdg).mint(_receiver, mintAmount);
emit BuyUSDG(_receiver, _token, tokenAmount, mintAmount, feeBasisPoints);
useSwapPricing = false;
return mintAmount;
}
function sellUSDG(address _token, address _receiver) external override nonReentrant returns (uint256) {
_validateManager();
_validate(whitelistedTokens[_token], 19);
useSwapPricing = true;
uint256 usdgAmount = _transferIn(usdg);
_validate(usdgAmount > 0, 20);
updateCumulativeFundingRate(_token, _token);
uint256 redemptionAmount = getRedemptionAmount(_token, usdgAmount);
_validate(redemptionAmount > 0, 21);
_decreaseUsdgAmount(_token, usdgAmount);
_decreasePoolAmount(_token, redemptionAmount);
IUSDG(usdg).burn(address(this), usdgAmount);
// the _transferIn call increased the value of tokenBalances[usdg]
// usually decreases in token balances are synced by calling _transferOut
// however, for usdg, the tokens are burnt, so _updateTokenBalance should
// be manually called to record the decrease in tokens
_updateTokenBalance(usdg);
uint256 feeBasisPoints = vaultUtils.getSellUsdgFeeBasisPoints(_token, usdgAmount);
uint256 amountOut = _collectSwapFees(_token, redemptionAmount, feeBasisPoints);
_validate(amountOut > 0, 22);
_transferOut(_token, amountOut, _receiver);
emit SellUSDG(_receiver, _token, usdgAmount, amountOut, feeBasisPoints);
useSwapPricing = false;
return amountOut;
}
function swap(address _tokenIn, address _tokenOut, address _receiver) external override nonReentrant returns (uint256) {
_validate(isSwapEnabled, 23);
_validate(whitelistedTokens[_tokenIn], 24);
_validate(whitelistedTokens[_tokenOut], 25);
_validate(_tokenIn != _tokenOut, 26);
useSwapPricing = true;
updateCumulativeFundingRate(_tokenIn, _tokenIn);
updateCumulativeFundingRate(_tokenOut, _tokenOut);
uint256 amountIn = _transferIn(_tokenIn);
_validate(amountIn > 0, 27);
uint256 priceIn = getMinPrice(_tokenIn);
uint256 priceOut = getMaxPrice(_tokenOut);
uint256 amountOut = amountIn.mul(priceIn).div(priceOut);
amountOut = adjustForDecimals(amountOut, _tokenIn, _tokenOut);
// adjust usdgAmounts by the same usdgAmount as debt is shifted between the assets
uint256 usdgAmount = amountIn.mul(priceIn).div(PRICE_PRECISION);
usdgAmount = adjustForDecimals(usdgAmount, _tokenIn, usdg);
uint256 feeBasisPoints = vaultUtils.getSwapFeeBasisPoints(_tokenIn, _tokenOut, usdgAmount);
uint256 amountOutAfterFees = _collectSwapFees(_tokenOut, amountOut, feeBasisPoints);
_increaseUsdgAmount(_tokenIn, usdgAmount);
_decreaseUsdgAmount(_tokenOut, usdgAmount);
_increasePoolAmount(_tokenIn, amountIn);
_decreasePoolAmount(_tokenOut, amountOut);
_validateBufferAmount(_tokenOut);
_transferOut(_tokenOut, amountOutAfterFees, _receiver);
emit Swap(_receiver, _tokenIn, _tokenOut, amountIn, amountOut, amountOutAfterFees, feeBasisPoints);
useSwapPricing = false;
return amountOutAfterFees;
}
function increasePosition(address _account, address _collateralToken, address _indexToken, uint256 _sizeDelta, bool _isLong) external override nonReentrant {
_validate(isLeverageEnabled, 28);
_validateGasPrice();
_validateRouter(_account);
_validateTokens(_collateralToken, _indexToken, _isLong);
vaultUtils.validateIncreasePosition(_account, _collateralToken, _indexToken, _sizeDelta, _isLong);
updateCumulativeFundingRate(_collateralToken, _indexToken);
bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);
Position storage position = positions[key];
uint256 price = _isLong ? getMaxPrice(_indexToken) : getMinPrice(_indexToken);
if (position.size == 0) {
position.averagePrice = price;
}
if (position.size > 0 && _sizeDelta > 0) {
position.averagePrice = getNextAveragePrice(_indexToken, position.size, position.averagePrice, _isLong, price, _sizeDelta, position.lastIncreasedTime);
}
uint256 fee = _collectMarginFees(_account, _collateralToken, _indexToken, _isLong, _sizeDelta, position.size, position.entryFundingRate);
uint256 collateralDelta = _transferIn(_collateralToken);
uint256 collateralDeltaUsd = tokenToUsdMin(_collateralToken, collateralDelta);
position.collateral = position.collateral.add(collateralDeltaUsd);
_validate(position.collateral >= fee, 29);
position.collateral = position.collateral.sub(fee);
position.entryFundingRate = getEntryFundingRate(_collateralToken, _indexToken, _isLong);
position.size = position.size.add(_sizeDelta);
position.lastIncreasedTime = block.timestamp;
_validate(position.size > 0, 30);
_validatePosition(position.size, position.collateral);
validateLiquidation(_account, _collateralToken, _indexToken, _isLong, true);
// reserve tokens to pay profits on the position
uint256 reserveDelta = usdToTokenMax(_collateralToken, _sizeDelta);
position.reserveAmount = position.reserveAmount.add(reserveDelta);
_increaseReservedAmount(_collateralToken, reserveDelta);
if (_isLong) {
// guaranteedUsd stores the sum of (position.size - position.collateral) for all positions
// if a fee is charged on the collateral then guaranteedUsd should be increased by that fee amount
// since (position.size - position.collateral) would have increased by `fee`
_increaseGuaranteedUsd(_collateralToken, _sizeDelta.add(fee));
_decreaseGuaranteedUsd(_collateralToken, collateralDeltaUsd);
// treat the deposited collateral as part of the pool
_increasePoolAmount(_collateralToken, collateralDelta);
// fees need to be deducted from the pool since fees are deducted from position.collateral
// and collateral is treated as part of the pool
_decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, fee));
} else {
if (globalShortSizes[_indexToken] == 0) {
globalShortAveragePrices[_indexToken] = price;
} else {
globalShortAveragePrices[_indexToken] = getNextGlobalShortAveragePrice(_indexToken, price, _sizeDelta);
}
_increaseGlobalShortSize(_indexToken, _sizeDelta);
}
emit IncreasePosition(key, _account, _collateralToken, _indexToken, collateralDeltaUsd, _sizeDelta, _isLong, price, fee);
emit UpdatePosition(key, position.size, position.collateral, position.averagePrice, position.entryFundingRate, position.reserveAmount, position.realisedPnl, price);
}
function decreasePosition(address _account, address _collateralToken, address _indexToken, uint256 _collateralDelta, uint256 _sizeDelta, bool _isLong, address _receiver) external override nonReentrant returns (uint256) {
_validateGasPrice();
_validateRouter(_account);
return _decreasePosition(_account, _collateralToken, _indexToken, _collateralDelta, _sizeDelta, _isLong, _receiver);
}
function _decreasePosition(address _account, address _collateralToken, address _indexToken, uint256 _collateralDelta, uint256 _sizeDelta, bool _isLong, address _receiver) private returns (uint256) {
vaultUtils.validateDecreasePosition(_account, _collateralToken, _indexToken, _collateralDelta, _sizeDelta, _isLong, _receiver);
updateCumulativeFundingRate(_collateralToken, _indexToken);
bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);
Position storage position = positions[key];
_validate(position.size > 0, 31);
_validate(position.size >= _sizeDelta, 32);
_validate(position.collateral >= _collateralDelta, 33);
uint256 collateral = position.collateral;
// scrop variables to avoid stack too deep errors
{
uint256 reserveDelta = position.reserveAmount.mul(_sizeDelta).div(position.size);
position.reserveAmount = position.reserveAmount.sub(reserveDelta);
_decreaseReservedAmount(_collateralToken, reserveDelta);
}
(uint256 usdOut, uint256 usdOutAfterFee) = _reduceCollateral(_account, _collateralToken, _indexToken, _collateralDelta, _sizeDelta, _isLong);
if (position.size != _sizeDelta) {
position.entryFundingRate = getEntryFundingRate(_collateralToken, _indexToken, _isLong);
position.size = position.size.sub(_sizeDelta);
_validatePosition(position.size, position.collateral);
validateLiquidation(_account, _collateralToken, _indexToken, _isLong, true);
if (_isLong) {
_increaseGuaranteedUsd(_collateralToken, collateral.sub(position.collateral));
_decreaseGuaranteedUsd(_collateralToken, _sizeDelta);
}
uint256 price = _isLong ? getMinPrice(_indexToken) : getMaxPrice(_indexToken);
emit DecreasePosition(key, _account, _collateralToken, _indexToken, _collateralDelta, _sizeDelta, _isLong, price, usdOut.sub(usdOutAfterFee));
emit UpdatePosition(key, position.size, position.collateral, position.averagePrice, position.entryFundingRate, position.reserveAmount, position.realisedPnl, price);
} else {
if (_isLong) {
_increaseGuaranteedUsd(_collateralToken, collateral);
_decreaseGuaranteedUsd(_collateralToken, _sizeDelta);
}
uint256 price = _isLong ? getMinPrice(_indexToken) : getMaxPrice(_indexToken);
emit DecreasePosition(key, _account, _collateralToken, _indexToken, _collateralDelta, _sizeDelta, _isLong, price, usdOut.sub(usdOutAfterFee));
emit ClosePosition(key, position.size, position.collateral, position.averagePrice, position.entryFundingRate, position.reserveAmount, position.realisedPnl);
delete positions[key];
}
if (!_isLong) {
_decreaseGlobalShortSize(_indexToken, _sizeDelta);
}
if (usdOut > 0) {
if (_isLong) {
_decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, usdOut));
}
uint256 amountOutAfterFees = usdToTokenMin(_collateralToken, usdOutAfterFee);
_transferOut(_collateralToken, amountOutAfterFees, _receiver);
return amountOutAfterFees;
}
return 0;
}
function liquidatePosition(address _account, address _collateralToken, address _indexToken, bool _isLong, address _feeReceiver) external override nonReentrant {
if (inPrivateLiquidationMode) {
_validate(isLiquidator[msg.sender], 34);
}
// set includeAmmPrice to false to prevent manipulated liquidations
includeAmmPrice = false;
updateCumulativeFundingRate(_collateralToken, _indexToken);
bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);
Position memory position = positions[key];
_validate(position.size > 0, 35);
(uint256 liquidationState, uint256 marginFees) = validateLiquidation(_account, _collateralToken, _indexToken, _isLong, false);
_validate(liquidationState != 0, 36);
if (liquidationState == 2) {
// max leverage exceeded but there is collateral remaining after deducting losses so decreasePosition instead
_decreasePosition(_account, _collateralToken, _indexToken, 0, position.size, _isLong, _account);
includeAmmPrice = true;
return;
}
uint256 feeTokens = usdToTokenMin(_collateralToken, marginFees);
feeReserves[_collateralToken] = feeReserves[_collateralToken].add(feeTokens);
emit CollectMarginFees(_collateralToken, marginFees, feeTokens);
_decreaseReservedAmount(_collateralToken, position.reserveAmount);
if (_isLong) {
_decreaseGuaranteedUsd(_collateralToken, position.size.sub(position.collateral));
_decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, marginFees));
}
uint256 markPrice = _isLong ? getMinPrice(_indexToken) : getMaxPrice(_indexToken);
emit LiquidatePosition(key, _account, _collateralToken, _indexToken, _isLong, position.size, position.collateral, position.reserveAmount, position.realisedPnl, markPrice);
if (!_isLong && marginFees < position.collateral) {
uint256 remainingCollateral = position.collateral.sub(marginFees);
_increasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, remainingCollateral));
}
if (!_isLong) {
_decreaseGlobalShortSize(_indexToken, position.size);
}
delete positions[key];
// pay the fee receiver using the pool, we assume that in general the liquidated amount should be sufficient to cover
// the liquidation fees
_decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, liquidationFeeUsd));
_transferOut(_collateralToken, usdToTokenMin(_collateralToken, liquidationFeeUsd), _feeReceiver);
includeAmmPrice = true;
}
// validateLiquidation returns (state, fees)
function validateLiquidation(address _account, address _collateralToken, address _indexToken, bool _isLong, bool _raise) public view returns (uint256, uint256) {
return vaultUtils.validateLiquidation(_account, _collateralToken, _indexToken, _isLong, _raise);
}
function getMaxPrice(address _token) public override view returns (uint256) {
return IVaultPriceFeed(priceFeed).getPrice(_token, true, includeAmmPrice, useSwapPricing);
}
function getMinPrice(address _token) public override view returns (uint256) {
return IVaultPriceFeed(priceFeed).getPrice(_token, false, includeAmmPrice, useSwapPricing);
}
function getRedemptionAmount(address _token, uint256 _usdgAmount) public override view returns (uint256) {
uint256 price = getMaxPrice(_token);
uint256 redemptionAmount = _usdgAmount.mul(PRICE_PRECISION).div(price);
return adjustForDecimals(redemptionAmount, usdg, _token);
}
function getRedemptionCollateral(address _token) public view returns (uint256) {
if (stableTokens[_token]) {
return poolAmounts[_token];
}
uint256 collateral = usdToTokenMin(_token, guaranteedUsd[_token]);
return collateral.add(poolAmounts[_token]).sub(reservedAmounts[_token]);
}
function getRedemptionCollateralUsd(address _token) public view returns (uint256) {
return tokenToUsdMin(_token, getRedemptionCollateral(_token));
}
function adjustForDecimals(uint256 _amount, address _tokenDiv, address _tokenMul) public view returns (uint256) {
uint256 decimalsDiv = _tokenDiv == usdg ? USDG_DECIMALS : tokenDecimals[_tokenDiv];
uint256 decimalsMul = _tokenMul == usdg ? USDG_DECIMALS : tokenDecimals[_tokenMul];
return _amount.mul(10 ** decimalsMul).div(10 ** decimalsDiv);
}
function tokenToUsdMin(address _token, uint256 _tokenAmount) public override view returns (uint256) {
if (_tokenAmount == 0) { return 0; }
uint256 price = getMinPrice(_token);
uint256 decimals = tokenDecimals[_token];
return _tokenAmount.mul(price).div(10 ** decimals);
}
function usdToTokenMax(address _token, uint256 _usdAmount) public view returns (uint256) {
if (_usdAmount == 0) { return 0; }
return usdToToken(_token, _usdAmount, getMinPrice(_token));
}
function usdToTokenMin(address _token, uint256 _usdAmount) public view returns (uint256) {
if (_usdAmount == 0) { return 0; }
return usdToToken(_token, _usdAmount, getMaxPrice(_token));
}
function usdToToken(address _token, uint256 _usdAmount, uint256 _price) public view returns (uint256) {
if (_usdAmount == 0) { return 0; }
uint256 decimals = tokenDecimals[_token];
return _usdAmount.mul(10 ** decimals).div(_price);
}
function getPosition(address _account, address _collateralToken, address _indexToken, bool _isLong) public override view returns (uint256, uint256, uint256, uint256, uint256, uint256, bool, uint256) {
bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);
Position memory position = positions[key];
uint256 realisedPnl = position.realisedPnl > 0 ? uint256(position.realisedPnl) : uint256(-position.realisedPnl);
return (
position.size, // 0
position.collateral, // 1
position.averagePrice, // 2
position.entryFundingRate, // 3
position.reserveAmount, // 4
realisedPnl, // 5
position.realisedPnl >= 0, // 6
position.lastIncreasedTime // 7
);
}
function getPositionKey(address _account, address _collateralToken, address _indexToken, bool _isLong) public pure returns (bytes32) {
return keccak256(abi.encodePacked(
_account,
_collateralToken,
_indexToken,
_isLong
));
}
function updateCumulativeFundingRate(address _collateralToken, address _indexToken) public {
bool shouldUpdate = vaultUtils.updateCumulativeFundingRate(_collateralToken, _indexToken);
if (!shouldUpdate) {
return;
}
if (lastFundingTimes[_collateralToken] == 0) {
lastFundingTimes[_collateralToken] = block.timestamp.div(fundingInterval).mul(fundingInterval);
return;
}
if (lastFundingTimes[_collateralToken].add(fundingInterval) > block.timestamp) {
return;
}
uint256 fundingRate = getNextFundingRate(_collateralToken);
cumulativeFundingRates[_collateralToken] = cumulativeFundingRates[_collateralToken].add(fundingRate);
lastFundingTimes[_collateralToken] = block.timestamp.div(fundingInterval).mul(fundingInterval);
emit UpdateFundingRate(_collateralToken, cumulativeFundingRates[_collateralToken]);
}
function getNextFundingRate(address _token) public override view returns (uint256) {
if (lastFundingTimes[_token].add(fundingInterval) > block.timestamp) { return 0; }
uint256 intervals = block.timestamp.sub(lastFundingTimes[_token]).div(fundingInterval);
uint256 poolAmount = poolAmounts[_token];
if (poolAmount == 0) { return 0; }
uint256 _fundingRateFactor = stableTokens[_token] ? stableFundingRateFactor : fundingRateFactor;
return _fundingRateFactor.mul(reservedAmounts[_token]).mul(intervals).div(poolAmount);
}
function getUtilisation(address _token) public view returns (uint256) {
uint256 poolAmount = poolAmounts[_token];
if (poolAmount == 0) { return 0; }
return reservedAmounts[_token].mul(FUNDING_RATE_PRECISION).div(poolAmount);
}
function getPositionLeverage(address _account, address _collateralToken, address _indexToken, bool _isLong) public view returns (uint256) {
bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);
Position memory position = positions[key];
_validate(position.collateral > 0, 37);
return position.size.mul(BASIS_POINTS_DIVISOR).div(position.collateral);
}
// for longs: nextAveragePrice = (nextPrice * nextSize)/ (nextSize + delta)
// for shorts: nextAveragePrice = (nextPrice * nextSize) / (nextSize - delta)
function getNextAveragePrice(address _indexToken, uint256 _size, uint256 _averagePrice, bool _isLong, uint256 _nextPrice, uint256 _sizeDelta, uint256 _lastIncreasedTime) public view returns (uint256) {
(bool hasProfit, uint256 delta) = getDelta(_indexToken, _size, _averagePrice, _isLong, _lastIncreasedTime);
uint256 nextSize = _size.add(_sizeDelta);
uint256 divisor;
if (_isLong) {
divisor = hasProfit ? nextSize.add(delta) : nextSize.sub(delta);
} else {
divisor = hasProfit ? nextSize.sub(delta) : nextSize.add(delta);
}
return _nextPrice.mul(nextSize).div(divisor);
}
// for longs: nextAveragePrice = (nextPrice * nextSize)/ (nextSize + delta)
// for shorts: nextAveragePrice = (nextPrice * nextSize) / (nextSize - delta)
function getNextGlobalShortAveragePrice(address _indexToken, uint256 _nextPrice, uint256 _sizeDelta) public view returns (uint256) {
uint256 size = globalShortSizes[_indexToken];
uint256 averagePrice = globalShortAveragePrices[_indexToken];
uint256 priceDelta = averagePrice > _nextPrice ? averagePrice.sub(_nextPrice) : _nextPrice.sub(averagePrice);
uint256 delta = size.mul(priceDelta).div(averagePrice);
bool hasProfit = averagePrice > _nextPrice;
uint256 nextSize = size.add(_sizeDelta);
uint256 divisor = hasProfit ? nextSize.sub(delta) : nextSize.add(delta);
return _nextPrice.mul(nextSize).div(divisor);
}
function getGlobalShortDelta(address _token) public view returns (bool, uint256) {
uint256 size = globalShortSizes[_token];
if (size == 0) { return (false, 0); }
uint256 nextPrice = getMaxPrice(_token);
uint256 averagePrice = globalShortAveragePrices[_token];
uint256 priceDelta = averagePrice > nextPrice ? averagePrice.sub(nextPrice) : nextPrice.sub(averagePrice);
uint256 delta = size.mul(priceDelta).div(averagePrice);
bool hasProfit = averagePrice > nextPrice;
return (hasProfit, delta);
}
function getPositionDelta(address _account, address _collateralToken, address _indexToken, bool _isLong) public view returns (bool, uint256) {
bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);
Position memory position = positions[key];
return getDelta(_indexToken, position.size, position.averagePrice, _isLong, position.lastIncreasedTime);
}
function getDelta(address _indexToken, uint256 _size, uint256 _averagePrice, bool _isLong, uint256 _lastIncreasedTime) public override view returns (bool, uint256) {
_validate(_averagePrice > 0, 38);
uint256 price = _isLong ? getMinPrice(_indexToken) : getMaxPrice(_indexToken);
uint256 priceDelta = _averagePrice > price ? _averagePrice.sub(price) : price.sub(_averagePrice);
uint256 delta = _size.mul(priceDelta).div(_averagePrice);
bool hasProfit;
if (_isLong) {
hasProfit = price > _averagePrice;
} else {
hasProfit = _averagePrice > price;
}
// if the minProfitTime has passed then there will be no min profit threshold
// the min profit threshold helps to prevent front-running issues
uint256 minBps = block.timestamp > _lastIncreasedTime.add(minProfitTime) ? 0 : minProfitBasisPoints[_indexToken];
if (hasProfit && delta.mul(BASIS_POINTS_DIVISOR) <= _size.mul(minBps)) {
delta = 0;
}
return (hasProfit, delta);
}
function getEntryFundingRate(address _collateralToken, address _indexToken, bool _isLong) public view returns (uint256) {
return vaultUtils.getEntryFundingRate(_collateralToken, _indexToken, _isLong);
}
function getFundingFee(address _account, address _collateralToken, address _indexToken, bool _isLong, uint256 _size, uint256 _entryFundingRate) public view returns (uint256) {
return vaultUtils.getFundingFee(_account, _collateralToken, _indexToken, _isLong, _size, _entryFundingRate);
}
function getPositionFee(address _account, address _collateralToken, address _indexToken, bool _isLong, uint256 _sizeDelta) public view returns (uint256) {
return vaultUtils.getPositionFee(_account, _collateralToken, _indexToken, _isLong, _sizeDelta);
}
// cases to consider
// 1. initialAmount is far from targetAmount, action increases balance slightly => high rebate
// 2. initialAmount is far from targetAmount, action increases balance largely => high rebate
// 3. initialAmount is close to targetAmount, action increases balance slightly => low rebate
// 4. initialAmount is far from targetAmount, action reduces balance slightly => high tax
// 5. initialAmount is far from targetAmount, action reduces balance largely => high tax
// 6. initialAmount is close to targetAmount, action reduces balance largely => low tax
// 7. initialAmount is above targetAmount, nextAmount is below targetAmount and vice versa
// 8. a large swap should have similar fees as the same trade split into multiple smaller swaps
function getFeeBasisPoints(address _token, uint256 _usdgDelta, uint256 _feeBasisPoints, uint256 _taxBasisPoints, bool _increment) public override view returns (uint256) {
return vaultUtils.getFeeBasisPoints(_token, _usdgDelta, _feeBasisPoints, _taxBasisPoints, _increment);
}
function getTargetUsdgAmount(address _token) public override view returns (uint256) {
uint256 supply = IERC20(usdg).totalSupply();
if (supply == 0) { return 0; }
uint256 weight = tokenWeights[_token];
return weight.mul(supply).div(totalTokenWeights);
}
function _reduceCollateral(address _account, address _collateralToken, address _indexToken, uint256 _collateralDelta, uint256 _sizeDelta, bool _isLong) private returns (uint256, uint256) {
bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);
Position storage position = positions[key];
uint256 fee = _collectMarginFees(_account, _collateralToken, _indexToken, _isLong, _sizeDelta, position.size, position.entryFundingRate);
bool hasProfit;
uint256 adjustedDelta;
// scope variables to avoid stack too deep errors