57668 sc high missing collateral tracking update during liquidation leads to inflated total value calculation and delayed under collateralization protection
Submitted on Oct 28th 2025 at 00:38:03 UTC by @enoch for Audit Comp | Alchemix V3
Report ID: #57668
Report Type: Smart Contract
Report severity: High
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol
Impacts:
Permanent freezing of funds
Protocol insolvency
Description
Relevant Context
The Alchemist protocol tracks the total yield token shares deposited via the _mytSharesDeposited state variable. This variable serves as the authoritative source for calculating the total underlying value locked in the protocol through the _getTotalUnderlyingValue() function, which converts these shares to their underlying token equivalent.
When users deposit collateral via AlchemistV3#deposit, the _mytSharesDeposited is incremented. Similarly, when users withdraw via AlchemistV3#withdraw, this value is decremented. The protocol also decrements this variable in other flows where yield tokens leave the system, such as in AlchemistV3#donate, AlchemistV3#repay, and AlchemistV3#redeem.
The _getTotalUnderlyingValue() calculation directly impacts two areas:
The
alchemistCurrentCollateralizationparameter inAlchemistV3#calculateLiquidation, which determines liquidation severityThe bad debt ratio calculation in
Transmuter#claimRedemption, which applies scaling when the system is under-collateralized
Finding Description
During the liquidation process in AlchemistV3#_liquidate, collateral is removed from user accounts through two internal functions: AlchemistV3#_forceRepay and AlchemistV3#_doLiquidation. Both functions decrease the account.collateralBalance when yield tokens are transferred out, but neither function decreases the _mytSharesDeposited variable accordingly.
In AlchemistV3#_forceRepay, when earmarked debt is repaid using the account's collateral:
The function decreases
account.collateralBalancebycreditToYieldProtocol fees are deducted from
account.collateralBalanceYield tokens are transferred to the transmuter and protocol fee receiver
However,
_mytSharesDepositedremains unchanged despite these transfers
In AlchemistV3#_doLiquidation, when actual liquidation occurs:
The function decreases
account.collateralBalancebyamountLiquidatedYield tokens are transferred to the transmuter and liquidator
Again,
_mytSharesDepositedremains unchanged
This creates a discrepancy where _mytSharesDeposited overstates the actual yield tokens held by the protocol, since it includes tokens that have already been transferred out during liquidations.
The inflated _mytSharesDeposited value propagates to _getTotalUnderlyingValue(), which returns a higher total value than actually exists. This inflated value affects:
First impact path: When _doLiquidation calls calculateLiquidation, it passes the inflated value as part of the alchemistCurrentCollateralization calculation. This parameter determines whether the protocol is globally under-collateralized. The calculation uses normalizeUnderlyingTokensToDebt(_getTotalUnderlyingValue()) * FIXED_POINT_SCALAR / totalDebt, making the system appear healthier than it actually is. When alchemistCurrentCollateralization < alchemistMinimumCollateralization, full liquidations occur. An inflated numerator means this condition triggers less frequently, potentially leaving underwater positions partially liquidated when they should be fully liquidated.
Second impact path: In Transmuter#claimRedemption, the bad debt ratio is calculated as alchemist.totalSyntheticsIssued() * 10**decimals / (alchemist.getTotalUnderlyingValue() + yieldTokenBalance). The inflated getTotalUnderlyingValue() increases the denominator, reducing the bad debt ratio. When the true bad debt ratio exceeds 1e18, the protocol scales down redemptions to protect against insolvency. The artificially lower ratio means this protection mechanism activates later than appropriate, allowing users to redeem at full value when they should receive scaled-down amounts.
Impact
Protocol users claiming redemptions through the Transmuter receive excess underlying tokens when the system is under-collateralized. The delayed activation of bad debt scaling means early redeemers extract more value than their fair share during insolvency events, while later redeemers absorb disproportionate losses. Additionally, accounts that should undergo full liquidation when the protocol is globally under-collateralized may only be partially liquidated, allowing unhealthy positions to persist and accumulate additional bad debt.
Recommendation
Update both AlchemistV3#_forceRepay and AlchemistV3#_doLiquidation to decrease _mytSharesDeposited whenever collateral is removed from accounts and transferred out of the contract.
For AlchemistV3#_forceRepay, add the following:
Additionally:
For AlchemistV3#_doLiquidation, add the following:
For AlchemistV3#_resolveRepaymentFee, add:
These changes ensure _mytSharesDeposited accurately reflects the yield tokens held by the protocol, maintaining consistency with the actual collateral backing and enabling proper under-collateralization detection.
Proof of Concept
Proof of Concept
The vulnerability can be demonstrated through a liquidation scenario where collateral is removed from an account but the global tracking variable remains unchanged.
Scenario walkthrough:
An account is created with 11,000 yield token shares deposited as collateral
The account mints 10,000 debt tokens, establishing a healthy collateralization ratio of 1.1
A redemption position is created in the Transmuter to establish system-wide debt obligations
The yield token value decreases by 6%, pushing the account below the liquidation threshold
A liquidator calls
AlchemistV3#liquidateon the underwater accountDuring liquidation,
AlchemistV3#_forceRepayand/orAlchemistV3#_doLiquidationexecute, transferring yield tokens to the transmuter and liquidatorThe
account.collateralBalanceis correctly decreased to reflect the removed collateralHowever,
_mytSharesDepositedis never updated despite yield tokens leaving the contractThe
_mytSharesDepositedvalue remains unchanged, creating an accounting discrepancy
This discrepancy means _getTotalUnderlyingValue() returns an inflated value that includes tokens no longer held by the protocol.
Coded Proof of Concept:
Add this test to src/test/AlchemistV3.t.sol and run with:
The test confirms the accounting discrepancy: even though amountLiquidated > 0 (collateral was removed), _mytSharesDeposited remains unchanged. This proves that the global tracking variable does not reflect the actual yield tokens held by the protocol after liquidation events.
Was this helpful?