@gearbox-protocol/sdk 14.3.1 → 14.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -454,10 +454,63 @@ class CreditAccountsServiceV310 extends import_base.SDKConstruct {
454
454
  };
455
455
  }
456
456
  /**
457
- * {@inheritDoc ICreditAccountsService.calcMinSeizedAmount}
457
+ * {@inheritDoc ICreditAccountsService.partiallyLiquidate}
458
458
  */
459
- calcMinSeizedAmount(props) {
460
- const { account, token, repaidAmount } = props;
459
+ async partiallyLiquidate(props) {
460
+ const { account, to } = props;
461
+ const tokenOut = props.tokenOut ?? this.#getBestTokenOut(account);
462
+ const optimalHF = props.optimalHF ?? this.getOptimalHFForPartialLiquidation(account);
463
+ const repaidAmount = props.repaidAmount ?? this.#calcOptimalRepaidAmount(account, tokenOut, optimalHF);
464
+ const minSeizedAmount = props.minSeizedAmount ?? this.#calcMinSeizedAmount(account, tokenOut, repaidAmount);
465
+ const cm = this.sdk.marketRegister.findCreditManager(account.creditManager);
466
+ const updates = await this.getOnDemandPriceUpdates(account, true);
467
+ const tx = cm.creditFacade.partiallyLiquidateCreditAccount(
468
+ account.creditAccount,
469
+ tokenOut,
470
+ repaidAmount,
471
+ minSeizedAmount,
472
+ to,
473
+ updates
474
+ );
475
+ return tx;
476
+ }
477
+ /**
478
+ * Picks the most valuable enabled non-underlying collateral token on the
479
+ * credit account (by oracle value in underlying).
480
+ *
481
+ * Ported from solidity:
482
+ * https://github.com/Gearbox-protocol/router-v3/blob/main/contracts/liquidation/AbstractLiquidator.sol#L270
483
+ */
484
+ #getBestTokenOut(account) {
485
+ const market = this.sdk.marketRegister.findByCreditManager(
486
+ account.creditManager
487
+ );
488
+ const underlying = market.underlying;
489
+ let bestVal = 0n;
490
+ let bestToken;
491
+ for (const t of account.tokens) {
492
+ if (t.token === underlying) continue;
493
+ if ((t.mask & account.enabledTokensMask) === 0n) continue;
494
+ if (t.balance === 0n) continue;
495
+ const val = market.priceOracle.convert(t.token, underlying, t.balance);
496
+ if (val > bestVal) {
497
+ bestVal = val;
498
+ bestToken = t.token;
499
+ }
500
+ }
501
+ if (!bestToken) {
502
+ throw new Error(
503
+ `cannot determine tokenOut for partial liquidation of ${this.sdk.labelAddress(account.creditAccount)}: no enabled non-underlying collateral with value`
504
+ );
505
+ }
506
+ return bestToken;
507
+ }
508
+ /**
509
+ * Returns the minimum amount of `token` collateral that must be seized when
510
+ * repaying `repaidAmount` of underlying. Mirrors the on-chain liquidation
511
+ * discount math, expired-aware.
512
+ */
513
+ #calcMinSeizedAmount(account, token, repaidAmount) {
461
514
  const market = this.sdk.marketRegister.findByCreditManager(
462
515
  account.creditManager
463
516
  );
@@ -473,21 +526,55 @@ class CreditAccountsServiceV310 extends import_base.SDKConstruct {
473
526
  return tokenAmount * 9990n / BigInt(fee);
474
527
  }
475
528
  /**
476
- * {@inheritDoc ICreditAccountsService.partiallyLiquidate}
529
+ * Computes the optimal `repaidAmount` (in underlying) that brings the credit
530
+ * account's health factor close to `optimalHF` after partial liquidation.
531
+ *
532
+ * Ported from solidity:
533
+ * https://github.com/Gearbox-protocol/router-v3/blob/56e2d515ec6d9bb1e324e71c3708e59710779b24/contracts/liquidation/AbstractLiquidator.sol#L292
477
534
  */
