58781 sc high totallocked accounting mismatch leading to token balance deficit in alchemistv3

Submitted on Nov 4th 2025 at 13:20:13 UTC by @oct0pwn for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58781

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Protocol insolvency

Description

Brief/Intro

In AlchemistV3.sol, the _sync() function reduces account debt and recomputes rawLocked collateral when debt is reduced due to redemption decay, but fails to update the global _totalLocked variable. When redeem() subsequently uses the inflated _totalLocked value to weight collateral removals, accounts are systematically under-deducted relative to the tokens actually transferred out. This creates a growing token balance deficit that accumulates over multiple redemption cycles, eventually causing withdraw() and redeem() operations to revert due to insufficient contract balance. The issue affects all users with positions that sync between redemptions, leading to a gradual drain of the contract's yield token balance.

Vulnerability Details

The vulnerability stems from an invariant violation where _totalLocked should equal the sum of all account.rawLocked values, but this invariant is broken when accounts sync between redemptions.

The Accounting Flow

1. When redeem() is called (lines 589-641):

The WeightIncrement function calculates the weight based on the ratio of tokens removed (totalOut) to the total locked collateral (old = _totalLocked). This assumes _totalLocked accurately represents the sum of all account.rawLocked values.

2. When _sync() is called (lines 1042-1095):

The Invariant Violation

The AlchemistV3.solcontract maintains _totalLocked correctly in other operations:

  • _addDebt() (line 923): _totalLocked += toLock

  • _subDebt() (line 946): _totalLocked -= toFree

  • _sync() (line 1086): Recomputes rawLocked but does NOT update _totalLocked

Evidence of Invariant

The comment in AlchemistV3.sol on line 128-129 confirms the intended invariant:

This clearly indicates _totalLocked should track the sum of all locked collateral, which is the sum of all account.rawLocked values.

Impact Details

This vulnerability causes a gradual but persistent drain of the contract's yield token balance, affecting the protocol's ability to fulfill withdrawals and redemptions.

Direct Impact

  1. Token Balance Deficit: The contract's actual myt token balance becomes less than _mytSharesDeposited over time. This deficit accumulates with each redemption cycle where accounts sync between redemptions.

  2. Operation Failures:

    • withdraw() operations will revert when users try to withdraw, as TokenUtils.safeTransfer(myt, recipient, amount) will fail due to insufficient balance

    • redeem() operations will eventually fail for the same reason

    • The severity increases over time as more accounts sync and the deficit grows

  3. User Funds at Risk:

    • Users cannot withdraw their deposited collateral

    • The protocol cannot fulfill redemption requests

    • All users with synced positions are affected, with the impact growing as more positions sync

  4. The deficit grows proportionally to:

    • Number of redemptions (redeem() calls)

    • Number of accounts that sync between redemptions

    • Amount by which each synced account's rawLocked decreases

References

  1. src/AlchemistV3.sol:

    • Line 128-130: _totalLocked variable declaration and comment

    • Line 589-641: redeem() function

    • Line 632-634: Weight increment calculation using _totalLocked

    • Line 1042-1095: _sync() function

    • Line 1086: rawLocked recomputation without _totalLocked update

    • Line 913-926: _addDebt() function (correctly updates _totalLocked)

    • Line 932-953: _subDebt() function (correctly updates _totalLocked)

  2. src/libraries/PositionDecay.sol:

    • Line 39-57: WeightIncrement() function

    • Line 67-84: ScaleByWeightDelta() function

Proof of Concept

Proof of Concept

Place the following test in AlchemistV3.t.sol:

Was this helpful?