From 00433435515b76e3f96fa9dac4e2e2d8a3180566 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 5 Aug 2024 01:45:38 +0200 Subject: [PATCH 1/9] basic setup for the strategy contract --- .../interfaces/aerodrome/ICLFactory.sol | 14 + .../interfaces/aerodrome/ICLPool.sol | 19 + .../aerodrome/INonfungiblePositionManager.sol | 149 ++++++ .../interfaces/aerodrome/ISugarHelper.sol | 66 +++ .../interfaces/aerodrome/ISwapRouter.sol | 65 +++ contracts/contracts/proxies/Proxies.sol | 7 + .../strategies/AerodromeAMOStrategy.sol | 469 ++++++++++++++++++ .../deploy/base/004_base_amo_strategy.js | 74 +++ contracts/utils/addresses.js | 7 + 9 files changed, 870 insertions(+) create mode 100644 contracts/contracts/interfaces/aerodrome/ICLFactory.sol create mode 100644 contracts/contracts/interfaces/aerodrome/ICLPool.sol create mode 100644 contracts/contracts/interfaces/aerodrome/INonfungiblePositionManager.sol create mode 100644 contracts/contracts/interfaces/aerodrome/ISugarHelper.sol create mode 100644 contracts/contracts/interfaces/aerodrome/ISwapRouter.sol create mode 100644 contracts/contracts/strategies/AerodromeAMOStrategy.sol create mode 100644 contracts/deploy/base/004_base_amo_strategy.js diff --git a/contracts/contracts/interfaces/aerodrome/ICLFactory.sol b/contracts/contracts/interfaces/aerodrome/ICLFactory.sol new file mode 100644 index 0000000000..9aca6d4360 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/ICLFactory.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +/// @title The interface for the CL Factory +/// @notice The CL Factory facilitates creation of CL pools and control over the protocol fees +interface ICLFactory { + /// @notice Returns the pool address for a given pair of tokens and a tick spacing, or address 0 if it does not exist + /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order + /// @param tokenA The contract address of either token0 or token1 + /// @param tokenB The contract address of the other token + /// @param tickSpacing The tick spacing of the pool + /// @return pool The pool address + function getPool(address tokenA, address tokenB, int24 tickSpacing) external view returns (address pool); +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/ICLPool.sol b/contracts/contracts/interfaces/aerodrome/ICLPool.sol new file mode 100644 index 0000000000..0342dbfe78 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/ICLPool.sol @@ -0,0 +1,19 @@ +pragma solidity >=0.5.0; + +/// @title The interface for a CL Pool +/// @notice A CL pool facilitates swapping and automated market making between any two assets that strictly conform +/// to the ERC20 specification +/// @dev The pool interface is broken up into many smaller pieces +interface ICLPool { + function slot0() + external + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + bool unlocked + ); +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/INonfungiblePositionManager.sol b/contracts/contracts/interfaces/aerodrome/INonfungiblePositionManager.sol new file mode 100644 index 0000000000..ede8aacba4 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/INonfungiblePositionManager.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +/// @title Non-fungible token for positions +/// @notice Wraps CL positions in a non-fungible token interface which allows for them to be transferred +/// and authorized. +interface INonfungiblePositionManager +{ + /// @notice Returns the position information associated with a given token ID. + /// @dev Throws if the token ID is not valid. + /// @param tokenId The ID of the token that represents the position + /// @return nonce The nonce for permits + /// @return operator The address that is approved for spending + /// @return token0 The address of the token0 for a specific pool + /// @return token1 The address of the token1 for a specific pool + /// @return tickSpacing The tick spacing associated with the pool + /// @return tickLower The lower end of the tick range for the position + /// @return tickUpper The higher end of the tick range for the position + /// @return liquidity The liquidity of the position + /// @return feeGrowthInside0LastX128 The fee growth of token0 as of the last action on the individual position + /// @return feeGrowthInside1LastX128 The fee growth of token1 as of the last action on the individual position + /// @return tokensOwed0 The uncollected amount of token0 owed to the position as of the last computation + /// @return tokensOwed1 The uncollected amount of token1 owed to the position as of the last computation + function positions(uint256 tokenId) + external + view + returns ( + uint96 nonce, + address operator, + address token0, + address token1, + int24 tickSpacing, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + + struct MintParams { + address token0; + address token1; + int24 tickSpacing; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + uint160 sqrtPriceX96; + } + + /// @notice Creates a new position wrapped in a NFT + /// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized + /// a method does not exist, i.e. the pool is assumed to be initialized. + /// @param params The params necessary to mint a position, encoded as `MintParams` in calldata + /// @return tokenId The ID of the token that represents the minted position + /// @return liquidity The amount of liquidity for this position + /// @return amount0 The amount of token0 + /// @return amount1 The amount of token1 + function mint(MintParams calldata params) + external + payable + returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + + struct IncreaseLiquidityParams { + uint256 tokenId; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + /// @notice Increases the amount of liquidity in a position, with tokens paid by the `msg.sender` + /// @param params tokenId The ID of the token for which liquidity is being increased, + /// amount0Desired The desired amount of token0 to be spent, + /// amount1Desired The desired amount of token1 to be spent, + /// amount0Min The minimum amount of token0 to spend, which serves as a slippage check, + /// amount1Min The minimum amount of token1 to spend, which serves as a slippage check, + /// deadline The time by which the transaction must be included to effect the change + /// @return liquidity The new liquidity amount as a result of the increase + /// @return amount0 The amount of token0 to acheive resulting liquidity + /// @return amount1 The amount of token1 to acheive resulting liquidity + function increaseLiquidity(IncreaseLiquidityParams calldata params) + external + payable + returns (uint128 liquidity, uint256 amount0, uint256 amount1); + + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + /// @notice Decreases the amount of liquidity in a position and accounts it to the position + /// @param params tokenId The ID of the token for which liquidity is being decreased, + /// amount The amount by which liquidity will be decreased, + /// amount0Min The minimum amount of token0 that should be accounted for the burned liquidity, + /// amount1Min The minimum amount of token1 that should be accounted for the burned liquidity, + /// deadline The time by which the transaction must be included to effect the change + /// @return amount0 The amount of token0 accounted to the position's tokens owed + /// @return amount1 The amount of token1 accounted to the position's tokens owed + /// @dev The use of this function can cause a loss to users of the NonfungiblePositionManager + /// @dev for tokens that have very high decimals. + /// @dev The amount of tokens necessary for the loss is: 3.4028237e+38. + /// @dev This is equivalent to 1e20 value with 18 decimals. + function decreaseLiquidity(DecreaseLiquidityParams calldata params) + external + payable + returns (uint256 amount0, uint256 amount1); + + struct CollectParams { + uint256 tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } + + /// @notice Collects up to a maximum amount of fees owed to a specific position to the recipient + /// @notice Used to update staked positions before deposit and withdraw + /// @param params tokenId The ID of the NFT for which tokens are being collected, + /// recipient The account that should receive the tokens, + /// amount0Max The maximum amount of token0 to collect, + /// amount1Max The maximum amount of token1 to collect + /// @return amount0 The amount of fees collected in token0 + /// @return amount1 The amount of fees collected in token1 + function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1); + + /// @notice Burns a token ID, which deletes it from the NFT contract. The token must have 0 liquidity and all tokens + /// must be collected first. + /// @param tokenId The ID of the token that is being burned + function burn(uint256 tokenId) external payable; + + /// @notice Sets a new Token Descriptor + /// @param _tokenDescriptor Address of the new Token Descriptor to be chosen + function setTokenDescriptor(address _tokenDescriptor) external; + + /// @notice Sets a new Owner address + /// @param _owner Address of the new Owner to be chosen + function setOwner(address _owner) external; +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol b/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol new file mode 100644 index 0000000000..05258e63f2 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; +pragma abicoder v2; + +import {INonfungiblePositionManager} from "./INonfungiblePositionManager.sol"; + +interface ISugarHelper { + struct PopulatedTick { + int24 tick; + uint160 sqrtRatioX96; + int128 liquidityNet; + uint128 liquidityGross; + } + + /// + /// Wrappers for LiquidityAmounts + /// + + function getAmountsForLiquidity( + uint160 sqrtRatioX96, + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96, + uint128 liquidity + ) external pure returns (uint256 amount0, uint256 amount1); + + function estimateAmount0(uint256 amount1, address pool, uint160 sqrtRatioX96, int24 tickLow, int24 tickHigh) + external + view + returns (uint256 amount0); + + function estimateAmount1(uint256 amount0, address pool, uint160 sqrtRatioX96, int24 tickLow, int24 tickHigh) + external + view + returns (uint256 amount1); + + /// + /// Wrappers for PositionValue + /// + + function principal(INonfungiblePositionManager positionManager, uint256 tokenId, uint160 sqrtRatioX96) + external + view + returns (uint256 amount0, uint256 amount1); + + function fees(INonfungiblePositionManager positionManager, uint256 tokenId) + external + view + returns (uint256 amount0, uint256 amount1); + + /// + /// Wrappers for TickMath + /// + + function getSqrtRatioAtTick(int24 tick) external pure returns (uint160 sqrtRatioX96); + + function getTickAtSqrtRatio(uint160 sqrtRatioX96) external pure returns (int24 tick); + + /// + /// TickLens Helper + /// + + function getPopulatedTicks(address pool, int24 startTick) + external + view + returns (PopulatedTick[] memory populatedTicks); +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/ISwapRouter.sol b/contracts/contracts/interfaces/aerodrome/ISwapRouter.sol new file mode 100644 index 0000000000..3f49489364 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/ISwapRouter.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via CL +interface ISwapRouter { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + int24 tickSpacing; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata + /// @return amountOut The amount of the received token + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata + /// @return amountOut The amount of the received token + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + int24 tickSpacing; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another token + /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata + /// @return amountIn The amount of the input token + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata + /// @return amountIn The amount of the input token + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); +} \ No newline at end of file diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index bdc6dbeffa..874583f20f 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -280,3 +280,10 @@ contract OETHBaseProxy is InitializeGovernedUpgradeabilityProxy { contract WOETHBaseProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice AerodromeAMOStrategyProxy delegates calls to AerodromeAMOStrategy implementation + */ +contract AerodromeAMOStrategyProxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/contracts/strategies/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/AerodromeAMOStrategy.sol new file mode 100644 index 0000000000..333356fc18 --- /dev/null +++ b/contracts/contracts/strategies/AerodromeAMOStrategy.sol @@ -0,0 +1,469 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title Aerodrome AMO strategy + * @author Origin Protocol Inc + */ +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { IERC20, InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol"; +import { ISugarHelper } from "../interfaces/aerodrome/ISugarHelper.sol"; +import { INonfungiblePositionManager } from "../interfaces/aerodrome/INonfungiblePositionManager.sol"; +import { ISwapRouter } from "../interfaces/aerodrome/ISwapRouter.sol"; +import { ICLFactory } from "../interfaces/aerodrome/ICLFactory.sol"; +import { ICLPool } from "../interfaces/aerodrome/ICLPool.sol"; + + +contract AerodromeAMOStrategy is InitializableAbstractStrategy { + using SafeERC20 for IERC20; + + /*************************************** + Storage slot members + ****************************************/ + + /// @notice tokenId of the liquidity position + uint256 public tokenId; + /// @notice amount of liquidity deployed + uint128 public liquidity; + /// @notice TODO is this redundant to liquidity??? + uint256 public netValue; + /// @notice the swapRouter for performing swaps + ISwapRouter public swapRouter; + /// @notice factory for pool creation + ICLFactory public clFactory; + /// @notice the pool used + ICLPool public clPool; + /// @notice the liquidity position + INonfungiblePositionManager public positionManager; + /// @notice helper contract for liquidity and ticker math + ISugarHelper public helper; + /// @notice sqrtRatioX96Tick0 + uint160 public sqrtRatioX96Tick0; + /// @notice sqrtRatioX96Tick1 + uint160 public sqrtRatioX96Tick1; + /// @dev reserved for inheritance + int256[50] private __reserved; + + /*************************************** + Constants, structs and events + ****************************************/ + + /// @notice The address of the Wrapped ETH (WETH) token contract + address public immutable WETH; + /// @notice The address of the OETHb token contract + address public immutable OETHb; + /// @notice lower tick set to 0 representing the price of 1. Equation 1.0001^0 = 1 + int24 public immutable lowerTick; + /// @notice lower tick set to 1 representing the price of 1.0001. Equation 1.0001^1 = 1.0001 + int24 public immutable upperTick; + /// @notice tick spacing of the pool (set to 1) + int24 public immutable tickSpacing; + + // Represents a position minted by UniswapV3Strategy contract + struct Position { + // The following two fields are redundant but since we use these + // two quite a lot, think it might be cheaper to store it than + // compute it every time? + uint160 sqrtRatioAX96; + uint160 sqrtRatioBX96; + uint256 netValue; // Last recorded net value of the position + } + + event UniswapV3LiquidityAdded( + uint256 indexed tokenId, + uint256 amount0Sent, + uint256 amount1Sent, + uint128 liquidityMinted + ); + event UniswapV3LiquidityRemoved( + uint256 indexed tokenId, + uint256 amount0Received, + uint256 amount1Received, + uint128 liquidityBurned + ); + event UniswapV3PositionMinted( + uint256 indexed tokenId, + int24 lowerTick, + int24 upperTick + ); + + /// @param _stratConfig st + /// @param _wethAddress Address of the Erc20 WETH Token contract + /// @param _OETHbAddress Address of the Erc20 OETHb Token contract + constructor( + BaseStrategyConfig memory _stratConfig, + address _wethAddress, + address _OETHbAddress + ) InitializableAbstractStrategy(_stratConfig) + { + WETH = _wethAddress; + OETHb = _OETHbAddress; + + lowerTick = 0; + upperTick = 1; + tickSpacing = 1; + } + + /** + * @notice initialize function, to set up initial internal state + * @param _rewardTokenAddresses Address of reward token for platform + * @param _assets Addresses of initial supported assets + * @param _pTokens Platform Token corresponding addresses + * @param _swapRouter Address of the Aerodrome Universal Swap Router + * @param _nonfungiblePositionManager Address of position manager to add/remove + * the liquidity + * @param _clFactory Address of the concentrated liquidity factory + */ + function initialize( + address[] memory _rewardTokenAddresses, + address[] memory _assets, + address[] memory _pTokens, + address _swapRouter, + address _nonfungiblePositionManager, + address _clFactory, + address _sugarHelper + ) external onlyGovernor initializer { + InitializableAbstractStrategy._initialize( + _rewardTokenAddresses, + _assets, + _pTokens + ); + + swapRouter = ISwapRouter(_swapRouter); + positionManager = INonfungiblePositionManager( + _nonfungiblePositionManager + ); + clFactory = ICLFactory(_clFactory); + helper = ISugarHelper(_sugarHelper); + sqrtRatioX96Tick0 = helper.getSqrtRatioAtTick(0); + sqrtRatioX96Tick1 = helper.getSqrtRatioAtTick(1); + } + + /** + * @notice Deposit an amount of assets into the platform + * @param _asset Address for the asset + * @param _amount Units of asset to deposit + */ + function deposit(address _asset, uint256 _amount) external virtual override { + + } + + /** + * @notice Deposit all supported assets in this strategy contract to the platform + */ + function depositAll() external virtual override { + + } + + /** + * @notice Withdraw an `amount` of assets from the platform and + * send to the `_recipient`. + * @param _recipient Address to which the asset should be sent + * @param _asset Address of the asset + * @param _amount Units of asset to withdraw + */ + function withdraw( + address _recipient, + address _asset, + uint256 _amount + ) external virtual override { + + } + + /** + * @notice Withdraw all supported assets from platform and + * sends to the OToken's Vault. + */ + function withdrawAll() external virtual override { + + } + + + /** + * @dev Default TODO + */ + function _collectRewardTokens() internal override { + // TODO do other stuff + super._collectRewardTokens(); + } + + /** + * @dev Retuns bool indicating whether asset is supported by strategy + * @param _asset Address of the asset + */ + function supportsAsset(address _asset) public view override returns (bool) { + return _asset == WETH; + } + + /** + * @dev Internal method to respond to the addition of new asset / pTokens + We need to give the Aerodrome pool approval to transfer the + asset. + * @param _asset Address of the asset to approve + * @param _pToken Address of the pToken + */ + // solhint-disable-next-line no-unused-vars + function _abstractSetPToken(address _asset, address _pToken) + internal + override + { + IERC20(_asset).safeApprove(address(positionManager), type(uint256).max); + IERC20(_asset).safeApprove(address(swapRouter), type(uint256).max); + } + + /** + * @dev Approve the spending of all assets by their corresponding aToken, + * if for some reason is it necessary. + */ + function safeApproveAllTokens() + external + override + onlyGovernor + nonReentrant + { + IERC20(WETH).safeApprove(address(positionManager), type(uint256).max); + IERC20(OETHb).safeApprove(address(positionManager), type(uint256).max); + IERC20(WETH).safeApprove(address(swapRouter), type(uint256).max); + IERC20(OETHb).safeApprove(address(swapRouter), type(uint256).max); + } + + /*************************************** + Balances and Fees + ****************************************/ + + /** + * @dev Get the total asset value held in the platform + * @param _asset Address of the asset + * @return balance Total value of the asset in the platform + */ + function checkBalance(address _asset) + external + view + override + returns (uint256) + { + // TODO verify + return netValue; + } + + /** + * @notice Returns the accumulated fees from the active position + * @return amount0 Amount of token0 ready to be collected as fee + * @return amount1 Amount of token1 ready to be collected as fee + */ + function getPendingFees() + external + view + returns (uint256 amount0, uint256 amount1) + { + + (amount0, amount1) = helper.fees( + positionManager, + tokenId + ); + } + + /** + * @dev Returns the balance of both tokens in a given position (excluding fees) + * @return amount0 Amount of token0 in position + * @return amount1 Amount of token1 in position + */ + function getPositionPrincipal() + internal + view + returns (uint256 amount0, uint256 amount1) + { + (uint160 sqrtRatioX96, , , , ,) = clPool.slot0(); + (amount0, amount1) = helper.principal( + positionManager, + tokenId, + sqrtRatioX96 + ); + } + + /** + * @dev Returns the fees in a given position + * TODO: test this, should return 0 since we don't earn fees + * @return amount0 Amount of token0 in position + * @return amount1 Amount of token1 in position + */ + function getPositionFees() + internal + view + returns (uint256 amount0, uint256 amount1) + { + (uint160 sqrtRatioX96, , , , ,) = clPool.slot0(); + (amount0, amount1) = helper.fees( + positionManager, + tokenId + ); + } + + /*************************************** + Hidden functions + ****************************************/ + /// @inheritdoc InitializableAbstractStrategy + function setPTokenAddress(address, address) external override { + // The pool tokens can never change. + revert("Unsupported method"); + } + + /// @inheritdoc InitializableAbstractStrategy + function removePToken(uint256) external override { + // The pool tokens can never change. + revert("Unsupported method"); + } + + /** + * @notice Mints a new position on the pool and provides liquidity to it + * + * @param _desiredAmount0 Desired amount of token0 to provide liquidity + * @param _desiredAmount1 Desired amount of token1 to provide liquidity + * @param _minAmount0 Min amount of token0 to deposit + * @param _minAmount1 Min amount of token1 to deposit + */ + function _mintPosition( + uint256 _desiredAmount0, + uint256 _desiredAmount1, + uint256 _minAmount0, + uint256 _minAmount1, + int24 _lowerTick, + int24 _upperTick + ) + internal + { + INonfungiblePositionManager.MintParams + memory params = INonfungiblePositionManager.MintParams({ + // TODO: we need to figure which token is smaller when it comes to addresses: + // https://github.com/velodrome-finance/slipstream/blob/87e4aae8143a2f3a800b8e1ef8c58d9b807caf4e/contracts/core/CLFactory.sol#L73 + + token0: WETH, + token1: OETHb, + tickSpacing: tickSpacing, + tickLower: _lowerTick, + tickUpper: _upperTick, + amount0Desired: _desiredAmount0, + amount1Desired: _desiredAmount1, + amount0Min: _minAmount0, + amount1Min: _minAmount1, + recipient: address(this), + deadline: block.timestamp, + /* Sets the initial price to 1 meaning we deposit only OETHb into the pool + * after that we swap 20% of it to WETH and burn the resulting OETHb + */ + sqrtPriceX96: sqrtRatioX96Tick0 + }); + + (uint256 mintedTokenId, uint128 mintedLiquidity, uint256 amount0, uint256 amount1) = positionManager.mint(params); + + tokenId = mintedTokenId; + liquidity = mintedLiquidity; + netValue = amount0 + amount1; + // TODO fetch the pool address + + //emit UniswapV3PositionMinted(mintedTokenId, lowerTick, upperTick); + //emit UniswapV3LiquidityAdded(mintedTokenId, amount0, amount1, liquidity); + } + + /** + * @dev Swaps one token for other and then provides liquidity to pools. + * + * @param _desiredAmount0 Minimum amount of token0 needed + * @param _desiredAmount1 Minimum amount of token1 needed + * @param _swapAmountIn Amount of tokens to swap + * @param _swapMinAmountOut Minimum amount of other tokens expected + * @param _sqrtPriceLimitX96 Max price limit for swap + * @param _swapZeroForOne True if swapping from token0 to token1 + */ + // function _ensureAssetsBySwapping( + // uint256 _desiredAmount0, + // uint256 _desiredAmount1, + // uint256 _swapAmountIn, + // uint256 _swapMinAmountOut, + // uint160 _sqrtPriceLimitX96, + // bool _swapZeroForOne + // ) internal { + // require(!swapsPaused, "Swaps are paused"); + + // uint256 token0Balance = IERC20(token0).balanceOf(address(this)); + // uint256 token1Balance = IERC20(token1).balanceOf(address(this)); + + // uint256 token0Needed = _desiredAmount0 > token0Balance + // ? _desiredAmount0 - token0Balance + // : 0; + // uint256 token1Needed = _desiredAmount1 > token1Balance + // ? _desiredAmount1 - token1Balance + // : 0; + + // if (_swapZeroForOne) { + // // Amount available in reserve strategies + // uint256 t1ReserveBal = reserveStrategy1.checkBalance(token1); + + // // Only swap when asset isn't available in reserve as well + // require(token1Needed > 0, "No need for swap"); + // require( + // token1Needed > t1ReserveBal, + // "Cannot swap when the asset is available in reserve" + // ); + // // Additional amount of token0 required for swapping + // token0Needed += _swapAmountIn; + // // Subtract token1 that we will get from swapping + // token1Needed = (_swapMinAmountOut >= token1Needed) + // ? 0 + // : (token1Needed - _swapMinAmountOut); + // } else { + // // Amount available in reserve strategies + // uint256 t0ReserveBal = reserveStrategy0.checkBalance(token0); + + // // Only swap when asset isn't available in reserve as well + // require(token0Needed > 0, "No need for swap"); + // require( + // token0Needed > t0ReserveBal, + // "Cannot swap when the asset is available in reserve" + // ); + // // Additional amount of token1 required for swapping + // token1Needed += _swapAmountIn; + // // Subtract token0 that we will get from swapping + // token0Needed = (_swapMinAmountOut >= token0Needed) + // ? 0 + // : (token0Needed - _swapMinAmountOut); + // } + + // // Fund strategy from reserve strategies + // if (token0Needed > 0) { + // IVault(vaultAddress).withdrawFromUniswapV3Reserve( + // token0, + // token0Needed + // ); + // } + + // if (token1Needed > 0) { + // IVault(vaultAddress).withdrawFromUniswapV3Reserve( + // token1, + // token1Needed + // ); + // } + + // // Swap it + // uint256 amountReceived = swapRouter.exactInputSingle( + // ISwapRouter.ExactInputSingleParams({ + // tokenIn: _swapZeroForOne ? token0 : token1, + // tokenOut: _swapZeroForOne ? token1 : token0, + // fee: poolFee, + // recipient: address(this), + // deadline: block.timestamp, + // amountIn: _swapAmountIn, + // amountOutMinimum: _swapMinAmountOut, + // sqrtPriceLimitX96: _sqrtPriceLimitX96 + // }) + // ); + + // emit AssetSwappedForRebalancing( + // _swapZeroForOne ? token0 : token1, + // _swapZeroForOne ? token1 : token0, + // _swapAmountIn, + // amountReceived + // ); + // } +} diff --git a/contracts/deploy/base/004_base_amo_strategy.js b/contracts/deploy/base/004_base_amo_strategy.js new file mode 100644 index 0000000000..43724bf94f --- /dev/null +++ b/contracts/deploy/base/004_base_amo_strategy.js @@ -0,0 +1,74 @@ +const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2"); +const { + deployWithConfirmation, + withConfirmation, +} = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); + +module.exports = deployOnBaseWithGuardian( + { + deployName: "004_base_amo_strategy", + }, + 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"); + + 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 + ]); + + const cAMOStrategyProxy = await ethers.getContract("AerodromeAMOStrategyProxy"); + const cAMOStrategy = await ethers.getContract("AerodromeAMOStrategy"); + + console.log("Deployed AMO strategy and proxy contracts"); + + // Init the AMO strategy + const initData = cAMOStrategy.interface.encodeFunctionData( + "initialize(address[],address[],address[],address,address,address,address)", + [ + [addresses.base.AERO], // rewardTokenAddresses + [], // assets + [], // pTokens + addresses.base.universalSwapRouter, // swapRouter + addresses.base.nonFungiblePositionManager, // nonfungiblePositionManager + addresses.base.poolFactory, // clFactory + addresses.base.sugarHelper // sugarHelper + ] + ); + // prettier-ignore + await withConfirmation( + cAMOStrategyProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + cAMOStrategy.address, + deployerAddr, + initData + ) + ); + console.log("Initialized cAMOStrategyProxy and implementation"); + + // Transfer ownership + await withConfirmation( + cAMOStrategyProxy.connect(sDeployer).transferGovernance(governorAddr) + ); + console.log("Transferred Governance"); + + return { + actions: [ + { + // 1. Claim Governance on the AMO strategy + contract: cAMOStrategyProxy, + signature: "claimGovernance()", + args: [], + } + ], + }; + } +); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 40a5ca16be..2aaa22688c 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -293,6 +293,7 @@ addresses.base.ethUsdPriceFeed = "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70"; addresses.base.aeroUsdPriceFeed = "0x4EC5970fC728C5f65ba413992CD5fF6FD70fcfF0"; addresses.base.WETH = "0x4200000000000000000000000000000000000006"; +addresses.base.AERO = "0x940181a94a35a4569e4529a3cdfb74e38fd98631"; addresses.base.wethAeroPoolAddress = "0x80aBe24A3ef1fc593aC5Da960F232ca23B2069d0"; addresses.base.governor = "0x92A19381444A001d62cE67BaFF066fA1111d7202"; @@ -301,6 +302,12 @@ addresses.base.governor = "0x92A19381444A001d62cE67BaFF066fA1111d7202"; addresses.base.BridgedWOETHOracleFeed = "0xe96EB1EDa83d18cbac224233319FA5071464e1b9"; +// Base Aerodrome +addresses.base.nonFungiblePositionManager = "0x827922686190790b37229fd06084350E74485b72"; +addresses.base.poolFactory = "0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A"; +addresses.base.universalSwapRouter = "0x6Cb442acF35158D5eDa88fe602221b67B400Be3E"; +addresses.base.sugarHelper = "0x0AD09A66af0154a84e86F761313d02d0abB6edd5"; + // Holesky addresses.holesky.WETH = "0x94373a4919B3240D86eA41593D5eBa789FEF3848"; From bbff69c9e66a46646bde84754184e49c7ed280c3 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 10 Aug 2024 20:52:35 +0200 Subject: [PATCH 2/9] create proof of concept to simulate required amount to swap considering the removal of liquidity when realancing --- .../interfaces/aerodrome/IAMOCallback.sol | 6 + .../interfaces/aerodrome/IAMOQuoteLoop.sol | 6 + .../interfaces/aerodrome/ICLFactory.sol | 14 - .../interfaces/aerodrome/ICLGauge.sol | 65 +++ .../interfaces/aerodrome/IQuoterV2.sol | 88 ++++ .../interfaces/aerodrome/ISugarHelper.sol | 35 +- .../{ => aerodrome}/AerodromeAMOStrategy.sol | 438 ++++++++++++++---- .../aerodrome/AerodromeQuoteLoop.sol | 18 + .../utils/InitializableAbstractStrategy.sol | 2 +- .../deploy/base/004_base_amo_strategy.js | 48 +- contracts/test/_fixture-base.js | 21 +- .../aerodrome-amo.base.fork-test.js | 59 +++ contracts/utils/addresses.js | 2 + 13 files changed, 676 insertions(+), 126 deletions(-) create mode 100644 contracts/contracts/interfaces/aerodrome/IAMOCallback.sol create mode 100644 contracts/contracts/interfaces/aerodrome/IAMOQuoteLoop.sol delete mode 100644 contracts/contracts/interfaces/aerodrome/ICLFactory.sol create mode 100644 contracts/contracts/interfaces/aerodrome/ICLGauge.sol create mode 100644 contracts/contracts/interfaces/aerodrome/IQuoterV2.sol rename contracts/contracts/strategies/{ => aerodrome}/AerodromeAMOStrategy.sol (51%) create mode 100644 contracts/contracts/strategies/aerodrome/AerodromeQuoteLoop.sol create mode 100644 contracts/test/strategies/aerodrome-amo.base.fork-test.js diff --git a/contracts/contracts/interfaces/aerodrome/IAMOCallback.sol b/contracts/contracts/interfaces/aerodrome/IAMOCallback.sol new file mode 100644 index 0000000000..2a9474c124 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/IAMOCallback.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.0; + +interface IAMOCallback { + function quoteCallback(uint256 _amount, bool _swapWETH) external; +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/IAMOQuoteLoop.sol b/contracts/contracts/interfaces/aerodrome/IAMOQuoteLoop.sol new file mode 100644 index 0000000000..3e8b759a96 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/IAMOQuoteLoop.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.0; + +interface IAMOQuoteLoop { + function quoteLoop(uint256 _amount, bool _swapWETH) external; +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/ICLFactory.sol b/contracts/contracts/interfaces/aerodrome/ICLFactory.sol deleted file mode 100644 index 9aca6d4360..0000000000 --- a/contracts/contracts/interfaces/aerodrome/ICLFactory.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.5.0; - -/// @title The interface for the CL Factory -/// @notice The CL Factory facilitates creation of CL pools and control over the protocol fees -interface ICLFactory { - /// @notice Returns the pool address for a given pair of tokens and a tick spacing, or address 0 if it does not exist - /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order - /// @param tokenA The contract address of either token0 or token1 - /// @param tokenB The contract address of the other token - /// @param tickSpacing The tick spacing of the pool - /// @return pool The pool address - function getPool(address tokenA, address tokenB, int24 tickSpacing) external view returns (address pool); -} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/ICLGauge.sol b/contracts/contracts/interfaces/aerodrome/ICLGauge.sol new file mode 100644 index 0000000000..b7174e948b --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/ICLGauge.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface ICLGauge { + /// @notice Returns the claimable rewards for a given account and tokenId + /// @dev Throws if account is not the position owner + /// @dev pool.updateRewardsGrowthGlobal() needs to be called first, to return the correct claimable rewards + /// @param account The address of the user + /// @param tokenId The tokenId of the position + /// @return The amount of claimable reward + function earned(address account, uint256 tokenId) external view returns (uint256); + + /// @notice Retrieve rewards for all tokens owned by an account + /// @dev Throws if not called by the voter + /// @param account The account of the user + function getReward(address account) external; + + /// @notice Retrieve rewards for a tokenId + /// @dev Throws if not called by the position owner + /// @param tokenId The tokenId of the position + function getReward(uint256 tokenId) external; + + /// @notice Notifies gauge of gauge rewards. + /// @param amount Amount of gauge rewards (emissions) to notify. Must be greater than 604_800. + function notifyRewardAmount(uint256 amount) external; + + /// @dev Notifies gauge of gauge rewards without distributing its fees. + /// Assumes gauge reward tokens is 18 decimals. + /// If not 18 decimals, rewardRate may have rounding issues. + /// @param amount Amount of gauge rewards (emissions) to notify. Must be greater than 604_800. + function notifyRewardWithoutClaim(uint256 amount) external; + + /// @notice Used to deposit a CL position into the gauge + /// @notice Allows the user to receive emissions instead of fees + /// @param tokenId The tokenId of the position + function deposit(uint256 tokenId) external; + + /// @notice Used to withdraw a CL position from the gauge + /// @notice Allows the user to receive fees instead of emissions + /// @notice Outstanding emissions will be collected on withdrawal + /// @param tokenId The tokenId of the position + function withdraw(uint256 tokenId) external; + + // /// @notice Fetch all tokenIds staked by a given account + // /// @param depositor The address of the user + // /// @return The tokenIds of the staked positions + // function stakedValues(address depositor) external view returns (uint256[] memory); + + // /// @notice Fetch a staked tokenId by index + // /// @param depositor The address of the user + // /// @param index The index of the staked tokenId + // /// @return The tokenId of the staked position + // function stakedByIndex(address depositor, uint256 index) external view returns (uint256); + + // /// @notice Check whether a position is staked in the gauge by a certain user + // /// @param depositor The address of the user + // /// @param tokenId The tokenId of the position + // /// @return Whether the position is staked in the gauge + // function stakedContains(address depositor, uint256 tokenId) external view returns (bool); + + // /// @notice The amount of positions staked in the gauge by a certain user + // /// @param depositor The address of the user + // /// @return The amount of positions staked in the gauge + // function stakedLength(address depositor) external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/IQuoterV2.sol b/contracts/contracts/interfaces/aerodrome/IQuoterV2.sol new file mode 100644 index 0000000000..ba863d8fe4 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/IQuoterV2.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +/// @title QuoterV2 Interface +/// @notice Supports quoting the calculated amounts from exact input or exact output swaps. +/// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap. +/// @dev These functions are not marked view because they rely on calling non-view functions and reverting +/// to compute the result. They are also not gas efficient and should not be called on-chain. +interface IQuoterV2 { + /// @notice Returns the amount out received for a given exact input swap without executing the swap + /// @param path The path of the swap, i.e. each token pair and the pool tick spacing + /// @param amountIn The amount of the first token to swap + /// @return amountOut The amount of the last token that would be received + /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path + /// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path + /// @return gasEstimate The estimate of the gas that the swap consumes + function quoteExactInput(bytes memory path, uint256 amountIn) + external + returns ( + uint256 amountOut, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksCrossedList, + uint256 gasEstimate + ); + + struct QuoteExactInputSingleParams { + address tokenIn; + address tokenOut; + uint256 amountIn; + int24 tickSpacing; + uint160 sqrtPriceLimitX96; + } + + /// @notice Returns the amount out received for a given exact input but for a swap of a single pool + /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` + /// tokenIn The token being swapped in + /// tokenOut The token being swapped out + /// tickSpacing The tick spacing of the token pool to consider for the pair + /// amountIn The desired input amount + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// @return amountOut The amount of `tokenOut` that would be received + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed + /// @return gasEstimate The estimate of the gas that the swap consumes + function quoteExactInputSingle(QuoteExactInputSingleParams memory params) + external + returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate); + + /// @notice Returns the amount in required for a given exact output swap without executing the swap + /// @param path The path of the swap, i.e. each token pair and the pool tick spacing. Path must be provided in reverse order + /// @param amountOut The amount of the last token to receive + /// @return amountIn The amount of first token required to be paid + /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path + /// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path + /// @return gasEstimate The estimate of the gas that the swap consumes + function quoteExactOutput(bytes memory path, uint256 amountOut) + external + returns ( + uint256 amountIn, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksCrossedList, + uint256 gasEstimate + ); + + struct QuoteExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint256 amount; + int24 tickSpacing; + uint160 sqrtPriceLimitX96; + } + + /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool + /// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams` + /// tokenIn The token being swapped in + /// tokenOut The token being swapped out + /// tickSpacing The tick spacing of the token pool to consider for the pair + /// amountOut The desired output amount + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed + /// @return gasEstimate The estimate of the gas that the swap consumes + function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params) + external + returns (uint256 amountIn, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate); +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol b/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol index 05258e63f2..0238d29ba4 100644 --- a/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol +++ b/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol @@ -23,11 +23,36 @@ interface ISugarHelper { uint128 liquidity ) external pure returns (uint256 amount0, uint256 amount1); + + function getLiquidityForAmounts( + uint256 amount0, + uint256 amount1, + uint160 sqrtRatioX96, + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96 + ) external pure returns (uint256 liquidity); + + /// @notice Computes the amount of token0 for a given amount of token1 and price range + /// @param amount1 Amount of token1 to estimate liquidity + /// @param pool Address of the pool to be used + /// @param sqrtRatioX96 A sqrt price representing the current pool prices + /// @param tickLow Lower tick boundary + /// @param tickLow Upper tick boundary + /// @dev If the given pool address is not the zero address, will fetch `sqrtRatioX96` from pool + /// @return amount0 Estimated amount of token0 function estimateAmount0(uint256 amount1, address pool, uint160 sqrtRatioX96, int24 tickLow, int24 tickHigh) external view returns (uint256 amount0); + /// @notice Computes the amount of token1 for a given amount of token0 and price range + /// @param amount0 Amount of token0 to estimate liquidity + /// @param pool Address of the pool to be used + /// @param sqrtRatioX96 A sqrt price representing the current pool prices + /// @param tickLow Lower tick boundary + /// @param tickLow Upper tick boundary + /// @dev If the given pool address is not the zero address, will fetch `sqrtRatioX96` from pool + /// @return amount1 Estimated amount of token1 function estimateAmount1(uint256 amount0, address pool, uint160 sqrtRatioX96, int24 tickLow, int24 tickHigh) external view @@ -55,10 +80,12 @@ interface ISugarHelper { function getTickAtSqrtRatio(uint160 sqrtRatioX96) external pure returns (int24 tick); - /// - /// TickLens Helper - /// - + /// @notice Fetches Tick Data for all populated Ticks in given bitmaps + /// @param pool Address of the pool from which to fetch data + /// @param startTick Tick from which the first bitmap will be fetched + /// @dev The number of bitmaps fetched by this function should always be `MAX_BITMAPS`, + /// unless there are less than `MAX_BITMAPS` left to iterate through + /// @return populatedTicks Array of all Populated Ticks in the provided bitmaps function getPopulatedTicks(address pool, int24 startTick) external view diff --git a/contracts/contracts/strategies/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol similarity index 51% rename from contracts/contracts/strategies/AerodromeAMOStrategy.sol rename to contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index 333356fc18..188a0fee0a 100644 --- a/contracts/contracts/strategies/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -7,15 +7,23 @@ pragma solidity ^0.8.0; */ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20, InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol"; -import { ISugarHelper } from "../interfaces/aerodrome/ISugarHelper.sol"; -import { INonfungiblePositionManager } from "../interfaces/aerodrome/INonfungiblePositionManager.sol"; -import { ISwapRouter } from "../interfaces/aerodrome/ISwapRouter.sol"; -import { ICLFactory } from "../interfaces/aerodrome/ICLFactory.sol"; -import { ICLPool } from "../interfaces/aerodrome/ICLPool.sol"; - - -contract AerodromeAMOStrategy is InitializableAbstractStrategy { +import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { StableMath } from "../../utils/StableMath.sol"; + +import { ISugarHelper } from "../../interfaces/aerodrome/ISugarHelper.sol"; +import { INonfungiblePositionManager } from "../../interfaces/aerodrome/INonfungiblePositionManager.sol"; +import { IQuoterV2 } from "../../interfaces/aerodrome/IQuoterV2.sol"; +import { ISwapRouter } from "../../interfaces/aerodrome/ISwapRouter.sol"; +import { ICLPool } from "../../interfaces/aerodrome/ICLPool.sol"; +import { ICLGauge } from "../../interfaces/aerodrome/ICLGauge.sol"; +import { IVault } from "../../interfaces/IVault.sol"; +import { IAMOCallback } from "../../interfaces/aerodrome/IAMOCallback.sol"; +import { IAMOQuoteLoop } from "../../interfaces/aerodrome/IAMOQuoteLoop.sol"; + +import "hardhat/console.sol"; + +contract AerodromeAMOStrategy is InitializableAbstractStrategy, IAMOCallback { + using StableMath for uint256; using SafeERC20 for IERC20; /*************************************** @@ -24,24 +32,39 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { /// @notice tokenId of the liquidity position uint256 public tokenId; - /// @notice amount of liquidity deployed - uint128 public liquidity; - /// @notice TODO is this redundant to liquidity??? + /// @dev Cumulative amount of WETH + OETHb tokens present in Aerodrome Slipstream pool + /// increase liquidity increases this number by the amount of tokens added + /// decrease liquidity decreases this number by the amount of tokens removed uint256 public netValue; /// @notice the swapRouter for performing swaps ISwapRouter public swapRouter; - /// @notice factory for pool creation - ICLFactory public clFactory; /// @notice the pool used ICLPool public clPool; + /// @notice the pool used + ICLGauge public clGauge; /// @notice the liquidity position INonfungiblePositionManager public positionManager; /// @notice helper contract for liquidity and ticker math ISugarHelper public helper; + /// @notice helper contract for liquidity and ticker math + IQuoterV2 public quoter; + /// @notice quote looper needed to utilize quotes + IAMOQuoteLoop public quoteLooper; /// @notice sqrtRatioX96Tick0 uint160 public sqrtRatioX96Tick0; /// @notice sqrtRatioX96Tick1 uint160 public sqrtRatioX96Tick1; + /** + * Specifies WETH to OETHb ratio the strategy contract aims for after rebalancing + * in basis point format. + * + * e.g. 2000 means 20% WETH 80% OETHb + */ + uint256 public poolWethShare; + /** + * Share of liquidity to remove on rebalance + */ + uint128 public withdrawLiquidityShare; /// @dev reserved for inheritance int256[50] private __reserved; @@ -60,32 +83,12 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { /// @notice tick spacing of the pool (set to 1) int24 public immutable tickSpacing; - // Represents a position minted by UniswapV3Strategy contract - struct Position { - // The following two fields are redundant but since we use these - // two quite a lot, think it might be cheaper to store it than - // compute it every time? - uint160 sqrtRatioAX96; - uint160 sqrtRatioBX96; - uint256 netValue; // Last recorded net value of the position - } - - event UniswapV3LiquidityAdded( - uint256 indexed tokenId, - uint256 amount0Sent, - uint256 amount1Sent, - uint128 liquidityMinted + event PoolWethShareUpdated( + uint256 newWethShare ); - event UniswapV3LiquidityRemoved( - uint256 indexed tokenId, - uint256 amount0Received, - uint256 amount1Received, - uint128 liquidityBurned - ); - event UniswapV3PositionMinted( - uint256 indexed tokenId, - int24 lowerTick, - int24 upperTick + + event WithdrawLiqiudityShareUpdated( + uint128 newWithdrawLiquidityShare ); /// @param _stratConfig st @@ -113,7 +116,6 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { * @param _swapRouter Address of the Aerodrome Universal Swap Router * @param _nonfungiblePositionManager Address of position manager to add/remove * the liquidity - * @param _clFactory Address of the concentrated liquidity factory */ function initialize( address[] memory _rewardTokenAddresses, @@ -121,8 +123,11 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { address[] memory _pTokens, address _swapRouter, address _nonfungiblePositionManager, - address _clFactory, - address _sugarHelper + address _clPool, + address _clGauge, + address _quoter, + address _sugarHelper, + address _quoteLooper ) external onlyGovernor initializer { InitializableAbstractStrategy._initialize( _rewardTokenAddresses, @@ -134,26 +139,303 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { positionManager = INonfungiblePositionManager( _nonfungiblePositionManager ); - clFactory = ICLFactory(_clFactory); + clPool = ICLPool(_clPool); + clGauge = ICLGauge(_clGauge); helper = ISugarHelper(_sugarHelper); + quoter = IQuoterV2(_quoter); + quoteLooper = IAMOQuoteLoop(_quoteLooper); sqrtRatioX96Tick0 = helper.getSqrtRatioAtTick(0); sqrtRatioX96Tick1 = helper.getSqrtRatioAtTick(1); } + /*************************************** + Configuration + ****************************************/ + + /** + * @notice Set the new desired WETH share + * @param _amount The new amount specified in basis points + */ + function setPoolWethShare(uint256 _amount) external onlyGovernor { + // TODO tests: + // - governor can update + // - non governor can not update + // - must be within allowed values (event emitted) + + require(_amount < 10000, "Invalid poolWethShare amount"); + + poolWethShare = _amount; + emit PoolWethShareUpdated(_amount); + } + + /** + * @notice Specifies the amount of liquidity that is to be removed when + * a rebalancing happens. + * @param _amount The new amount specified in basis points + */ + function setWithdrawLiquidityShare(uint128 _amount) external onlyGovernor { + // TODO tests: + // - governor can update + // - non governor can not update + // - must be within allowed values (event emitted) + + require(_amount < 10000, "Invalid withdrawLiquidityShare amount"); + + withdrawLiquidityShare = _amount; + emit WithdrawLiqiudityShareUpdated(_amount); + } + + /*************************************** + Strategy overrides + ****************************************/ + /** * @notice Deposit an amount of assets into the platform * @param _asset Address for the asset * @param _amount Units of asset to deposit */ function deposit(address _asset, uint256 _amount) external virtual override { + _deposit(_asset, _amount); + } + + /** + * @dev Deposit an asset into the underlying platform + * @param _asset Address of the asset to deposit + * @param _amount Amount of assets to deposit + */ + function _deposit(address _asset, uint256 _amount) internal { + require(_asset == WETH, "Unsupported asset"); + require(_amount > 0, "Must deposit something"); + emit Deposit(_asset, address(0), _amount); + + // TODO delete this. Just testing you knows?! + _addLiquidity(); + } + + /** + * @notice quote the target pool price after the tokens are swapped + * @dev execute this function optimistically. All state changes are reverted + * @param _amount Amount of asset to swap + * @param _swapWETH when true WETH is being swapped for OETHb + */ + function quotePriceAfterTokenSwap(uint256 _amount, bool _swapWETH) external + returns (uint160, uint160, uint256) + { + try quoteLooper.quoteLoop( + _amount, + _swapWETH + ) {} catch (bytes memory reason) { + + console.log("The reason received"); + return handleRevert(reason); + } + } + + function handleRevert(bytes memory reason) + private + view + returns (uint160 sqrtRatioX96Before, uint160 sqrtPriceX96After, uint256 amountReceived) + { + (uint160 sqrtRatioX96Before,,,,,) = clPool.slot0(); + (amountReceived, sqrtPriceX96After) = parseRevertReason(reason); + console.log("DECODED DATA"); + console.log(amountReceived); + console.log(sqrtPriceX96After); + + return (sqrtRatioX96Before, sqrtPriceX96After, amountReceived); + } + + /// @dev Parses a revert reason that should contain the numeric quote + function parseRevertReason(bytes memory reason) + private + pure + returns (uint256 amount, uint160 sqrtPriceX96After) + { + if (reason.length != 64) { + revert(abi.decode(reason, (string))); + } + return abi.decode(reason, (uint256, uint160)); + } + + + /** + * @dev try/catch can only be used by calling another contract. For that reason this loop + * around is required. Also this function will always revert + */ + function quoteCallback(uint256 _amount, bool _swapWETH) external override { + _removeLiquidity(); + (uint256 amountOut, uint160 sqrtPriceX96After,,) = quoter.quoteExactInputSingle( + IQuoterV2.QuoteExactInputSingleParams({ + tokenIn: _swapWETH ? WETH : OETHb, + tokenOut: _swapWETH ? OETHb : WETH, + amountIn: _amount, + tickSpacing: 1, + // TODO change the thing below + sqrtPriceLimitX96: helper.getSqrtRatioAtTick(1) + }) + ); + + assembly { + let ptr := mload(64) + // encode amountOut in the first 32 bytes of the error message data + mstore(ptr, amountOut) + // encode sqrtPriceX96After in the range of 32 -> 64 bytes of the error message data + mstore(add(ptr, 32), sqrtPriceX96After) + revert(ptr, 64) + } + } + + /** + * @notice Rebalance the pool to the desired token split + */ + function rebalace() external nonReentrant onlyVaultOrGovernorOrStrategist { + _rebalace(); + } + + /** + * @dev Rebalance the pool to the desired token split + */ + function _rebalace() internal { + // TODO remove + _checkLiquidityWithinExpectedShare(); + + _removeLiquidity(); + _swapToDesiredPosition(); + _addLiquidity(); + } + + /** + * @dev Decrease 100% of thex liquidity if strategy holds any. In practice the removal of liquidity + * will be skipped only on the first time called. + */ + function _removeLiquidity() internal { + if (tokenId == 0) { + return; + } + // TODO remove from gauge once we have it + // clGauge.withdraw(tokenId) + + (uint128 liquidity,,) = _getPositionInfo(); + uint128 liqudityToRemove = liquidity * withdrawLiquidityShare / 1e4; + + (uint256 amountWETH, uint256 amountOETHb) = positionManager.decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: tokenId, + liquidity: liqudityToRemove, + /** + * Both expected amounts can be 0 since we don't really care if any swaps + * happen just before the liquidity removal. + */ + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + + positionManager.positions(tokenId); + + positionManager.collect( + INonfungiblePositionManager.CollectParams({ + tokenId: tokenId, + recipient: address(this), + amount0Max: type(uint128).max, // defaults to all tokens owed + amount1Max: type(uint128).max // defaults to all tokens owed + }) + ); + + // TODO can this go negative? + netValue -= amountWETH + amountOETHb; + } + + /** + * @dev Check that the liquidity in the pool is withing the expected WETH to OETHb ratio + */ + function _checkLiquidityWithinExpectedShare() internal { + if (tokenId == 0) { + return; + } + console.log("checking shares"); + + (uint256 amount0, uint256 amount1) = _getPositionPrincipal(); + console.log(amount0); + console.log(amount1); + } + + /** + * @dev Perform a swap so that after the swap the ticker has the desired WETH to OETHb token share. + */ + function _swapToDesiredPosition() internal { + if (tokenId == 0) { + return; + } + + console.log("Swap to desired position?"); + + (uint256 amount0, uint256 amount1) = _getPositionPrincipal(); + console.log(amount0); + console.log(amount1); + } + + /** + * @dev Perform a swap so that after the swap the ticker has the desired WETH to OETHb token share. + */ + function _addLiquidity() internal { + uint256 wethBalance = IERC20(WETH).balanceOf(address(this)); + require(wethBalance > 0, "Must add some WETH"); + + // TODO mint corresponding + //IVault(vaultAddress).mintForStrategy(oethBRequired); + + if (tokenId == 0) { + (uint160 sqrtRatioX96, , , , ,) = clPool.slot0(); + console.log("WHAT IS Up?!?!"); + console.log(sqrtRatioX96); + + // TODO add new token id position minted event + (uint256 mintedTokenId,,uint256 amountWETH, uint256 amountOETHb) = positionManager.mint( + INonfungiblePositionManager.MintParams({ + token0: WETH, + token1: OETHb, + tickSpacing: 1, + tickLower: 0, + tickUpper: 1, + amount0Desired: wethBalance, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: block.timestamp, + // needs to be 0 because the pool is already created + // non zero amount attempts to create a new instance of the pool + sqrtPriceX96: 0 + }) + ); + tokenId = mintedTokenId; + // TODO add incerase liquidity event + netValue += amountWETH + amountOETHb; + } else { + + (,uint256 amountWETH, uint256 amountOETHb) = positionManager.increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: tokenId, + amount0Desired: wethBalance, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + // TODO add incerase liquidity event + netValue += amountWETH + amountOETHb; + } } /** * @notice Deposit all supported assets in this strategy contract to the platform */ function depositAll() external virtual override { - + _deposit(WETH, IERC20(WETH).balanceOf(address(this))); } /** @@ -176,7 +458,6 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { * sends to the OToken's Vault. */ function withdrawAll() external virtual override { - } @@ -243,7 +524,6 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { override returns (uint256) { - // TODO verify return netValue; } @@ -269,7 +549,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { * @return amount0 Amount of token0 in position * @return amount1 Amount of token1 in position */ - function getPositionPrincipal() + function _getPositionPrincipal() internal view returns (uint256 amount0, uint256 amount1) @@ -282,6 +562,19 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { ); } + function _getPositionInfo() + internal + returns ( + uint128 liquidity, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) { + + if (tokenId > 0) { + (,,,,,,,liquidity,,,tokensOwed0,tokensOwed1) = positionManager.positions(tokenId); + } + } + /** * @dev Returns the fees in a given position * TODO: test this, should return 0 since we don't earn fees @@ -315,56 +608,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { revert("Unsupported method"); } - /** - * @notice Mints a new position on the pool and provides liquidity to it - * - * @param _desiredAmount0 Desired amount of token0 to provide liquidity - * @param _desiredAmount1 Desired amount of token1 to provide liquidity - * @param _minAmount0 Min amount of token0 to deposit - * @param _minAmount1 Min amount of token1 to deposit - */ - function _mintPosition( - uint256 _desiredAmount0, - uint256 _desiredAmount1, - uint256 _minAmount0, - uint256 _minAmount1, - int24 _lowerTick, - int24 _upperTick - ) - internal - { - INonfungiblePositionManager.MintParams - memory params = INonfungiblePositionManager.MintParams({ - // TODO: we need to figure which token is smaller when it comes to addresses: - // https://github.com/velodrome-finance/slipstream/blob/87e4aae8143a2f3a800b8e1ef8c58d9b807caf4e/contracts/core/CLFactory.sol#L73 - - token0: WETH, - token1: OETHb, - tickSpacing: tickSpacing, - tickLower: _lowerTick, - tickUpper: _upperTick, - amount0Desired: _desiredAmount0, - amount1Desired: _desiredAmount1, - amount0Min: _minAmount0, - amount1Min: _minAmount1, - recipient: address(this), - deadline: block.timestamp, - /* Sets the initial price to 1 meaning we deposit only OETHb into the pool - * after that we swap 20% of it to WETH and burn the resulting OETHb - */ - sqrtPriceX96: sqrtRatioX96Tick0 - }); - - (uint256 mintedTokenId, uint128 mintedLiquidity, uint256 amount0, uint256 amount1) = positionManager.mint(params); - - tokenId = mintedTokenId; - liquidity = mintedLiquidity; - netValue = amount0 + amount1; - // TODO fetch the pool address - - //emit UniswapV3PositionMinted(mintedTokenId, lowerTick, upperTick); - //emit UniswapV3LiquidityAdded(mintedTokenId, amount0, amount1, liquidity); - } + /** * @dev Swaps one token for other and then provides liquidity to pools. diff --git a/contracts/contracts/strategies/aerodrome/AerodromeQuoteLoop.sol b/contracts/contracts/strategies/aerodrome/AerodromeQuoteLoop.sol new file mode 100644 index 0000000000..5b9c21c49f --- /dev/null +++ b/contracts/contracts/strategies/aerodrome/AerodromeQuoteLoop.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title Aerodrome AMO Quote looper + * @author Origin Protocol Inc + */ +import { IAMOCallback } from "../../interfaces/aerodrome/IAMOCallback.sol"; +import { IAMOQuoteLoop } from "../../interfaces/aerodrome/IAMOQuoteLoop.sol"; + +contract AerodromeQuoteLoop is IAMOQuoteLoop { + /** + * + */ + function quoteLoop(uint256 _amount, bool _swapWETH) external override { + IAMOCallback(msg.sender).quoteCallback(_amount, _swapWETH); + } +} diff --git a/contracts/contracts/utils/InitializableAbstractStrategy.sol b/contracts/contracts/utils/InitializableAbstractStrategy.sol index 19bb0df836..1888288f39 100644 --- a/contracts/contracts/utils/InitializableAbstractStrategy.sol +++ b/contracts/contracts/utils/InitializableAbstractStrategy.sol @@ -172,7 +172,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { ); _; } - + /** * @notice Set the reward token addresses. Any old addresses will be overwritten. * @param _rewardTokenAddresses Array of reward token addresses diff --git a/contracts/deploy/base/004_base_amo_strategy.js b/contracts/deploy/base/004_base_amo_strategy.js index 43724bf94f..5af359fc70 100644 --- a/contracts/deploy/base/004_base_amo_strategy.js +++ b/contracts/deploy/base/004_base_amo_strategy.js @@ -14,8 +14,13 @@ module.exports = deployOnBaseWithGuardian( 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 + ); await deployWithConfirmation("AerodromeAMOStrategyProxy"); + await deployWithConfirmation("AerodromeQuoteLoop"); await deployWithConfirmation("AerodromeAMOStrategy", [ /* The pool address is not yet known. Might be created before we deploy the * strategy or after. @@ -26,34 +31,59 @@ module.exports = deployOnBaseWithGuardian( ]); const cAMOStrategyProxy = await ethers.getContract("AerodromeAMOStrategyProxy"); - const cAMOStrategy = await ethers.getContract("AerodromeAMOStrategy"); + const cAMOStrategyImpl = await ethers.getContract("AerodromeAMOStrategy"); + const cAMOStrategy = await ethers.getContractAt("AerodromeAMOStrategy", cAMOStrategyProxy.address); + const cAMOQuoteLooper = await ethers.getContract("AerodromeQuoteLoop"); console.log("Deployed AMO strategy and proxy contracts"); // Init the AMO strategy - const initData = cAMOStrategy.interface.encodeFunctionData( - "initialize(address[],address[],address[],address,address,address,address)", + const initData = cAMOStrategyImpl.interface.encodeFunctionData( + "initialize(address[],address[],address[],address,address,address,address,address,address,address)", [ [addresses.base.AERO], // rewardTokenAddresses [], // assets [], // pTokens addresses.base.universalSwapRouter, // swapRouter addresses.base.nonFungiblePositionManager, // nonfungiblePositionManager - addresses.base.poolFactory, // clFactory - addresses.base.sugarHelper // sugarHelper + addresses.base.aerodromeOETHbWETHClPool, // clOETHbWethPool + addresses.zero, // clOETHbWethGauge + addresses.base.quoterV2, + addresses.base.sugarHelper, // sugarHelper + cAMOQuoteLooper.address // quote looper ] ); // prettier-ignore await withConfirmation( cAMOStrategyProxy .connect(sDeployer)["initialize(address,address,bytes)"]( - cAMOStrategy.address, + cAMOStrategyImpl.address, deployerAddr, initData ) ); console.log("Initialized cAMOStrategyProxy and implementation"); + await withConfirmation( + cAMOStrategy + .connect(sDeployer) + .setPoolWethShare(2000) // 20% + ); + + await withConfirmation( + cAMOStrategy + .connect(sDeployer) + .setWithdrawLiquidityShare(9900) // 99% + ); + + await withConfirmation( + cAMOStrategy + .connect(sDeployer) + .safeApproveAllTokens() + ); + + console.log("AMOStrategy configured"); + // Transfer ownership await withConfirmation( cAMOStrategyProxy.connect(sDeployer).transferGovernance(governorAddr) @@ -67,6 +97,12 @@ module.exports = deployOnBaseWithGuardian( contract: cAMOStrategyProxy, signature: "claimGovernance()", args: [], + }, + { + // 2. Approve the AMO strategy on the Vault + contract: cOETHbVault, + signature: "approveStrategy(address)", + args: [cAMOStrategyProxy.address], } ], }; diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index c76d8e8408..09c619bf6a 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -49,6 +49,10 @@ const defaultBaseFixture = deployments.createFixture(async () => { oethbVaultProxy.address ); + // Aerodrome AMO Strategy + const aerodromeAmoStrategyProxy = await ethers.getContract("AerodromeAMOStrategyProxy"); + const aerodromeAmoStrategy = await ethers.getContractAt("AerodromeAMOStrategy", aerodromeAmoStrategyProxy.address); + // Bridged wOETH const woethProxy = await ethers.getContract("BridgedBaseWOETHProxy"); const woeth = await ethers.getContractAt("BridgedWOETH", woethProxy.address); @@ -63,6 +67,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { const governor = await ethers.getSigner(governorAddr); const woethGovernor = await ethers.getSigner(await woethProxy.governor()); + // Make sure we can print bridged WOETH for tests if (isBaseFork) { await impersonateAndFund(woethGovernor.address); @@ -78,9 +83,15 @@ const defaultBaseFixture = deployments.createFixture(async () => { await woeth.connect(woethGovernor).grantRole(MINTER_ROLE, minter.address); await woeth.connect(woethGovernor).grantRole(BURNER_ROLE, burner.address); - // Mint some bridged WOETH - await woeth.connect(minter).mint(rafael.address, oethUnits("1")); - await woeth.connect(minter).mint(nick.address, oethUnits("1")); + for (const user of [rafael, nick]) { + // Mint some bridged WOETH + await woeth.connect(minter).mint(user.address, oethUnits("1")); + await weth.connect(user).deposit({ value: oethUnits("10") }); + + // Set allowance on the vault + await weth.connect(user).approve(oethbVault.address, oethUnits("10")); + } + await woeth.connect(minter).mint(woethGovernor.address, oethUnits("1")); // Governor opts in for rebasing @@ -96,6 +107,9 @@ const defaultBaseFixture = deployments.createFixture(async () => { woeth, woethProxy, + // Strategies + aerodromeAmoStrategy, + // WETH weth, @@ -118,7 +132,6 @@ mocha.after(async () => { module.exports = { defaultBaseFixture, - MINTER_ROLE, BURNER_ROLE, }; diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js new file mode 100644 index 0000000000..e967d23c67 --- /dev/null +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -0,0 +1,59 @@ +const { createFixtureLoader } = require("../_fixture"); +const { + defaultBaseFixture, +} = require("../_fixture-base"); +//const { expect } = require("chai"); +const { oethUnits } = require("../helpers"); + +const baseFixture = createFixtureLoader(defaultBaseFixture); + +describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { + let fixture, oethbVault, weth, aerodromeAmoStrategy, governor; + + beforeEach(async () => { + fixture = await baseFixture(); + weth = fixture.weth; + oethbVault = fixture.oethbVault; + aerodromeAmoStrategy = fixture.aerodromeAmoStrategy; + governor = fixture.governor; + }); + + it("Should be able to deposit to the pool", async () => { + const { rafael } = fixture; + await mintAndDeposit(rafael); + await mintAndDeposit(rafael); + }); + + it.only("Should be able to quote the amount required", async () => { + const { aerodromeAmoStrategy, rafael } = fixture; + + await mintAndDeposit(rafael); + + const result = await aerodromeAmoStrategy + .connect(rafael) + .quotePriceAfterTokenSwap(oethUnits("0.00003"), false); + + console.log("result"); + // In the trace result we get the corre + console.log(result); + + }); + + const mintAndDeposit = async (user) => { + await oethbVault + .connect(user) + .mint( + weth.address, + oethUnits("5"), + oethUnits("5") + ); + + await oethbVault + .connect(governor) + .depositToStrategy( + aerodromeAmoStrategy.address, + [weth.address], + [oethUnits("5")], + ); + }; +}); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 2aaa22688c..4cf978e026 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -305,8 +305,10 @@ addresses.base.BridgedWOETHOracleFeed = // Base Aerodrome addresses.base.nonFungiblePositionManager = "0x827922686190790b37229fd06084350E74485b72"; addresses.base.poolFactory = "0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A"; +addresses.base.aerodromeOETHbWETHClPool = "0x6446021F4E396dA3df4235C62537431372195D38"; addresses.base.universalSwapRouter = "0x6Cb442acF35158D5eDa88fe602221b67B400Be3E"; addresses.base.sugarHelper = "0x0AD09A66af0154a84e86F761313d02d0abB6edd5"; +addresses.base.quoterV2 = "0x254cF9E1E6e233aa1AC962CB9B05b2cfeAaE15b0"; // Holesky addresses.holesky.WETH = "0x94373a4919B3240D86eA41593D5eBa789FEF3848"; From 3b7bd5fdc20e571b42b6d690599b17e8e1f23cf0 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 12 Aug 2024 01:46:50 +0200 Subject: [PATCH 3/9] add strategist and simplifty the amo code --- .../interfaces/aerodrome/IAMOCallback.sol | 6 -- .../interfaces/aerodrome/IAMOQuoteLoop.sol | 6 -- .../aerodrome/AerodromeAMOStrategy.sol | 87 ++----------------- .../aerodrome/AerodromeQuoteLoop.sol | 18 ---- .../deploy/base/004_base_amo_strategy.js | 15 ++-- contracts/hardhat.config.js | 8 +- contracts/test/_fixture-base.js | 4 +- .../aerodrome-amo.base.fork-test.js | 27 +++--- contracts/utils/addresses.js | 1 + 9 files changed, 37 insertions(+), 135 deletions(-) delete mode 100644 contracts/contracts/interfaces/aerodrome/IAMOCallback.sol delete mode 100644 contracts/contracts/interfaces/aerodrome/IAMOQuoteLoop.sol delete mode 100644 contracts/contracts/strategies/aerodrome/AerodromeQuoteLoop.sol diff --git a/contracts/contracts/interfaces/aerodrome/IAMOCallback.sol b/contracts/contracts/interfaces/aerodrome/IAMOCallback.sol deleted file mode 100644 index 2a9474c124..0000000000 --- a/contracts/contracts/interfaces/aerodrome/IAMOCallback.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.8.0; - -interface IAMOCallback { - function quoteCallback(uint256 _amount, bool _swapWETH) external; -} \ No newline at end of file diff --git a/contracts/contracts/interfaces/aerodrome/IAMOQuoteLoop.sol b/contracts/contracts/interfaces/aerodrome/IAMOQuoteLoop.sol deleted file mode 100644 index 3e8b759a96..0000000000 --- a/contracts/contracts/interfaces/aerodrome/IAMOQuoteLoop.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.8.0; - -interface IAMOQuoteLoop { - function quoteLoop(uint256 _amount, bool _swapWETH) external; -} \ No newline at end of file diff --git a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index 188a0fee0a..194b3ad849 100644 --- a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -17,12 +17,10 @@ import { ISwapRouter } from "../../interfaces/aerodrome/ISwapRouter.sol"; import { ICLPool } from "../../interfaces/aerodrome/ICLPool.sol"; import { ICLGauge } from "../../interfaces/aerodrome/ICLGauge.sol"; import { IVault } from "../../interfaces/IVault.sol"; -import { IAMOCallback } from "../../interfaces/aerodrome/IAMOCallback.sol"; -import { IAMOQuoteLoop } from "../../interfaces/aerodrome/IAMOQuoteLoop.sol"; import "hardhat/console.sol"; -contract AerodromeAMOStrategy is InitializableAbstractStrategy, IAMOCallback { +contract AerodromeAMOStrategy is InitializableAbstractStrategy { using StableMath for uint256; using SafeERC20 for IERC20; @@ -48,8 +46,6 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy, IAMOCallback { ISugarHelper public helper; /// @notice helper contract for liquidity and ticker math IQuoterV2 public quoter; - /// @notice quote looper needed to utilize quotes - IAMOQuoteLoop public quoteLooper; /// @notice sqrtRatioX96Tick0 uint160 public sqrtRatioX96Tick0; /// @notice sqrtRatioX96Tick1 @@ -126,8 +122,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy, IAMOCallback { address _clPool, address _clGauge, address _quoter, - address _sugarHelper, - address _quoteLooper + address _sugarHelper ) external onlyGovernor initializer { InitializableAbstractStrategy._initialize( _rewardTokenAddresses, @@ -143,7 +138,6 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy, IAMOCallback { clGauge = ICLGauge(_clGauge); helper = ISugarHelper(_sugarHelper); quoter = IQuoterV2(_quoter); - quoteLooper = IAMOQuoteLoop(_quoteLooper); sqrtRatioX96Tick0 = helper.getSqrtRatioAtTick(0); sqrtRatioX96Tick1 = helper.getSqrtRatioAtTick(1); } @@ -212,80 +206,6 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy, IAMOCallback { _addLiquidity(); } - /** - * @notice quote the target pool price after the tokens are swapped - * @dev execute this function optimistically. All state changes are reverted - * @param _amount Amount of asset to swap - * @param _swapWETH when true WETH is being swapped for OETHb - */ - function quotePriceAfterTokenSwap(uint256 _amount, bool _swapWETH) external - returns (uint160, uint160, uint256) - { - try quoteLooper.quoteLoop( - _amount, - _swapWETH - ) {} catch (bytes memory reason) { - - console.log("The reason received"); - return handleRevert(reason); - } - } - - function handleRevert(bytes memory reason) - private - view - returns (uint160 sqrtRatioX96Before, uint160 sqrtPriceX96After, uint256 amountReceived) - { - (uint160 sqrtRatioX96Before,,,,,) = clPool.slot0(); - (amountReceived, sqrtPriceX96After) = parseRevertReason(reason); - - console.log("DECODED DATA"); - console.log(amountReceived); - console.log(sqrtPriceX96After); - - return (sqrtRatioX96Before, sqrtPriceX96After, amountReceived); - } - - /// @dev Parses a revert reason that should contain the numeric quote - function parseRevertReason(bytes memory reason) - private - pure - returns (uint256 amount, uint160 sqrtPriceX96After) - { - if (reason.length != 64) { - revert(abi.decode(reason, (string))); - } - return abi.decode(reason, (uint256, uint160)); - } - - - /** - * @dev try/catch can only be used by calling another contract. For that reason this loop - * around is required. Also this function will always revert - */ - function quoteCallback(uint256 _amount, bool _swapWETH) external override { - _removeLiquidity(); - (uint256 amountOut, uint160 sqrtPriceX96After,,) = quoter.quoteExactInputSingle( - IQuoterV2.QuoteExactInputSingleParams({ - tokenIn: _swapWETH ? WETH : OETHb, - tokenOut: _swapWETH ? OETHb : WETH, - amountIn: _amount, - tickSpacing: 1, - // TODO change the thing below - sqrtPriceLimitX96: helper.getSqrtRatioAtTick(1) - }) - ); - - assembly { - let ptr := mload(64) - // encode amountOut in the first 32 bytes of the error message data - mstore(ptr, amountOut) - // encode sqrtPriceX96After in the range of 32 -> 64 bytes of the error message data - mstore(add(ptr, 32), sqrtPriceX96After) - revert(ptr, 64) - } - } - /** * @notice Rebalance the pool to the desired token split */ @@ -429,6 +349,9 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy, IAMOCallback { // TODO add incerase liquidity event netValue += amountWETH + amountOETHb; } + + // TODO remove from gauge once we have it + // clGauge.deposit(tokenId) } /** diff --git a/contracts/contracts/strategies/aerodrome/AerodromeQuoteLoop.sol b/contracts/contracts/strategies/aerodrome/AerodromeQuoteLoop.sol deleted file mode 100644 index 5b9c21c49f..0000000000 --- a/contracts/contracts/strategies/aerodrome/AerodromeQuoteLoop.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -/** - * @title Aerodrome AMO Quote looper - * @author Origin Protocol Inc - */ -import { IAMOCallback } from "../../interfaces/aerodrome/IAMOCallback.sol"; -import { IAMOQuoteLoop } from "../../interfaces/aerodrome/IAMOQuoteLoop.sol"; - -contract AerodromeQuoteLoop is IAMOQuoteLoop { - /** - * - */ - function quoteLoop(uint256 _amount, bool _swapWETH) external override { - IAMOCallback(msg.sender).quoteCallback(_amount, _swapWETH); - } -} diff --git a/contracts/deploy/base/004_base_amo_strategy.js b/contracts/deploy/base/004_base_amo_strategy.js index 5af359fc70..04217ec4af 100644 --- a/contracts/deploy/base/004_base_amo_strategy.js +++ b/contracts/deploy/base/004_base_amo_strategy.js @@ -20,7 +20,6 @@ module.exports = deployOnBaseWithGuardian( ); await deployWithConfirmation("AerodromeAMOStrategyProxy"); - await deployWithConfirmation("AerodromeQuoteLoop"); await deployWithConfirmation("AerodromeAMOStrategy", [ /* The pool address is not yet known. Might be created before we deploy the * strategy or after. @@ -33,13 +32,12 @@ module.exports = deployOnBaseWithGuardian( const cAMOStrategyProxy = await ethers.getContract("AerodromeAMOStrategyProxy"); const cAMOStrategyImpl = await ethers.getContract("AerodromeAMOStrategy"); const cAMOStrategy = await ethers.getContractAt("AerodromeAMOStrategy", cAMOStrategyProxy.address); - const cAMOQuoteLooper = await ethers.getContract("AerodromeQuoteLoop"); console.log("Deployed AMO strategy and proxy contracts"); // Init the AMO strategy const initData = cAMOStrategyImpl.interface.encodeFunctionData( - "initialize(address[],address[],address[],address,address,address,address,address,address,address)", + "initialize(address[],address[],address[],address,address,address,address,address,address)", [ [addresses.base.AERO], // rewardTokenAddresses [], // assets @@ -49,8 +47,7 @@ module.exports = deployOnBaseWithGuardian( addresses.base.aerodromeOETHbWETHClPool, // clOETHbWethPool addresses.zero, // clOETHbWethGauge addresses.base.quoterV2, - addresses.base.sugarHelper, // sugarHelper - cAMOQuoteLooper.address // quote looper + addresses.base.sugarHelper // sugarHelper ] ); // prettier-ignore @@ -103,7 +100,13 @@ module.exports = deployOnBaseWithGuardian( contract: cOETHbVault, signature: "approveStrategy(address)", args: [cAMOStrategyProxy.address], - } + }, + { + // 3. Set strategist address + contract: cOETHbVault, + signature: "setStrategistAddr(address)", + args: [addresses.base.strategist], + }, ], }; } diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index 607e192e99..35739c1d1a 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -48,6 +48,7 @@ const MAINNET_STRATEGIST = "0xf14bbdf064e3f67f51cd9bd646ae3716ad938fdc"; const HOLESKY_DEPLOYER = "0x1b94CA50D3Ad9f8368851F8526132272d1a5028C"; const BASE_DEPLOYER = MAINNET_DEPLOYER; const BASE_GOVERNOR = "0x92A19381444A001d62cE67BaFF066fA1111d7202"; +const BASE_STRATEGIST = "0x28bce2eE5775B652D92bB7c2891A89F036619703"; const mnemonic = "replace hover unaware super where filter stone fine garlic address matrix basic"; @@ -293,18 +294,21 @@ module.exports = { process.env.FORK === "true" ? isHoleskyFork ? HOLESKY_DEPLOYER + : isBaseFork + ? BASE_STRATEGIST : MAINNET_STRATEGIST : 0, hardhat: process.env.FORK === "true" ? isHoleskyFork ? HOLESKY_DEPLOYER + : isBaseFork + ? BASE_STRATEGIST : MAINNET_STRATEGIST : 0, mainnet: MAINNET_STRATEGIST, holesky: HOLESKY_DEPLOYER, // on Holesky the deployer is also the strategist - // Base has no strategist - base: BASE_GOVERNOR, + base: BASE_STRATEGIST, }, }, contractSizer: { diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 09c619bf6a..d5d1ce842d 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -63,8 +63,9 @@ const defaultBaseFixture = deployments.createFixture(async () => { const signers = await hre.ethers.getSigners(); const [minter, burner, rafael, nick] = signers.slice(4); // Skip first 4 addresses to avoid conflict - const { governorAddr } = await getNamedAccounts(); + const { governorAddr, strategistAddr } = await getNamedAccounts(); const governor = await ethers.getSigner(governorAddr); + const strategist = await ethers.getSigner(strategistAddr); const woethGovernor = await ethers.getSigner(await woethProxy.governor()); @@ -115,6 +116,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { // Signers governor, + strategist, woethGovernor, minter, burner, diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index e967d23c67..42f6cbace5 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -7,8 +7,8 @@ const { oethUnits } = require("../helpers"); const baseFixture = createFixtureLoader(defaultBaseFixture); -describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { - let fixture, oethbVault, weth, aerodromeAmoStrategy, governor; +describe.only("ForkTest: Aerodrome AMO Strategy (Base)", function () { + let fixture, oethbVault, weth, aerodromeAmoStrategy, governor, strategist, rafael; beforeEach(async () => { fixture = await baseFixture(); @@ -21,25 +21,24 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", function () { it("Should be able to deposit to the pool", async () => { const { rafael } = fixture; await mintAndDeposit(rafael); - await mintAndDeposit(rafael); }); - it.only("Should be able to quote the amount required", async () => { - const { aerodromeAmoStrategy, rafael } = fixture; - + it("Should be able to deposit to the pool & rebalance", async () => { + const { rafael } = fixture; await mintAndDeposit(rafael); - const result = await aerodromeAmoStrategy - .connect(rafael) - .quotePriceAfterTokenSwap(oethUnits("0.00003"), false); + }); - console.log("result"); - // In the trace result we get the corre - console.log(result); + const rebalance = async (user) => { + await oethbVault + .connect(strategist) + .rebalace( + ); + } - }); + const mintAndDeposit = async (userOverride) => { + const user = userOverride || rafael; - const mintAndDeposit = async (user) => { await oethbVault .connect(user) .mint( diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 4cf978e026..b14e6caccc 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -297,6 +297,7 @@ addresses.base.AERO = "0x940181a94a35a4569e4529a3cdfb74e38fd98631"; addresses.base.wethAeroPoolAddress = "0x80aBe24A3ef1fc593aC5Da960F232ca23B2069d0"; addresses.base.governor = "0x92A19381444A001d62cE67BaFF066fA1111d7202"; +addresses.base.strategist = "0x28bce2eE5775B652D92bB7c2891A89F036619703"; // Chainlink: https://data.chain.link/feeds/base/base/woeth-oeth-exchange-rate addresses.base.BridgedWOETHOracleFeed = From 18d3e1164440c9349b1e9b23a317ac616616aba3 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 12 Aug 2024 02:27:06 +0200 Subject: [PATCH 4/9] for testing purposes create a gauge in the fixture --- .../aerodrome/AerodromeAMOStrategy.sol | 7 ++++ contracts/test/_fixture-base.js | 36 +++++++++++++++++++ .../test/abi/aerodromeSlipstreamPool.json | 1 + contracts/test/abi/aerodromeVoter.json | 1 + contracts/utils/addresses.js | 2 +- 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 contracts/test/abi/aerodromeSlipstreamPool.json create mode 100644 contracts/test/abi/aerodromeVoter.json diff --git a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index 194b3ad849..5ca58d354b 100644 --- a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -146,6 +146,13 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { Configuration ****************************************/ + /** + * TODO: delete once we get the gauge + */ + function setGauge(address _clGauge) external onlyGovernor { + clGauge = ICLGauge(_clGauge); + } + /** * @notice Set the new desired WETH share * @param _amount The new amount specified in basis points diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index d5d1ce842d..8d4021a4b4 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -8,6 +8,9 @@ const addresses = require("../utils/addresses"); const log = require("../utils/logger")("test:fixtures-arb"); +const aeroVoterAbi = require("./abi/aerodromeVoter.json"); +const slipstreamPoolAbi = require("./abi/aerodromeSlipstreamPool.json") + const MINTER_ROLE = "0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6"; const BURNER_ROLE = @@ -98,6 +101,9 @@ const defaultBaseFixture = deployments.createFixture(async () => { // Governor opts in for rebasing await oethb.connect(governor).rebaseOptIn(); + // TODO delete once we have gauge on the mainnet + await setupAerodromeOEthbWETHGauge(oethb.address, aerodromeAmoStrategy, governor); + return { // OETHb oethb, @@ -126,6 +132,36 @@ const defaultBaseFixture = deployments.createFixture(async () => { }; }); +/** + * This is needed only as long as the gauge isn't created on the base mainnet + */ +const setupAerodromeOEthbWETHGauge = async (oethbAddress, aerodromeAmoStrategy, governor) => { + const voter = await ethers.getContractAt(aeroVoterAbi, addresses.base.aeroVoterAddress); + const amoPool = await ethers.getContractAt(slipstreamPoolAbi, addresses.base.aerodromeOETHbWETHClPool); + + const aeroGaugeSigner = await impersonateAndFund(addresses.base.aeroGaugeGovernorAddress); + + // whitelist OETHb + await voter + .connect(aeroGaugeSigner) + .whitelistToken( + oethbAddress, + true + ); + + // create a gauge + await voter + .connect(aeroGaugeSigner) + .createGauge( + addresses.base.slipstreamPoolFactory, + addresses.base.aerodromeOETHbWETHClPool + ); + + await aerodromeAmoStrategy + .connect(governor) + .setGauge(await amoPool.gauge()); +}; + mocha.after(async () => { if (snapshotId) { await nodeRevert(snapshotId); diff --git a/contracts/test/abi/aerodromeSlipstreamPool.json b/contracts/test/abi/aerodromeSlipstreamPool.json new file mode 100644 index 0000000000..3b6d3b7147 --- /dev/null +++ b/contracts/test/abi/aerodromeSlipstreamPool.json @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"int24","name":"tickLower","type":"int24"},{"indexed":true,"internalType":"int24","name":"tickUpper","type":"int24"},{"indexed":false,"internalType":"uint128","name":"amount","type":"uint128"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":true,"internalType":"int24","name":"tickLower","type":"int24"},{"indexed":true,"internalType":"int24","name":"tickUpper","type":"int24"},{"indexed":false,"internalType":"uint128","name":"amount0","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"amount1","type":"uint128"}],"name":"Collect","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint128","name":"amount0","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"amount1","type":"uint128"}],"name":"CollectFees","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"paid0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"paid1","type":"uint256"}],"name":"Flash","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint16","name":"observationCardinalityNextOld","type":"uint16"},{"indexed":false,"internalType":"uint16","name":"observationCardinalityNextNew","type":"uint16"}],"name":"IncreaseObservationCardinalityNext","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"indexed":false,"internalType":"int24","name":"tick","type":"int24"}],"name":"Initialize","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"int24","name":"tickLower","type":"int24"},{"indexed":true,"internalType":"int24","name":"tickUpper","type":"int24"},{"indexed":false,"internalType":"uint128","name":"amount","type":"uint128"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"feeProtocol0Old","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"feeProtocol1Old","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"feeProtocol0New","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"feeProtocol1New","type":"uint8"}],"name":"SetFeeProtocol","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"int256","name":"amount0","type":"int256"},{"indexed":false,"internalType":"int256","name":"amount1","type":"int256"},{"indexed":false,"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"indexed":false,"internalType":"uint128","name":"liquidity","type":"uint128"},{"indexed":false,"internalType":"int24","name":"tick","type":"int24"}],"name":"Swap","type":"event"},{"inputs":[{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint128","name":"amount","type":"uint128"},{"internalType":"address","name":"owner","type":"address"}],"name":"burn","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint128","name":"amount","type":"uint128"}],"name":"burn","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint128","name":"amount0Requested","type":"uint128"},{"internalType":"uint128","name":"amount1Requested","type":"uint128"},{"internalType":"address","name":"owner","type":"address"}],"name":"collect","outputs":[{"internalType":"uint128","name":"amount0","type":"uint128"},{"internalType":"uint128","name":"amount1","type":"uint128"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint128","name":"amount0Requested","type":"uint128"},{"internalType":"uint128","name":"amount1Requested","type":"uint128"}],"name":"collect","outputs":[{"internalType":"uint128","name":"amount0","type":"uint128"},{"internalType":"uint128","name":"amount1","type":"uint128"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"collectFees","outputs":[{"internalType":"uint128","name":"amount0","type":"uint128"},{"internalType":"uint128","name":"amount1","type":"uint128"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"factoryRegistry","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeGrowthGlobal0X128","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeGrowthGlobal1X128","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"flash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"gauge","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"gaugeFees","outputs":[{"internalType":"uint128","name":"token0","type":"uint128"},{"internalType":"uint128","name":"token1","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint256","name":"_rewardGrowthGlobalX128","type":"uint256"}],"name":"getRewardGrowthInside","outputs":[{"internalType":"uint256","name":"rewardGrowthInside","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint16","name":"observationCardinalityNext","type":"uint16"}],"name":"increaseObservationCardinalityNext","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_token0","type":"address"},{"internalType":"address","name":"_token1","type":"address"},{"internalType":"int24","name":"_tickSpacing","type":"int24"},{"internalType":"address","name":"_factoryRegistry","type":"address"},{"internalType":"uint160","name":"_sqrtPriceX96","type":"uint160"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"lastUpdated","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"liquidity","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxLiquidityPerTick","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint128","name":"amount","type":"uint128"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"mint","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"nft","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"observations","outputs":[{"internalType":"uint32","name":"blockTimestamp","type":"uint32"},{"internalType":"int56","name":"tickCumulative","type":"int56"},{"internalType":"uint160","name":"secondsPerLiquidityCumulativeX128","type":"uint160"},{"internalType":"bool","name":"initialized","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32[]","name":"secondsAgos","type":"uint32[]"}],"name":"observe","outputs":[{"internalType":"int56[]","name":"tickCumulatives","type":"int56[]"},{"internalType":"uint160[]","name":"secondsPerLiquidityCumulativeX128s","type":"uint160[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"periodFinish","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"positions","outputs":[{"internalType":"uint128","name":"liquidity","type":"uint128"},{"internalType":"uint256","name":"feeGrowthInside0LastX128","type":"uint256"},{"internalType":"uint256","name":"feeGrowthInside1LastX128","type":"uint256"},{"internalType":"uint128","name":"tokensOwed0","type":"uint128"},{"internalType":"uint128","name":"tokensOwed1","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardGrowthGlobalX128","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardReserve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rollover","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_gauge","type":"address"},{"internalType":"address","name":"_nft","type":"address"}],"name":"setGaugeAndPositionManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"slot0","outputs":[{"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"internalType":"int24","name":"tick","type":"int24"},{"internalType":"uint16","name":"observationIndex","type":"uint16"},{"internalType":"uint16","name":"observationCardinality","type":"uint16"},{"internalType":"uint16","name":"observationCardinalityNext","type":"uint16"},{"internalType":"bool","name":"unlocked","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"}],"name":"snapshotCumulativesInside","outputs":[{"internalType":"int56","name":"tickCumulativeInside","type":"int56"},{"internalType":"uint160","name":"secondsPerLiquidityInsideX128","type":"uint160"},{"internalType":"uint32","name":"secondsInside","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"int128","name":"stakedLiquidityDelta","type":"int128"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"bool","name":"positionUpdate","type":"bool"}],"name":"stake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stakedLiquidity","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"bool","name":"zeroForOne","type":"bool"},{"internalType":"int256","name":"amountSpecified","type":"int256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"swap","outputs":[{"internalType":"int256","name":"amount0","type":"int256"},{"internalType":"int256","name":"amount1","type":"int256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_rewardRate","type":"uint256"},{"internalType":"uint256","name":"_rewardReserve","type":"uint256"},{"internalType":"uint256","name":"_periodFinish","type":"uint256"}],"name":"syncReward","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int16","name":"","type":"int16"}],"name":"tickBitmap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"tickSpacing","outputs":[{"internalType":"int24","name":"","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"int24","name":"","type":"int24"}],"name":"ticks","outputs":[{"internalType":"uint128","name":"liquidityGross","type":"uint128"},{"internalType":"int128","name":"liquidityNet","type":"int128"},{"internalType":"int128","name":"stakedLiquidityNet","type":"int128"},{"internalType":"uint256","name":"feeGrowthOutside0X128","type":"uint256"},{"internalType":"uint256","name":"feeGrowthOutside1X128","type":"uint256"},{"internalType":"uint256","name":"rewardGrowthOutsideX128","type":"uint256"},{"internalType":"int56","name":"tickCumulativeOutside","type":"int56"},{"internalType":"uint160","name":"secondsPerLiquidityOutsideX128","type":"uint160"},{"internalType":"uint32","name":"secondsOutside","type":"uint32"},{"internalType":"bool","name":"initialized","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token1","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unstakedFee","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"updateRewardsGrowthGlobal","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/contracts/test/abi/aerodromeVoter.json b/contracts/test/abi/aerodromeVoter.json new file mode 100644 index 0000000000..9d4daaf6e2 --- /dev/null +++ b/contracts/test/abi/aerodromeVoter.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_forwarder","type":"address"},{"internalType":"address","name":"_ve","type":"address"},{"internalType":"address","name":"_factoryRegistry","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AlreadyVotedOrDeposited","type":"error"},{"inputs":[],"name":"DistributeWindow","type":"error"},{"inputs":[],"name":"FactoryPathNotApproved","type":"error"},{"inputs":[],"name":"GaugeAlreadyKilled","type":"error"},{"inputs":[],"name":"GaugeAlreadyRevived","type":"error"},{"inputs":[{"internalType":"address","name":"_pool","type":"address"}],"name":"GaugeDoesNotExist","type":"error"},{"inputs":[],"name":"GaugeExists","type":"error"},{"inputs":[{"internalType":"address","name":"_gauge","type":"address"}],"name":"GaugeNotAlive","type":"error"},{"inputs":[],"name":"InactiveManagedNFT","type":"error"},{"inputs":[],"name":"MaximumVotingNumberTooLow","type":"error"},{"inputs":[],"name":"NonZeroVotes","type":"error"},{"inputs":[],"name":"NotAPool","type":"error"},{"inputs":[],"name":"NotApprovedOrOwner","type":"error"},{"inputs":[],"name":"NotEmergencyCouncil","type":"error"},{"inputs":[],"name":"NotGovernor","type":"error"},{"inputs":[],"name":"NotMinter","type":"error"},{"inputs":[],"name":"NotWhitelistedNFT","type":"error"},{"inputs":[],"name":"NotWhitelistedToken","type":"error"},{"inputs":[],"name":"SameValue","type":"error"},{"inputs":[],"name":"SpecialVotingWindow","type":"error"},{"inputs":[],"name":"TooManyPools","type":"error"},{"inputs":[],"name":"UnequalLengths","type":"error"},{"inputs":[],"name":"ZeroAddress","type":"error"},{"inputs":[],"name":"ZeroBalance","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"voter","type":"address"},{"indexed":true,"internalType":"address","name":"pool","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"weight","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"totalWeight","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"Abstained","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"gauge","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"DistributeReward","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"poolFactory","type":"address"},{"indexed":true,"internalType":"address","name":"votingRewardsFactory","type":"address"},{"indexed":true,"internalType":"address","name":"gaugeFactory","type":"address"},{"indexed":false,"internalType":"address","name":"pool","type":"address"},{"indexed":false,"internalType":"address","name":"bribeVotingReward","type":"address"},{"indexed":false,"internalType":"address","name":"feeVotingReward","type":"address"},{"indexed":false,"internalType":"address","name":"gauge","type":"address"},{"indexed":false,"internalType":"address","name":"creator","type":"address"}],"name":"GaugeCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"gauge","type":"address"}],"name":"GaugeKilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"gauge","type":"address"}],"name":"GaugeRevived","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"reward","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"NotifyReward","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"voter","type":"address"},{"indexed":true,"internalType":"address","name":"pool","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"weight","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"totalWeight","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"Voted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"whitelister","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":true,"internalType":"bool","name":"_bool","type":"bool"}],"name":"WhitelistNFT","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"whitelister","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"bool","name":"_bool","type":"bool"}],"name":"WhitelistToken","type":"event"},{"inputs":[{"internalType":"address[]","name":"_bribes","type":"address[]"},{"internalType":"address[][]","name":"_tokens","type":"address[][]"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"claimBribes","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_fees","type":"address[]"},{"internalType":"address[][]","name":"_tokens","type":"address[][]"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"claimFees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_gauges","type":"address[]"}],"name":"claimRewards","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"claimable","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_poolFactory","type":"address"},{"internalType":"address","name":"_pool","type":"address"}],"name":"createGauge","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"uint256","name":"_mTokenId","type":"uint256"}],"name":"depositManaged","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_gauges","type":"address[]"}],"name":"distribute","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_start","type":"uint256"},{"internalType":"uint256","name":"_finish","type":"uint256"}],"name":"distribute","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"emergencyCouncil","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"epochGovernor","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_timestamp","type":"uint256"}],"name":"epochNext","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"_timestamp","type":"uint256"}],"name":"epochStart","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"_timestamp","type":"uint256"}],"name":"epochVoteEnd","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"_timestamp","type":"uint256"}],"name":"epochVoteStart","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"factoryRegistry","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"forwarder","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"gaugeToBribe","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"gaugeToFees","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"gauges","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"governor","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address[]","name":"_tokens","type":"address[]"},{"internalType":"address","name":"_minter","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isAlive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isGauge","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"forwarder","type":"address"}],"name":"isTrustedForwarder","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"isWhitelistedNFT","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isWhitelistedToken","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_gauge","type":"address"}],"name":"killGauge","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"lastVoted","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"length","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxVotingNum","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minter","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"notifyRewardAmount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"poke","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"poolForGauge","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"poolVote","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"pools","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"reset","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_gauge","type":"address"}],"name":"reviveGauge","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_council","type":"address"}],"name":"setEmergencyCouncil","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_epochGovernor","type":"address"}],"name":"setEpochGovernor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_governor","type":"address"}],"name":"setGovernor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxVotingNum","type":"uint256"}],"name":"setMaxVotingNum","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"totalWeight","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_gauge","type":"address"}],"name":"updateFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"start","type":"uint256"},{"internalType":"uint256","name":"end","type":"uint256"}],"name":"updateFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_gauges","type":"address[]"}],"name":"updateFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"usedWeights","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ve","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"address[]","name":"_poolVote","type":"address[]"},{"internalType":"uint256[]","name":"_weights","type":"uint256[]"}],"name":"vote","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"votes","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"weights","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"bool","name":"_bool","type":"bool"}],"name":"whitelistNFT","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"bool","name":"_bool","type":"bool"}],"name":"whitelistToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"withdrawManaged","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index b14e6caccc..84f8e905ec 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -305,7 +305,7 @@ addresses.base.BridgedWOETHOracleFeed = // Base Aerodrome addresses.base.nonFungiblePositionManager = "0x827922686190790b37229fd06084350E74485b72"; -addresses.base.poolFactory = "0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A"; +addresses.base.slipstreamPoolFactory = "0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A"; addresses.base.aerodromeOETHbWETHClPool = "0x6446021F4E396dA3df4235C62537431372195D38"; addresses.base.universalSwapRouter = "0x6Cb442acF35158D5eDa88fe602221b67B400Be3E"; addresses.base.sugarHelper = "0x0AD09A66af0154a84e86F761313d02d0abB6edd5"; From 1594be5e0fd915cc23ee9f2dc6981d31cac9f6a9 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 12 Aug 2024 04:13:41 +0200 Subject: [PATCH 5/9] deposit and withdraw from gauges now works --- .../aerodrome/AerodromeAMOStrategy.sol | 219 +++++++----------- contracts/test/_fixture-base.js | 2 +- .../aerodrome-amo.base.fork-test.js | 12 + 3 files changed, 102 insertions(+), 131 deletions(-) diff --git a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index 5ca58d354b..30528597d0 100644 --- a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -57,6 +57,11 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { * e.g. 2000 means 20% WETH 80% OETHb */ uint256 public poolWethShare; + /** + * TODO: implement setters and tests + * how much variance is allowed (~slippage) when rebalancing the pool + */ + uint256 public poolWethShareVarianceAllowed; /** * Share of liquidity to remove on rebalance */ @@ -79,6 +84,8 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { /// @notice tick spacing of the pool (set to 1) int24 public immutable tickSpacing; + error NotEnoughWethForSwap(uint256 wethBalance, uint256 requiredWeth); // 0x989e5ca8 + event PoolWethShareUpdated( uint256 newWethShare ); @@ -87,6 +94,18 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { uint128 newWithdrawLiquidityShare ); + /** + * @dev Verifies that the caller is the Governor, or Strategist. + */ + modifier onlyGovernorOrStrategist() { + require( + msg.sender == governor() || + msg.sender == IVault(vaultAddress).strategistAddr(), + "Not the Governor or Strategist" + ); + _; + } + /// @param _stratConfig st /// @param _wethAddress Address of the Erc20 WETH Token contract /// @param _OETHbAddress Address of the Erc20 OETHb Token contract @@ -147,7 +166,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { ****************************************/ /** - * TODO: delete once we get the gauge + * TODO: delete once we get the gauge. */ function setGauge(address _clGauge) external onlyGovernor { clGauge = ICLGauge(_clGauge); @@ -200,7 +219,8 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { } /** - * @dev Deposit an asset into the underlying platform + * @dev Deposit WETH to the contract. This function doesn't deposit the liquidity to the + * pool, that is done via the rebalance call. * @param _asset Address of the asset to deposit * @param _amount Amount of assets to deposit */ @@ -208,44 +228,49 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { require(_asset == WETH, "Unsupported asset"); require(_amount > 0, "Must deposit something"); emit Deposit(_asset, address(0), _amount); - - // TODO delete this. Just testing you knows?! - _addLiquidity(); } /** - * @notice Rebalance the pool to the desired token split + * @notice Rebalance the pool to the desired token split and Deposit any WETH on the contract to the + * underlying aerodrome pool. Print the required amount of corresponding OETHb. + * + * Exact _amountToSwap, _minTokenReceived & _swapWETH parameters shall be determined by simulating the + * transaction off-chain. The strategy checks that after the swap the share of the tokens is in the + * expected ranges. + * + * @param _amountToSwap The amount of the token to swap + * @param _minTokenReceived Slippage check -> minimum amount of token expected in return + * @param _swapWETH Swap using WETH when true, use OETHb when false */ - function rebalace() external nonReentrant onlyVaultOrGovernorOrStrategist { - _rebalace(); + function rebalace(uint256 _amountToSwap, uint256 _minTokenReceived, bool _swapWETH) external nonReentrant onlyGovernorOrStrategist { + _rebalace(_amountToSwap, _minTokenReceived, _swapWETH); } /** * @dev Rebalance the pool to the desired token split */ - function _rebalace() internal { - // TODO remove - _checkLiquidityWithinExpectedShare(); - + function _rebalace(uint256 _amount, uint256 _minTokenReceived, bool _swapWETH) internal { _removeLiquidity(); - _swapToDesiredPosition(); + _swapToDesiredPosition(_amount, _minTokenReceived, _swapWETH); _addLiquidity(); + _checkLiquidityWithinExpectedShare(); } /** - * @dev Decrease 100% of thex liquidity if strategy holds any. In practice the removal of liquidity + * @dev Decrease withdrawLiquidityShare (currently set to 99%) of the liquidity if strategy holds any. In practice the removal of liquidity * will be skipped only on the first time called. */ function _removeLiquidity() internal { if (tokenId == 0) { return; } - // TODO remove from gauge once we have it - // clGauge.withdraw(tokenId) + + clGauge.withdraw(tokenId); (uint128 liquidity,,) = _getPositionInfo(); uint128 liqudityToRemove = liquidity * withdrawLiquidityShare / 1e4; + // TODO add events (uint256 amountWETH, uint256 amountOETHb) = positionManager.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams({ tokenId: tokenId, @@ -282,26 +307,64 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { if (tokenId == 0) { return; } + // TODO: calculate that the liquidiy shares are within the expected position console.log("checking shares"); (uint256 amount0, uint256 amount1) = _getPositionPrincipal(); console.log(amount0); console.log(amount1); + + // calculate in 1 of 2 ways + // 1st way + // uint256 currentWethShare = amount0 * 1e4 / (amount0 + amount1) + // check that currentWethShare is within the poolWethShareVarianceAllowed of the + // configurable poolWethShare + + // 2nd way + // math should roughly be: + // (uint160 sqrtRatioX96, , , , ,) = clPool.slot0(); + // uint160 tickPriceInterval = (sqrtRatioX96Tick1 - sqrtRatioX96Tick0); + // uint160 targetPriceX96 = sqrtRatioX96Tick0 + + // tickPriceInterval.mulTruncateScale(poolWethShare, 1e4) + // - check that the targetPricex96 is within sqrtRatioX96 (with poolWethShareVarianceAllowed) + } /** * @dev Perform a swap so that after the swap the ticker has the desired WETH to OETHb token share. */ - function _swapToDesiredPosition() internal { - if (tokenId == 0) { - return; + function _swapToDesiredPosition(uint256 _amount, uint256 _minTokenReceived, bool _swapWETH) internal { + IERC20 tokenToSwap = IERC20(_swapWETH ? WETH : OETHb); + uint256 balance = tokenToSwap.balanceOf(address(this)); + + // TODO not tested + if(balance < _amount) { + // if swapping OETHb + if (!_swapWETH) { + uint256 mintForSwap = _amount - balance; + IVault(vaultAddress).mintForStrategy(mintForSwap); + } else { + revert NotEnoughWethForSwap(balance, _amount); + } } - - console.log("Swap to desired position?"); - (uint256 amount0, uint256 amount1) = _getPositionPrincipal(); - console.log(amount0); - console.log(amount1); + // Swap it + uint256 amountReceived = swapRouter.exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: _swapWETH ? WETH : OETHb, + tokenOut: _swapWETH ? OETHb : WETH, + tickSpacing: tickSpacing, // set to 1 + recipient: address(this), + deadline: block.timestamp, + amountIn: _amount, + amountOutMinimum: _minTokenReceived, // slippage check + // just a rough sanity check that we are within 0 -> 1 tick + // a more fine check is performed in _checkLiquidityWithinExpectedShare + sqrtPriceLimitX96: _swapWETH ? sqrtRatioX96Tick0 : sqrtRatioX96Tick1 + }) + ); + + _checkLiquidityWithinExpectedShare(); } /** @@ -316,7 +379,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { if (tokenId == 0) { (uint160 sqrtRatioX96, , , , ,) = clPool.slot0(); - console.log("WHAT IS Up?!?!"); + console.log("The current square root balance of the pool"); console.log(sqrtRatioX96); // TODO add new token id position minted event @@ -357,8 +420,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { netValue += amountWETH + amountOETHb; } - // TODO remove from gauge once we have it - // clGauge.deposit(tokenId) + clGauge.deposit(tokenId); } /** @@ -537,107 +599,4 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { // The pool tokens can never change. revert("Unsupported method"); } - - - - /** - * @dev Swaps one token for other and then provides liquidity to pools. - * - * @param _desiredAmount0 Minimum amount of token0 needed - * @param _desiredAmount1 Minimum amount of token1 needed - * @param _swapAmountIn Amount of tokens to swap - * @param _swapMinAmountOut Minimum amount of other tokens expected - * @param _sqrtPriceLimitX96 Max price limit for swap - * @param _swapZeroForOne True if swapping from token0 to token1 - */ - // function _ensureAssetsBySwapping( - // uint256 _desiredAmount0, - // uint256 _desiredAmount1, - // uint256 _swapAmountIn, - // uint256 _swapMinAmountOut, - // uint160 _sqrtPriceLimitX96, - // bool _swapZeroForOne - // ) internal { - // require(!swapsPaused, "Swaps are paused"); - - // uint256 token0Balance = IERC20(token0).balanceOf(address(this)); - // uint256 token1Balance = IERC20(token1).balanceOf(address(this)); - - // uint256 token0Needed = _desiredAmount0 > token0Balance - // ? _desiredAmount0 - token0Balance - // : 0; - // uint256 token1Needed = _desiredAmount1 > token1Balance - // ? _desiredAmount1 - token1Balance - // : 0; - - // if (_swapZeroForOne) { - // // Amount available in reserve strategies - // uint256 t1ReserveBal = reserveStrategy1.checkBalance(token1); - - // // Only swap when asset isn't available in reserve as well - // require(token1Needed > 0, "No need for swap"); - // require( - // token1Needed > t1ReserveBal, - // "Cannot swap when the asset is available in reserve" - // ); - // // Additional amount of token0 required for swapping - // token0Needed += _swapAmountIn; - // // Subtract token1 that we will get from swapping - // token1Needed = (_swapMinAmountOut >= token1Needed) - // ? 0 - // : (token1Needed - _swapMinAmountOut); - // } else { - // // Amount available in reserve strategies - // uint256 t0ReserveBal = reserveStrategy0.checkBalance(token0); - - // // Only swap when asset isn't available in reserve as well - // require(token0Needed > 0, "No need for swap"); - // require( - // token0Needed > t0ReserveBal, - // "Cannot swap when the asset is available in reserve" - // ); - // // Additional amount of token1 required for swapping - // token1Needed += _swapAmountIn; - // // Subtract token0 that we will get from swapping - // token0Needed = (_swapMinAmountOut >= token0Needed) - // ? 0 - // : (token0Needed - _swapMinAmountOut); - // } - - // // Fund strategy from reserve strategies - // if (token0Needed > 0) { - // IVault(vaultAddress).withdrawFromUniswapV3Reserve( - // token0, - // token0Needed - // ); - // } - - // if (token1Needed > 0) { - // IVault(vaultAddress).withdrawFromUniswapV3Reserve( - // token1, - // token1Needed - // ); - // } - - // // Swap it - // uint256 amountReceived = swapRouter.exactInputSingle( - // ISwapRouter.ExactInputSingleParams({ - // tokenIn: _swapZeroForOne ? token0 : token1, - // tokenOut: _swapZeroForOne ? token1 : token0, - // fee: poolFee, - // recipient: address(this), - // deadline: block.timestamp, - // amountIn: _swapAmountIn, - // amountOutMinimum: _swapMinAmountOut, - // sqrtPriceLimitX96: _sqrtPriceLimitX96 - // }) - // ); - - // emit AssetSwappedForRebalancing( - // _swapZeroForOne ? token0 : token1, - // _swapZeroForOne ? token1 : token0, - // _swapAmountIn, - // amountReceived - // ); - // } } diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 8d4021a4b4..9815df292b 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -148,7 +148,7 @@ const setupAerodromeOEthbWETHGauge = async (oethbAddress, aerodromeAmoStrategy, oethbAddress, true ); - + // create a gauge await voter .connect(aeroGaugeSigner) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index 42f6cbace5..df57697ba9 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -27,6 +27,18 @@ describe.only("ForkTest: Aerodrome AMO Strategy (Base)", function () { const { rafael } = fixture; await mintAndDeposit(rafael); + }); + + // create initial position, swap out all the OETHb from the pool + it("Should be able to mint OETHb to facilitate swap transaction", async () => { + + + }); + + + it("Should throw an exception if not enough WETH on rebalance to perform a swap", async () => { + + }); const rebalance = async (user) => { From 91ac555fb6bc81440321ac302906aca123986ff7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 12 Aug 2024 10:47:02 +0200 Subject: [PATCH 6/9] add a setter function for pool weth share variance allowed --- .../aerodrome/AerodromeAMOStrategy.sol | 28 +++++++++++++++++-- .../deploy/base/004_base_amo_strategy.js | 6 ++++ .../aerodrome-amo.base.fork-test.js | 21 +++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index 30528597d0..d611a865e2 100644 --- a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -51,15 +51,15 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { /// @notice sqrtRatioX96Tick1 uint160 public sqrtRatioX96Tick1; /** - * Specifies WETH to OETHb ratio the strategy contract aims for after rebalancing + * @notice Specifies WETH to OETHb ratio the strategy contract aims for after rebalancing * in basis point format. * * e.g. 2000 means 20% WETH 80% OETHb */ uint256 public poolWethShare; /** - * TODO: implement setters and tests - * how much variance is allowed (~slippage) when rebalancing the pool + * @notice Specifies how the target WETH share of the pool defined by the `poolWethShare` can + * vary from the configured value after rebalancing. Expressed in basis points. */ uint256 public poolWethShareVarianceAllowed; /** @@ -94,6 +94,10 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { uint128 newWithdrawLiquidityShare ); + event PoolWethShareVarianceAllowedUpdated( + uint256 poolWethShareVarianceAllowed + ); + /** * @dev Verifies that the caller is the Governor, or Strategist. */ @@ -205,6 +209,24 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { emit WithdrawLiqiudityShareUpdated(_amount); } + /** + * @notice Specifies how the target WETH share of the pool defined by the `poolWethShare` can + * vary from the configured value after rebalancing. + * @param _amount The new amount specified in basis points + */ + function setPoolWethShareVarianceAllowed(uint256 _amount) external onlyGovernor { + // TODO tests: + // - governor can update + // - non governor can not update + // - must be within allowed values (event emitted) + + // no sensible reason to ever allow this over 20% + require(_amount < 2000, "PoolWethShareVariance"); + + poolWethShareVarianceAllowed = _amount; + emit PoolWethShareVarianceAllowedUpdated(_amount); + } + /*************************************** Strategy overrides ****************************************/ diff --git a/contracts/deploy/base/004_base_amo_strategy.js b/contracts/deploy/base/004_base_amo_strategy.js index 04217ec4af..07e016a578 100644 --- a/contracts/deploy/base/004_base_amo_strategy.js +++ b/contracts/deploy/base/004_base_amo_strategy.js @@ -73,6 +73,12 @@ module.exports = deployOnBaseWithGuardian( .setWithdrawLiquidityShare(9900) // 99% ); + await withConfirmation( + cAMOStrategy + .connect(sDeployer) + .setPoolWethShareVarianceAllowed(200) // 2% + ); + await withConfirmation( cAMOStrategy .connect(sDeployer) diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index df57697ba9..d3ec8652c7 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -2,7 +2,7 @@ const { createFixtureLoader } = require("../_fixture"); const { defaultBaseFixture, } = require("../_fixture-base"); -//const { expect } = require("chai"); +const { expect } = require("chai"); const { oethUnits } = require("../helpers"); const baseFixture = createFixtureLoader(defaultBaseFixture); @@ -18,6 +18,25 @@ describe.only("ForkTest: Aerodrome AMO Strategy (Base)", function () { governor = fixture.governor; }); + describe("ForkTest: Initial state (Base)", function () { + it("Should have the correct initial state", async function () { + // correct pool weth share variance + expect(await aerodromeAmoStrategy.poolWethShareVarianceAllowed()).to.equal( + 200 + ); + + // correct withdrawal liquity share + expect(await aerodromeAmoStrategy.withdrawLiquidityShare()).to.equal( + 9900 + ); + + // correct pool weth share + expect(await aerodromeAmoStrategy.poolWethShare()).to.equal( + 2000 + ); + }); + }); + it("Should be able to deposit to the pool", async () => { const { rafael } = fixture; await mintAndDeposit(rafael); From 04bc306d6194b70f9e22bf023e6374db83ad7c9b Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 12 Aug 2024 13:24:17 +0200 Subject: [PATCH 7/9] fix typo --- .../aerodrome/AerodromeAMOStrategy.sol | 18 +++++++++--------- .../strategies/aerodrome-amo.base.fork-test.js | 7 +++++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index d611a865e2..e974ef557c 100644 --- a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -264,16 +264,16 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { * @param _minTokenReceived Slippage check -> minimum amount of token expected in return * @param _swapWETH Swap using WETH when true, use OETHb when false */ - function rebalace(uint256 _amountToSwap, uint256 _minTokenReceived, bool _swapWETH) external nonReentrant onlyGovernorOrStrategist { - _rebalace(_amountToSwap, _minTokenReceived, _swapWETH); + function rebalance(uint256 _amountToSwap, uint256 _minTokenReceived, bool _swapWETH) external nonReentrant onlyGovernorOrStrategist { + _rebalance(_amountToSwap, _minTokenReceived, _swapWETH); } /** * @dev Rebalance the pool to the desired token split */ - function _rebalace(uint256 _amount, uint256 _minTokenReceived, bool _swapWETH) internal { + function _rebalance(uint256 _amountToSwap, uint256 _minTokenReceived, bool _swapWETH) internal { _removeLiquidity(); - _swapToDesiredPosition(_amount, _minTokenReceived, _swapWETH); + _swapToDesiredPosition(_amountToSwap, _minTokenReceived, _swapWETH); _addLiquidity(); _checkLiquidityWithinExpectedShare(); } @@ -355,18 +355,18 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { /** * @dev Perform a swap so that after the swap the ticker has the desired WETH to OETHb token share. */ - function _swapToDesiredPosition(uint256 _amount, uint256 _minTokenReceived, bool _swapWETH) internal { + function _swapToDesiredPosition(uint256 _amountToSwap, uint256 _minTokenReceived, bool _swapWETH) internal { IERC20 tokenToSwap = IERC20(_swapWETH ? WETH : OETHb); uint256 balance = tokenToSwap.balanceOf(address(this)); // TODO not tested - if(balance < _amount) { + if(balance < _amountToSwap) { // if swapping OETHb if (!_swapWETH) { - uint256 mintForSwap = _amount - balance; + uint256 mintForSwap = _amountToSwap - balance; IVault(vaultAddress).mintForStrategy(mintForSwap); } else { - revert NotEnoughWethForSwap(balance, _amount); + revert NotEnoughWethForSwap(balance, _amountToSwap); } } @@ -378,7 +378,7 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { tickSpacing: tickSpacing, // set to 1 recipient: address(this), deadline: block.timestamp, - amountIn: _amount, + amountIn: _amountToSwap, amountOutMinimum: _minTokenReceived, // slippage check // just a rough sanity check that we are within 0 -> 1 tick // a more fine check is performed in _checkLiquidityWithinExpectedShare diff --git a/contracts/test/strategies/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/aerodrome-amo.base.fork-test.js index d3ec8652c7..14e6f35f1c 100644 --- a/contracts/test/strategies/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -60,10 +60,13 @@ describe.only("ForkTest: Aerodrome AMO Strategy (Base)", function () { }); - const rebalance = async (user) => { + const rebalance = async () => { await oethbVault .connect(strategist) - .rebalace( + .rebalance( + oethUnits("0.1"), + oethUnits("0.1"), + true ); } From ce6db277e4ed62c3e791419fa38ecedd5d49fb74 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 12 Aug 2024 13:45:52 +0200 Subject: [PATCH 8/9] refactor stuff from initialize to the constructor --- .../aerodrome/AerodromeAMOStrategy.sol | 74 ++++++++++--------- .../deploy/base/004_base_amo_strategy.js | 14 ++-- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index e974ef557c..4666fb927f 100644 --- a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -34,22 +34,9 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { /// increase liquidity increases this number by the amount of tokens added /// decrease liquidity decreases this number by the amount of tokens removed uint256 public netValue; - /// @notice the swapRouter for performing swaps - ISwapRouter public swapRouter; - /// @notice the pool used - ICLPool public clPool; - /// @notice the pool used + /// @notice the gauge for the corresponding Slipstream pool (clPool) + /// @dev can become an immutable once the gauge is created on the base mainnet ICLGauge public clGauge; - /// @notice the liquidity position - INonfungiblePositionManager public positionManager; - /// @notice helper contract for liquidity and ticker math - ISugarHelper public helper; - /// @notice helper contract for liquidity and ticker math - IQuoterV2 public quoter; - /// @notice sqrtRatioX96Tick0 - uint160 public sqrtRatioX96Tick0; - /// @notice sqrtRatioX96Tick1 - uint160 public sqrtRatioX96Tick1; /** * @notice Specifies WETH to OETHb ratio the strategy contract aims for after rebalancing * in basis point format. @@ -83,6 +70,20 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { int24 public immutable upperTick; /// @notice tick spacing of the pool (set to 1) int24 public immutable tickSpacing; + /// @notice the swapRouter for performing swaps + ISwapRouter public immutable swapRouter; + /// @notice the pool used + ICLPool public immutable clPool; + /// @notice the liquidity position + INonfungiblePositionManager public immutable positionManager; + /// @notice helper contract for liquidity and ticker math + ISugarHelper public immutable helper; + /// @notice helper contract for liquidity and ticker math + IQuoterV2 public immutable quoter; + /// @notice sqrtRatioX96Tick0 + uint160 public immutable sqrtRatioX96Tick0; + /// @notice sqrtRatioX96Tick1 + uint160 public immutable sqrtRatioX96Tick1; error NotEnoughWethForSwap(uint256 wethBalance, uint256 requiredWeth); // 0x989e5ca8 @@ -113,14 +114,34 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { /// @param _stratConfig st /// @param _wethAddress Address of the Erc20 WETH Token contract /// @param _OETHbAddress Address of the Erc20 OETHb Token contract + /// @param _swapRouter Address of the Aerodrome Universal Swap Router + /// @param _nonfungiblePositionManager Address of position manager to add/remove + /// the liquidity + /// @param _clPool Address of the Aerodrome concentrated liquidity pool + /// @param _quoter Address of the Aerodrome pool swap quoter + /// @param _sugarHelper Address of the Aerodrome Sugar helper contract constructor( BaseStrategyConfig memory _stratConfig, address _wethAddress, - address _OETHbAddress + address _OETHbAddress, + address _swapRouter, + address _nonfungiblePositionManager, + address _clPool, + address _quoter, + address _sugarHelper ) InitializableAbstractStrategy(_stratConfig) { WETH = _wethAddress; OETHb = _OETHbAddress; + swapRouter = ISwapRouter(_swapRouter); + positionManager = INonfungiblePositionManager( + _nonfungiblePositionManager + ); + clPool = ICLPool(_clPool); + helper = ISugarHelper(_sugarHelper); + quoter = IQuoterV2(_quoter); + sqrtRatioX96Tick0 = ISugarHelper(_sugarHelper).getSqrtRatioAtTick(0); + sqrtRatioX96Tick1 = ISugarHelper(_sugarHelper).getSqrtRatioAtTick(1); lowerTick = 0; upperTick = 1; @@ -132,37 +153,20 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { * @param _rewardTokenAddresses Address of reward token for platform * @param _assets Addresses of initial supported assets * @param _pTokens Platform Token corresponding addresses - * @param _swapRouter Address of the Aerodrome Universal Swap Router - * @param _nonfungiblePositionManager Address of position manager to add/remove - * the liquidity + * @param _clGauge Address of the Aerodrome slipstream pool gauge */ function initialize( address[] memory _rewardTokenAddresses, address[] memory _assets, address[] memory _pTokens, - address _swapRouter, - address _nonfungiblePositionManager, - address _clPool, - address _clGauge, - address _quoter, - address _sugarHelper + address _clGauge ) external onlyGovernor initializer { InitializableAbstractStrategy._initialize( _rewardTokenAddresses, _assets, _pTokens ); - - swapRouter = ISwapRouter(_swapRouter); - positionManager = INonfungiblePositionManager( - _nonfungiblePositionManager - ); - clPool = ICLPool(_clPool); clGauge = ICLGauge(_clGauge); - helper = ISugarHelper(_sugarHelper); - quoter = IQuoterV2(_quoter); - sqrtRatioX96Tick0 = helper.getSqrtRatioAtTick(0); - sqrtRatioX96Tick1 = helper.getSqrtRatioAtTick(1); } /*************************************** diff --git a/contracts/deploy/base/004_base_amo_strategy.js b/contracts/deploy/base/004_base_amo_strategy.js index 07e016a578..143faa7691 100644 --- a/contracts/deploy/base/004_base_amo_strategy.js +++ b/contracts/deploy/base/004_base_amo_strategy.js @@ -26,7 +26,12 @@ module.exports = deployOnBaseWithGuardian( */ [addresses.zero, cOETHbVaultProxy.address], // platformAddress, VaultAddress addresses.base.WETH, // weth address - cOETHbProxy.address // OETHb address + cOETHbProxy.address, // OETHb address + addresses.base.universalSwapRouter, // swapRouter + addresses.base.nonFungiblePositionManager, // nonfungiblePositionManager + addresses.base.aerodromeOETHbWETHClPool, // clOETHbWethPool + addresses.base.quoterV2, + addresses.base.sugarHelper // sugarHelper ]); const cAMOStrategyProxy = await ethers.getContract("AerodromeAMOStrategyProxy"); @@ -37,17 +42,12 @@ module.exports = deployOnBaseWithGuardian( // Init the AMO strategy const initData = cAMOStrategyImpl.interface.encodeFunctionData( - "initialize(address[],address[],address[],address,address,address,address,address,address)", + "initialize(address[],address[],address[],address)", [ [addresses.base.AERO], // rewardTokenAddresses [], // assets [], // pTokens - addresses.base.universalSwapRouter, // swapRouter - addresses.base.nonFungiblePositionManager, // nonfungiblePositionManager - addresses.base.aerodromeOETHbWETHClPool, // clOETHbWethPool addresses.zero, // clOETHbWethGauge - addresses.base.quoterV2, - addresses.base.sugarHelper // sugarHelper ] ); // prettier-ignore From f802d17fb880784a89aa22d4caa5d261af8a66b9 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 12 Aug 2024 20:58:04 +0400 Subject: [PATCH 9/9] Add fork tests on snapshot --- .github/workflows/defi.yml | 43 +++++++++++++++++++++++++++++++++ contracts/fork-test.sh | 15 +++++------- contracts/test/_global-hooks.js | 6 ++++- contracts/test/helpers.js | 2 ++ 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/.github/workflows/defi.yml b/.github/workflows/defi.yml index 5872321d5b..5762a50111 100644 --- a/.github/workflows/defi.yml +++ b/.github/workflows/defi.yml @@ -289,6 +289,49 @@ jobs: ./contracts/coverage.json ./contracts/coverage/**/* retention-days: 1 + + contracts-base-snapshot-forktest: + name: "Base Fork Tests (Fixed Snapshot)" + runs-on: ubuntu-latest + continue-on-error: true + env: + HARDHAT_CACHE_DIR: ./cache + PROVIDER_URL: ${{ secrets.PROVIDER_URL }} + BASE_PROVIDER_URL: ${{ secrets.BASE_PROVIDER_URL }} + TEST_ON_SNAPSHOT: "true" + # Hardcoded number to be changed later + BASE_BLOCK_NUMBER: 18346074 + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.x" + cache: "yarn" + cache-dependency-path: contracts/yarn.lock + + - uses: actions/cache@v3 + id: hardhat-cache + with: + path: contracts/cache + key: ${{ runner.os }}-hardhat-${{ hashFiles('contracts/cache/*.json') }} + restore-keys: | + ${{ runner.os }}-hardhat-cache + + - run: yarn install --frozen-lockfile + working-directory: ./contracts + + - run: yarn run test:coverage:base-fork + working-directory: ./contracts + + - uses: actions/upload-artifact@v3 + with: + name: fork-test-base-coverage-${{ github.sha }} + path: | + ./contracts/coverage.json + ./contracts/coverage/**/* + retention-days: 1 coverage-uploader: name: "Upload Coverage Reports" diff --git a/contracts/fork-test.sh b/contracts/fork-test.sh index 915031e176..397d99f459 100755 --- a/contracts/fork-test.sh +++ b/contracts/fork-test.sh @@ -42,6 +42,9 @@ main() elif [[ $FORK_NETWORK_NAME == "holesky" ]]; then PROVIDER_URL=$HOLESKY_PROVIDER_URL; BLOCK_NUMBER=$HOLESKY_BLOCK_NUMBER; + elif [[ $FORK_NETWORK_NAME == "base" ]]; then + PROVIDER_URL=$BASE_PROVIDER_URL; + BLOCK_NUMBER=$BASE_BLOCK_NUMBER; fi if $is_local; then @@ -70,15 +73,9 @@ main() fi if [ -z "$1" ]; then - if [[ $FORK_NETWORK_NAME == "holesky" ]]; then - # Run all files with `.holesky.fork-test.js` suffix when no file name param is given - # pass all other params along - params+="test/**/*.holesky.fork-test.js" - else - # Run all files with `.fork-test.js` suffix when no file name param is given - # pass all other params along - params+="test/**/*.fork-test.js" - fi + # Run all files with `.fork-test.js` suffix when no file name param is given + # pass all other params along. Network level filtering happens in global hooks + params+="test/**/*.fork-test.js" else # Run specifc files when a param is given params+="$@" diff --git a/contracts/test/_global-hooks.js b/contracts/test/_global-hooks.js index 788d7ee577..184f9dfc2b 100644 --- a/contracts/test/_global-hooks.js +++ b/contracts/test/_global-hooks.js @@ -6,6 +6,7 @@ const { isHoleskyFork, isBaseFork, isBaseUnitTest, + isBaseSnapshotTest, } = require("./helpers"); const _chunkId = Number(process.env.CHUNK_ID); @@ -45,6 +46,9 @@ mocha.before(function () { const isHoleskyTestFile = s.file.endsWith(".holesky.fork-test.js"); const isArbTestFile = s.file.endsWith(".arb.fork-test.js"); const isBaseTestFile = s.file.endsWith(".base.fork-test.js"); + const isBaseSnapshotTestFile = s.file.endsWith( + ".base.snapshot-fork-test.js" + ); const isBaseUnitTestFile = s.file.endsWith(".base.js"); const unitTest = !s.file.endsWith(".fork-test.js") && !isBaseUnitTestFile; @@ -53,7 +57,7 @@ mocha.before(function () { } else if (isMainnetForkTest) { return isMainnetForkTestFile; } else if (isBaseFork) { - return isBaseTestFile; + return isBaseSnapshotTest ? isBaseSnapshotTestFile : isBaseTestFile; } else if (isHoleskyFork) { return isHoleskyTestFile; } else if (isBaseUnitTest) { diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index f3803ac116..f31c00454b 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -274,6 +274,7 @@ const isBase = hre.network.name == "base"; const isBaseFork = isFork && process.env.FORK_NETWORK_NAME == "base"; const isBaseOrFork = isBase || isBaseFork; const isBaseUnitTest = process.env.UNIT_TESTS_NETWORK === "base"; +const isBaseSnapshotTest = process.env.TEST_ON_SNAPSHOT === "true"; /// Advances the EVM time by the given number of seconds const advanceTime = async (seconds) => { @@ -807,6 +808,7 @@ module.exports = { isBaseFork, isBaseOrFork, isBaseUnitTest, + isBaseSnapshotTest, getOracleAddress, setOracleTokenPriceUsd, getOracleAddresses,