Skip to content

Commit 9dc28e7

Browse files
committed
Fix Recovery Mode liquidation loops: track gas compensation and collateral surplus
Backport of Liquity's fix (liquity/dev b9dcdbd3 2021-05-19, corrected in 47fb4d56 2021-09-03). The running entireSystemColl used by the Recovery Mode liquidation loops now subtracts collToSendToSP, collGasCompensation and collSurplus in both _getTotalsFromLiquidateTrovesSequence_RecoveryMode and _getTotalFromBatchLiquidate_RecoveryMode. The batch loop previously subtracted only collToSendToSP, so after a capped liquidation the TCR seen by the next trove in the batch was too high: a trove above the real TCR could be liquidated, and the back-to-Normal-Mode switch was judged on wrong numbers. Adds the ported Liquity regression suite (TroveManager_RecoveryMode_BatchLiquidationTest.js); "A trove over TCR is not liquidated" fails on the previous code. (cherry picked from commit f315d7f0f475edd6d04324d3cae6cb06a5d3d7ea)
1 parent 03ee119 commit 9dc28e7

2 files changed

Lines changed: 229 additions & 3 deletions

File tree

contracts/TroveManager.sol

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager {
529529
vars.entireSystemColl = vars
530530
.entireSystemColl
531531
.sub(singleLiquidation.collToSendToSP)
532+
.sub(singleLiquidation.collGasCompensation)
532533
.sub(singleLiquidation.collSurplus);
533534

534535
// Add liquidation values to their respective running totals
@@ -725,9 +726,11 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager {
725726
singleLiquidation.debtToOffset
726727
);
727728
vars.entireSystemDebt = vars.entireSystemDebt.sub(singleLiquidation.debtToOffset);
728-
vars.entireSystemColl = vars.entireSystemColl.sub(
729-
singleLiquidation.collToSendToSP
730-
);
729+
vars.entireSystemColl = vars
730+
.entireSystemColl
731+
.sub(singleLiquidation.collToSendToSP)
732+
.sub(singleLiquidation.collGasCompensation)
733+
.sub(singleLiquidation.collSurplus);
731734

732735
// Add liquidation values to their respective running totals
733736
totals = _addLiquidationValuesToTotals(totals, singleLiquidation);
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
const deploymentHelper = require("../../utils/js/deploymentHelpers.js");
2+
const testHelpers = require("../../utils/js/testHelpers.js");
3+
4+
const th = testHelpers.TestHelper;
5+
const mv = testHelpers.MoneyValues;
6+
const { toBN, dec } = th;
7+
const ZERO_ADDRESS = th.ZERO_ADDRESS || "0x0000000000000000000000000000000000000000";
8+
9+
const TroveManagerTester = artifacts.require("./TroveManagerTester");
10+
const ZUSDToken = artifacts.require("./ZUSDToken.sol");
11+
12+
/*
13+
* Ported from Liquity's TroveManager_RecoveryMode_Batch_Liqudation_Test.js
14+
* (liquity/dev, fix commits b9dcdbd3 2021-05-19 and 47fb4d56 2021-09-03).
15+
*
16+
* Covers the running system-collateral tracker used by the Recovery Mode
17+
* liquidation loops. Before the fix, _getTotalFromBatchLiquidate_RecoveryMode
18+
* only subtracted collToSendToSP, so after a capped liquidation the TCR seen by
19+
* the next trove in the batch was too high and a trove above the real TCR could
20+
* be liquidated ("A trove over TCR is not liquidated" fails on the old code).
21+
*/
22+
contract("TroveManager - in Recovery Mode - back to normal mode in 1 tx", async accounts => {
23+
const [bountyAddress, lpRewardsAddress, multisig] = accounts.slice(997, 1000);
24+
const [owner, alice, bob, carol, dennis, erin, freddy, greta, harry, ida, whale] = accounts;
25+
26+
let contracts;
27+
let troveManager;
28+
let stabilityPool;
29+
let priceFeed;
30+
let sortedTroves;
31+
32+
const openTrove = async params => th.openTrove(contracts, params);
33+
34+
beforeEach(async () => {
35+
contracts = await deploymentHelper.deployLiquityCore();
36+
contracts.troveManager = await TroveManagerTester.new(contracts.permit2.address);
37+
contracts.zusdToken = await ZUSDToken.new();
38+
await contracts.zusdToken.initialize(
39+
contracts.troveManager.address,
40+
contracts.stabilityPool.address,
41+
contracts.borrowerOperations.address
42+
);
43+
const ZEROContracts = await deploymentHelper.deployZEROContracts(multisig);
44+
45+
troveManager = contracts.troveManager;
46+
stabilityPool = contracts.stabilityPool;
47+
priceFeed = contracts.priceFeedTestnet;
48+
sortedTroves = contracts.sortedTroves;
49+
50+
await deploymentHelper.connectZEROContracts(ZEROContracts);
51+
await deploymentHelper.connectCoreContracts(contracts, ZEROContracts);
52+
await deploymentHelper.connectZEROContractsToCore(ZEROContracts, contracts);
53+
});
54+
55+
context("Batch liquidations", () => {
56+
const setup = async () => {
57+
const { collateral: A_coll, totalDebt: A_totalDebt } = await openTrove({ ICR: toBN(dec(296, 16)), extraParams: { from: alice } });
58+
const { collateral: B_coll, totalDebt: B_totalDebt } = await openTrove({ ICR: toBN(dec(280, 16)), extraParams: { from: bob } });
59+
const { collateral: C_coll, totalDebt: C_totalDebt } = await openTrove({ ICR: toBN(dec(150, 16)), extraParams: { from: carol } });
60+
61+
const totalLiquidatedDebt = A_totalDebt.add(B_totalDebt).add(C_totalDebt);
62+
const spDeposit = totalLiquidatedDebt.add(toBN(dec(1, 18)));
63+
64+
await openTrove({ ICR: toBN(dec(340, 16)), extraZUSDAmount: spDeposit, extraParams: { from: whale } });
65+
await stabilityPool.provideToSP(spDeposit, ZERO_ADDRESS, { from: whale });
66+
67+
// Price drops
68+
await priceFeed.setPrice(dec(100, 18));
69+
const price = await priceFeed.getPrice();
70+
const TCR = await th.getTCR(contracts);
71+
72+
// Check Recovery Mode is active
73+
assert.isTrue(await th.checkRecoveryMode(contracts));
74+
75+
// Check troves A, B are in range 110% < ICR < TCR, C is below 100%
76+
const ICR_A = await troveManager.getCurrentICR(alice, price);
77+
const ICR_B = await troveManager.getCurrentICR(bob, price);
78+
const ICR_C = await troveManager.getCurrentICR(carol, price);
79+
80+
assert.isTrue(ICR_A.gt(mv._MCR) && ICR_A.lt(TCR));
81+
assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR));
82+
assert.isTrue(ICR_C.lt(mv._ICR100));
83+
84+
return { A_coll, A_totalDebt, B_coll, B_totalDebt, C_coll, C_totalDebt, totalLiquidatedDebt, price };
85+
};
86+
87+
it("First trove only doesn't get out of Recovery Mode", async () => {
88+
await setup();
89+
await troveManager.batchLiquidateTroves([alice]);
90+
assert.isTrue(await th.checkRecoveryMode(contracts));
91+
});
92+
93+
it("Two troves over MCR are liquidated", async () => {
94+
await setup();
95+
const tx = await troveManager.batchLiquidateTroves([alice, bob, carol]);
96+
97+
const liquidationEvents = th.getAllEventsByName(tx, "TroveLiquidated");
98+
assert.equal(liquidationEvents.length, 3, "Not enough liquidations");
99+
100+
assert.isFalse(await sortedTroves.contains(alice));
101+
assert.isFalse(await sortedTroves.contains(bob));
102+
assert.isFalse(await sortedTroves.contains(carol));
103+
104+
// Status enum element idx 3 = closed by liquidation
105+
assert.equal((await troveManager.Troves(alice))[3], "3");
106+
assert.equal((await troveManager.Troves(bob))[3], "3");
107+
assert.equal((await troveManager.Troves(carol))[3], "3");
108+
});
109+
110+
it("Stability Pool profit matches", async () => {
111+
const { A_totalDebt, C_coll, price } = await setup();
112+
113+
const spEthBefore = await stabilityPool.getETH();
114+
const spZusdBefore = await stabilityPool.getTotalZUSDDeposits();
115+
116+
await troveManager.batchLiquidateTroves([alice, carol]);
117+
118+
assert.isFalse(await sortedTroves.contains(alice));
119+
assert.isFalse(await sortedTroves.contains(carol));
120+
assert.equal((await troveManager.Troves(alice))[3], "3");
121+
assert.equal((await troveManager.Troves(carol))[3], "3");
122+
123+
const spEthAfter = await stabilityPool.getETH();
124+
const spZusdAfter = await stabilityPool.getTotalZUSDDeposits();
125+
126+
// liquidated collateral with the gas compensation subtracted
127+
const expectedCollateralLiquidatedA = th.applyLiquidationFee(A_totalDebt.mul(mv._MCR).div(price));
128+
const expectedGainInZUSD = expectedCollateralLiquidatedA.mul(price).div(mv._1e18BN).sub(A_totalDebt);
129+
const realGainInZUSD = spEthAfter.sub(spEthBefore).mul(price).div(mv._1e18BN).sub(spZusdBefore.sub(spZusdAfter));
130+
131+
assert.equal(spEthAfter.sub(spEthBefore).toString(), expectedCollateralLiquidatedA.toString(), "Stability Pool RBTC doesn't match");
132+
assert.equal(spZusdBefore.sub(spZusdAfter).toString(), A_totalDebt.toString(), "Stability Pool ZUSD doesn't match");
133+
assert.equal(realGainInZUSD.toString(), expectedGainInZUSD.toString(), "Stability Pool gains don't match");
134+
});
135+
136+
it("A trove over TCR is not liquidated", async () => {
137+
const { totalDebt: A_totalDebt } = await openTrove({ ICR: toBN(dec(280, 16)), extraParams: { from: alice } });
138+
const { totalDebt: B_totalDebt } = await openTrove({ ICR: toBN(dec(276, 16)), extraParams: { from: bob } });
139+
const { totalDebt: C_totalDebt } = await openTrove({ ICR: toBN(dec(150, 16)), extraParams: { from: carol } });
140+
141+
const totalLiquidatedDebt = A_totalDebt.add(B_totalDebt).add(C_totalDebt);
142+
143+
await openTrove({ ICR: toBN(dec(310, 16)), extraZUSDAmount: totalLiquidatedDebt, extraParams: { from: whale } });
144+
await stabilityPool.provideToSP(totalLiquidatedDebt, ZERO_ADDRESS, { from: whale });
145+
146+
// Price drops
147+
await priceFeed.setPrice(dec(100, 18));
148+
const price = await priceFeed.getPrice();
149+
const TCR = await th.getTCR(contracts);
150+
151+
assert.isTrue(await th.checkRecoveryMode(contracts));
152+
153+
// A above TCR, B in 110% < ICR < TCR, C below 100%
154+
const ICR_A = await troveManager.getCurrentICR(alice, price);
155+
const ICR_B = await troveManager.getCurrentICR(bob, price);
156+
const ICR_C = await troveManager.getCurrentICR(carol, price);
157+
158+
assert.isTrue(ICR_A.gt(TCR));
159+
assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR));
160+
assert.isTrue(ICR_C.lt(mv._ICR100));
161+
162+
const tx = await troveManager.batchLiquidateTroves([bob, alice]);
163+
164+
const liquidationEvents = th.getAllEventsByName(tx, "TroveLiquidated");
165+
assert.equal(liquidationEvents.length, 1, "Not enough liquidations");
166+
167+
// Only Bob's trove removed
168+
assert.isTrue(await sortedTroves.contains(alice));
169+
assert.isFalse(await sortedTroves.contains(bob));
170+
assert.isTrue(await sortedTroves.contains(carol));
171+
172+
assert.equal((await troveManager.Troves(bob))[3], "3");
173+
assert.equal((await troveManager.Troves(alice))[3], "1");
174+
assert.equal((await troveManager.Troves(carol))[3], "1");
175+
});
176+
});
177+
178+
context("Sequential liquidations", () => {
179+
const setup = async () => {
180+
const { collateral: A_coll, totalDebt: A_totalDebt } = await openTrove({ ICR: toBN(dec(299, 16)), extraParams: { from: alice } });
181+
const { collateral: B_coll, totalDebt: B_totalDebt } = await openTrove({ ICR: toBN(dec(298, 16)), extraParams: { from: bob } });
182+
183+
const totalLiquidatedDebt = A_totalDebt.add(B_totalDebt);
184+
const spDeposit = totalLiquidatedDebt.add(toBN(dec(1, 18)));
185+
186+
await openTrove({ ICR: toBN(dec(300, 16)), extraZUSDAmount: spDeposit, extraParams: { from: whale } });
187+
await stabilityPool.provideToSP(spDeposit, ZERO_ADDRESS, { from: whale });
188+
189+
await priceFeed.setPrice(dec(100, 18));
190+
const price = await priceFeed.getPrice();
191+
const TCR = await th.getTCR(contracts);
192+
193+
assert.isTrue(await th.checkRecoveryMode(contracts));
194+
195+
const ICR_A = await troveManager.getCurrentICR(alice, price);
196+
const ICR_B = await troveManager.getCurrentICR(bob, price);
197+
198+
assert.isTrue(ICR_A.gt(mv._MCR) && ICR_A.lt(TCR));
199+
assert.isTrue(ICR_B.gt(mv._MCR) && ICR_B.lt(TCR));
200+
201+
return { A_coll, A_totalDebt, B_coll, B_totalDebt, totalLiquidatedDebt, price };
202+
};
203+
204+
it("First trove only doesn't get out of Recovery Mode", async () => {
205+
await setup();
206+
await troveManager.liquidateTroves(1);
207+
assert.isTrue(await th.checkRecoveryMode(contracts));
208+
});
209+
210+
it("Two troves over MCR are liquidated", async () => {
211+
await setup();
212+
const tx = await troveManager.liquidateTroves(10);
213+
214+
const liquidationEvents = th.getAllEventsByName(tx, "TroveLiquidated");
215+
assert.equal(liquidationEvents.length, 2, "Not enough liquidations");
216+
217+
assert.isFalse(await sortedTroves.contains(alice));
218+
assert.isFalse(await sortedTroves.contains(bob));
219+
assert.equal((await troveManager.Troves(alice))[3], "3");
220+
assert.equal((await troveManager.Troves(bob))[3], "3");
221+
});
222+
});
223+
});

0 commit comments

Comments
 (0)