58573 sc critical alchemistv3 repayment fee cross account theft vulnerability

Submitted on Nov 3rd 2025 at 09:49:23 UTC by @dray for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58573

  • Report Type: Smart Contract

  • Report severity: Critical

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

  • Impacts:

    • Direct theft of any user NFTs, whether at-rest or in-motion, other than unclaimed royalties

Description

Brief/Intro

The _resolveRepaymentFee function in AlchemistV3 (lines 900-907) contains a critical accounting bug that enables cross-account theft during liquidations. When a position with earmarked debt is liquidated, _forceRepay first drains the account's collateral to the transmuter. Subsequently, _resolveRepaymentFee calculates a repayment fee but can only deduct min(calculatedFee, 0) from the now-empty account. However, the function incorrectly returns the full calculated fee instead of the actual deducted amount . This causes the liquidator to be paid the full fee via ERC20 transfer from the contract's shared collateral pool, effectively stealing from other users' deposits. The bug occurs automatically during normal protocol operations whenever positions with earmarked debt are liquidated a frequent scenario given the protocol's transmuter mechanics, high LTV allowances (90%), and strategy performance variance.

Vulnerability Details

Root Cause

Located in AlchemistV3.sol lines 900-907:

function _resolveRepaymentFee(address owner, address collateralType) internal returns (uint256) {
    uint256 collateralBalance = _getCollateral(owner, collateralType);
    uint256 repaymentFee = collateralBalance * _getRepaymentFeeBps() / BPS;
    
    uint256 feeToDeduct = repaymentFee > collateralBalance ? collateralBalance : repaymentFee;
    _decreaseCollateral(owner, collateralType, feeToDeduct);
    
    return repaymentFee; // BUG: Should return feeToDeduct
}

The Critical Flaw: The function calculates repaymentFee, deducts min(repaymentFee, collateralBalance) from the owner's account, but then returns the full repaymentFee value regardless of what was actually deducted.

Attack Vector

This bug is triggered during liquidations that go through the force-repayment path:

  1. Force Repayment Exhausts Balance: When _forceRepay is called (lines 733-800), it can completely drain the liquidated account's collateral balance to repay earmarked debt

  2. Fee Calculated on Zero Balance: _resolveRepaymentFee is then called on the now-empty account

  3. Arithmetic Produces Non-Zero Fee: Even with collateralBalance = 0, the calculation 0 * repaymentFeeBps / BPS can produce a non-zero result due to rounding or if there's any phantom balance

  4. More commonly: The account has a tiny dust amount remaining, so repaymentFee is calculated on this dust amount

  5. Return Value Mismatch: Function deducts only what's available (often 0 or dust), but returns the calculated fee amount

  6. Liquidator Paid from Wrong Source: The liquidator receives the returned fee amount, but since it wasn't fully deducted from the victim's account, it comes from the contract's shared collateral pool (other users' deposits)

Impact Details

##Direct Financial Impact

  • Cross-Account Theft: Liquidators receive fees paid from other users' collateral deposits

  • Collateral Pool Drain: Each affected liquidation reduces the total collateral available to legitimate depositors.

Trust and Protocol Integrity

  • Accounting Desync: Contract's collateral accounting diverges from actual token balances

  • Insolvency Risk: Repeated theft can make the protocol insolvent, unable to honor all withdrawal requests

  • User Harm: Innocent users lose funds through no fault of their own

References

  • AlchemistV3.sol: _resolveRepaymentFee() lines 900-907

  • AlchemistV3.sol: _liquidate() lines 815-850

  • AlchemistV3.sol: _forceRepay() lines 733-800

Proof of Concept

Proof of Concept

Was this helpful?