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 V3arrow-up-right

  • 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)

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?