diff --git a/packages/euler-v2-sdk/src/services/executionService/executionService.ts b/packages/euler-v2-sdk/src/services/executionService/executionService.ts index ac80ba0e..7f25a965 100644 --- a/packages/euler-v2-sdk/src/services/executionService/executionService.ts +++ b/packages/euler-v2-sdk/src/services/executionService/executionService.ts @@ -105,6 +105,7 @@ import type { PlanDepositArgs, PlanDepositWithSwapFromWalletArgs, PlanLiquidationArgs, + PlanLiquidationWithMarginCollateralArgs, PlanMigrateSameAssetCollateralArgs, PlanMigrateSameAssetDebtArgs, PlanMintArgs, @@ -679,6 +680,9 @@ export interface IExecutionService< planRedeem(args: PlanRedeemArgs): TransactionPlan; planBorrow(args: PlanBorrowArgs): TransactionPlan; planLiquidation(args: PlanLiquidationArgs): TransactionPlan; + planLiquidationWithMarginCollateral( + args: PlanLiquidationWithMarginCollateralArgs, + ): TransactionPlan; planRepayFromWallet(args: PlanRepayFromWalletArgs): TransactionPlan; planRepayFromDeposit(args: PlanRepayFromDepositArgs): TransactionPlan; planRepayWithSwap(args: PlanRepayWithSwapArgs): TransactionPlan; @@ -2197,7 +2201,9 @@ export class ExecutionService const normalizedController = getAddress(controller); if (normalizedController === normalizedControllerToKeep) return; disabledControllers.add(normalizedController); - items.push(this.encodeDisableController(normalizedController, subAccount)); + items.push( + this.encodeDisableController(normalizedController, subAccount), + ); }; if (controllers.length === 0) { @@ -2514,9 +2520,9 @@ export class ExecutionService collateral && collateral.amount > 0n ? cleanup.disabledCollaterals.has(getAddress(collateral.vault)) || !( - account?.isCollateralEnabled(borrowAccount, collateral.vault) ?? - false - ) + account?.isCollateralEnabled(borrowAccount, collateral.vault) ?? + false + ) : false; // Check if controller needs to be enabled @@ -2525,7 +2531,9 @@ export class ExecutionService account?.getCurrentController(borrowAccount); const currentController = currentControllerBeforeCleanup && - !cleanup.disabledControllers.has(getAddress(currentControllerBeforeCleanup)) + !cleanup.disabledControllers.has( + getAddress(currentControllerBeforeCleanup), + ) ? currentControllerBeforeCleanup : undefined; const enableController = @@ -2645,6 +2653,63 @@ export class ExecutionService return plan; } + /** + * Builds a liquidation plan that first supplies margin collateral into the + * liquidator sub-account, then liquidates without planning a duplicate + * collateral enable for the same vault. + * + * @param args - Liquidation with margin collateral plan arguments + * @param args.marginCollateral - Collateral asset supplied from the wallet before liquidation + * @returns Transaction plan with margin collateral approval/deposit plus liquidation approval/batch + */ + planLiquidationWithMarginCollateral( + args: PlanLiquidationWithMarginCollateralArgs, + ): TransactionPlan { + const { + account, + collateral, + liquidatorSubAccountAddress, + marginCollateral, + } = args; + + if (marginCollateral.amount <= 0n) { + return this.planLiquidation(args); + } + + const depositPlan = this.planDeposit({ + account, + vault: marginCollateral.vault, + amount: marginCollateral.amount, + receiver: liquidatorSubAccountAddress, + asset: marginCollateral.asset, + enableCollateral: true, + wrappedNativeInfo: marginCollateral.wrappedNativeInfo, + }); + + const accountWithMarginCollateral = Object.create( + account, + ) as Account; + accountWithMarginCollateral.isCollateralEnabled = ( + subAccount: Address, + vault: Address, + ) => { + if ( + getAddress(subAccount) === getAddress(liquidatorSubAccountAddress) && + getAddress(vault) === getAddress(collateral) + ) { + return true; + } + return account.isCollateralEnabled(subAccount, vault); + }; + + const liquidationPlan = this.planLiquidation({ + ...args, + account: accountWithMarginCollateral, + }); + + return this.mergePlans([depositPlan, liquidationPlan]); + } + /** * Builds a transaction plan for repaying debt using assets from the wallet. * Use `maxUint256` for `liabilityAmount` to repay all available debt. @@ -3136,7 +3201,9 @@ export class ExecutionService account?.getCurrentController(borrowAccount); const currentController = currentControllerBeforeCleanup && - !cleanup.disabledControllers.has(getAddress(currentControllerBeforeCleanup)) + !cleanup.disabledControllers.has( + getAddress(currentControllerBeforeCleanup), + ) ? currentControllerBeforeCleanup : undefined; @@ -3740,7 +3807,9 @@ export class ExecutionService account?.getCurrentController(receiver); const currentController = currentControllerBeforeCleanup && - !cleanup.disabledControllers.has(getAddress(currentControllerBeforeCleanup)) + !cleanup.disabledControllers.has( + getAddress(currentControllerBeforeCleanup), + ) ? currentControllerBeforeCleanup : undefined; @@ -3909,7 +3978,9 @@ export class ExecutionService account?.getCurrentController(receiver); const currentController = currentControllerBeforeCleanup && - !cleanup.disabledControllers.has(getAddress(currentControllerBeforeCleanup)) + !cleanup.disabledControllers.has( + getAddress(currentControllerBeforeCleanup), + ) ? currentControllerBeforeCleanup : undefined; diff --git a/packages/euler-v2-sdk/src/services/executionService/executionServiceTypes.ts b/packages/euler-v2-sdk/src/services/executionService/executionServiceTypes.ts index 79d0e6d3..49591564 100644 --- a/packages/euler-v2-sdk/src/services/executionService/executionServiceTypes.ts +++ b/packages/euler-v2-sdk/src/services/executionService/executionServiceTypes.ts @@ -687,6 +687,15 @@ export type PlanLiquidationArgs = { minYieldBalance: bigint; }; +export type PlanLiquidationWithMarginCollateralArgs = PlanLiquidationArgs & { + marginCollateral: { + vault: Address; + amount: bigint; + asset: Address; + wrappedNativeInfo?: WrappedNativeInfo; + }; +}; + export type PlanRepayFromWalletArgs = { account: Account; liabilityVault: Address; diff --git a/packages/euler-v2-sdk/src/services/executionService/index.ts b/packages/euler-v2-sdk/src/services/executionService/index.ts index f348cdd2..96d26644 100644 --- a/packages/euler-v2-sdk/src/services/executionService/index.ts +++ b/packages/euler-v2-sdk/src/services/executionService/index.ts @@ -106,6 +106,8 @@ export type { PlanClosePositionWithCowArgs, PlanDepositArgs, PlanDepositWithSwapFromWalletArgs, + PlanLiquidationArgs, + PlanLiquidationWithMarginCollateralArgs, PlanMigrateSameAssetCollateralArgs, PlanMigrateSameAssetDebtArgs, PlanMintArgs, diff --git a/packages/euler-v2-sdk/test/executionService.test.ts b/packages/euler-v2-sdk/test/executionService.test.ts index 0e13602d..7ff1d5ae 100644 --- a/packages/euler-v2-sdk/test/executionService.test.ts +++ b/packages/euler-v2-sdk/test/executionService.test.ts @@ -811,6 +811,57 @@ test("deposit supports native wrapping before the vault deposit", () => { assert.deepEqual(deposit.args, [AMOUNT, getAddress(ACCOUNT)]); }); +test("liquidation with margin collateral supplies collateral before liquidating without duplicate collateral enable", () => { + const service = createExecutionService(); + const account = { + owner: ACCOUNT, + chainId: 1, + isCollateralEnabled: () => false, + isControllerEnabled: () => false, + } as never; + + const plan = service.planLiquidationWithMarginCollateral({ + account, + liquidatorSubAccountAddress: RECEIVER, + vault: LIABILITY_VAULT, + asset: TOKEN_IN, + violator: SOURCE_ACCOUNT, + collateral: COLLATERAL_VAULT, + repayAssets: AMOUNT, + minYieldBalance: 0n, + marginCollateral: { + vault: COLLATERAL_VAULT, + asset: SAME_ASSET, + amount: 999n, + }, + }); + + assert.equal(plan[0]?.type, "requiredApproval"); + assert.equal(plan[0]?.type === "requiredApproval" && plan[0].token, SAME_ASSET); + assert.equal(plan[0]?.type === "requiredApproval" && plan[0].owner, ACCOUNT); + assert.equal( + plan[0]?.type === "requiredApproval" && plan[0].spender, + COLLATERAL_VAULT, + ); + assert.equal(plan[0]?.type === "requiredApproval" && plan[0].amount, 999n); + assert.equal(plan[1]?.type, "requiredApproval"); + assert.equal(plan[1]?.type === "requiredApproval" && plan[1].token, TOKEN_IN); + assert.equal(plan[1]?.type === "requiredApproval" && plan[1].owner, ACCOUNT); + assert.equal( + plan[1]?.type === "requiredApproval" && plan[1].spender, + LIABILITY_VAULT, + ); + assert.equal(plan[1]?.type === "requiredApproval" && plan[1].amount, AMOUNT); + + const functionNames = getOnlyEvcBatchItems(plan).map(decodeBatchFunctionName); + assert.deepEqual(functionNames, [ + "enableCollateral", + "deposit", + "enableController", + "liquidate", + ]); +}); + test("redeem accepts assets and converts to shares from account vault state", () => { const service = createExecutionService(); const assets = 123_456n;