-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathBorrowerOperationsTest.js
More file actions
7120 lines (5911 loc) · 250 KB
/
BorrowerOperationsTest.js
File metadata and controls
7120 lines (5911 loc) · 250 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
const deploymentHelper = require("../../utils/js/deploymentHelpers.js");
const testHelpers = require("../../utils/js/testHelpers.js");
const { ethers } = require("hardhat");
const { signERC2612Permit } = require('eth-permit');
const timeMachine = require("ganache-time-traveler");
const BorrowerOperationsTester = artifacts.require("./BorrowerOperationsTester.sol");
const NonPayable = artifacts.require("NonPayable.sol");
const TroveManagerTester = artifacts.require("TroveManagerTester");
const ZUSDTokenTester = artifacts.require("./ZUSDTokenTester");
const MassetManagerTester = artifacts.require("MassetManagerTester");
const NueMockToken = artifacts.require("NueMockToken");
const BorrowerOperationsCrossReentrancy = artifacts.require("BorrowerOperationsCrossReentrancy");
const { AllowanceProvider, PermitTransferFrom, SignatureTransfer } = require("@uniswap/permit2-sdk");
const th = testHelpers.TestHelper;
const dec = th.dec;
const toBN = th.toBN;
const mv = testHelpers.MoneyValues;
const timeValues = testHelpers.TimeValues;
const ZERO_ADDRESS = th.ZERO_ADDRESS;
const assertRevert = th.assertRevert;
/* NOTE: Some of the borrowing tests do not test for specific ZUSD fee values. They only test that the
* fees are non-zero when they should occur, and that they decay over time.
*
* Specific ZUSD fee values will depend on the final fee schedule used, and the final choice for
* the parameter MINUTE_DECAY_FACTOR in the TroveManager, which is still TBD based on economic
* modelling.
*
*/
contract("BorrowerOperations", async accounts => {
const [
owner,
alice,
bob,
carol,
dennis,
whale,
A,
B,
C,
D,
E,
F,
G,
H,
// defaulter_1, defaulter_2,
frontEnd_1,
frontEnd_2,
frontEnd_3,
feeSharingCollector
] = accounts;
const multisig = accounts[999];
// const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3]
let priceFeed;
let zusdToken;
let sortedTroves;
let troveManager;
let activePool;
let stabilityPool;
let defaultPool;
let borrowerOperations;
let zeroStaking;
let zeroToken;
let massetManager;
let nueMockToken;
let permit2;
let contracts;
const getOpenTroveZUSDAmount = async totalDebt => th.getOpenTroveZUSDAmount(contracts, totalDebt);
const getNetBorrowingAmount = async debtWithFee =>
th.getNetBorrowingAmount(contracts, debtWithFee);
const getActualDebtFromComposite = async compositeDebt =>
th.getActualDebtFromComposite(compositeDebt, contracts);
const openTrove = async params => th.openTrove(contracts, params);
const openNueTrove = async params => th.openNueTrove(contracts, params);
const getTroveEntireColl = async trove => th.getTroveEntireColl(contracts, trove);
const getTroveEntireDebt = async trove => th.getTroveEntireDebt(contracts, trove);
const getTroveStake = async trove => th.getTroveStake(contracts, trove);
let ZUSD_GAS_COMPENSATION;
let MIN_NET_DEBT;
let BORROWING_FEE_FLOOR;
let owner_signer,
alice_signer,
bob_signer,
carol_signer,
dennis_signer,
whale_signer,
A_signer,
B_signer,
C_signer,
D_signer,
E_signer,
F_signer,
G_signer,
H_signer,
// defaulter_1_signer, defaulter_2_signer,
frontEnd_1_signer,
frontEnd_2_signer,
frontEnd_3_signer,
feeSharingCollector_signer;
before(async () => {
[
owner_signer,
alice_signer,
bob_signer,
carol_signer,
dennis_signer,
whale_signer,
A_signer,
B_signer,
C_signer,
D_signer,
E_signer,
F_signer,
G_signer,
H_signer,
// defaulter_1_signer, defaulter_2_signer,
frontEnd_1_signer,
frontEnd_2_signer,
frontEnd_3_signer,
feeSharingCollector_signer
] = await ethers.getSigners();
//console.log('accountsList[1].privateKey).replace...:', accountsList[1].privateKey.replace(/^0x/, ''));
});
const testCorpus = ({ withProxy = false }) => {
before(async () => {
contracts = await deploymentHelper.deployLiquityCore();
permit2 = contracts.permit2;
contracts.borrowerOperations = await BorrowerOperationsTester.new(permit2.address);
contracts.massetManager = await MassetManagerTester.new();
contracts.troveManager = await TroveManagerTester.new(permit2.address);
contracts = await deploymentHelper.deployZUSDTokenTester(contracts);
const ZEROContracts = await deploymentHelper.deployZEROTesterContractsHardhat(multisig);
await ZEROContracts.zeroToken.unprotectedMint(multisig, toBN(dec(20, 24)));
await deploymentHelper.connectZEROContracts(ZEROContracts);
await deploymentHelper.connectCoreContracts(contracts, ZEROContracts);
await deploymentHelper.connectZEROContractsToCore(ZEROContracts, contracts);
if (withProxy) {
const users = [alice, bob, carol, dennis, whale, A, B, C, D, E];
await deploymentHelper.deployProxyScripts(contracts, ZEROContracts, owner, users);
}
priceFeed = contracts.priceFeedTestnet;
zusdToken = contracts.zusdToken;
sortedTroves = contracts.sortedTroves;
troveManager = contracts.troveManager;
activePool = contracts.activePool;
stabilityPool = contracts.stabilityPool;
defaultPool = contracts.defaultPool;
borrowerOperations = contracts.borrowerOperations;
massetManager = contracts.massetManager;
//hintHelpers = contracts.hintHelpers;
zeroStaking = ZEROContracts.zeroStaking;
zeroToken = ZEROContracts.zeroToken;
//communityIssuance = ZEROContracts.communityIssuance;
ZUSD_GAS_COMPENSATION = await borrowerOperations.ZUSD_GAS_COMPENSATION();
MIN_NET_DEBT = await borrowerOperations.MIN_NET_DEBT();
BORROWING_FEE_FLOOR = await borrowerOperations.BORROWING_FEE_FLOOR();
const nueMockTokenAddress = await massetManager.nueMockToken();
nueMockToken = await NueMockToken.at(nueMockTokenAddress);
await borrowerOperations.setMassetManagerAddress(massetManager.address);
});
let revertToSnapshot;
beforeEach(async () => {
let snapshot = await timeMachine.takeSnapshot();
revertToSnapshot = () => timeMachine.revertToSnapshot(snapshot["result"]);
});
afterEach(async () => {
await revertToSnapshot();
});
it("addColl(): reverts when top-up would leave trove with ICR < MCR", async () => {
// alice creates a Trove and adds first collateral
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
await openTrove({ ICR: toBN(dec(10, 18)), extraParams: { from: bob } });
// Price drops
await priceFeed.setPrice(dec(100, 18));
const price = await priceFeed.getPrice();
assert.isFalse(await troveManager.checkRecoveryMode(price));
assert.isTrue((await troveManager.getCurrentICR(alice, price)).lt(toBN(dec(110, 16))));
const collTopUp = 1; // 1 wei top up
await assertRevert(
borrowerOperations.addColl(alice, alice, { from: alice, value: collTopUp }),
"BorrowerOps: An operation that would result in ICR < MCR is not permitted"
);
});
it("addColl(): Increases the activePool ETH and raw ether balance by correct amount", async () => {
const { collateral: aliceColl } = await openTrove({
ICR: toBN(dec(2, 18)),
extraParams: { from: alice }
});
const activePool_ETH_Before = await activePool.getETH();
const activePool_RawEther_Before = toBN(await web3.eth.getBalance(activePool.address));
assert.isTrue(activePool_ETH_Before.eq(aliceColl));
assert.isTrue(activePool_RawEther_Before.eq(aliceColl));
await borrowerOperations.addColl(alice, alice, { from: alice, value: dec(1, 16) });
const activePool_ETH_After = await activePool.getETH();
const activePool_RawEther_After = toBN(await web3.eth.getBalance(activePool.address));
assert.isTrue(activePool_ETH_After.eq(aliceColl.add(toBN(dec(1, 16)))));
assert.isTrue(activePool_RawEther_After.eq(aliceColl.add(toBN(dec(1, 16)))));
});
it("addColl(), active Trove: adds the correct collateral amount to the Trove", async () => {
// alice creates a Trove and adds first collateral
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
const alice_Trove_Before = await troveManager.Troves(alice);
const coll_before = alice_Trove_Before[1];
const status_Before = alice_Trove_Before[3];
// check status before
assert.equal(status_Before, 1);
// Alice adds second collateral
await borrowerOperations.addColl(alice, alice, { from: alice, value: dec(1, 16) });
const alice_Trove_After = await troveManager.Troves(alice);
const coll_After = alice_Trove_After[1];
const status_After = alice_Trove_After[3];
// check coll increases by correct amount,and status remains active
assert.isTrue(coll_After.eq(coll_before.add(toBN(dec(1, 16)))));
assert.equal(status_After, 1);
});
it("addColl(), active Trove: Trove is in sortedList before and after", async () => {
// alice creates a Trove and adds first collateral
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
// check Alice is in list before
const aliceTroveInList_Before = await sortedTroves.contains(alice);
const listIsEmpty_Before = await sortedTroves.isEmpty();
assert.equal(aliceTroveInList_Before, true);
assert.equal(listIsEmpty_Before, false);
await borrowerOperations.addColl(alice, alice, { from: alice, value: dec(1, 16) });
// check Alice is still in list after
const aliceTroveInList_After = await sortedTroves.contains(alice);
const listIsEmpty_After = await sortedTroves.isEmpty();
assert.equal(aliceTroveInList_After, true);
assert.equal(listIsEmpty_After, false);
});
it("addColl(), active Trove: updates the stake and updates the total stakes", async () => {
// Alice creates initial Trove with 1 ether
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
const alice_Trove_Before = await troveManager.Troves(alice);
const alice_Stake_Before = alice_Trove_Before[2];
const totalStakes_Before = await troveManager.totalStakes();
assert.isTrue(totalStakes_Before.eq(alice_Stake_Before));
// Alice tops up Trove collateral with 2 ether
await borrowerOperations.addColl(alice, alice, { from: alice, value: dec(2, "ether") });
// Check stake and total stakes get updated
const alice_Trove_After = await troveManager.Troves(alice);
const alice_Stake_After = alice_Trove_After[2];
const totalStakes_After = await troveManager.totalStakes();
assert.isTrue(alice_Stake_After.eq(alice_Stake_Before.add(toBN(dec(2, "ether")))));
assert.isTrue(totalStakes_After.eq(totalStakes_Before.add(toBN(dec(2, "ether")))));
});
it("addColl(), active Trove: applies pending rewards and updates user's L_ETH, L_ZUSDDebt snapshots", async () => {
// --- SETUP ---
const { collateral: aliceCollBefore, totalDebt: aliceDebtBefore } = await openTrove({
extraZUSDAmount: toBN(dec(15000, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: alice }
});
const { collateral: bobCollBefore, totalDebt: bobDebtBefore } = await openTrove({
extraZUSDAmount: toBN(dec(10000, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: bob }
});
await openTrove({
extraZUSDAmount: toBN(dec(5000, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: carol }
});
// --- TEST ---
// price drops to 1ETH:100ZUSD, reducing Carol's ICR below MCR
await priceFeed.setPrice("1000000000000000000");
// Liquidate Carol's Trove,
const tx = await troveManager.liquidate(carol, { from: owner });
assert.isFalse(await sortedTroves.contains(carol));
const L_ETH = await troveManager.L_ETH();
const L_ZUSDDebt = await troveManager.L_ZUSDDebt();
// check Alice and Bob's reward snapshots are zero before they alter their Troves
const alice_rewardSnapshot_Before = await troveManager.rewardSnapshots(alice);
const alice_ETHrewardSnapshot_Before = alice_rewardSnapshot_Before[0];
const alice_ZUSDDebtRewardSnapshot_Before = alice_rewardSnapshot_Before[1];
const bob_rewardSnapshot_Before = await troveManager.rewardSnapshots(bob);
const bob_ETHrewardSnapshot_Before = bob_rewardSnapshot_Before[0];
const bob_ZUSDDebtRewardSnapshot_Before = bob_rewardSnapshot_Before[1];
assert.equal(alice_ETHrewardSnapshot_Before, 0);
assert.equal(alice_ZUSDDebtRewardSnapshot_Before, 0);
assert.equal(bob_ETHrewardSnapshot_Before, 0);
assert.equal(bob_ZUSDDebtRewardSnapshot_Before, 0);
const alicePendingETHReward = await troveManager.getPendingETHReward(alice);
const bobPendingETHReward = await troveManager.getPendingETHReward(bob);
const alicePendingZUSDDebtReward = await troveManager.getPendingZUSDDebtReward(alice);
const bobPendingZUSDDebtReward = await troveManager.getPendingZUSDDebtReward(bob);
for (reward of [
alicePendingETHReward,
bobPendingETHReward,
alicePendingZUSDDebtReward,
bobPendingZUSDDebtReward
]) {
assert.isTrue(reward.gt(toBN("0")));
}
// Alice and Bob top up their Troves
const aliceTopUp = toBN(dec(5, "ether"));
const bobTopUp = toBN(dec(1, 16));
await borrowerOperations.addColl(alice, alice, { from: alice, value: aliceTopUp });
await borrowerOperations.addColl(bob, bob, { from: bob, value: bobTopUp });
// Check that both alice and Bob have had pending rewards applied in addition to their top-ups.
const aliceNewColl = await getTroveEntireColl(alice);
const aliceNewDebt = await getTroveEntireDebt(alice);
const bobNewColl = await getTroveEntireColl(bob);
const bobNewDebt = await getTroveEntireDebt(bob);
assert.isTrue(aliceNewColl.eq(aliceCollBefore.add(alicePendingETHReward).add(aliceTopUp)));
assert.isTrue(aliceNewDebt.eq(aliceDebtBefore.add(alicePendingZUSDDebtReward)));
assert.isTrue(bobNewColl.eq(bobCollBefore.add(bobPendingETHReward).add(bobTopUp)));
assert.isTrue(bobNewDebt.eq(bobDebtBefore.add(bobPendingZUSDDebtReward)));
/* Check that both Alice and Bob's snapshots of the rewards-per-unit-staked metrics should be updated
to the latest values of L_ETH and L_ZUSDDebt */
const alice_rewardSnapshot_After = await troveManager.rewardSnapshots(alice);
const alice_ETHrewardSnapshot_After = alice_rewardSnapshot_After[0];
const alice_ZUSDDebtRewardSnapshot_After = alice_rewardSnapshot_After[1];
const bob_rewardSnapshot_After = await troveManager.rewardSnapshots(bob);
const bob_ETHrewardSnapshot_After = bob_rewardSnapshot_After[0];
const bob_ZUSDDebtRewardSnapshot_After = bob_rewardSnapshot_After[1];
assert.isAtMost(th.getDifference(alice_ETHrewardSnapshot_After, L_ETH), 100);
assert.isAtMost(th.getDifference(alice_ZUSDDebtRewardSnapshot_After, L_ZUSDDebt), 100);
assert.isAtMost(th.getDifference(bob_ETHrewardSnapshot_After, L_ETH), 100);
assert.isAtMost(th.getDifference(bob_ZUSDDebtRewardSnapshot_After, L_ZUSDDebt), 100);
});
// it("addColl(), active Trove: adds the right corrected stake after liquidations have occured", async () => {
// // TODO - check stake updates for addColl/withdrawColl/adustTrove ---
// // --- SETUP ---
// // A,B,C add 15/5/5 ETH, withdraw 100/100/900 ZUSD
// await borrowerOperations.openTrove(th._100pct, dec(100, 16), alice, alice, { from: alice, value: dec(15, 'ether') })
// await borrowerOperations.openTrove(th._100pct, dec(100, 16), bob, bob, { from: bob, value: dec(4, 'ether') })
// await borrowerOperations.openTrove(th._100pct, dec(900, 18), carol, carol, { from: carol, value: dec(5, 'ether') })
// await borrowerOperations.openTrove(th._100pct, 0, dennis, dennis, { from: dennis, value: dec(1, 16) })
// // --- TEST ---
// // price drops to 1ETH:100ZUSD, reducing Carol's ICR below MCR
// await priceFeed.setPrice('1000000000000000000');
// // close Carol's Trove, liquidating her 5 ether and 900ZUSD.
// await troveManager.liquidate(carol, { from: owner });
// // dennis tops up his trove by 1 ETH
// await borrowerOperations.addColl(dennis, dennis, { from: dennis, value: dec(1, 16) })
// /* Check that Dennis's recorded stake is the right corrected stake, less than his collateral. A corrected
// stake is given by the formula:
// s = totalStakesSnapshot / totalCollateralSnapshot
// where snapshots are the values immediately after the last liquidation. After Carol's liquidation,
// the ETH from her Trove has now become the totalPendingETHReward. So:
// totalStakes = (alice_Stake + bob_Stake + dennis_orig_stake ) = (15 + 4 + 1) = 20 ETH.
// totalCollateral = (alice_Collateral + bob_Collateral + dennis_orig_coll + totalPendingETHReward) = (15 + 4 + 1 + 5) = 25 ETH.
// Therefore, as Dennis adds 1 ether collateral, his corrected stake should be: s = 2 * (20 / 25 ) = 1.6 ETH */
// const dennis_Trove = await troveManager.Troves(dennis)
// const dennis_Stake = dennis_Trove[2]
// console.log(dennis_Stake.toString())
// assert.isAtMost(th.getDifference(dennis_Stake), 100)
// })
it("addColl(), reverts if trove is non-existent or closed", async () => {
// A, B open troves
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: bob } });
// Carol attempts to add collateral to her non-existent trove
try {
const txCarol = await borrowerOperations.addColl(carol, carol, {
from: carol,
value: dec(1, 16)
});
assert.isFalse(txCarol.receipt.status);
} catch (error) {
assert.include(error.message, "revert");
assert.include(error.message, "Trove does not exist or is closed");
}
// Price drops
await priceFeed.setPrice(dec(100, 18));
// Bob gets liquidated
await troveManager.liquidate(bob);
assert.isFalse(await sortedTroves.contains(bob));
// Bob attempts to add collateral to his closed trove
try {
const txBob = await borrowerOperations.addColl(bob, bob, { from: bob, value: dec(1, 16) });
assert.isFalse(txBob.receipt.status);
} catch (error) {
assert.include(error.message, "revert");
assert.include(error.message, "Trove does not exist or is closed");
}
});
it("addColl(): can add collateral in Recovery Mode", async () => {
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
const aliceCollBefore = await getTroveEntireColl(alice);
assert.isFalse(await th.checkRecoveryMode(contracts));
await priceFeed.setPrice("105000000000000000000");
assert.isTrue(await th.checkRecoveryMode(contracts));
const collTopUp = toBN(dec(1, 16));
await borrowerOperations.addColl(alice, alice, { from: alice, value: collTopUp });
// Check Alice's collateral
const aliceCollAfter = (await troveManager.Troves(alice))[1];
assert.isTrue(aliceCollAfter.eq(aliceCollBefore.add(collTopUp)));
});
// --- withdrawColl() ---
it("withdrawColl(): reverts when withdrawal would leave trove with ICR < MCR", async () => {
// alice creates a Trove and adds first collateral
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
await openTrove({ ICR: toBN(dec(10, 18)), extraParams: { from: bob } });
// Price drops
await priceFeed.setPrice(dec(100, 18));
const price = await priceFeed.getPrice();
assert.isFalse(await troveManager.checkRecoveryMode(price));
assert.isTrue((await troveManager.getCurrentICR(alice, price)).lt(toBN(dec(110, 16))));
const collWithdrawal = 1; // 1 wei withdrawal
await assertRevert(
borrowerOperations.withdrawColl(1, alice, alice, { from: alice }),
"BorrowerOps: An operation that would result in ICR < MCR is not permitted"
);
});
// reverts when calling address does not have active trove
it("withdrawColl(): reverts when calling address does not have active trove", async () => {
await openTrove({
extraZUSDAmount: toBN(dec(10000, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: alice }
});
await openTrove({
extraZUSDAmount: toBN(dec(10000, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: bob }
});
// Bob successfully withdraws some coll
const txBob = await borrowerOperations.withdrawColl(dec(100, "finney"), bob, bob, {
from: bob
});
assert.isTrue(txBob.receipt.status);
// Carol with no active trove attempts to withdraw
try {
const txCarol = await borrowerOperations.withdrawColl(dec(1, 16), carol, carol, {
from: carol
});
assert.isFalse(txCarol.receipt.status);
} catch (err) {
assert.include(err.message, "revert");
}
});
it("withdrawColl(): reverts when system is in Recovery Mode", async () => {
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: bob } });
assert.isFalse(await th.checkRecoveryMode(contracts));
// Withdrawal possible when recoveryMode == false
const txAlice = await borrowerOperations.withdrawColl(1000, alice, alice, { from: alice });
assert.isTrue(txAlice.receipt.status);
await priceFeed.setPrice("105000000000000000000");
assert.isTrue(await th.checkRecoveryMode(contracts));
//Check withdrawal impossible when recoveryMode == true
try {
const txBob = await borrowerOperations.withdrawColl(1000, bob, bob, { from: bob });
assert.isFalse(txBob.receipt.status);
} catch (err) {
assert.include(err.message, "revert");
}
});
it("withdrawColl(): reverts when requested ETH withdrawal is > the trove's collateral", async () => {
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: bob } });
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: carol } });
const carolColl = await getTroveEntireColl(carol);
const bobColl = await getTroveEntireColl(bob);
// Carol withdraws exactly all her collateral
await assertRevert(
borrowerOperations.withdrawColl(carolColl, carol, carol, { from: carol }),
"BorrowerOps: An operation that would result in ICR < MCR is not permitted"
);
// Bob attempts to withdraw 1 wei more than his collateral
try {
const txBob = await borrowerOperations.withdrawColl(bobColl.add(toBN(1)), bob, bob, {
from: bob
});
assert.isFalse(txBob.receipt.status);
} catch (err) {
assert.include(err.message, "revert");
}
});
it("withdrawColl(): reverts when withdrawal would bring the user's ICR < MCR", async () => {
await openTrove({ ICR: toBN(dec(10, 18)), extraParams: { from: whale } });
await openTrove({ ICR: toBN(dec(11, 17)), extraParams: { from: bob } }); // 110% ICR
// Bob attempts to withdraws 1 wei, Which would leave him with < 110% ICR.
try {
const txBob = await borrowerOperations.withdrawColl(1, bob, bob, { from: bob });
assert.isFalse(txBob.receipt.status);
} catch (err) {
assert.include(err.message, "revert");
}
});
it("withdrawColl(): reverts if system is in Recovery Mode", async () => {
// --- SETUP ---
// A and B open troves at 150% ICR
await openTrove({ ICR: toBN(dec(15, 17)), extraParams: { from: bob } });
await openTrove({ ICR: toBN(dec(15, 17)), extraParams: { from: alice } });
const TCR = (await th.getTCR(contracts)).toString();
assert.equal(TCR, "1500000000000000000");
// --- TEST ---
// price drops to 1ETH:150ZUSD, reducing TCR below 150%
await priceFeed.setPrice("150000000000000000000");
//Alice tries to withdraw collateral during Recovery Mode
try {
const txData = await borrowerOperations.withdrawColl("1", alice, alice, { from: alice });
assert.isFalse(txData.receipt.status);
} catch (err) {
assert.include(err.message, "revert");
}
});
it("withdrawColl(): doesn’t allow a user to completely withdraw all collateral from their Trove (due to gas compensation)", async () => {
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: bob } });
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
const aliceColl = (await troveManager.getEntireDebtAndColl(alice))[1];
// Check Trove is active
const alice_Trove_Before = await troveManager.Troves(alice);
const status_Before = alice_Trove_Before[3];
assert.equal(status_Before, 1);
assert.isTrue(await sortedTroves.contains(alice));
// Alice attempts to withdraw all collateral
await assertRevert(
borrowerOperations.withdrawColl(aliceColl, alice, alice, { from: alice }),
"BorrowerOps: An operation that would result in ICR < MCR is not permitted"
);
});
it("withdrawColl(): leaves the Trove active when the user withdraws less than all the collateral", async () => {
// Open Trove
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
// Check Trove is active
const alice_Trove_Before = await troveManager.Troves(alice);
const status_Before = alice_Trove_Before[3];
assert.equal(status_Before, 1);
assert.isTrue(await sortedTroves.contains(alice));
// Withdraw some collateral
await borrowerOperations.withdrawColl(dec(100, "finney"), alice, alice, { from: alice });
// Check Trove is still active
const alice_Trove_After = await troveManager.Troves(alice);
const status_After = alice_Trove_After[3];
assert.equal(status_After, 1);
assert.isTrue(await sortedTroves.contains(alice));
});
it("withdrawColl(): reduces the Trove's collateral by the correct amount", async () => {
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
const aliceCollBefore = await getTroveEntireColl(alice);
// Alice withdraws 1 ether
await borrowerOperations.withdrawColl(dec(1, 16), alice, alice, { from: alice });
// Check 1 ether remaining
const alice_Trove_After = await troveManager.Troves(alice);
const aliceCollAfter = await getTroveEntireColl(alice);
assert.isTrue(aliceCollAfter.eq(aliceCollBefore.sub(toBN(dec(1, 16)))));
});
it("withdrawColl(): reduces ActivePool ETH and raw ether by correct amount", async () => {
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
const aliceCollBefore = await getTroveEntireColl(alice);
// check before
const activePool_ETH_before = await activePool.getETH();
const activePool_RawEther_before = toBN(await web3.eth.getBalance(activePool.address));
await borrowerOperations.withdrawColl(dec(1, 16), alice, alice, { from: alice });
// check after
const activePool_ETH_After = await activePool.getETH();
const activePool_RawEther_After = toBN(await web3.eth.getBalance(activePool.address));
assert.isTrue(activePool_ETH_After.eq(activePool_ETH_before.sub(toBN(dec(1, 16)))));
assert.isTrue(activePool_RawEther_After.eq(activePool_RawEther_before.sub(toBN(dec(1, 16)))));
});
it("withdrawColl(): updates the stake and updates the total stakes", async () => {
// Alice creates initial Trove with 2 ether
await openTrove({
ICR: toBN(dec(2, 18)),
extraParams: { from: alice, value: toBN(dec(5, "ether")) }
});
const aliceColl = await getTroveEntireColl(alice);
assert.isTrue(aliceColl.gt(toBN("0")));
const alice_Trove_Before = await troveManager.Troves(alice);
const alice_Stake_Before = alice_Trove_Before[2];
const totalStakes_Before = await troveManager.totalStakes();
assert.isTrue(alice_Stake_Before.eq(aliceColl));
assert.isTrue(totalStakes_Before.eq(aliceColl));
// Alice withdraws 1 ether
await borrowerOperations.withdrawColl(dec(1, 16), alice, alice, { from: alice });
// Check stake and total stakes get updated
const alice_Trove_After = await troveManager.Troves(alice);
const alice_Stake_After = alice_Trove_After[2];
const totalStakes_After = await troveManager.totalStakes();
assert.isTrue(alice_Stake_After.eq(alice_Stake_Before.sub(toBN(dec(1, 16)))));
assert.isTrue(totalStakes_After.eq(totalStakes_Before.sub(toBN(dec(1, 16)))));
});
it("withdrawColl(): sends the correct amount of ETH to the user", async () => {
await openTrove({
ICR: toBN(dec(2, 18)),
extraParams: { from: alice, value: dec(2, "ether") }
});
const alice_ETHBalance_Before = toBN(web3.utils.toBN(await web3.eth.getBalance(alice)));
await borrowerOperations.withdrawColl(dec(1, 16), alice, alice, { from: alice, gasPrice: 0 });
const alice_ETHBalance_After = toBN(web3.utils.toBN(await web3.eth.getBalance(alice)));
const balanceDiff = alice_ETHBalance_After.sub(alice_ETHBalance_Before);
assert.isTrue(balanceDiff.eq(toBN(dec(1, 16))));
});
it("withdrawColl(): applies pending rewards and updates user's L_ETH, L_ZUSDDebt snapshots", async () => {
// --- SETUP ---
// Alice adds 15 ether, Bob adds 5 ether, Carol adds 1 ether
await openTrove({ ICR: toBN(dec(10, 18)), extraParams: { from: whale } });
await openTrove({
ICR: toBN(dec(3, 18)),
extraParams: { from: alice, value: toBN(dec(100, 16)) }
});
await openTrove({
ICR: toBN(dec(3, 18)),
extraParams: { from: bob, value: toBN(dec(100, 16)) }
});
await openTrove({
ICR: toBN(dec(2, 18)),
extraParams: { from: carol, value: toBN(dec(10, 16)) }
});
const aliceCollBefore = await getTroveEntireColl(alice);
const aliceDebtBefore = await getTroveEntireDebt(alice);
const bobCollBefore = await getTroveEntireColl(bob);
const bobDebtBefore = await getTroveEntireDebt(bob);
// --- TEST ---
// price drops to 1ETH:100ZUSD, reducing Carol's ICR below MCR
await priceFeed.setPrice("100000000000000000000");
// close Carol's Trove, liquidating her 1 ether and 180ZUSD.
await troveManager.liquidate(carol, { from: owner });
const L_ETH = await troveManager.L_ETH();
const L_ZUSDDebt = await troveManager.L_ZUSDDebt();
// check Alice and Bob's reward snapshots are zero before they alter their Troves
const alice_rewardSnapshot_Before = await troveManager.rewardSnapshots(alice);
const alice_ETHrewardSnapshot_Before = alice_rewardSnapshot_Before[0];
const alice_ZUSDDebtRewardSnapshot_Before = alice_rewardSnapshot_Before[1];
const bob_rewardSnapshot_Before = await troveManager.rewardSnapshots(bob);
const bob_ETHrewardSnapshot_Before = bob_rewardSnapshot_Before[0];
const bob_ZUSDDebtRewardSnapshot_Before = bob_rewardSnapshot_Before[1];
assert.equal(alice_ETHrewardSnapshot_Before, 0);
assert.equal(alice_ZUSDDebtRewardSnapshot_Before, 0);
assert.equal(bob_ETHrewardSnapshot_Before, 0);
assert.equal(bob_ZUSDDebtRewardSnapshot_Before, 0);
// Check A and B have pending rewards
const pendingCollReward_A = await troveManager.getPendingETHReward(alice);
const pendingDebtReward_A = await troveManager.getPendingZUSDDebtReward(alice);
const pendingCollReward_B = await troveManager.getPendingETHReward(bob);
const pendingDebtReward_B = await troveManager.getPendingZUSDDebtReward(bob);
for (reward of [
pendingCollReward_A,
pendingDebtReward_A,
pendingCollReward_B,
pendingDebtReward_B
]) {
assert.isTrue(reward.gt(toBN("0")));
}
// Alice and Bob withdraw from their Troves
const aliceCollWithdrawal = toBN(dec(5, 16));
const bobCollWithdrawal = toBN(dec(1, 16));
await borrowerOperations.withdrawColl(aliceCollWithdrawal, alice, alice, { from: alice });
await borrowerOperations.withdrawColl(bobCollWithdrawal, bob, bob, { from: bob });
// Check that both alice and Bob have had pending rewards applied in addition to their top-ups.
const aliceCollAfter = await getTroveEntireColl(alice);
const aliceDebtAfter = await getTroveEntireDebt(alice);
const bobCollAfter = await getTroveEntireColl(bob);
const bobDebtAfter = await getTroveEntireDebt(bob);
// Check rewards have been applied to troves
th.assertIsApproximatelyEqual(
aliceCollAfter,
aliceCollBefore.add(pendingCollReward_A).sub(aliceCollWithdrawal),
10000
);
th.assertIsApproximatelyEqual(aliceDebtAfter, aliceDebtBefore.add(pendingDebtReward_A), 10000);
th.assertIsApproximatelyEqual(
bobCollAfter,
bobCollBefore.add(pendingCollReward_B).sub(bobCollWithdrawal),
10000
);
th.assertIsApproximatelyEqual(bobDebtAfter, bobDebtBefore.add(pendingDebtReward_B), 10000);
/* After top up, both Alice and Bob's snapshots of the rewards-per-unit-staked metrics should be updated
to the latest values of L_ETH and L_ZUSDDebt */
const alice_rewardSnapshot_After = await troveManager.rewardSnapshots(alice);
const alice_ETHrewardSnapshot_After = alice_rewardSnapshot_After[0];
const alice_ZUSDDebtRewardSnapshot_After = alice_rewardSnapshot_After[1];
const bob_rewardSnapshot_After = await troveManager.rewardSnapshots(bob);
const bob_ETHrewardSnapshot_After = bob_rewardSnapshot_After[0];
const bob_ZUSDDebtRewardSnapshot_After = bob_rewardSnapshot_After[1];
assert.isAtMost(th.getDifference(alice_ETHrewardSnapshot_After, L_ETH), 100);
assert.isAtMost(th.getDifference(alice_ZUSDDebtRewardSnapshot_After, L_ZUSDDebt), 100);
assert.isAtMost(th.getDifference(bob_ETHrewardSnapshot_After, L_ETH), 100);
assert.isAtMost(th.getDifference(bob_ZUSDDebtRewardSnapshot_After, L_ZUSDDebt), 100);
});
// --- withdrawZUSD() ---
it("withdrawZUSD(): reverts when withdrawal would leave trove with ICR < MCR", async () => {
// alice creates a Trove and adds first collateral
await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } });
await openTrove({ ICR: toBN(dec(10, 18)), extraParams: { from: bob } });
// Price drops
await priceFeed.setPrice(dec(100, 18));
const price = await priceFeed.getPrice();
assert.isFalse(await troveManager.checkRecoveryMode(price));
assert.isTrue((await troveManager.getCurrentICR(alice, price)).lt(toBN(dec(110, 16))));
const ZUSDwithdrawal = 1; // withdraw 1 wei ZUSD
await assertRevert(
borrowerOperations.withdrawZUSD(th._100pct, ZUSDwithdrawal, alice, alice, { from: alice }),
"BorrowerOps: An operation that would result in ICR < MCR is not permitted"
);
});
it("withdrawZUSD(): decays a non-zero base rate", async () => {
await openTrove({ ICR: toBN(dec(10, 18)), extraParams: { from: whale } });
await openTrove({
extraZUSDAmount: toBN(dec(20, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: A }
});
await openTrove({
extraZUSDAmount: toBN(dec(20, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: B }
});
await openTrove({
extraZUSDAmount: toBN(dec(20, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: D }
});
await openTrove({
extraZUSDAmount: toBN(dec(20, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: E }
});
const A_ZUSDBal = await zusdToken.balanceOf(A);
// Artificially set base rate to 5%
await troveManager.setBaseRate(dec(5, 16));
// Check baseRate is now non-zero
const baseRate_1 = await troveManager.baseRate();
assert.isTrue(baseRate_1.gt(toBN("0")));
// 2 hours pass
th.fastForwardTime(7200, web3.currentProvider);
// D withdraws ZUSD
await borrowerOperations.withdrawZUSD(th._100pct, dec(1, 16), A, A, { from: D });
// Check baseRate has decreased
const baseRate_2 = await troveManager.baseRate();
assert.isTrue(baseRate_2.lt(baseRate_1));
// 1 hour passes
th.fastForwardTime(3600, web3.currentProvider);
// E withdraws ZUSD
await borrowerOperations.withdrawZUSD(th._100pct, dec(1, 16), A, A, { from: E });
const baseRate_3 = await troveManager.baseRate();
assert.isTrue(baseRate_3.lt(baseRate_2));
});
it("withdrawZUSD(): reverts if max fee > 100%", async () => {
await openTrove({
extraZUSDAmount: toBN(dec(10, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: A }
});
await openTrove({
extraZUSDAmount: toBN(dec(20, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: B }
});
await openTrove({
extraZUSDAmount: toBN(dec(40, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: C }
});
await openTrove({
extraZUSDAmount: toBN(dec(40, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: D }
});
await assertRevert(
borrowerOperations.withdrawZUSD(dec(2, 18), dec(1, 16), A, A, { from: A }),
"Max fee percentage must be between 0.5% and 100%"
);
await assertRevert(
borrowerOperations.withdrawZUSD("1000000000000000001", dec(1, 16), A, A, { from: A }),
"Max fee percentage must be between 0.5% and 100%"
);
});
it("withdrawZUSD(): reverts if max fee < 0.5% in Normal mode", async () => {
await openTrove({
extraZUSDAmount: toBN(dec(10, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: A }
});
await openTrove({
extraZUSDAmount: toBN(dec(20, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: B }
});
await openTrove({
extraZUSDAmount: toBN(dec(40, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: C }
});
await openTrove({
extraZUSDAmount: toBN(dec(40, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: D }
});
await assertRevert(
borrowerOperations.withdrawZUSD(0, dec(1, 16), A, A, { from: A }),
"Max fee percentage must be between 0.5% and 100%"
);
await assertRevert(
borrowerOperations.withdrawZUSD(1, dec(1, 16), A, A, { from: A }),
"Max fee percentage must be between 0.5% and 100%"
);
await assertRevert(
borrowerOperations.withdrawZUSD("4999999999999999", dec(1, 16), A, A, { from: A }),
"Max fee percentage must be between 0.5% and 100%"
);
});
it("withdrawZUSD(): reverts if fee exceeds max fee percentage", async () => {
await openTrove({
extraZUSDAmount: toBN(dec(60, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: A }
});
await openTrove({
extraZUSDAmount: toBN(dec(60, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: B }
});
await openTrove({
extraZUSDAmount: toBN(dec(70, 16)),
ICR: toBN(dec(2, 18)),
extraParams: { from: C }
});
await openTrove({