diff --git a/.gitmodules b/.gitmodules index 1494de22..1b7db210 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,6 @@ [submodule "lib/pendle-core-v2-public"] path = lib/pendle-core-v2-public url = https://github.com/pendle-finance/pendle-core-v2-public +[submodule "lib/solmate"] + path = lib/solmate + url = https://github.com/transmissions11/solmate diff --git a/src/Constants.sol b/src/Constants.sol index ff78aba7..bf4b6afc 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -20,6 +20,7 @@ import { IRenzoOracle, IRestakeManager } from "./interfaces/ProviderInterfaces.sol"; +import { IWETH9 } from "./interfaces/IWETH9.sol"; import { IRedstonePriceFeed } from "./interfaces/IRedstone.sol"; import { IChainlink } from "./interfaces/IChainlink.sol"; import { ICreateX } from "./interfaces/ICreateX.sol"; @@ -27,6 +28,8 @@ import { ICreateX } from "./interfaces/ICreateX.sol"; import { IPMarketV3 } from "pendle-core-v2-public/interfaces/IPMarketV3.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import {IPool} from "./interfaces/aerodrome/IPool.sol"; uint8 constant REDSTONE_DECIMALS = 8; @@ -53,14 +56,18 @@ IEtherFiLiquidityPool constant ETHER_FI_LIQUIDITY_POOL_ADDRESS = IWeEth constant WEETH_ADDRESS = IWeEth(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); IRedstonePriceFeed constant REDSTONE_WEETH_ETH_PRICE_FEED = IRedstonePriceFeed(0x8751F736E94F6CD167e8C5B97E245680FbD9CC36); +IChainlink constant BASE_WEETH_ETH_PRICE_CHAINLINK = IChainlink(0xFC1415403EbB0c693f9a7844b92aD2Ff24775C65); +IChainlink constant BASE_WEETH_ETH_EXCHANGE_RATE_CHAINLINK = IChainlink(0x35e9D7001819Ea3B39Da906aE6b06A62cfe2c181); // rsETH IRedstonePriceFeed constant REDSTONE_RSETH_ETH_PRICE_FEED = IRedstonePriceFeed(0xA736eAe8805dDeFFba40cAB8c99bCB309dEaBd9B); IRsEth constant RSETH = IRsEth(0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7); +IRsEth constant BASE_RSETH = IRsEth(0xEDfa23602D0EC14714057867A78d01e94176BEA0); ILRTOracle constant RSETH_LRT_ORACLE = ILRTOracle(0x349A73444b1a310BAe67ef67973022020d70020d); ILRTConfig constant RSETH_LRT_CONFIG = ILRTConfig(0x947Cb49334e6571ccBFEF1f1f1178d8469D65ec7); ILRTDepositPool constant RSETH_LRT_DEPOSIT_POOL = ILRTDepositPool(0x036676389e48133B63a802f8635AD39E752D375D); +IPool constant BASE_RSETH_WETH_AERODROME = IPool(0xA24382874A6FD59de45BbccFa160488647514c28); // rswETH IRedstonePriceFeed constant REDSTONE_RSWETH_ETH_PRICE_FEED = @@ -71,13 +78,18 @@ IRswEth constant RSWETH = IRswEth(0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0); IRedstonePriceFeed constant REDSTONE_EZETH_ETH_PRICE_FEED = IRedstonePriceFeed(0xF4a3e183F59D2599ee3DF213ff78b1B3b1923696); IEzEth constant EZETH = IEzEth(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); +IEzEth constant BASE_EZETH = IEzEth(0x2416092f143378750bb29b79eD961ab195CcEea5); IRenzoOracle constant RENZO_ORACLE = IRenzoOracle(0x5a12796f7e7EBbbc8a402667d266d2e65A814042); IRestakeManager constant RENZO_RESTAKE_MANAGER = IRestakeManager(0x74a09653A083691711cF8215a6ab074BB4e99ef5); +IPool constant BASE_EZTETH_WETH_AERODROME = IPool(0x0C8bF3cb3E1f951B284EF14aa95444be86a33E2f); // Chainlink IChainlink constant ETH_PER_STETH_CHAINLINK = IChainlink(0x86392dC19c0b719886221c78AB11eb8Cf5c52812); IChainlink constant MAINNET_USD_PER_ETH_CHAINLINK = IChainlink(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); - +IChainlink constant BASE_EZETH_ETH_PRICE_CHAINLINK = IChainlink(0x960BDD1dFD20d7c98fa482D793C3dedD73A113a3); +// will add address once rseth/eth feed is live on base, for now use ezeth/eth feed for spot and reserve oracles +IChainlink constant BASE_RSETH_ETH_PRICE_CHAINLINK = IChainlink(0x960BDD1dFD20d7c98fa482D793C3dedD73A113a3); +IChainlink constant BASE_RSETH_ETH_EXCHANGE_RATE_CHAINLINK = IChainlink(0xC4300B7CF0646F0Fe4C5B2ACFCCC4dCA1346f5d8); // Redstone IRedstonePriceFeed constant MAINNET_USD_PER_ETHX_REDSTONE = IRedstonePriceFeed(0xFaBEb1474C2Ab34838081BFdDcE4132f640E7D2d); @@ -88,6 +100,7 @@ IUniswapV3Pool constant MAINNET_WSTETH_WETH_UNISWAP = IUniswapV3Pool(0x109830a1A // Balancer bytes32 constant EZETH_WETH_BALANCER_POOL_ID = 0x596192bb6e41802428ac943d2f1476c1af25cc0e000000000000000000000659; +bytes32 constant BASE_EZETH_WETH_BALANCER_POOL_ID = 0x0000000000000000000000000000000000000000000000000000000000000000; // Pendle Pools IPMarketV3 constant PT_WEETH_POOL = IPMarketV3(0xF32e58F92e60f4b0A37A69b95d642A471365EAe8); @@ -97,3 +110,12 @@ IPMarketV3 constant PT_RSWETH_POOL = IPMarketV3(0x1729981345aa5CaCdc19eA9eeffea9 // CreateX ICreateX constant CREATEX = ICreateX(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); + +// --- BASE --- + +// EtherFi +bytes32 constant BASE_WEETH_WETH_BALANCER_POOL_ID = 0xab99a3e856deb448ed99713dfce62f937e2d4d74000000000000000000000118; +IUniswapV3Pool constant BASE_WSTETH_WETH_UNISWAP = IUniswapV3Pool(0x20E068D76f9E90b90604500B84c7e19dCB923e7e); +IChainlink constant BASE_SEQUENCER_UPTIME_FEED = IChainlink(0xBCF85224fc0756B9Fa45aA7892530B47e10b6433); +IERC20 constant BASE_WEETH = IERC20(0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A); +IWETH9 constant BASE_WETH = IWETH9(0x4200000000000000000000000000000000000006); diff --git a/src/flash/AerodromeFlashswapHandler.sol b/src/flash/AerodromeFlashswapHandler.sol new file mode 100644 index 00000000..d55596d0 --- /dev/null +++ b/src/flash/AerodromeFlashswapHandler.sol @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.21; + +import { IonHandlerBase } from "./IonHandlerBase.sol"; +import { WadRayMath } from "../libraries/math/WadRayMath.sol"; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IPool} from "../interfaces/aerodrome/IPool.sol"; +import {IIonPool} from "../interfaces/IIonPool.sol"; + +import {console} from "forge-std/Test.sol"; + +interface IPoolCallee { + function hook(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external; +} + +interface IPoolFactory { + function getFee(address pool, bool isStable) external view returns (uint256); +} + +/** + * @notice This contract allows for easy creation and closing of leverage + * positions through Aerodrome flashswaps--flashloan not necessary! In terms of + * creation, this may be a more desirable path than directly minting from an LRT/LST + * provider since market prices tend to be slightly lower than provider exchange + * rates. DEXes also provide an avenue for atomic deleveraging since the LRT/LST -> + * ETH exchange can be made. + * + * @dev When using the `AerodromeFlashSwapHandler`, the `IPool pool` fed to the + * constructor should be the WETH/[LRT/LST] pool. + * + * This flow can be used in case when the Aerodrome Pool has a collateral <> + * base asset pair. However, the current version of this contract always assumes + * that the base asset is `WETH`. + * + * Unlike Balancer flashloans, there is no concern here that somebody else could + * initiate a flashswap, then direct the callback to be called on this contract. + * + * @custom:security-contact security@molecularlabs.io + */ +abstract contract AerodromeFlashswapHandler is IonHandlerBase, IPoolCallee { + using WadRayMath for *; + using SafeCast for uint256; + using SafeERC20 for IERC20; + + error InvalidAerodromePool(); + error InvalidZeroLiquidityRegionSwap(); + + error FlashswapRepaymentTooExpensive(uint256 amountIn, uint256 maxAmountIn); + error CallbackOnlyCallableByPool(address unauthorizedCaller); + error OutputAmountNotReceived(uint256 amountReceived, uint256 amountRequired); + error ZeroAmountIn(); + error AmountInTooHigh(uint256 amountIn, uint256 maxAmountIn); + error SwapOnlyCallableByHandler(address sender); + + IPool public immutable AERODROME_POOL; + bool private immutable WETH_IS_TOKEN0; + + /** + * @notice Creates a new `AerodromeFlashswapHandler` instance. + * @param _pool Pool to perform the flashswap on. + */ + constructor(IPool _pool, bool /*_wethIsToken0*/){ + if (address(_pool) == address(0)) revert InvalidAerodromePool(); + + address token0 = _pool.token0(); + address token1 = _pool.token1(); + + // I added this + // require(_wethIsToken0 && token0 == address(WETH) || !_wethIsToken0 && token1 == address(WETH), "incorrect weth is token 0"); + + if (token0 != address(WETH) && token1 != address(WETH)) revert InvalidAerodromePool(); + if (token0 == address(WETH) && token1 == address(WETH)) revert InvalidAerodromePool(); + + AERODROME_POOL = _pool; + + WETH_IS_TOKEN0 = token0 == address(WETH); + } + + struct FlashSwapData { + address user; + uint256 changeInCollateralOrDebt; + uint256 amountToPay; + address tokenIn; + address tokenOut; + bool isLeverage; + } + + /** + * @notice Transfer collateral from user -> initiate swap for collateral from + * WETH on Aerodrome (contract will receive collateral first) -> deposit all + * collateral into `IonPool` -> borrow WETH from `IonPool` -> complete swap + * by sending WETH to Aerodrome. + * + * @param initialDeposit in collateral terms. [WAD] + * @param resultingAdditionalCollateral in collateral terms. [WAD] + * @param maxResultingAdditionalDebt in WETH terms. This value also allows + * the user to control slippage of the swap. [WAD] + * @param deadline timestamp for which the transaction must be executed. + * This prevents txs that have sat in the mempool for too long to be + * executed. + * @param proof that the user is whitelisted. + */ + function flashswapLeverage( + uint256 initialDeposit, + uint256 resultingAdditionalCollateral, + uint256 maxResultingAdditionalDebt, + uint256 deadline, + bytes32[] calldata proof + ) + external + checkDeadline(deadline) + onlyWhitelistedBorrowers(proof) + { + LST_TOKEN.safeTransferFrom(msg.sender, address(this), initialDeposit); + _flashswapLeverage(initialDeposit, resultingAdditionalCollateral, maxResultingAdditionalDebt); + } + + /** + * @param initialDeposit in terms of LRT + * @param resultingAdditionalCollateral in terms of LRT. How much + * collateral to add to the position in the vault. + * @param maxResultingAdditionalDebt in terms of WETH. How much debt to add + * to the position in the vault. + */ + function _flashswapLeverage( + uint256 initialDeposit, + uint256 resultingAdditionalCollateral, + uint256 maxResultingAdditionalDebt + ) + internal + { + uint256 amountToLeverage = resultingAdditionalCollateral - initialDeposit; // in swETH + + if (amountToLeverage == 0) { + // AmountToBorrow.IS_MAX because we don't want to create any new debt here + _depositAndBorrow(msg.sender, address(this), resultingAdditionalCollateral, 0, AmountToBorrow.IS_MAX); + return; + } + + // Flashswap WETH for LRT collateral. We will receive collateral first and then + // return the WETH inside the Uniswap callback + + console.log("Amount Out: ", amountToLeverage); + console.log("Before K: ", AERODROME_POOL.getK()); + console.log("balance of pool in collateral pre: ", LST_TOKEN.balanceOf(address(AERODROME_POOL))); + console.log("balance of pool in WETH pre: ", WETH.balanceOf(address(AERODROME_POOL))); + // leverage case token going in to pool is WETH and coming from pool to handler is collateral token + (uint256 reserveIn, uint256 reserveOut,) = AERODROME_POOL.getReserves(); + uint256 balanceIn = WETH.balanceOf(address(AERODROME_POOL)); + uint256 balanceOut = LST_TOKEN.balanceOf(address(AERODROME_POOL)); + if (!WETH_IS_TOKEN0) { + (reserveIn, reserveOut) = (reserveOut, reserveIn); + } + if(reserveIn != balanceIn || reserveOut != balanceOut){ + console.log("Reserves and balances are not equal"); + // sync balances with reserves to avoid cases where there are unpredictable fee calculations + IPool(address(AERODROME_POOL)).sync(); + } + // revert if trying to take all (or more) of the collateral + if(amountToLeverage >= balanceOut){ + revert AmountInTooHigh(amountToLeverage, balanceOut); + } + + uint256 amountToPay = _calculateAmountToPay(balanceIn, balanceOut, amountToLeverage, balanceIn*balanceOut); + console.log("Amount to Pay: ", amountToPay); + + // This protects against a potential sandwich attack + if (amountToPay > maxResultingAdditionalDebt) revert FlashswapRepaymentTooExpensive(amountToPay, maxResultingAdditionalDebt); + + FlashSwapData memory flashswapData = FlashSwapData({ + user: msg.sender, + changeInCollateralOrDebt: resultingAdditionalCollateral, + amountToPay: amountToPay, + tokenIn: address(WETH), + tokenOut: address(LST_TOKEN), + isLeverage: true + }); + + _initiateFlashSwap(WETH_IS_TOKEN0, amountToLeverage, address(this), flashswapData); + + console.log("AfterK actual ", AERODROME_POOL.getK()); + console.log("balance of pool in collateral post: ", LST_TOKEN.balanceOf(address(AERODROME_POOL))); + console.log("balance of pool in WETH post: ", WETH.balanceOf(address(AERODROME_POOL))); + } + + /** + * @notice Initiate swap for WETH from collateral (contract will receive + * WETH first) -> repay debt on `IonPool` -> withdraw (and gem-exit) + * collateral from `IonPool` -> complete swap by sending collateral to + * Aerodrome. + * + * @dev The two function parameters must be chosen carefully. If + * `maxCollateralToRemove`'s ETH valuation were higher then `debtToRemove`, + * it would theoretically be possible to sell more collateral then was + * required for `debtToRemove` to be repaid (even if `debtToRemove` is worth + * nowhere near that valuation) due to the slippage of the sell. + * `maxCollateralToRemove` is essentially a slippage guard here. + * @param maxCollateralToRemove he max amount of collateral user is willing + * to sell to repay `debtToRemove` debt. [WAD] + * @param debtToRemove The desired amount of debt to remove. [WAD] + */ + function flashswapDeleverage( + uint256 maxCollateralToRemove, + uint256 debtToRemove, + uint256 deadline + ) + external + checkDeadline(deadline) + { + if (debtToRemove == type(uint256).max) { + (debtToRemove,) = _getFullRepayAmount(msg.sender); + } + + if (debtToRemove == 0) return; + + console.log("Before K: ", AERODROME_POOL.getK()); + console.log("balance of pool in collateral pre: ", LST_TOKEN.balanceOf(address(AERODROME_POOL))); + console.log("balance of pool in WETH pre: ", WETH.balanceOf(address(AERODROME_POOL))); + + (uint256 reserveOut, uint256 reserveIn,) = AERODROME_POOL.getReserves(); + uint256 balanceIn = LST_TOKEN.balanceOf(address(AERODROME_POOL)); + uint256 balanceOut = WETH.balanceOf(address(AERODROME_POOL)); + if (!WETH_IS_TOKEN0) { + (reserveOut, reserveIn) = (reserveIn, reserveOut); + } + if(reserveOut != balanceOut || reserveIn != balanceIn){ + console.log("Reserves and balances are not equal"); + // sync balances with reserves to avoid cases where there are unpredictable fee calculations + IPool(address(AERODROME_POOL)).sync(); + } + + // revert if trying to take all (or more) of the weth + if(debtToRemove >= balanceOut){ + revert AmountInTooHigh(debtToRemove, balanceOut); + } + uint256 amountToPay = _calculateAmountToPay(balanceIn, balanceOut, debtToRemove, balanceIn*balanceOut); + + // This protects against a potential sandwich attack + if (amountToPay > maxCollateralToRemove) revert FlashswapRepaymentTooExpensive(amountToPay, maxCollateralToRemove); + + FlashSwapData memory flashswapData = FlashSwapData({ + user: msg.sender, + changeInCollateralOrDebt: debtToRemove, + amountToPay: amountToPay, + tokenIn: address(LST_TOKEN), + tokenOut: address(WETH), + isLeverage: false + }); + + _initiateFlashSwap(!WETH_IS_TOKEN0, debtToRemove, address(this), flashswapData); + + console.log("AfterK actual ", AERODROME_POOL.getK()); + console.log("balance of pool in collateral post: ", LST_TOKEN.balanceOf(address(AERODROME_POOL))); + console.log("balance of pool in WETH post: ", WETH.balanceOf(address(AERODROME_POOL))); + } + + /** + * @notice Handles swap initiation logic. This function can only initiate + * exact output swaps. + * @param zeroForOne Direction of the swap. + * @param amountOut Desired amount of output. + * @param recipient of output tokens. + * @param data Arbitrary data to be passed through swap callback. + */ + function _initiateFlashSwap( + bool zeroForOne, + uint256 amountOut, + address recipient, + FlashSwapData memory data + ) + private + { + // the following are AerodromePool.swap()s first 3 inputs: + // @param amount0Out Amount of token0 to send to `to` + // @param amount1Out Amount of token1 to send to `to` + // @param to Address to recieve the swapped output + if(zeroForOne){ + AERODROME_POOL.swap(0, amountOut, recipient, abi.encode(data)); + }else{ + AERODROME_POOL.swap(amountOut, 0, recipient, abi.encode(data)); + } + } + + /** + * @notice From the perspective of the pool. This function is intended to never be called directly. It should + * only be called by the Aerodrome pool during a swap initiated by this + * contract. + * + * @dev One thing to note from a security perspective is that the pool only calls + * the callback on `msg.sender`. So a theoretical attacker cannot call this + * function by directing where to call the callback. + * + * @param sender address which called swap function (this handler address only!) + * @param amount0 change in token0 + * @param amount1 change in token1 + * @param _data flashswap data + */ + function hook(address sender, uint256 amount0, uint256 amount1, bytes calldata _data) external override { + if (msg.sender != address(AERODROME_POOL)) revert CallbackOnlyCallableByPool(msg.sender); + if(sender != address(this)) revert SwapOnlyCallableByHandler(sender); + + // swaps entirely within 0-liquidity regions are not supported + if (amount0 == 0 && amount1 == 0) revert InvalidZeroLiquidityRegionSwap(); + FlashSwapData memory data = abi.decode(_data, (FlashSwapData)); + + address tokenIn = data.tokenIn; + address tokenOut = data.tokenOut; + uint256 amountToPay = data.amountToPay; + console.log("Amount To pay", amountToPay); + + console.log("HOOK EXECUTED"); + console.log("Amount0: ", amount0); + console.log("Amount1: ", amount1); + console.log("TokenIn: ", tokenIn); + console.log("TokenOut: ", tokenOut); + console.log("Balance of this in Collateral: ", LST_TOKEN.balanceOf(address(this))); + console.log("Balance of this in WETH: ", WETH.balanceOf(address(this))); + console.log("Balance of pool in collateral start of hook: ", LST_TOKEN.balanceOf(address(AERODROME_POOL))); + console.log("Balance of pool in WETH start of hook: ", WETH.balanceOf(address(AERODROME_POOL))); + + // leverage case + if(data.isLeverage){ + console.log("leverage"); + console.log("Change in col: ", data.changeInCollateralOrDebt); + _depositAndBorrow( + data.user, address(this), data.changeInCollateralOrDebt, amountToPay, AmountToBorrow.IS_MIN + ); + console.log("Balance of this in Collateral: ", LST_TOKEN.balanceOf(address(this))); + console.log("Balance of this in WETH: ", WETH.balanceOf(address(this))); + } + // deleverage case + else { + console.log("deleverage"); + _repayAndWithdraw(data.user, address(this), amountToPay, data.changeInCollateralOrDebt); + console.log("Balance of this in Collateral: ", LST_TOKEN.balanceOf(address(this))); + console.log("Balance of this in WETH: ", WETH.balanceOf(address(this))); + } + console.log("sending back: ", amountToPay); + console.log("of: ", tokenIn); + IERC20(tokenIn).safeTransfer(msg.sender, amountToPay); + + console.log("Balance of this in Collateral end of hook: ", LST_TOKEN.balanceOf(address(this))); + console.log("Balance of this in WETH end of hook: ", WETH.balanceOf(address(this))); + console.log("Balance of pool in collateral end of hook: ", LST_TOKEN.balanceOf(address(AERODROME_POOL))); + console.log("Balance of pool in WETH end of hook: ", WETH.balanceOf(address(AERODROME_POOL))); + console.log("After K manual: ", (IERC20(tokenIn).balanceOf(address(AERODROME_POOL)) - 30*amountToPay / 10000 )* (IERC20(tokenOut).balanceOf(address(AERODROME_POOL)))); + } + + function getAmountOutGivenAmountIn (uint256 amountIn, bool isLeverage) external view returns(uint256 amountOut){ + if(amountIn == 0){ + revert ZeroAmountIn(); + } + uint256 balanceWeth = WETH.balanceOf(address(AERODROME_POOL)); + uint256 balanceCollateral = LST_TOKEN.balanceOf(address(AERODROME_POOL)); + uint256 maxAmountIn = isLeverage ? balanceCollateral : balanceWeth; + if(amountIn >= maxAmountIn){ + revert AmountInTooHigh(amountIn, maxAmountIn); + } + if(isLeverage){ + return _calculateAmountToPay(balanceWeth, balanceCollateral, amountIn, balanceWeth*balanceCollateral); + } + return _calculateAmountToPay(balanceCollateral, balanceWeth, amountIn, balanceWeth*balanceCollateral); + } + + // F = Fee multiplier e.g. 0.3% for 30 bps + // a = amount Token Out from pool (e.g. LRT for leverage or WETH for deleverage) + // balOut = out token reserve BEFORE a was taken out (after sync is called will be current initial balance) + // balIn = in token reserve initially (after sync is called will be current initial balance) + // b = amountToPay of in token (this is unknown and what we are solving for) + // + // balOut * balIn = (balOut - a) * (balIn + b(1-F)) => + // balOut * balIn = balOut * balIn - a * balIn + balOut * b(1-F) - a * b(1-F) => + // a * balIn = b(1-F)(balOut - a) => + // a * balIn / [(1-F)(balOut - a)] = b => note* 1-F = (10000 - fee)/ 10000 + // 10000 * a * balIn / [(10000 - fee) * (balOut - a)] = b + + function _calculateAmountToPay(uint256 balIn, uint256 balOut, uint256 amountChangeCollOrDebt, uint256 poolKBefore) internal view returns(uint256 amountToPay){ + address factory = AERODROME_POOL.factory(); + uint256 fee = IPoolFactory(factory).getFee(address(AERODROME_POOL), false); + uint256 a = amountChangeCollOrDebt; + + amountToPay = (10000 * a * balIn ) / (9970 * (balOut-a)); + + uint256 afterK = (balIn + amountToPay - (fee*amountToPay) / 10000 )* (balOut - a); + + if(afterK < poolKBefore){ + console.log("K is less than before"); + amountToPay += 1; + } + else{ + console.log("K is greater than before"); + } + console.log("Amount to Pay inside helper: ", amountToPay); + console.log("afterK inside helper: ", afterK); + return amountToPay; + } +} diff --git a/src/flash/lrt/BaseRsEthHandler.sol b/src/flash/lrt/BaseRsEthHandler.sol new file mode 100644 index 00000000..1747e87f --- /dev/null +++ b/src/flash/lrt/BaseRsEthHandler.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.21; + +import { IonPool } from "../../IonPool.sol"; +import { GemJoin } from "../../join/GemJoin.sol"; +import { Whitelist } from "../../Whitelist.sol"; +import { AerodromeFlashswapHandler, IPool } from "./../AerodromeFlashswapHandler.sol"; +import { IonHandlerBase } from "../IonHandlerBase.sol"; +import { WETH_ADDRESS } from "../../Constants.sol"; + +import { IWETH9 } from "./../../interfaces/IWETH9.sol"; + +/** + * @notice Handler for the rsETH collateral. + * + * @custom:security-contact security@molecularlabs.io + */ +contract BaseRsEthHandler is AerodromeFlashswapHandler { + /** + * @notice Creates a new `RsEthHandler` instance. + * @param _ilkIndex Ilk index of the pool. + * @param _ionPool address. + * @param _gemJoin address. + * @param _whitelist address. + * @param _wrsEthAerodromePool address of the wrsETH/WETH Aerodrome pool (0.3% fee). + */ + constructor( + uint8 _ilkIndex, + IonPool _ionPool, + GemJoin _gemJoin, + Whitelist _whitelist, + IPool _wrsEthAerodromePool, + IWETH9 _weth + ) + IonHandlerBase(_ilkIndex, _ionPool, _gemJoin, _whitelist, _weth) + AerodromeFlashswapHandler(_wrsEthAerodromePool, true) + { } + +} diff --git a/src/interfaces/aerodrome/IPool.sol b/src/interfaces/aerodrome/IPool.sol new file mode 100644 index 00000000..a1536273 --- /dev/null +++ b/src/interfaces/aerodrome/IPool.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +interface IPool { + error DepositsNotEqual(); + error BelowMinimumK(); + error FactoryAlreadySet(); + error InsufficientLiquidity(); + error InsufficientLiquidityMinted(); + error InsufficientLiquidityBurned(); + error InsufficientOutputAmount(); + error InsufficientInputAmount(); + error IsPaused(); + error InvalidTo(); + error K(); + error NotEmergencyCouncil(); + + event Fees(address indexed sender, uint256 amount0, uint256 amount1); + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event Burn(address indexed sender, address indexed to, uint256 amount0, uint256 amount1); + event Swap( + address indexed sender, + address indexed to, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out + ); + event Sync(uint256 reserve0, uint256 reserve1); + event Claim(address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1); + + // Struct to capture time period obervations every 30 minutes, used for local oracles + struct Observation { + uint256 timestamp; + uint256 reserve0Cumulative; + uint256 reserve1Cumulative; + } + + /// @notice Returns the decimal (dec), reserves (r), stable (st), and tokens (t) of token0 and token1 + function metadata() + external + view + returns (uint256 dec0, uint256 dec1, uint256 r0, uint256 r1, bool st, address t0, address t1); + + /// @notice Claim accumulated but unclaimed fees (claimable0 and claimable1) + function claimFees() external returns (uint256, uint256); + + /// @notice Returns [token0, token1] + function tokens() external view returns (address, address); + + function token0() external view returns (address); + + /// @notice Address of token in the poool with the higher address value + function token1() external view returns (address); + + /// @notice Address of linked PoolFees.sol + function poolFees() external view returns (address); + + /// @notice Address of PoolFactory that created this contract + function factory() external view returns (address); + + /// @notice Capture oracle reading every 30 minutes (1800 seconds) + function periodSize() external view returns (uint256); + + /// @notice Amount of token0 in pool + function reserve0() external view returns (uint256); + + /// @notice Amount of token1 in pool + function reserve1() external view returns (uint256); + + /// @notice Timestamp of last update to pool + function blockTimestampLast() external view returns (uint256); + + /// @notice Cumulative of reserve0 factoring in time elapsed + function reserve0CumulativeLast() external view returns (uint256); + + /// @notice Cumulative of reserve1 factoring in time elapsed + function reserve1CumulativeLast() external view returns (uint256); + + /// @notice Accumulated fees of token0 (global) + function index0() external view returns (uint256); + + /// @notice Accumulated fees of token1 (global) + function index1() external view returns (uint256); + + /// @notice Get an LP's relative index0 to index0 + function supplyIndex0(address) external view returns (uint256); + + /// @notice Get an LP's relative index1 to index1 + function supplyIndex1(address) external view returns (uint256); + + /// @notice Amount of unclaimed, but claimable tokens from fees of token0 for an LP + function claimable0(address) external view returns (uint256); + + /// @notice Amount of unclaimed, but claimable tokens from fees of token1 for an LP + function claimable1(address) external view returns (uint256); + + /// @notice Returns the value of K in the Pool, based on its reserves. + function getK() external returns (uint256); + + /// @notice Set pool name + /// Only callable by Voter.emergencyCouncil() + /// @param __name String of new name + function setName(string calldata __name) external; + + /// @notice Set pool symbol + /// Only callable by Voter.emergencyCouncil() + /// @param __symbol String of new symbol + function setSymbol(string calldata __symbol) external; + + /// @notice Get the number of observations recorded + function observationLength() external view returns (uint256); + + /// @notice Get the value of the most recent observation + function lastObservation() external view returns (Observation memory); + + /// @notice True if pool is stable, false if volatile + function stable() external view returns (bool); + + /// @notice Produces the cumulative price using counterfactuals to save gas and avoid a call to sync. + function currentCumulativePrices() + external + view + returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp); + + /// @notice Provides twap price with user configured granularity, up to the full window size + /// @param tokenIn . + /// @param amountIn . + /// @param granularity . + /// @return amountOut . + function quote(address tokenIn, uint256 amountIn, uint256 granularity) external view returns (uint256 amountOut); + + /// @notice Returns a memory set of TWAP prices + /// Same as calling sample(tokenIn, amountIn, points, 1) + /// @param tokenIn . + /// @param amountIn . + /// @param points Number of points to return + /// @return Array of TWAP prices + function prices(address tokenIn, uint256 amountIn, uint256 points) external view returns (uint256[] memory); + + /// @notice Same as prices with with an additional window argument. + /// Window = 2 means 2 * 30min (or 1 hr) between observations + /// @param tokenIn . + /// @param amountIn . + /// @param points . + /// @param window . + /// @return Array of TWAP prices + function sample( + address tokenIn, + uint256 amountIn, + uint256 points, + uint256 window + ) external view returns (uint256[] memory); + + /// @notice This low-level function should be called from a contract which performs important safety checks + /// @param amount0Out Amount of token0 to send to `to` + /// @param amount1Out Amount of token1 to send to `to` + /// @param to Address to recieve the swapped output + /// @param data Additional calldata for flashloans + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; + + /// @notice This low-level function should be called from a contract which performs important safety checks + /// standard uniswap v2 implementation + /// @param to Address to receive token0 and token1 from burning the pool token + /// @return amount0 Amount of token0 returned + /// @return amount1 Amount of token1 returned + function burn(address to) external returns (uint256 amount0, uint256 amount1); + + /// @notice This low-level function should be called by addLiquidity functions in Router.sol, which performs important safety checks + /// standard uniswap v2 implementation + /// @param to Address to receive the minted LP token + /// @return liquidity Amount of LP token minted + function mint(address to) external returns (uint256 liquidity); + + /// @notice Update reserves and, on the first call per block, price accumulators + /// @return _reserve0 . + /// @return _reserve1 . + /// @return _blockTimestampLast . + function getReserves() external view returns (uint256 _reserve0, uint256 _reserve1, uint256 _blockTimestampLast); + + /// @notice Get the amount of tokenOut given the amount of tokenIn + /// @param amountIn Amount of token in + /// @param tokenIn Address of token + /// @return Amount out + function getAmountOut(uint256 amountIn, address tokenIn) external view returns (uint256); + + /// @notice Force balances to match reserves + /// @param to Address to receive any skimmed rewards + function skim(address to) external; + + /// @notice Force reserves to match balances + function sync() external; + + /// @notice Called on pool creation by PoolFactory + /// @param _token0 Address of token0 + /// @param _token1 Address of token1 + /// @param _stable True if stable, false if volatile + function initialize(address _token0, address _token1, bool _stable) external; +} \ No newline at end of file diff --git a/src/oracles/reserve/lrt/base/BaseRsEthWethReserveOracle.sol b/src/oracles/reserve/lrt/base/BaseRsEthWethReserveOracle.sol new file mode 100644 index 00000000..e0e3412e --- /dev/null +++ b/src/oracles/reserve/lrt/base/BaseRsEthWethReserveOracle.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { WadRayMath } from "../../../../libraries/math/WadRayMath.sol"; +import { ReserveOracle } from "../../ReserveOracle.sol"; +import { BASE_RSETH_ETH_EXCHANGE_RATE_CHAINLINK, BASE_SEQUENCER_UPTIME_FEED } from "../../../../Constants.sol"; +import { SafeCast } from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; + +/** + * @notice Reserve Oracle for rsETH denominated in WETH. + * + * @custom:security-contact security@molecularlabs.io + */ +contract BaseRsEthWethReserveOracle is ReserveOracle { + using WadRayMath for uint256; + using SafeCast for int256; + + error SequencerDown(); + error GracePeriodNotOver(); + error MaxTimeFromLastUpdateExceeded(uint256, uint256); + + uint256 public immutable MAX_TIME_FROM_LAST_UPDATE; // seconds + uint256 public immutable GRACE_PERIOD; + + /** + * @notice Creates a new `BaseRsEthWethReserveOracle` instance. Provides + * the amount of WETH equal to one rsETH (ETH / rsETH). + * @dev The value of rsETH denominated in WETH by Chainlink. + * @param _feeds List of alternative data sources for the WETH/rsETH exchange rate. + * @param _quorum The amount of alternative data sources to aggregate. + * @param _maxChange Maximum percent change between exchange rate updates. [RAY] + */ + constructor( + uint8 _ilkIndex, + address[] memory _feeds, + uint8 _quorum, + uint256 _maxChange, + uint256 _maxTimeFromLastUpdate, + uint256 _gracePeriod + ) + ReserveOracle(_ilkIndex, _feeds, _quorum, _maxChange) + { + MAX_TIME_FROM_LAST_UPDATE = _maxTimeFromLastUpdate; + GRACE_PERIOD = _gracePeriod; + _initializeExchangeRate(); + } + + function _getProtocolExchangeRate() internal view override returns (uint256) { + ( + /*uint80 roundID*/ + , + int256 answer, + uint256 startedAt, + /*uint256 updatedAt*/ + , + /*uint80 answeredInRound*/ + ) = BASE_SEQUENCER_UPTIME_FEED.latestRoundData(); + + if (answer == 1) revert SequencerDown(); + if (block.timestamp - startedAt <= GRACE_PERIOD) revert GracePeriodNotOver(); + + (, int256 ethPerRsEth,, uint256 ethPerRsEthUpdatedAt,) = + BASE_RSETH_ETH_EXCHANGE_RATE_CHAINLINK.latestRoundData(); + + if (block.timestamp - ethPerRsEthUpdatedAt > MAX_TIME_FROM_LAST_UPDATE) { + revert MaxTimeFromLastUpdateExceeded(block.timestamp - ethPerRsEthUpdatedAt, MAX_TIME_FROM_LAST_UPDATE); + } else { + return ethPerRsEth.toUint256(); + } + } +} \ No newline at end of file diff --git a/src/oracles/spot/base/BaseRsEthWethSpotOracle.sol b/src/oracles/spot/base/BaseRsEthWethSpotOracle.sol new file mode 100644 index 00000000..7c3663e2 --- /dev/null +++ b/src/oracles/spot/base/BaseRsEthWethSpotOracle.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.21; + +import { SpotOracle } from "../../../../oracles/spot/SpotOracle.sol"; +import { WadRayMath } from "../../../../libraries/math/WadRayMath.sol"; +import { BASE_SEQUENCER_UPTIME_FEED, BASE_RSETH_ETH_PRICE_CHAINLINK } from "../../../../Constants.sol"; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/** + * @notice The rsETH spot oracle denominated in WETH on Base. + * + * @custom:security-contact security@molecularlabs.io + */ +contract BaseRsEthWethSpotOracle is SpotOracle { + using WadRayMath for uint256; + using SafeCast for int256; + + error SequencerDown(); + error GracePeriodNotOver(); + + /** + * @notice The maximum delay for the oracle update in seconds before the + * data is considered stale. + */ + uint256 public immutable MAX_TIME_FROM_LAST_UPDATE; // seconds + + /** + * @notice Amount of time to wait after the sequencer restarts. + */ + uint256 public immutable GRACE_PERIOD; + + /** + * @notice Creates a new `BaseRsEthWethSpotOracle` instance. + * @param _ltv The loan to value ratio for the RsETH/WETH market. + * @param _reserveOracle The associated reserve oracle. + * @param _maxTimeFromLastUpdate The maximum delay for the oracle update in seconds + */ + constructor( + uint256 _ltv, + address _reserveOracle, + uint256 _maxTimeFromLastUpdate, + uint256 _gracePeriod + ) + SpotOracle(_ltv, _reserveOracle) + { + MAX_TIME_FROM_LAST_UPDATE = _maxTimeFromLastUpdate; + GRACE_PERIOD = _gracePeriod; + } + + /** + * @notice Gets the price of RsETH in WETH. + * @return wethPerRsEth price of RsETH in WETH. [WAD] + */ + function getPrice() public view override returns (uint256) { + ( + /*uint80 roundID*/ + , + int256 answer, + uint256 startedAt, + /*uint256 updatedAt*/ + , + /*uint80 answeredInRound*/ + ) = BASE_SEQUENCER_UPTIME_FEED.latestRoundData(); + + if (answer == 1) revert SequencerDown(); + if (block.timestamp - startedAt <= GRACE_PERIOD) revert GracePeriodNotOver(); + + ( + /*uint80 roundID*/ + , + int256 ethPerRsEth, + /*uint startedAt*/ + , + uint256 ethPerRsEthUpdatedAt, + /*uint80 answeredInRound*/ + ) = BASE_EZETH_ETH_PRICE_CHAINLINK.latestRoundData(); // [WAD] + + if (block.timestamp - ethPerRsEthUpdatedAt > MAX_TIME_FROM_LAST_UPDATE) { + return 0; // collateral valuation is zero if oracle data is stale + } else { + return ethPerRsEth.toUint256(); // [wad] + } + } +} \ No newline at end of file diff --git a/test/fork/concrete/handlers-base/AerodromeFlashswapHandler.t.sol b/test/fork/concrete/handlers-base/AerodromeFlashswapHandler.t.sol new file mode 100644 index 00000000..82ff08a1 --- /dev/null +++ b/test/fork/concrete/handlers-base/AerodromeFlashswapHandler.t.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { LrtHandler_ForkBase } from "../../../helpers/handlers/LrtHandlerForkBase.sol"; +import { WadRayMath, RAY, WAD } from "../../../../src/libraries/math/WadRayMath.sol"; +import { AerodromeFlashswapHandler } from "../../../../src/flash/AerodromeFlashswapHandler.sol"; +import { IonHandlerBase } from "../../../../src/flash/IonHandlerBase.sol"; +import { Whitelist } from "../../../../src/Whitelist.sol"; +import { BASE_RSETH_WETH_AERODROME, BASE_RSETH, BASE_WETH } from "../../../../src/Constants.sol"; +import { IPool } from "../../../../src/interfaces/aerodrome/IPool.sol"; + +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +import { Vm } from "forge-std/Vm.sol"; +import { console2 } from "forge-std/console2.sol"; +import { StdUtils } from "forge-std/Test.sol"; + +using WadRayMath for uint256; + +interface IPoolFactory { + function getFee(address pool, bool isStable) external view returns (uint256); +} + +abstract contract AerodromeFlashswapHandler_Test is LrtHandler_ForkBase { + + function testFork_FlashswapLeverage() external { + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = 6e18; // In weth + + console2.log("initial deposit: %d", initialDeposit); + console2.log("resulting additional collateral: %d", resultingAdditionalCollateral); + console2.log("max resulting debt: %d", maxResultingDebt); + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.expectRevert(abi.encodeWithSelector(IonHandlerBase.TransactionDeadlineReached.selector, block.timestamp)); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + block.timestamp, + borrowerWhitelistProof + ); + + if (Whitelist(whitelist).borrowersRoot(0) != 0) { + vm.expectRevert(abi.encodeWithSelector(Whitelist.NotWhitelistedBorrower.selector, 0, address(this))); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + block.timestamp + 1, + new bytes32[](0) + ); + } + + uint256 gasBefore = gasleft(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + block.timestamp + 1, + borrowerWhitelistProof + ); + uint256 gasAfter = gasleft(); + if (vm.envOr("SHOW_GAS", uint256(0)) == 1) console2.log("Gas used: %d", gasBefore - gasAfter); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral); + // assertEq(IERC20(address(MAINNET_SWELL)).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + } + + function testFork_FlashswapDeleverage() external { + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = type(uint256).max; + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.recordLogs(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + block.timestamp + 1, + borrowerWhitelistProof + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 normalizedDebtCreated; + for (uint256 i = 0; i < entries.length; i++) { + // keccak256("Borrow(uint8,address,address,uint256,uint256,uint256)") + if (entries[i].topics[0] != 0xe3e92e977f830d2a0b92c58e8866694b5dc929a35e2b95846f427de0f0bb412f) continue; + normalizedDebtCreated = abi.decode(entries[i].data, (uint256)); + } + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), normalizedDebtCreated); + + vm.warp(block.timestamp + 3 hours); + + uint256 slippageAndFeeTolerance = 1.007e18; // 0.7% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingAdditionalCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + // Remove all debt + uint256 normalizedDebtToRemove = ionPool.normalizedDebt(_getIlkIndex(), address(this)); + + // Round up otherwise can leave 1 wei of dust in debt left + uint256 debtToRemove = normalizedDebtToRemove.rayMulUp(ionPool.rate(_getIlkIndex())); + + vm.expectRevert(abi.encodeWithSelector(IonHandlerBase.TransactionDeadlineReached.selector, block.timestamp)); + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, block.timestamp); + + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, block.timestamp + 1); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertGe( + ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral - maxCollateralToRemove + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), 0); + // assertEq(IERC20(address(MAINNET_SWELL)).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + } + + function testFork_FlashswapDeleverageFull() external { + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = type(uint256).max; + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.recordLogs(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + block.timestamp + 1, + borrowerWhitelistProof + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 normalizedDebtCreated; + for (uint256 i = 0; i < entries.length; i++) { + // keccak256("Borrow(uint8,address,address,uint256,uint256,uint256)") + if (entries[i].topics[0] != 0xe3e92e977f830d2a0b92c58e8866694b5dc929a35e2b95846f427de0f0bb412f) continue; + normalizedDebtCreated = abi.decode(entries[i].data, (uint256)); + } + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), normalizedDebtCreated); + + uint256 slippageAndFeeTolerance = 1.007e18; // 0.7% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingAdditionalCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + + // Remove all debt + uint256 debtToRemove = type(uint256).max; + + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, block.timestamp + 1); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + // assertGe( + // ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral - maxCollateralToRemove + // ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), 0); + // assertEq(IERC20(address(MAINNET_SWELL)).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + } + + function testFork_RevertWhen_HandlerIsNotSwapCaller() external { + vm.skip(borrowerWhitelistProof.length > 0); + + vm.expectRevert( + abi.encodeWithSelector(AerodromeFlashswapHandler.SwapOnlyCallableByHandler.selector, address(this)) + ); + IPool(BASE_RSETH_WETH_AERODROME).swap(1e18, 0, address(_getTypedUFHandler()), "0x1"); + } + + function testFork_RevertWhen_UntrustedCallerCallsFlashswapCallback() external { + vm.skip(borrowerWhitelistProof.length > 0); + + vm.expectRevert( + abi.encodeWithSelector(AerodromeFlashswapHandler.CallbackOnlyCallableByPool.selector, address(this)) + ); + _getTypedUFHandler().hook(address(_getHandler()), 1, 1, ""); + } + + function testFork_RevertWhen_TradingInZeroLiquidityRegion() external { + vm.skip(borrowerWhitelistProof.length > 0); + + vm.startPrank(address(BASE_RSETH_WETH_AERODROME)); + vm.expectRevert(AerodromeFlashswapHandler.InvalidZeroLiquidityRegionSwap.selector); + _getTypedUFHandler().hook(address(_getTypedUFHandler()), 0, 0, ""); + vm.stopPrank(); + } + + function testFork_RevertWhen_FlashswapLeverageCreatesMoreDebtThanUserIsWilling() external { + vm.skip(borrowerWhitelistProof.length > 0); + + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = 3e18; // In weth + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.expectRevert(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + block.timestamp + 1, + new bytes32[](0) + ); + } + + function testFork_RevertWhen_FlashswapDeleverageSellsMoreCollateralThanUserIsWilling() external { + vm.skip(borrowerWhitelistProof.length > 0); + + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = type(uint256).max; + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + block.timestamp + 1, + new bytes32[](0) + ); + + uint256 slippageAndFeeTolerance = 1.0e18; // 0% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingAdditionalCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + // Remove all debt + uint256 normalizedDebtToRemove = ionPool.normalizedDebt(_getIlkIndex(), address(this)); + + // Round up otherwise can leave 1 wei of dust in debt left + uint256 debtToRemove = normalizedDebtToRemove.rayMulUp(ionPool.rate(_getIlkIndex())); + + vm.expectRevert(); + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, block.timestamp + 1); + } + + function _getTypedUFHandler() private view returns (AerodromeFlashswapHandler) { + return AerodromeFlashswapHandler(payable(_getHandler())); + } +} diff --git a/test/fork/concrete/lrt/BaseMainnet/RsEthWethHandler.t.sol b/test/fork/concrete/lrt/BaseMainnet/RsEthWethHandler.t.sol new file mode 100644 index 00000000..cc785ca3 --- /dev/null +++ b/test/fork/concrete/lrt/BaseMainnet/RsEthWethHandler.t.sol @@ -0,0 +1,87 @@ +pragma solidity ^0.8.21; + +import { BaseRsEthHandler } from "../../../../../src/flash/lrt/BaseRsEthHandler.sol"; +import { Whitelist } from "../../../../../src/Whitelist.sol"; +import { + BASE_RSETH_WETH_AERODROME, + BASE_WETH, + BASE_RSETH, + BASE_RSETH_ETH_PRICE_CHAINLINK +} from "../../../../../src/Constants.sol"; +import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; + +import { AerodromeFlashswapHandler_Test } from + "../../../concrete/handlers-base/AerodromeFlashswapHandler.t.sol"; +import { IProviderLibraryExposed } from "../../../../helpers/IProviderLibraryExposed.sol"; +import { SafeCast } from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; + +using SafeCast for int256; + +contract RsEthWethHandler_ForkTest is AerodromeFlashswapHandler_Test { + BaseRsEthHandler handler; + uint8 immutable ILK_INDEX = 0; + + function setUp() public virtual override { + super.setUp(); + handler = new BaseRsEthHandler( + ILK_INDEX, + ionPool, + gemJoins[ILK_INDEX], + Whitelist(whitelist), + BASE_RSETH_WETH_AERODROME, + BASE_WETH + ); + + BASE_RSETH.approve(address(handler), type(uint256).max); + + // Remove debt ceiling for this test + for (uint8 i = 0; i < lens.ilkCount(iIonPool); i++) { + ionPool.updateIlkDebtCeiling(i, type(uint256).max); + } + + deal(address(BASE_RSETH), address(this), INITIAL_BORROWER_COLLATERAL_BALANCE); + } + + function _getCollaterals() internal pure override returns (IERC20[] memory _collaterals) { + _collaterals = new IERC20[](1); + _collaterals[0] = BASE_RSETH; + } + + function _getHandler() internal view override returns (address) { + return address(handler); + } + + function _getIlkIndex() internal pure override returns (uint8) { + return ILK_INDEX; + } + + function _getUnderlying() internal pure override returns (address) { + return address(BASE_WETH); + } + + function _getInitialSpotPrice() internal view override returns (uint256) { + (, int256 ethPerRsEth,,,) = BASE_RSETH_ETH_PRICE_CHAINLINK.latestRoundData(); // [WAD] + return ethPerRsEth.toUint256(); + } + + // NOTE Should be unused + function _getProviderLibrary() internal pure override returns (IProviderLibraryExposed) { + return IProviderLibraryExposed(address(0)); + } + + function _getDepositContracts() internal pure override returns (address[] memory) { + return new address[](1); + } + + function _getForkRpc() internal view override returns (string memory) { + return vm.envString("BASE_MAINNET_RPC_URL"); + } +} + +contract RsEthHandler_WithRateChange_ForkTest is RsEthWethHandler_ForkTest { + function setUp() public virtual override { + super.setUp(); + + ionPool.setRate(ILK_INDEX, 3.5708923502395e27); + } +} diff --git a/test/fork/fuzz/handlers-base/AerodromeFlashswapHandler.t.sol b/test/fork/fuzz/handlers-base/AerodromeFlashswapHandler.t.sol new file mode 100644 index 00000000..ec766995 --- /dev/null +++ b/test/fork/fuzz/handlers-base/AerodromeFlashswapHandler.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { LrtHandler_ForkBase } from "../../../helpers/handlers/LrtHandlerForkBase.sol"; +import { WadRayMath, RAY, WAD } from "../../../../src/libraries/math/WadRayMath.sol"; +import { AerodromeFlashswapHandler } from "../../../../src/flash/AerodromeFlashswapHandler.sol"; +import { IonHandlerBase } from "../../../../src/flash/IonHandlerBase.sol"; +import { Whitelist } from "../../../../src/Whitelist.sol"; +import { BASE_RSETH_WETH_AERODROME, BASE_RSETH, BASE_WETH } from "../../../../src/Constants.sol"; +import { IPool } from "../../../../src/interfaces/aerodrome/IPool.sol"; + +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +import { Vm } from "forge-std/Vm.sol"; +import { console2 } from "forge-std/console2.sol"; +import { StdUtils } from "forge-std/Test.sol"; + +using WadRayMath for uint256; + +interface IPoolFactory { + function getFee(address pool, bool isStable) external view returns (uint256); +} + +abstract contract AerodromeFlashswapHandler_FuzzTest is LrtHandler_ForkBase { + + function testForkFuzz_FlashswapLeverage(uint256 initialDeposit, uint256 resultingCollateralMultiplier) public { + uint256 lrtBalance = BASE_RSETH.balanceOf(address(BASE_RSETH_WETH_AERODROME)); + // bound initial deposit to be between 10^-12 wrsEth and 4% of wrsEth balance of pool + // with up to 5x leverage in test this should test borrowing up to close to 1/6 of the pool + // (5-1)*4% = 16% + initialDeposit = bound(initialDeposit, 1e6, lrtBalance/25); + uint256 resultingCollateral = initialDeposit * bound(resultingCollateralMultiplier, 1, 5); + uint256 maxResultingDebt = resultingCollateral*2; // in weth. This is technically subject to slippage but we will + // skip protecting for this in the test + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingCollateral, + maxResultingDebt, + block.timestamp + 1, + new bytes32[](0) + ); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral); + assertEq(IERC20(address(_getCollaterals()[_getIlkIndex()])).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + } + + function testForkFuzz_FlashswapDeleverage(uint256 initialDeposit, uint256 resultingCollateralMultiplier) public { + uint256 lrtBalance = BASE_RSETH.balanceOf(address(BASE_RSETH_WETH_AERODROME)); + // bound initial deposit to be between 10^-12 wrsEth and 4% of wrsEth balance of pool + // with up to 5x leverage in test this should test borrowing up to close to 1/6 of the pool + // (5-1)*4% = 16% + initialDeposit = bound(initialDeposit, 1e6, lrtBalance/25); + uint256 resultingCollateral = initialDeposit * bound(resultingCollateralMultiplier, 1, 5); + uint256 maxResultingDebt = resultingCollateral; // in weth. This is technically subject to slippage but we will + // skip protecting for this in the test + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.recordLogs(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingCollateral, + maxResultingDebt, + block.timestamp + 1, + new bytes32[](0) + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 normalizedDebtCreated; + for (uint256 i = 0; i < entries.length; i++) { + // keccak256("Borrow(uint8,address,address,uint256,uint256,uint256)") + if (entries[i].topics[0] != 0xe3e92e977f830d2a0b92c58e8866694b5dc929a35e2b95846f427de0f0bb412f) continue; + normalizedDebtCreated = abi.decode(entries[i].data, (uint256)); + } + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), normalizedDebtCreated); + + vm.warp(block.timestamp + 3 hours); + + uint256 slippageAndFeeTolerance = 1.007e18; // 7% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + // Remove all debt + uint256 normalizedDebtToRemove = ionPool.normalizedDebt(_getIlkIndex(), address(this)); + + // Round up otherwise can leave 1 wei of dust in debt left + uint256 debtToRemove = normalizedDebtToRemove.rayMulUp(ionPool.rate(_getIlkIndex())); + + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, block.timestamp + 1); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertGe(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral - maxCollateralToRemove); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), 0); + assertEq(IERC20(address(_getCollaterals()[_getIlkIndex()])).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + } + + function testForkFuzz_FlashswapDeleverageFull( + uint256 initialDeposit, + uint256 resultingCollateralMultiplier + ) + public + { + uint256 lrtBalance = BASE_RSETH.balanceOf(address(BASE_RSETH_WETH_AERODROME)); + // bound initial deposit to be between 10^-12 wrsEth and 4% of wrsEth balance of pool + // with up to 5x leverage in test this should test borrowing up to close to 1/6 of the pool + // (5-1)*4% = 16% + initialDeposit = bound(initialDeposit, 1e6, lrtBalance/25); + uint256 resultingCollateral = initialDeposit * bound(resultingCollateralMultiplier, 1, 5); + uint256 maxResultingDebt = resultingCollateral; // in weth. This is technically subject to slippage but we will + // skip protecting for this in the test + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.recordLogs(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingCollateral, + maxResultingDebt, + block.timestamp + 1, + new bytes32[](0) + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 normalizedDebtCreated; + for (uint256 i = 0; i < entries.length; i++) { + // keccak256("Borrow(uint8,address,address,uint256,uint256,uint256)") + if (entries[i].topics[0] != 0xe3e92e977f830d2a0b92c58e8866694b5dc929a35e2b95846f427de0f0bb412f) continue; + normalizedDebtCreated = abi.decode(entries[i].data, (uint256)); + } + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), normalizedDebtCreated); + + uint256 slippageAndFeeTolerance = 1.007e18; // 0.7% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + uint256 normalizedDebtCurrent = ionPool.normalizedDebt(_getIlkIndex(), address(this)); + + // Remove all debt if any + uint256 debtToRemove = normalizedDebtCurrent == 0 ? 0 : type(uint256).max; + + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, block.timestamp + 1); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertGe(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral - maxCollateralToRemove); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), 0); + assertEq(IERC20(address(_getCollaterals()[_getIlkIndex()])).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + } + + function testForkFuzz_amountOutGivenAmountIn(uint256 amountInToHandler, bool isLeverage) external{ + uint256 poolK = IPool(BASE_RSETH_WETH_AERODROME).getK(); + uint256 wethBalance = BASE_WETH.balanceOf(address(BASE_RSETH_WETH_AERODROME)); + uint256 lrtBalance = BASE_RSETH.balanceOf(address(BASE_RSETH_WETH_AERODROME)); + address factory = IPool(BASE_RSETH_WETH_AERODROME).factory(); + uint256 fee = IPoolFactory(factory).getFee(address(BASE_RSETH_WETH_AERODROME), false); + uint256 maxValue = isLeverage ? lrtBalance : wethBalance; + // skip 0 case since that would have returned already with no leverage or deleverage + // also bound so that amount does not completely wipe out + amountInToHandler = StdUtils.bound(amountInToHandler, 1, maxValue - 1); + + if(isLeverage){ + lrtBalance -= amountInToHandler; + } else{ + wethBalance -= amountInToHandler; + } + uint256 amountOutFromUser = _getTypedUFHandler().getAmountOutGivenAmountIn(amountInToHandler, isLeverage); + uint256 lowerAmountOutFromUser = amountOutFromUser - 1; + uint256 wethLowerBound; + uint256 lrtLowerBound; + if(isLeverage){ + lrtLowerBound = lrtBalance; + wethLowerBound = wethBalance + lowerAmountOutFromUser - (fee * lowerAmountOutFromUser)/10000; + wethBalance += amountOutFromUser - (fee * amountOutFromUser)/10000; + + } else{ + wethLowerBound = wethBalance; + lrtLowerBound = lrtBalance + lowerAmountOutFromUser - (fee * lowerAmountOutFromUser)/10000; + lrtBalance += amountOutFromUser - (fee * amountOutFromUser)/10000; + } + + uint256 newPoolK = wethBalance * lrtBalance; + uint256 lowerBoundPoolK = wethLowerBound * lrtLowerBound; + assertGe(newPoolK, poolK); + assertGt(poolK, lowerBoundPoolK); + } + + function _getTypedUFHandler() internal virtual view returns (AerodromeFlashswapHandler) { + return AerodromeFlashswapHandler(payable(_getHandler())); + } +} + +abstract contract AerodromeFlashswapHandler_WithRateChange_FuzzTest is AerodromeFlashswapHandler_FuzzTest { + function testForkFuzz_WithRateChange_FlashswapLeverage( + uint256 initialDeposit, + uint256 resultingCollateralMultiplier, + uint104 rate + ) + external + { + rate = uint104(bound(rate, 1e27, 10e27)); + ionPool.setRate(_getIlkIndex(), rate); + super.testForkFuzz_FlashswapLeverage(initialDeposit, resultingCollateralMultiplier); + } + + function testForkFuzz_WithRateChange_FlashswapDeleverage( + uint256 initialDeposit, + uint256 resultingCollateralMultiplier, + uint104 rate + ) + external + { + rate = uint104(bound(rate, 1e27, 10e27)); + ionPool.setRate(_getIlkIndex(), rate); + super.testForkFuzz_FlashswapDeleverage(initialDeposit, resultingCollateralMultiplier); + } + + function testForkFuzz_WithRateChange_FlashswapDeleverageFull( + uint256 initialDeposit, + uint256 resultingCollateralMultiplier, + uint104 rate + ) + external + { + rate = uint104(bound(rate, 1e27, 10e27)); + ionPool.setRate(_getIlkIndex(), rate); + super.testForkFuzz_FlashswapDeleverageFull(initialDeposit, resultingCollateralMultiplier); + } +} \ No newline at end of file diff --git a/test/fork/fuzz/lrt/BaseMainnet/RsEthWethHandler.t.sol b/test/fork/fuzz/lrt/BaseMainnet/RsEthWethHandler.t.sol new file mode 100644 index 00000000..54de34c9 --- /dev/null +++ b/test/fork/fuzz/lrt/BaseMainnet/RsEthWethHandler.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { BaseRsEthHandler } from "../../../../../src/flash/lrt/BaseRsEthHandler.sol"; +import { Whitelist } from "../../../../../src/Whitelist.sol"; +import { AerodromeFlashswapHandler } from "../../../../../src/flash/AerodromeFlashswapHandler.sol"; +import { LrtHandler_ForkBase } from "../../../../helpers/handlers/LrtHandlerForkBase.sol"; +import { IonHandler_ForkBase } from "../../../../helpers/handlers/IonHandlerForkBase.sol"; +import { + AerodromeFlashswapHandler_FuzzTest, + AerodromeFlashswapHandler_WithRateChange_FuzzTest +} from "../../handlers-base/AerodromeFlashswapHandler.t.sol"; +import { + BASE_RSETH_WETH_AERODROME, + BASE_WETH, + BASE_RSETH, + BASE_RSETH_ETH_PRICE_CHAINLINK, + RSETH_LRT_DEPOSIT_POOL +} from "../../../../../src/Constants.sol"; +import { IProviderLibraryExposed } from "../../../../helpers/IProviderLibraryExposed.sol"; + +// import { IonHandler_ForkBase } from "../../../../helpers/handlers/IonHandlerForkBase.sol"; +import { IonPoolSharedSetup } from "../../../../helpers/IonPoolSharedSetup.sol"; + +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import { SafeCast } from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; + +using SafeCast for int256; + +contract RsEthWethHandler_ForkFuzzTest is AerodromeFlashswapHandler_FuzzTest { + BaseRsEthHandler handler; + uint8 immutable ILK_INDEX = 0; + + function setUp() public virtual override { + super.setUp(); + handler = new BaseRsEthHandler( + ILK_INDEX, + ionPool, + gemJoins[ILK_INDEX], + Whitelist(whitelist), + BASE_RSETH_WETH_AERODROME, + BASE_WETH + ); + + BASE_RSETH.approve(address(handler), type(uint256).max); + + // Remove debt ceiling for this test + for (uint8 i = 0; i < lens.ilkCount(iIonPool); i++) { + ionPool.updateIlkDebtCeiling(i, type(uint256).max); + } + + deal(address(BASE_RSETH), address(this), INITIAL_BORROWER_COLLATERAL_BALANCE); + } + + function _getCollaterals() internal pure virtual override returns (IERC20[] memory _collaterals) { + _collaterals = new IERC20[](1); + _collaterals[0] = BASE_RSETH; + } + + function _getHandler() internal view override returns (address) { + return address(handler); + } + + function _getIlkIndex() internal pure override returns (uint8) { + return ILK_INDEX; + } + + function _getUnderlying() internal pure virtual override returns (address) { + return address(BASE_WETH); + } + + function _getInitialSpotPrice() internal view virtual override returns (uint256) { + (, int256 ethPerRsEth,,,) = BASE_RSETH_ETH_PRICE_CHAINLINK.latestRoundData(); // [WAD] + return ethPerRsEth.toUint256(); + } + + // NOTE Should be unused + function _getProviderLibrary() internal pure override returns (IProviderLibraryExposed) { + return IProviderLibraryExposed(address(0)); + } + + function _getDepositContracts() internal pure virtual override returns (address[] memory) { + return new address[](1); + } + + function _getForkRpc() internal view virtual override returns (string memory) { + return vm.envString("BASE_MAINNET_RPC_URL"); + } +} + +contract RsEthWethHandler_WithRateChange_ForkFuzzTest is + RsEthWethHandler_ForkFuzzTest, + AerodromeFlashswapHandler_WithRateChange_FuzzTest +{ + function setUp() public virtual override(LrtHandler_ForkBase, RsEthWethHandler_ForkFuzzTest) { + RsEthWethHandler_ForkFuzzTest.setUp(); + } + + function _getCollaterals() internal pure override(IonPoolSharedSetup, RsEthWethHandler_ForkFuzzTest) returns (IERC20[] memory _collaterals) { + _collaterals = new IERC20[](1); + _collaterals[0] = BASE_RSETH; + } + + function _getDepositContracts() internal pure override(IonPoolSharedSetup, RsEthWethHandler_ForkFuzzTest) returns (address[] memory depositContracts) { + depositContracts = new address[](1); + depositContracts[0] = address(RSETH_LRT_DEPOSIT_POOL); + } + + function _getUnderlying() internal pure virtual override(LrtHandler_ForkBase, RsEthWethHandler_ForkFuzzTest) returns (address) { + return address(BASE_WETH); + } + + function _getInitialSpotPrice() internal view virtual override(LrtHandler_ForkBase, RsEthWethHandler_ForkFuzzTest) returns (uint256) { + (, int256 ethPerRsEth,,,) = BASE_RSETH_ETH_PRICE_CHAINLINK.latestRoundData(); // [WAD] + return ethPerRsEth.toUint256(); + } + + function _getForkRpc() internal view virtual override(IonHandler_ForkBase, RsEthWethHandler_ForkFuzzTest) returns (string memory) { + return vm.envString("BASE_MAINNET_RPC_URL"); + } +} \ No newline at end of file