58266 sc high partial liquidation strands base fee due to post seizure balance check

Submitted on Oct 31st 2025 at 20:32:37 UTC by @zcai for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58266

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol

  • Impacts:

    • Permanent freezing of funds

Description

Brief/Intro

The liquidation flow contains a critical sequencing issue where collateral seizure occurs before fee validation, causing base liquidation fees to be stranded in the contract when the borrower's remaining collateral is insufficient.

Vulnerability Details

The _doLiquidation() function implements a problematic sequence of operations that creates an atomicity failure in fee handling. The function first seizes the gross collateral amount (including the base fee) from the borrower's account and transfers the net amount to the transmuter. Only after these irreversible state changes does it attempt to validate and pay the base fee to the liquidator.

The vulnerability manifests in the conditional fee transfer logic:

// Line 871: Collateral is seized first
account.collateralBalance = account.collateralBalance > amountLiquidated ? account.collateralBalance - amountLiquidated : 0;

// Line 875: Net amount sent to transmuter  
TokenUtils.safeTransfer(myt, transmuter, amountLiquidated - feeInYield);

// Line 878-880: Fee transfer is conditional on post-seizure balance
if (feeInYield > 0 && account.collateralBalance >= feeInYield) {
    TokenUtils.safeTransfer(myt, msg.sender, feeInYield);
}

When the borrower's remaining collateral after seizure is less than the calculated fee amount, the conditional check fails and the fee transfer is silently skipped. Since the gross collateral (including fee) was already debited from the borrower's account, the fee amount becomes stranded in the contract with no mechanism for recovery or reconciliation.

This occurs frequently in partial liquidations where the gross seizure amount consumes most of the user's available collateral, leaving insufficient balance for the subsequent fee validation check. The calculateLiquidation() function properly computes the fee as part of the gross seizure amount, but the implementation fails to ensure atomic execution of the entire liquidation flow.

Impact Details

The vulnerability causes direct financial loss to borrowers whose collateral is reduced by the full gross liquidation amount while the corresponding base fee is neither paid to the liquidator nor returned to their account. The stranded funds remain locked in the contract without any recovery mechanism, representing a permanent loss for the affected borrowers. Additionally, liquidators receive reduced compensation when fees are skipped, potentially undermining the economic incentives that ensure timely liquidation of undercollateralized positions and threatening the overall stability of the liquidation system.

References

https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol?utm_source=immunefi#L852-L894

https://gist.github.com/i-am-zcai/d9c1db2875cc80255e19341a03a2d227

Proof of Concept

Proof of Concept

// PoC: Demonstrate stranded liquidation fee when post-seizure borrower collateral is insufficient to pay base fee contract AlchemistV3_LiquidationStrandedFee_PoC is AlchemistV3Test { function test_PoC_LiquidationStrandsFeeWhenPostSeizureCollateralInsufficient() external { // Configure liquidator fee to 100% of surplus to deterministically force gross seize == full collateral vm.startPrank(alOwner); alchemist.setLiquidatorFee(10_000); // 100% of surplus vm.stopPrank();

}

forge test --match-path src/test/poc/AlchemistV3_LiquidationStrandedFee.t.sol --match-test test_PoC_LiquidationStrandsFeeWhenPostSeizureCollateralInsufficient -vvv

Was this helpful?