From dfde2d8d14e81e50b088e1c99f26b48aded15c35 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 28 Oct 2024 13:42:02 +0100 Subject: [PATCH] Enable pool fees when rebalancing Aerodrome AMO (#2276) * make aerodrome AMO leave swap fees when rebalancing * minor adjustements and test fixes * fix bug * burn OETHb on the contract and minor renames * fix various issues with quoter and the tests * space * undo removal of default values * remove comment * add view to _checkForExpectedPoolPrice function * burn all the Super OETHb after performing the swap * put back burning OETHb after the swap function * add comment * minor test correction * OETHb on the contract after rebalance should be 0 * simplify nr of functions * Aerodrome AMO remove liquidity proper fix (#2290) * enhance the way we withdraw liquidity * better location * fix comment * update tests and simplify liquidity calculation * small fix * ignore dust in burning oeth as well * Update deployment ID --------- Co-authored-by: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> --- .../interfaces/aerodrome/IAMOStrategy.sol | 5 + .../interfaces/aerodrome/ISugarHelper.sol | 2 +- .../aerodrome/AerodromeAMOStrategy.sol | 84 ++++++++---- .../contracts/utils/AerodromeAMOQuoter.sol | 70 +++++++--- contracts/deploy/base/020_upgrade_amo.js | 28 ++++ .../base/aerodrome-amo.base.fork-test.js | 127 ++++++++++-------- contracts/utils/addresses.js | 1 + 7 files changed, 209 insertions(+), 108 deletions(-) create mode 100644 contracts/deploy/base/020_upgrade_amo.js diff --git a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol index 49260cf1e0..d642c834f5 100644 --- a/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol +++ b/contracts/contracts/interfaces/aerodrome/IAMOStrategy.sol @@ -59,4 +59,9 @@ interface IAMOStrategy { function claimGovernance() external; function transferGovernance(address _governor) external; + + function getPositionPrincipal() + external + view + returns (uint256 _amountWeth, uint256 _amountOethb); } diff --git a/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol b/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol index f0191ee297..eb2c00e548 100644 --- a/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol +++ b/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol @@ -29,7 +29,7 @@ interface ISugarHelper { uint160 sqrtRatioX96, uint160 sqrtRatioAX96, uint160 sqrtRatioBX96 - ) external pure returns (uint256 liquidity); + ) external pure returns (uint128 liquidity); /// @notice Computes the amount of token0 for a given amount of token1 and price range /// @param amount1 Amount of token1 to estimate liquidity diff --git a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index c2fa9d80f3..eab735145b 100644 --- a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -351,7 +351,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { */ function depositAll() external override onlyVault nonReentrant { uint256 _wethBalance = IERC20(WETH).balanceOf(address(this)); - if (_wethBalance > 0) { + if (_wethBalance > 1e12) { _deposit(WETH, _wethBalance); } } @@ -426,11 +426,13 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { /** * When rebalance is called for the first time there is no strategy - * liquidity in the pool yet. The full liquidity removal is thus skipped. + * liquidity in the pool yet. The liquidity removal is thus skipped. + * Also execute this function when WETH is required for the swap. */ - if (tokenId != 0) { - _removeLiquidity(1e18); + if (tokenId != 0 && _swapWeth && _amountToSwap > 0) { + _ensureWETHBalance(_amountToSwap); } + // in some cases we will just want to add liquidity and not issue a swap to move the // active trading position within the pool if (_amountToSwap > 0) { @@ -531,6 +533,8 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { _amountOethbCollected, underlyingAssets ); + + _burnOethbOnTheContract(); } /** @@ -545,6 +549,8 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { uint256 _balance = _tokenToSwap.balanceOf(address(this)); if (_balance < _amountToSwap) { + // This should never trigger since _ensureWETHBalance will already + // throw an error if there is not enough WETH if (_swapWeth) { revert NotEnoughWethForSwap(_balance, _amountToSwap); } @@ -576,6 +582,14 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { : sqrtRatioX96TickHigher }) ); + + /** + * In the interest of each function in _rebalance to leave the contract state as + * clean as possible the OETHb tokens here are burned. This decreases the + * dependence where `_swapToDesiredPosition` function relies on later functions + * (`addLiquidity`) to burn the OETHb. Reducing the risk of error introduction. + */ + _burnOethbOnTheContract(); } /** @@ -590,7 +604,10 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { function _addLiquidity() internal gaugeUnstakeAndRestake { uint256 _wethBalance = IERC20(WETH).balanceOf(address(this)); uint256 _oethbBalance = IERC20(OETHb).balanceOf(address(this)); - require(_wethBalance > 0, "Must add some WETH"); + // don't deposit small liquidity amounts + if (_wethBalance <= 1e12) { + return; + } uint160 _currentPrice = getPoolX96Price(); /** @@ -709,6 +726,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { */ function _checkForExpectedPoolPrice(bool throwException) internal + view returns (bool _isExpectedRange, uint256 _wethSharePct) { require( @@ -759,7 +777,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { */ function _burnOethbOnTheContract() internal { uint256 _oethbBalance = IERC20(OETHb).balanceOf(address(this)); - if (_oethbBalance > 0) { + if (_oethbBalance > 1e12) { IVault(vaultAddress).burnForStrategy(_oethbBalance); } } @@ -802,6 +820,36 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { emit UnderlyingAssetsUpdated(underlyingAssets); } + /** + * @dev This function removes the appropriate amount of liquidity to assure that the required + * amount of WETH is available on the contract + * + * @param _amount WETH balance required on the contract + */ + function _ensureWETHBalance(uint256 _amount) internal { + uint256 _wethBalance = IERC20(WETH).balanceOf(address(this)); + if (_wethBalance >= _amount) { + return; + } + + require(tokenId != 0, "No liquidity available"); + uint256 _additionalWethRequired = _amount - _wethBalance; + (uint256 _wethInThePool, ) = getPositionPrincipal(); + + if (_wethInThePool < _additionalWethRequired) { + revert NotEnoughWethLiquidity( + _wethInThePool, + _additionalWethRequired + ); + } + + uint256 shareOfWethToRemove = Math.min( + _additionalWethRequired.divPrecisely(_wethInThePool) + 1, + 1e18 + ); + _removeLiquidity(shareOfWethToRemove); + } + /** * @notice Withdraw an `amount` of assets from the platform and * send to the `_recipient`. @@ -817,28 +865,8 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { require(_asset == WETH, "Unsupported asset"); require(_recipient == vaultAddress, "Only withdraw to vault allowed"); - uint256 _wethBalance = IERC20(WETH).balanceOf(address(this)); - if (_wethBalance < _amount) { - require(tokenId != 0, "No liquidity available"); - uint256 _additionalWethRequired = _amount - _wethBalance; - (uint256 _wethInThePool, ) = getPositionPrincipal(); - - if (_wethInThePool < _additionalWethRequired) { - revert NotEnoughWethLiquidity( - _wethInThePool, - _additionalWethRequired - ); - } - - uint256 shareOfWethToRemove = Math.min( - _additionalWethRequired.divPrecisely(_wethInThePool) + 1, - 1e18 - ); - _removeLiquidity(shareOfWethToRemove); - } + _ensureWETHBalance(_amount); - // burn remaining OETHb - _burnOethbOnTheContract(); _withdraw(_recipient, _amount); } @@ -854,8 +882,6 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { if (_balance > 0) { _withdraw(vaultAddress, _balance); } - // burn remaining OETHb - _burnOethbOnTheContract(); } function _withdraw(address _recipient, uint256 _amount) internal { diff --git a/contracts/contracts/utils/AerodromeAMOQuoter.sol b/contracts/contracts/utils/AerodromeAMOQuoter.sol index bb8e0ee544..6f41fe0ce5 100644 --- a/contracts/contracts/utils/AerodromeAMOQuoter.sol +++ b/contracts/contracts/utils/AerodromeAMOQuoter.sol @@ -39,12 +39,10 @@ contract QuoterHelper { /// --- CONSTANT & IMMUTABLE //////////////////////////////////////////////////////////////// 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 = 5_000_000 ether; - uint256 public constant BINARY_MAX_ITERATIONS = 100; + uint256 public constant BINARY_MAX_ITERATIONS = 40; uint256 public constant PERCENTAGE_BASE = 1e18; // 100% - uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e12; // 0.0001% + uint256 public constant ALLOWED_VARIANCE_PERCENTAGE = 1e15; // 0.1% //////////////////////////////////////////////////////////////// /// --- VARIABLES STORAGE @@ -101,9 +99,11 @@ contract QuoterHelper { strategy.setAllowedPoolWethShareInterval(shareStart, shareEnd); } + uint256 iterations = 0; uint256 low = BINARY_MIN_AMOUNT; - uint256 high = BINARY_MAX_AMOUNT_FOR_REBALANCE; + uint256 high; + (high, ) = strategy.getPositionPrincipal(); int24 lowerTick = strategy.lowerTick(); int24 upperTick = strategy.upperTick(); bool swapWETHForOETHB = getSwapDirectionForRebalance(); @@ -338,6 +338,14 @@ contract QuoterHelper { return currentPrice > targetPrice; } + // returns total amount in the position principal of the Aerodrome AMO strategy. Needed as a + // separate function because of the limitation in local variable count in getAmountToSwapToReachPrice + function getTotalStrategyPosition() internal returns (uint256) { + (uint256 wethAmount, uint256 oethBalance) = strategy + .getPositionPrincipal(); + return wethAmount + oethBalance; + } + /// @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, @@ -359,34 +367,53 @@ contract QuoterHelper { { uint256 iterations = 0; uint256 low = BINARY_MIN_AMOUNT; - uint256 high = BINARY_MAX_AMOUNT_FOR_PUSH_PRICE; + // high search start is twice the position principle of Aerodrome AMO strategy. + // should be more than enough + uint256 high = getTotalStrategyPosition() * 2; 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 - }) - ); + (uint256 amountOut, 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 ( isWithinAllowedVariance(sqrtPriceX96After, sqrtPriceTargetX96) ) { - return (mid, iterations, swapWETHForOETHB, sqrtPriceX96After); + /** Very important to return `amountOut` instead of `mid` as the first return parameter. + * The issues was that when quoting we impose a swap price limit (sqrtPriceLimitX96: sqrtPriceTargetX96) + * and in that case the `amountIn` acts like a maximum amount to swap. And we don't know how much + * of that amount was actually consumed. For that reason we "estimate" it by returning the + * amountOut since that is only going to be a couple of basis point away from amountIn in the + * worst cases. + * + * Note: we could be returning mid instead of amountOut in cases when those values are only basis + * points apart (assuming that complete balance of amountIn has been consumed) but that might increase + * complexity too much in an already complex contract. + */ + return ( + amountOut, + iterations, + swapWETHForOETHB, + sqrtPriceX96After + ); } else if (low == high) { // target swap amount not found. - // try increasing BINARY_MAX_AMOUNT_FOR_PUSH_PRICE + // might be that "high" amount is too low on start revert("SwapAmountNotFound"); } else if ( swapWETHForOETHB @@ -539,7 +566,6 @@ contract AerodromeAMOQuoter { 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; diff --git a/contracts/deploy/base/020_upgrade_amo.js b/contracts/deploy/base/020_upgrade_amo.js new file mode 100644 index 0000000000..36111d0b55 --- /dev/null +++ b/contracts/deploy/base/020_upgrade_amo.js @@ -0,0 +1,28 @@ +const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2"); +const { + deployBaseAerodromeAMOStrategyImplementation, +} = require("../deployActions"); + +module.exports = deployOnBaseWithGuardian( + { + deployName: "020_upgrade_amo", + }, + async ({ ethers }) => { + const cAMOStrategyProxy = await ethers.getContract( + "AerodromeAMOStrategyProxy" + ); + const cAMOStrategyImpl = + await deployBaseAerodromeAMOStrategyImplementation(); + + return { + actions: [ + { + // 1. Upgrade AMO + contract: cAMOStrategyProxy, + signature: "upgradeTo(address)", + args: [cAMOStrategyImpl.address], + }, + ], + }; + } +); diff --git a/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js index 92ae5399ea..edd546b278 100644 --- a/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js @@ -142,9 +142,9 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", async funct }); // Ensure the price has been pushed enough - expect(await aerodromeAmoStrategy.getPoolX96Price()).to.be.eq( - priceAtTickM2 - ); + expect( + await aerodromeAmoStrategy.getPoolX96Price() + ).to.be.approxEqualTolerance(priceAtTickM2); await expect( aerodromeAmoStrategy @@ -167,7 +167,9 @@ describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", async funct }); // Ensure the price has been pushed enough - expect(await aerodromeAmoStrategy.getPoolX96Price()).to.be.eq(priceAtTick1); + expect( + await aerodromeAmoStrategy.getPoolX96Price() + ).to.be.approxEqualTolerance(priceAtTick1); await expect( aerodromeAmoStrategy @@ -287,8 +289,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { aeroNftManager = fixture.aeroNftManager; oethbVaultSigner = await impersonateAndFund(oethbVault.address); gauge = fixture.aeroClGauge; - quoter = fixture.quoter; harvester = fixture.harvester; + quoter = fixture.quoter; await setup(); await weth @@ -374,7 +376,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { harvester.address ); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Can safe approve all tokens", async function () { @@ -487,7 +489,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { ); expect(aeroBalancediff).to.gte(oethUnits("1337")); // Gte to take into account rewards already accumulated. - await assetLpStakedInGauge(); + await verifyEndConditions(); }); }); @@ -545,7 +547,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { BigNumber.from("1000000") ); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should allow withdrawAll when the pool is 80:20 balanced", async () => { @@ -642,7 +644,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { BigNumber.from("1000000") ); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should withdrawAll when there's little WETH in the pool", async () => { @@ -678,7 +680,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { oethUnits("0") ); - await assetLpNOTStakedInGauge(); + await verifyEndConditions(false); }); it("Should withdraw when there's little OETHb in the pool", async () => { @@ -734,7 +736,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { BigNumber.from("1000000") ); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should withdrawAll when there's little OETHb in the pool", async () => { @@ -770,13 +772,15 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { oethUnits("0") ); - await assetLpNOTStakedInGauge(); + await verifyEndConditions(false); }); }); describe("Deposit and rebalance", function () { it("Should be able to deposit to the strategy", async () => { await mintAndDepositToStrategy(); + + await verifyEndConditions(); }); it("Should revert when not depositing WETH or amount is 0", async () => { @@ -803,7 +807,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { const tx = await rebalance(value, direction, value.mul("99").div("100")); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should be able to deposit to the pool & rebalance multiple times", async () => { @@ -816,7 +820,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { const tx = await rebalance(value, direction, value.mul("99").div("100")); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); - await assetLpStakedInGauge(); + await verifyEndConditions(); await mintAndDepositToStrategy({ amount: oethUnits("5") }); // prettier-ignore @@ -827,7 +831,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { ); await expect(tx1).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should check that add liquidity in difference cases leaves no to little weth on the contract", async () => { @@ -852,7 +856,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { oethUnits("0") ); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should revert when there is not enough WETH to perform a swap", async () => { @@ -867,7 +871,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { true, // _swapWETH oethUnits("0.009") ) - ).to.be.revertedWithCustomError("NotEnoughWethForSwap(uint256,uint256)"); + ).to.be.revertedWithCustomError( + "NotEnoughWethLiquidity(uint256,uint256)" + ); }); it("Should revert when pool rebalance is off target", async () => { @@ -883,16 +889,21 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { ); }); - it("Should be able to rebalance the pool when price pushed to 1:1", async () => { + it("Should be able to rebalance the pool when price pushed very close to 1:1", async () => { await depositLiquidityToPool(); // supply some WETH for the rebalance await mintAndDepositToStrategy({ amount: oethUnits("1") }); - const priceAtTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + const priceAtTickLower = + await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + const priceAtTickHigher = + await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + const pctTickerPrice = priceAtTickHigher.sub(priceAtTickLower).div(100); + let { value: value0, direction: direction0 } = await quoteAmountToSwapToReachPrice({ - price: priceAtTick0, + price: priceAtTickHigher.sub(pctTickerPrice), }); await swap({ @@ -905,21 +916,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { 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. Add a surplus of 1 WETH so that - // some liquidity gets deployed on rebalance. - await weth - .connect(rafael) - .transfer(aerodromeAmoStrategy.address, value.add(oethUnits("1"))); await rebalance(value, direction, value.mul("99").div("100")); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should be able to rebalance the pool when price pushed to over the 1 OETHb costing 1.0001 WETH", async () => { @@ -928,13 +927,13 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { const priceAtTickHigher = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); // 5% of the price diff within a single ticker - const fivePctTickerPrice = priceAtTickHigher + const twentyPctTickerPrice = priceAtTickHigher .sub(priceAtTickLower) .div(20); let { value: value0, direction: direction0 } = await quoteAmountToSwapToReachPrice({ - price: priceAtTickLower.add(fivePctTickerPrice), + price: priceAtTickLower.add(twentyPctTickerPrice), }); await swap({ amount: value0, @@ -947,7 +946,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { }); await rebalance(value, direction, value.mul("99").div("100")); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should be able to rebalance the pool when price pushed to close to the 1 OETHb costing 1.0001 WETH", async () => { @@ -964,6 +963,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { await quoteAmountToSwapToReachPrice({ price: priceAtTickLower.sub(fivePctTickerPrice), }); + await swap({ amount: value0, swapWeth: direction0, @@ -975,7 +975,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { }); await rebalance(value, direction, value.mul("99").div("100")); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should have the correct balance within some tolerance", async () => { @@ -989,7 +989,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { await aerodromeAmoStrategy.checkBalance(weth.address) ).to.approxEqualTolerance(balance.add(oethUnits("6").mul("4")), 1.5); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should revert on non WETH balance", async () => { @@ -1012,9 +1012,11 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { true, oethUnits("4") ) - ).to.be.revertedWithCustomError("NotEnoughWethForSwap(uint256,uint256)"); + ).to.be.revertedWithCustomError( + "NotEnoughWethLiquidity(uint256,uint256)" + ); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should not be able to rebalance when protocol is insolvent", async () => { @@ -1082,7 +1084,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { await weth.balanceOf(oethbVault.address) ).to.approxEqualTolerance(amountBelowThreshold); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should deposit amount above the vault buffer threshold to the strategy on mint", async () => { @@ -1106,7 +1108,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { await weth.balanceOf(oethbVault.address) ).to.approxEqualTolerance(minAmountReserved); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Should leave WETH on the contract when pool price outside allowed limits", async () => { @@ -1160,7 +1162,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { oethUnits("0.000009") ); await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); - await assetLpStakedInGauge(); + await verifyEndConditions(); // deposit into pool again await mintAndDepositToStrategy({ amount: oethUnits("5") }); @@ -1171,13 +1173,13 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { oethUnits("0") ); await expect(tx1).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); - await assetLpStakedInGauge(); + await verifyEndConditions(); // Withdraw from the pool await aerodromeAmoStrategy .connect(impersonatedVaultSigner) .withdraw(oethbVault.address, weth.address, oethUnits("1")); - await assetLpStakedInGauge(); + await verifyEndConditions(); // deposit into pool again await mintAndDepositToStrategy({ amount: oethUnits("5") }); @@ -1188,13 +1190,13 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { oethUnits("0") ); await expect(tx2).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); - await assetLpStakedInGauge(); + await verifyEndConditions(); // Withdraw from the pool await aerodromeAmoStrategy .connect(impersonatedVaultSigner) .withdraw(oethbVault.address, weth.address, oethUnits("1")); - await assetLpStakedInGauge(); + await verifyEndConditions(); // Withdraw from the pool await aerodromeAmoStrategy.connect(impersonatedVaultSigner).withdrawAll(); @@ -1209,10 +1211,29 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { oethUnits("0") ); await expect(tx3).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); }); + /** When tests finish: + * - nft LP token should remain staked + * - there should be no substantial amount of WETH / OETHb left on the strategy contract + */ + const verifyEndConditions = async (lpStaked = true) => { + if (lpStaked) { + await assetLpStakedInGauge(); + } else { + await assetLpNOTStakedInGauge(); + } + + await expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.lte( + oethUnits("0.00001") + ); + await expect(await oethb.balanceOf(aerodromeAmoStrategy.address)).to.equal( + oethUnits("0") + ); + }; + const assetLpStakedInGauge = async () => { const tokenId = await aerodromeAmoStrategy.tokenId(); await expect(await aeroNftManager.ownerOf(tokenId)).to.equal(gauge.address); @@ -1233,7 +1254,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { highValue: oethUnits("0"), }); - // move the price to pre-configured 20% value + // move the price close to pre-configured 20% value await rebalance( value, direction, // _swapWETH @@ -1269,9 +1290,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { // }; const quoteAmountToSwapToReachPrice = async ({ price }) => { - let txResponse = await quoter["quoteAmountToSwapToReachPrice(uint160)"]( - price - ); + let txResponse = await quoter.quoteAmountToSwapToReachPrice(price); const txReceipt = await txResponse.wait(); const [transferEvent] = txReceipt.events; const value = transferEvent.args.value; @@ -1325,10 +1344,6 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { .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) { diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 1a0290dce0..1b5accb57f 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -284,6 +284,7 @@ addresses.arbitrumOne.WOETHProxy = "0xD8724322f44E5c58D7A815F542036fb17DbbF839"; // Base addresses.base = {}; +addresses.base.HarvesterProxy = "0x247872f58f2fF11f9E8f89C1C48e460CfF0c6b29"; addresses.base.BridgedWOETH = "0xD8724322f44E5c58D7A815F542036fb17DbbF839"; addresses.base.AERO = "0x940181a94A35A4569E4529A3CDfB74e38FD98631"; addresses.base.aeroRouterAddress = "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43";