We observe that during liquidations, the protocol would receive MYT yield tokens base on the amount of earmarked debt repaid in _forceRepay and the liquidator will receive a repayment fee base on how much yield token was used to repay the user's earmarked debt position.
However, the protocol in fact does not reduce the total _mytSharesDeposited by depositors when the fee transfers happens, which results in overstating of the actual amount of _mytSharesDeposited in the contract.
function_liquidate(uint256accountId)internalreturns(uint256amountLiquidated,uint256feeInYield,uint256feeInUnderlying){// Query transmuter and earmark global debt_earmark();// Sync current user debt before deciding how much needs to be liquidated_sync(accountId); Account storage account = _accounts[accountId];// Early return if no debt existsif(account.debt ==0){return(0,0,0);}// In the rare scenario where 1 share is worth 0 underlying assetif(IVaultV2(myt).convertToAssets(1e18)==0){return(0,0,0);}// Calculate initial collateralization ratiouint256 collateralInUnderlying =totalValue(accountId);uint256 collateralizationRatio = collateralInUnderlying * FIXED_POINT_SCALAR / account.debt;// If account is healthy, nothing to liquidateif(collateralizationRatio > collateralizationLowerBound){return(0,0,0);}// Try to repay earmarked debt if it existsuint256 repaidAmountInYield =0;if(account.earmarked >0){ repaidAmountInYield =_forceRepay(accountId, account.earmarked);}// If debt is fully cleared, return with only the repaid amount, no liquidation needed, caller receives repayment feeif(account.debt ==0){@> feeInYield =_resolveRepaymentFee(accountId, repaidAmountInYield); TokenUtils.safeTransfer(myt,msg.sender, feeInYield);return(repaidAmountInYield, feeInYield,0);}// Recalculate ratio after any repayment to determine if further liquidation is needed collateralInUnderlying =totalValue(accountId); collateralizationRatio = collateralInUnderlying * FIXED_POINT_SCALAR / account.debt;if(collateralizationRatio <= collateralizationLowerBound){// Do actual liquidationreturn_doLiquidation(accountId, collateralInUnderlying, repaidAmountInYield);}else{// Since only a repayment happened, send repayment fee to caller@> feeInYield =_resolveRepaymentFee(accountId, repaidAmountInYield); TokenUtils.safeTransfer(myt,msg.sender, feeInYield);return(repaidAmountInYield, feeInYield,0);}}
This can result in wrong accounting of _mytSharesDeposited tracked, and one of the impacts is that during liquidations of underwater positions, overstating the amount of _mytSharesDeposited can result in the subsequent overstating of the current global collaterization. Then the actual debt calculated to be reduced in AlchemistV3::calculateLiquidation will also be lower since the protocol assumes that alchemistCurrentCollateralization > alchemistMinimumCollateralization.
alchemistCurrentCollateralization is the collaterization calculated using debt token value of _mytSharesDeposited
Protocol insolvency can happen, since bad debt can happen and user debt is not rightfully reduced from the user's position in accordance to global collaterization ratios
Proof of Concept
POC
From below POC, we observe that the amount of debt that was reduce (178324273236511862925 - 175195694050991501434) in totalDebt is lot lesser than the intended amount to be reduced. If multiple liquidations happen and _mytSharesDeposited is not corectly reduced, it can cause protocol to incur more debt than intended since the _mytSharesDeposited was overstated (resulting in global collaterization being overstated) and full debt position of user is not liquidated in accordance to the global collaterization set.
forge test --mc AlchemistV3POC --mt test_Liquidate_mytSharesDepositedNotDecremented -vv
forge test --mc AlchemistV3POC --mt test_Liquidate_mytSharesDepositedDecremented -vv
==== After Deposit, Mint and Earmarked ====
alchemistMytBalance: 282400000000000000000
_mytSharesDeposited: 282400000000000000000
=== Liquidate External User ===
totalDebt: 175195694050991501434
==== After Deposit, Mint and Earmarked ====
alchemistMytBalance: 282400000000000000000
_mytSharesDeposited: 282400000000000000000
=== Liquidate External User ===
totalDebt: 178324273236511862925
// 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 AlchemistV3POC is Test {
// ----- [SETUP] Variables for setting up a minimal CDP -----
// Callable contract variables
AlchemistV3 alchemist;
Transmuter transmuter;
AlchemistV3Position alchemistNFT;
AlchemistTokenVault alchemistFeeVault;
// // Proxy variables
TransparentUpgradeableProxy proxyAlchemist;
TransparentUpgradeableProxy proxyTransmuter;
// // Contract variables
// CheatCodes cheats = CheatCodes(HEVM_ADDRESS);
AlchemistV3 alchemistLogic;
Transmuter transmuterLogic;
AlchemicTokenV3 alToken;
Whitelist whitelist;
// Parameters for AlchemicTokenV2
string public _name;
string public _symbol;
uint256 public _flashFee;
address public alOwner;
mapping(address => bool) users;
uint256 public constant FIXED_POINT_SCALAR = 1e18;
uint256 public constant BPS = 10_000;
uint256 public protocolFee = 100;
uint256 public liquidatorFeeBPS = 300; // in BPS, 3%
uint256 public minimumCollateralization = uint256(FIXED_POINT_SCALAR * FIXED_POINT_SCALAR) / 9e17;
// ----- Variables for deposits & withdrawals -----
// account funds to make deposits/test with
uint256 accountFunds;
// large amount to test with
uint256 whaleSupply;
// amount of yield/underlying token to deposit
uint256 depositAmount;
// minimum amount of yield/underlying token to deposit
uint256 minimumDeposit = 1000e18;
// minimum amount of yield/underlying token to deposit
uint256 minimumDepositOrWithdrawalLoss = FIXED_POINT_SCALAR;
// random EOA for testing
address externalUser = address(0x69E8cE9bFc01AA33cD2d02Ed91c72224481Fa420);
// another random EOA for testing
address anotherExternalUser = address(0x420Ab24368E5bA8b727E9B8aB967073Ff9316969);
// another random EOA for testing
address yetAnotherExternalUser = address(0x520aB24368e5Ba8B727E9b8aB967073Ff9316961);
// another random EOA for testing
address someWhale = address(0x521aB24368E5Ba8b727e9b8AB967073fF9316961);
// WETH address
address public weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
address public protocolFeeReceiver = address(10);
// MYT variables
VaultV2 vault;
MockAlchemistAllocator allocator;
MockMYTStrategy mytStrategy;
address public operator = address(0x2222222222222222222222222222222222222222); // default operator
address public admin = address(0x4444444444444444444444444444444444444444); // DAO OSX
address public curator = address(0x8888888888888888888888888888888888888888);
address public mockVaultCollateral = address(new TestERC20(100e18, uint8(18)));
address public mockStrategyYieldToken = address(new MockYieldToken(mockVaultCollateral));
uint256 public defaultStrategyAbsoluteCap = 2_000_000_000e18;
uint256 public defaultStrategyRelativeCap = 1e18; // 100%
struct CalculateLiquidationResult {
uint256 liquidationAmountInYield;
uint256 debtToBurn;
uint256 outSourcedFee;
uint256 baseFeeInYield;
}
struct AccountPosition {
address user;
uint256 collateral;
uint256 debt;
uint256 tokenId;
}
function setUp() external {
adJustTestFunds(18);
setUpMYT(18);
deployCoreContracts(18);
}
function adJustTestFunds(uint256 alchemistUnderlyingTokenDecimals) public {
accountFunds = 200_000 * 10 ** alchemistUnderlyingTokenDecimals;
whaleSupply = 20_000_000_000 * 10 ** alchemistUnderlyingTokenDecimals;
depositAmount = 200_000 * 10 ** alchemistUnderlyingTokenDecimals;
}
function setUpMYT(uint256 alchemistUnderlyingTokenDecimals) public {
vm.startPrank(admin);
uint256 TOKEN_AMOUNT = 1_000_000; // Base token amount
uint256 initialSupply = TOKEN_AMOUNT * 10 ** alchemistUnderlyingTokenDecimals;
mockVaultCollateral = address(new TestERC20(initialSupply, uint8(alchemistUnderlyingTokenDecimals)));
mockStrategyYieldToken = address(new MockYieldToken(mockVaultCollateral));
vault = MYTTestHelper._setupVault(mockVaultCollateral, admin, curator);
mytStrategy = MYTTestHelper._setupStrategy(address(vault), mockStrategyYieldToken, admin, "MockToken", "MockTokenProtocol", IMYTStrategy.RiskClass.LOW);
allocator = new MockAlchemistAllocator(address(vault), admin, operator);
vm.stopPrank();
vm.startPrank(curator);
_vaultSubmitAndFastForward(abi.encodeCall(IVaultV2.setIsAllocator, (address(allocator), true)));
vault.setIsAllocator(address(allocator), true);
_vaultSubmitAndFastForward(abi.encodeCall(IVaultV2.addAdapter, address(mytStrategy)));
vault.addAdapter(address(mytStrategy));
bytes memory idData = mytStrategy.getIdData();
_vaultSubmitAndFastForward(abi.encodeCall(IVaultV2.increaseAbsoluteCap, (idData, defaultStrategyAbsoluteCap)));
vault.increaseAbsoluteCap(idData, defaultStrategyAbsoluteCap);
_vaultSubmitAndFastForward(abi.encodeCall(IVaultV2.increaseRelativeCap, (idData, defaultStrategyRelativeCap)));
vault.increaseRelativeCap(idData, defaultStrategyRelativeCap);
vm.stopPrank();
}
function _magicDepositToVault(address vault, address depositor, uint256 amount) internal returns (uint256) {
deal(address(mockVaultCollateral), address(depositor), amount);
vm.startPrank(depositor);
TokenUtils.safeApprove(address(mockVaultCollateral), vault, amount);
uint256 shares = IVaultV2(vault).deposit(amount, depositor);
vm.stopPrank();
return shares;
}
function _vaultSubmitAndFastForward(bytes memory data) internal {
vault.submit(data);
bytes4 selector = bytes4(data);
vm.warp(block.timestamp + vault.timelock(selector));
}
function deployCoreContracts(uint256 alchemistUnderlyingTokenDecimals) public {
// test maniplulation for convenience
address caller = address(0xdead);
address proxyOwner = address(this);
vm.assume(caller != address(0));
vm.assume(proxyOwner != address(0));
vm.assume(caller != proxyOwner);
vm.startPrank(caller);
// Fake tokens
alToken = new AlchemicTokenV3(_name, _symbol, _flashFee);
ITransmuter.TransmuterInitializationParams memory transParams = ITransmuter.TransmuterInitializationParams({
syntheticToken: address(alToken),
feeReceiver: address(this),
timeToTransmute: 5_256_000,
transmutationFee: 10,
exitFee: 20,
graphSize: 52_560_000
});
// Contracts and logic contracts
alOwner = caller;
transmuterLogic = new Transmuter(transParams);
alchemistLogic = new AlchemistV3();
whitelist = new Whitelist();
// AlchemistV3 proxy
AlchemistInitializationParams memory params = AlchemistInitializationParams({
admin: alOwner,
debtToken: address(alToken),
underlyingToken: address(vault.asset()),
depositCap: type(uint256).max,
minimumCollateralization: minimumCollateralization,
collateralizationLowerBound: 1_052_631_578_950_000_000, // 1.05 collateralization
globalMinimumCollateralization: 1_111_111_111_111_111_111, // 1.1
transmuter: address(transmuterLogic),
protocolFee: 0,
protocolFeeReceiver: protocolFeeReceiver,
liquidatorFee: liquidatorFeeBPS,
repaymentFee: 100,
myt: address(vault)
});
bytes memory alchemParams = abi.encodeWithSelector(AlchemistV3.initialize.selector, params);
proxyAlchemist = new TransparentUpgradeableProxy(address(alchemistLogic), proxyOwner, alchemParams);
alchemist = AlchemistV3(address(proxyAlchemist));
// Whitelist alchemist proxy for minting tokens
alToken.setWhitelist(address(proxyAlchemist), true);
whitelist.add(address(0xbeef));
whitelist.add(externalUser);
whitelist.add(anotherExternalUser);
transmuterLogic.setAlchemist(address(alchemist));
transmuterLogic.setDepositCap(uint256(type(int256).max));
alchemistNFT = new AlchemistV3Position(address(alchemist));
alchemist.setAlchemistPositionNFT(address(alchemistNFT));
alchemistFeeVault = new AlchemistTokenVault(address(vault.asset()), address(alchemist), alOwner);
alchemistFeeVault.setAuthorization(address(alchemist), true);
alchemist.setAlchemistFeeVault(address(alchemistFeeVault));
_magicDepositToVault(address(vault), address(0xbeef), accountFunds);
_magicDepositToVault(address(vault), address(0xdad), accountFunds);
_magicDepositToVault(address(vault), externalUser, accountFunds);
_magicDepositToVault(address(vault), yetAnotherExternalUser, accountFunds);
_magicDepositToVault(address(vault), anotherExternalUser, accountFunds);
vm.stopPrank();
vm.startPrank(address(admin));
allocator.allocate(address(mytStrategy), vault.convertToAssets(vault.totalSupply()));
vm.stopPrank();
deal(address(alToken), address(0xdad), accountFunds);
deal(address(alToken), address(anotherExternalUser), accountFunds);
deal(address(vault.asset()), address(0xbeef), accountFunds);
deal(address(vault.asset()), externalUser, accountFunds);
deal(address(vault.asset()), yetAnotherExternalUser, accountFunds);
deal(address(vault.asset()), anotherExternalUser, accountFunds);
deal(address(vault.asset()), alchemist.alchemistFeeVault(), 10_000 * (10 ** alchemistUnderlyingTokenDecimals));
vm.startPrank(anotherExternalUser);
SafeERC20.safeApprove(address(vault.asset()), address(vault), accountFunds);
vm.stopPrank();
vm.startPrank(yetAnotherExternalUser);
SafeERC20.safeApprove(address(vault.asset()), address(vault), accountFunds);
vm.stopPrank();
vm.startPrank(someWhale);
deal(address(vault), someWhale, whaleSupply);
deal(address(vault.asset()), someWhale, whaleSupply);
SafeERC20.safeApprove(address(vault.asset()), address(mockStrategyYieldToken), whaleSupply);
vm.stopPrank();
}
function testSetV3PositionNFTAlreadySetRevert() public {
vm.startPrank(alOwner);
vm.expectRevert();
alchemist.setAlchemistPositionNFT(address(0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF));
vm.stopPrank();
}
function test_Liquidate_mytSharesDepositedNotDecremented() external {
uint256 amount = 80e18;
// 1. Whale mints so that alchemistCurrentCollateralization >= alchemistMinimumCollateralization
vm.startPrank(someWhale);
IMockYieldToken(mockStrategyYieldToken).mint(whaleSupply, someWhale);
vm.stopPrank();
// 2. 0xbeef holds a borrow position
vm.startPrank(address(0xbeef));
SafeERC20.safeApprove(address(vault), address(alchemist), amount + 100e18);
alchemist.deposit(amount, address(0xbeef), 0);
// A single position nft would have been minted to 0xbeef
uint256 tokenIdFor0xBeef = AlchemistNFTHelper.getFirstTokenId(address(0xbeef), address(alchemistNFT));
alchemist.mint(tokenIdFor0xBeef, alchemist.totalValue(tokenIdFor0xBeef) * FIXED_POINT_SCALAR / minimumCollateralization, address(0xbeef));
vm.stopPrank();
// 3. External user holds debt position
vm.startPrank(anotherExternalUser);
SafeERC20.safeApprove(address(vault), address(alchemist), amount * 0.9e18 / 1e18);
alchemist.deposit(amount * 0.9e18 / 1e18, anotherExternalUser, 0);
uint256 tokenIdForExternalUser = AlchemistNFTHelper.getFirstTokenId(anotherExternalUser, address(alchemistNFT));
alchemist.mint(tokenIdForExternalUser, alchemist.totalValue(tokenIdForExternalUser) * FIXED_POINT_SCALAR / minimumCollateralization, anotherExternalUser);
vm.stopPrank();
// 4. Yet another external user holds debt positon
vm.startPrank(yetAnotherExternalUser);
SafeERC20.safeApprove(address(vault), address(alchemist), amount * 1.63e18 / 1e18);
alchemist.deposit(amount * 1.63e18 / 1e18, yetAnotherExternalUser, 0);
uint256 tokenIdForYetAnotherExternalUser = AlchemistNFTHelper.getFirstTokenId(yetAnotherExternalUser, address(alchemistNFT));
alchemist.mint(tokenIdForYetAnotherExternalUser, alchemist.totalValue(tokenIdForYetAnotherExternalUser) * FIXED_POINT_SCALAR / minimumCollateralization, yetAnotherExternalUser);
vm.stopPrank();
uint256 alchemistMytBalance = IERC20(address(vault)).balanceOf(address(alchemist));
uint256 _mytSharesDeposited = alchemist.getMytSharesDeposited();
vm.startPrank(anotherExternalUser);
SafeERC20.safeApprove(address(alToken), address(transmuterLogic), 50e18);
transmuterLogic.createRedemption(50e18);
vm.stopPrank();
vm.roll(block.number + 5_256_000);
console.log("==== After Deposit, Mint and Earmarked ====");
console.log("alchemistMytBalance: ", alchemistMytBalance);
console.log("_mytSharesDeposited: ", _mytSharesDeposited);
// 5. modify yield token price via modifying underlying token supply
uint256 initialVaultSupply = IERC20(address(mockStrategyYieldToken)).totalSupply();
IMockYieldToken(mockStrategyYieldToken).updateMockTokenSupply(initialVaultSupply);
// 6. increasing yeild token suppy by 59 bps or 5.9% while keeping the unederlying supply unchanged
uint256 modifiedVaultSupply = (initialVaultSupply * 590 / 10_000) + initialVaultSupply;
IMockYieldToken(mockStrategyYieldToken).updateMockTokenSupply(modifiedVaultSupply);
// 7. let another user liquidate the previous user position
uint256 externalUserBalanceBefore = IERC20(address(vault)).balanceOf(externalUser);
vm.startPrank(externalUser);
(uint256 yieldAmount, uint256 feeInYield, uint256 feeInUnderlying) = alchemist.liquidate(tokenIdFor0xBeef);
vm.stopPrank();
uint256 externalUserBalanceAfter = IERC20(address(vault)).balanceOf(externalUser);
alchemistMytBalance = IERC20(address(vault)).balanceOf(address(alchemist));
_mytSharesDeposited = alchemist.getMytSharesDeposited();
// 8. _mytSharesDeposited should have reduced by feeInYield + protocolFee
// Here we dont set the correct value
// alchemist.setMytSharesDeposited(alchemist.getMytSharesDeposited() - feeInYield);
// 9. Mock a global minimum collaterization that is slightly higher that minimum collaterization
// This is so that it is easier to depict the scenario
vm.startPrank(alOwner);
alchemist.setGlobalMinimumCollateralization(1.1246e18);
vm.stopPrank();
// Further Drop price by ~10.5%
initialVaultSupply = IERC20(address(mockStrategyYieldToken)).totalSupply();
IMockYieldToken(mockStrategyYieldToken).updateMockTokenSupply(initialVaultSupply);
modifiedVaultSupply = (initialVaultSupply * 1.05e17 / 1e18) + initialVaultSupply;
IMockYieldToken(mockStrategyYieldToken).updateMockTokenSupply(modifiedVaultSupply);
// Check totalDebt after liquidation
console.log("=== Liquidate External User ===");
vm.startPrank(externalUser);
(yieldAmount, feeInYield, feeInUnderlying) = alchemist.liquidate(tokenIdForExternalUser);
vm.stopPrank();
// Debt should be higher
console.log("totalDebt: ", alchemist.totalDebt());
}
function test_Liquidate_mytSharesDepositedDecremented() external {
uint256 amount = 80e18;
// 1. Whale mints so that alchemistCurrentCollateralization >= alchemistMinimumCollateralization
vm.startPrank(someWhale);
IMockYieldToken(mockStrategyYieldToken).mint(whaleSupply, someWhale);
vm.stopPrank();
// 2. 0xbeef holds a borrow position
vm.startPrank(address(0xbeef));
SafeERC20.safeApprove(address(vault), address(alchemist), amount + 100e18);
alchemist.deposit(amount, address(0xbeef), 0);
// A single position nft would have been minted to 0xbeef
uint256 tokenIdFor0xBeef = AlchemistNFTHelper.getFirstTokenId(address(0xbeef), address(alchemistNFT));
alchemist.mint(tokenIdFor0xBeef, alchemist.totalValue(tokenIdFor0xBeef) * FIXED_POINT_SCALAR / minimumCollateralization, address(0xbeef));
vm.stopPrank();
// 3. External user holds debt position
vm.startPrank(anotherExternalUser);
SafeERC20.safeApprove(address(vault), address(alchemist), amount * 0.9e18 / 1e18);
alchemist.deposit(amount * 0.9e18 / 1e18, anotherExternalUser, 0);
uint256 tokenIdForExternalUser = AlchemistNFTHelper.getFirstTokenId(anotherExternalUser, address(alchemistNFT));
alchemist.mint(tokenIdForExternalUser, alchemist.totalValue(tokenIdForExternalUser) * FIXED_POINT_SCALAR / minimumCollateralization, anotherExternalUser);
vm.stopPrank();
// 4. Yet another external user holds debt positon
vm.startPrank(yetAnotherExternalUser);
SafeERC20.safeApprove(address(vault), address(alchemist), amount * 1.63e18 / 1e18);
alchemist.deposit(amount * 1.63e18 / 1e18, yetAnotherExternalUser, 0);
uint256 tokenIdForYetAnotherExternalUser = AlchemistNFTHelper.getFirstTokenId(yetAnotherExternalUser, address(alchemistNFT));
alchemist.mint(tokenIdForYetAnotherExternalUser, alchemist.totalValue(tokenIdForYetAnotherExternalUser) * FIXED_POINT_SCALAR / minimumCollateralization, yetAnotherExternalUser);
vm.stopPrank();
uint256 alchemistMytBalance = IERC20(address(vault)).balanceOf(address(alchemist));
uint256 _mytSharesDeposited = alchemist.getMytSharesDeposited();
vm.startPrank(anotherExternalUser);
SafeERC20.safeApprove(address(alToken), address(transmuterLogic), 50e18);
transmuterLogic.createRedemption(50e18);
vm.stopPrank();
vm.roll(block.number + 5_256_000);
console.log("==== After Deposit, Mint and Earmarked ====");
console.log("alchemistMytBalance: ", alchemistMytBalance);
console.log("_mytSharesDeposited: ", _mytSharesDeposited);
// 5. modify yield token price via modifying underlying token supply
uint256 initialVaultSupply = IERC20(address(mockStrategyYieldToken)).totalSupply();
IMockYieldToken(mockStrategyYieldToken).updateMockTokenSupply(initialVaultSupply);
// 6. increasing yeild token suppy by 59 bps or 5.9% while keeping the unederlying supply unchanged
uint256 modifiedVaultSupply = (initialVaultSupply * 590 / 10_000) + initialVaultSupply;
IMockYieldToken(mockStrategyYieldToken).updateMockTokenSupply(modifiedVaultSupply);
// 7. let another user liquidate the previous user position
uint256 externalUserBalanceBefore = IERC20(address(vault)).balanceOf(externalUser);
vm.startPrank(externalUser);
(uint256 yieldAmount, uint256 feeInYield, uint256 feeInUnderlying) = alchemist.liquidate(tokenIdFor0xBeef);
vm.stopPrank();
uint256 externalUserBalanceAfter = IERC20(address(vault)).balanceOf(externalUser);
alchemistMytBalance = IERC20(address(vault)).balanceOf(address(alchemist));
_mytSharesDeposited = alchemist.getMytSharesDeposited();
// 8. _mytSharesDeposited should have reduced by feeInYield + protocolFee
// We manually set the correct value here
alchemist.setMytSharesDeposited(alchemist.getMytSharesDeposited() - feeInYield);
// 9. Mock a global minimum collaterization that is slightly higher that minimum collaterization
// This is so that it is easier to depict the scenario
vm.startPrank(alOwner);
alchemist.setGlobalMinimumCollateralization(1.1246e18);
vm.stopPrank();
// Further Drop price by ~10.5%
initialVaultSupply = IERC20(address(mockStrategyYieldToken)).totalSupply();
IMockYieldToken(mockStrategyYieldToken).updateMockTokenSupply(initialVaultSupply);
modifiedVaultSupply = (initialVaultSupply * 1.05e17 / 1e18) + initialVaultSupply;
IMockYieldToken(mockStrategyYieldToken).updateMockTokenSupply(modifiedVaultSupply);
// Check totalDebt after liquidation
console.log("=== Liquidate External User ===");
vm.startPrank(externalUser);
(yieldAmount, feeInYield, feeInUnderlying) = alchemist.liquidate(tokenIdForExternalUser);
vm.stopPrank();
// Debt is lower when _mytSharesDeposited is correctly reduced
console.log("totalDebt: ", alchemist.totalDebt());
}
}