56678 sc high missing internal myt shares accounting in liquidation functions causes deposit blocking and protocol insolvency risk through inflated tvl calculations
Submitted on Oct 19th 2025 at 09:54:59 UTC by @godwinudo for Audit Comp | Alchemix V3
Report ID: #56678
Report Type: Smart Contract
Report severity: High
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol
Impacts:
Protocol insolvency
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The AlchemistV3.sol contract maintains an internal accounting variable _mytSharesDeposited to track total MYT shares deposited as collateral and enforce deposit caps. During liquidations, MYT tokens are transferred out of the contract to the transmuter and liquidators, but _mytSharesDeposited is never decremented. This causes the internal tracking to become permanently inflated relative to the actual MYT balance. The inflated tracking blocks legitimate deposits prematurely and, more critically, inflates the Total Value Locked (TVL) calculations used in liquidation logic, making the protocol appear healthier than reality. This can prevent full liquidations during some scenarios, allowing bad debt to accumulate and potentially leading to protocol insolvency.
Vulnerability Details
The AlchemistV3 contract declares a private state variable that tracks the total MYT shares held as user collateral:
/// @dev Total yield tokens deposited
/// This is used to differentiate between tokens deposited into a CDP and balance of the contract
uint256 private _mytSharesDeposited;This variable serves two critical purposes in the protocol. First, it enforces the deposit cap to prevent the contract from accepting more collateral than intended. Second, it calculates the total value locked in the protocol, which directly influences liquidation decisions about whether positions should be partially or fully liquidated.
To understand the issue, we need to first see how the accounting should work. Let me show you the deposit function where _mytSharesDeposited is correctly incremented:
Notice how after the MYT tokens are transferred into the contract, the code increments _mytSharesDeposited by the same amount. This maintains perfect synchronization between the actual token balance and the internal accounting.
Now let me show you the withdrawal function where the accounting is correctly decremented:
After transferring MYT tokens out of the contract to the recipient, the code decrements _mytSharesDeposited by that same amount. The internal ledger stays perfectly synchronized with reality.
Now let me show you where the accounting breaks down. The liquidation process has two internal functions that transfer MYT tokens out of the contract but completely fail to update _mytSharesDeposited. First, let's look at the _forceRepay function, which is called during liquidations to handle early repayment of earmarked debt:
Look carefully at those two TokenUtils.safeTransfer calls. The function sends protocolFeeTotal amount of MYT to the protocol fee receiver and creditToYield amount to the transmuter. These are real token transfers that permanently remove MYT from the AlchemistV3 contract's balance. However, there is no corresponding _mytSharesDeposited -= ... statement after either transfer. The internal accounting variable still believes these tokens are in the contract when they are not.
The second location where this bug occurs is in the _doLiquidation function, which handles the actual collateral seizure during liquidations:
Again, look at the two TokenUtils.safeTransfer calls that send MYT tokens. The function transfers amountLiquidated - feeInYield to the transmuter and feeInYield to the liquidator. These are real token movements that reduce the contract's actual MYT balance, but there is no _mytSharesDeposited decrement anywhere in this function. The internal ledger remains unchanged even though tokens have permanently left the contract.
To understand when this bug manifests, we need to look at when liquidations occur. The public liquidate function is the entry point:
This calls the internal _liquidate function, which determines whether a position is eligible for liquidation and handles the multi-step process:
The liquidation process first attempts to repay any earmarked debt through _forceRepay, and if the position is still undercollateralized after that, it proceeds to the actual liquidation through _doLiquidation. Both of these functions, as we have seen, transfer MYT tokens out without updating _mytSharesDeposited.
Impact Details
First, it affects the deposit cap enforcement. Deposits are incorrectly rejected because the internal accounting is out of sync with reality.
This function uses the inflated _mytSharesDeposited value to calculate the total underlying value of all collateral in the protocol. Because _mytSharesDeposited was not decremented during liquidation, this function returns a value that is higher than the actual collateral value.
This inflated TVL is then used in the liquidation logic:
The fourth parameter to calculateLiquidation is the protocol-wide collateralization ratio, which is calculated by dividing the total underlying value (from _getTotalUnderlyingValue) by the total debt. Because the TVL is inflated, this ratio is artificially high. Now let's look at what calculateLiquidation does with this value:
Look at the second if statement. When the protocol-wide collateralization ratio is below the minimum threshold, the function returns a full liquidation where all the debt is burned and the outsourced fee is charged. This is the emergency mode that is supposed to protect the protocol when it is in danger of insolvency. However, because alchemistCurrentCollateralization is calculated using the inflated TVL, this condition will evaluate to false even when the real collateralization is dangerously low. The protocol will proceed to a partial liquidation when it should be doing a full liquidation, leaving bad debt in the system.
Proof of Concept
Proof of Concept
Add this to the AlchemistV3.t.sol test contract and run
Was this helpful?