diff --git a/.gitmodules b/.gitmodules index 7f9e985..bbf3513 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "lib/euler-vault-kit"] path = lib/euler-vault-kit url = https://github.com/euler-xyz/euler-vault-kit +[submodule "lib/euler-data-lenses"] + path = lib/euler-data-lenses + url = https://github.com/euler-xyz/euler-data-lenses diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..3b488fc --- /dev/null +++ b/foundry.lock @@ -0,0 +1,17 @@ +{ + "lib/erc4626-tests": { + "rev": "232ff9ba8194e406967f52ecc5cb52ed764209e9" + }, + "lib/ethereum-vault-connector": { + "rev": "a7d3c29ef7e4964736e47675e0588630d6afbfd7" + }, + "lib/euler-vault-kit": { + "rev": "5b98b42048ba11ae82fb62dfec06d1010c8e41e6" + }, + "lib/forge-std": { + "rev": "77041d2ce690e692d6e03cc812b57d1ddaa4d505" + }, + "lib/openzeppelin-contracts": { + "rev": "e4f70216d759d8e6a64144a9e1f7bbeed78e7079" + } +} \ No newline at end of file diff --git a/lib/euler-data-lenses b/lib/euler-data-lenses new file mode 160000 index 0000000..b03cca6 --- /dev/null +++ b/lib/euler-data-lenses @@ -0,0 +1 @@ +Subproject commit b03cca64587c7c09552f10b318b0be0d7e7261d0 diff --git a/src/RescueStrategy.sol b/src/RescueStrategy.sol index 509abf2..32b18c5 100644 --- a/src/RescueStrategy.sol +++ b/src/RescueStrategy.sol @@ -2,13 +2,16 @@ pragma solidity ^0.8.26; import {IERC20} from "openzeppelin-contracts/interfaces/IERC20.sol"; +import {IERC20Metadata} from "openzeppelin-contracts/interfaces/IERC20Metadata.sol"; import {IERC4626} from "openzeppelin-contracts/interfaces/IERC4626.sol"; import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {IEVC} from "ethereum-vault-connector/interfaces/IEthereumVaultConnector.sol"; -import {IEulerEarn} from "./interfaces/IEulerEarn.sol"; +import {IEulerEarn, IEulerEarnBase} from "./interfaces/IEulerEarn.sol"; +import {IEulerEarnFactory} from "./interfaces/IEulerEarnFactory.sol"; import {SafeERC20Permit2Lib} from "./libraries/SafeERC20Permit2Lib.sol"; import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; -import {IBorrowing, IRiskManager} from "../lib/euler-vault-kit/src/EVault/IEVault.sol"; +import {ReentrancyGuard} from "openzeppelin-contracts/utils/ReentrancyGuard.sol"; +import {IEVault, IBorrowing, IRiskManager} from "../lib/euler-vault-kit/src/EVault/IEVault.sol"; /* Rescue procedure: @@ -38,9 +41,10 @@ interface IFlashLoan { ) external; } -contract RescueStrategy { +contract RescueStrategy is IEVault { address public immutable rescueAccount; address public immutable earnVault; + address public immutable earnFactory; IERC20 internal immutable _asset; bool internal rescueActive; @@ -59,12 +63,20 @@ contract RescueStrategy { } modifier onlyWhenRescueActive() { - require(rescueActive, "vault operations are paused"); + require(rescueActive, "unauthorized"); _; } - modifier onlyAllowedEarnVault() { - require(msg.sender == earnVault, "wrong vault"); + modifier notEarnMutatingCall() { + if (!rescueActive && msg.sender == earnVault) { + // if reentrancy locked - earn is calling from `withdraw` or `deposit`, which should be prevented + // if unlocked - let it through because e.g. `maxWithdrawFromStrategy` is called, which is relied upon by the Lens contract + (bool success, bytes memory reason) = earnVault.staticcall(abi.encodeCall(IEulerEarnBase.setFee, (0))); + require(!success, "expected revert"); // if reentrancy was unlocked, attempt to set it will panic + + if (reason.length == 4 && bytes4(reason) == ReentrancyGuard.ReentrancyGuardReentrantCall.selector) + revert("vault operations are paused"); + } _; } @@ -74,39 +86,39 @@ contract RescueStrategy { rescueAccount = _rescueAccount; earnVault = _earnVault; _asset = IERC20(IEulerEarn(earnVault).asset()); + earnFactory = IEulerEarn(_earnVault).creator(); } - // ---------------- VAULT INTERFACE -------------------- - - function asset() external view returns (address) { - return address(_asset); - } + // ---------------- RESCUE ENABLING BEHAVIOR -------------------- // will revert user deposits - function maxDeposit(address) external view onlyAllowedEarnVault onlyWhenRescueActive returns (uint256) { - return type(uint256).max; + function maxDeposit(address) external view notEarnMutatingCall returns (uint256) { + return msg.sender == earnVault && rescueActive ? type(uint256).max : 0; } // will revert user withdrawals - function maxWithdraw(address) external view onlyAllowedEarnVault onlyWhenRescueActive returns (uint256) { - return 0; - } - - function previewRedeem(uint256) external pure returns (uint256) { + function maxWithdraw(address) external view notEarnMutatingCall returns (uint256) { return 0; } // this reverts acceptCaps to prevent reusing the whitelisted strategy on other vaults - function balanceOf(address) external view onlyAllowedEarnVault returns (uint256) { + function balanceOf(address) external view returns (uint256) { + require(!IEulerEarnFactory(earnFactory).isVault(msg.sender) || msg.sender == earnVault, "wrong vault"); return 0; } - function deposit(uint256 amount, address) external onlyAllowedEarnVault onlyWhenRescueActive returns (uint256) { - SafeERC20Permit2Lib.safeTransferFromWithPermit2( - _asset, msg.sender, address(this), amount, IEulerEarn(earnVault).permit2Address() - ); + function deposit(uint256 amount, address) external returns (uint256) { + if (msg.sender == earnVault) { + require(rescueActive, "only during rescue"); + + SafeERC20Permit2Lib.safeTransferFromWithPermit2( + _asset, msg.sender, address(this), amount, IEulerEarn(earnVault).permit2Address() + ); - return amount; + return amount; + } + + _revertNotSupported(); } // ---------------- RESCUE FUNCTIONS -------------------- @@ -226,10 +238,6 @@ contract RescueStrategy { require(success, "call failed"); } - fallback() external { - revert("vault operations are paused"); - } - function _processFlashLoan(uint256 loanAmount, uint256 loops) internal { SafeERC20Permit2Lib.forceApproveMaxWithPermit2(_asset, earnVault, address(0)); @@ -258,4 +266,349 @@ contract RescueStrategy { // Must be first in withdraw queue (bank-run guard) require(address(vault.withdrawQueue(0)) == address(this), "rescue: withdrawQueue[0] != rescue"); } + + function _revertNotSupported() internal pure { + revert("not supported"); + } + + // ---------------- EVault compatibility stubs -------------------- + + function symbol() external pure returns (string memory) { + return "RS"; + } + + function name() external pure returns (string memory) { + return "Rescue Strategy"; + } + + function decimals() external view returns (uint8) { + return IERC20Metadata(address(_asset)).decimals(); + } + + function allowance(address, address) external pure returns (uint256) { + return 0; + } + + function totalSupply() external pure returns (uint256) { + return 0; + } + + function asset() external view returns (address) { + return address(_asset); + } + + function totalAssets() external pure returns (uint256) { + return 0; + } + + function convertToShares(uint256) external pure returns (uint256) { + return 0; + } + + function convertToAssets(uint256) external pure returns (uint256) { + return 0; + } + + function previewDeposit(uint256) external pure returns (uint256) { + return 0; + } + + function maxMint(address) external pure returns (uint256) { + return 0; + } + + function previewMint(uint256) external pure returns (uint256) { + return 0; + } + + function previewWithdraw(uint256) external pure returns (uint256) { + return 0; + } + + function maxRedeem(address) external pure returns (uint256) { + return 0; + } + + function previewRedeem(uint256) external pure returns (uint256) { + return 0; + } + + function approve(address, uint256) external pure returns (bool) { + _revertNotSupported(); + } + + function transfer(address, uint256) external pure returns (bool) { + _revertNotSupported(); + } + + function transferFrom(address, address, uint256) external pure returns (bool) { + _revertNotSupported(); + } + + function mint(uint256, address) external pure returns (uint256) { + _revertNotSupported(); + } + + function redeem(uint256, address, address) external pure returns (uint256) { + _revertNotSupported(); + } + + function withdraw(uint256, address, address) external pure returns (uint256) { + _revertNotSupported(); + } + + function transferFromMax(address, address) external pure returns (bool) { + _revertNotSupported(); + } + + function accumulatedFees() external pure returns (uint256) { + return 0; + } + + function accumulatedFeesAssets() external pure returns (uint256) { + return 0; + } + + function creator() external pure returns (address) { + return address(0); + } + + function skim(uint256, address) external pure returns (uint256) { + _revertNotSupported(); + } + + function totalBorrows() external pure returns (uint256) { + return 0; + } + + function totalBorrowsExact() external pure returns (uint256) { + return 0; + } + + function cash() external pure returns (uint256) { + return 0; + } + + function debtOf(address) external pure returns (uint256) { + return 0; + } + + function debtOfExact(address) external pure returns (uint256) { + return 0; + } + + function interestRate() external pure returns (uint256) { + return 0; + } + + function interestAccumulator() external pure returns (uint256) { + return 0; + } + + function dToken() external pure returns (address) { + return address(0); + } + + function borrow(uint256, address) external pure returns (uint256) { + _revertNotSupported(); + } + + function repay(uint256, address) external pure returns (uint256) { + _revertNotSupported(); + } + + function repayWithShares(uint256, address) external pure returns (uint256, uint256) { + _revertNotSupported(); + } + + function pullDebt(uint256, address) external pure { + _revertNotSupported(); + } + + function flashLoan(uint256, bytes calldata) external pure { + _revertNotSupported(); + } + + function touch() external pure { + _revertNotSupported(); + } + + function checkLiquidation(address, address, address) external pure returns (uint256, uint256) { + return (0, 0); + } + + function liquidate(address, address, uint256, uint256) external pure { + _revertNotSupported(); + } + + function accountLiquidity(address, bool) external pure returns (uint256, uint256) { + return (0, 0); + } + + function accountLiquidityFull(address, bool) external pure returns (address[] memory c, uint256[] memory cv, uint256 lv) {} + + function disableController() external pure { + _revertNotSupported(); + } + + function checkAccountStatus(address, address[] calldata) external pure returns (bytes4) { + return bytes4(0); + } + + function checkVaultStatus() external pure returns (bytes4) { + return bytes4(0); + } + + function balanceTrackerAddress() external pure returns (address) { + return address(0); + } + + function balanceForwarderEnabled(address) external pure returns (bool) { + return false; + } + + function enableBalanceForwarder() external pure { + _revertNotSupported(); + } + + function disableBalanceForwarder() external pure { + _revertNotSupported(); + } + + function governorAdmin() external pure returns (address) { + return address(0); + } + + function feeReceiver() external pure returns (address) { + return address(0); + } + + function interestFee() external pure returns (uint16) { + return 0; + } + + function interestRateModel() external pure returns (address) { + return address(0); + } + + function protocolConfigAddress() external pure returns (address) { + return address(0); + } + + function protocolFeeShare() external pure returns (uint256) { + return 0; + } + + function protocolFeeReceiver() external pure returns (address) { + return address(0); + } + + function caps() external pure returns (uint16, uint16) { + return (0, 0); + } + + function LTVBorrow(address) external pure returns (uint16) { + return 0; + } + + function LTVLiquidation(address) external pure returns (uint16) { + return 0; + } + + function LTVFull(address collateral) external pure returns ( + uint16 borrowLTV, + uint16 liquidationLTV, + uint16 initialLiquidationLTV, + uint48 targetTimestamp, + uint32 rampDuration + ) {} + + function LTVList() external pure returns (address[] memory l) {} + + function maxLiquidationDiscount() external pure returns (uint16) { + return 0; + } + + function liquidationCoolOffTime() external pure returns (uint16) { + return 0; + } + + function hookConfig() external pure returns (address hookTarget, uint32 hookedOps) {} + + function configFlags() external pure returns (uint32) { + return 0; + } + + function EVC() external pure returns (address) { + return address(0); + } + + function unitOfAccount() external pure returns (address) { + return address(0); + } + + function oracle() external pure returns (address) { + return address(0); + } + + function permit2Address() external pure returns (address) { + return address(0); + } + + function convertFees() external pure { + _revertNotSupported(); + } + + function setGovernorAdmin(address) external pure { + _revertNotSupported(); + } + + function setFeeReceiver(address) external pure { + _revertNotSupported(); + } + + function setLTV(address, uint16, uint16, uint32) external pure { + _revertNotSupported(); + } + + function setMaxLiquidationDiscount(uint16) external pure { + _revertNotSupported(); + } + + function setLiquidationCoolOffTime(uint16) external pure { + _revertNotSupported(); + } + + function setInterestRateModel(address) external pure { + _revertNotSupported(); + } + + function setHookConfig(address, uint32) external pure { + _revertNotSupported(); + } + + function setConfigFlags(uint32) external pure { + _revertNotSupported(); + } + + function setCaps(uint16, uint16) external pure { + _revertNotSupported(); + } + + function setInterestFee(uint16) external pure { + _revertNotSupported(); + } + + function initialize(address) external pure { + _revertNotSupported(); + } + + function MODULE_INITIALIZE() external pure returns (address) { return address(0); } + function MODULE_TOKEN() external pure returns (address) { return address(0); } + function MODULE_VAULT() external pure returns (address) { return address(0); } + function MODULE_BORROWING() external pure returns (address) { return address(0); } + function MODULE_LIQUIDATION() external pure returns (address) { return address(0); } + function MODULE_RISKMANAGER() external pure returns (address) { return address(0); } + function MODULE_BALANCE_FORWARDER() external pure returns (address) { return address(0); } + function MODULE_GOVERNANCE() external pure returns (address) { return address(0); } } diff --git a/test/RescueStrategyTest.sol b/test/RescueStrategyTest.sol index a297f14..2642071 100644 --- a/test/RescueStrategyTest.sol +++ b/test/RescueStrategyTest.sol @@ -10,6 +10,7 @@ import {IAllowanceTransfer} from "../src/interfaces/IAllowanceTransfer.sol"; import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; import {IEVC} from "ethereum-vault-connector/interfaces/IEthereumVaultConnector.sol"; import {RescueStrategy} from "../src/RescueStrategy.sol"; +import {EulerEarnVaultLens as EarnIndexerLens, EulerEarnVaultInfoFull} from "../lib/euler-data-lenses/src/EulerEarnLens.sol"; import "forge-std/Test.sol"; contract RescuePOC is Test { @@ -18,11 +19,17 @@ contract RescuePOC is Test { address constant OTHER_EARN_VAULT = 0x3cd3718f8f047aA32F775E2cb4245A164E1C99fB; // https://app.euler.finance/earn/0x3cd3718f8f047aA32F775E2cb4245A164E1C99fB?network=ethereum address constant FLASH_LOAN_SOURCE_MORPHO = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb; - address constant FLASH_LOAN_SOURCE_EULER = 0x797DD80692c3b2dAdabCe8e30C07fDE5307D48a9; // Euler Prime - also a strategy in earn + address constant FLASH_LOAN_SOURCE_EULER = 0x9bD52F2805c6aF014132874124686e7b248c2Cbb; + address constant FLASH_LOAN_SOURCE_EULER_BATCH = 0x797DD80692c3b2dAdabCe8e30C07fDE5307D48a9; // Euler Prime - also a strategy in earn address constant FLASH_LOAN_SOURCE_AAVE = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + address constant EARN_LENS = 0xA09144BeAe23D8e7836Aeb0Fe17DD2647241A8bE; + address constant EVAULT_LENS = 0xc3c45633E45041BF3BE841f89d2cb51E2F657403; uint256 constant BLOCK_NUMBER = 23753054; IEulerEarn vault; + IEulerEarn otherVault; + + EarnIndexerLens indexerLens; string FORK_RPC_URL = vm.envOr("FORK_RPC_URL_MAINNET", string("")); @@ -41,6 +48,7 @@ contract RescuePOC is Test { } vault = IEulerEarn(EARN_VAULT); + otherVault = IEulerEarn(OTHER_EARN_VAULT); // hyperithm euler usdc mainnet deal(vault.asset(), user, 100e18); vm.startPrank(user); @@ -48,6 +56,8 @@ contract RescuePOC is Test { IAllowanceTransfer(vault.permit2Address()).approve( vault.asset(), address(vault), type(uint160).max, type(uint48).max ); + + indexerLens = new EarnIndexerLens(); } function testRescue_assertRescueMode() public { @@ -58,7 +68,7 @@ contract RescuePOC is Test { vm.prank(rescueAccount); vm.expectRevert("rescue: supplyQueue len != 1"); - rescueStrategy.rescueEulerBatch(1, 1, FLASH_LOAN_SOURCE_EULER); + rescueStrategy.rescueEulerBatch(1, 1, FLASH_LOAN_SOURCE_EULER_BATCH); IERC4626[] memory supplyQueue = new IERC4626[](1); supplyQueue[0] = vault.supplyQueue(0); @@ -68,7 +78,7 @@ contract RescuePOC is Test { vm.prank(rescueAccount); vm.expectRevert("rescue: supplyQueue[0] != rescue"); - rescueStrategy.rescueEulerBatch(1, 1, FLASH_LOAN_SOURCE_EULER); + rescueStrategy.rescueEulerBatch(1, 1, FLASH_LOAN_SOURCE_EULER_BATCH); vm.prank(vault.curator()); vault.submitCap(id, type(uint184).max); @@ -84,7 +94,7 @@ contract RescuePOC is Test { vm.prank(rescueAccount); vm.expectRevert("rescue: withdrawQueue[0] != rescue"); - rescueStrategy.rescueEulerBatch(1, 1, FLASH_LOAN_SOURCE_EULER); + rescueStrategy.rescueEulerBatch(1, 1, FLASH_LOAN_SOURCE_EULER_BATCH); } function testRescue_pauseForUsers() public { @@ -99,23 +109,72 @@ contract RescuePOC is Test { vault.withdraw(0, user, user); vm.expectRevert("vault operations are paused"); vault.redeem(0, user, user); + + assertEq(vault.maxWithdrawFromStrategy(IERC4626(address(rescueStrategy))), 0); } + mapping (address => uint256) strategyCaps; function testRescue_rescueEulerBatch() public { _installRescueStrategy(); + uint256 withdrawQueueLength = vault.withdrawQueueLength(); + + for (uint256 i = 0; i < withdrawQueueLength; i++) { + address strategy = address(vault.withdrawQueue(i)); + strategyCaps[strategy] = vault.config(vault.withdrawQueue(i)).cap; + } + uint256 amount = 100_000e6; uint256 loops = 1; uint256 snapshot = vm.snapshotState(); // only rescue account vm.prank(user); vm.expectRevert("unauthorized"); - rescueStrategy.rescueEulerBatch(amount, loops, FLASH_LOAN_SOURCE_EULER); + rescueStrategy.rescueEulerBatch(amount, loops, FLASH_LOAN_SOURCE_EULER_BATCH); vm.startPrank(rescueAccount); vm.expectEmit(true, true, false, false); emit RescueStrategy.Rescued(address(vault), 0); - rescueStrategy.rescueEulerBatch(amount, loops, FLASH_LOAN_SOURCE_EULER); + rescueStrategy.rescueEulerBatch(amount, loops, FLASH_LOAN_SOURCE_EULER_BATCH); + + assertGt(IERC20(vault.asset()).balanceOf(rescueAccount), 0); + assertEq(IEVC(vault.EVC()).getControllers(address(rescueStrategy)).length, 0); + uint256 rescueOneLoop = IERC20(vault.asset()).balanceOf(rescueAccount); + + console.log("Rescued", rescueOneLoop, IEulerEarn(vault.asset()).symbol()); + console.log("Received shares", IERC4626(vault).balanceOf(rescueAccount)); + + vm.revertToState(snapshot); + loops = 2; + + rescueStrategy.rescueEulerBatch(amount, loops, FLASH_LOAN_SOURCE_EULER_BATCH); + assertEq(IERC20(vault.asset()).balanceOf(rescueAccount), rescueOneLoop * 2); + + // caps are unchanged on other strategies + + for (uint256 i = 0; i < withdrawQueueLength; i++) { + address strategy = address(vault.withdrawQueue(i)); + if (strategy != address(rescueStrategy)) { + assertEq(strategyCaps[strategy], vault.config(vault.withdrawQueue(i)).cap); + } + } + } + + function testRescue_rescueEuler() public { + _installRescueStrategy(); + + uint256 amount = 100_000e6; + uint256 loops = 1; + uint256 snapshot = vm.snapshotState(); + // only rescue account + vm.prank(user); + vm.expectRevert("unauthorized"); + rescueStrategy.rescueEuler(amount, loops, FLASH_LOAN_SOURCE_EULER); + + vm.startPrank(rescueAccount); + vm.expectEmit(true, true, false, false); + emit RescueStrategy.Rescued(address(vault), 0); + rescueStrategy.rescueEuler(amount, loops, FLASH_LOAN_SOURCE_EULER); assertGt(IERC20(vault.asset()).balanceOf(rescueAccount), 0); assertEq(IEVC(vault.EVC()).getControllers(address(rescueStrategy)).length, 0); @@ -127,7 +186,7 @@ contract RescuePOC is Test { vm.revertTo(snapshot); loops = 2; - rescueStrategy.rescueEulerBatch(amount, loops, FLASH_LOAN_SOURCE_EULER); + rescueStrategy.rescueEuler(amount, loops, FLASH_LOAN_SOURCE_EULER); assertEq(IERC20(vault.asset()).balanceOf(rescueAccount), rescueOneLoop * 2); } @@ -219,8 +278,6 @@ contract RescuePOC is Test { // install perspective in earn factory which will allow custom strategies _installPerspective(); - IEulerEarn otherVault = IEulerEarn(OTHER_EARN_VAULT); // hyperithm euler usdc mainnet - vm.startPrank(otherVault.curator()); otherVault.submitCap(IERC4626(address(rescueStrategy)), type(uint184).max); @@ -233,10 +290,25 @@ contract RescuePOC is Test { function testRescue_uninstall() public { _installRescueStrategy(); + // exchange rate should remain constant (besides rounding) + uint256 sharePriceBefore = vault.convertToAssets(1e18); + uint256 lostAssetsBefore = vault.lostAssets(); + vm.startPrank(user); vm.expectRevert("vault operations are paused"); vault.deposit(10, user); + // rescue + uint256 amount = vault.previewMint(vault.totalSupply()) * 10001 / 10000 ; + uint256 loops = 1; + + vm.startPrank(rescueAccount); + rescueStrategy.rescueMorpho(amount, loops, FLASH_LOAN_SOURCE_MORPHO); + + assertApproxEqAbs(sharePriceBefore, vault.convertToAssets(1e18), 1e5); + // all the deposited amount is counted as lost + assertEq(vault.lostAssets(), lostAssetsBefore + amount); + vm.startPrank(vault.curator()); IERC4626 id = IERC4626(address(rescueStrategy)); @@ -258,6 +330,8 @@ contract RescuePOC is Test { // the vault is functional + assertApproxEqAbs(sharePriceBefore, vault.convertToAssets(1e18), 1e5); + vm.startPrank(user); vault.deposit(10, user); uint256 balance = vault.balanceOf(user); @@ -268,6 +342,8 @@ contract RescuePOC is Test { assertEq(vault.balanceOf(user), balance); vault.withdraw(vault.maxWithdraw(user), user, user); assertEq(vault.balanceOf(user), 0); + + assertApproxEqAbs(sharePriceBefore, vault.convertToAssets(1e18), 1e6); } function testRescue_onlyRescueAccountCallFunc() external { @@ -284,16 +360,76 @@ contract RescuePOC is Test { function testRescue_flashloanCallbacks() external { _installRescueStrategy(); - vm.expectRevert("vault operations are paused"); + vm.expectRevert("unauthorized"); rescueStrategy.onBatchLoan(1, 1); - vm.expectRevert("vault operations are paused"); + vm.expectRevert("unauthorized"); rescueStrategy.onFlashLoan(""); - vm.expectRevert("vault operations are paused"); + vm.expectRevert("unauthorized"); rescueStrategy.onMorphoFlashLoan(1, ""); - vm.expectRevert("vault operations are paused"); + vm.expectRevert("unauthorized"); rescueStrategy.executeOperation(address(1), 1, 1, address(1), ""); } + function testRescue_callLenses() external { + _installRescueStrategy(); + // lens calls don't revert + + // onchain lens + (bool success, bytes memory data) = EARN_LENS.call(abi.encodeWithSignature("getVaultInfoFull(address)", address(vault))); + assertTrue(success && data.length > 0); + + // lens used in the indexer by setting the `code` in eth_call + EulerEarnVaultInfoFull memory lensData = indexerLens.getVaultInfoFull(address(vault)); + assertEq(lensData.vault, address(vault)); + + // evault lens + (success, data) = EVAULT_LENS.call(abi.encodeWithSignature("getVaultInfoFull(address)", address(rescueStrategy))); + assertTrue(success && data.length > 0); + } + + function testRescue_maxWithdrawView() external { + _installRescueStrategy(); + vm.prank(user); + assertEq(rescueStrategy.maxWithdraw(user), 0); + + vm.startPrank(address(vault)); + assertEq(rescueStrategy.maxWithdraw(user), 0); + } + + function testRescue_maxDepositView() external { + _installRescueStrategy(); + vm.prank(user); + assertEq(rescueStrategy.maxDeposit(user), 0); + + vm.startPrank(address(vault)); + assertEq(rescueStrategy.maxDeposit(user), 0); + } + + function testRescue_deposit() external { + _installRescueStrategy(); + vm.prank(user); + vm.expectRevert("not supported"); + rescueStrategy.deposit(1, user); + + vm.prank(address(vault)); + vm.expectRevert("only during rescue"); + rescueStrategy.deposit(1, address(vault)); + } + + function testRescue_balanceOfView() external { + _installRescueStrategy(); + vm.prank(user); + assertEq(rescueStrategy.balanceOf(user), 0); + + vm.startPrank(address(vault)); + assertEq(rescueStrategy.balanceOf(user), 0); + + vm.startPrank(address(otherVault)); + vm.expectRevert("wrong vault"); + rescueStrategy.balanceOf(user); + + } + function _installRescueStrategy() internal { // install perspective in earn factory which will allow custom strategies (use mock here) _installPerspective();