57544 sc high mytsharesdeposited is not reduced upon fee transfers to protocol

Submitted on Oct 27th 2025 at 05:21:41 UTC by @X0sauce for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57544

  • 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

Vulnerability Details

We observe that during liquidations, the protocol would receive MYT yield tokens base on the amount of earmarked debt repaid in _forceRepay and the liquidator will receive a repayment fee base on how much yield token was used to repay the user's earmarked debt position.

However, the protocol in fact does not reduce the total _mytSharesDeposited by depositors when the fee transfers happens, which results in overstating of the actual amount of _mytSharesDeposited in the contract.

    function _liquidate(uint256 accountId) internal returns (uint256 amountLiquidated, uint256 feeInYield, uint256 feeInUnderlying) {
        // Query transmuter and earmark global debt
        _earmark();
        // Sync current user debt before deciding how much needs to be liquidated
        _sync(accountId);

        Account storage account = _accounts[accountId];

        // Early return if no debt exists
        if (account.debt == 0) {
            return (0, 0, 0);
        }

        // In the rare scenario where 1 share is worth 0 underlying asset
        if (IVaultV2(myt).convertToAssets(1e18) == 0) {
            return (0, 0, 0);
        }

        // Calculate initial collateralization ratio
        uint256 collateralInUnderlying = totalValue(accountId);
        uint256 collateralizationRatio = collateralInUnderlying * FIXED_POINT_SCALAR / account.debt;

        // If account is healthy, nothing to liquidate
        if (collateralizationRatio > collateralizationLowerBound) {
            return (0, 0, 0);
        }

        // Try to repay earmarked debt if it exists
        uint256 repaidAmountInYield = 0;
        if (account.earmarked > 0) {
            repaidAmountInYield = _forceRepay(accountId, account.earmarked);
        }
        // If debt is fully cleared, return with only the repaid amount, no liquidation needed, caller receives repayment fee
        if (account.debt == 0) {
@>            feeInYield = _resolveRepaymentFee(accountId, repaidAmountInYield);
            TokenUtils.safeTransfer(myt, msg.sender, feeInYield);
            return (repaidAmountInYield, feeInYield, 0);
        }

        // Recalculate ratio after any repayment to determine if further liquidation is needed
        collateralInUnderlying = totalValue(accountId);
        collateralizationRatio = collateralInUnderlying * FIXED_POINT_SCALAR / account.debt;

        if (collateralizationRatio <= collateralizationLowerBound) {
            // Do actual liquidation
            return _doLiquidation(accountId, collateralInUnderlying, repaidAmountInYield);
        } else {
            // Since only a repayment happened, send repayment fee to caller
@>            feeInYield = _resolveRepaymentFee(accountId, repaidAmountInYield);
            TokenUtils.safeTransfer(myt, msg.sender, feeInYield);
            return (repaidAmountInYield, feeInYield, 0);
        }
    }

This can result in wrong accounting of _mytSharesDeposited tracked, and one of the impacts is that during liquidations of underwater positions, overstating the amount of _mytSharesDeposited can result in the subsequent overstating of the current global collaterization. Then the actual debt calculated to be reduced in AlchemistV3::calculateLiquidation will also be lower since the protocol assumes that alchemistCurrentCollateralization > alchemistMinimumCollateralization.

alchemistCurrentCollateralization is the collaterization calculated using debt token value of _mytSharesDeposited

Protocol insolvency can happen, since bad debt can happen and user debt is not rightfully reduced from the user's position in accordance to global collaterization ratios

Proof of Concept

POC

From below POC, we observe that the amount of debt that was reduce (178324273236511862925 - 175195694050991501434) in totalDebt is lot lesser than the intended amount to be reduced. If multiple liquidations happen and _mytSharesDeposited is not corectly reduced, it can cause protocol to incur more debt than intended since the _mytSharesDeposited was overstated (resulting in global collaterization being overstated) and full debt position of user is not liquidated in accordance to the global collaterization set.

  1. Create a new file called AlchemistV3POC.t.sol

  2. Run the below two commands

test_Liquidate_mytSharesDepositedDecremented output

test_Liquidate_mytSharesDepositedNotDecremented output

Was this helpful?