56395 sc high accounting desync in liquidation outflows leads to artificial deposit cap exhaustion and denial of service on recapitalization

Submitted on Oct 15th 2025 at 13:50:07 UTC by @HandsomeEarthworm6 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #56395

  • 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 maintains an internal tracker _mytSharesDeposited to enforce the deposit cap, but fails to decrement it during MYT share outflows in liquidation functions (_forceRepay and _doLiquidation). This creates a desync where the tracker overstates deposited shares, artificially hitting the cap and reverting new deposits with IllegalState even when actual balance has room. In production, during yield token crashes triggering mass liquidations and redemptions, this DoS prevents users from depositing collateral to restore healthy ratios, exacerbating undercollateralization spirals and potentially leading to protocol insolvency without manual admin intervention.

Vulnerability Details

The contract uses getTotalDeposited() for external queries, which correctly returns IERC20(myt).balanceOf(address(this)) to reflect true TVL. However, deposit cap enforcement in deposit() relies on the internal uint256 private _mytSharesDeposited (slot 30), incremented in deposit() (_mytSharesDeposited += amount) and decremented in withdraw() (_mytSharesDeposited -= amount), but ignored during liquidation outflows.Liquidations (via liquidate() → _liquidate() → _forceRepay() or _doLiquidation()) transfer MYT shares out of the contract via TokenUtils.safeTransfer(myt, ...), reducing balanceOf but leaving _mytSharesDeposited stale. Key paths:

1.In _forceRepay() (triggered for earmarked debt during redemptions/liquidations):

if (creditToYield > 0) { // Transfer the repaid tokens from the account to the transmuter. TokenUtils.safeTransfer(myt, address(transmuter), creditToYield); // Outflow: reduces balanceOf // MISSING: _mytSharesDeposited -= creditToYield; @audit } // Fee transfer similarly missing decrement if (account.collateralBalance > protocolFeeTotal) { account.collateralBalance -= protocolFeeTotal; TokenUtils.safeTransfer(myt, protocolFeeReceiver, protocolFeeTotal); // Outflow // MISSING: _mytSharesDeposited -= protocolFeeTotal; @audit }

2.In _doLiquidation() (core liquidation):

// send liquidation amount - fee to transmuter TokenUtils.safeTransfer(myt, transmuter, amountLiquidated - feeInYield); // Outflow // send base fee to liquidator if (feeInYield > 0 && account.collateralBalance >= feeInYield) { TokenUtils.safeTransfer(myt, msg.sender, feeInYield); // Outflow } // MISSING: _mytSharesDeposited -= amountLiquidated; // Total outflow @audit

3.Partial liquidation (repay-only in _liquidate()):

feeInYield = _resolveRepaymentFee(accountId, repaidAmountInYield); TokenUtils.safeTransfer(myt, msg.sender, feeInYield); // Outflow // MISSING: _mytSharesDeposited -= feeInYield; @audit

Redemptions via redeem() correctly decrement (_mytSharesDeposited -= collRedeemed + feeCollateral), but liquidations compound the drift linearly with each event. Yield accrual (vault auto-mints to Alchemist) can create opposite drift (phantom room), but outflows dominate in crises.Deposit check: _checkState(_mytSharesDeposited + amount <= depositCap) uses stale internal, reverting while getTotalDeposited() < depositCap.

Impact Details

Direct: No funds loss—transfers succeed, but deposits revert, denying users the ability to add collateral.

This bug creates a protocol insolvency risk by disabling the recapitalization mechanism during market crashes—the only time mass liquidations occur. The desync compounds exponentially with each liquidation, creating a death spiral where blocked deposits lead to more liquidations. Without manual admin intervention (which may not occur in time during rapid market moves), the protocol can become insolvent with no automated recovery path.

References

n/a

Proof of Concept

Proof of Concept

add the following test code to src/test/AlchemistV3.t.sol ,and then run forge test --match-test test_Issue7 and then check the test output ,check the amount after and before

function test_Issue7_MytSharesDepositedDesyncOnLiquidation() external { // This test demonstrates that liquidations don't update _mytSharesDeposited, // causing a desync that blocks deposits when the protocol needs recapitalization most

}

function test_Issue7_MytSharesDepositedDesync_MultipleScenarios() external { // This test shows the desync in multiple scenarios to prove it's systematic

}

function test_Issue7_DepositCapBypassScenario() external { // This test shows how the desync can lead to deposit cap being artificially reached

}

Was this helpful?