58408 sc low underflow account rawlocked on subdebt due to rounding inconsistency

Submitted on Nov 2nd 2025 at 02:01:14 UTC by @Jugger63 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58408

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Temporary freezing of funds for at least 1 hour

    • Temporary freezing of funds for at least 24 hour

Description

Finding description

The _subDebt() function in AlchemistV3 calculates how much collateral can be freed (toFree) when the debt (debt) is reduced:

function _subDebt(uint256 tokenId, uint256 amount) internal {
    Account storage account = _accounts[tokenId];

    // collateral that can be released from reducing the debt `amount`
    uint256 toFree = convertDebtTokensToYield(amount)
        * minimumCollateralization / FIXED_POINT_SCALAR;

    // collateral that is locked before reduction
    uint256 lockedCollateral = convertDebtTokensToYield(account.debt)
        * minimumCollateralization / FIXED_POINT_SCALAR;

    if (toFree > _totalLocked) {
        toFree = _totalLocked;               // clamp (global) 🔹
    }

    account.debt        -= amount;
    totalDebt           -= amount;
    _totalLocked        -= toFree;

    // per-account lock update
    account.rawLocked    = lockedCollateral - toFree; // underflow
}

Free and lock Collateral both depend on the conversion chain:

Since all operations perform rounding down (floor), the results for large numbers (account.debt) and small numbers (amount) are not guaranteed to be proportional. At a certain threshold, toFree can exceed the smallest unit of lockedCollateral (±1 wei) resulting in a reduction:

becomes negative and triggers panic(0x11) (arithmetic underflow), the existing Clamp only compares toFree with _totalLocked (global), not with lockedCollateral (per-account) and so does not prevent individual underflows.

Impact

Transactions called repay, burn, liquidate, or other internal functions that call _subDebt() may revert on edge-case conditions. Users cannot repay or liquidate positions even if they are economically sound, resulting in funds being frozen until parameters (APR and price) change.

Recommendation

Either clamp toFree against lockedCollateral (per-account) before the deduction, or recalculate rawLocked using the debt balance after the deduction.

Reference

https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L936-L947

Proof of Concept

Scenario Considerations

  1. The position has debt = D and rawLocked = floor(f(D))

  2. The user executes repay(amount) with amount ≈ D

  3. Integer conversion + rounding results in toFree = floor(f(amount)) > lockedCollateral = floor(f(D))

  4. _subDebt() executes account.rawLocked = lockedCollateral – toFree → underflow → revert.

  5. The transaction fails, and the user cannot pay off the balance, even though mathematically there is sufficient collateral.

POC

Add to AlchemistV3.t.sol.

Was this helpful?