From d29dc6fdcd3d11bd5f06603aa90e91507615d46d Mon Sep 17 00:00:00 2001 From: clement-ux Date: Thu, 29 Aug 2024 17:44:48 +0200 Subject: [PATCH] feat: add inverted quoter for rebalancing. --- .../interfaces/aerodrome/IAMOStrategy.sol | 46 +++ .../contracts/utils/AerodromeAMOQuoter.sol | 316 ++++++++++++++++++ 2 files changed, 362 insertions(+) create mode 100644 contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol create mode 100644 contracts/contracts/utils/AerodromeAMOQuoter.sol diff --git a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol new file mode 100644 index 0000000000..8a67a379b9 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { ICLPool } from "./ICLPool.sol"; + +interface IAMOStrategy { + error NotEnoughWethForSwap(uint256 wethBalance, uint256 requiredWeth); + error NotEnoughWethLiquidity(uint256 wethBalance, uint256 requiredWeth); + error PoolRebalanceOutOfBounds( + uint256 currentPoolWethShare, + uint256 allowedWethShareStart, + uint256 allowedWethShareEnd + ); + error OutsideExpectedTickRange(int24 currentTick); + + function governor() external view returns (address); + + function rebalance( + uint256 _amountToSwap, + bool _swapWeth, + uint256 _minTokenReceived + ) external; + + function clPool() external view returns (ICLPool); + + function vaultAddress() external view returns (address); + + function poolWethShareVarianceAllowed() external view returns (uint256); + + function poolWethShare() external view returns (uint256); + + function tokenId() external view returns (uint256); + + function withdrawAll() external; + + function setAllowedPoolWethShareInterval( + uint256 _allowedWethShareStart, + uint256 _allowedWethShareEnd + ) external; + + function setWithdrawLiquidityShare(uint128 share) external; + + function lowerTick() external view returns (int24); + + function upperTick() external view returns (int24); +} diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol new file mode 100644 index 0000000000..55c6b05382 --- /dev/null +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.7; + +import { IAMOStrategy } from "../interfaces/aerodrome/IAMOStrategy.sol"; + +contract QuoterHelper { + enum RevertReasons { + DefaultStatus, + RebalanceOutOfBounds, + NotInExpectedTickRange, + NotEnoughWethForSwap, + NotEnoughWethLiquidity, + UnexpectedError, + Found, + NotFound + } + + struct RebalanceStatus { + RevertReasons reason; + uint256 currentPoolWETHShare; // Case 1 + uint256 allowedWETHShareStart; // Case 1 + uint256 allowedWETHShareEnd; // Case 1 + int24 currentTick; // Case 2 + uint256 balanceWETH; // Case 3 + uint256 amountWETH; // Case 3 + string revertMessage; + } + + struct QuoterParams { + address strategy; + bool swapWETHForOETHB; + uint256 minAmount; + uint256 maxAmount; + uint256 maxIterations; + } + + error UnexpectedError(string message); + error OutOfIterations(uint256 iterations); + error ValidAmount(uint256 amount, uint256 iterations); + + /// @notice This call can only end with a revert. + function getAmountToSwapBeforeRebalance(QuoterParams memory params) public { + IAMOStrategy strategy = IAMOStrategy(params.strategy); + uint256 iterations; + uint256 low = params.minAmount; + uint256 high = params.maxAmount; + int24 lowerTick = strategy.lowerTick(); + int24 upperTick = strategy.upperTick(); + + while (low <= high && iterations < params.maxIterations) { + uint256 mid = (low + high) / 2; + + RebalanceStatus memory status = getRebalanceStatus( + params.strategy, + mid, + params.swapWETHForOETHB + ); + + // Best case, we found the `amount` that will reach the target pool share! + // We can revert with the amount and the number of iterations + if (status.reason == RevertReasons.Found) { + revert ValidAmount(mid, iterations); + } + + // If the rebalance failed then we should try to change the amount. + // We will handle all possible revert reasons here. + + // Case 1: Rebalance out of bounds + // If the pool is out of bounds, we need to adjust the amount to reach the target pool share + if (status.reason == RevertReasons.RebalanceOutOfBounds) { + // If the current pool share is less than the target pool share, we need to increase the amount + if ( + params.swapWETHForOETHB + ? status.currentPoolWETHShare < + status.allowedWETHShareStart + : status.currentPoolWETHShare > + status.allowedWETHShareEnd + ) { + low = mid + 1; + } + // Else we need to decrease the amount + else { + high = mid; + } + } + + // Case 2: Not in expected tick range + // If the pool is not in the expected tick range, we need to adjust the amount + // to reach the target pool share + if (status.reason == RevertReasons.NotInExpectedTickRange) { + // If we are buying OETHb and the current tick is greater than the lower tick, + //we need to increase the amount in order to continue to push price down. + // If we are selling OETHb and the current tick is less than the upper tick, + // we need to increase the amount in order to continue to push price up. + if ( + params.swapWETHForOETHB + ? status.currentTick > lowerTick + : status.currentTick < upperTick + ) { + low = mid + 1; + } + // Else we need to decrease the amount + else { + high = mid; + } + } + + // Case 3: Not enough WETH for swap + // If we don't have enough WETH to swap, we need to decrease the amount + // This error can happen, when initial value of mid is too high, so we need to decrease it + if (status.reason == RevertReasons.NotEnoughWethForSwap) { + high = mid; + } + + // Case 4: Not enough WETH liquidity + // If we don't have enough WETH liquidity + // Revert for the moment, we need to improve this + if (status.reason == RevertReasons.NotEnoughWethLiquidity) { + revert("Quoter: Not enough WETH liquidity"); + } + + // Case 5: Unexpected error + // Worst case, it reverted with an unexpected error. + if (status.reason == RevertReasons.UnexpectedError) { + revert UnexpectedError(status.revertMessage); + } + + iterations++; + } + + // Case 6: Out of iterations + // If we didn't find the amount after the max iterations, we need to revert. + revert OutOfIterations(iterations); + } + + function getRebalanceStatus( + address strategy, + uint256 amount, + bool swapWETH + ) public returns (RebalanceStatus memory status) { + try IAMOStrategy(strategy).rebalance(amount, swapWETH, 0) { + status.reason = RevertReasons.Found; + return status; + } catch Error(string memory reason) { + status.reason = RevertReasons.UnexpectedError; + status.revertMessage = reason; + } catch (bytes memory reason) { + bytes4 receivedSelector = bytes4(reason); + + // Case 1: Rebalance out of bounds + if ( + receivedSelector == + IAMOStrategy.PoolRebalanceOutOfBounds.selector + ) { + uint256 currentPoolWETHShare; + uint256 allowedWETHShareStart; + uint256 allowedWETHShareEnd; + + // solhint-disable-next-line no-inline-assembly + assembly { + currentPoolWETHShare := mload(add(reason, 0x24)) + allowedWETHShareStart := mload(add(reason, 0x44)) + allowedWETHShareEnd := mload(add(reason, 0x64)) + } + return + RebalanceStatus({ + reason: RevertReasons.RebalanceOutOfBounds, + currentPoolWETHShare: currentPoolWETHShare, + allowedWETHShareStart: allowedWETHShareStart, + allowedWETHShareEnd: allowedWETHShareEnd, + currentTick: 0, + balanceWETH: 0, + amountWETH: 0, + revertMessage: "" + }); + } + + // Case 2: Not in expected tick range + if ( + receivedSelector == + IAMOStrategy.OutsideExpectedTickRange.selector + ) { + int24 currentTick; + + // solhint-disable-next-line no-inline-assembly + assembly { + currentTick := mload(add(reason, 0x24)) + } + return + RebalanceStatus({ + reason: RevertReasons.NotInExpectedTickRange, + currentPoolWETHShare: 0, + allowedWETHShareStart: 0, + allowedWETHShareEnd: 0, + currentTick: currentTick, + balanceWETH: 0, + amountWETH: 0, + revertMessage: "" + }); + } + + // Case 3: Not enough WETH for swap + if ( + receivedSelector == IAMOStrategy.NotEnoughWethForSwap.selector + ) { + uint256 balanceWETH; + uint256 amountWETH; + + // solhint-disable-next-line no-inline-assembly + assembly { + balanceWETH := mload(add(reason, 0x24)) + amountWETH := mload(add(reason, 0x44)) + } + return + RebalanceStatus({ + reason: RevertReasons.NotEnoughWethForSwap, + currentPoolWETHShare: 0, + allowedWETHShareStart: 0, + allowedWETHShareEnd: 0, + currentTick: 0, + balanceWETH: balanceWETH, + amountWETH: amountWETH, + revertMessage: "" + }); + } + + // Case 4: Not enough WETH liquidity + if ( + receivedSelector == IAMOStrategy.NotEnoughWethLiquidity.selector + ) { + return + RebalanceStatus({ + reason: RevertReasons.NotEnoughWethLiquidity, + currentPoolWETHShare: 0, + allowedWETHShareStart: 0, + allowedWETHShareEnd: 0, + currentTick: 0, + balanceWETH: 0, + amountWETH: 0, + revertMessage: "" + }); + } + + // Case 5: Unexpected error + return + RebalanceStatus({ + reason: RevertReasons.UnexpectedError, + currentPoolWETHShare: 0, + allowedWETHShareStart: 0, + allowedWETHShareEnd: 0, + currentTick: 0, + balanceWETH: 0, + amountWETH: 0, + revertMessage: abi.decode(reason, (string)) + }); + } + } +} + +contract AerodromeAMOQuoter { + QuoterHelper public quoterHelper; + + constructor() { + quoterHelper = new QuoterHelper(); + } + + struct Data { + uint256 amount; + uint256 iterations; + } + + event ValueFound(uint256 value, uint256 iterations); + event ValueNotFound(string message); + + function quoteAmountToSwapBeforeRebalance( + address strategy, + bool swapWETHForOETHB, + uint256 minAmount, + uint256 maxAmount, + uint256 maxIterations + ) public returns (Data memory data) { + QuoterHelper.QuoterParams memory params = QuoterHelper.QuoterParams({ + strategy: strategy, + swapWETHForOETHB: swapWETHForOETHB, + minAmount: minAmount, + maxAmount: maxAmount, + maxIterations: maxIterations + }); + try quoterHelper.getAmountToSwapBeforeRebalance(params) { + revert("Previous call should only revert, it cannot succeed"); + } catch (bytes memory reason) { + bytes4 receivedSelector = bytes4(reason); + + if (receivedSelector == QuoterHelper.ValidAmount.selector) { + uint256 value; + uint256 iterations; + + // solhint-disable-next-line no-inline-assembly + assembly { + value := mload(add(reason, 0x24)) + iterations := mload(add(reason, 0x44)) + } + emit ValueFound(value, iterations); + return Data({ amount: value, iterations: iterations }); + } + + if (receivedSelector == QuoterHelper.OutOfIterations.selector) { + emit ValueNotFound("Out of iterations"); + revert("Out of iterations"); + } + + emit ValueNotFound("Unexpected error"); + revert(abi.decode(reason, (string))); + } + } +}