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/018_upgrade_amo.js b/contracts/deploy/base/018_upgrade_amo.js new file mode 100644 index 0000000000..eb0726d7b9 --- /dev/null +++ b/contracts/deploy/base/018_upgrade_amo.js @@ -0,0 +1,28 @@ +const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2"); +const { + deployBaseAerodromeAMOStrategyImplementation, +} = require("../deployActions"); + +module.exports = deployOnBaseWithGuardian( + { + deployName: "018_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/_fixture-base.js b/contracts/test/_fixture-base.js index 4b73d0f896..9b85516adf 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -148,7 +148,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { ); const oethVaultSigner = await impersonateAccount(oethbVault.address); - let strategist; + let strategist, harvesterSigner; if (isFork) { // Impersonate strategist on Fork strategist = await impersonateAndFund(strategistAddr); @@ -156,6 +156,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { await impersonateAndFund(governor.address); await impersonateAndFund(timelock.address); + harvesterSigner = await impersonateAndFund(harvester.address); // configure Vault to not automatically deposit to strategy await oethbVault.connect(governor).setVaultBuffer(oethUnits("1")); @@ -208,6 +209,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { wOETHb, zapper, harvester, + harvesterSigner, dripper, // Bridged WOETH diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 289e33eb3e..cdb4298fd3 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/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 @@ -270,6 +272,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { rafael, aeroSwapRouter, aeroNftManager, + harvester, + harvesterSigner, quoter; beforeEach(async () => { @@ -286,6 +290,8 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { aeroNftManager = fixture.aeroNftManager; oethbVaultSigner = await impersonateAndFund(oethbVault.address); gauge = fixture.aeroClGauge; + harvester = fixture.harvester; + harvesterSigner = fixture.harvesterSigner; quoter = fixture.quoter; await setup(); @@ -369,10 +375,10 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { // correct harvester set expect(await aerodromeAmoStrategy.harvesterAddress()).to.equal( - await strategist.getAddress() + addresses.base.HarvesterProxy ); - await assetLpStakedInGauge(); + await verifyEndConditions(); }); it("Can safe approve all tokens", async function () { @@ -471,23 +477,21 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { describe("Harvest rewards", function () { it("Should be able to collect reward tokens", async () => { - const strategistAddr = await strategist.getAddress(); - await setERC20TokenBalance( aerodromeAmoStrategy.address, aero, "1337", hre ); - const aeroBalanceBefore = await aero.balanceOf(strategistAddr); - await aerodromeAmoStrategy.connect(strategist).collectRewardTokens(); + const aeroBalanceBefore = await aero.balanceOf(harvester.address); + await aerodromeAmoStrategy.connect(harvesterSigner).collectRewardTokens(); - const aeroBalancediff = (await aero.balanceOf(strategistAddr)).sub( + const aeroBalancediff = (await aero.balanceOf(harvester.address)).sub( aeroBalanceBefore ); expect(aeroBalancediff).to.gte(oethUnits("1337")); // Gte to take into account rewards already accumulated. - await assetLpStakedInGauge(); + await verifyEndConditions(); }); }); @@ -545,7 +549,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 +646,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 +682,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 +738,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 +774,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 +809,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 +822,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 +833,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 +858,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 +873,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 +891,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 +918,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 +929,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 +948,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 +965,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { await quoteAmountToSwapToReachPrice({ price: priceAtTickLower.sub(fivePctTickerPrice), }); + await swap({ amount: value0, swapWeth: direction0, @@ -975,7 +977,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 +991,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 +1014,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 +1086,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 +1110,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 +1164,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 +1175,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 +1192,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 +1213,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 +1256,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 +1292,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 +1346,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 8e1db6f777..608c4f09b0 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -282,6 +282,7 @@ addresses.arbitrumOne.WOETHProxy = "0xD8724322f44E5c58D7A815F542036fb17DbbF839"; // Base addresses.base = {}; +addresses.base.HarvesterProxy = "0x247872f58f2fF11f9E8f89C1C48e460CfF0c6b29"; addresses.base.BridgedWOETH = "0xD8724322f44E5c58D7A815F542036fb17DbbF839"; addresses.base.AERO = "0x940181a94A35A4569E4529A3CDfB74e38FD98631"; addresses.base.aeroRouterAddress = "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43";