Skip to content

Commit 5c08647

Browse files
authored
feat(liquidator): prevent liquidator from liquidating itself (#3128)
1 parent dd7925d commit 5c08647

File tree

2 files changed

+142
-3
lines changed

2 files changed

+142
-3
lines changed

packages/liquidator/src/liquidator.js

+15
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,21 @@ class Liquidator {
266266
position: position,
267267
});
268268

269+
if (
270+
position.sponsor ==
271+
(this.proxyTransactionWrapper.useDsProxyToLiquidate
272+
? this.proxyTransactionWrapper.dsProxyManager.getDSProxyAddress()
273+
: this.account)
274+
) {
275+
this.logger.warn({
276+
at: "Liquidator",
277+
message: "The liquidator has an open position that is liquidatable! Bot will not liquidate itself! 😟",
278+
scaledPrice: scaledPrice.toString(),
279+
maxCollateralPerToken: maxCollateralPerToken.toString(),
280+
position: position,
281+
});
282+
continue;
283+
}
269284
// Note: query the time again during each iteration to ensure the deadline is set reasonably.
270285
const currentBlockTime = this.financialContractClient.getLastUpdateTime();
271286

packages/liquidator/test/Liquidator.js

+127-3
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,64 @@ contract("Liquidator.js", function (accounts) {
999999
assert.equal(positionObject.tokensOutstanding.rawValue, convertSynthetic("5"));
10001000
}
10011001
);
1002+
versionedIt([{ contractType: "any", contractVersion: "any" }])(
1003+
"Liquidator will not liquidate itself with EOA sponsor",
1004+
async function () {
1005+
// liquidator creates a position with 125 units of collateral, creating 100 synthetic tokens.
1006+
await financialContract.create(
1007+
{ rawValue: convertCollateral("125") },
1008+
{ rawValue: convertSynthetic("100") },
1009+
{ from: liquidatorBot }
1010+
);
1011+
1012+
// sponsor2 creates a position with 150 units of collateral, creating 100 synthetic tokens.
1013+
await financialContract.create(
1014+
{ rawValue: convertCollateral("150") },
1015+
{ rawValue: convertSynthetic("100") },
1016+
{ from: sponsor1 }
1017+
);
1018+
1019+
// Assume the price feed given to the liquidator is 1.2 this makes the liquidator under water but sponsor1
1020+
// is above water. The liquidator bot should correctly identify this but it should not liquidate itself.
1021+
// A price of 1.2 USD per token: debt * price * coltReq > debt for collateralized position.
1022+
// liquidator: 100 * 1.3 * 1.2 > 125 [undercollateralized]
1023+
// Sponsor1: 100 * 1.3 * 1.2 < 150 [sufficiently collateralized]
1024+
1025+
priceFeedMock.setCurrentPrice(convertPrice("1.2"));
1026+
await liquidator.update();
1027+
await liquidator.liquidatePositions();
1028+
assert.equal(spy.callCount, 1); // 1 info level events should be sent warning that the bot wont liquidate itself!.
1029+
1030+
// Should emit the right log message
1031+
assert.isTrue(spyLogIncludes(spy, 0, "The liquidator has an open position that is liquidatable"));
1032+
1033+
// liquidatorBot should have all their collateral left and no liquidations.
1034+
assert.deepStrictEqual(await financialContract.getLiquidations(liquidatorBot), []);
1035+
assert.equal((await financialContract.getCollateral(liquidatorBot)).rawValue, convertCollateral("125"));
1036+
1037+
// Sponsor1 should have all their collateral left and no liquidations.
1038+
assert.deepStrictEqual(await financialContract.getLiquidations(sponsor1), []);
1039+
assert.equal((await financialContract.getCollateral(sponsor1)).rawValue, convertCollateral("150"));
1040+
1041+
// Run the liquidator again but this time at a price that will liquidatoe the sponsor. The liquidator
1042+
// should still not liquidate itself, but should take out the underwater sponsor.
1043+
priceFeedMock.setCurrentPrice(convertPrice("1.3"));
1044+
await liquidator.update();
1045+
await liquidator.liquidatePositions();
1046+
assert.equal(spy.callCount, 3);
1047+
1048+
// liquidatorBot should have all their collateral left and no liquidations.
1049+
assert.deepStrictEqual(await financialContract.getLiquidations(liquidatorBot), []);
1050+
assert.equal((await financialContract.getCollateral(liquidatorBot)).rawValue, convertCollateral("125"));
1051+
1052+
// Sponsor1 should be in a liquidation state with the bot as the liquidator.
1053+
let liquidationObject = (await financialContract.getLiquidations(sponsor1))[0];
1054+
assert.equal(liquidationObject.sponsor, sponsor1);
1055+
assert.equal(liquidationObject.liquidator, liquidatorBot);
1056+
assert.equal(liquidationObject.state, LiquidationStatesEnum.PRE_DISPUTE);
1057+
assert.equal(liquidationObject.liquidatedCollateral, convertCollateral("150"));
1058+
}
1059+
);
10021060
describe("Partial liquidations", function () {
10031061
versionedIt([{ contractType: "any", contractVersion: "any" }])(
10041062
"amount-to-liquidate > min-sponsor-tokens",
@@ -2031,7 +2089,7 @@ contract("Liquidator.js", function (accounts) {
20312089
}
20322090
}
20332091
);
2034-
versionedIt([{ contractType: "any", contractVersion: "any" }])(
2092+
versionedIt([{ contractType: "any", contractVersion: "any" }], true)(
20352093
"Correctly deals with reserve being the same as collateral currency using DSProxy",
20362094
async function () {
20372095
// create a new liquidator and set the reserve currency to the collateral currency.
@@ -2117,7 +2175,7 @@ contract("Liquidator.js", function (accounts) {
21172175
assert.isTrue(spyLogIncludes(spy, 3, "Submitting a partial liquidation"));
21182176
assert.isTrue(spyLogIncludes(spy, 4, "Executed function on a freshly deployed library"));
21192177
assert.isTrue(spyLogIncludes(spy, 5, "Position has been liquidated"));
2120-
assert.isTrue(spyLogIncludes(spy, 6, "Insufficient balance to liquidate the minimum sponsor size"));
2178+
assert.isTrue(spyLogIncludes(spy, 6, "The liquidator has an open position that is liquidatable"));
21212179
}
21222180
);
21232181
versionedIt([{ contractType: "any", contractVersion: "any" }])(
@@ -2280,7 +2338,7 @@ contract("Liquidator.js", function (accounts) {
22802338
assert.isTrue((await reserveToken.balanceOf(dsProxy.address)).lt(toBN(toWei("0.000001"))));
22812339
}
22822340
);
2283-
versionedIt([{ contractType: "any", contractVersion: "any" }], true)(
2341+
versionedIt([{ contractType: "any", contractVersion: "any" }])(
22842342
"Correctly respects max slippage parameters",
22852343
async function () {
22862344
await reserveToken.mint(dsProxy.address, toWei("1000000"), { from: contractCreator });
@@ -2433,6 +2491,72 @@ contract("Liquidator.js", function (accounts) {
24332491
assert.equal(await getPoolSpotPrice(), "1.0342");
24342492
}
24352493
);
2494+
versionedIt([{ contractType: "any", contractVersion: "any" }])(
2495+
"Liquidator will not liquidate itself with DSProxy sponsor",
2496+
async function () {
2497+
// sponsor1 creates a position with 125 units of collateral, creating 100 synthetic tokens.
2498+
await financialContract.create(
2499+
{ rawValue: convertCollateral("125") },
2500+
{ rawValue: convertSynthetic("100") },
2501+
{ from: sponsor1 }
2502+
);
2503+
2504+
// sponsor2 creates a position with 150 units of collateral, creating 100 synthetic tokens.
2505+
await financialContract.create(
2506+
{ rawValue: convertCollateral("150") },
2507+
{ rawValue: convertSynthetic("100") },
2508+
{ from: sponsor2 }
2509+
);
2510+
2511+
// liquidatorBot creates NO position. This will happen atomically within 1tx by the dsProxy. Send reserve.
2512+
await reserveToken.mint(dsProxy.address, toWei("1000"), { from: contractCreator });
2513+
2514+
// Assume the price feed given to the liquidator is 1.1 this makes sponsor1 under water. The bot should
2515+
// mint and liquidate in one transaction to take out the position.
2516+
// Sponsor1: 100 * 1.3 * 1.1 > 125 [undercollateralized]
2517+
// Sponsor2: 100 * 1.3 * 1.1 < 150 [sufficiently collateralized]
2518+
// Note that at this point the GCR is 275/200 so the liquidator will mint at this rate.
2519+
2520+
priceFeedMock.setCurrentPrice(convertPrice("1.1"));
2521+
await liquidator.update();
2522+
await liquidator.liquidatePositions();
2523+
2524+
assert.equal(spy.callCount, 3); // 3 info level events should be sent at the conclusion of the 1 liquidations.
2525+
// 1 for the deployment of the DSProxy, 1 for the execution of the DSPRoxy ta and 1 for the liquidation.
2526+
2527+
// Sponsor1 should be in a liquidation state with the bot as the liquidator.
2528+
let liquidationObject = (await financialContract.getLiquidations(sponsor1))[0];
2529+
assert.equal(liquidationObject.sponsor, sponsor1);
2530+
assert.equal(liquidationObject.liquidator, dsProxy.address);
2531+
assert.equal(liquidationObject.state, LiquidationStatesEnum.PRE_DISPUTE);
2532+
assert.equal(liquidationObject.liquidatedCollateral, convertCollateral("125"));
2533+
2534+
// Next, assume that sponsor2 re-collateralizes their position taking it above the GCR.
2535+
await financialContract.deposit({ rawValue: convertCollateral("100") }, { from: sponsor2 });
2536+
2537+
// The liquidator's position has 100 units of debt and 137.5 units of collateral. At this ratio a price of
2538+
// anything more than 1.145 would make the liquidator under water. If we set the price to 1.2 then the liquidator's
2539+
// position should be liquidatable. However, the liquidator should never liquidate itself.
2540+
2541+
priceFeedMock.setCurrentPrice(convertPrice("1.2"));
2542+
await liquidator.update();
2543+
await liquidator.liquidatePositions();
2544+
2545+
// Should emit the right log message
2546+
assert.isTrue(spyLogIncludes(spy, -1, "The liquidator has an open position that is liquidatable"));
2547+
2548+
// dsProxy.address should have all their collateral left and no liquidations.
2549+
assert.deepStrictEqual(await financialContract.getLiquidations(dsProxy.address), []);
2550+
assert.equal(
2551+
(await financialContract.getCollateral(dsProxy.address)).rawValue,
2552+
convertCollateral("137.5")
2553+
);
2554+
2555+
// Sponsor2 should have all their collateral left and no liquidations.
2556+
assert.deepStrictEqual(await financialContract.getLiquidations(sponsor2), []);
2557+
assert.equal((await financialContract.getCollateral(sponsor2)).rawValue, convertCollateral("250"));
2558+
}
2559+
);
24362560
});
24372561
});
24382562
}

0 commit comments

Comments
 (0)