57530 sc high stale tvl accounting in liquidations leads to protocol insolvency
Submitted on Oct 27th 2025 at 00:26:52 UTC by @legion for Audit Comp | Alchemix V3
Report ID: #57530
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
The AlchemistV3 contract contains a critical accounting flaw where liquidations transfer yield token shares out of the protocol but fail to update the global _mytSharesDeposited counter. This creates a permanent divergence between the protocol's reported Total Value Locked (TVL) and the actual on-chain balance. Because critical protocol operations—including deposit caps, liquidation calculations, and health checks—rely on this inflated TVL number, the system gradually becomes insolvent as liquidations accumulate bad debt while appearing healthy. Users are unable to redeem their synthetic tokens once the transmuter exhausts its real collateral reserves.
Vulnerability Details
The AlchemistV3 contract accepts deposits of yield-bearing tokens (e.g., Morpho vault shares) and allows users to borrow synthetic debt tokens against this collateral. The protocol maintains a single global state variable _mytSharesDeposited to track the total amount of yield token shares held by the contract:
uint256 private _mytSharesDeposited;This counter is intended to represent the actual on-chain balance of yield tokens that the contract controls. It serves as the foundation for several critical protocol functions:
TVL Reporting:
getTotalUnderlyingValue()converts_mytSharesDepositedto underlying token valueDeposit Cap Enforcement: Checks
_mytSharesDeposited + amount <= depositCapbefore accepting depositsGlobal Health Metrics: Feeds into
calculateLiquidation()to determine protocol-wide collateralizationRisk Assessment: Used by operators and dashboards to monitor protocol solvency
The bug occurs in the way it was used during liquidation, during liquidations; tokens are transferred OUT of the contract but _mytSharesDeposited is never decremented:
*State after liquidation:
Contract's actual balance:
-amountLiquidated(tokens sent to transmuter + liquidator)_mytSharesDeposited: unchanged (still includes the liquidated tokens)Accounting is now desynchronized by
amountLiquidated.
Since the protocol's TVL calculation fully trusts _mytSharesDeposited:
After liquidations occur, this function returns a value higher than the actual contract balance:
Reported TVL = convertYieldTokensToUnderlying(_mytSharesDeposited) Includes tokens already sent to transmuter/liquidators, while Actual TVL = Only counts tokens still in the contract.
Example with Numbers
Initial State:
_mytSharesDeposited = 1000e18sharesActual contract balance =
1000e18sharesShare price = 1:1 for simplicity
Reported TVL = 1000 underlying tokens
User deposits and borrows:
User deposits
100e18more shares_mytSharesDeposited = 1100e18User borrows max amount (assuming 200% collateralization)
User debt =
50 debt tokens(worth 50 underlying)
Price drops, liquidation triggered:
Liquidator calls
liquidate(tokenId)amountLiquidated = 60e18shares calculatedfeeInYield = 5e18sharesTotal removed from contract =
65e18shares
Transfers executed:
55e18shares → Transmuter5e18shares → LiquidatorUser's
collateralBalancereduced by60e18Actual contract balance =
1100e18 - 65e18 = 1035e18shares
BUT:
_mytSharesDepositedstill =1100e18Reported TVL =
1100 underlying(inflated by 65)Actual TVL =
1035 underlyingDiscrepancy = 65 underlying tokens
After just one liquidation, the protocol believes it has 6.3% more collateral than it actually controls. This gap compounds with every subsequent liquidation. since the calculateLiquidation logic checks if the protocol's global collateralization ratio is healthy:
With inflated TVL:
Protocol appears healthier than reality
Liquidations seize less collateral than needed
Under-liquidation leaves bad debt in the system
Global collateralization continues degrading
Impact Details
Protocol Insolvency Mechanism:
Liquidations appear to restore health but actually accumulate bad debt
Global collateralization calculation uses inflated TVL
System believes it's overcollateralized when actually undercollateralized
No mechanism exists to correct the accounting error retroactively
Consequences:
Bad debt accrues silently across multiple liquidation events
Eventually, total debt > actual collateral value
Transmuter cannot fulfill redemption requests
Synthetic token loses peg permanently
Users lose funds with no recovery mechanism
Permanent Deposit Denial-of-Service (High)
Mechanism:
Since _mytSharesDeposited never decreases after liquidations:
Example:
Deposit cap set to
10,000e18Current
_mytSharesDeposited = 9,800e18Actual balance after liquidations =
8,500e18Real capacity available =
1,500e18Reported capacity =
200e18New deposit of
500e18rejected despite ample real space
Consequences:
Protocol cannot accept new capital even when undercollateralized
Emergency capital injections during crisis are blocked
Strategy migrations fail (require new deposits)
System enters "frozen" state where it can only bleed value
Recovery becomes impossible without contract upgrade
Under-Liquidation and Cascading Failures (High)
Mechanism: The calculateLiquidation function includes this logic:
With inflated TVL:
Global ratio appears:
(inflated TVL) / (total debt)= looks healthyTriggers partial liquidation when full liquidation needed
Leaves underwater positions in the system
Creates a death spiral: a. Under-liquidation → bad debt remains b. Next liquidation also under-liquidates (TVL still inflated) c. Bad debt compounds d. Actual collateralization degrades e. More positions become liquidatable f. Cycle repeats
Consequences:
Liquidators seize less collateral than required to restore health
Individual accounts left with debt > collateral value
Protocol accumulates systemic bad debt
Even proper liquidations can't fix earlier under-liquidations
Point of no return reached quickly
False Risk Metrics and Failed Monitoring (Medium)
Mechanism:
Operators and automated systems call
getTotalUnderlyingValue()Dashboards show healthy collateralization ratios
Alert thresholds never trigger
Governance believes system is functioning normally
Consequences:
No early warning of insolvency
Risk management decisions based on false data
Emergency response delayed until user funds already lost
Reputation damage when "healthy" system suddenly fails
Regulatory and legal exposure from misleading metrics
References
Primary Source Code:
src/AlchemistV3.sol#L134— Declaration of_mytSharesDepositedstate variablesrc/AlchemistV3.sol#L363-L389—deposit()function correctly incrementing countersrc/AlchemistV3.sol#L391-L418—withdraw()function correctly decrementing countersrc/AlchemistV3.sol#L851-L914—_doLiquidation()function with missing decrementsrc/AlchemistV3.sol#L1239-L1242—_getTotalUnderlyingValue()trusting stale countersrc/AlchemistV3.sol#L869-L876—calculateLiquidation()call site using inflated TVL
Proof of Concept
Proof of Concept## Proof of Concept
// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.28;
import {AlchemistV3Test} from "./AlchemistV3.t.sol"; import {IERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IVaultV2} from "../../lib/vault-v2/src/interfaces/IVaultV2.sol"; import {IMockYieldToken} from "./mocks/MockYieldToken.sol"; import {SafeERC20} from "../libraries/SafeERC20.sol"; import {AlchemistNFTHelper} from "./libraries/AlchemistNFTHelper.sol";
contract AlchemistV3TVLInflationPoC is AlchemistV3Test { /// @dev Demonstrates that _mytSharesDeposited is not decremented when liquidations move /// MYT shares out of the contract, causing the reported TVL to diverge from reality. function testReportedTVLInflatedAfterLiquidation() external { // Prime the mock yield token so the vault has ample supply. vm.startPrank(someWhale); IMockYieldToken(mockStrategyYieldToken).mint(whaleSupply, someWhale); vm.stopPrank();
} //Deposit-cap DoS: once a few liquidations happen the cap stays “full,” so fresh deposits or strategy rotations just brick. //Under-liquidation & systemic insolvency: calculateLiquidation leans on the inflated TVL when deciding whether to escalate to full liquidations. The system ends up believing it’s healthy while the real collateral is gone, leaving bad debt to accumulate. //Operators flying blind: every TVL-derived metric is wrong, so risk dashboards and on-chain checks miss the insolvency until redemptions start failing.
Was this helpful?