58347 sc high accounting drift due to missing mytsharesdeposited decrements during liquidation

Submitted on Nov 1st 2025 at 12:50:44 UTC by @InquisitorScythe for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58347

  • 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 fails to decrement the _mytSharesDeposited state variable when transferring MYT tokens during liquidation operations. This causes the recorded total value locked (TVL) to diverge from the actual vault balance, making the TVL inflated. Since subsequent liquidation strategy calculations depend on accurate system-wide TVL metrics, this accounting drift can lead to incorrect liquidation decisions that fail to restore collateralization properly, ultimately causing cascading defaults and bad debt accumulation across the protocol.

Vulnerability Details

In the AlchemistV3 protocol, when borrowers are liquidated, the contract transfers MYT (Alchemix Yield Token) from the alchemist contract to either the transmuter or the liquidator. However, the code fails to decrement the _mytSharesDeposited state variable that tracks how much MYT the protocol has deposited. This creates an accounting inconsistency where _getTotalUnderlyingValue() returns a figure larger than the actual MYT balance held by the contract.

The bug manifests in three code paths within the liquidation logic: the primary liquidation flow in _liquidate(), the main liquidation calculation in _doLiquidation(), and the forced repayment path in _forceRepay(). In all three cases, when TokenUtils.safeTransfer(myt, ...) is called to move tokens out of the alchemist contract, the corresponding _mytSharesDeposited -= amount; statement is missing.

The _getTotalUnderlyingValue() function calculates TVL by converting the recorded _mytSharesDeposited to underlying tokens:

When liquidation occurs, tokens are transferred out but _mytSharesDeposited is never decremented. This means the recorded state still counts these transferred tokens as belonging to the protocol, even though they no longer exist in the contract.

The affected transfer calls are:

  1. In _liquidate() at line 828: TokenUtils.safeTransfer(myt, msg.sender, feeInYield); - transfers repayment fee to liquidator

  2. In _doLiquidation() at lines 879-880: TokenUtils.safeTransfer(myt, transmuter, amountLiquidated - feeInYield); and TokenUtils.safeTransfer(myt, msg.sender, feeInYield); - transfers liquidated amount to transmuter and fee to liquidator

  3. In _forceRepay() at lines 777-778: TokenUtils.safeTransfer(myt, protocolFeeReceiver, protocolFeeTotal); and TokenUtils.safeTransfer(myt, address(transmuter), creditToYield); - transfers protocol fees and repaid debt

The accounting drift created by this bug affects downstream protocol mechanics through the calculateLiquidation() function. This function takes the system-wide collateralization ratio as a parameter:

Because _getTotalUnderlyingValue() returns an inflated value, the system appears more collateralized than it actually is. When subsequent liquidations of other borrowers are calculated, the liquidation algorithm receives this false system health metric and may decide to liquidate less debt than necessary, leaving undercollateralized positions unfixed.

For example, if after a liquidation the true system TVL is 3.7 billion tokens but _getTotalUnderlyingValue() returns 3.78 billion tokens due to the 8.67 million token drift, the system appears 0.2% healthier than reality. A borrower on the liquidation edge who should be fully liquidated might instead be only partially liquidated based on the inflated metrics, leaving residual debt that cannot be sufficiently secured.

Unlike a one-time loss vulnerability, this bug accumulates with each liquidation. If borrower A is liquidated with a 10M token drift, and then borrower B is liquidated with another 10M token drift, the total unaccounted TVL becomes 20M. Each liquidation compounds the accounting error, creating a growing wedge between actual protocol collateral and recorded collateral. This makes the system increasingly vulnerable to subsequent market downturns and can trigger cascading liquidations that the protocol cannot properly handle.

Impact Details

The vulnerability directly impacts protocol solvency by creating systematic underestimation of risk. As the accounting drift accumulates across multiple liquidations, the protocol's ability to assess and manage collateralization ratios degrades. This has several concrete consequences:

First, liquidation strategy failures emerge. When the next undercollateralized borrower enters liquidation after the drift exists, the liquidation calculation receives inflated system collateralization data. This causes the protocol to calculate smaller liquidation amounts than necessary to restore the borrower to the minimum collateralization threshold. The borrower remains partially undercollateralized even after liquidation.

Second, bad debt accumulates when borrowers cannot be properly liquidated. If a liquidation is insufficient due to using inflated TVL metrics, the remaining debt persists. If the asset price continues to decline, the borrower may become severely undercollateralized or completely insolvent, with debt exceeding collateral value. This bad debt must be absorbed by the protocol or remaining depositors.

Third, protocol insolvency becomes possible at scale. With each liquidation introducing drift, and each drift making subsequent liquidations less effective, the protocol enters a vicious cycle. Under market stress with multiple liquidations in succession, the accumulated drift could cause the protocol to be unable to maintain sufficient collateralization of its total debt, resulting in either frozen funds or insolvency.

Quantified Impact

From the POC test execution:

  • Single liquidation transfer: 109M MYT

  • Amount recorded as deducted from TVL: 0 MYT

  • Actual drift created: 109M MYT (100% of transfer undecrmented)

  • System collateralization inflation: from 2.098x to 4.885x (2.78x increase)

  • False system health improvement: 132% higher collateralization than reality

This means after a single liquidation, if the system needed to liquidate another borrower, it would believe the system is 2.78 times more collateralized than it actually is, leading to significantly undersized liquidations.

References

  • File: src/AlchemistV3.sol

  • Functions affected: _liquidate() (lines 791-846), _doLiquidation() (lines 851-916), _forceRepay() (lines 738-788)

  • TVL calculation function: _getTotalUnderlyingValue() (lines 1238-1242)

  • Liquidation strategy function: calculateLiquidation() (lines 1244+)

Proof of Concept

Proof of Concept

create src/test/LiquidationBugPOC.t.sol

run forge test --match-path ./src/test/LiquidationBugPOC.t.sol to see two failing tests.

explain:

Test 1: Direct Drift Detection

The first POC demonstrates the most direct evidence of the bug. It creates two positions: one health deposit to maintain system collateralization, and one overleveraged position to liquidate. After triggering undercollateralization via a simulated price drop and executing liquidation, it compares the actual vault balance with the recorded TVL.

Expected behavior: vault.balanceOf(alchemist) should equal getTotalUnderlyingValue()

Actual behavior: getTotalUnderlyingValue() exceeds vault balance by 8.67M MYT

Console output:

Test 2: Impact on Liquidation Strategy

The second POC demonstrates how this accounting drift directly affects liquidation strategy calculations. It records the system's collateralization ratio before liquidation (when TVL is accurate), then records it again after liquidation (when TVL is inflated by the drift), showing the system appears significantly healthier than it actually is.

Expected behavior: System collateralization should remain constant or decrease after liquidation

Actual behavior: System collateralization increases by 2.78x due to inflated TVL

Console output:

This demonstrates the concrete impact: if another borrower were to be liquidated based on the post-liquidation metrics, the system would believe collateralization is 4.88x when it's actually 2.10x, leading to under-liquidation.

Was this helpful?