58471 sc high accounting error in forcerepay doliquidation overstates tvl enabling under scaled redemptions and potential insolvency
Submitted on Nov 2nd 2025 at 14:40:22 UTC by @winnerz for Audit Comp | Alchemix V3
Report ID: #58471
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
When _forceRepay and liquidation _doLiquidation transfer MYT out of the Alchemist, the contract does not decrement _mytSharesDeposited. Since getTotalUnderlyingValue() derives TVL from _mytSharesDeposited, reported TVL remains inflated while actual holdings drop. This depresses badDebtRatio used by the Transmuter and can delay or under‑apply scaling of redemptions, creating a path to protocol insolvency. It can also soften liquidation sizing by overstating global collateralization inputs.
Vulnerability Details
TVL is derived from _mytSharesDeposited:
// src/AlchemistV3.sol: 1238-1241
function _getTotalUnderlyingValue() internal view returns (uint256 totalUnderlyingValue) {
uint256 yieldTokenTVLInUnderlying = convertYieldTokensToUnderlying(_mytSharesDeposited);
totalUnderlyingValue = yieldTokenTVLInUnderlying;
}In _forceRepay, MYT is transferred out but _mytSharesDeposited is not decremented, so reported TVL does not change:
Similarly, in _doLiquidation, MYT leaves the contract without decreasing _mytSharesDeposited:
By contrast, other outflow paths do reduce _mytSharesDeposited:
Transmuter claimRedemption() relies on the Alchemist TVL in its denominator; an inflated TVL depresses this ratio and delays scaling:
flow-of-funds
_mytSharesDepositedintent (state):
Why
repay()only decrements fees: the principal is transferred from the payer directly to the Transmuter (Alchemist never increases its MYT), so only the fee portion is an Alchemist outflow that must reduce_mytSharesDeposited.In
_forceRepayand_doLiquidation, the principal leaves from the Alchemist balance itself, so_mytSharesDepositedmust be decreased by what is sent out (plus any fee outflows) to keep TVL honest.
Effect on the Transmuter ratio
Let
den_used = TVL_used + transmuterUnderlyingandden_true = TVL_true + transmuterUnderlying.Since TVL_used > TVL_true after drift,
den_used > den_truewhich impliesusedR = totalSyntheticsIssued / den_used < trueR = totalSyntheticsIssued / den_true.Scaling in
claimRedemption()usesmin(1, 1/ratio). IftrueR > 1butusedR <= 1, the claim is paid unscaled when it should be scaled. If both exceed 1, the paid amount under the used ratio is larger than under the true ratio by a factortrueR/usedR > 1.
The drift persists until a path that decrements _mytSharesDeposited runs (withdraw or redeem or the fee leg of repay). Multiple _forceRepay/liquidation events can accumulate drift across blocks, affecting all subsequent redemptions and liquidation decisions protocol-wide.
Why this matters:
getTotalUnderlyingValue()uses_mytSharesDepositedto compute TVL. Omitting decrements during_forceRepay/liquidation keeps reported TVL unchanged even though MYT actually left the protocol.Transmuter
claimRedemption()computes abadDebtRatiousinggetTotalUnderlyingValue(). Inflated TVL depresses this ratio (usedR < trueR), delaying or under‑applying scaling when bad debt exists. This can systematically overpay claims and lead to insolvency.Liquidation sizing (
calculateLiquidation) that takes global collateralization as an input can be influenced to under‑liquidate due to the overstated TVL.
Impact Details
Impact: Protocol insolvency
Rationale:
Inflated TVL depresses
badDebtRatioand reduces/delays redemption scaling, overpaying claimants when bad debt exists.Over time, this can drain reserves and result in protocol insolvency.
Secondary: Under-liquidation due to overstated global collateralization inputs.
References
_forceRepayMYT outflow without_mytSharesDepositeddecrement: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L738_doLiquidationMYT outflow without_mytSharesDepositeddecrement: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L867withdraw()correct decrement: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L410redeem()correct decrement: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L638repay()fee decrement (principal comes from payer, not Alchemist): https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L541
Mitigation
In
_forceRepay, decrement_mytSharesDepositedby the MYT actually sent out:Subtract
creditToYieldandprotocolFeeTotalwhen they are transferred to Transmuter andprotocolFeeReceiverrespectively.
In
_doLiquidation, decrement_mytSharesDepositedby the fullamountLiquidated(the gross MYT outflow), i.e., the portion sent to Transmuter and the fee to the liquidator.
Proof of Concept
Proof of Concept
Set-up
Foundry; solc 0.8.28; EVM cancun. No RPC/fork required.
Command
Work-flow
PoC 1 - TVL overstatement after _forceRepay/liquidation
Create a position: deposit 20,000 MYT shares and mint 12,000 debt.
Create a matured redemption (1,000 debt) and
poke()the position to earmark.Tighten collateralization bounds to 2.0 so liquidation executes.
Record
reportedBefore = getTotalUnderlyingValue()andtrueBefore = convert(MYT balanceOf(Alchemist))in debt units.Call
liquidate(tokenId); record Transmuter MYT balance delta andyPaid.Record
reportedAfterandtrueAfter.Assertions:
reportedAfter == reportedBefore,trueAfter < trueBefore,drift = reportedAfter - trueAfter > 0.
PoC 2 - Depressed ratio delays scaling in claimRedemption()
Set Transmuter fee to 0 for simple accounting.
Induce drift as in PoC 1 (create position, earmark, liquidate).
Compute two denominators for the ratio:
den_used = getTotalUnderlyingValue() + convert(transmuterShares)andden_true = trueTVL + convert(transmuterShares).Compute
usedR = totalSyntheticsIssued / den_usedandtrueR = totalSyntheticsIssued / den_true; verifyusedR < trueR.Create a small matured redemption (500 debt) and ensure Transmuter already holds enough MYT to pay it (no
redeem()call).Claim and measure
debtClaimedfrom the user’s MYT delta converted back to debt units.Expected in healthy conditions:
expectedDebtTrue = 500(no scaling). AssertdebtClaimed == expectedDebtTruewhileusedR < trueR, showing scaling activation is delayed by the inflated denominator.
Supporting observation - under‑liquidation at the decision boundary
After drift, compute global ratios
usedGlobalM(withgetTotalUnderlyingValue()) andtrueGlobalM(with actual MYT balance).Set
globalMinimumCollateralizationto a midpoint between them.Call
calculateLiquidation(...)twice, once withusedGlobalMand once withtrueGlobalM.Expect
debtBurnUsed < accountDebtwhiledebtBurnTrue == accountDebt, demonstrating that inflated TVL can reduce liquidation severity.
Output
PoC 1: TVL overstatement after _forceRepay/liquidation
Asserts reported TVL (via
_mytSharesDeposited) stays constant while true MYT holdings decrease; prints a positive “tvl drift”.
PoC 2: Depressed ratio delays scaling in claimRedemption()
Shows
usedR < trueRafter drift. A small claim remains unscaled, evidencing that the inflated denominator depresses the ratio and delays scaling activation.
Supporting observation
Demonstrates that liquidation sizing based on an inflated global collateralization can under‑liquidate compared to the “true” input computed from actual MYT holdings.
Was this helpful?