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
bob completes a borrow
bob transmutes some debt
after half the time, half of the original redemption amount has been transmuted
bob repays part of his debt which clears previous earmark (collateral now backs previous earmark)
the other half of the transmutation time passes
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
bob claims his redemption but only claims half the the total value of the deb the transmutted
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
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 -----