Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import type {
PlanDepositArgs,
PlanDepositWithSwapFromWalletArgs,
PlanLiquidationArgs,
PlanLiquidationWithMarginCollateralArgs,
PlanMigrateSameAssetCollateralArgs,
PlanMigrateSameAssetDebtArgs,
PlanMintArgs,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2197,7 +2201,9 @@ export class ExecutionService<TVaultEntity extends VaultEntity = VaultEntity>
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) {
Expand Down Expand Up @@ -2514,9 +2520,9 @@ export class ExecutionService<TVaultEntity extends VaultEntity = VaultEntity>
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
Expand All @@ -2525,7 +2531,9 @@ export class ExecutionService<TVaultEntity extends VaultEntity = VaultEntity>
account?.getCurrentController(borrowAccount);
const currentController =
currentControllerBeforeCleanup &&
!cleanup.disabledControllers.has(getAddress(currentControllerBeforeCleanup))
!cleanup.disabledControllers.has(
getAddress(currentControllerBeforeCleanup),
)
? currentControllerBeforeCleanup
: undefined;
const enableController =
Expand Down Expand Up @@ -2645,6 +2653,63 @@ export class ExecutionService<TVaultEntity extends VaultEntity = VaultEntity>
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<IHasVaultAddress>;
accountWithMarginCollateral.isCollateralEnabled = (
subAccount: Address,
vault: Address,
) => {
if (
getAddress(subAccount) === getAddress(liquidatorSubAccountAddress) &&
getAddress(vault) === getAddress(collateral)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wrapper makes the liquidation account report the seized collateral vault as enabled, but the preceding deposit only enabled marginCollateral.vault. When those two addresses differ, planLiquidation skips the enableCollateral call for the seized collateral vault, so the generated batch can be missing the collateral enable needed for the liquidation output. I reproduced it with a focused probe using distinct margin/seized vaults: the batch is [enableCollateral(margin), deposit, enableController, liquidate] instead of also appending enableCollateral(seized). Either compare this override against marginCollateral.vault (so only the actually-enabled vault is faked as enabled), or enforce a same-vault invariant in the exported API with a runtime guard + test.

) {
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.
Expand Down Expand Up @@ -3136,7 +3201,9 @@ export class ExecutionService<TVaultEntity extends VaultEntity = VaultEntity>
account?.getCurrentController(borrowAccount);
const currentController =
currentControllerBeforeCleanup &&
!cleanup.disabledControllers.has(getAddress(currentControllerBeforeCleanup))
!cleanup.disabledControllers.has(
getAddress(currentControllerBeforeCleanup),
)
? currentControllerBeforeCleanup
: undefined;

Expand Down Expand Up @@ -3740,7 +3807,9 @@ export class ExecutionService<TVaultEntity extends VaultEntity = VaultEntity>
account?.getCurrentController(receiver);
const currentController =
currentControllerBeforeCleanup &&
!cleanup.disabledControllers.has(getAddress(currentControllerBeforeCleanup))
!cleanup.disabledControllers.has(
getAddress(currentControllerBeforeCleanup),
)
? currentControllerBeforeCleanup
: undefined;

Expand Down Expand Up @@ -3909,7 +3978,9 @@ export class ExecutionService<TVaultEntity extends VaultEntity = VaultEntity>
account?.getCurrentController(receiver);
const currentController =
currentControllerBeforeCleanup &&
!cleanup.disabledControllers.has(getAddress(currentControllerBeforeCleanup))
!cleanup.disabledControllers.has(
getAddress(currentControllerBeforeCleanup),
)
? currentControllerBeforeCleanup
: undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IHasVaultAddress>;
liabilityVault: Address;
Expand Down
2 changes: 2 additions & 0 deletions packages/euler-v2-sdk/src/services/executionService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export type {
PlanClosePositionWithCowArgs,
PlanDepositArgs,
PlanDepositWithSwapFromWalletArgs,
PlanLiquidationArgs,
PlanLiquidationWithMarginCollateralArgs,
PlanMigrateSameAssetCollateralArgs,
PlanMigrateSameAssetDebtArgs,
PlanMintArgs,
Expand Down
51 changes: 51 additions & 0 deletions packages/euler-v2-sdk/test/executionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down