From d29dc6fdcd3d11bd5f06603aa90e91507615d46d Mon Sep 17 00:00:00 2001 From: clement-ux Date: Thu, 29 Aug 2024 17:44:48 +0200 Subject: [PATCH 01/28] 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))); + } + } +} From 4e9d161a4662bb4c8878121f608be01539548279 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Thu, 29 Aug 2024 17:46:11 +0200 Subject: [PATCH 02/28] test: use quoter for AMO rebalancing. --- .../aerodrome-amo.base.fork-test.js | 170 ++++++++++++++---- 1 file changed, 135 insertions(+), 35 deletions(-) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index e63a179619..5f4acdf997 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -7,6 +7,7 @@ const { expect } = require("chai"); const { oethUnits } = require("../helpers"); const ethers = require("ethers"); const { impersonateAndFund } = require("../../utils/signers"); +const { deployWithConfirmation } = require("../../utils/deploy"); //const { formatUnits } = ethers.utils; const { BigNumber } = ethers; @@ -124,7 +125,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { strategist, rafael, aeroSwapRouter, - aeroNftManager; + aeroNftManager, + quoter; beforeEach(async () => { fixture = await baseFixture(); @@ -141,6 +143,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { oethbVaultSigner = await impersonateAndFund(oethbVault.address); gauge = fixture.aeroClGauge; + await deployWithConfirmation("AerodromeAMOQuoter"); + quoter = await hre.ethers.getContract("AerodromeAMOQuoter"); + await setup(); await weth .connect(rafael) @@ -275,7 +280,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { aeroBalanceBefore ); - expect(aeroBalancediff).to.equal(oethUnits("1337")); + expect(aeroBalancediff).to.gte(oethUnits("1337")); // Gte to take into account rewards already accumulated. await assetLpStakedInGauge(); }); }); @@ -322,9 +327,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 10 wei is really small + // Little to no weth should be left on the strategy contract - 100 wei is really small expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( - BigNumber.from("10") + BigNumber.from("100") ); await assetLpStakedInGauge(); @@ -412,9 +417,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 10 wei is really small + // Little to no weth should be left on the strategy contract - 100 wei is really small expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( - BigNumber.from("10") + BigNumber.from("100") ); await assetLpStakedInGauge(); @@ -497,9 +502,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 10 wei is really small + // Little to no weth should be left on the strategy contract - 100 wei is really small expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( - BigNumber.from("10") + BigNumber.from("100") ); await assetLpStakedInGauge(); @@ -564,11 +569,21 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should be able to deposit to the pool & rebalance", async () => { await mintAndDepositToStrategy({ amount: oethUnits("5") }); - // prettier-ignore - const tx = await rebalance( + const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); + const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; + + let direction = currentPrice > targetPrice ? true : false; + const value = await quoteAmountToSwapBeforeRebalance( + direction, oethUnits("0.00001"), - true, // _swapWETHs - oethUnits("0.000009") + oethUnits("1") + ); + const tx = await rebalance( + value, + direction, + 0 ); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); @@ -578,11 +593,21 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should be able to deposit to the pool & rebalance multiple times", async () => { await mintAndDepositToStrategy({ amount: oethUnits("5") }); - // prettier-ignore - const tx = await rebalance( + const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); + const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; + + let direction = currentPrice > targetPrice ? true : false; + const value = await quoteAmountToSwapBeforeRebalance( + direction, oethUnits("0.00001"), - true, // _swapWETHs - oethUnits("0.000009") + oethUnits("1") + ); + const tx = await rebalance( + value, + direction, + 0 ); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); @@ -640,7 +665,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { await expect( rebalance( - oethUnits("0.01"), + oethUnits("1000000000"), true, // _swapWETH oethUnits("0.009") ) @@ -648,11 +673,23 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); it("Should revert when pool rebalance is off target", async () => { + const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); + const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; + + let direction = currentPrice > targetPrice ? true : false; + const value = await quoteAmountToSwapBeforeRebalance( + direction, + oethUnits("0.00000001"), + oethUnits("1000") + ); + await expect( rebalance( - oethUnits("0.001"), - true, // _swapWETH - oethUnits("0.0009") + value.mul("200").div("100"), + direction, + 0 ) ).to.be.revertedWith("PoolRebalanceOutOfBounds"); }); @@ -666,10 +703,21 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { // supply some WETH for the rebalance await mintAndDepositToStrategy({ amount: oethUnits("1") }); + const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); + const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; + + let direction = currentPrice > targetPrice ? true : false; + const value = await quoteAmountToSwapBeforeRebalance( + direction, + oethUnits("0.00000001"), + oethUnits("1000") + ); await rebalance( - oethUnits("0.0083"), - true, // _swapWETH - oethUnits("0.0082") + value, + direction, + 0 ); await assetLpStakedInGauge(); @@ -681,29 +729,39 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { swapWeth: true, }); + const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); + const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; + + let direction = currentPrice > targetPrice ? true : false; + const value = await quoteAmountToSwapBeforeRebalance( + direction, + oethUnits("0.00000001"), + oethUnits("1000") + ); await rebalance( - oethUnits("0.0079"), - false, // _swapWETH - oethUnits("0.0036") + value, + direction, + 0 ); await assetLpStakedInGauge(); }); it("Should have the correct balance within some tolerance", async () => { - await expect( - await aerodromeAmoStrategy.checkBalance(weth.address) - ).to.approxEqualTolerance(oethUnits("23.2")); + const balance = await aerodromeAmoStrategy.checkBalance(weth.address); await mintAndDepositToStrategy({ amount: oethUnits("6") }); await expect( await aerodromeAmoStrategy.checkBalance(weth.address) - ).to.approxEqualTolerance(oethUnits("29.35")); + ).to.equal(balance.add(oethUnits("6"))); + // just add liquidity don't move the active trading position await rebalance(BigNumber.from("0"), true, BigNumber.from("0")); await expect( await aerodromeAmoStrategy.checkBalance(weth.address) - ).to.approxEqualTolerance(oethUnits("51.09")); + ).to.approxEqualTolerance(balance.add(oethUnits("6").mul("4")), 1.5); await assetLpStakedInGauge(); }); @@ -723,7 +781,11 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); await expect( - rebalance(oethUnits("4.99"), true, oethUnits("4")) + rebalance( + (await weth.balanceOf(await aerodromeAmoStrategy.clPool())).mul("2"), + true, + oethUnits("4") + ) ).to.be.revertedWith("NotEnoughWethForSwap"); await assetLpStakedInGauge(); @@ -833,11 +895,24 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { const setup = async () => { await mintAndDepositToStrategy({ amount: oethUnits("5") }); + const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); + const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; + + let direction = currentPrice > targetPrice ? true : false; + + const value = await quoteAmountToSwapBeforeRebalance( + direction, + oethUnits("0.00000001"), + oethUnits("1000") + ); + // move the price to pre-configured 20% value await rebalance( - oethUnits("0.00875"), - true, // _swapWETH - oethUnits("0.0086") + value, + direction, // _swapWETH + value.mul(90).div(100) ); }; @@ -889,6 +964,31 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); }; + const quoteAmountToSwapBeforeRebalance = async ( + swapWETH, + minAmount, + maxAmount + ) => { + await oethbVault + .connect(await impersonateAndFund(addresses.base.governor)) + .setStrategistAddr(await quoter.quoterHelper()); + const txResponse = await quoter.quoteAmountToSwapBeforeRebalance( + aerodromeAmoStrategy.address, + swapWETH, + minAmount, + maxAmount, + 50 + ); + const txReceipt = await txResponse.wait(); + const [transferEvent] = txReceipt.events; + const value = transferEvent.args.value; + await oethbVault + .connect(await impersonateAndFund(addresses.base.governor)) + .setStrategistAddr(addresses.base.strategist); + //console.log("Value to swap", value.toString()); + return value; + }; + const rebalance = async (amountToSwap, swapWETH, minTokenReceived) => { return await aerodromeAmoStrategy .connect(strategist) From 8810a77ef3f95c6d7d7214f3b65d0c3588f2a68b Mon Sep 17 00:00:00 2001 From: clement-ux Date: Thu, 29 Aug 2024 17:46:29 +0200 Subject: [PATCH 03/28] chore: increase gas limit for test for AMO quoter. --- contracts/hardhat.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index 9218077d91..841693cd7e 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -103,6 +103,7 @@ module.exports = { accounts: { mnemonic, }, + blockGasLimit: 1000000000, chainId, ...(isArbitrumFork ? { tags: ["arbitrumOne"] } From 5e7df119c8bfdf6ccc2b4b9555720e75a927f683 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 30 Aug 2024 18:10:49 +0200 Subject: [PATCH 04/28] fix: remove arguments from quoter call. --- .../interfaces/aerodrome/IAMOStrategy.sol | 6 + .../contracts/utils/AerodromeAMOQuoter.sol | 132 +++++++++++------- .../aerodrome-amo.base.fork-test.js | 127 +++-------------- 3 files changed, 110 insertions(+), 155 deletions(-) diff --git a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol index 8a67a379b9..a7dbc3bb3c 100644 --- a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol +++ b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol @@ -43,4 +43,10 @@ interface IAMOStrategy { function lowerTick() external view returns (int24); function upperTick() external view returns (int24); + + function getPoolX96Price() external view returns (uint160 _sqrtRatioX96); + + function sqrtRatioX96TickLower() external view returns (uint160); + + function sqrtRatioX96TickHigher() external view returns (uint160); } diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index 55c6b05382..f7c457a865 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.7; import { IAMOStrategy } from "../interfaces/aerodrome/IAMOStrategy.sol"; contract QuoterHelper { + //////////////////////////////////////////////////////////////// + /// --- STRUCTS & ENUMS + //////////////////////////////////////////////////////////////// enum RevertReasons { DefaultStatus, RebalanceOutOfBounds, @@ -26,40 +29,60 @@ contract QuoterHelper { string revertMessage; } - struct QuoterParams { - address strategy; - bool swapWETHForOETHB; - uint256 minAmount; - uint256 maxAmount; - uint256 maxIterations; - } - + //////////////////////////////////////////////////////////////// + /// --- CONSTANT & IMMUTABLE + //////////////////////////////////////////////////////////////// + uint256 public constant BINARY_MIN_AMOUNT = 0.000_000_01 ether; + uint256 public constant BINARY_MAX_AMOUNT = 1_000 ether; + uint256 public constant BINARY_MAX_ITERATIONS = 50; + + //////////////////////////////////////////////////////////////// + /// --- VARIABLES STORAGE + //////////////////////////////////////////////////////////////// + IAMOStrategy public strategy; + + //////////////////////////////////////////////////////////////// + /// --- ERRORS & EVENTS + //////////////////////////////////////////////////////////////// error UnexpectedError(string message); error OutOfIterations(uint256 iterations); - error ValidAmount(uint256 amount, uint256 iterations); + error ValidAmount( + uint256 amount, + uint256 iterations, + bool swapWETHForOETHB + ); + + //////////////////////////////////////////////////////////////// + /// --- CONSTRUCTOR + //////////////////////////////////////////////////////////////// + constructor(IAMOStrategy _strategy) { + strategy = _strategy; + } + //////////////////////////////////////////////////////////////// + /// --- FUNCTIONS + //////////////////////////////////////////////////////////////// /// @notice This call can only end with a revert. - function getAmountToSwapBeforeRebalance(QuoterParams memory params) public { - IAMOStrategy strategy = IAMOStrategy(params.strategy); + function getAmountToSwapBeforeRebalance() public { uint256 iterations; - uint256 low = params.minAmount; - uint256 high = params.maxAmount; + uint256 low = BINARY_MIN_AMOUNT; + uint256 high = BINARY_MAX_AMOUNT; int24 lowerTick = strategy.lowerTick(); int24 upperTick = strategy.upperTick(); + bool swapWETHForOETHB = getSwapDirection(); - while (low <= high && iterations < params.maxIterations) { + while (low <= high && iterations < BINARY_MAX_ITERATIONS) { uint256 mid = (low + high) / 2; RebalanceStatus memory status = getRebalanceStatus( - params.strategy, mid, - params.swapWETHForOETHB + 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); + revert ValidAmount(mid, iterations, swapWETHForOETHB); } // If the rebalance failed then we should try to change the amount. @@ -70,7 +93,7 @@ contract QuoterHelper { 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 + swapWETHForOETHB ? status.currentPoolWETHShare < status.allowedWETHShareStart : status.currentPoolWETHShare > @@ -93,7 +116,7 @@ contract QuoterHelper { // 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 + swapWETHForOETHB ? status.currentTick > lowerTick : status.currentTick < upperTick ) { @@ -133,12 +156,11 @@ contract QuoterHelper { revert OutOfIterations(iterations); } - function getRebalanceStatus( - address strategy, - uint256 amount, - bool swapWETH - ) public returns (RebalanceStatus memory status) { - try IAMOStrategy(strategy).rebalance(amount, swapWETH, 0) { + function getRebalanceStatus(uint256 amount, bool swapWETH) + public + returns (RebalanceStatus memory status) + { + try strategy.rebalance(amount, swapWETH, 0) { status.reason = RevertReasons.Found; return status; } catch Error(string memory reason) { @@ -255,38 +277,52 @@ contract QuoterHelper { }); } } -} -contract AerodromeAMOQuoter { - QuoterHelper public quoterHelper; + function getSwapDirection() public view returns (bool) { + uint160 currentPrice = strategy.getPoolX96Price(); + uint160 ticker0Price = strategy.sqrtRatioX96TickLower(); + uint160 ticker1Price = strategy.sqrtRatioX96TickHigher(); + uint160 targetPrice = (ticker0Price * 20 + ticker1Price * 80) / 100; - constructor() { - quoterHelper = new QuoterHelper(); + return currentPrice > targetPrice; } +} +contract AerodromeAMOQuoter { + //////////////////////////////////////////////////////////////// + /// --- STRUCTS & ENUMS + /////////////////////////////////////////////////////////////// struct Data { uint256 amount; uint256 iterations; } - event ValueFound(uint256 value, uint256 iterations); + //////////////////////////////////////////////////////////////// + /// --- VARIABLES STORAGE + //////////////////////////////////////////////////////////////// + QuoterHelper public quoterHelper; + + //////////////////////////////////////////////////////////////// + /// --- CONSTRUCTOR + //////////////////////////////////////////////////////////////// + constructor(address _strategy) { + quoterHelper = new QuoterHelper(IAMOStrategy(_strategy)); + } + + //////////////////////////////////////////////////////////////// + /// --- ERRORS & EVENTS + //////////////////////////////////////////////////////////////// + event ValueFound(uint256 value, uint256 iterations, bool swapWETHForOETHB); 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) { + //////////////////////////////////////////////////////////////// + /// --- FUNCTIONS + //////////////////////////////////////////////////////////////// + function quoteAmountToSwapBeforeRebalance() + public + returns (Data memory data) + { + try quoterHelper.getAmountToSwapBeforeRebalance() { revert("Previous call should only revert, it cannot succeed"); } catch (bytes memory reason) { bytes4 receivedSelector = bytes4(reason); @@ -294,13 +330,15 @@ contract AerodromeAMOQuoter { if (receivedSelector == QuoterHelper.ValidAmount.selector) { uint256 value; uint256 iterations; + bool swapWETHForOETHB; // solhint-disable-next-line no-inline-assembly assembly { value := mload(add(reason, 0x24)) iterations := mload(add(reason, 0x44)) + swapWETHForOETHB := mload(add(reason, 0x64)) } - emit ValueFound(value, iterations); + emit ValueFound(value, iterations, swapWETHForOETHB); return Data({ amount: value, iterations: iterations }); } diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 5f4acdf997..17b1e01a5f 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -143,7 +143,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { oethbVaultSigner = await impersonateAndFund(oethbVault.address); gauge = fixture.aeroClGauge; - await deployWithConfirmation("AerodromeAMOQuoter"); + await deployWithConfirmation("AerodromeAMOQuoter", [ + aerodromeAmoStrategy.address, + ]); quoter = await hre.ethers.getContract("AerodromeAMOQuoter"); await setup(); @@ -569,22 +571,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should be able to deposit to the pool & rebalance", async () => { await mintAndDepositToStrategy({ amount: oethUnits("5") }); - const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); - const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); - const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); - const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; - - let direction = currentPrice > targetPrice ? true : false; - const value = await quoteAmountToSwapBeforeRebalance( - direction, - oethUnits("0.00001"), - oethUnits("1") - ); - const tx = await rebalance( - value, - direction, - 0 - ); + const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + const tx = await rebalance(value, direction, 0); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); await assetLpStakedInGauge(); @@ -593,22 +581,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should be able to deposit to the pool & rebalance multiple times", async () => { await mintAndDepositToStrategy({ amount: oethUnits("5") }); - const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); - const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); - const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); - const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; - - let direction = currentPrice > targetPrice ? true : false; - const value = await quoteAmountToSwapBeforeRebalance( - direction, - oethUnits("0.00001"), - oethUnits("1") - ); - const tx = await rebalance( - value, - direction, - 0 - ); + const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + const tx = await rebalance(value, direction, 0); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); await assetLpStakedInGauge(); @@ -673,24 +647,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); it("Should revert when pool rebalance is off target", async () => { - const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); - const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); - const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); - const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; - - let direction = currentPrice > targetPrice ? true : false; - const value = await quoteAmountToSwapBeforeRebalance( - direction, - oethUnits("0.00000001"), - oethUnits("1000") - ); + const { value, direction } = await quoteAmountToSwapBeforeRebalance(); await expect( - rebalance( - value.mul("200").div("100"), - direction, - 0 - ) + rebalance(value.mul("200").div("100"), direction, 0) ).to.be.revertedWith("PoolRebalanceOutOfBounds"); }); @@ -703,22 +663,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { // supply some WETH for the rebalance await mintAndDepositToStrategy({ amount: oethUnits("1") }); - const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); - const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); - const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); - const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; - - let direction = currentPrice > targetPrice ? true : false; - const value = await quoteAmountToSwapBeforeRebalance( - direction, - oethUnits("0.00000001"), - oethUnits("1000") - ); - await rebalance( - value, - direction, - 0 - ); + const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + await rebalance(value, direction, 0); await assetLpStakedInGauge(); }); @@ -729,22 +675,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { swapWeth: true, }); - const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); - const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); - const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); - const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; - - let direction = currentPrice > targetPrice ? true : false; - const value = await quoteAmountToSwapBeforeRebalance( - direction, - oethUnits("0.00000001"), - oethUnits("1000") - ); - await rebalance( - value, - direction, - 0 - ); + const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + await rebalance(value, direction, 0); await assetLpStakedInGauge(); }); @@ -895,24 +827,13 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { const setup = async () => { await mintAndDepositToStrategy({ amount: oethUnits("5") }); - const currentPrice = await aerodromeAmoStrategy.getPoolX96Price(); - const priceTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); - const priceTick1 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); - const targetPrice = priceTick0 * 0.2 + priceTick1 * 0.8; - - let direction = currentPrice > targetPrice ? true : false; - - const value = await quoteAmountToSwapBeforeRebalance( - direction, - oethUnits("0.00000001"), - oethUnits("1000") - ); + const { value, direction } = await quoteAmountToSwapBeforeRebalance(); // move the price to pre-configured 20% value await rebalance( value, direction, // _swapWETH - value.mul(90).div(100) + 0 ); }; @@ -964,29 +885,19 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); }; - const quoteAmountToSwapBeforeRebalance = async ( - swapWETH, - minAmount, - maxAmount - ) => { + const quoteAmountToSwapBeforeRebalance = async () => { await oethbVault .connect(await impersonateAndFund(addresses.base.governor)) .setStrategistAddr(await quoter.quoterHelper()); - const txResponse = await quoter.quoteAmountToSwapBeforeRebalance( - aerodromeAmoStrategy.address, - swapWETH, - minAmount, - maxAmount, - 50 - ); + const txResponse = await quoter.quoteAmountToSwapBeforeRebalance(); const txReceipt = await txResponse.wait(); const [transferEvent] = txReceipt.events; const value = transferEvent.args.value; + const direction = transferEvent.args.swapWETHForOETHB; await oethbVault .connect(await impersonateAndFund(addresses.base.governor)) .setStrategistAddr(addresses.base.strategist); - //console.log("Value to swap", value.toString()); - return value; + return { value, direction }; }; const rebalance = async (amountToSwap, swapWETH, minTokenReceived) => { From 9b91a6a3e03a081f96cf3ff149ebfe0b59d8e6d5 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Sat, 31 Aug 2024 11:19:55 +0200 Subject: [PATCH 05/28] fix: set strategist back to origin strategist for quoter. --- .../test/strategies/aerodrome-amo.base.fork-test.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 17b1e01a5f..8d125598f6 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -886,17 +886,27 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }; const quoteAmountToSwapBeforeRebalance = async () => { + // Get the strategist address + const strategist = oethbVault.strategistAddr(); + + // Set Quoter as strategist to pass the `onlyGovernorOrStrategist` requirement await oethbVault .connect(await impersonateAndFund(addresses.base.governor)) .setStrategistAddr(await quoter.quoterHelper()); + + // Get the quote const txResponse = await quoter.quoteAmountToSwapBeforeRebalance(); const txReceipt = await txResponse.wait(); const [transferEvent] = txReceipt.events; const value = transferEvent.args.value; const direction = transferEvent.args.swapWETHForOETHB; + + // Set back the original strategist await oethbVault .connect(await impersonateAndFund(addresses.base.governor)) - .setStrategistAddr(addresses.base.strategist); + .setStrategistAddr(strategist); + + // Return the value and direction return { value, direction }; }; From 585b40a1f588e3aaa3be6369a321bbf2b285391c Mon Sep 17 00:00:00 2001 From: clement-ux Date: Sat, 31 Aug 2024 12:27:52 +0200 Subject: [PATCH 06/28] feat: add AMO quoter for `amountToSwapToReachPrice`. --- .../interfaces/aerodrome/IAMOStrategy.sol | 2 + .../contracts/utils/AerodromeAMOQuoter.sol | 120 +++++++++++++++++- .../aerodrome-amo.base.fork-test.js | 1 + contracts/utils/addresses.js | 2 + 4 files changed, 120 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol index a7dbc3bb3c..fa7334e3a4 100644 --- a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol +++ b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol @@ -49,4 +49,6 @@ interface IAMOStrategy { function sqrtRatioX96TickLower() external view returns (uint160); function sqrtRatioX96TickHigher() external view returns (uint160); + + function tickSpacing() external view returns (int24); } diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index f7c457a865..fc3d858356 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.7; +import { ICLPool } from "../interfaces/aerodrome/ICLPool.sol"; +import { IQuoterV2 } from "../interfaces/aerodrome/IQuoterV2.sol"; import { IAMOStrategy } from "../interfaces/aerodrome/IAMOStrategy.sol"; contract QuoterHelper { @@ -35,10 +37,14 @@ contract QuoterHelper { uint256 public constant BINARY_MIN_AMOUNT = 0.000_000_01 ether; uint256 public constant BINARY_MAX_AMOUNT = 1_000 ether; uint256 public constant BINARY_MAX_ITERATIONS = 50; + uint256 public constant PERCENTAGE_BASE = 1e27; // 100% + uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e18; // 1% //////////////////////////////////////////////////////////////// /// --- VARIABLES STORAGE //////////////////////////////////////////////////////////////// + ICLPool public clPool; + IQuoterV2 public quoterV2; IAMOStrategy public strategy; //////////////////////////////////////////////////////////////// @@ -55,8 +61,10 @@ contract QuoterHelper { //////////////////////////////////////////////////////////////// /// --- CONSTRUCTOR //////////////////////////////////////////////////////////////// - constructor(IAMOStrategy _strategy) { + constructor(IAMOStrategy _strategy, IQuoterV2 _quoterV2) { strategy = _strategy; + quoterV2 = _quoterV2; + clPool = strategy.clPool(); } //////////////////////////////////////////////////////////////// @@ -69,7 +77,7 @@ contract QuoterHelper { uint256 high = BINARY_MAX_AMOUNT; int24 lowerTick = strategy.lowerTick(); int24 upperTick = strategy.upperTick(); - bool swapWETHForOETHB = getSwapDirection(); + bool swapWETHForOETHB = getSwapDirectionForRebalance(); while (low <= high && iterations < BINARY_MAX_ITERATIONS) { uint256 mid = (low + high) / 2; @@ -278,7 +286,7 @@ contract QuoterHelper { } } - function getSwapDirection() public view returns (bool) { + function getSwapDirectionForRebalance() public view returns (bool) { uint160 currentPrice = strategy.getPoolX96Price(); uint160 ticker0Price = strategy.sqrtRatioX96TickLower(); uint160 ticker1Price = strategy.sqrtRatioX96TickHigher(); @@ -286,6 +294,80 @@ contract QuoterHelper { return currentPrice > targetPrice; } + + function getAmountToSwapToReachPrice(uint160 sqrtPriceTargetX96) + public + returns ( + uint256, + uint256, + bool, + uint160 + ) + { + uint256 iterations; + uint256 low = BINARY_MIN_AMOUNT; + uint256 high = BINARY_MAX_AMOUNT; + bool swapWETHForOETHB = getSwapDirection(sqrtPriceTargetX96); + + while (low <= high && iterations < BINARY_MAX_ITERATIONS) { + uint256 mid = (low + high) / 2; + + // Call QuoterV2 from SugarHelper + (, uint160 sqrtPriceX96After, , ) = quoterV2.quoteExactInputSingle( + IQuoterV2.QuoteExactInputSingleParams({ + tokenIn: swapWETHForOETHB + ? clPool.token0() + : clPool.token1(), + tokenOut: swapWETHForOETHB + ? clPool.token1() + : clPool.token0(), + amountIn: mid, + tickSpacing: strategy.tickSpacing(), + sqrtPriceLimitX96: sqrtPriceTargetX96 + }) + ); + + if ( + low == high || + isWithinAllowedVariance(sqrtPriceX96After, sqrtPriceTargetX96) + ) { + return (mid, iterations, swapWETHForOETHB, sqrtPriceX96After); + } else if ( + swapWETHForOETHB + ? sqrtPriceX96After > sqrtPriceTargetX96 + : sqrtPriceX96After < sqrtPriceTargetX96 + ) { + low = mid + 1; + } else { + high = mid; + } + iterations++; + } + + revert OutOfIterations(iterations); + } + + function isWithinAllowedVariance( + uint160 sqrtPriceCurrentX96, + uint160 sqrtPriceTargetX96 + ) public pure returns (bool) { + uint256 allowedVariance = (sqrtPriceTargetX96 * + ALLOWED_VARIANCE_PERCENTAGE) / PERCENTAGE_BASE; + if (sqrtPriceCurrentX96 > sqrtPriceTargetX96) { + return sqrtPriceCurrentX96 - sqrtPriceTargetX96 <= allowedVariance; + } else { + return sqrtPriceTargetX96 - sqrtPriceCurrentX96 <= allowedVariance; + } + } + + function getSwapDirection(uint160 sqrtPriceTargetX96) + public + view + returns (bool) + { + uint160 currentPrice = strategy.getPoolX96Price(); + return currentPrice > sqrtPriceTargetX96; + } } contract AerodromeAMOQuoter { @@ -305,19 +387,31 @@ contract AerodromeAMOQuoter { //////////////////////////////////////////////////////////////// /// --- CONSTRUCTOR //////////////////////////////////////////////////////////////// - constructor(address _strategy) { - quoterHelper = new QuoterHelper(IAMOStrategy(_strategy)); + constructor(address _strategy, address _quoterV2) { + quoterHelper = new QuoterHelper( + IAMOStrategy(_strategy), + IQuoterV2(_quoterV2) + ); } //////////////////////////////////////////////////////////////// /// --- ERRORS & EVENTS //////////////////////////////////////////////////////////////// event ValueFound(uint256 value, uint256 iterations, bool swapWETHForOETHB); + event ValueFoundBis( + uint256 value, + uint256 iterations, + bool swapWETHForOETHB, + uint160 sqrtPriceAfterX96 + ); event ValueNotFound(string message); //////////////////////////////////////////////////////////////// /// --- FUNCTIONS //////////////////////////////////////////////////////////////// + /// @notice Use this to get the amount to swap before rebalance + /// @dev This call will only revert, check the logs to get returned values + /// @return data Data struct with the amount and the number of iterations function quoteAmountToSwapBeforeRebalance() public returns (Data memory data) @@ -351,4 +445,20 @@ contract AerodromeAMOQuoter { revert(abi.decode(reason, (string))); } } + + function quoteAmountToSwapToReachPrice(uint160 sqrtPriceTargetX96) public { + ( + uint256 amount, + uint256 iterations, + bool swapWETHForOETHB, + uint160 sqrtPriceAfterX96 + ) = quoterHelper.getAmountToSwapToReachPrice(sqrtPriceTargetX96); + + emit ValueFoundBis( + amount, + iterations, + swapWETHForOETHB, + sqrtPriceAfterX96 + ); + } } diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 8d125598f6..47f26c14ed 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -145,6 +145,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { await deployWithConfirmation("AerodromeAMOQuoter", [ aerodromeAmoStrategy.address, + addresses.base.aeroQuoterV2Address, ]); quoter = await hre.ethers.getContract("AerodromeAMOQuoter"); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 2ac563b9ed..5f43d891d1 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -288,6 +288,8 @@ addresses.base.aeroFactoryAddress = "0x420DD381b31aEf6683db6B902084cB0FFECe40Da"; addresses.base.aeroGaugeGovernorAddress = "0xE6A41fE61E7a1996B59d508661e3f524d6A32075"; +addresses.base.aeroQuoterV2Address = + "0x254cF9E1E6e233aa1AC962CB9B05b2cfeAaE15b0"; addresses.base.ethUsdPriceFeed = "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70"; From 4390da3206ac7802731e3c81b86e76ffd60cd5c6 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Sun, 1 Sep 2024 00:11:09 +0200 Subject: [PATCH 07/28] feat: add quoter for moving price in AMO tests and upgrade swap function. --- .../aerodrome-amo.base.fork-test.js | 137 ++++++++++++++++-- 1 file changed, 126 insertions(+), 11 deletions(-) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 47f26c14ed..d348fe371a 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -25,7 +25,8 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () strategist, rafael, aeroSwapRouter, - aeroNftManager; + aeroNftManager, + quoter; beforeEach(async () => { fixture = await baseFixture(); @@ -47,6 +48,12 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () await oethb .connect(rafael) .approve(aeroSwapRouter.address, oethUnits("1000")); + + await deployWithConfirmation("AerodromeAMOQuoter", [ + aerodromeAmoStrategy.address, + addresses.base.aeroQuoterV2Address, + ]); + quoter = await hre.ethers.getContract("AerodromeAMOQuoter"); }); // Haven't found away to test for this in the strategy contract yet @@ -70,6 +77,12 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () }); it("Should be reverted trying to rebalance and we are not in the correct tick", async () => { + // Push price to tick 0, which is OutisdeExpectedTickRange + const { value, direction } = await quoteAmountToSwapToReachPrice( + await aerodromeAmoStrategy.sqrtRatioX96TickHigher() + ); + await swap({ amount: value, swapWeth: direction }); + await expect( aerodromeAmoStrategy .connect(strategist) @@ -110,6 +123,61 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () deadline: futureEpoch, }); }; + + const quoteAmountToSwapToReachPrice = async (price) => { + const txResponse = await quoter.quoteAmountToSwapToReachPrice(price); + const txReceipt = await txResponse.wait(); + const [transferEvent] = txReceipt.events; + const value = transferEvent.args.value; + const direction = transferEvent.args.swapWETHForOETHB; + const priceReached = transferEvent.args.sqrtPriceAfterX96; + return { value, direction, priceReached }; + }; + + const swap = async ({ amount, swapWeth }) => { + // Check if rafael as enough token to perfom swap + // If not, mint some + const balanceOETHb = await oethb.balanceOf(rafael.address); + const balanceWETH = await weth.balanceOf(rafael.address); + if (swapWeth && balanceWETH.lt(amount)) { + // Deal tokens + await setERC20TokenBalance( + rafael.address, + weth, + amount + balanceWETH, + hre + ); + } else if (!swapWeth && balanceOETHb.lt(amount)) { + await setERC20TokenBalance( + rafael.address, + weth, + amount + balanceWETH, + hre + ); + await weth.connect(rafael).approve(oethbVault.address, amount); + await oethbVault.connect(rafael).mint(weth.address, amount, amount); + // Deal WETH and mint OETHb + } + + const sqrtRatioX96Tick1000 = BigNumber.from( + "83290069058676223003182343270" + ); + const sqrtRatioX96TickM1000 = BigNumber.from( + "75364347830767020784054125655" + ); + await aeroSwapRouter.connect(rafael).exactInputSingle({ + tokenIn: swapWeth ? weth.address : oethb.address, + tokenOut: swapWeth ? oethb.address : weth.address, + tickSpacing: 1, + recipient: rafael.address, + deadline: 9999999999, + amountIn: amount, + amountOutMinimum: 0, // slippage check + sqrtPriceLimitX96: swapWeth + ? sqrtRatioX96TickM1000 + : sqrtRatioX96Tick1000, + }); + }; }); describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { @@ -330,9 +398,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 100 wei is really small + // Little to no weth should be left on the strategy contract - 1000 wei is really small expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( - BigNumber.from("100") + BigNumber.from("1000") ); await assetLpStakedInGauge(); @@ -420,9 +488,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 100 wei is really small + // Little to no weth should be left on the strategy contract - 1000 wei is really small expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( - BigNumber.from("100") + BigNumber.from("1000") ); await assetLpStakedInGauge(); @@ -505,9 +573,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 100 wei is really small + // Little to no weth should be left on the strategy contract - 1000 wei is really small expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( - BigNumber.from("100") + BigNumber.from("1000") ); await assetLpStakedInGauge(); @@ -603,6 +671,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should check that add liquidity in difference cases leaves no to little weth on the contract", async () => { const amount = oethUnits("5"); + weth.connect(rafael).approve(oethbVault.address, amount); await oethbVault.connect(rafael).mint(weth.address, amount, amount); expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.equal( oethUnits("0") @@ -656,9 +725,12 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); it("Should be able to rebalance the pool when price pushed to 1:1", async () => { + const priceAtTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + let { value: value0, direction: direction0 } = + await quoteAmountToSwapToReachPrice(priceAtTick0); await swap({ - amount: oethUnits("5"), - swapWeth: false, + amount: value0, + swapWeth: direction0, }); // supply some WETH for the rebalance @@ -671,9 +743,13 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); it("Should be able to rebalance the pool when price pushed to close to 1 OETHb costing 1.0001 WETH", async () => { + const priceAtTickLower = + await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + let { value: value0, direction: direction0 } = + await quoteAmountToSwapToReachPrice(priceAtTickLower); await swap({ - amount: oethUnits("20.44"), - swapWeth: true, + amount: value0, + swapWeth: direction0, }); const { value, direction } = await quoteAmountToSwapBeforeRebalance(); @@ -865,7 +941,41 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { // console.log("price of OETHb : ", displayedPoolPrice); // }; + const quoteAmountToSwapToReachPrice = async (price) => { + const txResponse = await quoter.quoteAmountToSwapToReachPrice(price); + const txReceipt = await txResponse.wait(); + const [transferEvent] = txReceipt.events; + const value = transferEvent.args.value; + const direction = transferEvent.args.swapWETHForOETHB; + const priceReached = transferEvent.args.sqrtPriceAfterX96; + return { value, direction, priceReached }; + }; + const swap = async ({ amount, swapWeth }) => { + // Check if rafael as enough token to perfom swap + // If not, mint some + const balanceOETHb = await oethb.balanceOf(rafael.address); + const balanceWETH = await weth.balanceOf(rafael.address); + if (swapWeth && balanceWETH.lt(amount)) { + // Deal tokens + await setERC20TokenBalance( + rafael.address, + weth, + amount + balanceWETH, + hre + ); + } else if (!swapWeth && balanceOETHb.lt(amount)) { + await setERC20TokenBalance( + rafael.address, + weth, + amount + balanceWETH, + hre + ); + await weth.connect(rafael).approve(oethbVault.address, amount); + await oethbVault.connect(rafael).mint(weth.address, amount, amount); + // Deal WETH and mint OETHb + } + const sqrtRatioX96Tick1000 = BigNumber.from( "83290069058676223003182343270" ); @@ -921,6 +1031,11 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { const user = userOverride || rafael; amount = amount || oethUnits("5"); + const balance = weth.balanceOf(user.address); + if (balance < amount) { + await setERC20TokenBalance(user.address, weth, amount + balance, hre); + } + await weth.connect(user).approve(oethbVault.address, amount); await oethbVault.connect(user).mint(weth.address, amount, amount); await oethbVault From aea2198cd0c0437de1ca8d2e2ee1e6ce822b4d26 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Wed, 4 Sep 2024 16:31:39 +0200 Subject: [PATCH 08/28] docs: add descriptions to functions. --- .../contracts/utils/AerodromeAMOQuoter.sol | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index fc3d858356..cb2ac78d1a 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -164,6 +164,10 @@ contract QuoterHelper { revert OutOfIterations(iterations); } + /// @notice Get the status of the rebalance + /// @param amount The amount of token to swap + /// @param swapWETH True if we need to swap WETH for OETHb, false otherwise + /// @return status The status of the rebalance function getRebalanceStatus(uint256 amount, bool swapWETH) public returns (RebalanceStatus memory status) @@ -286,6 +290,8 @@ contract QuoterHelper { } } + /// @notice Get the swap direction to reach the target price before rebalance. + /// @return bool True if we need to swap WETH for OETHb, false otherwise. function getSwapDirectionForRebalance() public view returns (bool) { uint160 currentPrice = strategy.getPoolX96Price(); uint160 ticker0Price = strategy.sqrtRatioX96TickLower(); @@ -295,6 +301,13 @@ contract QuoterHelper { return currentPrice > targetPrice; } + /// @notice Get the amount of tokens to swap to reach the target price. + /// @dev This act like a quoter, i.e. the transaction is not performed. + /// @param sqrtPriceTargetX96 The target price to reach. + /// @return amount The amount of tokens to swap. + /// @return iterations The number of iterations to find the amount. + /// @return swapWETHForOETHB True if we need to swap WETH for OETHb, false otherwise. + /// @return sqrtPriceX96After The price after the swap. function getAmountToSwapToReachPrice(uint160 sqrtPriceTargetX96) public returns ( @@ -347,6 +360,8 @@ contract QuoterHelper { revert OutOfIterations(iterations); } + /// @notice Check if the current price is within the allowed variance in comparison to the target price + /// @return bool True if the current price is within the allowed variance, false otherwise function isWithinAllowedVariance( uint160 sqrtPriceCurrentX96, uint160 sqrtPriceTargetX96 @@ -360,6 +375,9 @@ contract QuoterHelper { } } + /// @notice Get the swap direction to reach the target price. + /// @param sqrtPriceTargetX96 The target price to reach. + /// @return bool True if we need to swap WETH for OETHb, false otherwise. function getSwapDirection(uint160 sqrtPriceTargetX96) public view @@ -446,6 +464,9 @@ contract AerodromeAMOQuoter { } } + /// @notice Use this to get the amount to swap to reach the target price after swap. + /// @dev This call will only revert, check the logs to get returned values. + /// @param sqrtPriceTargetX96 The target price to reach. function quoteAmountToSwapToReachPrice(uint160 sqrtPriceTargetX96) public { ( uint256 amount, From bf51ef92d49b4be21f34f0ea9184f2f5f4eb75c9 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Wed, 4 Sep 2024 21:38:38 +0200 Subject: [PATCH 09/28] feat: add `overrideWethShare` on `quoteAmountToSwapBeforeRebalance`. --- .../interfaces/aerodrome/IAMOStrategy.sol | 4 ++ .../contracts/utils/AerodromeAMOQuoter.sol | 65 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol index fa7334e3a4..89ac9a0e65 100644 --- a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol +++ b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol @@ -51,4 +51,8 @@ interface IAMOStrategy { function sqrtRatioX96TickHigher() external view returns (uint160); function tickSpacing() external view returns (int24); + + function allowedWethShareStart() external view returns (uint256); + + function allowedWethShareEnd() external view returns (uint256); } diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index cb2ac78d1a..8dc485b8df 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -71,7 +71,28 @@ contract QuoterHelper { /// --- FUNCTIONS //////////////////////////////////////////////////////////////// /// @notice This call can only end with a revert. - function getAmountToSwapBeforeRebalance() public { + function getAmountToSwapBeforeRebalance( + uint256 overrideBottomWethShare, + uint256 overrideTopWethShare + ) public { + if ( + overrideBottomWethShare != type(uint256).max || + overrideTopWethShare != type(uint256).max + ) { + // Current values + uint256 shareStart = strategy.allowedWethShareStart(); + uint256 shareEnd = strategy.allowedWethShareEnd(); + + // Override values + if (overrideBottomWethShare != type(uint256).max) { + shareStart = overrideBottomWethShare; + } + if (overrideTopWethShare != type(uint256).max) { + shareEnd = overrideTopWethShare; + } + + strategy.setAllowedPoolWethShareInterval(shareStart, shareEnd); + } uint256 iterations; uint256 low = BINARY_MIN_AMOUNT; uint256 high = BINARY_MAX_AMOUNT; @@ -428,13 +449,51 @@ contract AerodromeAMOQuoter { /// --- FUNCTIONS //////////////////////////////////////////////////////////////// /// @notice Use this to get the amount to swap before rebalance - /// @dev This call will only revert, check the logs to get returned values + /// @dev This call will only revert, check the logs to get returned values. + /// @dev Need to perform this call while impersonating the governor or strategist of AMO. /// @return data Data struct with the amount and the number of iterations function quoteAmountToSwapBeforeRebalance() public returns (Data memory data) { - try quoterHelper.getAmountToSwapBeforeRebalance() { + return + _quoteAmountToSwapBeforeRebalance( + type(uint256).max, + type(uint256).max + ); + } + + /// @notice Use this to get the amount to swap before rebalance and + /// update allowedWethShareStart and allowedWethShareEnd on AMO. + /// @dev This call will only revert, check the logs to get returned values. + /// @dev Need to perform this call while impersonating the governor of AMO. + /// @param overrideBottomWethShare New value for the allowedWethShareStart on AMO. + /// Use type(uint256).max to keep same value. + /// @param overrideTopWethShare New value for the allowedWethShareEnd on AMO. + /// Use type(uint256).max to keep same value. + /// @return data Data struct with the amount and the number of iterations + function quoteAmountToSwapBeforeRebalance( + uint256 overrideBottomWethShare, + uint256 overrideTopWethShare + ) public returns (Data memory data) { + return + _quoteAmountToSwapBeforeRebalance( + overrideBottomWethShare, + overrideTopWethShare + ); + } + + /// @notice Internal logic for quoteAmountToSwapBeforeRebalance. + function _quoteAmountToSwapBeforeRebalance( + uint256 overrideBottomWethShare, + uint256 overrideTopWethShare + ) public returns (Data memory data) { + try + quoterHelper.getAmountToSwapBeforeRebalance( + overrideBottomWethShare, + overrideTopWethShare + ) + { revert("Previous call should only revert, it cannot succeed"); } catch (bytes memory reason) { bytes4 receivedSelector = bytes4(reason); From c502ed61d88bc9416b01b34264000f71c69307d8 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Wed, 4 Sep 2024 22:26:05 +0200 Subject: [PATCH 10/28] fix: use dictionnary syntax to call function that has same name as another. --- contracts/test/strategies/aerodrome-amo.base.fork-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index d348fe371a..47cc3c9103 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -1006,7 +1006,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { .setStrategistAddr(await quoter.quoterHelper()); // Get the quote - const txResponse = await quoter.quoteAmountToSwapBeforeRebalance(); + const txResponse = await quoter["quoteAmountToSwapBeforeRebalance()"](); const txReceipt = await txResponse.wait(); const [transferEvent] = txReceipt.events; const value = transferEvent.args.value; From 7383e7978221a6027ab094fbf1cd5d6429fb0edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 9 Sep 2024 17:47:23 +0200 Subject: [PATCH 11/28] fix: increase max iteration for quoter. --- contracts/contracts/utils/AerodromeAMOQuoter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index 8dc485b8df..fc003ba438 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -36,7 +36,7 @@ contract QuoterHelper { //////////////////////////////////////////////////////////////// uint256 public constant BINARY_MIN_AMOUNT = 0.000_000_01 ether; uint256 public constant BINARY_MAX_AMOUNT = 1_000 ether; - uint256 public constant BINARY_MAX_ITERATIONS = 50; + uint256 public constant BINARY_MAX_ITERATIONS = 100; uint256 public constant PERCENTAGE_BASE = 1e27; // 100% uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e18; // 1% From ffa2d8ea407607546f00658e77d1e96e84bd066b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 9 Sep 2024 17:48:02 +0200 Subject: [PATCH 12/28] fix: failling test. --- contracts/test/strategies/aerodrome-amo.base.fork-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 47cc3c9103..e45ed01593 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -807,7 +807,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { await mintAndDepositToStrategy({ amount: oethUnits("10") }); // transfer WETH out making the protocol insolvent - await weth.connect(stratSigner).transfer(addresses.dead, oethUnits("5")); + const bal = await weth.balanceOf(aerodromeAmoStrategy.address); + await weth.connect(stratSigner).transfer(addresses.dead, bal); await expect( rebalance( From 280f6b4f666b2ed1b928448162e061106676908a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 11 Sep 2024 15:09:54 +0200 Subject: [PATCH 13/28] fix: move AMOQuoter to fixture. --- contracts/test/_fixture-base.js | 10 ++++++++++ .../test/strategies/aerodrome-amo.base.fork-test.js | 7 +------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index a3d1c1abdb..53ee2e3666 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -4,6 +4,7 @@ const mocha = require("mocha"); const { isFork, isBaseFork, oethUnits } = require("./helpers"); const { impersonateAndFund } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); +const { deployWithConfirmation } = require("../utils/deploy"); const addresses = require("../utils/addresses"); const erc20Abi = require("./abi/erc20.json"); @@ -163,6 +164,12 @@ const defaultBaseFixture = deployments.createFixture(async () => { addresses.base.nonFungiblePositionManager ); + await deployWithConfirmation("AerodromeAMOQuoter", [ + aerodromeAmoStrategy.address, + addresses.base.aeroQuoterV2Address, + ]); + const quoter = await hre.ethers.getContract("AerodromeAMOQuoter"); + return { // Aerodrome aeroSwapRouter, @@ -197,6 +204,9 @@ const defaultBaseFixture = deployments.createFixture(async () => { rafael, nick, clement, + + // Helper + quoter, }; }); diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index e45ed01593..d480bf21c3 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -210,12 +210,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { aeroNftManager = fixture.aeroNftManager; oethbVaultSigner = await impersonateAndFund(oethbVault.address); gauge = fixture.aeroClGauge; - - await deployWithConfirmation("AerodromeAMOQuoter", [ - aerodromeAmoStrategy.address, - addresses.base.aeroQuoterV2Address, - ]); - quoter = await hre.ethers.getContract("AerodromeAMOQuoter"); + quoter = fixture.quoter; await setup(); await weth From 8abb41225c07deda4df9da1cd5a804c7402aebee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 11 Sep 2024 15:16:56 +0200 Subject: [PATCH 14/28] feat: add more comments. --- contracts/contracts/utils/AerodromeAMOQuoter.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index fc003ba438..8c873e78ba 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -5,6 +5,10 @@ import { ICLPool } from "../interfaces/aerodrome/ICLPool.sol"; import { IQuoterV2 } from "../interfaces/aerodrome/IQuoterV2.sol"; import { IAMOStrategy } from "../interfaces/aerodrome/IAMOStrategy.sol"; +/// @title QuoterHelper +/// @author Origin Protocol +/// @notice Helper for Aerodrome AMO Quoter, as `_quoteAmountToSwapBeforeRebalance` use try/catch method and +/// this can only be used when calling external contracts. contract QuoterHelper { //////////////////////////////////////////////////////////////// /// --- STRUCTS & ENUMS @@ -409,6 +413,9 @@ contract QuoterHelper { } } +/// @title AerodromeAMOQuoter +/// @author Origin Protocol +/// @notice Quoter for Aerodrome AMO contract AerodromeAMOQuoter { //////////////////////////////////////////////////////////////// /// --- STRUCTS & ENUMS From 3e076b96c718e46b3df982854fe335bd1caa405a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 11 Sep 2024 15:59:31 +0200 Subject: [PATCH 15/28] fix: rethink variance calculation for AMO Quoter. --- contracts/contracts/utils/AerodromeAMOQuoter.sol | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index 8c873e78ba..9493f40961 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -41,8 +41,8 @@ contract QuoterHelper { uint256 public constant BINARY_MIN_AMOUNT = 0.000_000_01 ether; uint256 public constant BINARY_MAX_AMOUNT = 1_000 ether; uint256 public constant BINARY_MAX_ITERATIONS = 100; - uint256 public constant PERCENTAGE_BASE = 1e27; // 100% - uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e18; // 1% + uint256 public constant PERCENTAGE_BASE = 1e18; // 100% + uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e16; // 1% //////////////////////////////////////////////////////////////// /// --- VARIABLES STORAGE @@ -390,13 +390,12 @@ contract QuoterHelper { function isWithinAllowedVariance( uint160 sqrtPriceCurrentX96, uint160 sqrtPriceTargetX96 - ) public pure returns (bool) { - uint256 allowedVariance = (sqrtPriceTargetX96 * - ALLOWED_VARIANCE_PERCENTAGE) / PERCENTAGE_BASE; + ) public view returns (bool) { + uint160 range = strategy.sqrtRatioX96TickHigher() - strategy.sqrtRatioX96TickLower(); if (sqrtPriceCurrentX96 > sqrtPriceTargetX96) { - return sqrtPriceCurrentX96 - sqrtPriceTargetX96 <= allowedVariance; + return (sqrtPriceCurrentX96 - sqrtPriceTargetX96) * PERCENTAGE_BASE <= ALLOWED_VARIANCE_PERCENTAGE * range; } else { - return sqrtPriceTargetX96 - sqrtPriceCurrentX96 <= allowedVariance; + return (sqrtPriceTargetX96 - sqrtPriceCurrentX96) * PERCENTAGE_BASE <= ALLOWED_VARIANCE_PERCENTAGE * range; } } From a4d585bd7b9fbeb7b01e213cef67aaba6d92fc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 11 Sep 2024 17:17:05 +0200 Subject: [PATCH 16/28] fix: give more WETH at start and simplifies swap. --- contracts/test/_fixture-base.js | 2 +- .../aerodrome-amo.base.fork-test.js | 43 ++----------------- 2 files changed, 4 insertions(+), 41 deletions(-) diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 53ee2e3666..093d2e6f4a 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -138,7 +138,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { for (const user of [rafael, nick]) { // Mint some bridged WOETH await woeth.connect(minter).mint(user.address, oethUnits("1")); - await weth.connect(user).deposit({ value: oethUnits("100") }); + await weth.connect(user).deposit({ value: oethUnits("5000") }); // Set allowance on the vault await weth.connect(user).approve(oethbVault.address, oethUnits("50")); diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index d480bf21c3..979cc7293f 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -7,7 +7,6 @@ const { expect } = require("chai"); const { oethUnits } = require("../helpers"); const ethers = require("ethers"); const { impersonateAndFund } = require("../../utils/signers"); -const { deployWithConfirmation } = require("../../utils/deploy"); //const { formatUnits } = ethers.utils; const { BigNumber } = ethers; @@ -39,6 +38,7 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () rafael = fixture.rafael; aeroSwapRouter = fixture.aeroSwapRouter; aeroNftManager = fixture.aeroNftManager; + quoter = fixture.quoter; await setupEmpty(); @@ -48,12 +48,6 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () await oethb .connect(rafael) .approve(aeroSwapRouter.address, oethUnits("1000")); - - await deployWithConfirmation("AerodromeAMOQuoter", [ - aerodromeAmoStrategy.address, - addresses.base.aeroQuoterV2Address, - ]); - quoter = await hre.ethers.getContract("AerodromeAMOQuoter"); }); // Haven't found away to test for this in the strategy contract yet @@ -138,25 +132,9 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () // Check if rafael as enough token to perfom swap // If not, mint some const balanceOETHb = await oethb.balanceOf(rafael.address); - const balanceWETH = await weth.balanceOf(rafael.address); - if (swapWeth && balanceWETH.lt(amount)) { - // Deal tokens - await setERC20TokenBalance( - rafael.address, - weth, - amount + balanceWETH, - hre - ); - } else if (!swapWeth && balanceOETHb.lt(amount)) { - await setERC20TokenBalance( - rafael.address, - weth, - amount + balanceWETH, - hre - ); + if (!swapWeth && balanceOETHb.lt(amount)) { await weth.connect(rafael).approve(oethbVault.address, amount); await oethbVault.connect(rafael).mint(weth.address, amount, amount); - // Deal WETH and mint OETHb } const sqrtRatioX96Tick1000 = BigNumber.from( @@ -951,22 +929,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { // Check if rafael as enough token to perfom swap // If not, mint some const balanceOETHb = await oethb.balanceOf(rafael.address); - const balanceWETH = await weth.balanceOf(rafael.address); - if (swapWeth && balanceWETH.lt(amount)) { - // Deal tokens - await setERC20TokenBalance( - rafael.address, - weth, - amount + balanceWETH, - hre - ); - } else if (!swapWeth && balanceOETHb.lt(amount)) { - await setERC20TokenBalance( - rafael.address, - weth, - amount + balanceWETH, - hre - ); + if (!swapWeth && balanceOETHb.lt(amount)) { await weth.connect(rafael).approve(oethbVault.address, amount); await oethbVault.connect(rafael).mint(weth.address, amount, amount); // Deal WETH and mint OETHb From ef2bf26888dc04a7711ad171aa802a1b10c67513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 11 Sep 2024 17:17:16 +0200 Subject: [PATCH 17/28] fix: remove unused import. --- contracts/contracts/utils/AerodromeAMOQuoter.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index 9493f40961..4f7a84bc9a 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -391,11 +391,16 @@ contract QuoterHelper { uint160 sqrtPriceCurrentX96, uint160 sqrtPriceTargetX96 ) public view returns (bool) { - uint160 range = strategy.sqrtRatioX96TickHigher() - strategy.sqrtRatioX96TickLower(); + uint160 range = strategy.sqrtRatioX96TickHigher() - + strategy.sqrtRatioX96TickLower(); if (sqrtPriceCurrentX96 > sqrtPriceTargetX96) { - return (sqrtPriceCurrentX96 - sqrtPriceTargetX96) * PERCENTAGE_BASE <= ALLOWED_VARIANCE_PERCENTAGE * range; + return + (sqrtPriceCurrentX96 - sqrtPriceTargetX96) * PERCENTAGE_BASE <= + ALLOWED_VARIANCE_PERCENTAGE * range; } else { - return (sqrtPriceTargetX96 - sqrtPriceCurrentX96) * PERCENTAGE_BASE <= ALLOWED_VARIANCE_PERCENTAGE * range; + return + (sqrtPriceTargetX96 - sqrtPriceCurrentX96) * PERCENTAGE_BASE <= + ALLOWED_VARIANCE_PERCENTAGE * range; } } From 2d663be39a4d0792bf24566c38b9f50f7ae6b716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 11 Sep 2024 18:30:37 +0200 Subject: [PATCH 18/28] fix: _minTokenReceived is 99% of amountToSwap. --- contracts/test/strategies/aerodrome-amo.base.fork-test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 979cc7293f..341cd37440 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -614,7 +614,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { await mintAndDepositToStrategy({ amount: oethUnits("5") }); const { value, direction } = await quoteAmountToSwapBeforeRebalance(); - const tx = await rebalance(value, direction, 0); + const tx = await rebalance(value, direction, value.mul("99").div("100")); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); await assetLpStakedInGauge(); @@ -624,7 +624,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { await mintAndDepositToStrategy({ amount: oethUnits("5") }); const { value, direction } = await quoteAmountToSwapBeforeRebalance(); - const tx = await rebalance(value, direction, 0); + const tx = await rebalance(value, direction, value.mul("99").div("100")); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); await assetLpStakedInGauge(); @@ -710,7 +710,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { await mintAndDepositToStrategy({ amount: oethUnits("1") }); const { value, direction } = await quoteAmountToSwapBeforeRebalance(); - await rebalance(value, direction, 0); + await rebalance(value, direction, value.mul("99").div("100")); await assetLpStakedInGauge(); }); @@ -726,7 +726,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); const { value, direction } = await quoteAmountToSwapBeforeRebalance(); - await rebalance(value, direction, 0); + await rebalance(value, direction, value.mul("99").div("100")); await assetLpStakedInGauge(); }); From 80d115bcb971a42b2bf83fa8a68aa406d6ed5912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 11 Sep 2024 18:38:09 +0200 Subject: [PATCH 19/28] fix: refactor % comparison. --- contracts/contracts/utils/AerodromeAMOQuoter.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index 4f7a84bc9a..c48cdcaf3f 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -395,12 +395,12 @@ contract QuoterHelper { strategy.sqrtRatioX96TickLower(); if (sqrtPriceCurrentX96 > sqrtPriceTargetX96) { return - (sqrtPriceCurrentX96 - sqrtPriceTargetX96) * PERCENTAGE_BASE <= - ALLOWED_VARIANCE_PERCENTAGE * range; + (sqrtPriceCurrentX96 - sqrtPriceTargetX96) <= + (ALLOWED_VARIANCE_PERCENTAGE * range) / PERCENTAGE_BASE; } else { return - (sqrtPriceTargetX96 - sqrtPriceCurrentX96) * PERCENTAGE_BASE <= - ALLOWED_VARIANCE_PERCENTAGE * range; + (sqrtPriceTargetX96 - sqrtPriceCurrentX96) <= + (ALLOWED_VARIANCE_PERCENTAGE * range) / PERCENTAGE_BASE; } } From 6b72dc91debffda6d8065d0a05374654947d0bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 16 Sep 2024 11:54:09 +0200 Subject: [PATCH 20/28] fix: change public into internal. --- contracts/contracts/utils/AerodromeAMOQuoter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index c48cdcaf3f..a2bf2ff157 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -498,7 +498,7 @@ contract AerodromeAMOQuoter { function _quoteAmountToSwapBeforeRebalance( uint256 overrideBottomWethShare, uint256 overrideTopWethShare - ) public returns (Data memory data) { + ) internal returns (Data memory data) { try quoterHelper.getAmountToSwapBeforeRebalance( overrideBottomWethShare, From 944bb283ffa4204e41b05251173a99d11ff74ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 16 Sep 2024 15:40:00 +0200 Subject: [PATCH 21/28] fix: adjust quoteAmountToSwapBeforeRebalance for custom shares. --- .../interfaces/aerodrome/IAMOStrategy.sol | 4 ++ .../contracts/utils/AerodromeAMOQuoter.sol | 31 ++++++++- .../aerodrome-amo.base.fork-test.js | 69 ++++++++++++++----- 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol index 89ac9a0e65..49260cf1e0 100644 --- a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol +++ b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol @@ -55,4 +55,8 @@ interface IAMOStrategy { function allowedWethShareStart() external view returns (uint256); function allowedWethShareEnd() external view returns (uint256); + + function claimGovernance() external; + + function transferGovernance(address _governor) external; } diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index a2bf2ff157..44b7768bea 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -51,6 +51,8 @@ contract QuoterHelper { IQuoterV2 public quoterV2; IAMOStrategy public strategy; + address public originalGovernor; + //////////////////////////////////////////////////////////////// /// --- ERRORS & EVENTS //////////////////////////////////////////////////////////////// @@ -321,7 +323,13 @@ contract QuoterHelper { uint160 currentPrice = strategy.getPoolX96Price(); uint160 ticker0Price = strategy.sqrtRatioX96TickLower(); uint160 ticker1Price = strategy.sqrtRatioX96TickHigher(); - uint160 targetPrice = (ticker0Price * 20 + ticker1Price * 80) / 100; + uint256 allowedWethShareStart = strategy.allowedWethShareStart(); + uint256 allowedWethShareEnd = strategy.allowedWethShareEnd(); + uint160 mid = uint160(allowedWethShareStart + allowedWethShareEnd) / 2; + uint160 targetPrice = (ticker0Price * + mid + + ticker1Price * + (1 ether - mid)) / 1 ether; return currentPrice > targetPrice; } @@ -415,6 +423,19 @@ contract QuoterHelper { uint160 currentPrice = strategy.getPoolX96Price(); return currentPrice > sqrtPriceTargetX96; } + + function claimGovernanceOnAMO() public { + originalGovernor = strategy.governor(); + strategy.claimGovernance(); + } + + function giveBackGovernanceOnAMO() public { + require( + originalGovernor != address(0), + "Quoter: Original governor not set" + ); + strategy.transferGovernance(originalGovernor); + } } /// @title AerodromeAMOQuoter @@ -552,4 +573,12 @@ contract AerodromeAMOQuoter { sqrtPriceAfterX96 ); } + + function claimGovernance() public { + quoterHelper.claimGovernanceOnAMO(); + } + + function giveBackGovernance() public { + quoterHelper.giveBackGovernanceOnAMO(); + } } diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 341cd37440..8a9f071a88 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -613,7 +613,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should be able to deposit to the pool & rebalance", async () => { await mintAndDepositToStrategy({ amount: oethUnits("5") }); - const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + const { value, direction } = await quoteAmountToSwapBeforeRebalance({ + lowValue: oethUnits("0"), + highValue: oethUnits("0"), + }); const tx = await rebalance(value, direction, value.mul("99").div("100")); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); @@ -623,7 +626,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should be able to deposit to the pool & rebalance multiple times", async () => { await mintAndDepositToStrategy({ amount: oethUnits("5") }); - const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + const { value, direction } = await quoteAmountToSwapBeforeRebalance({ + lowValue: oethUnits("0"), + highValue: oethUnits("0"), + }); const tx = await rebalance(value, direction, value.mul("99").div("100")); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); @@ -690,11 +696,14 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); it("Should revert when pool rebalance is off target", async () => { - const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + const { value, direction } = await quoteAmountToSwapBeforeRebalance({ + lowValue: oethUnits("0.90"), + highValue: oethUnits("0.92"), + }); - await expect( - rebalance(value.mul("200").div("100"), direction, 0) - ).to.be.revertedWith("PoolRebalanceOutOfBounds"); + await expect(rebalance(value, direction, 0)).to.be.revertedWith( + "PoolRebalanceOutOfBounds" + ); }); it("Should be able to rebalance the pool when price pushed to 1:1", async () => { @@ -709,7 +718,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { // supply some WETH for the rebalance await mintAndDepositToStrategy({ amount: oethUnits("1") }); - const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + const { value, direction } = await quoteAmountToSwapBeforeRebalance({ + lowValue: oethUnits("0"), + highValue: oethUnits("0"), + }); await rebalance(value, direction, value.mul("99").div("100")); await assetLpStakedInGauge(); @@ -725,7 +737,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { swapWeth: direction0, }); - const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + const { value, direction } = await quoteAmountToSwapBeforeRebalance({ + lowValue: oethUnits("0"), + highValue: oethUnits("0"), + }); await rebalance(value, direction, value.mul("99").div("100")); await assetLpStakedInGauge(); @@ -878,7 +893,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { const setup = async () => { await mintAndDepositToStrategy({ amount: oethUnits("5") }); - const { value, direction } = await quoteAmountToSwapBeforeRebalance(); + const { value, direction } = await quoteAmountToSwapBeforeRebalance({ + lowValue: oethUnits("0"), + highValue: oethUnits("0"), + }); // move the price to pre-configured 20% value await rebalance( @@ -955,26 +973,41 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); }; - const quoteAmountToSwapBeforeRebalance = async () => { - // Get the strategist address - const strategist = oethbVault.strategistAddr(); - + const quoteAmountToSwapBeforeRebalance = async ({ lowValue, highValue }) => { // Set Quoter as strategist to pass the `onlyGovernorOrStrategist` requirement - await oethbVault - .connect(await impersonateAndFund(addresses.base.governor)) - .setStrategistAddr(await quoter.quoterHelper()); - + // Get governor + const gov = await aerodromeAmoStrategy.governor(); + // Set pending governance to quoter helper + await aerodromeAmoStrategy + .connect(await impersonateAndFund(gov)) + .transferGovernance(await quoter.quoterHelper()); + // Quoter claim governance) + await quoter.claimGovernance(); + + let txResponse; + if (lowValue == 0 && highValue == 0) { + txResponse = await quoter["quoteAmountToSwapBeforeRebalance()"](); + } else { + txResponse = await quoter[ + "quoteAmountToSwapBeforeRebalance(uint256,uint256)" + ](lowValue, highValue); + } // Get the quote - const txResponse = await quoter["quoteAmountToSwapBeforeRebalance()"](); const txReceipt = await txResponse.wait(); const [transferEvent] = txReceipt.events; const value = transferEvent.args.value; const direction = transferEvent.args.swapWETHForOETHB; // Set back the original strategist + /* await oethbVault .connect(await impersonateAndFund(addresses.base.governor)) .setStrategistAddr(strategist); + */ + quoter.giveBackGovernance(); + await aerodromeAmoStrategy + .connect(await impersonateAndFund(gov)) + .claimGovernance(); // Return the value and direction return { value, direction }; From 26035cca4f0fe1e359db344a8c618739e637563e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 16 Sep 2024 23:35:49 +0200 Subject: [PATCH 22/28] fix: split boundaries for quoter. --- contracts/contracts/utils/AerodromeAMOQuoter.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index 44b7768bea..f658788ceb 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -38,8 +38,10 @@ contract QuoterHelper { //////////////////////////////////////////////////////////////// /// --- CONSTANT & IMMUTABLE //////////////////////////////////////////////////////////////// - uint256 public constant BINARY_MIN_AMOUNT = 0.000_000_01 ether; - uint256 public constant BINARY_MAX_AMOUNT = 1_000 ether; + uint256 public constant BINARY_MIN_AMOUNT = 1 wei; + uint256 public constant BINARY_MAX_AMOUNT_FOR_REBALANCE = 3_000 ether; + uint256 public constant BINARY_MAX_AMOUNT_FOR_PUSH_PRICE = 1_000 ether; + uint256 public constant BINARY_MAX_ITERATIONS = 100; uint256 public constant PERCENTAGE_BASE = 1e18; // 100% uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e16; // 1% @@ -101,7 +103,7 @@ contract QuoterHelper { } uint256 iterations; uint256 low = BINARY_MIN_AMOUNT; - uint256 high = BINARY_MAX_AMOUNT; + uint256 high = BINARY_MAX_AMOUNT_FOR_REBALANCE; int24 lowerTick = strategy.lowerTick(); int24 upperTick = strategy.upperTick(); bool swapWETHForOETHB = getSwapDirectionForRebalance(); @@ -352,7 +354,7 @@ contract QuoterHelper { { uint256 iterations; uint256 low = BINARY_MIN_AMOUNT; - uint256 high = BINARY_MAX_AMOUNT; + uint256 high = BINARY_MAX_AMOUNT_FOR_PUSH_PRICE; bool swapWETHForOETHB = getSwapDirection(sqrtPriceTargetX96); while (low <= high && iterations < BINARY_MAX_ITERATIONS) { From ea4447240db8c6186941aa91e959be3db5b16286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 16 Sep 2024 23:37:50 +0200 Subject: [PATCH 23/28] fix: fetch governor before impersonning. --- contracts/test/_fixture-base.js | 2 ++ .../aerodrome-amo.base.fork-test.js | 29 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 093d2e6f4a..4dca6033d1 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -7,6 +7,7 @@ const { nodeRevert, nodeSnapshot } = require("./_fixture"); const { deployWithConfirmation } = require("../utils/deploy"); const addresses = require("../utils/addresses"); const erc20Abi = require("./abi/erc20.json"); +const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); const log = require("../utils/logger")("test:fixtures-arb"); @@ -111,6 +112,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { const [minter, burner, rafael, nick, clement] = signers.slice(4); // Skip first 4 addresses to avoid conflict const { governorAddr, strategistAddr } = await getNamedAccounts(); const governor = await ethers.getSigner(governorAddr); + await hhHelpers.setBalance(governorAddr, oethUnits("1")); // Fund governor with some ETH const woethGovernor = await ethers.getSigner(await woethProxy.governor()); let strategist; diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 8a9f071a88..34e154905d 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -70,7 +70,7 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () ).to.be.revertedWith("Can not rebalance empty pool"); }); - it("Should be reverted trying to rebalance and we are not in the correct tick", async () => { + it.skip("Should be reverted trying to rebalance and we are not in the correct tick", async () => { // Push price to tick 0, which is OutisdeExpectedTickRange const { value, direction } = await quoteAmountToSwapToReachPrice( await aerodromeAmoStrategy.sqrtRatioX96TickHigher() @@ -236,7 +236,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { .connect(aerodromeSigner) .approve(aeroSwapRouter.address, BigNumber.from("0")); - await aerodromeAmoStrategy.connect(governor).safeApproveAllTokens(); + const gov = await aerodromeAmoStrategy.governor(); + await aerodromeAmoStrategy + .connect(await impersonateAndFund(gov)) + .safeApproveAllTokens(); }); it("Should revert setting ptoken address", async function () { @@ -256,10 +259,11 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { describe("Configuration", function () { it("Governor can set the allowed pool weth share interval", async () => { - const { governor, aerodromeAmoStrategy } = fixture; + const { aerodromeAmoStrategy } = fixture; + const gov = await aerodromeAmoStrategy.governor(); await aerodromeAmoStrategy - .connect(governor) + .connect(await impersonateAndFund(gov)) .setAllowedPoolWethShareInterval(oethUnits("0.19"), oethUnits("0.23")); expect(await aerodromeAmoStrategy.allowedWethShareStart()).to.equal( @@ -282,17 +286,18 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); it("Can not set incorrect pool WETH share intervals", async () => { - const { governor, aerodromeAmoStrategy } = fixture; + const { aerodromeAmoStrategy } = fixture; + const gov = await aerodromeAmoStrategy.governor(); await expect( aerodromeAmoStrategy - .connect(governor) + .connect(await impersonateAndFund(gov)) .setAllowedPoolWethShareInterval(oethUnits("0.5"), oethUnits("0.4")) ).to.be.revertedWith("Invalid interval"); await expect( aerodromeAmoStrategy - .connect(governor) + .connect(await impersonateAndFund(gov)) .setAllowedPoolWethShareInterval( oethUnits("0.0001"), oethUnits("0.5") @@ -301,7 +306,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { await expect( aerodromeAmoStrategy - .connect(governor) + .connect(await impersonateAndFund(gov)) .setAllowedPoolWethShareInterval(oethUnits("0.2"), oethUnits("0.96")) ).to.be.revertedWith("Invalid interval end"); }); @@ -656,8 +661,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { oethUnits("0") ); + const gov = await aerodromeAmoStrategy.governor(); await oethbVault - .connect(governor) + .connect(await impersonateAndFund(gov)) .depositToStrategy( aerodromeAmoStrategy.address, [weth.address], @@ -793,7 +799,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { aerodromeAmoStrategy.address ); - await mintAndDepositToStrategy({ amount: oethUnits("10") }); + await mintAndDepositToStrategy({ amount: oethUnits("1000") }); // transfer WETH out making the protocol insolvent const bal = await weth.balanceOf(aerodromeAmoStrategy.address); await weth.connect(stratSigner).transfer(addresses.dead, bal); @@ -1030,8 +1036,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { await weth.connect(user).approve(oethbVault.address, amount); await oethbVault.connect(user).mint(weth.address, amount, amount); + const gov = await oethbVault.governor(); await oethbVault - .connect(governor) + .connect(await impersonateAndFund(gov)) .depositToStrategy( aerodromeAmoStrategy.address, [weth.address], From d6d386a6aa42a751a6bfb80e66c5a4924f44f62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Mon, 16 Sep 2024 23:41:22 +0200 Subject: [PATCH 24/28] fix: increase tolereance eth remaining in AMO after withdraw. --- .../test/strategies/aerodrome-amo.base.fork-test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 34e154905d..d0a6d9cce7 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -376,9 +376,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 1000 wei is really small + // Little to no weth should be left on the strategy contract - 1000000 wei is really small expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( - BigNumber.from("1000") + BigNumber.from("1000000") ); await assetLpStakedInGauge(); @@ -466,9 +466,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 1000 wei is really small + // Little to no weth should be left on the strategy contract - 1000000 wei is really small expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( - BigNumber.from("1000") + BigNumber.from("1000000") ); await assetLpStakedInGauge(); @@ -551,9 +551,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 1000 wei is really small + // Little to no weth should be left on the strategy contract - 1000000 wei is really small expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( - BigNumber.from("1000") + BigNumber.from("1000000") ); await assetLpStakedInGauge(); From aa3bb5bbd0df337d45f5d57574943e881b75e572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Tue, 17 Sep 2024 01:18:23 +0200 Subject: [PATCH 25/28] fix: fix failing test and add description. --- .../contracts/utils/AerodromeAMOQuoter.sol | 42 +++++++- contracts/test/_fixture-base.js | 3 +- .../aerodrome-amo.base.fork-test.js | 101 ++++++++++++++---- 3 files changed, 121 insertions(+), 25 deletions(-) diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index f658788ceb..fa398f044a 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -40,7 +40,7 @@ contract QuoterHelper { //////////////////////////////////////////////////////////////// uint256 public constant BINARY_MIN_AMOUNT = 1 wei; uint256 public constant BINARY_MAX_AMOUNT_FOR_REBALANCE = 3_000 ether; - uint256 public constant BINARY_MAX_AMOUNT_FOR_PUSH_PRICE = 1_000 ether; + uint256 public constant BINARY_MAX_AMOUNT_FOR_PUSH_PRICE = 50_000 ether; uint256 public constant BINARY_MAX_ITERATIONS = 100; uint256 public constant PERCENTAGE_BASE = 1e18; // 100% @@ -338,12 +338,18 @@ contract QuoterHelper { /// @notice Get the amount of tokens to swap to reach the target price. /// @dev This act like a quoter, i.e. the transaction is not performed. + /// @dev Because the amount to swap can be largely overestimated, because CLAMM alow partial orders, + /// i.e. when we ask to swap a very large amount, with a close priceLimite, it will swap only a part of it, + /// and not revert. So if overestimated amount is to high, use a custom maxAmount to avoid this issue. /// @param sqrtPriceTargetX96 The target price to reach. /// @return amount The amount of tokens to swap. /// @return iterations The number of iterations to find the amount. /// @return swapWETHForOETHB True if we need to swap WETH for OETHb, false otherwise. /// @return sqrtPriceX96After The price after the swap. - function getAmountToSwapToReachPrice(uint160 sqrtPriceTargetX96) + function getAmountToSwapToReachPrice( + uint160 sqrtPriceTargetX96, + uint256 maxAmount + ) public returns ( uint256, @@ -354,7 +360,9 @@ contract QuoterHelper { { uint256 iterations; uint256 low = BINARY_MIN_AMOUNT; - uint256 high = BINARY_MAX_AMOUNT_FOR_PUSH_PRICE; + uint256 high = maxAmount == 0 + ? BINARY_MAX_AMOUNT_FOR_PUSH_PRICE + : maxAmount; bool swapWETHForOETHB = getSwapDirection(sqrtPriceTargetX96); while (low <= high && iterations < BINARY_MAX_ITERATIONS) { @@ -557,6 +565,32 @@ contract AerodromeAMOQuoter { } } + /// @notice Use this to get the amount to swap to reach the target price after swap. + /// @dev This call will only revert, check the logs to get returned values. + /// @param sqrtPriceTargetX96 The target price to reach. + /// @param maxAmount The maximum amount to swap. (See QuoterHelper for more details) + function quoteAmountToSwapToReachPrice( + uint160 sqrtPriceTargetX96, + uint256 maxAmount + ) public { + ( + uint256 amount, + uint256 iterations, + bool swapWETHForOETHB, + uint160 sqrtPriceAfterX96 + ) = quoterHelper.getAmountToSwapToReachPrice( + sqrtPriceTargetX96, + maxAmount + ); + + emit ValueFoundBis( + amount, + iterations, + swapWETHForOETHB, + sqrtPriceAfterX96 + ); + } + /// @notice Use this to get the amount to swap to reach the target price after swap. /// @dev This call will only revert, check the logs to get returned values. /// @param sqrtPriceTargetX96 The target price to reach. @@ -566,7 +600,7 @@ contract AerodromeAMOQuoter { uint256 iterations, bool swapWETHForOETHB, uint160 sqrtPriceAfterX96 - ) = quoterHelper.getAmountToSwapToReachPrice(sqrtPriceTargetX96); + ) = quoterHelper.getAmountToSwapToReachPrice(sqrtPriceTargetX96, 0); emit ValueFoundBis( amount, diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 4dca6033d1..7d3e3724f1 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -140,7 +140,8 @@ const defaultBaseFixture = deployments.createFixture(async () => { for (const user of [rafael, nick]) { // Mint some bridged WOETH await woeth.connect(minter).mint(user.address, oethUnits("1")); - await weth.connect(user).deposit({ value: oethUnits("5000") }); + await hhHelpers.setBalance(user.address, oethUnits("10000000")); + await weth.connect(user).deposit({ value: oethUnits("100000") }); // Set allowance on the vault await weth.connect(user).approve(oethbVault.address, oethUnits("50")); diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index d0a6d9cce7..0ca76633e2 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -44,10 +44,10 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () await weth .connect(rafael) - .approve(aeroSwapRouter.address, oethUnits("1000")); + .approve(aeroSwapRouter.address, oethUnits("1000000000")); await oethb .connect(rafael) - .approve(aeroSwapRouter.address, oethUnits("1000")); + .approve(aeroSwapRouter.address, oethUnits("1000000000")); }); // Haven't found away to test for this in the strategy contract yet @@ -70,17 +70,51 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () ).to.be.revertedWith("Can not rebalance empty pool"); }); - it.skip("Should be reverted trying to rebalance and we are not in the correct tick", async () => { - // Push price to tick 0, which is OutisdeExpectedTickRange - const { value, direction } = await quoteAmountToSwapToReachPrice( - await aerodromeAmoStrategy.sqrtRatioX96TickHigher() + it("Should be reverted trying to rebalance and we are not in the correct tick, below", async () => { + // Push price to tick -2, which is OutisdeExpectedTickRange + const priceAtTickM2 = BigNumber.from("79220240490215316061937756561"); // tick -2 + const { value, direction } = await quoteAmountToSwapToReachPrice({ + price: priceAtTickM2, + maxAmount: 0, + }); + await swap({ + amount: value, + swapWeth: direction, + priceLimit: priceAtTickM2, + }); + + // Ensure the price has been pushed enough + expect(await aerodromeAmoStrategy.getPoolX96Price()).to.be.eq( + priceAtTickM2 ); - await swap({ amount: value, swapWeth: direction }); await expect( aerodromeAmoStrategy .connect(strategist) - .rebalance(oethUnits("0"), false, oethUnits("0")) + .rebalance(oethUnits("0"), direction, oethUnits("0")) + ).to.be.revertedWith("OutsideExpectedTickRange"); + }); + + it("Should be reverted trying to rebalance and we are not in the correct tick, above", async () => { + // Push price to tick 1, which is OutisdeExpectedTickRange + const priceAtTick1 = BigNumber.from("79232123823359799118286999568"); // tick 1 + const { value, direction } = await quoteAmountToSwapToReachPrice({ + price: priceAtTick1, + maxAmount: 0, + }); + await swap({ + amount: value, + swapWeth: direction, + priceLimit: priceAtTick1, + }); + + // Ensure the price has been pushed enough + expect(await aerodromeAmoStrategy.getPoolX96Price()).to.be.eq(priceAtTick1); + + await expect( + aerodromeAmoStrategy + .connect(strategist) + .rebalance(oethUnits("0"), direction, oethUnits("0")) ).to.be.revertedWith("OutsideExpectedTickRange"); }); @@ -118,8 +152,17 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () }); }; - const quoteAmountToSwapToReachPrice = async (price) => { - const txResponse = await quoter.quoteAmountToSwapToReachPrice(price); + const quoteAmountToSwapToReachPrice = async ({ price, maxAmount }) => { + let txResponse; + if (maxAmount == 0) { + txResponse = await quoter["quoteAmountToSwapToReachPrice(uint160)"]( + price + ); + } else { + txResponse = await quoter[ + "quoteAmountToSwapToReachPrice(uint160,uint256)" + ](price, maxAmount); + } const txReceipt = await txResponse.wait(); const [transferEvent] = txReceipt.events; const value = transferEvent.args.value; @@ -128,7 +171,7 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () return { value, direction, priceReached }; }; - const swap = async ({ amount, swapWeth }) => { + const swap = async ({ amount, swapWeth, priceLimit }) => { // Check if rafael as enough token to perfom swap // If not, mint some const balanceOETHb = await oethb.balanceOf(rafael.address); @@ -151,9 +194,12 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () deadline: 9999999999, amountIn: amount, amountOutMinimum: 0, // slippage check - sqrtPriceLimitX96: swapWeth - ? sqrtRatioX96TickM1000 - : sqrtRatioX96Tick1000, + sqrtPriceLimitX96: + priceLimit == 0 + ? swapWeth + ? sqrtRatioX96TickM1000 + : sqrtRatioX96Tick1000 + : priceLimit, }); }; }); @@ -193,10 +239,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { await setup(); await weth .connect(rafael) - .approve(aeroSwapRouter.address, oethUnits("1000")); + .approve(aeroSwapRouter.address, oethUnits("1000000000")); await oethb .connect(rafael) - .approve(aeroSwapRouter.address, oethUnits("1000")); + .approve(aeroSwapRouter.address, oethUnits("1000000000")); }); describe("ForkTest: Initial state (Base)", function () { @@ -715,7 +761,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should be able to rebalance the pool when price pushed to 1:1", async () => { const priceAtTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); let { value: value0, direction: direction0 } = - await quoteAmountToSwapToReachPrice(priceAtTick0); + await quoteAmountToSwapToReachPrice({ + price: priceAtTick0, + maxAmount: oethUnits("2000"), + }); await swap({ amount: value0, swapWeth: direction0, @@ -737,7 +786,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { const priceAtTickLower = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); let { value: value0, direction: direction0 } = - await quoteAmountToSwapToReachPrice(priceAtTickLower); + await quoteAmountToSwapToReachPrice({ + price: priceAtTickLower, + maxAmount: oethUnits("2000"), + }); await swap({ amount: value0, swapWeth: direction0, @@ -939,8 +991,17 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { // console.log("price of OETHb : ", displayedPoolPrice); // }; - const quoteAmountToSwapToReachPrice = async (price) => { - const txResponse = await quoter.quoteAmountToSwapToReachPrice(price); + const quoteAmountToSwapToReachPrice = async ({ price, maxAmount }) => { + let txResponse; + if (maxAmount == 0) { + txResponse = await quoter["quoteAmountToSwapToReachPrice(uint160)"]( + price + ); + } else { + txResponse = await quoter[ + "quoteAmountToSwapToReachPrice(uint160,uint256)" + ](price, maxAmount); + } const txReceipt = await txResponse.wait(); const [transferEvent] = txReceipt.events; const value = transferEvent.args.value; From 29c18b5c187a9d5ef95d63378f3dddf12ef40cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Tue, 17 Sep 2024 11:46:45 +0200 Subject: [PATCH 26/28] fix: add doc for edge case in withdraw. --- .../aerodrome-amo.base.fork-test.js | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 0ca76633e2..984e6c9644 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -422,7 +422,14 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 1000000 wei is really small + // There may remain some WETH left on the strategy contract because: + // When calculating `shareOfWetToRemove` on `withdraw` function in `AerodromeAMOStrategy.sol`, the result is rounded up. + // This leads to a maximum of 1wei error of the `shareOfWetToRemove` value. + // Then this value is multiplied by the `_getLiquidity()` value which multiplies the previous error. + // The value of `_getLiquidity()` is expressed in ethers, for example at block 19670000 it was approx 18_000_000 ethers. + // This leads to a maximum of 18_000_000 wei error in this situation (due to `mulTruncate()` function). + // At the end, the bigger the `_getLiquidity()` value the bigger the error. + // However, during test the error values remains most of the time below 1e6wei. expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( BigNumber.from("1000000") ); @@ -512,7 +519,14 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 1000000 wei is really small + // There may remain some WETH left on the strategy contract because: + // When calculating `shareOfWetToRemove` on `withdraw` function in `AerodromeAMOStrategy.sol`, the result is rounded up. + // This leads to a maximum of 1wei error of the `shareOfWetToRemove` value. + // Then this value is multiplied by the `_getLiquidity()` value which multiplies the previous error. + // The value of `_getLiquidity()` is expressed in ethers, for example at block 19670000 it was approx 18_000_000 ethers. + // This leads to a maximum of 18_000_000 wei error in this situation (due to `mulTruncate()` function). + // At the end, the bigger the `_getLiquidity()` value the bigger the error. + // However, during test the error values remains most of the time below 1e6wei. expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( BigNumber.from("1000000") ); @@ -597,7 +611,14 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { balanceBefore.add(oethUnits("1")) ); - // Little to no weth should be left on the strategy contract - 1000000 wei is really small + // There may remain some WETH left on the strategy contract because: + // When calculating `shareOfWetToRemove` on `withdraw` function in `AerodromeAMOStrategy.sol`, the result is rounded up. + // This leads to a maximum of 1wei error of the `shareOfWetToRemove` value. + // Then this value is multiplied by the `_getLiquidity()` value which multiplies the previous error. + // The value of `_getLiquidity()` is expressed in ethers, for example at block 19670000 it was approx 18_000_000 ethers. + // This leads to a maximum of 18_000_000 wei error in this situation (due to `mulTruncate()` function). + // At the end, the bigger the `_getLiquidity()` value the bigger the error. + // However, during test the error values remains most of the time below 1e6wei. expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( BigNumber.from("1000000") ); From 10e1ba8b958ce221194fd33c86cc1702f66d1167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 18 Sep 2024 08:57:52 +0200 Subject: [PATCH 27/28] fix: adjust test with latest values. --- contracts/test/strategies/aerodrome-amo.base.fork-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 984e6c9644..8849ceeaf7 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -249,11 +249,11 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should have the correct initial state", async function () { // correct pool weth share interval expect(await aerodromeAmoStrategy.allowedWethShareStart()).to.equal( - oethUnits("0.18") + oethUnits("0.10") ); expect(await aerodromeAmoStrategy.allowedWethShareEnd()).to.equal( - oethUnits("0.22") + oethUnits("0.20") ); // correct harvester set From 8dce05ed980ee4b4cb9ecb6f77abca804ef4e68d Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 30 Sep 2024 10:17:56 +0200 Subject: [PATCH 28/28] Fix issues with fork testing (#2255) * remove unneeded var * fix unit tests * fix some slither stuff * fix fork test * fix bug when swap amount too small. fix bug when no liquidity in AMO pool to reach tick * remove not needed quoter functions. Correct the tests * remove * return * prettier * add ability to chai to parse custom errors * de-nest test file * correct min values --- .../contracts/utils/AerodromeAMOQuoter.sol | 62 ++---- contracts/test/_fixture-base.js | 40 ++-- contracts/test/abi/aerodromeSugarHelper.json | 1 + contracts/test/helpers.js | 26 ++- .../aerodrome-amo.base.fork-test.js | 203 +++++++++++++----- .../test/vault/oethb-vault.base.fork-test.js | 8 +- 6 files changed, 233 insertions(+), 107 deletions(-) create mode 100644 contracts/test/abi/aerodromeSugarHelper.json diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index fa398f044a..bb8e0ee544 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -40,18 +40,18 @@ contract QuoterHelper { //////////////////////////////////////////////////////////////// uint256 public constant BINARY_MIN_AMOUNT = 1 wei; uint256 public constant BINARY_MAX_AMOUNT_FOR_REBALANCE = 3_000 ether; - uint256 public constant BINARY_MAX_AMOUNT_FOR_PUSH_PRICE = 50_000 ether; + uint256 public constant BINARY_MAX_AMOUNT_FOR_PUSH_PRICE = 5_000_000 ether; uint256 public constant BINARY_MAX_ITERATIONS = 100; uint256 public constant PERCENTAGE_BASE = 1e18; // 100% - uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e16; // 1% + uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e12; // 0.0001% //////////////////////////////////////////////////////////////// /// --- VARIABLES STORAGE //////////////////////////////////////////////////////////////// - ICLPool public clPool; - IQuoterV2 public quoterV2; - IAMOStrategy public strategy; + ICLPool public immutable clPool; + IQuoterV2 public immutable quoterV2; + IAMOStrategy public immutable strategy; address public originalGovernor; @@ -72,7 +72,7 @@ contract QuoterHelper { constructor(IAMOStrategy _strategy, IQuoterV2 _quoterV2) { strategy = _strategy; quoterV2 = _quoterV2; - clPool = strategy.clPool(); + clPool = _strategy.clPool(); } //////////////////////////////////////////////////////////////// @@ -101,7 +101,7 @@ contract QuoterHelper { strategy.setAllowedPoolWethShareInterval(shareStart, shareEnd); } - uint256 iterations; + uint256 iterations = 0; uint256 low = BINARY_MIN_AMOUNT; uint256 high = BINARY_MAX_AMOUNT_FOR_REBALANCE; int24 lowerTick = strategy.lowerTick(); @@ -328,10 +328,12 @@ contract QuoterHelper { uint256 allowedWethShareStart = strategy.allowedWethShareStart(); uint256 allowedWethShareEnd = strategy.allowedWethShareEnd(); uint160 mid = uint160(allowedWethShareStart + allowedWethShareEnd) / 2; + // slither-disable-start divide-before-multiply uint160 targetPrice = (ticker0Price * mid + ticker1Price * (1 ether - mid)) / 1 ether; + // slither-disable-end divide-before-multiply return currentPrice > targetPrice; } @@ -346,10 +348,7 @@ contract QuoterHelper { /// @return iterations The number of iterations to find the amount. /// @return swapWETHForOETHB True if we need to swap WETH for OETHb, false otherwise. /// @return sqrtPriceX96After The price after the swap. - function getAmountToSwapToReachPrice( - uint160 sqrtPriceTargetX96, - uint256 maxAmount - ) + function getAmountToSwapToReachPrice(uint160 sqrtPriceTargetX96) public returns ( uint256, @@ -358,11 +357,9 @@ contract QuoterHelper { uint160 ) { - uint256 iterations; + uint256 iterations = 0; uint256 low = BINARY_MIN_AMOUNT; - uint256 high = maxAmount == 0 - ? BINARY_MAX_AMOUNT_FOR_PUSH_PRICE - : maxAmount; + uint256 high = BINARY_MAX_AMOUNT_FOR_PUSH_PRICE; bool swapWETHForOETHB = getSwapDirection(sqrtPriceTargetX96); while (low <= high && iterations < BINARY_MAX_ITERATIONS) { @@ -384,10 +381,13 @@ contract QuoterHelper { ); if ( - low == high || isWithinAllowedVariance(sqrtPriceX96After, sqrtPriceTargetX96) ) { return (mid, iterations, swapWETHForOETHB, sqrtPriceX96After); + } else if (low == high) { + // target swap amount not found. + // try increasing BINARY_MAX_AMOUNT_FOR_PUSH_PRICE + revert("SwapAmountNotFound"); } else if ( swapWETHForOETHB ? sqrtPriceX96After > sqrtPriceTargetX96 @@ -463,7 +463,7 @@ contract AerodromeAMOQuoter { //////////////////////////////////////////////////////////////// /// --- VARIABLES STORAGE //////////////////////////////////////////////////////////////// - QuoterHelper public quoterHelper; + QuoterHelper public immutable quoterHelper; //////////////////////////////////////////////////////////////// /// --- CONSTRUCTOR @@ -565,32 +565,6 @@ contract AerodromeAMOQuoter { } } - /// @notice Use this to get the amount to swap to reach the target price after swap. - /// @dev This call will only revert, check the logs to get returned values. - /// @param sqrtPriceTargetX96 The target price to reach. - /// @param maxAmount The maximum amount to swap. (See QuoterHelper for more details) - function quoteAmountToSwapToReachPrice( - uint160 sqrtPriceTargetX96, - uint256 maxAmount - ) public { - ( - uint256 amount, - uint256 iterations, - bool swapWETHForOETHB, - uint160 sqrtPriceAfterX96 - ) = quoterHelper.getAmountToSwapToReachPrice( - sqrtPriceTargetX96, - maxAmount - ); - - emit ValueFoundBis( - amount, - iterations, - swapWETHForOETHB, - sqrtPriceAfterX96 - ); - } - /// @notice Use this to get the amount to swap to reach the target price after swap. /// @dev This call will only revert, check the logs to get returned values. /// @param sqrtPriceTargetX96 The target price to reach. @@ -600,7 +574,7 @@ contract AerodromeAMOQuoter { uint256 iterations, bool swapWETHForOETHB, uint160 sqrtPriceAfterX96 - ) = quoterHelper.getAmountToSwapToReachPrice(sqrtPriceTargetX96, 0); + ) = quoterHelper.getAmountToSwapToReachPrice(sqrtPriceTargetX96); emit ValueFoundBis( amount, diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 08b194ee89..e16b97a7cb 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -2,7 +2,7 @@ const hre = require("hardhat"); const { ethers } = hre; const mocha = require("mocha"); const { isFork, isBaseFork, oethUnits } = require("./helpers"); -const { impersonateAndFund } = require("../utils/signers"); +const { impersonateAndFund, impersonateAccount } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); const { deployWithConfirmation } = require("../utils/deploy"); const addresses = require("../utils/addresses"); @@ -14,6 +14,7 @@ const log = require("../utils/logger")("test:fixtures-arb"); const aeroSwapRouterAbi = require("./abi/aerodromeSwapRouter.json"); const aeroNonfungiblePositionManagerAbi = require("./abi/aerodromeNonfungiblePositionManager.json"); const aerodromeClGaugeAbi = require("./abi/aerodromeClGauge.json"); +const aerodromeSugarAbi = require("./abi/aerodromeSugarHelper.json"); const MINTER_ROLE = "0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6"; @@ -22,7 +23,7 @@ const BURNER_ROLE = let snapshotId; const defaultBaseFixture = deployments.createFixture(async () => { - let aerodromeAmoStrategy; + let aerodromeAmoStrategy, quoter, sugar; if (!snapshotId && !isFork) { snapshotId = await nodeSnapshot(); @@ -57,6 +58,12 @@ const defaultBaseFixture = deployments.createFixture(async () => { const wOETHbProxy = await ethers.getContract("WOETHBaseProxy"); const wOETHb = await ethers.getContractAt("WOETHBase", wOETHbProxy.address); + const dipperProxy = await ethers.getContract("OETHBaseDripperProxy"); + const dripper = await ethers.getContractAt( + "OETHDripper", + dipperProxy.address + ); + // OETHb Vault const oethbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); const oethbVault = await ethers.getContractAt( @@ -73,6 +80,18 @@ const defaultBaseFixture = deployments.createFixture(async () => { "AerodromeAMOStrategy", aerodromeAmoStrategyProxy.address ); + + sugar = await ethers.getContractAt( + aerodromeSugarAbi, + addresses.base.sugarHelper + ); + + await deployWithConfirmation("AerodromeAMOQuoter", [ + aerodromeAmoStrategy.address, + addresses.base.aeroQuoterV2Address, + ]); + + quoter = await hre.ethers.getContract("AerodromeAMOQuoter"); } // Bridged wOETH @@ -114,13 +133,13 @@ const defaultBaseFixture = deployments.createFixture(async () => { await getNamedAccounts(); const governor = await ethers.getSigner(isFork ? timelockAddr : governorAddr); await hhHelpers.setBalance(governorAddr, oethUnits("1")); // Fund governor with some ETH - const woethGovernor = await ethers.getSigner(await woethProxy.governor()); const guardian = await ethers.getSigner(governorAddr); const timelock = await ethers.getContractAt( "ITimelockController", timelockAddr ); + const oethVaultSigner = await impersonateAccount(oethbVault.address); let strategist; if (isFork) { @@ -139,11 +158,11 @@ const defaultBaseFixture = deployments.createFixture(async () => { for (const user of [rafael, nick]) { // Mint some bridged WOETH await woeth.connect(minter).mint(user.address, oethUnits("1")); - await hhHelpers.setBalance(user.address, oethUnits("10000000")); - await weth.connect(user).deposit({ value: oethUnits("100000") }); + await hhHelpers.setBalance(user.address, oethUnits("100000000")); + await weth.connect(user).deposit({ value: oethUnits("10000000") }); // Set allowance on the vault - await weth.connect(user).approve(oethbVault.address, oethUnits("50")); + await weth.connect(user).approve(oethbVault.address, oethUnits("5000")); } await woeth.connect(minter).mint(governor.address, oethUnits("1")); @@ -166,12 +185,6 @@ const defaultBaseFixture = deployments.createFixture(async () => { addresses.base.nonFungiblePositionManager ); - await deployWithConfirmation("AerodromeAMOQuoter", [ - aerodromeAmoStrategy.address, - addresses.base.aeroQuoterV2Address, - ]); - const quoter = await hre.ethers.getContract("AerodromeAMOQuoter"); - return { // Aerodrome aeroSwapRouter, @@ -181,6 +194,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { // OETHb oethb, oethbVault, + dripper, wOETHb, zapper, @@ -203,6 +217,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { strategist, minter, burner, + oethVaultSigner, rafael, nick, @@ -210,6 +225,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { // Helper quoter, + sugar, }; }); diff --git a/contracts/test/abi/aerodromeSugarHelper.json b/contracts/test/abi/aerodromeSugarHelper.json new file mode 100644 index 0000000000..998b83cd33 --- /dev/null +++ b/contracts/test/abi/aerodromeSugarHelper.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"uint256","name":"amount1","type":"uint256"},{"internalType":"address","name":"pool","type":"address"},{"internalType":"uint160","name":"sqrtRatioX96","type":"uint160"},{"internalType":"int24","name":"tickLow","type":"int24"},{"internalType":"int24","name":"tickHigh","type":"int24"}],"name":"estimateAmount0","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"address","name":"pool","type":"address"},{"internalType":"uint160","name":"sqrtRatioX96","type":"uint160"},{"internalType":"int24","name":"tickLow","type":"int24"},{"internalType":"int24","name":"tickHigh","type":"int24"}],"name":"estimateAmount1","outputs":[{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract INonfungiblePositionManager","name":"positionManager","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"fees","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint160","name":"sqrtRatioAX96","type":"uint160"},{"internalType":"uint160","name":"sqrtRatioBX96","type":"uint160"},{"internalType":"uint128","name":"liquidity","type":"uint128"},{"internalType":"bool","name":"roundUp","type":"bool"}],"name":"getAmount0Delta","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint160","name":"sqrtRatioAX96","type":"uint160"},{"internalType":"uint160","name":"sqrtRatioBX96","type":"uint160"},{"internalType":"int128","name":"liquidity","type":"int128"}],"name":"getAmount0Delta","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint160","name":"sqrtRatioAX96","type":"uint160"},{"internalType":"uint160","name":"sqrtRatioBX96","type":"uint160"},{"internalType":"int128","name":"liquidity","type":"int128"}],"name":"getAmount1Delta","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint160","name":"sqrtRatioAX96","type":"uint160"},{"internalType":"uint160","name":"sqrtRatioBX96","type":"uint160"},{"internalType":"uint128","name":"liquidity","type":"uint128"},{"internalType":"bool","name":"roundUp","type":"bool"}],"name":"getAmount1Delta","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint160","name":"sqrtRatioX96","type":"uint160"},{"internalType":"uint160","name":"sqrtRatioAX96","type":"uint160"},{"internalType":"uint160","name":"sqrtRatioBX96","type":"uint160"},{"internalType":"uint128","name":"liquidity","type":"uint128"}],"name":"getAmountsForLiquidity","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"},{"internalType":"uint160","name":"sqrtRatioX96","type":"uint160"},{"internalType":"uint160","name":"sqrtRatioAX96","type":"uint160"},{"internalType":"uint160","name":"sqrtRatioBX96","type":"uint160"}],"name":"getLiquidityForAmounts","outputs":[{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"pool","type":"address"},{"internalType":"int24","name":"startTick","type":"int24"}],"name":"getPopulatedTicks","outputs":[{"components":[{"internalType":"int24","name":"tick","type":"int24"},{"internalType":"uint160","name":"sqrtRatioX96","type":"uint160"},{"internalType":"int128","name":"liquidityNet","type":"int128"},{"internalType":"uint128","name":"liquidityGross","type":"uint128"}],"internalType":"struct ISugarHelper.PopulatedTick[]","name":"populatedTicks","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"int24","name":"tick","type":"int24"}],"name":"getSqrtRatioAtTick","outputs":[{"internalType":"uint160","name":"sqrtRatioX96","type":"uint160"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"}],"name":"getTickAtSqrtRatio","outputs":[{"internalType":"int24","name":"tick","type":"int24"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"pool","type":"address"},{"internalType":"uint128","name":"liquidity","type":"uint128"},{"internalType":"int24","name":"tickCurrent","type":"int24"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"}],"name":"poolFees","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract INonfungiblePositionManager","name":"positionManager","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint160","name":"sqrtRatioX96","type":"uint160"}],"name":"principal","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index f3803ac116..03fbe56f5d 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -1,6 +1,7 @@ const hre = require("hardhat"); const chai = require("chai"); -const { parseUnits, formatUnits } = require("ethers").utils; +const { parseUnits, formatUnits, keccak256, toUtf8Bytes } = + require("ethers").utils; const { BigNumber } = require("ethers"); const addresses = require("../utils/addresses"); @@ -151,6 +152,29 @@ chai.Assertion.addMethod("emittedEvent", async function (eventName, args) { } }); +chai.Assertion.addMethod( + "revertedWithCustomError", + async function (errorSignature) { + let txSucceeded = false; + try { + await this._obj; + txSucceeded = true; + } catch (e) { + const errorHash = keccak256(toUtf8Bytes(errorSignature)).substr(0, 10); + chai + .expect(e.message) + .to.contain( + errorHash, + `Expected error message with signature ${errorSignature} but another was thrown.` + ); + } + + if (txSucceeded) { + chai.expect.fail(`Expected ${errorSignature} error but none was thrown`); + } + } +); + function ognUnits(amount) { return parseUnits(amount, 18); } diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 8849ceeaf7..eddfef411e 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -1,5 +1,9 @@ const hre = require("hardhat"); -const { createFixtureLoader } = require("../_fixture"); +const { + createFixtureLoader, + nodeRevert, + nodeSnapshot, +} = require("../_fixture"); const addresses = require("../../utils/addresses"); const { defaultBaseFixture } = require("../_fixture-base"); @@ -14,7 +18,7 @@ const baseFixture = createFixtureLoader(defaultBaseFixture); const { setERC20TokenBalance } = require("../_fund"); const futureEpoch = 1924064072; -describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () { +describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", async function () { let fixture, oethbVault, oethb, @@ -25,6 +29,7 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () rafael, aeroSwapRouter, aeroNftManager, + sugar, quoter; beforeEach(async () => { @@ -38,6 +43,7 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () rafael = fixture.rafael; aeroSwapRouter = fixture.aeroSwapRouter; aeroNftManager = fixture.aeroNftManager; + sugar = fixture.sugar; quoter = fixture.quoter; await setupEmpty(); @@ -50,6 +56,56 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () .approve(aeroSwapRouter.address, oethUnits("1000000000")); }); + // tests need liquidity outside AMO ticks in order to test for fail states + const depositLiquidityToPool = async () => { + await weth + .connect(rafael) + .approve(aeroNftManager.address, oethUnits("1000000000")); + await oethb + .connect(rafael) + .approve(aeroNftManager.address, oethUnits("1000000000")); + + let blockTimestamp = (await hre.ethers.provider.getBlock("latest")) + .timestamp; + + await weth.connect(rafael).approve(oethbVault.address, oethUnits("200")); + await oethbVault + .connect(rafael) + .mint(weth.address, oethUnits("200"), oethUnits("199.999")); + + // we need to supply liquidity in 2 separate transactions so liquidity position is populated + // outside the active tick. + await aeroNftManager.connect(rafael).mint({ + token0: weth.address, + token1: oethb.address, + tickSpacing: BigNumber.from("1"), + tickLower: -3, + tickUpper: -1, + amount0Desired: oethUnits("100"), + amount1Desired: oethUnits("100"), + amount0Min: BigNumber.from("0"), + amount1Min: BigNumber.from("0"), + recipient: rafael.address, + deadline: blockTimestamp + 2000, + sqrtPriceX96: BigNumber.from("0"), + }); + + await aeroNftManager.connect(rafael).mint({ + token0: weth.address, + token1: oethb.address, + tickSpacing: BigNumber.from("1"), + tickLower: 0, + tickUpper: 3, + amount0Desired: oethUnits("100"), + amount1Desired: oethUnits("100"), + amount0Min: BigNumber.from("0"), + amount1Min: BigNumber.from("0"), + recipient: rafael.address, + deadline: blockTimestamp + 2000, + sqrtPriceX96: BigNumber.from("0"), + }); + }; + // Haven't found away to test for this in the strategy contract yet it.skip("Revert when there is no token id yet and no liquidity to perform the swap.", async () => { const amount = oethUnits("5"); @@ -71,12 +127,14 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () }); it("Should be reverted trying to rebalance and we are not in the correct tick, below", async () => { + await depositLiquidityToPool(); + // Push price to tick -2, which is OutisdeExpectedTickRange - const priceAtTickM2 = BigNumber.from("79220240490215316061937756561"); // tick -2 + const priceAtTickM2 = await sugar.getSqrtRatioAtTick(-2); const { value, direction } = await quoteAmountToSwapToReachPrice({ price: priceAtTickM2, - maxAmount: 0, }); + await swap({ amount: value, swapWeth: direction, @@ -92,15 +150,15 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () aerodromeAmoStrategy .connect(strategist) .rebalance(oethUnits("0"), direction, oethUnits("0")) - ).to.be.revertedWith("OutsideExpectedTickRange"); + ).to.be.revertedWithCustomError("OutsideExpectedTickRange(int24)"); }); it("Should be reverted trying to rebalance and we are not in the correct tick, above", async () => { + await depositLiquidityToPool(); // Push price to tick 1, which is OutisdeExpectedTickRange - const priceAtTick1 = BigNumber.from("79232123823359799118286999568"); // tick 1 + const priceAtTick1 = await sugar.getSqrtRatioAtTick(1); const { value, direction } = await quoteAmountToSwapToReachPrice({ price: priceAtTick1, - maxAmount: 0, }); await swap({ amount: value, @@ -115,7 +173,7 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () aerodromeAmoStrategy .connect(strategist) .rebalance(oethUnits("0"), direction, oethUnits("0")) - ).to.be.revertedWith("OutsideExpectedTickRange"); + ).to.be.revertedWithCustomError("OutsideExpectedTickRange(int24)"); }); const setupEmpty = async () => { @@ -152,17 +210,11 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () }); }; - const quoteAmountToSwapToReachPrice = async ({ price, maxAmount }) => { - let txResponse; - if (maxAmount == 0) { - txResponse = await quoter["quoteAmountToSwapToReachPrice(uint160)"]( - price - ); - } else { - txResponse = await quoter[ - "quoteAmountToSwapToReachPrice(uint160,uint256)" - ](price, maxAmount); - } + const quoteAmountToSwapToReachPrice = async ({ price }) => { + let txResponse = await quoter["quoteAmountToSwapToReachPrice(uint160)"]( + price + ); + const txReceipt = await txResponse.wait(); const [transferEvent] = txReceipt.events; const value = transferEvent.args.value; @@ -204,7 +256,7 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () }; }); -describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { +describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { let fixture, gauge, oethbVault, @@ -245,6 +297,56 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { .approve(aeroSwapRouter.address, oethUnits("1000000000")); }); + // tests need liquidity outside AMO ticks in order to test for fail states + const depositLiquidityToPool = async () => { + await weth + .connect(rafael) + .approve(aeroNftManager.address, oethUnits("1000000000")); + await oethb + .connect(rafael) + .approve(aeroNftManager.address, oethUnits("1000000000")); + + let blockTimestamp = (await hre.ethers.provider.getBlock("latest")) + .timestamp; + + await weth.connect(rafael).approve(oethbVault.address, oethUnits("200")); + await oethbVault + .connect(rafael) + .mint(weth.address, oethUnits("200"), oethUnits("199.999")); + + // we need to supply liquidity in 2 separate transactions so liquidity position is populated + // outside the active tick. + await aeroNftManager.connect(rafael).mint({ + token0: weth.address, + token1: oethb.address, + tickSpacing: BigNumber.from("1"), + tickLower: -3, + tickUpper: -1, + amount0Desired: oethUnits("100"), + amount1Desired: oethUnits("100"), + amount0Min: BigNumber.from("0"), + amount1Min: BigNumber.from("0"), + recipient: rafael.address, + deadline: blockTimestamp + 2000, + sqrtPriceX96: BigNumber.from("0"), + }); + + await aeroNftManager.connect(rafael).mint({ + token0: weth.address, + token1: oethb.address, + tickSpacing: BigNumber.from("1"), + tickLower: 0, + tickUpper: 3, + amount0Desired: oethUnits("100"), + amount1Desired: oethUnits("100"), + amount0Min: BigNumber.from("0"), + amount1Min: BigNumber.from("0"), + recipient: rafael.address, + deadline: blockTimestamp + 2000, + sqrtPriceX96: BigNumber.from("0"), + }); + }; + describe("ForkTest: Initial state (Base)", function () { it("Should have the correct initial state", async function () { // correct pool weth share interval @@ -765,7 +867,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { true, // _swapWETH oethUnits("0.009") ) - ).to.be.revertedWith("NotEnoughWethForSwap"); + ).to.be.revertedWithCustomError("NotEnoughWethForSwap(uint256,uint256)"); }); it("Should revert when pool rebalance is off target", async () => { @@ -774,18 +876,22 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { highValue: oethUnits("0.92"), }); - await expect(rebalance(value, direction, 0)).to.be.revertedWith( - "PoolRebalanceOutOfBounds" + await expect( + rebalance(value, direction, 0) + ).to.be.revertedWithCustomError( + "PoolRebalanceOutOfBounds(uint256,uint256,uint256)" ); }); it("Should be able to rebalance the pool when price pushed to 1:1", async () => { + await depositLiquidityToPool(); + const priceAtTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); let { value: value0, direction: direction0 } = await quoteAmountToSwapToReachPrice({ price: priceAtTick0, - maxAmount: oethUnits("2000"), }); + await swap({ amount: value0, swapWeth: direction0, @@ -798,6 +904,16 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { lowValue: oethUnits("0"), highValue: oethUnits("0"), }); + + // when price is pushed close to 1:1 the strategy has mostly OETHb and no WETH liquidity + // and is for that reason not able to rebalance the position. In other words the protocol + // is not liquid + await expect( + rebalance(value, direction, value.mul("99").div("100")) + ).to.be.revertedWithCustomError("NotEnoughWethForSwap(uint256,uint256)"); + + // but if we help it out with some liquidity it should rebalance + await weth.connect(rafael).transfer(aerodromeAmoStrategy.address, value); await rebalance(value, direction, value.mul("99").div("100")); await assetLpStakedInGauge(); @@ -809,7 +925,6 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { let { value: value0, direction: direction0 } = await quoteAmountToSwapToReachPrice({ price: priceAtTickLower, - maxAmount: oethUnits("2000"), }); await swap({ amount: value0, @@ -862,7 +977,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { true, oethUnits("4") ) - ).to.be.revertedWith("NotEnoughWethForSwap"); + ).to.be.revertedWithCustomError("NotEnoughWethForSwap(uint256,uint256)"); await assetLpStakedInGauge(); }); @@ -1012,17 +1127,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { // console.log("price of OETHb : ", displayedPoolPrice); // }; - const quoteAmountToSwapToReachPrice = async ({ price, maxAmount }) => { - let txResponse; - if (maxAmount == 0) { - txResponse = await quoter["quoteAmountToSwapToReachPrice(uint160)"]( - price - ); - } else { - txResponse = await quoter[ - "quoteAmountToSwapToReachPrice(uint160,uint256)" - ](price, maxAmount); - } + const quoteAmountToSwapToReachPrice = async ({ price }) => { + let txResponse = await quoter["quoteAmountToSwapToReachPrice(uint160)"]( + price + ); const txReceipt = await txResponse.wait(); const [transferEvent] = txReceipt.events; const value = transferEvent.args.value; @@ -1062,15 +1170,24 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { }; const quoteAmountToSwapBeforeRebalance = async ({ lowValue, highValue }) => { + // create a snapshot so any changes in this function are reverted before + // it returns. + const snapshotId = await nodeSnapshot(); + // Set Quoter as strategist to pass the `onlyGovernorOrStrategist` requirement // Get governor const gov = await aerodromeAmoStrategy.governor(); + // Set pending governance to quoter helper await aerodromeAmoStrategy .connect(await impersonateAndFund(gov)) .transferGovernance(await quoter.quoterHelper()); // Quoter claim governance) await quoter.claimGovernance(); + // send WETH so rebalance is possible + await weth + .connect(rafael) + .transfer(aerodromeAmoStrategy.address, oethUnits("10000")); let txResponse; if (lowValue == 0 && highValue == 0) { @@ -1086,18 +1203,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { const value = transferEvent.args.value; const direction = transferEvent.args.swapWETHForOETHB; - // Set back the original strategist - /* - await oethbVault - .connect(await impersonateAndFund(addresses.base.governor)) - .setStrategistAddr(strategist); - */ - quoter.giveBackGovernance(); - await aerodromeAmoStrategy - .connect(await impersonateAndFund(gov)) - .claimGovernance(); - // Return the value and direction + await nodeRevert(snapshotId); return { value, direction }; }; diff --git a/contracts/test/vault/oethb-vault.base.fork-test.js b/contracts/test/vault/oethb-vault.base.fork-test.js index 2ab0dc8aa2..7b9be983f3 100644 --- a/contracts/test/vault/oethb-vault.base.fork-test.js +++ b/contracts/test/vault/oethb-vault.base.fork-test.js @@ -25,6 +25,9 @@ describe("ForkTest: OETHb Vault", function () { it("Should allow anyone to mint", async () => { const { nick, weth, oethb, oethbVault } = fixture; + // issue a pre-mint so that Dripper collect gets called so next mint + // doesn't include dripper funds + await _mint(nick); await oethbVault.rebase(); const vaultBalanceBefore = await weth.balanceOf(oethbVault.address); @@ -43,8 +46,9 @@ describe("ForkTest: OETHb Vault", function () { expect(userBalanceAfter).to.approxEqual( userBalanceBefore.add(oethUnits("1")) ); - expect(vaultBalanceAfter).to.approxEqual( - vaultBalanceBefore.add(oethUnits("1")) + expect(vaultBalanceAfter).to.approxEqualTolerance( + vaultBalanceBefore.add(oethUnits("1")), + 0.1 ); });