58336 sc medium additive update to survival accumulator causing overflow
Submitted on Nov 1st 2025 at 11:20:56 UTC by @w3llyc4de20Ik2nn1 for Audit Comp | Alchemix V3
Report ID: #58336
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol
Impacts:
Protocol insolvency
Description
Brief/Intro
The additive update to _survivalAccumulator in _earmark() causes unbounded linear growth and uint256 overflow by summing weighted survival fractions instead of multiplicatively compounding them, corrupting cumulative decay tracking and enabling protocol insolvency through distorted debt and collateralization calculations.
Vulnerability Details
The additive update to _survivalAccumulator in the _earmark function of the AlchemistV3 contract causes unbounded linear growth and eventual uint256 overflow by summing weighted survival fractions over multiple calls, corrupting cumulative decay tracking instead of multiplicatively compounding them to remain bounded ≤1 as intended in the matching redeem logic.
// ============================================ // IN redeem() - Uses MULTIPLICATION // ============================================ function redeem(uint256 amount) external onlyTransmuter { _earmark();
uint256 liveEarmarked = cumulativeEarmarked;
if (amount > liveEarmarked) amount = liveEarmarked;
uint256 transmuterBal = TokenUtils.safeBalanceOf(myt, address(transmuter));
uint256 deltaYield = transmuterBal > lastTransmuterTokenBalance ? transmuterBal - lastTransmuterTokenBalance : 0;
uint256 coverDebt = convertYieldTokensToDebt(deltaYield);
uint256 coverToApplyDebt = amount + coverDebt > liveEarmarked ? (liveEarmarked - amount) : coverDebt;
uint256 redeemedDebtTotal = amount + coverToApplyDebt;
// LOOK HERE: _survivalAccumulator is MULTIPLIED
if (liveEarmarked != 0 && redeemedDebtTotal != 0) {
uint256 survival = ((liveEarmarked - redeemedDebtTotal) << 128) / liveEarmarked;
_survivalAccumulator = _mulQ128(_survivalAccumulator, survival); // ← MULTIPLICATION
_redemptionWeight += PositionDecay.WeightIncrement(redeemedDebtTotal, cumulativeEarmarked);
}
// ... rest of redemption logic
}
// ============================================ // IN _earmark() - Uses ADDITION (BUG!) // ============================================ function _earmark() internal { if (totalDebt == 0) return; if (block.number <= lastEarmarkBlock) return;
uint256 transmuterCurrentBalance = TokenUtils.safeBalanceOf(myt, address(transmuter));
uint256 transmuterDifference = transmuterCurrentBalance > lastTransmuterTokenBalance ? transmuterCurrentBalance - lastTransmuterTokenBalance : 0;
uint256 amount = ITransmuter(transmuter).queryGraph(lastEarmarkBlock + 1, block.number);
uint256 coverInDebt = convertYieldTokensToDebt(transmuterDifference);
amount = amount > coverInDebt ? amount - coverInDebt : 0;
lastTransmuterTokenBalance = transmuterCurrentBalance;
uint256 liveUnearmarked = totalDebt - cumulativeEarmarked;
if (amount > liveUnearmarked) amount = liveUnearmarked;
if (amount > 0 && liveUnearmarked != 0) {
uint256 previousSurvival = PositionDecay.SurvivalFromWeight(_earmarkWeight);
if (previousSurvival == 0) previousSurvival = ONE_Q128;
uint256 earmarkedFraction = _divQ128(amount, liveUnearmarked);
// BUG HERE: _survivalAccumulator uses ADDITION instead of MULTIPLICATION
_survivalAccumulator += _mulQ128(previousSurvival, earmarkedFraction); // ← ADDITION (WRONG!)
_earmarkWeight += PositionDecay.WeightIncrement(amount, liveUnearmarked);
cumulativeEarmarked += amount;
}
lastEarmarkBlock = block.number;
}
The Problem:
redeem() does:
_earmark() does:
This line adds the product of the previous survival rate and the earmarked fraction to _survivalAccumulator on every earmark call, leading to linear accumulation that grows unbounded over time (e.g., repeated partial earmarks sum fractions exceeding 1.0), eventually overflowing uint256 and corrupting the fixed-point value; the intended behavior is multiplicative compounding to track cumulative survival rates bounded ≤1.0, as seen in redeem's similar update.
Impact Details
The overflow corrupts position decay and survival calculations, allowing manipulated earmarks to distort debt tracking and potentially cause protocol insolvency through incorrect collateralization assessments.
Why This is a Bug:
Survival rates should be compounded multiplicatively, not added:
If 90% survives event 1, then 90% of remainder survives event 2
Total survival = 0.9 × 0.9 = 0.81 (not 0.9 + 0.9 = 1.8)
Recommended Mitigation
Replace the additive update with a multiplicative compound of _survivalAccumulator by the blended survival fraction for the new earmark (prior survival for the earmarked portion + 1.0 for fresh unearmarked debt), ensuring bounded growth while preserving the weighted survival intent.
Fixed Code Snippet
(Apply analogously to the simulation in _calculateUnrealizedDebt for consistency.)
References
https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L738
Proof of Concept
Proof of Concept
First put the test in the AlchemistV3.t.sol Run with forge test --match-test testSurvivalAccumulatorUnboundedGrowthProof –vvvv
Was this helpful?