57212 sc high totallocked is not properly decremented in the redeem function causing system insolvency
Submitted on Oct 24th 2025 at 12:45:10 UTC by @godwinudo for Audit Comp | Alchemix V3
Report ID: #57212
Report Type: Smart Contract
Report severity: High
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol
Impacts:
Protocol insolvency
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The redeem function in AlchemistV3.sol contains an error when reducing the global _totalLocked variable during transmuter redemptions. The function uses an incorrect multiplier (approximately 1.05 for a 5% fee) instead of the correct minimumCollateralization ratio (2.0 for 200% LTV), and operates on the wrong debt amount. This causes _totalLocked to remain inflated after each redemption, resulting in user positions retaining excess withdrawable collateral that should have been reserved for transmuter obligations. Over time, this leads to a collateral deficit where the system cannot fulfill all transmuter redemptions, resulting in protocol insolvency.
Vulnerability Details
The _totalLocked variable tracks the total amount of collateral across all user positions that must remain locked to maintain minimum collateralization ratios. When a user borrows debt against their collateral, a portion of that collateral becomes locked and cannot be withdrawn. The locked amount is calculated as the debt amount converted to yield tokens multiplied by the minimum collateralization ratio (typically 200% or 2.0).
Throughout the AlchemistV3 codebase, whenever debt changes occur, the _totalLocked variable is updated using a consistent formula. When debt increases, locked collateral increases using this calculation:
Similarly, when debt decreases, the locked collateral should be freed using the inverse calculation:
This pattern appears consistently in functions like _addDebt() and _subDebt(), where the multiplier is always minimumCollateralization / FIXED_POINT_SCALAR, which equals 2.0 for a 200% collateralization requirement.
However, the redeem() function breaks this pattern. When the transmuter redeems earmarked debt, the function is supposed to reduce _totalLocked by the amount of locked collateral that corresponds to the redeemed debt.
The error occurs in these lines where _totalLocked is reduced:
The function calculates totalOut as the collateral being redeemed plus the protocol fee, which equals convertDebtTokensToYield(amount) * (1 + protocolFee/BPS). With a typical 5% protocol fee, this multiplier becomes approximately 1.05. The function then reduces _totalLocked by this totalOut value.
This approach contains two distinct errors. First, it uses the wrong multiplier. The reduction should use minimumCollateralization / FIXED_POINT_SCALAR (which is 2.0) but instead uses (1 + protocolFee/BPS) (which is approximately 1.05). Second, it operates on the wrong debt amount. The function uses amount to calculate the reduction, but the actual debt removed from the system is redeemedDebtTotal, which includes both the direct redemption amount and any cover that was applied.
The correct implementation should be:
To illustrate the magnitude of this error, consider a concrete example with standard protocol parameters. Assume a 200% minimum collateralization ratio and a 5% protocol fee. When 100 units of debt are redeemed with an additional 10 units from cover (totaling 110 units of debt reduction):
The correct calculation would reduce _totalLocked by:
110 * 2.0 = 220 units
The actual incorrect calculation reduces _totalLocked by:
100 * 1.05 = 105 units
This creates a discrepancy of 115 units per redemption that accumulates in _totalLocked, leaving it permanently inflated.
The inflated _totalLocked value then propagates through the system's accounting mechanism. The protocol uses a weight-based system to distribute collateral reductions across user positions when redemptions occur. The _collateralWeight variable tracks these distributions, and its increments are calculated using _totalLocked as the denominator:
Where WeightIncrement internally performs a division by the denominator (old, which is _totalLocked). When _totalLocked is inflated, these weight increments become artificially small. Later, when individual user positions are synced, the amount of collateral removed from each position is calculated as:
Because the weight delta is too small (due to the inflated denominator), less collateral is removed from user positions than should be. This means users retain excess collateral that should have been marked as consumed by the redemption process. Since withdrawable collateral is calculated as total collateral minus locked collateral, this excess remains available for withdrawal by users, even though it should have been reserved to fulfill transmuter redemption obligations.
Impact Details
After each redemption, the _totalLocked variable becomes more inflated, causing user positions to retain increasingly more collateral than they should. The excess collateral that users can withdraw represents funds that should have been reserved to fulfill transmuter obligations, creating a deficit in the system.
As more redemptions occur over time, this error accumulates. Each redemption adds to the total collateral deficit, and eventually, the Alchemist contract will not hold sufficient collateral to fulfill all outstanding transmuter positions.
Proof of Concept
Proof of Concept
Add this to the AlchemistV3.t.sol test suite and run
Was this helpful?