57973 sc critical repay doesnt set lasttransmutertokenbalance leading to the same balance covering earmark twice

Submitted on Oct 29th 2025 at 18:20:24 UTC by @silver_eth for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57973

  • Report Type: Smart Contract

  • Report severity: Critical

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

  • Impacts:

    • Permanent freezing of unclaimed yield

    • Permanent freezing of unclaimed royalties

Description

Brief/Intro

A core invariant of the system could be defined as transmutted debt might either be included in earmark or there must be subsequent collateral backing it already in the transmutter contract any break in this invariant would constitute an issue any contract that sends collateral into the transmuter and updates earmarked based on this collateral without setting lastTransmuterTokenBalance breaks this invariant

Vulnerability Details

one of the ways this invariant breaks that occurs in both repay and _forceRepay is that the functions decrease earmark (using the funds to cover current earmark) but doesnt set lastTransmuterTokenBalance (meaning those same funds can be used to cover future transmutations when _earmark is called next )

the example described in the poc demonstrates this

  1. bob completes a borrow

  2. bob transmutes some debt

  3. after half the time, half of the original redemption amount has been transmuted

  4. bob repays part of his debt which clears previous earmark (collateral now backs previous earmark)

  5. the other half of the transmutation time passes

  6. bobs position is poked and _earmark is called, it sees the collateral that is in the transmuter and uses it to back all the newly transmuted debt meaning necessary state updates dont happen

  7. bob claims his redemption but only claims half the the total value of the deb the transmutted

  8. the remaining half of the transmuted debt is also burned without clearing any debt, confirmed by the last assertion which could be counted as either a loss for the redemption owner or protocol

Impact Details

direct and complete loss of both some collateral and debt tokens belonging to redeemers

References

https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L526-L539 https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L1103-L1110

Proof of Concept

Proof of Concept

// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.28;

import {IERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

import {TransparentUpgradeableProxy} from "../../lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {SafeCast} from "../libraries/SafeCast.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; import {SafeERC20} from "../libraries/SafeERC20.sol"; import {console} from "../../lib/forge-std/src/console.sol"; import {AlchemistV3} from "../AlchemistV3.sol"; import {AlchemicTokenV3} from "../test/mocks/AlchemicTokenV3.sol"; import {Transmuter} from "../Transmuter.sol"; import {AlchemistV3Position} from "../AlchemistV3Position.sol";

import {Whitelist} from "../utils/Whitelist.sol"; import {TestERC20} from "./mocks/TestERC20.sol"; import {TestYieldToken} from "./mocks/TestYieldToken.sol"; import {TokenAdapterMock} from "./mocks/TokenAdapterMock.sol"; import {IAlchemistV3, IAlchemistV3Errors, AlchemistInitializationParams} from "../interfaces/IAlchemistV3.sol"; import {ITransmuter} from "../interfaces/ITransmuter.sol"; import {ITestYieldToken} from "../interfaces/test/ITestYieldToken.sol"; import {InsufficientAllowance} from "../base/Errors.sol"; import {Unauthorized, IllegalArgument, IllegalState, MissingInputData} from "../base/Errors.sol"; import {AlchemistNFTHelper} from "./libraries/AlchemistNFTHelper.sol"; import {IAlchemistV3Position} from "../interfaces/IAlchemistV3Position.sol"; import {AggregatorV3Interface} from "../../lib/chainlink-brownie-contracts/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; import {TokenUtils} from "../libraries/TokenUtils.sol"; import {AlchemistTokenVault} from "../AlchemistTokenVault.sol"; import {MockMYTStrategy} from "./mocks/MockMYTStrategy.sol"; import {MYTTestHelper} from "./libraries/MYTTestHelper.sol"; import {IMYTStrategy} from "../interfaces/IMYTStrategy.sol"; import {MockAlchemistAllocator} from "./mocks/MockAlchemistAllocator.sol"; import {IMockYieldToken} from "./mocks/MockYieldToken.sol"; import {IVaultV2} from "../../lib/vault-v2/src/interfaces/IVaultV2.sol"; import {VaultV2} from "../../lib/vault-v2/src/VaultV2.sol"; import {MockYieldToken} from "./mocks/MockYieldToken.sol";

contract SilverAlchemistTest is Test { // ----- [SETUP] Variables for setting up a minimal CDP -----

}

Was this helpful?