Skip to content

Commit 3606272

Browse files
committed
feat: debt-only full liquidation
1 parent 70f0d01 commit 3606272

File tree

6 files changed

+181
-28
lines changed

6 files changed

+181
-28
lines changed

src/sdk/accounts/AbstractCreditAccountsService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ export abstract class AbstractCreditAccountService extends SDKConstruct {
362362
keepAssets,
363363
ignoreReservePrices,
364364
applyLossPolicy,
365+
debtOnly,
365366
} = props;
366367
const cm = this.sdk.marketRegister.findCreditManager(account.creditManager);
367368
const routerCloseResult = await this.sdk
@@ -372,6 +373,7 @@ export abstract class AbstractCreditAccountService extends SDKConstruct {
372373
slippage,
373374
force,
374375
keepAssets,
376+
debtOnly,
375377
});
376378
const priceUpdates = await this.getPriceUpdatesForFacade({
377379
creditManager: account.creditManager,

src/sdk/accounts/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,10 @@ export interface FullyLiquidateProps {
407407
* If true, will try to apply loss policy
408408
*/
409409
applyLossPolicy?: boolean;
410+
/**
411+
* Debt only mode - will try to sell just enought of most valuable token to cover debt
412+
*/
413+
debtOnly?: boolean;
410414
}
411415

412416
export interface PermitResult {

src/sdk/router/AbstractRouterContract.ts

Lines changed: 146 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import type { Abi, Address } from "viem";
22

33
import { BaseContract } from "../base/index.js";
4-
import { AddressMap, isDust } from "../utils/index.js";
4+
import { PERCENTAGE_FACTOR } from "../constants/math.js";
5+
import type { IPriceOracleContract } from "../market/index.js";
6+
import { AddressMap, AddressSet, formatBN, isDust } from "../utils/index.js";
57
import type { IHooks } from "../utils/internal/index.js";
68
import { Hooks } from "../utils/internal/index.js";
79
import { limitLeftover } from "./helpers.js";
810
import type {
911
Asset,
12+
ExpectedAndLeftoverOptions,
1013
RouterCASlice,
1114
RouterCMSlice,
1215
RouterHooks,
@@ -32,10 +35,15 @@ export abstract class AbstractRouterContract<
3235
protected getExpectedAndLeftover(
3336
ca: RouterCASlice,
3437
cm: RouterCMSlice,
35-
balances?: Leftovers,
36-
keepAssets?: Address[],
38+
options: ExpectedAndLeftoverOptions = {},
3739
): Leftovers {
38-
const b = balances || this.getDefaultExpectedAndLeftover(ca, keepAssets);
40+
const b = options.balances
41+
? options.balances
42+
: this.getDefaultExpectedAndLeftover(
43+
ca,
44+
options.keepAssets,
45+
options.debtOnly,
46+
);
3947
const { leftoverBalances, expectedBalances, tokensToClaim } = b;
4048

4149
const expected: AddressMap<Asset> = new AddressMap<Asset>();
@@ -65,18 +73,28 @@ export abstract class AbstractRouterContract<
6573
protected getDefaultExpectedAndLeftover(
6674
ca: RouterCASlice,
6775
keepAssets?: Address[],
76+
debtOnly?: boolean,
6877
): Leftovers {
6978
const expectedBalances = new AddressMap<Asset>();
7079
const leftoverBalances = new AddressMap<Asset>();
71-
const keepAssetsSet = new Set(keepAssets?.map(a => a.toLowerCase()));
72-
for (const { token: t, balance, mask } of ca.tokens) {
73-
const token = t as Address;
80+
const keepAssetsSet = new AddressSet(keepAssets);
81+
82+
if (debtOnly) {
83+
const result = this.getLeftoversAfterBuyingDebt(ca, keepAssetsSet);
84+
if (result) {
85+
return result;
86+
} else {
87+
this.logger?.warn("no token found to cover debt");
88+
}
89+
}
90+
91+
for (const { token, balance, mask } of ca.tokens) {
7492
const isEnabled = (mask & ca.enabledTokensMask) !== 0n;
7593
expectedBalances.upsert(token, { token, balance });
7694
// filter out dust, we don't want to swap it
7795
// also: gearbox liquidator does not need to swap disabled tokens. third-party liquidators might want to do it
7896
if (
79-
keepAssetsSet.has(token.toLowerCase()) ||
97+
keepAssetsSet.has(token) ||
8098
!isEnabled ||
8199
isDust({
82100
sdk: this.sdk,
@@ -98,4 +116,124 @@ export abstract class AbstractRouterContract<
98116
tokensToClaim: new AddressMap<Asset>(),
99117
};
100118
}
119+
120+
/**
121+
* Tries to sell just enought of most valuable token to cover debt
122+
* @param ca
123+
* @param keepAssets
124+
* @returns
125+
*/
126+
protected getLeftoversAfterBuyingDebt(
127+
ca: RouterCASlice,
128+
keepAssets: AddressSet,
129+
): Leftovers | undefined {
130+
const { priceOracle } = this.sdk.marketRegister.findByCreditManager(
131+
ca.creditManager,
132+
);
133+
134+
const expectedBalances = new AddressMap<Asset>();
135+
const leftoverBalances = new AddressMap<Asset>();
136+
const usdBalances: Asset[] = [];
137+
138+
for (const { token, balance, mask } of ca.tokens) {
139+
const isEnabled = (mask & ca.enabledTokensMask) !== 0n;
140+
expectedBalances.upsert(token, { token, balance });
141+
leftoverBalances.upsert(token, {
142+
token,
143+
balance: limitLeftover(balance, token) ?? balance,
144+
});
145+
if (isEnabled && !keepAssets.has(token)) {
146+
usdBalances.push({
147+
token,
148+
balance: this.safeConvertToUSD(priceOracle, token, balance),
149+
});
150+
}
151+
}
152+
153+
usdBalances.sort((a, b) => {
154+
if (a.balance > b.balance) return -1;
155+
if (a.balance < b.balance) return 1;
156+
return 0;
157+
});
158+
159+
if (usdBalances.length === 0) {
160+
return undefined;
161+
}
162+
163+
// found token with highest balance in USD which is not in keepAssets and is enabled
164+
const highestToken = usdBalances[0];
165+
const lt = this.sdk.marketRegister
166+
.findCreditManager(ca.creditManager)
167+
.creditManager.liquidationThresholds.mustGet(highestToken.token);
168+
const requiredDebtUSD = (ca.totalDebtUSD * PERCENTAGE_FACTOR) / BigInt(lt);
169+
170+
if (highestToken.balance < requiredDebtUSD) {
171+
return undefined;
172+
}
173+
const tokenAmount = this.safeConvertFromUSD(
174+
priceOracle,
175+
highestToken.token,
176+
requiredDebtUSD,
177+
);
178+
if (tokenAmount === 0n) {
179+
return undefined;
180+
}
181+
let leftoverBalance =
182+
leftoverBalances.get(highestToken.token)?.balance ?? 0n;
183+
leftoverBalance -= tokenAmount;
184+
if (leftoverBalance < 0n) {
185+
return undefined;
186+
}
187+
leftoverBalances.upsert(highestToken.token, {
188+
token: highestToken.token,
189+
balance: leftoverBalance,
190+
});
191+
const tokenAmountStr = this.sdk.tokensMeta.formatBN(
192+
highestToken.token,
193+
tokenAmount,
194+
{ symbol: true },
195+
);
196+
const totalDebtUSDStr = formatBN(ca.totalDebtUSD, 8);
197+
this.logger?.debug(
198+
`will sell ${tokenAmountStr} (LT=${lt}) to cover debt of ${totalDebtUSDStr} USD`,
199+
);
200+
201+
return {
202+
expectedBalances,
203+
leftoverBalances,
204+
tokensToClaim: new AddressMap<Asset>(),
205+
};
206+
}
207+
208+
protected safeConvertToUSD(
209+
priceOracle: IPriceOracleContract,
210+
token: Address,
211+
balance: bigint,
212+
): bigint {
213+
try {
214+
return priceOracle.convertToUSD(token, balance);
215+
} catch {
216+
try {
217+
return priceOracle.convertToUSD(token, balance, true);
218+
} catch {
219+
return 0n;
220+
}
221+
}
222+
}
223+
224+
protected safeConvertFromUSD(
225+
priceOracle: IPriceOracleContract,
226+
token: Address,
227+
balance: bigint,
228+
): bigint {
229+
try {
230+
return priceOracle.convertFromUSD(token, balance);
231+
} catch {
232+
try {
233+
return priceOracle.convertFromUSD(token, balance, true);
234+
} catch {
235+
return 0n;
236+
}
237+
}
238+
}
101239
}

src/sdk/router/RouterV300Contract.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { PathOptionFactory } from "./PathOptionFactory.js";
2121
import type {
2222
Asset,
23+
ExpectedAndLeftoverOptions,
2324
FindAllSwapsProps,
2425
FindBestClosePathProps,
2526
FindClaimAllRewardsProps,
@@ -256,23 +257,23 @@ export class RouterV300Contract
256257
creditManager: cm,
257258
slippage,
258259
balances,
260+
debtOnly,
259261
}: FindBestClosePathProps): Promise<RouterCloseResult> {
260262
const {
261263
pathOptions,
262264
expected,
263265
leftover: leftoverUnsafe,
264266
connectors,
265-
} = this.getFindClosePathInput(
266-
ca,
267-
cm,
268-
balances
267+
} = this.getFindClosePathInput(ca, cm, {
268+
balances: balances
269269
? {
270270
expectedBalances: assetsMap(balances.expectedBalances),
271271
leftoverBalances: assetsMap(balances.leftoverBalances),
272272
tokensToClaim: assetsMap(balances.tokensToClaim || []),
273273
}
274274
: undefined,
275-
);
275+
debtOnly,
276+
});
276277

277278
const leftover: Array<Asset> = leftoverUnsafe.map(a => ({
278279
...a,
@@ -351,14 +352,12 @@ export class RouterV300Contract
351352
public getFindClosePathInput(
352353
ca: RouterCASlice,
353354
cm: RouterCMSlice,
354-
balances?: Leftovers,
355-
keepAssets?: Address[],
355+
options?: ExpectedAndLeftoverOptions,
356356
): FindClosePathInput {
357357
const { expectedBalances, leftoverBalances } = this.getExpectedAndLeftover(
358358
ca,
359359
cm,
360-
balances,
361-
keepAssets,
360+
options,
362361
);
363362

364363
const leftover: Array<Asset> = leftoverBalances.values().map(a => ({

src/sdk/router/RouterV310Contract.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { AbstractRouterContract } from "./AbstractRouterContract.js";
1111
import { assetsMap, balancesMap, limitLeftover } from "./helpers.js";
1212
import type {
1313
Asset,
14+
ExpectedAndLeftoverOptions,
1415
FindAllSwapsProps,
1516
FindBestClosePathProps,
1617
FindClaimAllRewardsProps,
@@ -213,20 +214,20 @@ export class RouterV310Contract
213214
slippage,
214215
balances,
215216
keepAssets,
217+
debtOnly,
216218
} = props;
217219
const { expectedBalances, leftoverBalances, tokensToClaim } =
218-
this.getExpectedAndLeftover(
219-
ca,
220-
cm,
221-
balances
220+
this.getExpectedAndLeftover(ca, cm, {
221+
balances: balances
222222
? {
223223
expectedBalances: assetsMap(balances.expectedBalances),
224224
leftoverBalances: assetsMap(balances.leftoverBalances),
225225
tokensToClaim: assetsMap(balances.tokensToClaim || []),
226226
}
227227
: undefined,
228-
keepAssets,
229-
);
228+
keepAssets: balances ? undefined : keepAssets,
229+
debtOnly,
230+
});
230231

231232
const getNumSplits = this.#numSplitsGetter(cm, expectedBalances.values());
232233
const tData: TokenData[] = [];
@@ -421,7 +422,7 @@ export class RouterV310Contract
421422
public getFindClosePathInput(
422423
_: RouterCASlice,
423424
__: RouterCMSlice,
424-
___?: Leftovers,
425+
___?: ExpectedAndLeftoverOptions,
425426
): FindClosePathInput {
426427
throw ERR_NOT_IMPLEMENTED;
427428
}

src/sdk/router/types.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export type RouterCASlice = Pick<
101101
| "creditAccount"
102102
| "creditFacade"
103103
| "debt"
104+
| "totalDebtUSD"
104105
| "creditManager"
105106
>;
106107

@@ -246,6 +247,10 @@ export interface FindBestClosePathProps {
246247
* TODO: legacy v3 option to pass to contract
247248
*/
248249
force?: boolean;
250+
/**
251+
* Debt only mode - will try to sell just enought of most valuable token to cover debt
252+
*/
253+
debtOnly?: boolean;
249254
}
250255

251256
export interface ClosePathBalances {
@@ -267,6 +272,12 @@ export interface ClosePathBalances {
267272
tokensToClaim?: Array<Asset>;
268273
}
269274

275+
export interface ExpectedAndLeftoverOptions {
276+
balances?: Leftovers;
277+
keepAssets?: Address[];
278+
debtOnly?: boolean;
279+
}
280+
270281
export interface IRouterContract extends IBaseContract {
271282
/**
272283
* Find the best path to swap token A to token B (target token).
@@ -331,15 +342,13 @@ export interface IRouterContract extends IBaseContract {
331342
*
332343
* @param ca
333344
* @param cm
334-
* @param balances
335-
* @param keepAssets
345+
* @param options
336346
* @returns
337347
*/
338348
getFindClosePathInput: (
339349
ca: RouterCASlice,
340350
cm: RouterCMSlice,
341-
balances?: Leftovers,
342-
keepAssets?: Address[],
351+
options?: ExpectedAndLeftoverOptions,
343352
) => FindClosePathInput;
344353
}
345354

0 commit comments

Comments
 (0)