-
Notifications
You must be signed in to change notification settings - Fork 91
/
LibUbiquityPool.sol
1167 lines (1033 loc) · 42.9 KB
/
LibUbiquityPool.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
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: GPL-2.0-or-later
pragma solidity 0.8.19;
import {AggregatorV3Interface} from "@chainlink/interfaces/AggregatorV3Interface.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol";
import {ICurveStableSwapMetaNG} from "../interfaces/ICurveStableSwapMetaNG.sol";
import {ICurveTwocryptoOptimized} from "../interfaces/ICurveTwocryptoOptimized.sol";
import {IDollarAmoMinter} from "../interfaces/IDollarAmoMinter.sol";
import {IERC20Ubiquity} from "../interfaces/IERC20Ubiquity.sol";
import {UBIQUITY_POOL_PRICE_PRECISION} from "./Constants.sol";
import {AppStorage, LibAppStorage} from "./LibAppStorage.sol";
/**
* @notice Ubiquity pool library
* @notice Allows users to:
* - deposit collateral in exchange for Ubiquity Dollars
* - redeem Ubiquity Dollars in exchange for the earlier provided collateral
*/
library LibUbiquityPool {
using SafeERC20 for IERC20;
using SafeMath for uint256;
/// @notice Storage slot used to store data for this library
bytes32 constant UBIQUITY_POOL_STORAGE_POSITION =
bytes32(
uint256(keccak256("ubiquity.contracts.ubiquity.pool.storage")) - 1
) & ~bytes32(uint256(0xff));
/// @notice Struct used as a storage for this library
struct UbiquityPoolStorage {
//========
// Core
//========
// minter address -> is it enabled
mapping(address amoMinter => bool isEnabled) isAmoMinterEnabled;
//======================
// Collateral related
//======================
// available collateral tokens
address[] collateralAddresses;
// collateral address -> collateral index
mapping(address collateralAddress => uint256 collateralIndex) collateralIndex;
// collateral index -> chainlink price feed addresses
address[] collateralPriceFeedAddresses;
// collateral index -> threshold in seconds when chainlink answer should be considered stale
uint256[] collateralPriceFeedStalenessThresholds;
// collateral index -> collateral price
uint256[] collateralPrices;
// how much collateral/governance tokens user should provide/get to mint/redeem Dollar tokens, 1e6 precision
uint256 collateralRatio;
// array collateral symbols
string[] collateralSymbols;
// collateral address -> is it enabled
mapping(address collateralAddress => bool isEnabled) isCollateralEnabled;
// Number of decimals needed to get to E18. collateral index -> missing decimals
uint256[] missingDecimals;
// Total across all collaterals. Accounts for missing_decimals
uint256[] poolCeilings;
//====================
// Redeem related
//====================
// user -> block number (collateral independent)
mapping(address => uint256) lastRedeemedBlock;
// 1010000 = $1.01
uint256 mintPriceThreshold;
// 990000 = $0.99
uint256 redeemPriceThreshold;
// address -> collateral index -> balance
mapping(address user => mapping(uint256 collateralIndex => uint256 amount)) redeemCollateralBalances;
// address -> balance
mapping(address user => uint256 amount) redeemGovernanceBalances;
// number of blocks to wait before being able to collectRedemption()
uint256 redemptionDelayBlocks;
// collateral index -> balance
uint256[] unclaimedPoolCollateral;
// total amount of unclaimed Governance tokens in the pool
uint256 unclaimedPoolGovernance;
//================
// Fees related
//================
// minting fee of a particular collateral index, 1_000_000 = 100%
uint256[] mintingFee;
// redemption fee of a particular collateral index, 1_000_000 = 100%
uint256[] redemptionFee;
//=================
// Pause related
//=================
// whether borrowing collateral by AMO minters is paused for a particular collateral index
bool[] isBorrowPaused;
// whether minting is paused for a particular collateral index
bool[] isMintPaused;
// whether redeeming is paused for a particular collateral index
bool[] isRedeemPaused;
//====================================
// Governance token pricing related
//====================================
// chainlink price feed for ETH/USD pair
address ethUsdPriceFeedAddress;
// threshold in seconds when chainlink's ETH/USD price feed answer should be considered stale
uint256 ethUsdPriceFeedStalenessThreshold;
// Curve's CurveTwocryptoOptimized contract for Governance/ETH pair
address governanceEthPoolAddress;
}
/// @notice Struct used for detailed collateral information
struct CollateralInformation {
uint256 index;
string symbol;
address collateralAddress;
address collateralPriceFeedAddress;
uint256 collateralPriceFeedStalenessThreshold;
bool isEnabled;
uint256 missingDecimals;
uint256 price;
uint256 poolCeiling;
bool isMintPaused;
bool isRedeemPaused;
bool isBorrowPaused;
uint256 mintingFee;
uint256 redemptionFee;
}
/**
* @notice Returns struct used as a storage for this library
* @return uPoolStorage Struct used as a storage
*/
function ubiquityPoolStorage()
internal
pure
returns (UbiquityPoolStorage storage uPoolStorage)
{
bytes32 position = UBIQUITY_POOL_STORAGE_POSITION;
assembly {
uPoolStorage.slot := position
}
}
//===========
// Events
//===========
/// @notice Emitted when new AMO minter is added
event AmoMinterAdded(address amoMinterAddress);
/// @notice Emitted when AMO minter is removed
event AmoMinterRemoved(address amoMinterAddress);
/// @notice Emitted on setting a chainlink's collateral price feed params
event CollateralPriceFeedSet(
uint256 collateralIndex,
address priceFeedAddress,
uint256 stalenessThreshold
);
/// @notice Emitted on setting a collateral price
event CollateralPriceSet(uint256 collateralIndex, uint256 newPrice);
/// @notice Emitted on setting a collateral ratio
event CollateralRatioSet(uint256 newCollateralRatio);
/// @notice Emitted on enabling/disabling a particular collateral token
event CollateralToggled(uint256 collateralIndex, bool newState);
/// @notice Emitted on setting chainlink's price feed for ETH/USD pair
event EthUsdPriceFeedSet(
address newPriceFeedAddress,
uint256 newStalenessThreshold
);
/// @notice Emitted when fees are updated
event FeesSet(
uint256 collateralIndex,
uint256 newMintFee,
uint256 newRedeemFee
);
/// @notice Emitted on setting a pool for Governance/ETH pair
event GovernanceEthPoolSet(address newGovernanceEthPoolAddress);
/// @notice Emitted on toggling pause for mint/redeem/borrow
event MintRedeemBorrowToggled(uint256 collateralIndex, uint8 toggleIndex);
/// @notice Emitted when new pool ceiling (i.e. max amount of collateral) is set
event PoolCeilingSet(uint256 collateralIndex, uint256 newCeiling);
/// @notice Emitted when mint and redeem price thresholds are updated (1_000_000 = $1.00)
event PriceThresholdsSet(
uint256 newMintPriceThreshold,
uint256 newRedeemPriceThreshold
);
/// @notice Emitted when a new redemption delay in blocks is set
event RedemptionDelayBlocksSet(uint256 redemptionDelayBlocks);
//=====================
// Modifiers
//=====================
/**
* @notice Checks whether collateral token is enabled (i.e. mintable and redeemable)
* @param collateralIndex Collateral token index
*/
modifier collateralEnabled(uint256 collateralIndex) {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
require(
poolStorage.isCollateralEnabled[
poolStorage.collateralAddresses[collateralIndex]
],
"Collateral disabled"
);
_;
}
/**
* @notice Checks whether a caller is the AMO minter address
*/
modifier onlyAmoMinter() {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
require(
poolStorage.isAmoMinterEnabled[msg.sender],
"Not an AMO Minter"
);
_;
}
//=====================
// Views
//=====================
/**
* @notice Returns all collateral addresses
* @return All collateral addresses
*/
function allCollaterals() internal view returns (address[] memory) {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
return poolStorage.collateralAddresses;
}
/**
* @notice Check if collateral token with given address already exists
* @param collateralAddress The collateral token address to check
*/
function collateralExists(
address collateralAddress
) internal view returns (bool) {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
address[] memory collateralAddresses = poolStorage.collateralAddresses;
for (uint256 i = 0; i < collateralAddresses.length; i++) {
if (collateralAddresses[i] == collateralAddress) {
return true;
}
}
return false;
}
/**
* @notice Returns collateral information
* @param collateralAddress Address of the collateral token
* @return returnData Collateral info
*/
function collateralInformation(
address collateralAddress
) internal view returns (CollateralInformation memory returnData) {
// load the storage
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
// validation
require(
poolStorage.isCollateralEnabled[collateralAddress],
"Invalid collateral"
);
// get the index
uint256 index = poolStorage.collateralIndex[collateralAddress];
returnData = CollateralInformation(
index,
poolStorage.collateralSymbols[index],
collateralAddress,
poolStorage.collateralPriceFeedAddresses[index],
poolStorage.collateralPriceFeedStalenessThresholds[index],
poolStorage.isCollateralEnabled[collateralAddress],
poolStorage.missingDecimals[index],
poolStorage.collateralPrices[index],
poolStorage.poolCeilings[index],
poolStorage.isMintPaused[index],
poolStorage.isRedeemPaused[index],
poolStorage.isBorrowPaused[index],
poolStorage.mintingFee[index],
poolStorage.redemptionFee[index]
);
}
/**
* @notice Returns current collateral ratio
* @return Collateral ratio
*/
function collateralRatio() internal view returns (uint256) {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
return poolStorage.collateralRatio;
}
/**
* @notice Returns USD value of all collateral tokens held in the pool, in E18
* @return balanceTally USD value of all collateral tokens
*/
function collateralUsdBalance()
internal
view
returns (uint256 balanceTally)
{
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
uint256 collateralTokensCount = poolStorage.collateralAddresses.length;
balanceTally = 0;
for (uint256 i = 0; i < collateralTokensCount; i++) {
balanceTally += freeCollateralBalance(i)
.mul(10 ** poolStorage.missingDecimals[i])
.mul(poolStorage.collateralPrices[i])
.div(UBIQUITY_POOL_PRICE_PRECISION);
}
}
/**
* @notice Returns chainlink price feed information for ETH/USD pair
* @return Price feed address and staleness threshold in seconds
*/
function ethUsdPriceFeedInformation()
internal
view
returns (address, uint256)
{
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
return (
poolStorage.ethUsdPriceFeedAddress,
poolStorage.ethUsdPriceFeedStalenessThreshold
);
}
/**
* @notice Returns free collateral balance (i.e. that can be borrowed by AMO minters)
* @param collateralIndex collateral token index
* @return Amount of free collateral
*/
function freeCollateralBalance(
uint256 collateralIndex
) internal view returns (uint256) {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
return
IERC20(poolStorage.collateralAddresses[collateralIndex])
.balanceOf(address(this))
.sub(poolStorage.unclaimedPoolCollateral[collateralIndex]);
}
/**
* @notice Returns Dollar value in collateral tokens
* @param collateralIndex collateral token index
* @param dollarAmount Amount of Dollars
* @return Value in collateral tokens
*/
function getDollarInCollateral(
uint256 collateralIndex,
uint256 dollarAmount
) internal view returns (uint256) {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
return
dollarAmount
.mul(UBIQUITY_POOL_PRICE_PRECISION)
.div(10 ** poolStorage.missingDecimals[collateralIndex])
.div(poolStorage.collateralPrices[collateralIndex]);
}
/**
* @notice Returns Ubiquity Dollar token USD price (1e6 precision) from Curve Metapool (Ubiquity Dollar, Curve Tri-Pool LP)
* @return dollarPriceUsd USD price of Ubiquity Dollar
*/
function getDollarPriceUsd()
internal
view
returns (uint256 dollarPriceUsd)
{
// load storage shared across all libraries
AppStorage storage store = LibAppStorage.appStorage();
// get Dollar price from Curve Metapool (18 decimals)
uint256 dollarPriceUsdD18 = ICurveStableSwapMetaNG(
store.stableSwapMetaPoolAddress
).price_oracle(0);
// convert to 6 decimals
dollarPriceUsd = dollarPriceUsdD18
.mul(UBIQUITY_POOL_PRICE_PRECISION)
.div(1e18);
}
/**
* @notice Returns Governance token price in USD (6 decimals precision)
* @dev How it works:
* 1. Fetch ETH/USD price from chainlink oracle
* 2. Fetch Governance/ETH price from Curve's oracle
* 3. Calculate Governance token price in USD
* @return governancePriceUsd Governance token price in USD
*/
function getGovernancePriceUsd()
internal
view
returns (uint256 governancePriceUsd)
{
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
// fetch latest ETH/USD price
AggregatorV3Interface ethUsdPriceFeed = AggregatorV3Interface(
poolStorage.ethUsdPriceFeedAddress
);
(, int256 answer, , uint256 updatedAt, ) = ethUsdPriceFeed
.latestRoundData();
uint256 ethUsdPriceFeedDecimals = ethUsdPriceFeed.decimals();
// validate ETH/USD chainlink response
require(answer > 0, "Invalid price");
require(
block.timestamp - updatedAt <
poolStorage.ethUsdPriceFeedStalenessThreshold,
"Stale data"
);
// convert ETH/USD chainlink price to 6 decimals
uint256 ethUsdPrice = uint256(answer)
.mul(UBIQUITY_POOL_PRICE_PRECISION)
.div(10 ** ethUsdPriceFeedDecimals);
// fetch ETH/Governance price (18 decimals)
uint256 ethGovernancePriceD18 = ICurveTwocryptoOptimized(
poolStorage.governanceEthPoolAddress
).price_oracle();
// calculate Governance/ETH price (18 decimals)
uint256 governanceEthPriceD18 = uint256(1e18).mul(1e18).div(
ethGovernancePriceD18
);
// calculate Governance token price in USD (6 decimals)
governancePriceUsd = governanceEthPriceD18.mul(ethUsdPrice).div(1e18);
}
/**
* @notice Returns user's balance available for redemption
* @param userAddress User address
* @param collateralIndex Collateral token index
* @return User's balance available for redemption
*/
function getRedeemCollateralBalance(
address userAddress,
uint256 collateralIndex
) internal view returns (uint256) {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
return
poolStorage.redeemCollateralBalances[userAddress][collateralIndex];
}
/**
* @notice Returns user's Governance tokens balance available for redemption
* @param userAddress User address
* @return User's Governance tokens balance available for redemption
*/
function getRedeemGovernanceBalance(
address userAddress
) internal view returns (uint256) {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
return poolStorage.redeemGovernanceBalances[userAddress];
}
/**
* @notice Returns pool address for Governance/ETH pair
* @return Pool address
*/
function governanceEthPoolAddress() internal view returns (address) {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
return poolStorage.governanceEthPoolAddress;
}
//====================
// Public functions
//====================
/**
* @notice Mints Dollars in exchange for collateral tokens
* @param collateralIndex Collateral token index
* @param dollarAmount Amount of dollars to mint
* @param dollarOutMin Min amount of dollars to mint (slippage protection)
* @param maxCollateralIn Max amount of collateral to send (slippage protection)
* @param maxGovernanceIn Max amount of Governance tokens to send (slippage protection)
* @param isOneToOne Force providing only collateral without Governance tokens
* @return totalDollarMint Amount of Dollars minted
* @return collateralNeeded Amount of collateral sent to the pool
* @return governanceNeeded Amount of Governance tokens burnt from sender
*/
function mintDollar(
uint256 collateralIndex,
uint256 dollarAmount,
uint256 dollarOutMin,
uint256 maxCollateralIn,
uint256 maxGovernanceIn,
bool isOneToOne
)
internal
collateralEnabled(collateralIndex)
returns (
uint256 totalDollarMint,
uint256 collateralNeeded,
uint256 governanceNeeded
)
{
require(
ubiquityPoolStorage().isMintPaused[collateralIndex] == false,
"Minting is paused"
);
// prevent unnecessary mints
require(
getDollarPriceUsd() >= ubiquityPoolStorage().mintPriceThreshold,
"Dollar price too low"
);
// update collateral price
updateChainLinkCollateralPrice(collateralIndex);
// user forces 1-to-1 override or collateral ratio >= 100%
if (
isOneToOne ||
ubiquityPoolStorage().collateralRatio >=
UBIQUITY_POOL_PRICE_PRECISION
) {
// get amount of collateral for minting Dollars
collateralNeeded = getDollarInCollateral(
collateralIndex,
dollarAmount
);
governanceNeeded = 0;
} else if (ubiquityPoolStorage().collateralRatio == 0) {
// collateral ratio is 0%, Dollar tokens can be minted by providing only Governance tokens (i.e. fully algorithmic stablecoin)
collateralNeeded = 0;
governanceNeeded = dollarAmount
.mul(UBIQUITY_POOL_PRICE_PRECISION)
.div(getGovernancePriceUsd());
} else {
// fractional, user has to provide both collateral and Governance tokens
uint256 dollarForCollateral = dollarAmount
.mul(ubiquityPoolStorage().collateralRatio)
.div(UBIQUITY_POOL_PRICE_PRECISION);
uint256 dollarForGovernance = dollarAmount.sub(dollarForCollateral);
collateralNeeded = getDollarInCollateral(
collateralIndex,
dollarForCollateral
);
governanceNeeded = dollarForGovernance
.mul(UBIQUITY_POOL_PRICE_PRECISION)
.div(getGovernancePriceUsd());
}
// subtract the minting fee
totalDollarMint = dollarAmount
.mul(
UBIQUITY_POOL_PRICE_PRECISION.sub(
ubiquityPoolStorage().mintingFee[collateralIndex]
)
)
.div(UBIQUITY_POOL_PRICE_PRECISION);
// check slippages
require((totalDollarMint >= dollarOutMin), "Dollar slippage");
require((collateralNeeded <= maxCollateralIn), "Collateral slippage");
require((governanceNeeded <= maxGovernanceIn), "Governance slippage");
// check the pool ceiling
require(
freeCollateralBalance(collateralIndex).add(collateralNeeded) <=
ubiquityPoolStorage().poolCeilings[collateralIndex],
"Pool ceiling"
);
// burn Governance tokens from sender and send collateral to the pool
IERC20Ubiquity(LibAppStorage.appStorage().governanceTokenAddress)
.burnFrom(msg.sender, governanceNeeded);
IERC20(ubiquityPoolStorage().collateralAddresses[collateralIndex])
.safeTransferFrom(msg.sender, address(this), collateralNeeded);
// mint Dollars
IERC20Ubiquity(LibAppStorage.appStorage().dollarTokenAddress).mint(
msg.sender,
totalDollarMint
);
}
/**
* @notice Burns redeemable Ubiquity Dollars and sends back 1 USD of collateral token for every 1 Ubiquity Dollar burned
* @dev Redeem process is split in two steps:
* @dev 1. `redeemDollar()`
* @dev 2. `collectRedemption()`
* @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block
* @param collateralIndex Collateral token index being withdrawn
* @param dollarAmount Amount of Ubiquity Dollars being burned
* @param governanceOutMin Minimum amount of Governance tokens that'll be withdrawn, used to set acceptable slippage
* @param collateralOutMin Minimum amount of collateral tokens that'll be withdrawn, used to set acceptable slippage
* @return collateralOut Amount of collateral tokens ready for redemption
*/
function redeemDollar(
uint256 collateralIndex,
uint256 dollarAmount,
uint256 governanceOutMin,
uint256 collateralOutMin
)
internal
collateralEnabled(collateralIndex)
returns (uint256 collateralOut, uint256 governanceOut)
{
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
require(
poolStorage.isRedeemPaused[collateralIndex] == false,
"Redeeming is paused"
);
// prevent unnecessary redemptions that could adversely affect the Dollar price
require(
getDollarPriceUsd() <= poolStorage.redeemPriceThreshold,
"Dollar price too high"
);
uint256 dollarAfterFee = dollarAmount
.mul(
UBIQUITY_POOL_PRICE_PRECISION.sub(
poolStorage.redemptionFee[collateralIndex]
)
)
.div(UBIQUITY_POOL_PRICE_PRECISION);
// update collateral price
updateChainLinkCollateralPrice(collateralIndex);
// get current collateral ratio
uint256 currentCollateralRatio = poolStorage.collateralRatio;
// fully collateralized
if (currentCollateralRatio >= UBIQUITY_POOL_PRICE_PRECISION) {
// get collateral output for incoming Dollars
collateralOut = getDollarInCollateral(
collateralIndex,
dollarAfterFee
);
governanceOut = 0;
} else if (currentCollateralRatio == 0) {
// algorithmic, fully covered by Governance tokens
collateralOut = 0;
governanceOut = dollarAfterFee
.mul(UBIQUITY_POOL_PRICE_PRECISION)
.div(getGovernancePriceUsd());
} else {
// fractional, partially covered by collateral and Governance tokens
collateralOut = getDollarInCollateral(
collateralIndex,
dollarAfterFee
).mul(currentCollateralRatio).div(UBIQUITY_POOL_PRICE_PRECISION);
governanceOut = dollarAfterFee
.mul(UBIQUITY_POOL_PRICE_PRECISION.sub(currentCollateralRatio))
.div(getGovernancePriceUsd());
}
// checks
require(
collateralOut <=
(IERC20(poolStorage.collateralAddresses[collateralIndex]))
.balanceOf(address(this))
.sub(poolStorage.unclaimedPoolCollateral[collateralIndex]),
"Insufficient pool collateral"
);
require(collateralOut >= collateralOutMin, "Collateral slippage");
require(governanceOut >= governanceOutMin, "Governance slippage");
// increase collateral redemption balances
poolStorage.redeemCollateralBalances[msg.sender][
collateralIndex
] = poolStorage
.redeemCollateralBalances[msg.sender][collateralIndex].add(
collateralOut
);
poolStorage.unclaimedPoolCollateral[collateralIndex] = poolStorage
.unclaimedPoolCollateral[collateralIndex]
.add(collateralOut);
// increase Governance redemption balances
poolStorage.redeemGovernanceBalances[msg.sender] = poolStorage
.redeemGovernanceBalances[msg.sender]
.add(governanceOut);
poolStorage.unclaimedPoolGovernance = poolStorage
.unclaimedPoolGovernance
.add(governanceOut);
poolStorage.lastRedeemedBlock[msg.sender] = block.number;
// burn Dollars
IERC20Ubiquity(LibAppStorage.appStorage().dollarTokenAddress).burnFrom(
msg.sender,
dollarAmount
);
// mint Governance tokens to this address
IERC20Ubiquity(LibAppStorage.appStorage().governanceTokenAddress).mint(
address(this),
governanceOut
);
}
/**
* @notice Used to collect collateral and Governance tokens after redeeming/burning Ubiquity Dollars
* @dev Redeem process is split in two steps:
* @dev 1. `redeemDollar()`
* @dev 2. `collectRedemption()`
* @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block
* @param collateralIndex Collateral token index being collected
* @return governanceAmount Amount of Governance tokens redeemed
* @return collateralAmount Amount of collateral tokens redeemed
*/
function collectRedemption(
uint256 collateralIndex
)
internal
collateralEnabled(collateralIndex)
returns (uint256 governanceAmount, uint256 collateralAmount)
{
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
require(
poolStorage.isRedeemPaused[collateralIndex] == false,
"Redeeming is paused"
);
require(
(
poolStorage.lastRedeemedBlock[msg.sender].add(
poolStorage.redemptionDelayBlocks
)
) < block.number,
"Too soon to collect redemption"
);
bool sendGovernance = false;
bool sendCollateral = false;
if (poolStorage.redeemGovernanceBalances[msg.sender] > 0) {
governanceAmount = poolStorage.redeemGovernanceBalances[msg.sender];
poolStorage.redeemGovernanceBalances[msg.sender] = 0;
poolStorage.unclaimedPoolGovernance = poolStorage
.unclaimedPoolGovernance
.sub(governanceAmount);
sendGovernance = true;
}
if (
poolStorage.redeemCollateralBalances[msg.sender][collateralIndex] >
0
) {
collateralAmount = poolStorage.redeemCollateralBalances[msg.sender][
collateralIndex
];
poolStorage.redeemCollateralBalances[msg.sender][
collateralIndex
] = 0;
poolStorage.unclaimedPoolCollateral[collateralIndex] = poolStorage
.unclaimedPoolCollateral[collateralIndex]
.sub(collateralAmount);
sendCollateral = true;
}
// send out tokens
if (sendGovernance) {
IERC20(LibAppStorage.appStorage().governanceTokenAddress)
.safeTransfer(msg.sender, governanceAmount);
}
if (sendCollateral) {
IERC20(poolStorage.collateralAddresses[collateralIndex])
.safeTransfer(msg.sender, collateralAmount);
}
}
/**
* @notice Updates collateral token price in USD from ChainLink price feed
* @param collateralIndex Collateral token index
*/
function updateChainLinkCollateralPrice(uint256 collateralIndex) internal {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
AggregatorV3Interface priceFeed = AggregatorV3Interface(
poolStorage.collateralPriceFeedAddresses[collateralIndex]
);
// fetch latest price
(
,
// roundId
int256 answer, // startedAt
,
uint256 updatedAt, // answeredInRound
) = priceFeed.latestRoundData();
// fetch number of decimals in chainlink feed
uint256 priceFeedDecimals = priceFeed.decimals();
// validation
require(answer > 0, "Invalid price");
require(
block.timestamp - updatedAt <
poolStorage.collateralPriceFeedStalenessThresholds[
collateralIndex
],
"Stale data"
);
// convert chainlink price to 6 decimals
uint256 price = uint256(answer).mul(UBIQUITY_POOL_PRICE_PRECISION).div(
10 ** priceFeedDecimals
);
poolStorage.collateralPrices[collateralIndex] = price;
emit CollateralPriceSet(collateralIndex, price);
}
//=========================
// AMO minters functions
//=========================
/**
* @notice Allows AMO minters to borrow collateral to make yield in external
* protocols like Compound, Curve, erc...
* @dev Bypasses the gassy mint->redeem cycle for AMOs to borrow collateral
* @param collateralAmount Amount of collateral to borrow
*/
function amoMinterBorrow(uint256 collateralAmount) internal onlyAmoMinter {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
// checks the collateral index of the minter as an additional safety check
uint256 minterCollateralIndex = IDollarAmoMinter(msg.sender)
.collateralIndex();
// checks to see if borrowing is paused
require(
poolStorage.isBorrowPaused[minterCollateralIndex] == false,
"Borrowing is paused"
);
// ensure collateral is enabled
require(
poolStorage.isCollateralEnabled[
poolStorage.collateralAddresses[minterCollateralIndex]
],
"Collateral disabled"
);
// ensure the pool is solvent (i.e. AMO minter borrows less than users want to redeem)
require(
collateralAmount <= freeCollateralBalance(minterCollateralIndex),
"Not enough free collateral"
);
// transfer
IERC20(poolStorage.collateralAddresses[minterCollateralIndex])
.safeTransfer(msg.sender, collateralAmount);
}
//========================
// Restricted functions
//========================
/**
* @notice Adds a new AMO minter
* @param amoMinterAddress AMO minter address
*/
function addAmoMinter(address amoMinterAddress) internal {
require(amoMinterAddress != address(0), "Zero address detected");
// make sure the AMO Minter has collateralDollarBalance()
uint256 collatValE18 = IDollarAmoMinter(amoMinterAddress)
.collateralDollarBalance();
require(collatValE18 >= 0, "Invalid AMO");
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
poolStorage.isAmoMinterEnabled[amoMinterAddress] = true;
emit AmoMinterAdded(amoMinterAddress);
}
/**
* @notice Adds a new collateral token
* @param collateralAddress Collateral token address
* @param chainLinkPriceFeedAddress Chainlink's price feed address
* @param poolCeiling Max amount of available tokens for collateral
*/
function addCollateralToken(
address collateralAddress,
address chainLinkPriceFeedAddress,
uint256 poolCeiling
) internal {
require(
!collateralExists(collateralAddress),
"Collateral already added"
);
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
uint256 collateralIndex = poolStorage.collateralAddresses.length;
// add collateral address to all collaterals
poolStorage.collateralAddresses.push(collateralAddress);
// for fast collateral address -> collateral idx lookups later
poolStorage.collateralIndex[collateralAddress] = collateralIndex;
// set collateral initially to disabled
poolStorage.isCollateralEnabled[collateralAddress] = false;
// add in the missing decimals
poolStorage.missingDecimals.push(
uint256(18).sub(ERC20(collateralAddress).decimals())
);
// add in the collateral symbols
poolStorage.collateralSymbols.push(ERC20(collateralAddress).symbol());
// initialize unclaimed pool collateral
poolStorage.unclaimedPoolCollateral.push(0);
// initialize paused prices to $1 as a backup
poolStorage.collateralPrices.push(UBIQUITY_POOL_PRICE_PRECISION);
// set fees to 0 by default
poolStorage.mintingFee.push(0);
poolStorage.redemptionFee.push(0);
// handle the pauses
poolStorage.isMintPaused.push(false);
poolStorage.isRedeemPaused.push(false);
poolStorage.isBorrowPaused.push(false);
// set pool ceiling
poolStorage.poolCeilings.push(poolCeiling);
// set price feed address
poolStorage.collateralPriceFeedAddresses.push(
chainLinkPriceFeedAddress
);
// set price feed staleness threshold in seconds
poolStorage.collateralPriceFeedStalenessThresholds.push(1 days);
}
/**
* @notice Removes AMO minter
* @param amoMinterAddress AMO minter address to remove
*/
function removeAmoMinter(address amoMinterAddress) internal {
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
poolStorage.isAmoMinterEnabled[amoMinterAddress] = false;
emit AmoMinterRemoved(amoMinterAddress);
}
/**
* @notice Sets collateral ChainLink price feed params
* @param collateralAddress Collateral token address
* @param chainLinkPriceFeedAddress ChainLink price feed address
* @param stalenessThreshold Threshold in seconds when chainlink answer should be considered stale
*/
function setCollateralChainLinkPriceFeed(
address collateralAddress,
address chainLinkPriceFeedAddress,
uint256 stalenessThreshold
) internal {
require(
collateralExists(collateralAddress),
"Collateral does not exist"
);
UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage();
uint256 collateralIndex = poolStorage.collateralIndex[
collateralAddress
];
// set price feed address
poolStorage.collateralPriceFeedAddresses[
collateralIndex
] = chainLinkPriceFeedAddress;
// set staleness threshold in seconds when chainlink answer should be considered stale
poolStorage.collateralPriceFeedStalenessThresholds[
collateralIndex
] = stalenessThreshold;
emit CollateralPriceFeedSet(
collateralIndex,
chainLinkPriceFeedAddress,
stalenessThreshold
);
}
/**
* @notice Sets collateral ratio
* @dev How much collateral/governance tokens user should provide/get to mint/redeem Dollar tokens, 1e6 precision
*
* @dev Example (1_000_000 = 100%):
* - Mint: user provides 1 collateral token to get 1 Dollar