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 V3
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
The position has
debt = DandrawLocked = floor(f(D))The user executes
repay(amount)withamount ≈ DInteger conversion + rounding results in
toFree = floor(f(amount)) > lockedCollateral = floor(f(D))_subDebt()executesaccount.rawLocked = lockedCollateral – toFree→ underflow →revert.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?