diff --git a/README.md b/README.md index a98f554..ad66350 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,43 @@ -## Foundry +# JITpilot -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +JITpilot is an EulerSwap liquidity pool rebalancing system which allows users to set up automated monitoring for their EulerSwap positions, and rebalance them when needed by reparametrizing their pool and letting the market arbitrage the new state. It is a work in progress and is not ready for production use. -Foundry consists of: - -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. - -## Documentation - -https://book.getfoundry.sh/ +* Time-weighted metrics: JITpilot uses time-weighted metrics to mitigate the effects of volatile markets and prevent frequent rebalancing that could increase gas costs. +* EVC-friendly: JITpilot is designed to work with the EulerVaultConnector (EVC) to better integrate into the Euler ecosystem. +* Peace of mind: Rest easy; while your EulerSwap pool rakes in yield and fees, JITpilot will keep it healthy. ## Usage -### Build - -```shell -$ forge build -``` - -### Test +### Installation -```shell -$ forge test +First [install foundry](https://getfoundry.sh/) then run: +```sh +./install.sh ``` -### Format - -```shell -$ forge fmt +### Run example scripts +```sh +./devland.sh JITpilotRebalanceScenario ``` -### Gas Snapshots +If all goes well, your RPC will be available on the standard `http://127.0.0.1:8545` endpoint. It will print the logs pertaining to the EulerSwap pool rebalancing. +![alt text](image.png) -```shell -$ forge snapshot +### Setting up an LP +```solidity + JITpilot.configureLp(address _lp, uint256 _hfMin, uint256 _hfDesired); ``` +To start monitoring your EulerSwap LP position, you need to call the `configureLp` function with the following parameters: -### Anvil +* `_lp`: The address of your EulerSwap LP position. +* `_hfMin`: The minimum health factor for your LP position. +* `_hfDesired`: The desired health factor for your LP position. -```shell -$ anvil +### Monitoring and rebalancing +```solidity + JITpilot.updateMetrics(address _lp); ``` +This function will update the metrics for the given LP. It will calculate the time-weighted average of the health factor and yield, and trigger a rebalance if needed, or trigger the EulerSwap reparametrization to the original state once the desired health factor is reached. -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +### Testing different scenarios +This repository is built on top of [euler-devland](https://github.com/euler-xyz/euler-devland), which is a tool for testing EulerSwap scenarios. You can find more information about it [here](https://github.com/euler-xyz/euler-devland). You can craft different scenarios where rebalancing could be needed and test them with JITpilot. diff --git a/foundry.toml b/foundry.toml index ec90863..adfa41a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,9 @@ src = "src" out = "out" libs = ["lib"] +viaIR = true +optimizer = true +optimizer_runs = 1000 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/public/JITpilot_output.png b/public/JITpilot_output.png new file mode 100644 index 0000000..fd08986 Binary files /dev/null and b/public/JITpilot_output.png differ diff --git a/script/scenarios/EulerSwapDebtScenario.s.sol b/script/scenarios/EulerSwapDebtScenario.s.sol deleted file mode 100644 index 1070249..0000000 --- a/script/scenarios/EulerSwapDebtScenario.s.sol +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {DeployScenario} from "../DeployScenario.s.sol"; -import {JITpilot} from "../../src/JITpilot.sol"; -import {IEulerSwap} from "euler-swap/interfaces/IEulerSwap.sol"; -import {HookMiner} from "../../libflat/euler-swap/test/utils/HookMiner.sol"; -import {Hooks} from "v4-core/libraries/Hooks.sol"; -import {MetaProxyDeployer} from "euler-swap/utils/MetaProxyDeployer.sol"; -import {console2 as console} from "forge-std/console2.sol"; -import {TestERC20} from "evk-test/unit/evault/EVaultTestBase.t.sol"; -import {IEVault} from "evk/EVault/IEVault.sol"; - -contract EulerSwapDebtScenario is DeployScenario { - address eulerSwap; - JITpilot jitPilot; - - function setup() internal virtual override { - vm.startBroadcast(user3PK); - eUSDC.setLTV(address(eWETH), 0.65e4, 0.67e4, 0); - eUSDC.setLTV(address(eUSDT), 0.85e4, 0.87e4, 0); - deployJITPilot(); - vm.stopBroadcast(); - - createEulerSwap(); // user 2 - - // create borrow positions for other users - giveTonsOfCash(user0); - giveTonsOfCash(user1); - // giveTonsOfCash(user3); - - depositCollateralIntoVault(user0, user0PK, address(eUSDC), 3_000_000_000e6); - depositCollateralIntoVault(user1, user1PK, address(eWETH), 1_000_000e18); - - borrowFromVault(user0, user0PK, address(eWETH), 500_000e18); - borrowFromVault(user1, user1PK, address(eUSDC), 1_600_000_000e6); - - jitPilot.getData(user2); - console.log("user2", user2); - - // buy USDC so that user2's EulerSwap position has to borrow - uint256 amountOut = 286_500_000e6; - _swapExactOut(address(assetWETH), address(assetUSDC), amountOut, user0, user0PK); - } - - function giveTonsOfCash(address user) internal virtual { - assetUSDC.mint(user, 100_000_000_000e6); - assetUSDT.mint(user, 100_000_000_000e6); - assetWETH.mint(user, 10_000_000e18); - assetwstETH.mint(user, 10_000_000e18); - assetDAI.mint(user, 1_000_000_000e18); - assetUSDZ.mint(user, 1_000_000_000e18); - } - - function deployJITPilot() internal { - jitPilot = new JITpilot(); - jitPilot.setEVC(address(evc)); - // jitPilot.setEVK(address(factory)); - jitPilot.setMaglevLens(address(maglevLens)); - jitPilot.setEulerSwapFactory(address(eulerSwapFactory)); - - string memory result = vm.serializeAddress("jitpilot", "jitPilot", address(jitPilot)); - vm.writeJson(result, "./dev-ctx/addresses/31337/JITpilotAddresses.json"); - } - - function createEulerSwap() internal { - vm.startBroadcast(user2PK); - - // Create pool parameters - IEulerSwap.Params memory poolParams = IEulerSwap.Params({ - vault0: address(eUSDC), - vault1: address(eWETH), - eulerAccount: user2, - equilibriumReserve0: 800_000_000e6, - equilibriumReserve1: 280_000e18, - priceX: 1e18, - priceY: 2865e6, - concentrationX: 0.9e18, - concentrationY: 0.9e18, - fee: 0, - protocolFee: 0, - protocolFeeRecipient: address(0) - }); - - // Define required hook flags - uint160 flags = uint160( - Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG - | Hooks.BEFORE_DONATE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG - ); - - // Mine salt - bytes memory creationCode = MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImpl, abi.encode(poolParams)); - (address hookAddress, bytes32 salt) = HookMiner.find(address(eulerSwapFactory), flags, creationCode); - - eulerSwap = hookAddress; - evc.setAccountOperator(user2, eulerSwap, true); - - eulerSwapFactory.deployPool( - poolParams, IEulerSwap.InitialState({currReserve0: 800_000_000e6, currReserve1: 280_000e18}), salt - ); - - string memory result = vm.serializeAddress("eulerSwap", "eulerSwap", address(eulerSwap)); - vm.writeJson(result, "./dev-ctx/addresses/31337/EulerSwapAddresses.json"); - vm.stopBroadcast(); - } - - function depositCollateralIntoVault(address user, uint256 userPK, address vaultAddress, uint256 amount) internal { - vm.startBroadcast(userPK); - TestERC20(IEVault(vaultAddress).asset()).mint(user, amount); - TestERC20(IEVault(vaultAddress).asset()).approve(vaultAddress, type(uint256).max); - IEVault(vaultAddress).deposit(amount, user); - - if (!evc.isCollateralEnabled(user, vaultAddress)) { - evc.enableCollateral(user, vaultAddress); - } - - vm.stopBroadcast(); - } - - function borrowFromVault(address user, uint256 userPK, address vaultAddress, uint256 amount) internal { - vm.startBroadcast(userPK); - - if (evc.getControllers(user).length > 0) { - require(evc.isControllerEnabled(user, vaultAddress), "Controller not enabled"); - } else { - evc.enableController(user, vaultAddress); - } - - IEVault(vaultAddress).borrow(amount, user); - - vm.stopBroadcast(); - } - - function _swapExactOut(address tokenIn, address tokenOut, uint256 amountOut, address receiver, uint256 userPK) - internal - { - vm.startBroadcast(userPK); - uint256 amountIn = eulerSwapPeriphery.quoteExactOutput(eulerSwap, tokenIn, tokenOut, amountOut); - console.log("amountIn", amountIn); - - TestERC20(tokenIn).approve(address(eulerSwapPeriphery), type(uint256).max); - console.log("user1's WETH balance (tokenIn)", TestERC20(tokenIn).balanceOf(user1)); - console.log("WETH address (tokenIn)", tokenIn); - console.log("user1's USDC balance (tokenOut)", TestERC20(tokenOut).balanceOf(user1)); - console.log("USDC address (tokenOut)", tokenOut); - console.log("user1's address: ", user1); - console.log("eulerSwap address: ", eulerSwap); - eulerSwapPeriphery.swapExactOut( - eulerSwap, - tokenIn, - tokenOut, - amountOut, - receiver, - amountIn * 101 / 100, // allow up to 1% slippage - 0 - ); - vm.stopBroadcast(); - } -} diff --git a/script/scenarios/JITpilotRebalanceScenario.s.sol b/script/scenarios/JITpilotRebalanceScenario.s.sol new file mode 100644 index 0000000..ca47169 --- /dev/null +++ b/script/scenarios/JITpilotRebalanceScenario.s.sol @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {DeployScenario} from "../DeployScenario.s.sol"; +import {JITpilot} from "../../src/JITpilot.sol"; +import {IEulerSwap} from "euler-swap/interfaces/IEulerSwap.sol"; +import {HookMiner} from "../../libflat/euler-swap/test/utils/HookMiner.sol"; +import {Hooks} from "v4-core/libraries/Hooks.sol"; +import {MetaProxyDeployer} from "euler-swap/utils/MetaProxyDeployer.sol"; +import {TestERC20} from "evk-test/unit/evault/EVaultTestBase.t.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; +import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol"; +import {IEulerSwapFactory} from "euler-swap/interfaces/IEulerSwapFactory.sol"; +import {CurveLib} from "euler-swap/libraries/CurveLib.sol"; + +import {console2 as console} from "forge-std/console2.sol"; + +contract JITpilotRebalanceScenario is DeployScenario { + address eulerSwap; + JITpilot jitPilot; + + function setup() internal virtual override { + vm.startBroadcast(user3PK); + eUSDC.setLTV(address(eWETH), 0.65e4, 0.67e4, 0); + eUSDC.setLTV(address(eUSDT), 0.85e4, 0.87e4, 0); + eWETH.setLTV(address(eUSDC), 0.65e4, 0.67e4, 0); + eWETH.setLTV(address(eUSDT), 0.85e4, 0.87e4, 0); + eUSDT.setLTV(address(eWETH), 0.85e4, 0.87e4, 0); + eUSDT.setLTV(address(eUSDC), 0.85e4, 0.87e4, 0); + deployJITpilot(); + vm.stopBroadcast(); + + // create borrow positions for other users + giveTonsOfCash(user0); + giveTonsOfCash(user1); + + depositCollateralIntoVault(user0, user0PK, address(eUSDC), 3_000_000_000e6); + depositCollateralIntoVault(user1, user1PK, address(eWETH), 1_000_000e18); + + borrowFromVault(user0, user0PK, address(eWETH), 500_000e18); + borrowFromVault(user1, user1PK, address(eUSDC), 1_600_000_000e6); + + vm.startBroadcast(user2PK); + deployEulerSwap(getInitialEulerSwapParams(), false); + authorizeJITpilot(user2); + jitPilot.configureLp(user2, 1.4e18, 1.5e18); + vm.stopBroadcast(); + + // Initial EulerSwap state + console.log("Initial EulerSwap state: "); + printEulerSwapData(user2); + + // buy USDC so that user2's EulerSwap position has to borrow USDC + uint256 amountOut = 286_500_000e6; + uint256 amountIn = _swapExactOut(address(assetWETH), address(assetUSDC), amountOut, user0, user0PK); + console.log("marketUser SOLD %s WETH FOR %s USDC", amountIn, amountOut); + + // Get a quote for the current price of ETH in the EulerSwap instance + uint256 startingEthPrice = 2865e18; + uint256 ethPriceInUSDC = + eulerSwapPeriphery.quoteExactOutput(eulerSwap, address(assetUSDC), address(assetWETH), 1e18); + console.log("Starting price of ETH: ", startingEthPrice); + console.log("New price of ETH: ", ethPriceInUSDC); + + // ETH price has dropped to ~2510. Let's update the oracle + vm.prank(user3); + oracle.setPrice(address(eWETH), unitOfAccount, ethPriceInUSDC * 1e18 / 1e6); + + // user2 is in debt now. Let's see their position state + console.log("EulerSwap state now after servicing trades: "); + printEulerSwapData(user2); + + // Let's see what the rebalancing params would be + IEulerSwap.Params memory newParams = jitPilot.getRebalancingParams(user2); + console.log("Rebalancing params:"); + printEulerSwapParams(newParams); + + // let's do the actual rebalancing then + vm.startBroadcast(user2PK); + jitPilot.updateMetrics(user2); + IEVC(evc).setAccountOperator(user2, eulerSwap, false); + IEulerSwapFactory(eulerSwapFactory).uninstallPool(); + deployEulerSwap(newParams, true); + vm.stopBroadcast(); + + // EulerSwap state before arbitrage + console.log("EulerSwap state now after rebalancing, before arbitrage: "); + printEulerSwapData(user2); + + // Let's buy some WETH on the EulerSwap pool and see the effect on the debt + // sell USDC so that user2's EulerSwap position has to borrow USDC + uint256 newEthPrice = + eulerSwapPeriphery.quoteExactOutput(eulerSwap, address(assetUSDC), address(assetWETH), 1e18); + console.log("price of ETH (market): ", ethPriceInUSDC); + console.log("price of ETH (EulerSwap): ", newEthPrice); + amountIn = 101_000_000e6; + amountOut = _swapExactIn(address(assetUSDC), address(assetWETH), amountIn, user0, user0PK); + console.log( + "marketUser BOUGHT %s WETH FOR %s USDC (price: %s)", amountOut, amountIn, amountIn * 1e18 / amountOut + ); + console.log( + "price of ETH (EulerSwap, after arbitrage): ", + eulerSwapPeriphery.quoteExactOutput(eulerSwap, address(assetUSDC), address(assetWETH), 1e18) + ); + + // Let's see the new state of the EulerSwap pool after arbitrage + console.log("EulerSwap state now (after arbitrage):"); + printEulerSwapData(user2); + } + + function giveTonsOfCash(address user) internal virtual { + assetUSDC.mint(user, 100_000_000_000e6); + assetUSDT.mint(user, 100_000_000_000e6); + assetWETH.mint(user, 10_000_000e18); + assetwstETH.mint(user, 10_000_000e18); + assetDAI.mint(user, 1_000_000_000e18); + assetUSDZ.mint(user, 1_000_000_000e18); + } + + function deployJITpilot() internal { + jitPilot = new JITpilot(); + jitPilot.setEVC(address(evc)); + // jitPilot.setEVK(address(factory)); + jitPilot.setMaglevLens(address(maglevLens)); + jitPilot.setEulerSwapFactory(address(eulerSwapFactory)); + + string memory result = vm.serializeAddress("jitpilot", "jitPilot", address(jitPilot)); + vm.writeJson(result, "./dev-ctx/addresses/31337/JITpilotAddresses.json"); + } + + function authorizeJITpilot(address user) internal { + evc.setAccountOperator(user, address(jitPilot), true); + } + + function deployEulerSwap(IEulerSwap.Params memory poolParams, bool rebalancing) internal { + console.log("DEPLOYING EULERSWAP WITH PARAMS"); + printEulerSwapParams(poolParams); + + bool asset0IsDebt = getCurrentControllerVault(poolParams.eulerAccount) == poolParams.vault0; + uint112 currentReserve0 = poolParams.equilibriumReserve0; + uint112 currentReserve1 = poolParams.equilibriumReserve1; + + IEulerSwap.InitialState memory initialState; + if (!rebalancing) { + initialState = IEulerSwap.InitialState({ + currReserve0: poolParams.equilibriumReserve0, + currReserve1: poolParams.equilibriumReserve1 + }); + } else { + if (asset0IsDebt) { + { + uint256 deltaReservesAsset0 = poolParams.equilibriumReserve0 * 1 / 3; + // uint256 deltaReservesAsset1 = deltaReservesAsset0 * poolParams.priceX / poolParams.priceY; + currentReserve0 = uint112(poolParams.equilibriumReserve0 - deltaReservesAsset0); + // currentReserve1 = uint112(poolParams.equilibriumReserve1 + deltaReservesAsset1); + currentReserve1 = uint112( + CurveLib.f( + uint256(currentReserve0), + uint256(poolParams.priceX), + uint256(poolParams.priceY), + uint256(poolParams.equilibriumReserve0), + uint256(poolParams.equilibriumReserve1), + uint256(poolParams.concentrationX) + ) + ); + } + } else { + { + uint256 deltaReservesAsset1 = poolParams.equilibriumReserve1 * 1 / 3; + // uint256 deltaReservesAsset0 = deltaReservesAsset1 * poolParams.priceY / poolParams.priceX; + currentReserve1 = uint112(poolParams.equilibriumReserve1 - deltaReservesAsset1); + // currentReserve0 = uint112(poolParams.equilibriumReserve0 + deltaReservesAsset0); + currentReserve0 = uint112( + CurveLib.fInverse( + uint256(currentReserve1), + uint256(poolParams.priceY), + uint256(poolParams.priceX), + uint256(poolParams.equilibriumReserve1), + uint256(poolParams.equilibriumReserve0), + uint256(poolParams.concentrationY) + ) + ); + } + } + initialState = IEulerSwap.InitialState({currReserve0: currentReserve0, currReserve1: currentReserve1}); + } + + // Define required hook flags + uint160 flags = uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG + | Hooks.BEFORE_DONATE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + ); + + // Mine salt + bytes memory creationCode = MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImpl, abi.encode(poolParams)); + (address hookAddress, bytes32 salt) = HookMiner.find(address(eulerSwapFactory), flags, creationCode); + + eulerSwap = hookAddress; + // Deploy pool via EVC batch + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(evc.setAccountOperator, (user2, eulerSwap, true)) + }); + items[1] = IEVC.BatchItem({ + onBehalfOfAccount: user2, + targetContract: address(eulerSwapFactory), + value: 0, + data: abi.encodeCall(IEulerSwapFactory.deployPool, (poolParams, initialState, salt)) + }); + evc.batch(items); + + string memory result = vm.serializeAddress("eulerSwap", "eulerSwap", address(eulerSwap)); + vm.writeJson(result, "./dev-ctx/addresses/31337/EulerSwapAddresses.json"); + } + + function depositCollateralIntoVault(address user, uint256 userPK, address vaultAddress, uint256 amount) internal { + vm.startBroadcast(userPK); + TestERC20(IEVault(vaultAddress).asset()).mint(user, amount); + TestERC20(IEVault(vaultAddress).asset()).approve(vaultAddress, type(uint256).max); + IEVault(vaultAddress).deposit(amount, user); + + if (!evc.isCollateralEnabled(user, vaultAddress)) { + evc.enableCollateral(user, vaultAddress); + } + + vm.stopBroadcast(); + } + + function borrowFromVault(address user, uint256 userPK, address vaultAddress, uint256 amount) internal { + vm.startBroadcast(userPK); + + if (evc.getControllers(user).length > 0) { + require(evc.isControllerEnabled(user, vaultAddress), "Controller not enabled"); + } else { + evc.enableController(user, vaultAddress); + } + + IEVault(vaultAddress).borrow(amount, user); + + vm.stopBroadcast(); + } + + function _swapExactOut(address tokenIn, address tokenOut, uint256 amountOut, address receiver, uint256 userPK) + internal + returns (uint256 amountIn) + { + vm.startBroadcast(userPK); + amountIn = eulerSwapPeriphery.quoteExactOutput(eulerSwap, tokenIn, tokenOut, amountOut); + // console.log("amountIn", amountIn); + + TestERC20(tokenIn).approve(address(eulerSwapPeriphery), type(uint256).max); + eulerSwapPeriphery.swapExactOut( + eulerSwap, + tokenIn, + tokenOut, + amountOut, + receiver, + amountIn * 101 / 100, // allow up to 1% slippage + 0 + ); + vm.stopBroadcast(); + + return amountIn; + } + + function _swapExactIn(address tokenIn, address tokenOut, uint256 amountIn, address receiver, uint256 userPK) + internal + returns (uint256 amountOut) + { + vm.startBroadcast(userPK); + amountOut = eulerSwapPeriphery.quoteExactInput(eulerSwap, tokenIn, tokenOut, amountIn); + // console.log("amountOut", amountOut); + + TestERC20(tokenIn).approve(address(eulerSwapPeriphery), type(uint256).max); + eulerSwapPeriphery.swapExactIn( + eulerSwap, + tokenIn, + tokenOut, + amountIn, + receiver, + amountOut * 99 / 100, // allow up to 1% slippage + 0 + ); + vm.stopBroadcast(); + + return amountOut; + } + + function getInitialEulerSwapParams() internal view returns (IEulerSwap.Params memory) { + return IEulerSwap.Params({ + vault0: address(eUSDC), + vault1: address(eWETH), + eulerAccount: user2, + equilibriumReserve0: 800_000_000e6, + equilibriumReserve1: 280_000e18, + priceX: 1e18, + priceY: 2865e6, + concentrationX: 0.9e18, + concentrationY: 0.9e18, + fee: 0, + protocolFee: 0, + protocolFeeRecipient: address(0) + }); + } + + function printEulerSwapParams(IEulerSwap.Params memory params) internal pure { + console.log("=========================================================="); + console.log("vault0: ", params.vault0); + console.log("vault1: ", params.vault1); + console.log("eulerAccount: ", params.eulerAccount); + console.log("equilibriumReserve0: ", params.equilibriumReserve0, "<---------------"); + console.log("equilibriumReserve1: ", params.equilibriumReserve1, "<---------------"); + console.log("priceX: ", params.priceX); + console.log("priceY: ", params.priceY); + console.log("concentrationX: ", params.concentrationX, "<---------------"); + console.log("concentrationY: ", params.concentrationY, "<---------------"); + console.log("fee: ", params.fee); + console.log("protocolFee: ", params.protocolFee); + console.log("protocolFeeRecipient: ", params.protocolFeeRecipient); + console.log("=========================================================="); + } + + function printEulerSwapData(address user) internal view { + JITpilot.BlockData memory blockData = jitPilot.getData(user); + uint256 healthFactor = blockData.allowedLTV > 0 ? blockData.allowedLTV * 1e4 / blockData.currentLTV / 100 : 0; + + console.log("=========================================================="); + console.log("healthFactor: ", healthFactor, "%"); + console.log("allowedLTV: ", blockData.allowedLTV); + console.log("currentLTV: ", blockData.currentLTV); + console.log("swapFees: ", blockData.swapFees); + console.log("netInterest: ", blockData.netInterest); + console.log("depositValue: ", blockData.depositValue); + console.log("controllerVault: ", blockData.controllerVault); + console.log("=========================================================="); + } + + function getCurrentControllerVault(address lp) internal view returns (address) { + address[] memory controllerVaults = evc.getControllers(lp); + address currentControllerVault; + + if (controllerVaults.length == 0) return address(0); + + // find which of the LP's controller vaults is the enabled debt vault + for (uint256 i; i < controllerVaults.length; ++i) { + if (evc.isControllerEnabled(lp, controllerVaults[i])) { + currentControllerVault = controllerVaults[i]; + break; + } + } + return currentControllerVault; + } +} diff --git a/src/JITpilot.sol b/src/JITpilot.sol index 9ca73dc..12e9d6a 100644 --- a/src/JITpilot.sol +++ b/src/JITpilot.sol @@ -8,6 +8,12 @@ import {IEulerSwapFactory} from "euler-swap/interfaces/IEulerSwapFactory.sol"; import {IMaglevLens} from "src/interfaces/IMaglevLens.sol"; import {IPriceOracle} from "evk/interfaces/IPriceOracle.sol"; import {console2 as console} from "forge-std/console2.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {MetaProxyDeployer} from "euler-swap/utils/MetaProxyDeployer.sol"; +import {HookMiner} from "../libflat/euler-swap/test/utils/HookMiner.sol"; +import {Hooks} from "v4-core/libraries/Hooks.sol"; + +import {CurveLib} from "euler-swap/libraries/CurveLib.sol"; /** * @title JITpilot @@ -22,24 +28,21 @@ contract JITpilot { uint256 private constant WINDOW_SIZE = 100; uint256 private constant PRECISION = 1e18; + enum RebalancingStatus { + NOT_REBALANCING, + REBALANCING + } + // Configurable parameters uint256 public weightHF = 6e17; // 0.6 weight for Health Factor uint256 public weightYield = 4e17; // 0.4 weight for Yield // Euler contract addresses - address public EVC; - address public EVK; - address public MaglevLens; - address public EulerSwapFactory; - - // Data structure to store block-level data from fetchData - struct BlockData { - uint256 allowedLTV; - uint256 currentLTV; - uint256 swapFees; - int256 netInterest; - int256 depositValue; - } + address public evcAddress; + address public evkAddress; + address public maglevLensAddress; + address public eulerSwapFactoryAddress; + address public eulerSwapImplAddress; // LP data structure struct LPData { @@ -59,7 +62,20 @@ contract JITpilot { // Tracking uint256 lastUpdateBlock; // Last block when metrics were updated uint256 startBlock; // Block when LP started + EulerSwapData eulerSwapData; // Latest EulerSwap parameters + BlockData blockData; // Latest EulerSwap state data bool initialized; // Whether LP data is initialized + RebalancingStatus rebalancingStatus; // Whether LP is currently rebalancing + } + + // Data structure to store block-level data from fetchData + struct BlockData { + uint256 allowedLTV; + uint256 currentLTV; + uint256 swapFees; + int256 netInterest; + int256 depositValue; + address controllerVault; } // Mappings @@ -73,13 +89,12 @@ contract JITpilot { uint256 healthFactor, uint256 yield, uint256 twaHF, - uint256 twaYield, - uint256 compositeScore + uint256 twaYield ); - event RebalanceTriggered(address indexed lp, uint256 indexed blockNumber, uint256 compositeScore, uint256 threshold, uint256 targetScore); + event RebalanceTriggered(address indexed lp, uint256 indexed blockNumber, uint256 hf, uint256 threshold); - event LPConfigured(address indexed lp, uint256 hfMin, uint256 hfDesired, uint256 yieldTarget); + event LPConfigured(address indexed lp, uint256 hfMin, uint256 hfDesired); // Modifiers modifier onlyAuthorized() { @@ -100,19 +115,23 @@ contract JITpilot { // Setters for contract addresses function setEVC(address _EVC) external onlyOwner { - EVC = _EVC; + evcAddress = _EVC; } function setEVK(address _EVK) external onlyOwner { - EVK = _EVK; + evkAddress = _EVK; } function setMaglevLens(address _MaglevLens) external onlyOwner { - MaglevLens = _MaglevLens; + maglevLensAddress = _MaglevLens; } function setEulerSwapFactory(address _EulerSwapFactory) external onlyOwner { - EulerSwapFactory = _EulerSwapFactory; + eulerSwapFactoryAddress = _EulerSwapFactory; + } + + function setEulerSwapImpl(address _EulerSwapImpl) external onlyOwner { + eulerSwapImplAddress = _EulerSwapImpl; } /** @@ -120,24 +139,22 @@ contract JITpilot { * @param lp LP address * @param _hfMin Liquidation threshold * @param _hfDesired Target health factor - * @param _yieldTarget Target yield */ - function configureLp(address lp, uint256 _hfMin, uint256 _hfDesired, uint256 _yieldTarget) external { + function configureLp(address lp, uint256 _hfMin, uint256 _hfDesired) external { require(lp != address(0), "Invalid LP address"); require(_hfDesired > _hfMin, "HF desired must be > HF min"); LPData storage data = lpData[lp]; data.hfMin = _hfMin; data.hfDesired = _hfDesired; - data.yieldTarget = _yieldTarget; + // TODO: implement yield-based rebalancing + // data.yieldTarget = _yieldTarget; data.initialized = true; data.startBlock = block.number; + data.rebalanceThreshold = _hfDesired; + data.rebalanceDesired = _hfDesired; - // Calculate and store thresholds once during configuration - data.rebalanceThreshold = calculateRebalanceThreshold(lp); - data.rebalanceDesired = calculateRebalanceDesired(lp); - - emit LPConfigured(lp, _hfMin, _hfDesired, _yieldTarget); + emit LPConfigured(lp, _hfMin, _hfDesired); } /** @@ -149,6 +166,10 @@ contract JITpilot { LPData storage data = lpData[lp]; + // update EulerSwap pool data + address poolAddr = IEulerSwapFactory(eulerSwapFactoryAddress).poolByEulerAccount(lp); + data.eulerSwapData = getEulerSwapData(poolAddr); + // Fetch current block data BlockData memory currentData = fetchData(lp); @@ -158,26 +179,12 @@ contract JITpilot { currentHF = (currentData.allowedLTV * PRECISION) / currentData.currentLTV; } - // Calculate current Yield - uint256 currentYield = 0; - uint256 swapApy = uint256(int256(currentData.swapFees) * 1e18 / currentData.depositValue); - if (currentData.depositValue > 0) { - if (currentData.netInterest >= 0) { - currentYield = (uint256(currentData.netInterest) * PRECISION) + swapApy; - } else if (swapApy >= uint256(currentData.netInterest)) { - // netInterest is negative, but swapApy is greater, so yield is positive. - currentYield = swapApy - uint256(currentData.netInterest); - } else { - // Yield is negative. Set to 0 for simplicity. - currentYield = 0; - } - } - // Update sliding window for Health Factor _updateSlidingWindow(data.hfHistory, currentHF); + // TODO: re-enable yield-based rebalancing // Update sliding window for Yield - _updateSlidingWindow(data.yieldHistory, currentYield); + // _updateSlidingWindow(data.yieldHistory, currentYield); // Calculate TWA for Health Factor data.twaHF = _calculateTWA(data.hfHistory, data.startBlock); @@ -196,12 +203,21 @@ contract JITpilot { data.lastUpdateBlock = block.number; // Emit metrics updated event - emit MetricsUpdated(lp, block.number, currentHF, currentYield, data.twaHF, data.twaYield, compositeScore); - - // Check if rebalancing is needed using stored threshold values - if (compositeScore < data.rebalanceThreshold) { - emit RebalanceTriggered(lp, block.number, compositeScore, data.rebalanceThreshold, data.rebalanceDesired); - rebalance(lp, data.rebalanceDesired); + // emit MetricsUpdated(lp, block.number, currentHF, currentYield, data.twaHF, data.twaYield); + emit MetricsUpdated(lp, block.number, currentHF, 0, data.twaHF, 0); + if (data.rebalancingStatus == RebalancingStatus.NOT_REBALANCING) { + // Check if rebalancing is needed + if (currentHF < data.rebalanceThreshold) { + emit RebalanceTriggered(lp, block.number, currentHF, data.rebalanceThreshold); + _rebalance(lp); + data.rebalancingStatus = RebalancingStatus.REBALANCING; + } + } else { + // LP is in the middle of rebalancing + if (currentHF >= data.hfDesired) { + _afterRebalanceFinished(lp); + data.rebalancingStatus = RebalancingStatus.NOT_REBALANCING; + } } } @@ -282,13 +298,13 @@ contract JITpilot { function calculateRebalanceThreshold(address lp) internal view returns (uint256) { LPData storage data = lpData[lp]; if (!data.initialized) return 0; - + // Placeholder implementation - to be researched and implemented // Should calculate threshold based on hfMin as main parameter // with thresholdSafetyMargin for fine-tuning - return 5e17; // Default 0.5 for now + return data.hfDesired; // Default hfDesired for now } - + /** * @dev Calculate dynamic rebalance desired target based on LP configuration (placeholder) * @param lp LP address @@ -297,7 +313,7 @@ contract JITpilot { function calculateRebalanceDesired(address lp) internal view returns (uint256) { LPData storage data = lpData[lp]; if (!data.initialized) return 0; - + // Placeholder implementation - to be researched and implemented // Should calculate target based on hfDesired and yieldTarget as main parameters // with desiredTargetRatio for fine-tuning @@ -313,10 +329,8 @@ contract JITpilot { // Placeholder implementation - this will fetch real data from Euler contracts // For now, return dummy data to avoid compilation errors - // get LP's eulerSwap pool address - address poolAddr = IEulerSwapFactory(EulerSwapFactory).poolByEulerAccount(lp); - - // get eulerSwap pool data + // update EulerSwap pool data + address poolAddr = IEulerSwapFactory(eulerSwapFactoryAddress).poolByEulerAccount(lp); EulerSwapData memory eulerSwapData = getEulerSwapData(poolAddr); BlockData memory blockData; @@ -325,35 +339,33 @@ contract JITpilot { uint256 supplyApyTotal = 0; uint256 borrowApyTotal = 0; - (uint256 collateralValueTotal, uint256 debtValue) = getDepositValue(lp); + (uint256 collateralValueTotal, uint256 debtValue) = _getDepositValue(lp); blockData.depositValue = int256(collateralValueTotal) - int256(debtValue); supplyApyTotal = getSupplyApy(lp); // get the currently enabled controller vault (i.e. the debt vault) - address controllerVault = getCurrentControllerVault(lp); + address controllerVault = _getCurrentControllerVault(lp); // If there is no controller, there is no debt, and no liquidation metrics to calculate if (controllerVault == address(0)) { - console.log("doesn't have controller vault: ", controllerVault); + // console.log("doesn't have controller vault: ", controllerVault); blockData.allowedLTV = 0; blockData.currentLTV = 0; // If there is no debt, there is no looping or leverage, so interest is just supplyAPY blockData.netInterest = int256(supplyApyTotal); + blockData.controllerVault = address(0); } else { - console.log("has controller vault: ", controllerVault); // Figure out which vault is the collateralVault address collateralVault = (controllerVault == eulerSwapData.params.vault0) ? eulerSwapData.params.vault1 : eulerSwapData.params.vault0; - console.log("debtValue: ", debtValue); blockData.allowedLTV = uint256(IEVault(controllerVault).LTVLiquidation(collateralVault)) * 1e18 / 1e4; blockData.currentLTV = debtValue * 1e18 / collateralValueTotal; - console.log("currentLTV: ", blockData.currentLTV); address[] memory controllerVaultArray = new address[](1); controllerVaultArray[0] = controllerVault; IMaglevLens.VaultGlobal[] memory controllerVaultsGlobal = - IMaglevLens(MaglevLens).vaultsGlobal(controllerVaultArray); + IMaglevLens(maglevLensAddress).vaultsGlobal(controllerVaultArray); // borrow APY is on the last 48 bits. Shift left then right to extract it. uint256 borrowApy = uint256((controllerVaultsGlobal[0].packed2 << (256 - 48)) >> (256 - 48)); borrowApyTotal = borrowApy; @@ -369,50 +381,11 @@ contract JITpilot { / uint256(blockData.depositValue) ) * 1e9; } + blockData.controllerVault = controllerVault; } blockData.depositValue = int256(collateralValueTotal) - int256(debtValue); - // get LP's liquidity status in the controller vault, with regards to liquidation - // ( - // address[] memory collateralVaults, - // uint256[] memory collateralValues, - // uint256 liabilityValue - // ) = IEVault(controllerVault).accountLiquidityFull(lp, true); - - // these collateralValues are adjusted to LTV. We need to divide by liquidationLTV to get the non-adjusted LTV - // uint256 collateralValueTotal; - // uint256 allowedLTV = 0; // Will be weighted average of all collateral LTVs - // uint256 totalWeight = 0; - // for (uint256 i; i < collateralVaults.length; ++i) { - // uint16 ltv = IEVault(controllerVault).LTVLiquidation(collateralVaults[i]); - // collateralValues[i] = collateralValues[i] * 10000 / ltv; // Convert back to non-adjusted value - // collateralValueTotal += collateralValues[i]; - - // // Calculate weighted average LTV - // allowedLTV += ltv * collateralValues[i]; - // totalWeight += collateralValues[i]; - // } - - // Finalize weighted average LTV - // if (totalWeight > 0) { - // allowedLTV = allowedLTV / totalWeight; - // } - - // #2 calculate the currentLTV - // uint256 currentLTV = liabilityValue / collateralValueTotal; - - // TODO: get swap fees - // uint256 swapFees = 0; - - // uint256 borrowApy = uint256((controllerVaultGlobals[0].packed2 << (256 - 48)) >> (256 - 48)); - - // #4 calculate net interest - // uint256 netInterest = supplyApyTotal - borrowApy; - - // #5 get the depositValue - // uint256 depositValue = collateralValueTotal - liabilityValue; - return blockData; } @@ -423,10 +396,6 @@ contract JITpilot { address asset1; uint256 reserve0; uint256 reserve1; - uint256 inLimit01; - uint256 outLimit01; - uint256 inLimit10; - uint256 outLimit10; uint16 borrowLTV01; uint16 borrowLTV10; } @@ -443,22 +412,167 @@ contract JITpilot { (address asset0, address asset1) = pool.getAssets(); output.asset0 = asset0; output.asset1 = asset1; - (output.inLimit01, output.outLimit01) = pool.getLimits(asset0, asset1); - (output.inLimit10, output.outLimit10) = pool.getLimits(asset1, asset0); // fetch borrow LTVs. These will be used to calculate reserves for rebalancing output.borrowLTV01 = IEVault(output.params.vault0).LTVBorrow(output.params.vault1); output.borrowLTV10 = IEVault(output.params.vault1).LTVBorrow(output.params.vault0); } + /** * @dev Rebalance LP position (placeholder - to be implemented later) * @param lp LP address to rebalance - * @param targetScore Target composite score to achieve after rebalancing */ - - function rebalance(address lp, uint256 targetScore) internal { + function _rebalance(address lp) internal { // Placeholder implementation - this will perform actual rebalancing // Will be implemented in later iterations - // The rebalancing logic will adjust positions to achieve targetScore + // This function is only called after confirming that a rebalance is needed + + // #1 Fetch current EulerSwap data + address poolAddr = IEulerSwapFactory(eulerSwapFactoryAddress).poolByEulerAccount(lp); + EulerSwapData memory eulerSwapData = getEulerSwapData(poolAddr); + + // #2 Calculate new EulerSwap params + bool asset0IsDebt = _getCurrentControllerVault(lp) == eulerSwapData.params.vault0; + IEulerSwap.Params memory newParams = calculateRebalancingParams(lp, eulerSwapData, asset0IsDebt); + + // IEulerSwap.InitialState memory initialState = IEulerSwap.InitialState({ + // currReserve0: newParams.equilibriumReserve0, + // currReserve1: newParams.equilibriumReserve1 + // }); + // #3 Uninstall current EulerSwap via EVC batch + // IEVC(evcAddress).call( + // address(evcAddress), + // lp, + // 0, + // abi.encodeCall(IEVC(evcAddress).setAccountOperator, (lp, poolAddr, false)) + // ); + // IEVC(evcAddress).call( + // address(eulerSwapFactoryAddress), + // lp, + // 0, + // abi.encodeCall(IEulerSwapFactory.uninstallPool, ()) + // ); + // IEVC(evcAddress).setAccountOperator(lp, poolAddr, false); + // IEulerSwapFactory(eulerSwapFactoryAddress).uninstallPool(); + + // #4 Reinstall EulerSwap through EVC + // #4.1 Mine salt + // (address hookAddress, bytes32 salt) = mineSalt(newParams); + // #4.2 Deploy pool via EVC batch + // IEVC.BatchItem[] memory items3 = new IEVC.BatchItem[](2); + // items3[0] = IEVC.BatchItem({ + // onBehalfOfAccount: address(0), + // targetContract: address(evcAddress), + // value: 0, + // data: abi.encodeCall(IEVC(evcAddress).setAccountOperator, (lp, hookAddress, true)) + // }); + // items3[1] = IEVC.BatchItem({ + // onBehalfOfAccount: lp, + // targetContract: address(eulerSwapFactoryAddress), + // value: 0, + // data: abi.encodeCall(IEulerSwapFactory.deployPool, (newParams, initialState, salt)) + // }); + // IEVC(evcAddress).batch(items3); + } + + /** + * @dev Calculate rebalancing params for an LP. + * We create a new EulerSwap pool with the equilibrium reserves and initial state set in + * a way that incentivizes the market to arbitrage by selling the debt asset to this EulerSwap + * instance, causing the user's debt to be repaid. + * @param lp LP address + * @param eulerSwapData EulerSwap data + * @param asset0IsDebt Whether asset0 is the debt asset + * @return newParams New EulerSwap params to use when deploying the new pool + */ + function calculateRebalancingParams(address lp, EulerSwapData memory eulerSwapData, bool asset0IsDebt) + internal + view + returns (IEulerSwap.Params memory) + { + // Calculate delta reserves. This is the amount of trading we want to service, to repay the debt. + uint256 deltaReservesValueUsd = calculateDeltaReserves(lp, eulerSwapData.params); + (uint256 collateralValueTotal, uint256 debtValue) = _getDepositValue(lp); + uint256 depositValue = collateralValueTotal - debtValue; + + uint256 asset0Scale = + FixedPointMathLib.rpow(10e18, IERC20(IEVault(eulerSwapData.params.vault0).asset()).decimals(), 1e18) / 1e18; + uint256 asset1Scale = + FixedPointMathLib.rpow(10e18, IERC20(IEVault(eulerSwapData.params.vault1).asset()).decimals(), 1e18) / 1e18; + uint256 asset0PriceUsd = IPriceOracle(IEVault(eulerSwapData.params.vault0).oracle()).getQuote( + asset0Scale, + IEVault(eulerSwapData.params.vault0).asset(), + IEVault(eulerSwapData.params.vault0).unitOfAccount() + ); + uint256 asset1PriceUsd = IPriceOracle(IEVault(eulerSwapData.params.vault1).oracle()).getQuote( + asset1Scale, + IEVault(eulerSwapData.params.vault1).asset(), + IEVault(eulerSwapData.params.vault1).unitOfAccount() + ); + + // Calculate balancedEquilibriumReserves given current depositValue + // Both equilibriumReserves are maxed at the point in the curve where vaults are balanced. + uint256 desiredEqRsvCollateralAsset; + uint256 desiredEqRsvDebtAsset; + { + if (asset0IsDebt) { + // we've chosen an arbitrary amount of 3x deltaReserves and a 99.3% concentration to prevent over-borrowing and allow for arbitrage + desiredEqRsvDebtAsset = deltaReservesValueUsd * 3 * asset0Scale / asset0PriceUsd; + + uint256 balEqRsv1 = + depositValue * 1e4 / (1e4 - eulerSwapData.borrowLTV01) * asset1Scale / asset1PriceUsd; + desiredEqRsvCollateralAsset = balEqRsv1 + depositValue * asset1Scale / 2 / asset1PriceUsd + + debtValue * asset1Scale / asset1PriceUsd - deltaReservesValueUsd * asset1Scale / asset1PriceUsd; + } else { + // we've chosen an arbitrary amount of 3x deltaReserves and a 99% concentration to prevent over-borrowing and allow for arbitrage + desiredEqRsvDebtAsset = deltaReservesValueUsd * 3 * asset1Scale / asset1PriceUsd; + + uint256 balEqRsv0 = + depositValue * 1e4 / (1e4 - eulerSwapData.borrowLTV01) * asset0Scale / asset0PriceUsd; + desiredEqRsvCollateralAsset = balEqRsv0 + depositValue * asset0Scale / 2 / asset0PriceUsd + + debtValue * asset0Scale / asset0PriceUsd - deltaReservesValueUsd * asset0Scale / asset0PriceUsd; + } + } + uint256 concentrationDebtAsset = 99.3 * 1e16; + + return IEulerSwap.Params({ + vault0: eulerSwapData.params.vault0, + vault1: eulerSwapData.params.vault1, + eulerAccount: eulerSwapData.params.eulerAccount, + equilibriumReserve0: uint112(asset0IsDebt ? desiredEqRsvDebtAsset : desiredEqRsvCollateralAsset), + equilibriumReserve1: uint112(asset0IsDebt ? desiredEqRsvCollateralAsset : desiredEqRsvDebtAsset), + priceX: 1 * asset1Scale, + priceY: asset1PriceUsd * 1e18 / asset0PriceUsd * asset0Scale / asset1Scale, + concentrationX: asset0IsDebt ? concentrationDebtAsset : eulerSwapData.params.concentrationX, + concentrationY: asset0IsDebt ? eulerSwapData.params.concentrationY : concentrationDebtAsset, + fee: eulerSwapData.params.fee, + protocolFee: eulerSwapData.params.protocolFee, + protocolFeeRecipient: eulerSwapData.params.protocolFeeRecipient + }); + } + + /** + * @dev Calculate delta reserves for an LP. This is the amount of trading that the LP needs to service + * in order for their debt to be repaid and their Health Factor to be restored to their desired value. + * @param lp LP address + * @param poolParams EulerSwap params + * @return deltaReservesValueUsd Delta reserves value in USD + */ + function calculateDeltaReserves(address lp, IEulerSwap.Params memory poolParams) internal view returns (uint256) { + // $\Delta L = \frac{\frac{HF'}{LLTV} \cdot L - C}{\frac{HF'}{LLTV} - 1}$ + uint256 hfPrime = lpData[lp].hfDesired; + address controllerVault = _getCurrentControllerVault(lp); + address collateralVault = controllerVault == poolParams.vault0 ? poolParams.vault1 : poolParams.vault0; + uint256 lltv = uint256(IEVault(controllerVault).LTVLiquidation(collateralVault)) * 1e18 / 1e4; + + uint256 collateralValue = _getPositionValue(lp, collateralVault, false); + uint256 liabilityValue = _getPositionValue(lp, controllerVault, true); + + return ((hfPrime * liabilityValue / lltv) - collateralValue) / (hfPrime * 1e18 / lltv - 1e18) * 1.01e18; + } + + function _afterRebalanceFinished(address lp) internal { + // reinstall EulerSwap with original params but updated price. + // TODO: implement redeploying EulerSwap pool with original params. } /** @@ -499,7 +613,10 @@ contract JITpilot { * @return rebalanceThreshold Threshold below which rebalancing is triggered * @return rebalanceDesired Target score to achieve after rebalancing * @return lastUpdateBlock Last block when metrics were updated + * @return eulerSwapData Latest EulerSwap parameters + * @return blockData Latest EulerSwap state data * @return initialized Whether LP data is initialized + * @return rebalancingStatus Whether LP is currently rebalancing */ function getLPData(address lp) external @@ -513,7 +630,10 @@ contract JITpilot { uint256 rebalanceThreshold, uint256 rebalanceDesired, uint256 lastUpdateBlock, - bool initialized + EulerSwapData memory eulerSwapData, // Latest EulerSwap parameters + BlockData memory blockData, // Latest EulerSwap state data + bool initialized, // Whether LP data is initialized + RebalancingStatus rebalancingStatus // Whether LP is currently rebalancing ) { LPData storage data = lpData[lp]; @@ -523,8 +643,13 @@ contract JITpilot { data.hfMin, data.hfDesired, data.yieldTarget, + data.rebalanceThreshold, + data.rebalanceDesired, data.lastUpdateBlock, - data.initialized + data.eulerSwapData, + data.blockData, + data.initialized, + data.rebalancingStatus ); } @@ -551,7 +676,7 @@ contract JITpilot { function getRebalanceThreshold(address lp) external view returns (uint256) { return lpData[lp].rebalanceThreshold; } - + /** * @dev Get rebalance desired target for an LP * @param lp LP address @@ -560,7 +685,7 @@ contract JITpilot { function getRebalanceDesired(address lp) external view returns (uint256) { return lpData[lp].rebalanceDesired; } - + /** * @dev Get all key metrics for an LP in one call * @param lp LP address @@ -569,12 +694,11 @@ contract JITpilot { * @return desired Rebalance target * @return needsRebalance Whether LP currently needs rebalancing */ - function getLPMetrics(address lp) external view returns ( - uint256 compositeScore, - uint256 threshold, - uint256 desired, - bool needsRebalance - ) { + function getLPMetrics(address lp) + external + view + returns (uint256 compositeScore, uint256 threshold, uint256 desired, bool needsRebalance) + { LPData storage data = lpData[lp]; compositeScore = this.getCompositeScore(lp); threshold = data.rebalanceThreshold; @@ -584,15 +708,20 @@ contract JITpilot { // HELPER FUNCTIONS - function getCurrentControllerVault(address lp) internal view returns (address) { - address[] memory controllerVaults = IEVC(EVC).getControllers(lp); + /** + * @dev Get current controller vault for an LP. This tells us which vault is the debt vault. + * @param lp LP address + * @return Controller vault address + */ + function _getCurrentControllerVault(address lp) internal view returns (address) { + address[] memory controllerVaults = IEVC(evcAddress).getControllers(lp); address currentControllerVault; if (controllerVaults.length == 0) return address(0); // find which of the LP's controller vaults is the enabled debt vault for (uint256 i; i < controllerVaults.length; ++i) { - if (IEVC(EVC).isControllerEnabled(lp, controllerVaults[i])) { + if (IEVC(evcAddress).isControllerEnabled(lp, controllerVaults[i])) { currentControllerVault = controllerVaults[i]; break; } @@ -600,70 +729,65 @@ contract JITpilot { return currentControllerVault; } - function getCollateralValue(address account, address collateral) internal view virtual returns (uint256 value) { - IEVault collateralVault = IEVault(collateral); - uint256 balance = IERC20(collateral).balanceOf(account); - if (balance == 0) return 0; - - uint256 currentCollateralValue; - - // mid-point price - currentCollateralValue = - IPriceOracle(collateralVault.oracle()).getQuote(balance, collateral, collateralVault.unitOfAccount()); - - return currentCollateralValue; - } - - function getDebtValue(address account, address vault) internal view virtual returns (uint256 value) { - IEVault controllerVault = IEVault(vault); - uint256 debt = controllerVault.debtOf(account); - if (debt == 0) return 0; - - uint256 currentDebtValue; - - // mid-point price - currentDebtValue = IPriceOracle(controllerVault.oracle()).getQuote(debt, vault, controllerVault.unitOfAccount()); - - return currentDebtValue; - } - - function getData(address lp) external view returns (BlockData memory) { - return fetchData(lp); - } - /** * @dev Get the deposit value of an LP * @param lp LP address * @return collateralValueTotal Collateral value of the LP * @return debtValue Debt value of the LP */ - function getDepositValue(address lp) + function _getDepositValue(address lp) internal view virtual returns (uint256 collateralValueTotal, uint256 debtValue) { - address poolAddr = IEulerSwapFactory(EulerSwapFactory).poolByEulerAccount(lp); + address poolAddr = IEulerSwapFactory(eulerSwapFactoryAddress).poolByEulerAccount(lp); IEulerSwap.Params memory eulerSwapParams = IEulerSwap(poolAddr).getParams(); collateralValueTotal = - getCollateralValue(lp, eulerSwapParams.vault0) + getCollateralValue(lp, eulerSwapParams.vault1); - debtValue = getDebtValue(lp, eulerSwapParams.vault0) + getDebtValue(lp, eulerSwapParams.vault1); + _getPositionValue(lp, eulerSwapParams.vault0, false) + _getPositionValue(lp, eulerSwapParams.vault1, false); + debtValue = + _getPositionValue(lp, eulerSwapParams.vault0, true) + _getPositionValue(lp, eulerSwapParams.vault1, true); return (collateralValueTotal, debtValue); } + /** + * @dev Get the USD value of a collateral or debt position + * @param account Account address + * @param vaultAddress Vault address + * @param isControllerVault Whether the vault is the controller vault + * @return value Position value in USD + */ + function _getPositionValue(address account, address vaultAddress, bool isControllerVault) + internal + view + virtual + returns (uint256 value) + { + IEVault vault = IEVault(vaultAddress); + uint256 balance = isControllerVault ? vault.debtOf(account) : IERC20(vaultAddress).balanceOf(account); + if (balance == 0) return 0; + + uint256 currentPositionValue; + + // mid-point price + currentPositionValue = IPriceOracle(vault.oracle()).getQuote(balance, vaultAddress, vault.unitOfAccount()); + + return currentPositionValue; + } + /** * @dev Get the supply APY of an LP * @param lp LP address * @return supplyApyTotal Supply APY of the LP, weighted by collateral value */ function getSupplyApy(address lp) internal view virtual returns (uint256 supplyApyTotal) { - address poolAddr = IEulerSwapFactory(EulerSwapFactory).poolByEulerAccount(lp); + address poolAddr = IEulerSwapFactory(eulerSwapFactoryAddress).poolByEulerAccount(lp); IEulerSwap.Params memory eulerSwapParams = IEulerSwap(poolAddr).getParams(); // get supply APY data using the MaglevLens contract - IMaglevLens maglevLens = IMaglevLens(MaglevLens); + IMaglevLens maglevLens = IMaglevLens(maglevLensAddress); address[] memory collateralVaults = new address[](2); collateralVaults[0] = eulerSwapParams.vault0; collateralVaults[1] = eulerSwapParams.vault1; @@ -672,15 +796,56 @@ contract JITpilot { uint256 collateralValueTotal; for (uint256 i; i < collateralVaultsGlobal.length; ++i) { uint256 supplyApy = uint256((collateralVaultsGlobal[i].packed2 << (256 - 96)) >> (256 - 48)); - uint256 collateralValue = getCollateralValue(lp, collateralVaults[i]); + uint256 collateralValue = _getPositionValue(lp, collateralVaults[i], false); supplyApyTotal += supplyApy * collateralValue; collateralValueTotal += collateralValue; - console.log("Supply APY for vault", collateralVaults[i], "is", supplyApy); - console.log("Supply APY total is", supplyApyTotal); } supplyApyTotal = supplyApyTotal / collateralValueTotal; return supplyApyTotal; } + + function mineSalt(IEulerSwap.Params memory params) internal view returns (address, bytes32) { + // Define required hook flags + uint160 flags = uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG + | Hooks.BEFORE_DONATE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + ); + + bytes memory creationCode = MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImplAddress, abi.encode(params)); + return HookMiner.find(address(eulerSwapFactoryAddress), flags, creationCode); + } + + // EXTERNAL HELPER FUNCTIONS FOR TESTING + function getData(address lp) external view returns (BlockData memory) { + return fetchData(lp); + } + + function getRebalancingParams(address lp) external view returns (IEulerSwap.Params memory) { + // #1 Fetch current EulerSwap data + address poolAddr = IEulerSwapFactory(eulerSwapFactoryAddress).poolByEulerAccount(lp); + EulerSwapData memory eulerSwapData = getEulerSwapData(poolAddr); + + // #2 Calculate new EulerSwap params + bool asset0IsDebt = _getCurrentControllerVault(lp) == eulerSwapData.params.vault0; + + return calculateRebalancingParams(lp, eulerSwapData, asset0IsDebt); + } + + /** + * @dev Rebalance LP position (placeholder - to be implemented later) + * @param lp LP address to rebalance + */ + function rebalance(address lp) external { + _rebalance(lp); + } + + function getDepositValue(address lp) external view returns (uint256 collateralValueTotal, uint256 debtValue) { + return _getDepositValue(lp); + } + + function getDeltaReserves(address lp, IEulerSwap.Params memory poolParams) external view returns (uint256) { + return calculateDeltaReserves(lp, poolParams); + } } diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 58305bb..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {JITpilot} from "../src/JITpilot.sol"; - -contract JITpilotTest is Test { - // JITpilot public JITpilot; - - function setUp() public { - // JITpilot = new JITpilot(); - // counter.setNumber(0); - } - - function test_Increment() public { - // counter.increment(); - // assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - // counter.setNumber(x); - // assertEq(counter.number(), x); - } -}