Copy // SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {AlchemistV3} from "../src/AlchemistV3.sol";
import {AlchemistV3Position} from "../src/AlchemistV3Position.sol";
import {AlchemistInitializationParams, IAlchemistV3} from "../src/interfaces/IAlchemistV3.sol";
import {ITransmuter} from "../src/interfaces/ITransmuter.sol";
import {TokenUtils} from "../src/libraries/TokenUtils.sol";
import {Transmuter} from "../src/Transmuter.sol";
import {AlchemicTokenV3} from "../src/test/mocks/AlchemicTokenV3.sol";
import {TestERC20} from "../src/test/mocks/TestERC20.sol";
import {MockYieldToken} from "../src/test/mocks/MockYieldToken.sol";
import {MockMYTStrategy} from "../src/test/mocks/MockMYTStrategy.sol";
import {MockAlchemistAllocator} from "../src/test/mocks/MockAlchemistAllocator.sol";
import {MockMYTVault} from "../src/test/mocks/MockMYTVault.sol";
import {MYTTestHelper} from "../src/test/libraries/MYTTestHelper.sol";
import {IMYTStrategy} from "../src/interfaces/IMYTStrategy.sol";
import {EulerUSDCAdapter} from "../src/adapters/EulerUSDCAdapter.sol";
import {AlchemistTokenVault} from "../src/AlchemistTokenVault.sol";
/// @notice High-fidelity PoC using proxy deployment and real MYT vault/adapter plumbing.
contract AlchemistV3CollateralizationLowerBoundProxyTest is Test {
uint256 constant FIXED_POINT_SCALAR = 1e18;
uint256 constant INITIAL_MIN_COLLATERAL = 1_500_000_000_000_000_000; // 150%
uint256 constant INITIAL_LOWER_BOUND = 1_100_000_000_000_000_000; // 110%
uint256 constant NEW_MIN_COLLATERAL = 1_050_000_000_000_000_000; // 105%
uint256 constant TARGET_RATIO = 1_090_000_000_000_000_000; // 109%
uint256 constant STRATEGY_ABSOLUTE_CAP = 2_000_000_000e18;
address constant ADMIN = address(0x4444);
address constant CURATOR = address(0x8888);
address constant OPERATOR = address(0x2222);
address constant GOVERNANCE = address(0xDEAD);
address constant DEPOSITOR = address(0xAA11);
address constant LIQUIDATOR = address(0xD00D);
AlchemistV3 alchemist;
AlchemistV3Position alchemistNFT;
Transmuter transmuterLogic;
AlchemicTokenV3 alToken;
TransparentUpgradeableProxy proxyAlchemist;
MockMYTVault vault;
MockMYTStrategy mytStrategy;
MockAlchemistAllocator allocator;
MockYieldToken yieldToken;
TestERC20 underlying;
EulerUSDCAdapter adapter;
AlchemistTokenVault feeVault;
address externalUser = address(0xBEEF);
function setUp() public {
// Underlying ERC20 and MYT setup
underlying = new TestERC20(0, 18);
yieldToken = new MockYieldToken(address(underlying));
vm.startPrank(ADMIN);
vault = MYTTestHelper._setupVault(address(underlying), ADMIN, CURATOR);
mytStrategy = MYTTestHelper._setupStrategy(address(vault), address(yieldToken), ADMIN, "MockToken", "MockTokenProtocol", IMYTStrategy.RiskClass.LOW);
allocator = new MockAlchemistAllocator(address(vault), ADMIN, OPERATOR);
vm.stopPrank();
vm.startPrank(CURATOR);
_vaultSubmitAndFastForward(abi.encodeCall(vault.setIsAllocator, (address(allocator), true)));
vault.setIsAllocator(address(allocator), true);
_vaultSubmitAndFastForward(abi.encodeCall(vault.addAdapter, address(mytStrategy)));
vault.addAdapter(address(mytStrategy));
bytes memory idData = mytStrategy.getIdData();
_vaultSubmitAndFastForward(abi.encodeCall(vault.increaseAbsoluteCap, (idData, STRATEGY_ABSOLUTE_CAP)));
vault.increaseAbsoluteCap(idData, STRATEGY_ABSOLUTE_CAP);
_vaultSubmitAndFastForward(abi.encodeCall(vault.increaseRelativeCap, (idData, FIXED_POINT_SCALAR)));
vault.increaseRelativeCap(idData, FIXED_POINT_SCALAR);
vm.stopPrank();
// Deploy alchemist via proxy with real components
alToken = new AlchemicTokenV3("AlToken", "AL", 0);
transmuterLogic = new Transmuter(ITransmuter.TransmuterInitializationParams({
syntheticToken: address(alToken),
feeReceiver: ADMIN,
timeToTransmute: 10,
transmutationFee: 0,
exitFee: 0,
graphSize: 10
}));
AlchemistInitializationParams memory params = AlchemistInitializationParams({
admin: ADMIN,
debtToken: address(alToken),
underlyingToken: address(vault.asset()),
depositCap: type(uint256).max,
minimumCollateralization: INITIAL_MIN_COLLATERAL,
globalMinimumCollateralization: INITIAL_MIN_COLLATERAL,
collateralizationLowerBound: INITIAL_LOWER_BOUND,
transmuter: address(transmuterLogic),
protocolFee: 0,
protocolFeeReceiver: ADMIN,
liquidatorFee: 300,
repaymentFee: 0,
myt: address(vault)
});
AlchemistV3 alchemistLogic = new AlchemistV3();
proxyAlchemist = new TransparentUpgradeableProxy(address(alchemistLogic), ADMIN, abi.encodeWithSelector(AlchemistV3.initialize.selector, params));
alchemist = AlchemistV3(address(proxyAlchemist));
alToken.setWhitelist(address(alchemist), true);
transmuterLogic.setAlchemist(address(alchemist));
transmuterLogic.setDepositCap(uint256(type(int256).max));
alchemistNFT = new AlchemistV3Position(address(alchemist));
vm.prank(ADMIN);
alchemist.setAlchemistPositionNFT(address(alchemistNFT));
feeVault = new AlchemistTokenVault(address(underlying), address(alchemist), ADMIN);
vm.prank(ADMIN);
alchemist.setAlchemistFeeVault(address(feeVault));
adapter = new EulerUSDCAdapter(address(yieldToken), address(underlying));
vm.prank(ADMIN);
alchemist.setTokenAdapter(address(adapter));
// Seed balances and approvals
// Unique actors:
// - `GOVERNANCE` controls risk parameters (e.g., lowering minimum collateralization)
// - `DEPOSITOR` manages the position and bears the loss
// - `LIQUIDATOR` independently seizes collateral once invariant is broken
deal(address(underlying), DEPOSITOR, 1_000 ether);
deal(address(underlying), externalUser, 1_000 ether);
deal(address(underlying), address(allocator), 1_000 ether);
vm.startPrank(DEPOSITOR);
TokenUtils.safeApprove(address(underlying), address(vault), type(uint256).max);
vault.deposit(500 ether, DEPOSITOR);
vm.stopPrank();
vm.startPrank(externalUser);
TokenUtils.safeApprove(address(underlying), address(vault), type(uint256).max);
vault.deposit(500 ether, externalUser);
vm.stopPrank();
vm.startPrank(address(allocator));
TokenUtils.safeApprove(address(underlying), address(vault), type(uint256).max);
vault.deposit(500 ether, address(allocator));
vm.stopPrank();
vm.startPrank(ADMIN);
allocator.allocate(address(mytStrategy), 500 ether);
vm.stopPrank();
}
function testProxySetupAllowsLiquidationDespiteHigherMinCollateral() public {
vm.startPrank(ADMIN);
alchemist.setPendingAdmin(GOVERNANCE);
vm.stopPrank();
vm.prank(GOVERNANCE);
alchemist.acceptAdmin();
vm.startPrank(DEPOSITOR);
TokenUtils.safeApprove(address(vault), address(alchemist), type(uint256).max);
alchemist.deposit(100 ether, DEPOSITOR, 0);
uint256 tokenId = 1;
emit log("--- Position setup ---");
emit log_named_uint("Initial collateral (MYT shares)", 100 ether);
alchemist.mint(tokenId, 60 ether, DEPOSITOR);
emit log_named_uint("Minted debt (alTokens)", 60 ether);
vm.stopPrank();
vm.prank(GOVERNANCE);
alchemist.setMinimumCollateralization(NEW_MIN_COLLATERAL);
emit log("--- Governance change ---");
emit log_named_uint("Minimum collateralization (new)", NEW_MIN_COLLATERAL);
emit log_named_uint("Collateralization lower bound (unchanged)", alchemist.collateralizationLowerBound());
uint256 collateralBefore = alchemist.totalValue(tokenId);
uint256 targetCollateral = (60 ether * TARGET_RATIO) / FIXED_POINT_SCALAR;
uint256 withdrawAmount = collateralBefore - targetCollateral;
vm.prank(DEPOSITOR);
alchemist.withdraw(withdrawAmount, DEPOSITOR, tokenId);
emit log("--- Depositor rebalances to new minimum ---");
emit log_named_uint("Collateral before withdraw (shares)", collateralBefore);
emit log_named_uint("Target collateral at 109%", targetCollateral);
emit log_named_uint("Collateral withdrawn", withdrawAmount);
(uint256 collateral,,) = alchemist.getCDP(tokenId);
uint256 ratio = collateral * FIXED_POINT_SCALAR / 60 ether;
vm.assertGt(ratio, NEW_MIN_COLLATERAL);
vm.assertLt(ratio, alchemist.collateralizationLowerBound());
emit log_named_uint("Post-withdraw collateral", collateral);
emit log_named_uint("Resulting ratio (1e18 scale)", ratio);
emit log_named_uint("Check: minimum collateralization", NEW_MIN_COLLATERAL);
emit log_named_uint("Check: lower bound threshold", alchemist.collateralizationLowerBound());
vm.startPrank(LIQUIDATOR);
(uint256 amountLiquidated,,) = alchemist.liquidate(tokenId);
vm.stopPrank();
emit log("--- Liquidation ---");
emit log_named_uint("Collateral seized", amountLiquidated);
vm.assertGt(amountLiquidated, 0);
uint256 collateralAfter = alchemist.totalValue(tokenId);
(, uint256 postDebt,) = alchemist.getCDP(tokenId);
vm.assertLt(collateralAfter, collateral);
vm.assertLt(postDebt, 60 ether);
emit log_named_uint("Collateral remaining", collateralAfter);
emit log_named_uint("Debt remaining", postDebt);
}
function _vaultSubmitAndFastForward(bytes memory data) internal {
vault.submit(data);
bytes4 selector = bytes4(data);
vm.warp(block.timestamp + vault.timelock(selector));
}
}