56960 sc medium missing slippage protection during redemption execution lead to loss of token for user

Submitted on Oct 22nd 2025 at 08:18:13 UTC by @aua_oo7 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #56960

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/Transmuter.sol

  • Impacts:

    • Permanent freezing of funds

Description

Summary:

The claimRedemption() function lacks slippage protection when applying the badDebtRatio, exposing users to unexpected redemption value losses if the protocol’s collateral ratio worsens before redemption execution.

Description:

When users call createRedemption(uint256 syntheticDepositAmount), the syntheticDepositAmount is amount that user want to change to myt token(yield), user redemption is scheduled based on system parameters at that time block.number + timeToTransmute. However, the actual redemption value is determined later in claimRedemption() function call when user call it. inside the claimRedemption() the amountTransmuted is the amount that will be transmute and send as myt token to user, the function also computes a dynamic badDebtRatio using the following formula:


    function claimRedemption(uint256 id) external {
        StakingPosition storage position = _positions[id];

        if (position.maturationBlock == 0) {
            revert PositionNotFound();
        }

        if (position.startBlock == block.number) {
            revert PrematureClaim();
        }

        uint256 transmutationTime = position.maturationBlock - position.startBlock;
        uint256 blocksLeft = position.maturationBlock > block.number ? position.maturationBlock - block.number : 0;
        uint256 rounded = position.amount * blocksLeft / transmutationTime + (position.amount * blocksLeft % transmutationTime == 0 ? 0 : 1);
        uint256 amountNottransmuted = blocksLeft > 0 ? rounded : 0;
        uint256 amountTransmuted = position.amount - amountNottransmuted;

        if (_requireOwned(id) != msg.sender) {
            revert CallerNotOwner();
        }

        // Burn position NFT
        _burn(id);
        
        // Ratio of total synthetics issued by the alchemist / underlingying value of collateral stored in the alchemist
        // If the system experiences bad debt we use this ratio to scale back the value of yield tokens that are transmuted
        uint256 yieldTokenBalance = TokenUtils.safeBalanceOf(alchemist.myt(), address(this));
        // Avoid divide by 0
        uint256 denominator = alchemist.getTotalUnderlyingValue() + alchemist.convertYieldTokensToUnderlying(yieldTokenBalance) > 0 ? alchemist.getTotalUnderlyingValue() + alchemist.convertYieldTokensToUnderlying(yieldTokenBalance) : 1;

    @>    uint256 badDebtRatio = alchemist.totalSyntheticsIssued() * 10**TokenUtils.expectDecimals(alchemist.underlyingToken()) / denominator;

    @>    uint256 scaledTransmuted = amountTransmuted;
    @>    if (badDebtRatio > 1e18) {
            scaledTransmuted = amountTransmuted * FIXED_POINT_SCALAR / badDebtRatio;
        }

if badDebtRatio get bigger than 1e18(1:1 ratio of synthetic to Myt token break, mean more synthetic token exist in alchemist then underlying token) then the amountTransumted should divide by badDebtRation which lead to decrease the actual transmute amount user set and want. now after calculating badDebt scaling a portion from user transmuted amount, this portion is reduced from user amountTransmuted and scaledTransmuted which is transmuteAmount - portion will transfer to user, mean problem is that the claimRedemption function burn the amountTransmuted which is the hole amount user request for:

at the end we see the amountTransmuted is burn for user instead of scaledTransmuted. user will loss that scale reduce amount forever and user will face with loss. becouse user will receive scaledTransmuted amount as myt token instead of amountTransumted amount, but burn is vice versa in value. If between creation and redemption the badDebtRatio rises (due to system bad debt or synthetic inflation) which is not fault of user who request claim, the user’s redemption value can be drastically reduced without prior notice. There is no mechanism allowing users to define acceptable slippage bounds or to abort under unfavorable conditions.

Impact:

Users face unexpected synthetic token loss and burn forever and unfair redemption outcomes, as their expected transmutation output can significantly drop due to system-level ratio changes unrelated to their position.

Mitigation:

Implement a minimum acceptable output (slippage protection) mechanism during redemption execution. Example approach:

Proof of Concept

Proof of Concept

add the following function into Transmuter.t.sol test file and run the test.

Was this helpful?