From 3c85d1a3ff63db90fff6b94111fbac35fd538ba4 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 24 Sep 2024 17:23:13 +0200 Subject: [PATCH] fix bug when swap amount too small. fix bug when no liquidity in AMO pool to reach tick --- .../contracts/utils/AerodromeAMOQuoter.sol | 9 ++- contracts/test/_fixture-base.js | 20 +++++-- contracts/test/abi/aerodromeSugarHelper.json | 1 + .../aerodrome-amo.base.fork-test.js | 57 ++++++++++++++++++- 4 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 contracts/test/abi/aerodromeSugarHelper.json diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index 7e22d17c6c..b2b1089f3e 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -40,11 +40,11 @@ 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 @@ -386,10 +386,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 diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index a756f4d110..e16b97a7cb 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -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, quoter; + let aerodromeAmoStrategy, quoter, sugar; if (!snapshotId && !isFork) { snapshotId = await nodeSnapshot(); @@ -58,7 +59,10 @@ const defaultBaseFixture = deployments.createFixture(async () => { const wOETHb = await ethers.getContractAt("WOETHBase", wOETHbProxy.address); const dipperProxy = await ethers.getContract("OETHBaseDripperProxy"); - const dripper = await ethers.getContractAt("OETHDripper", dipperProxy.address); + const dripper = await ethers.getContractAt( + "OETHDripper", + dipperProxy.address + ); // OETHb Vault const oethbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); @@ -77,6 +81,11 @@ const defaultBaseFixture = deployments.createFixture(async () => { aerodromeAmoStrategyProxy.address ); + sugar = await ethers.getContractAt( + aerodromeSugarAbi, + addresses.base.sugarHelper + ); + await deployWithConfirmation("AerodromeAMOQuoter", [ aerodromeAmoStrategy.address, addresses.base.aeroQuoterV2Address, @@ -149,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")); @@ -216,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/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 8849ceeaf7..8a442dd23e 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -25,6 +25,7 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", function () rafael, aeroSwapRouter, aeroNftManager, + sugar, quoter; beforeEach(async () => { @@ -38,6 +39,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 +52,54 @@ 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 oethbVault + .connect(rafael) + .mint(weth.address, oethUnits("200"), oethUnits("19.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,8 +121,10 @@ 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, @@ -96,8 +148,9 @@ 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, 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,