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/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/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/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 new file mode 100644 index 0000000000..0238d29ba4 --- /dev/null +++ b/contracts/contracts/interfaces/aerodrome/ISugarHelper.sol @@ -0,0 +1,93 @@ +// 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 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 + 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); + + /// @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 + 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/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol new file mode 100644 index 0000000000..4666fb927f --- /dev/null +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -0,0 +1,628 @@ +// 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 { 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 "hardhat/console.sol"; + +contract AerodromeAMOStrategy is InitializableAbstractStrategy { + using StableMath for uint256; + using SafeERC20 for IERC20; + + /*************************************** + Storage slot members + ****************************************/ + + /// @notice tokenId of the liquidity position + uint256 public tokenId; + /// @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 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 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; + /** + * @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; + /** + * Share of liquidity to remove on rebalance + */ + uint128 public withdrawLiquidityShare; + /// @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; + /// @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 + + event PoolWethShareUpdated( + uint256 newWethShare + ); + + event WithdrawLiqiudityShareUpdated( + uint128 newWithdrawLiquidityShare + ); + + event PoolWethShareVarianceAllowedUpdated( + uint256 poolWethShareVarianceAllowed + ); + + /** + * @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 + /// @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 _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; + 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 _clGauge Address of the Aerodrome slipstream pool gauge + */ + function initialize( + address[] memory _rewardTokenAddresses, + address[] memory _assets, + address[] memory _pTokens, + address _clGauge + ) external onlyGovernor initializer { + InitializableAbstractStrategy._initialize( + _rewardTokenAddresses, + _assets, + _pTokens + ); + clGauge = ICLGauge(_clGauge); + } + + /*************************************** + 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 + */ + 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); + } + + /** + * @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 + ****************************************/ + + /** + * @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 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 + */ + function _deposit(address _asset, uint256 _amount) internal { + require(_asset == WETH, "Unsupported asset"); + require(_amount > 0, "Must deposit something"); + emit Deposit(_asset, address(0), _amount); + } + + /** + * @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 rebalance(uint256 _amountToSwap, uint256 _minTokenReceived, bool _swapWETH) external nonReentrant onlyGovernorOrStrategist { + _rebalance(_amountToSwap, _minTokenReceived, _swapWETH); + } + + /** + * @dev Rebalance the pool to the desired token split + */ + function _rebalance(uint256 _amountToSwap, uint256 _minTokenReceived, bool _swapWETH) internal { + _removeLiquidity(); + _swapToDesiredPosition(_amountToSwap, _minTokenReceived, _swapWETH); + _addLiquidity(); + _checkLiquidityWithinExpectedShare(); + } + + /** + * @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; + } + + clGauge.withdraw(tokenId); + + (uint128 liquidity,,) = _getPositionInfo(); + uint128 liqudityToRemove = liquidity * withdrawLiquidityShare / 1e4; + + // TODO add events + (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; + } + // 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(uint256 _amountToSwap, uint256 _minTokenReceived, bool _swapWETH) internal { + IERC20 tokenToSwap = IERC20(_swapWETH ? WETH : OETHb); + uint256 balance = tokenToSwap.balanceOf(address(this)); + + // TODO not tested + if(balance < _amountToSwap) { + // if swapping OETHb + if (!_swapWETH) { + uint256 mintForSwap = _amountToSwap - balance; + IVault(vaultAddress).mintForStrategy(mintForSwap); + } else { + revert NotEnoughWethForSwap(balance, _amountToSwap); + } + } + + // 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: _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 + sqrtPriceLimitX96: _swapWETH ? sqrtRatioX96Tick0 : sqrtRatioX96Tick1 + }) + ); + + _checkLiquidityWithinExpectedShare(); + } + + /** + * @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("The current square root balance of the pool"); + 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; + } + + clGauge.deposit(tokenId); + } + + /** + * @notice Deposit all supported assets in this strategy contract to the platform + */ + function depositAll() external virtual override { + _deposit(WETH, IERC20(WETH).balanceOf(address(this))); + } + + /** + * @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) + { + 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 + ); + } + + 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 + * @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"); + } +} 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 new file mode 100644 index 0000000000..143faa7691 --- /dev/null +++ b/contracts/deploy/base/004_base_amo_strategy.js @@ -0,0 +1,119 @@ +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"); + const cOETHbVault = await ethers.getContractAt( + "IVault", + cOETHbVaultProxy.address + ); + + await deployWithConfirmation("AerodromeAMOStrategyProxy"); + await deployWithConfirmation("AerodromeAMOStrategy", [ + /* The pool address is not yet known. Might be created before we deploy the + * strategy or after. + */ + [addresses.zero, cOETHbVaultProxy.address], // platformAddress, VaultAddress + addresses.base.WETH, // weth address + cOETHbProxy.address, // OETHb address + addresses.base.universalSwapRouter, // swapRouter + addresses.base.nonFungiblePositionManager, // nonfungiblePositionManager + addresses.base.aerodromeOETHbWETHClPool, // clOETHbWethPool + addresses.base.quoterV2, + addresses.base.sugarHelper // sugarHelper + ]); + + const cAMOStrategyProxy = await ethers.getContract("AerodromeAMOStrategyProxy"); + const cAMOStrategyImpl = await ethers.getContract("AerodromeAMOStrategy"); + const cAMOStrategy = await ethers.getContractAt("AerodromeAMOStrategy", cAMOStrategyProxy.address); + + console.log("Deployed AMO strategy and proxy contracts"); + + // Init the AMO strategy + const initData = cAMOStrategyImpl.interface.encodeFunctionData( + "initialize(address[],address[],address[],address)", + [ + [addresses.base.AERO], // rewardTokenAddresses + [], // assets + [], // pTokens + addresses.zero, // clOETHbWethGauge + ] + ); + // prettier-ignore + await withConfirmation( + cAMOStrategyProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + 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) + .setPoolWethShareVarianceAllowed(200) // 2% + ); + + await withConfirmation( + cAMOStrategy + .connect(sDeployer) + .safeApproveAllTokens() + ); + + console.log("AMOStrategy configured"); + + // 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: [], + }, + { + // 2. Approve the AMO strategy on the Vault + 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/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/hardhat.config.js b/contracts/hardhat.config.js index fde22cb171..d096712039 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -49,6 +49,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"; @@ -294,18 +295,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 a3a4f291e0..c07d6cda24 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 = @@ -55,6 +58,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); @@ -71,10 +78,12 @@ 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()); + // Make sure we can print bridged WOETH for tests if (isBaseFork) { await impersonateAndFund(woethGovernor.address); @@ -90,9 +99,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")); if (isFork) { @@ -100,6 +115,9 @@ const defaultBaseFixture = deployments.createFixture(async () => { await oethb.connect(governor).rebaseOptIn(); } + // TODO delete once we have gauge on the mainnet + await setupAerodromeOEthbWETHGauge(oethb.address, aerodromeAmoStrategy, governor); + return { // OETHb oethb, @@ -111,11 +129,15 @@ const defaultBaseFixture = deployments.createFixture(async () => { woethProxy, oracleRouter, + // Strategies + aerodromeAmoStrategy, + // WETH weth, // Signers governor, + strategist, woethGovernor, minter, burner, @@ -125,6 +147,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); @@ -133,7 +185,6 @@ mocha.after(async () => { module.exports = { defaultBaseFixture, - MINTER_ROLE, BURNER_ROLE, }; 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/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/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, 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..14e6f35f1c --- /dev/null +++ b/contracts/test/strategies/aerodrome-amo.base.fork-test.js @@ -0,0 +1,92 @@ +const { createFixtureLoader } = require("../_fixture"); +const { + defaultBaseFixture, +} = require("../_fixture-base"); +const { expect } = require("chai"); +const { oethUnits } = require("../helpers"); + +const baseFixture = createFixtureLoader(defaultBaseFixture); + +describe.only("ForkTest: Aerodrome AMO Strategy (Base)", function () { + let fixture, oethbVault, weth, aerodromeAmoStrategy, governor, strategist, rafael; + + beforeEach(async () => { + fixture = await baseFixture(); + weth = fixture.weth; + oethbVault = fixture.oethbVault; + aerodromeAmoStrategy = fixture.aerodromeAmoStrategy; + 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); + }); + + it("Should be able to deposit to the pool & rebalance", async () => { + 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 () => { + await oethbVault + .connect(strategist) + .rebalance( + oethUnits("0.1"), + oethUnits("0.1"), + true + ); + } + + const mintAndDeposit = async (userOverride) => { + const user = userOverride || rafael; + + 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 40a5ca16be..84f8e905ec 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -293,14 +293,24 @@ addresses.base.ethUsdPriceFeed = "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70"; addresses.base.aeroUsdPriceFeed = "0x4EC5970fC728C5f65ba413992CD5fF6FD70fcfF0"; addresses.base.WETH = "0x4200000000000000000000000000000000000006"; +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 = "0xe96EB1EDa83d18cbac224233319FA5071464e1b9"; +// Base Aerodrome +addresses.base.nonFungiblePositionManager = "0x827922686190790b37229fd06084350E74485b72"; +addresses.base.slipstreamPoolFactory = "0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A"; +addresses.base.aerodromeOETHbWETHClPool = "0x6446021F4E396dA3df4235C62537431372195D38"; +addresses.base.universalSwapRouter = "0x6Cb442acF35158D5eDa88fe602221b67B400Be3E"; +addresses.base.sugarHelper = "0x0AD09A66af0154a84e86F761313d02d0abB6edd5"; +addresses.base.quoterV2 = "0x254cF9E1E6e233aa1AC962CB9B05b2cfeAaE15b0"; + // Holesky addresses.holesky.WETH = "0x94373a4919B3240D86eA41593D5eBa789FEF3848";