478
- async partiallyLiquidate(props) {
479
- const { account, token, repaidAmount, minSeizedAmount, to } = props;
480
- const cm = this.sdk.marketRegister.findCreditManager(account.creditManager);
481
- const updates = await this.getOnDemandPriceUpdates(account, true);
482
- const tx = cm.creditFacade.partiallyLiquidateCreditAccount(
483
- account.creditAccount,
484
- token,
485
- repaidAmount,
486
- minSeizedAmount,
487
- to,
488
- updates
535
+ #calcOptimalRepaidAmount(account, token, optimalHF) {
536
+ const suite = this.sdk.marketRegister.findCreditManager(
537
+ account.creditManager
489
538
  );
490
- return tx;
539
+ const market = this.sdk.marketRegister.findByCreditManager(
540
+ account.creditManager
541
+ );
542
+ const cm = suite.creditManager;
543
+ const feeLiquidation = suite.isExpired ? cm.feeLiquidationExpired : cm.feeLiquidation;
544
+ const liquidationDiscount = suite.isExpired ? cm.liquidationDiscountExpired : cm.liquidationDiscount;
545
+ const discount = BigInt(liquidationDiscount) - BigInt(feeLiquidation);
546
+ const ltTokenOut = cm.liquidationThresholds.get(token);
547
+ if (ltTokenOut === void 0) {
548
+ throw new Error(
549
+ `token ${this.sdk.labelAddress(token)} is not a collateral token in credit manager ${this.sdk.labelAddress(account.creditManager)}`
550
+ );
551
+ }
552
+ const totalDebt = account.debt + account.accruedInterest + account.accruedFees;
553
+ const twvUnderlying = market.priceOracle.convertFromUSD(
554
+ market.underlying,
555
+ account.twvUSD
556
+ );
557
+ const denominator = discount * optimalHF / import_constants.PERCENTAGE_FACTOR - BigInt(ltTokenOut);
558
+ if (denominator <= 0n) {
559
+ throw new Error(
560
+ "cannot compute optimal repaid amount: invalid liquidation parameters (discount * hfOptimal <= ltTokenOut)"
561
+ );
562
+ }
563
+ const numerator = totalDebt * optimalHF - twvUnderlying * import_constants.PERCENTAGE_FACTOR;
564
+ if (numerator <= 0n) {
565
+ return 0n;
566
+ }
567
+ const optimalValueSeized = numerator / denominator;
568
+ let repaidAmount = optimalValueSeized * discount / import_constants.PERCENTAGE_FACTOR;
569
+ const minDebt = suite.creditFacade.minDebt;
570
+ if (totalDebt < minDebt) {
571
+ return 0n;
572
+ }
573
+ const surplusDebt = totalDebt - minDebt;
574
+ if (repaidAmount > surplusDebt) {
575
+ repaidAmount = surplusDebt * 999n / 1000n;
576
+ }
577
+ return repaidAmount;
491
578
  }
492
579
  /**
493
580
  * {@inheritDoc ICreditAccountsService.closeCreditAccount}
@@ -449,10 +449,63 @@ class CreditAccountsServiceV310 extends SDKConstruct {
449
449
  };
450
450
  }
451
451
  /**
452
- * {@inheritDoc ICreditAccountsService.calcMinSeizedAmount}
452
+ * {@inheritDoc ICreditAccountsService.partiallyLiquidate}
453
453
  */
454
- calcMinSeizedAmount(props) {
455
- const { account, token, repaidAmount } = props;
454
+ async partiallyLiquidate(props) {
455
+ const { account, to } = props;
456
+ const tokenOut = props.tokenOut ?? this.#getBestTokenOut(account);
457
+ const optimalHF = props.optimalHF ?? this.getOptimalHFForPartialLiquidation(account);
458
+ const repaidAmount = props.repaidAmount ?? this.#calcOptimalRepaidAmount(account, tokenOut, optimalHF);
459
+ const minSeizedAmount = props.minSeizedAmount ?? this.#calcMinSeizedAmount(account, tokenOut, repaidAmount);
460
+ const cm = this.sdk.marketRegister.findCreditManager(account.creditManager);
461
+ const updates = await this.getOnDemandPriceUpdates(account, true);
462
+ const tx = cm.creditFacade.partiallyLiquidateCreditAccount(
463
+ account.creditAccount,
464
+ tokenOut,
465
+ repaidAmount,
466
+ minSeizedAmount,
467
+ to,
468
+ updates
469
+ );
470
+ return tx;
471
+ }
472
+ /**
473
+ * Picks the most valuable enabled non-underlying collateral token on the
474
+ * credit account (by oracle value in underlying).
475
+ *
476
+ * Ported from solidity:
477
+ * https://github.com/Gearbox-protocol/router-v3/blob/main/contracts/liquidation/AbstractLiquidator.sol#L270
478
+ */
479
+ #getBestTokenOut(account) {
480
+ const market = this.sdk.marketRegister.findByCreditManager(
481
+ account.creditManager
482
+ );
483
+ const underlying = market.underlying;
484
+ let bestVal = 0n;
485
+ let bestToken;
486
+ for (const t of account.tokens) {
487
+ if (t.token === underlying) continue;
488
+ if ((t.mask & account.enabledTokensMask) === 0n) continue;
489
+ if (t.balance === 0n) continue;
490
+ const val = market.priceOracle.convert(t.token, underlying, t.balance);
491
+ if (val > bestVal) {
492
+ bestVal = val;
493
+ bestToken = t.token;
494
+ }
495
+ }
496
+ if (!bestToken) {
497
+ throw new Error(
498
+ `cannot determine tokenOut for partial liquidation of ${this.sdk.labelAddress(account.creditAccount)}: no enabled non-underlying collateral with value`
499
+ );
500
+ }
501
+ return bestToken;
502
+ }
503
+ /**
504
+ * Returns the minimum amount of `token` collateral that must be seized when
505
+ * repaying `repaidAmount` of underlying. Mirrors the on-chain liquidation
506
+ * discount math, expired-aware.
507
+ */
508
+ #calcMinSeizedAmount(account, token, repaidAmount) {
456
509
  const market = this.sdk.marketRegister.findByCreditManager(
457
510
  account.creditManager
458
511
  );
@@ -468,21 +521,55 @@ class CreditAccountsServiceV310 extends SDKConstruct {
468
521
  return tokenAmount * 9990n / BigInt(fee);
469
522
  }
470
523
  /**
471
- * {@inheritDoc ICreditAccountsService.partiallyLiquidate}
524
+ * Computes the optimal `repaidAmount` (in underlying) that brings the credit
525
+ * account's health factor close to `optimalHF` after partial liquidation.
526
+ *
527
+ * Ported from solidity:
528
+ * https://github.com/Gearbox-protocol/router-v3/blob/56e2d515ec6d9bb1e324e71c3708e59710779b24/contracts/liquidation/AbstractLiquidator.sol#L292
472
529
  */
473
- async partiallyLiquidate(props) {
474
- const { account, token, repaidAmount, minSeizedAmount, to } = props;
475
- const cm = this.sdk.marketRegister.findCreditManager(account.creditManager);
476
- const updates = await this.getOnDemandPriceUpdates(account, true);
477
- const tx = cm.creditFacade.partiallyLiquidateCreditAccount(
478
- account.creditAccount,
479
- token,
480
- repaidAmount,
481
- minSeizedAmount,
482
- to,
483
- updates
530
+ #calcOptimalRepaidAmount(account, token, optimalHF) {
531
+ const suite = this.sdk.marketRegister.findCreditManager(
532
+ account.creditManager
484
533
  );
485
- return tx;
534
+ const market = this.sdk.marketRegister.findByCreditManager(
535
+ account.creditManager
536
+ );
537
+ const cm = suite.creditManager;
538
+ const feeLiquidation = suite.isExpired ? cm.feeLiquidationExpired : cm.feeLiquidation;
539
+ const liquidationDiscount = suite.isExpired ? cm.liquidationDiscountExpired : cm.liquidationDiscount;
540
+ const discount = BigInt(liquidationDiscount) - BigInt(feeLiquidation);
541
+ const ltTokenOut = cm.liquidationThresholds.get(token);
542
+ if (ltTokenOut === void 0) {
543
+ throw new Error(
544
+ `token ${this.sdk.labelAddress(token)} is not a collateral token in credit manager ${this.sdk.labelAddress(account.creditManager)}`
545
+ );
546
+ }
547
+ const totalDebt = account.debt + account.accruedInterest + account.accruedFees;
548
+ const twvUnderlying = market.priceOracle.convertFromUSD(
549
+ market.underlying,
550
+ account.twvUSD
551
+ );
552
+ const denominator = discount * optimalHF / PERCENTAGE_FACTOR - BigInt(ltTokenOut);
553
+ if (denominator <= 0n) {
554
+ throw new Error(
555
+ "cannot compute optimal repaid amount: invalid liquidation parameters (discount * hfOptimal <= ltTokenOut)"
556
+ );
557
+ }
558
+ const numerator = totalDebt * optimalHF - twvUnderlying * PERCENTAGE_FACTOR;
559
+ if (numerator <= 0n) {
560
+ return 0n;
561
+ }
562
+ const optimalValueSeized = numerator / denominator;
563
+ let repaidAmount = optimalValueSeized * discount / PERCENTAGE_FACTOR;
564
+ const minDebt = suite.creditFacade.minDebt;
565
+ if (totalDebt < minDebt) {
566
+ return 0n;
567
+ }
568
+ const surplusDebt = totalDebt - minDebt;
569
+ if (repaidAmount > surplusDebt) {
570
+ repaidAmount = surplusDebt * 999n / 1000n;
571
+ }
572
+ return repaidAmount;
486
573
  }
487
574
  /**
488
575
  * {@inheritDoc ICreditAccountsService.closeCreditAccount}
@@ -64,10 +64,6 @@ export declare class CreditAccountsServiceV310 extends SDKConstruct implements I
64
64
  * {@inheritDoc ICreditAccountsService.fullyLiquidate}
65
65
  **/
66
66
  fullyLiquidate(props: FullyLiquidateProps): Promise<FullyLiquidateResult>;
67
- /**
68
- * {@inheritDoc ICreditAccountsService.calcMinSeizedAmount}
69
- */
70
- calcMinSeizedAmount(props: PartiallyLiquidateProps): bigint;
71
67
  /**
72
68
  * {@inheritDoc ICreditAccountsService.partiallyLiquidate}
73
69
  */
@@ -488,41 +488,37 @@ export interface FullyLiquidateProps {
488
488
  */
489
489
  debtOnly?: boolean;
490
490
  }
491
- export interface CalcMinSeizedAmountProps {
492
- /**
493
- * Credit account to liquidate
494
- */
495
- account: RouterCASlice;
496
- /**
497
- * Collateral token to seize
498
- */
499
- token: Address;
500
- /**
501
- * Amount of underlying token to repay
502
- */
503
- repaidAmount: bigint;
504
- }
505
491
  export interface PartiallyLiquidateProps {
506
492
  /**
507
493
  * Credit account to liquidate
508
494
  */
509
- account: RouterCASlice;
495
+ account: CreditAccountData;
510
496
  /**
511
497
  * Address to transfer underlying left after liquidation
512
498
  */
513
499
  to: Address;
514
500
  /**
515
- * Collateral token to seize
501
+ * Collateral token to seize.
502
+ * If omitted, the most valuable enabled non-underlying collateral token
503
+ * (by oracle)
516
504
  */
517
- token: Address;
505
+ tokenOut?: Address;
518
506
  /**
519
- * Amount of underlying token to repay
507
+ * Amount of underlying token to repay.
508
+ * If omitted, computed internally
520
509
  */
521
- repaidAmount: bigint;
510
+ repaidAmount?: bigint;
522
511
  /**
523
- * Minimum amount of `token` to seize from `creditAccount`
512
+ * Minimum amount of `token` to seize from `creditAccount`.
513
+ * If omitted, computed internally
524
514
  */
525
- minSeizedAmount: bigint;
515
+ minSeizedAmount?: bigint;
516
+ /**
517
+ * Target health factor for partial liquidation (4 digits precision, 10000 = 100%).
518
+ * If omitted, defaults to {@link ICreditAccountsService.getOptimalHFForPartialLiquidation}.
519
+ * Only used when `repaidAmount` is not explicitly provided.
520
+ */
521
+ optimalHF?: bigint;
526
522
  }
527
523
  /**
528
524
  * EIP-2612 permit signature data for a token, enabling gasless approval for credit account operations.
@@ -717,13 +713,6 @@ export interface ICreditAccountsService extends Construct {
717
713
  * @returns Transaction data and optional loss policy data
718
714
  */
719
715
  fullyLiquidate(props: FullyLiquidateProps): Promise<FullyLiquidateResult>;
720
- /**
721
- * Calculates minimum amount of collateral token to seize from credit account
722
- * for partial liquidation, can be passed to {@link partiallyLiquidate} as `minSeizedAmount`
723
- * @param props - {@link CalcMinSeizedAmountProps}
724
- * @returns Minimum amount of collateral token to seize
725
- */
726
- calcMinSeizedAmount(props: CalcMinSeizedAmountProps): bigint;
727
716
  /**
728
717
  * Generates transaction to partially liquidate credit account;
729
718
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gearbox-protocol/sdk",
3
- "version": "14.3.1",
3
+ "version": "14.4.0",
4
4
  "description": "Gearbox SDK",
5
5
  "license": "MIT",
6
6
  "main": "./dist/cjs/sdk/index.js",