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 V3
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):
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):
_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): RecomputesrawLockedbut 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
Token Balance Deficit: The contract's actual
myttoken balance becomes less than_mytSharesDepositedover time. This deficit accumulates with each redemption cycle where accounts sync between redemptions.Operation Failures:
withdraw()operations will revert when users try to withdraw, asTokenUtils.safeTransfer(myt, recipient, amount)will fail due to insufficient balanceredeem()operations will eventually fail for the same reasonThe severity increases over time as more accounts sync and the deficit grows
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
The deficit grows proportionally to:
Number of redemptions (
redeem()calls)Number of accounts that sync between redemptions
Amount by which each synced account's
rawLockeddecreases
References
src/AlchemistV3.sol:Line 128-130:
_totalLockedvariable declaration and commentLine 589-641:
redeem()functionLine 632-634: Weight increment calculation using
_totalLockedLine 1042-1095:
_sync()functionLine 1086:
rawLockedrecomputation without_totalLockedupdateLine 913-926:
_addDebt()function (correctly updates_totalLocked)Line 932-953:
_subDebt()function (correctly updates_totalLocked)
src/libraries/PositionDecay.sol:Line 39-57:
WeightIncrement()functionLine 67-84:
ScaleByWeightDelta()function
Proof of Concept
Proof of Concept
Place the following test in AlchemistV3.t.sol:
Was this helpful?