diff --git a/brownie/runlogs/2024_10_strategist.py b/brownie/runlogs/2024_10_strategist.py index e504b1f11a..37cd268fa6 100644 --- a/brownie/runlogs/2024_10_strategist.py +++ b/brownie/runlogs/2024_10_strategist.py @@ -209,3 +209,58 @@ def main(): print("-----") print("Profit", "{:.6f}".format(profit / 10**18), profit) print("Vault Change", "{:.6f}".format(vault_change / 10**18), vault_change) + +# ----------------------------------------------------- +# Oct 9th 2024 - OETHb allocation & rebalance +# ----------------------------------------------------- + +from world_base import * + +def main(): + with TemporaryForkForOETHbReallocations() as txs: + # Before + txs.append(vault_core.rebase({ 'from': OETHB_STRATEGIST })) + txs.append(vault_value_checker.takeSnapshot({ 'from': OETHB_STRATEGIST })) + + # Deposit all WETH + wethDepositAmount = weth.balanceOf(OETHB_VAULT_PROXY_ADDRESS) + txs.append( + vault_admin.depositToStrategy( + OETHB_AERODROME_AMO_STRATEGY, + [weth], + [wethDepositAmount], + {'from': OETHB_STRATEGIST} + ) + ) + + amo_snapsnot() + swapWeth = True + swapAmount = 0 + minAmount = swapAmount * 0.98 + print("--------------------") + print("WETH Deposit ", c18(wethDepositAmount)) + print("-----") + print("Swap amount ", c18(swapAmount)) + print("Min amount ", c18(minAmount)) + print("-----") + + txs.append( + amo_strat.rebalance( + swapAmount, + swapWeth, + minAmount, + {'from': OETHB_STRATEGIST} + ) + ) + + # After + vault_change = vault_core.totalValue() - vault_value_checker.snapshots(OETHB_STRATEGIST)[0] + supply_change = oethb.totalSupply() - vault_value_checker.snapshots(OETHB_STRATEGIST)[1] + profit = vault_change - supply_change + + txs.append(vault_value_checker.checkDelta(profit, (1 * 10**18), vault_change, (1 * 10**18), {'from': OETHB_STRATEGIST})) + + amo_snapsnot() + print("--------------------") + print("Profit ", c18(profit), profit) + print("Vault Change ", c18(vault_change), vault_change) diff --git a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index a86192eb6e..c2fa9d80f3 100644 --- a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -366,6 +366,14 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { require(_asset == WETH, "Unsupported asset"); require(_amount > 0, "Must deposit something"); emit Deposit(_asset, address(0), _amount); + + // if the pool price is not within the expected interval leave the WETH on the contract + // as to not break the mints + (bool _isExpectedRange, ) = _checkForExpectedPoolPrice(false); + if (_isExpectedRange) { + // deposit funds into the underlying pool + _rebalance(0, false, 0); + } } /** @@ -400,6 +408,14 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { bool _swapWeth, uint256 _minTokenReceived ) external nonReentrant onlyGovernorOrStrategist { + _rebalance(_amountToSwap, _swapWeth, _minTokenReceived); + } + + function _rebalance( + uint256 _amountToSwap, + bool _swapWeth, + uint256 _minTokenReceived + ) internal { /** * Would be nice to check if there is any total liquidity in the pool before performing this swap * but there is no easy way to do that in UniswapV3: @@ -422,15 +438,18 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { } // calling check liquidity early so we don't get unexpected errors when adding liquidity // in the later stages of this function - _checkForExpectedPoolPrice(); + _checkForExpectedPoolPrice(true); _addLiquidity(); + // this call shouldn't be necessary, since adding liquidity shouldn't affect the active // trading price. It is a defensive programming measure. - _checkForExpectedPoolPrice(); + (, uint256 _wethSharePct) = _checkForExpectedPoolPrice(true); // revert if protocol insolvent _solvencyAssert(); + + emit PoolRebalanced(_wethSharePct); } /** @@ -534,6 +553,11 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { IVault(vaultAddress).mintForStrategy(mintForSwap); } + // approve the specific amount of WETH required + if (_swapWeth) { + IERC20(WETH).approve(address(swapRouter), _amountToSwap); + } + // Swap it swapRouter.exactInputSingle( // sqrtPriceLimitX96 is just a rough sanity check that we are within 0 -> 1 tick @@ -605,6 +629,9 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { ); } + // approve the specific amount of WETH required + IERC20(WETH).approve(address(positionManager), _wethBalance); + uint256 _wethAmountSupplied; uint256 _oethbAmountSupplied; if (tokenId == 0) { @@ -672,9 +699,18 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { * @dev Check that the Aerodrome pool price is within the expected * parameters. * This function works whether the strategy contract has liquidity - * position in the pool or not. + * position in the pool or not. The function returns _wethSharePct + * as a gas optimization measure. + * @param throwException when set to true the function throws an exception + * when pool's price is not within expected range. + * @return _isExpectedRange Bool expressing price is within expected range + * @return _wethSharePct Share of WETH owned by this strategy contract in the + * configured ticker. */ - function _checkForExpectedPoolPrice() internal { + function _checkForExpectedPoolPrice(bool throwException) + internal + returns (bool _isExpectedRange, uint256 _wethSharePct) + { require( allowedWethShareStart != 0 && allowedWethShareEnd != 0, "Weth share interval not set" @@ -692,40 +728,30 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { _currentPrice <= sqrtRatioX96TickLower || _currentPrice >= sqrtRatioX96TickHigher ) { - revert OutsideExpectedTickRange(getCurrentTradingTick()); + if (throwException) { + revert OutsideExpectedTickRange(getCurrentTradingTick()); + } + return (false, 0); } - /** - * If estimateAmount1 call fails it could be due to _currentPrice being really - * close to a tick and amount1 too big to compute. - * - * If token addresses were reversed estimateAmount0 would be required here - */ - uint256 _normalizedWethAmount = 1 ether; - uint256 _correspondingOethAmount = helper.estimateAmount1( - _normalizedWethAmount, - address(0), // no need to pass pool address when current price is specified - _currentPrice, - lowerTick, - upperTick - ); - - // 18 decimal number expressed weth tick share - uint256 _wethSharePct = _normalizedWethAmount.divPrecisely( - _normalizedWethAmount + _correspondingOethAmount - ); + // 18 decimal number expressed WETH tick share + _wethSharePct = _getWethShare(_currentPrice); if ( _wethSharePct < allowedWethShareStart || _wethSharePct > allowedWethShareEnd ) { - revert PoolRebalanceOutOfBounds( - _wethSharePct, - allowedWethShareStart, - allowedWethShareEnd - ); + if (throwException) { + revert PoolRebalanceOutOfBounds( + _wethSharePct, + allowedWethShareStart, + allowedWethShareEnd + ); + } + return (false, _wethSharePct); } - emit PoolRebalanced(_wethSharePct); + + return (true, _wethSharePct); } /** @@ -868,11 +894,17 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { nonReentrant { // to add liquidity to the clPool - IERC20(WETH).safeApprove(address(positionManager), type(uint256).max); - IERC20(OETHb).safeApprove(address(positionManager), type(uint256).max); + IERC20(OETHb).approve(address(positionManager), type(uint256).max); // to be able to rebalance using the swapRouter - IERC20(WETH).safeApprove(address(swapRouter), type(uint256).max); - IERC20(OETHb).safeApprove(address(swapRouter), type(uint256).max); + IERC20(OETHb).approve(address(swapRouter), type(uint256).max); + + /* the behaviour of this strategy has slightly changed and WETH could be + * present on the contract between the transactions. For that reason we are + * un-approving WETH to the swapRouter & positionManager and only approving + * the required amount before a transaction + */ + IERC20(WETH).approve(address(swapRouter), 0); + IERC20(WETH).approve(address(positionManager), 0); } /*************************************** @@ -940,6 +972,16 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { (, _currentTick, , , , ) = clPool.slot0(); } + /** + * @notice Returns the percentage of WETH liquidity in the configured ticker + * owned by this strategy contract. + * @return uint256 1e18 denominated percentage expressing the share + */ + function getWETHShare() external view returns (uint256) { + uint160 _currentPrice = getPoolX96Price(); + return _getWethShare(_currentPrice); + } + /** * @notice Returns the amount of liquidity in the contract's LP position * @return _liquidity Amount of liquidity in the position @@ -952,6 +994,33 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { (, , , , , , , _liquidity, , , , ) = positionManager.positions(tokenId); } + function _getWethShare(uint160 _currentPrice) + internal + view + returns (uint256) + { + /** + * If estimateAmount1 call fails it could be due to _currentPrice being really + * close to a tick and amount1 too big to compute. + * + * If token addresses were reversed estimateAmount0 would be required here + */ + uint256 _normalizedWethAmount = 1 ether; + uint256 _correspondingOethAmount = helper.estimateAmount1( + _normalizedWethAmount, + address(0), // no need to pass pool address when current price is specified + _currentPrice, + lowerTick, + upperTick + ); + + // 18 decimal number expressed weth tick share + return + _normalizedWethAmount.divPrecisely( + _normalizedWethAmount + _correspondingOethAmount + ); + } + /*************************************** Hidden functions ****************************************/ diff --git a/contracts/deploy/base/006_base_amo_strategy.js b/contracts/deploy/base/006_base_amo_strategy.js index c9f724b486..e2e53b2a48 100644 --- a/contracts/deploy/base/006_base_amo_strategy.js +++ b/contracts/deploy/base/006_base_amo_strategy.js @@ -5,6 +5,9 @@ const { } = require("../../utils/deploy"); const addresses = require("../../utils/addresses"); const { oethUnits } = require("../../test/helpers"); +const { + deployBaseAerodromeAMOStrategyImplementation, +} = require("../deployActions"); //const aeroVoterAbi = require("../../test/abi/aerodromeVoter.json"); //const slipstreamPoolAbi = require("../../test/abi/aerodromeSlipstreamPool.json") @@ -47,35 +50,18 @@ module.exports = deployOnBaseWithGuardian( async ({ ethers }) => { const { deployerAddr, governorAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); const cOETHbVault = await ethers.getContractAt( "IVault", cOETHbVaultProxy.address ); + const cAMOStrategyImpl = + await deployBaseAerodromeAMOStrategyImplementation(); await deployWithConfirmation("AerodromeAMOStrategyProxy"); - await deployWithConfirmation("AerodromeAMOStrategy", [ - /* The pool address is not yet known. Might be created before we deploy the - * strategy or after. - */ - [addresses.zero, cOETHbVaultProxy.address], // platformAddress, VaultAddress - addresses.base.WETH, // weth address - cOETHbProxy.address, // OETHb address - addresses.base.swapRouter, // swapRouter - addresses.base.nonFungiblePositionManager, // nonfungiblePositionManager - addresses.base.aerodromeOETHbWETHClPool, // clOETHbWethPool - addresses.base.aerodromeOETHbWETHClGauge, // gauge address - addresses.base.sugarHelper, // sugarHelper - -1, // lowerBoundingTick - 0, // upperBoundingTick - 0, // tickClosestToParity - ]); - const cAMOStrategyProxy = await ethers.getContract( "AerodromeAMOStrategyProxy" ); - const cAMOStrategyImpl = await ethers.getContract("AerodromeAMOStrategy"); const cAMOStrategy = await ethers.getContractAt( "AerodromeAMOStrategy", cAMOStrategyProxy.address diff --git a/contracts/deploy/base/017_upgrade_amo.js b/contracts/deploy/base/017_upgrade_amo.js new file mode 100644 index 0000000000..52b296771d --- /dev/null +++ b/contracts/deploy/base/017_upgrade_amo.js @@ -0,0 +1,71 @@ +const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); +const { utils } = require("ethers"); +const { + deployBaseAerodromeAMOStrategyImplementation, +} = require("../deployActions"); + +module.exports = deployOnBaseWithGuardian( + { + deployName: "017_upgrade_amo", + }, + async ({ ethers }) => { + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + const cOETHbVault = await ethers.getContractAt( + "IVault", + cOETHbVaultProxy.address + ); + + const cAMOStrategyProxy = await ethers.getContract( + "AerodromeAMOStrategyProxy" + ); + const cAMOStrategyImpl = + await deployBaseAerodromeAMOStrategyImplementation(); + const cAMOStrategy = await ethers.getContractAt( + "AerodromeAMOStrategy", + cAMOStrategyProxy.address + ); + + return { + actions: [ + { + // 1. Upgrade AMO + contract: cAMOStrategyProxy, + signature: "upgradeTo(address)", + args: [cAMOStrategyImpl.address], + }, + { + // 2. Reset WETH approvals to 0 on swapRouter and positionManager + contract: cAMOStrategy, + signature: "safeApproveAllTokens()", + args: [], + }, + { + // 2. set auto allocate threshold to 0.1 - gas is cheap + contract: cOETHbVault, + signature: "setAutoAllocateThreshold(uint256)", + args: [utils.parseUnits("0.1", 18)], + }, + // { + // // 3. set that 0.04% (4 basis points) of Vualt TVL triggers the allocation. + // // At the time of writing this is ~53 ETH + // contract: cOETHbVault, + // signature: "setVaultBuffer(uint256)", + // args: [utils.parseUnits("4", 14)], + // }, + { + // 3. for now disable allocating weth + contract: cOETHbVault, + signature: "setVaultBuffer(uint256)", + args: [utils.parseUnits("1", 18)], + }, + { + // 4. set aerodrome AMO as WETH asset default strategy + contract: cOETHbVault, + signature: "setAssetDefaultStrategy(address,address)", + args: [addresses.base.WETH, cAMOStrategyProxy.address], + }, + ], + }; + } +); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 28c226354d..30bad63887 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1566,6 +1566,29 @@ const deployOUSDSwapper = async () => { await vault.connect(sGovernor).setOracleSlippage(assetAddresses.USDT, 50); }; +const deployBaseAerodromeAMOStrategyImplementation = async () => { + const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + + await deployWithConfirmation("AerodromeAMOStrategy", [ + /* Check all these values match 006_base_amo_strategy deploy file + */ + [addresses.zero, cOETHbVaultProxy.address], // platformAddress, VaultAddress + addresses.base.WETH, // weth address + cOETHbProxy.address, // OETHb address + addresses.base.swapRouter, // swapRouter + addresses.base.nonFungiblePositionManager, // nonfungiblePositionManager + addresses.base.aerodromeOETHbWETHClPool, // clOETHbWethPool + addresses.base.aerodromeOETHbWETHClGauge, // gauge address + addresses.base.sugarHelper, // sugarHelper + -1, // lowerBoundingTick + 0, // upperBoundingTick + 0, // tickClosestToParity + ]); + + return await ethers.getContract("AerodromeAMOStrategy"); +}; + module.exports = { deployOracles, deployCore, @@ -1600,4 +1623,5 @@ module.exports = { deployOUSDSwapper, upgradeNativeStakingSSVStrategy, upgradeNativeStakingFeeAccumulator, + deployBaseAerodromeAMOStrategyImplementation, }; diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 629bbe051c..4b73d0f896 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -156,6 +156,9 @@ const defaultBaseFixture = deployments.createFixture(async () => { await impersonateAndFund(governor.address); await impersonateAndFund(timelock.address); + + // configure Vault to not automatically deposit to strategy + await oethbVault.connect(governor).setVaultBuffer(oethUnits("1")); } // Make sure we can print bridged WOETH for tests diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index 03fbe56f5d..eae2879037 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -161,12 +161,19 @@ chai.Assertion.addMethod( 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.` + const errorName = errorSignature.substring( + 0, + errorSignature.indexOf("(") + ); + + const containsError = + e.message.includes(errorHash) || e.message.includes(errorName); + + if (!containsError) { + chai.expect.fail( + `Expected error message with signature ${errorSignature} but another was thrown: ${e.message}` ); + } } if (txSucceeded) { diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index eddfef411e..289e33eb3e 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -297,6 +297,15 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { .approve(aeroSwapRouter.address, oethUnits("1000000000")); }); + const cofigureAutomaticDepositOnMint = async (vaultBuffer) => { + await oethbVault.connect(governor).setVaultBuffer(vaultBuffer); + + const totalValue = await oethbVault.totalValue(); + + // min mint to trigger deposits + return totalValue.mul(vaultBuffer).div(oethUnits("1")); + }; + // tests need liquidity outside AMO ticks in order to test for fail states const depositLiquidityToPool = async () => { await weth @@ -838,15 +847,6 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { [weth.address], [amount] ); - await expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.equal( - amount - ); - - await expect( - aerodromeAmoStrategy - .connect(strategist) - .rebalance(oethUnits("0"), false, oethUnits("0")) - ); await expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.equal( oethUnits("0") @@ -886,6 +886,9 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { it("Should be able to rebalance the pool when price pushed to 1:1", async () => { await depositLiquidityToPool(); + // supply some WETH for the rebalance + await mintAndDepositToStrategy({ amount: oethUnits("1") }); + const priceAtTick0 = await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); let { value: value0, direction: direction0 } = await quoteAmountToSwapToReachPrice({ @@ -897,9 +900,6 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { swapWeth: direction0, }); - // supply some WETH for the rebalance - await mintAndDepositToStrategy({ amount: oethUnits("1") }); - const { value, direction } = await quoteAmountToSwapBeforeRebalance({ lowValue: oethUnits("0"), highValue: oethUnits("0"), @@ -912,19 +912,57 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { 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); + // 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(); }); - it("Should be able to rebalance the pool when price pushed to close to 1 OETHb costing 1.0001 WETH", async () => { + it("Should be able to rebalance the pool when price pushed to over the 1 OETHb costing 1.0001 WETH", async () => { const priceAtTickLower = await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + const priceAtTickHigher = + await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + // 5% of the price diff within a single ticker + const fivePctTickerPrice = priceAtTickHigher + .sub(priceAtTickLower) + .div(20); + let { value: value0, direction: direction0 } = await quoteAmountToSwapToReachPrice({ - price: priceAtTickLower, + price: priceAtTickLower.add(fivePctTickerPrice), + }); + await swap({ + amount: value0, + swapWeth: direction0, + }); + + const { value, direction } = await quoteAmountToSwapBeforeRebalance({ + lowValue: oethUnits("0"), + highValue: oethUnits("0"), + }); + await rebalance(value, direction, value.mul("99").div("100")); + + await assetLpStakedInGauge(); + }); + + it("Should be able to rebalance the pool when price pushed to close to the 1 OETHb costing 1.0001 WETH", async () => { + const priceAtTickLower = + await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + const priceAtTickHigher = + await aerodromeAmoStrategy.sqrtRatioX96TickHigher(); + // 5% of the price diff within a single ticker + const fivePctTickerPrice = priceAtTickHigher + .sub(priceAtTickLower) + .div(20); + + let { value: value0, direction: direction0 } = + await quoteAmountToSwapToReachPrice({ + price: priceAtTickLower.sub(fivePctTickerPrice), }); await swap({ amount: value0, @@ -943,9 +981,6 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { it("Should have the correct balance within some tolerance", async () => { const balance = await aerodromeAmoStrategy.checkBalance(weth.address); await mintAndDepositToStrategy({ amount: oethUnits("6") }); - await expect( - await aerodromeAmoStrategy.checkBalance(weth.address) - ).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")); @@ -983,18 +1018,29 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { }); it("Should not be able to rebalance when protocol is insolvent", async () => { - const stratSigner = await impersonateAndFund( - aerodromeAmoStrategy.address - ); - await mintAndDepositToStrategy({ amount: oethUnits("1000") }); + await aerodromeAmoStrategy.connect(oethbVaultSigner).withdrawAll(); + + // ensure there is a LP position + await mintAndDepositToStrategy({ amount: oethUnits("1") }); + // transfer WETH out making the protocol insolvent - const bal = await weth.balanceOf(aerodromeAmoStrategy.address); - await weth.connect(stratSigner).transfer(addresses.dead, bal); + const swapBal = oethUnits("0.00001"); + const addLiquidityBal = oethUnits("1"); + const balRemaining = (await weth.balanceOf(oethbVault.address)) + .sub(swapBal) + .sub(addLiquidityBal); + + await weth + .connect(oethbVaultSigner) + .transfer(aerodromeAmoStrategy.address, swapBal.add(addLiquidityBal)); + await weth + .connect(oethbVaultSigner) + .transfer(addresses.dead, balRemaining); await expect( rebalance( - oethUnits("0.00001"), + swapBal, true, // _swapWETHs oethUnits("0.000009") ) @@ -1002,6 +1048,101 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { await assetLpStakedInGauge(); }); + + const depositAllWethAndConfigure1Bp = async () => { + // configure to leave no WETH on the vault + await cofigureAutomaticDepositOnMint(oethUnits("0")); + // deposit all Vault's WETH + await depositAllVaultWeth(); + // cnofigure to only keep 1bp of the Vault's totalValue in the Vault; + const minAmountReserved = await cofigureAutomaticDepositOnMint( + oethUnits("0.0001") + ); + + return minAmountReserved; + }; + + it("Should not automatically deposit to strategy when below vault buffer threshold", async () => { + const minAmountReserved = await depositAllWethAndConfigure1Bp(); + + await expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.equal( + oethUnits("0") + ); + await expect(await weth.balanceOf(oethbVault.address)).to.equal( + oethUnits("0") + ); + + const amountBelowThreshold = minAmountReserved.div(BigNumber.from("2")); + + await mint({ amount: amountBelowThreshold }); + await expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.equal( + oethUnits("0") + ); + await expect( + await weth.balanceOf(oethbVault.address) + ).to.approxEqualTolerance(amountBelowThreshold); + + await assetLpStakedInGauge(); + }); + + it("Should deposit amount above the vault buffer threshold to the strategy on mint", async () => { + const minAmountReserved = await depositAllWethAndConfigure1Bp(); + + await expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.equal( + oethUnits("0") + ); + await expect(await weth.balanceOf(oethbVault.address)).to.equal( + oethUnits("0") + ); + + const amountDoubleThreshold = minAmountReserved.mul(BigNumber.from("2")); + + await mint({ amount: amountDoubleThreshold }); + await expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.equal( + oethUnits("0") + ); + // threshold amount should be left on the vault + await expect( + await weth.balanceOf(oethbVault.address) + ).to.approxEqualTolerance(minAmountReserved); + + await assetLpStakedInGauge(); + }); + + it("Should leave WETH on the contract when pool price outside allowed limits", async () => { + const minAmountReserved = await depositAllWethAndConfigure1Bp(); + const amountDoubleThreshold = minAmountReserved.mul(BigNumber.from("2")); + + await expect(await weth.balanceOf(aerodromeAmoStrategy.address)).to.equal( + oethUnits("0") + ); + + const priceAtTickLower = + await aerodromeAmoStrategy.sqrtRatioX96TickLower(); + let { value: value0, direction: direction0 } = + await quoteAmountToSwapToReachPrice({ + price: priceAtTickLower, + }); + + // push price so 1 OETHb costs 1.0001 WETH + await swap({ + amount: value0, + swapWeth: direction0, + }); + + await mint({ amount: amountDoubleThreshold }); + + // roughly half of WETH should stay on the Aerodrome contract + await expect( + await weth.balanceOf(aerodromeAmoStrategy.address) + ).to.approxEqualTolerance(minAmountReserved); + // roughly half of WETH should stay on the Vault + await expect( + await weth.balanceOf(oethbVault.address) + ).to.approxEqualTolerance(minAmountReserved); + + await assetLpStakedInGauge(); + }); }); describe("Perform multiple actions", function () { @@ -1214,7 +1355,44 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { .rebalance(amountToSwap, swapWETH, minTokenReceived); }; - const mintAndDepositToStrategy = async ({ userOverride, amount } = {}) => { + const depositAllVaultWeth = async ({ returnTransaction } = {}) => { + const balance = weth.balanceOf(oethbVault.address); + const gov = await oethbVault.governor(); + const tx = await oethbVault + .connect(await impersonateAndFund(gov)) + .depositToStrategy( + aerodromeAmoStrategy.address, + [weth.address], + [balance] + ); + + if (returnTransaction) { + return tx; + } + + await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); + }; + + const mint = async ({ userOverride, amount } = {}) => { + 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); + const tx = await oethbVault + .connect(user) + .mint(weth.address, amount, amount); + return tx; + }; + + const mintAndDepositToStrategy = async ({ + userOverride, + amount, + returnTransaction, + } = {}) => { const user = userOverride || rafael; amount = amount || oethUnits("5"); @@ -1226,12 +1404,18 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { await oethbVault.connect(user).mint(weth.address, amount, amount); const gov = await oethbVault.governor(); - await oethbVault + const tx = await oethbVault .connect(await impersonateAndFund(gov)) .depositToStrategy( aerodromeAmoStrategy.address, [weth.address], [amount] ); + + if (returnTransaction) { + return tx; + } + + await expect(tx).to.emit(aerodromeAmoStrategy, "PoolRebalanced"); }; });