11import type { Abi , Address } from "viem" ;
22
33import { 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" ;
57import type { IHooks } from "../utils/internal/index.js" ;
68import { Hooks } from "../utils/internal/index.js" ;
79import { limitLeftover } from "./helpers.js" ;
810import 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}
0 commit comments