From 0cdb508d22e9a287749009ade36d6acf18705bcc Mon Sep 17 00:00:00 2001 From: Shahul Hameed Date: Wed, 1 Mar 2023 12:24:57 +0530 Subject: [PATCH 01/83] checkpoint --- .vscode/settings.json | 3 + brownie/uniswap-v3.py | 3 + .../interfaces/IUniswapV3Strategy.sol | 8 + contracts/contracts/interfaces/IVault.sol | 6 + .../v3/INonfungiblePositionManager.sol | 143 ++++++ .../uniswap/v3/IUniswapV3Helper.sol | 21 + .../v2}/MockMintableUniswapPair.sol | 2 +- .../{ => uniswap/v2}/MockUniswapPair.sol | 2 +- .../{ => uniswap/v2}/MockUniswapRouter.sol | 6 +- .../v3/MockNonfungiblePositionManager.sol | 247 ++++++++++ .../mocks/uniswap/v3/MockUniswapV3Pool.sol | 56 +++ contracts/contracts/proxies/Proxies.sol | 9 +- .../GeneralizedUniswapV3Strategy.sol | 465 ++++++++++++++++++ .../utils/InitializableAbstractStrategy.sol | 7 +- contracts/contracts/utils/UniswapV3Helper.sol | 30 ++ contracts/contracts/vault/VaultAdmin.sol | 47 +- contracts/contracts/vault/VaultCore.sol | 20 +- contracts/contracts/vault/VaultStorage.sol | 5 + contracts/deploy/000_mock.js | 25 + contracts/deploy/001_core.js | 73 ++- .../deploy/048_deposit_withdraw_tooling.js | 2 +- .../deploy/049_uniswap_usdc_usdt_strategy.js | 147 ++++++ contracts/hardhat.config.js | 25 +- contracts/node.sh | 2 +- contracts/package.json | 4 +- contracts/test/_fixture.js | 79 ++- contracts/test/helpers.js | 6 + contracts/test/strategies/uniswap-v3.js | 82 +++ contracts/utils/addresses.js | 3 + contracts/utils/deploy.js | 2 +- contracts/yarn.lock | 35 +- package.json | 1 - 32 files changed, 1515 insertions(+), 51 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 brownie/uniswap-v3.py create mode 100644 contracts/contracts/interfaces/IUniswapV3Strategy.sol create mode 100644 contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol create mode 100644 contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol rename contracts/contracts/mocks/{ => uniswap/v2}/MockMintableUniswapPair.sol (93%) rename contracts/contracts/mocks/{ => uniswap/v2}/MockUniswapPair.sol (97%) rename contracts/contracts/mocks/{ => uniswap/v2}/MockUniswapRouter.sol (94%) create mode 100644 contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol create mode 100644 contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol create mode 100644 contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol create mode 100644 contracts/contracts/utils/UniswapV3Helper.sol create mode 100644 contracts/deploy/049_uniswap_usdc_usdt_strategy.js create mode 100644 contracts/test/strategies/uniswap-v3.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..2006168bcc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "solidity.defaultCompiler": "localNodeModule" +} \ No newline at end of file diff --git a/brownie/uniswap-v3.py b/brownie/uniswap-v3.py new file mode 100644 index 0000000000..e93e08d2bb --- /dev/null +++ b/brownie/uniswap-v3.py @@ -0,0 +1,3 @@ +from world import * + + diff --git a/contracts/contracts/interfaces/IUniswapV3Strategy.sol b/contracts/contracts/interfaces/IUniswapV3Strategy.sol new file mode 100644 index 0000000000..ff13315fb9 --- /dev/null +++ b/contracts/contracts/interfaces/IUniswapV3Strategy.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import { IStrategy } from "./IStrategy.sol"; + +interface IUniswapV3Strategy is IStrategy { + function reserveStrategy(address token) external view returns (address); +} diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index 1514fa4904..1bee56dec6 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -74,6 +74,8 @@ interface IVault { function approveStrategy(address _addr) external; + function approveUniswapV3Strategy(address _addr) external; + function removeStrategy(address _addr) external; function setAssetDefaultStrategy(address _asset, address _strategy) @@ -170,4 +172,8 @@ interface IVault { function setNetOusdMintForStrategyThreshold(uint256 _threshold) external; function netOusdMintedForStrategy() external view returns (int256); + + function depositForUniswapV3(address asset, uint256 amount) external; + + function withdrawForUniswapV3(address recipient, address asset, uint256 amount) external; } diff --git a/contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol b/contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol new file mode 100644 index 0000000000..ae3b4d6b1b --- /dev/null +++ b/contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +/// @title Non-fungible token for positions +/// @notice Wraps Uniswap V3 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 fee The fee 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, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + + struct MintParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + } + + /// @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 + 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 + /// @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; +} diff --git a/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol b/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol new file mode 100644 index 0000000000..7397e7d880 --- /dev/null +++ b/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +interface IUniswapV3Helper { + function getAmountsForLiquidity( + uint160 sqrtRatioX96, + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96, + uint128 liquidity + ) external view returns (uint256 amount0, uint256 amount1); + + function getLiquidityForAmounts( + uint160 sqrtRatioX96, + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96, + uint256 amount0, + uint256 amount1 + ) external view returns (uint128 liquidity); + + function getSqrtRatioAtTick(int24 tick) external view returns (uint160 sqrtPriceX96); +} diff --git a/contracts/contracts/mocks/MockMintableUniswapPair.sol b/contracts/contracts/mocks/uniswap/v2/MockMintableUniswapPair.sol similarity index 93% rename from contracts/contracts/mocks/MockMintableUniswapPair.sol rename to contracts/contracts/mocks/uniswap/v2/MockMintableUniswapPair.sol index f5d4f92bb0..7dccb75637 100644 --- a/contracts/contracts/mocks/MockMintableUniswapPair.sol +++ b/contracts/contracts/mocks/uniswap/v2/MockMintableUniswapPair.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.0; -import "./MintableERC20.sol"; +import "./../../MintableERC20.sol"; import "./MockUniswapPair.sol"; contract MockMintableUniswapPair is MockUniswapPair, MintableERC20 { diff --git a/contracts/contracts/mocks/MockUniswapPair.sol b/contracts/contracts/mocks/uniswap/v2/MockUniswapPair.sol similarity index 97% rename from contracts/contracts/mocks/MockUniswapPair.sol rename to contracts/contracts/mocks/uniswap/v2/MockUniswapPair.sol index 6bbadb1bf0..799713ba28 100644 --- a/contracts/contracts/mocks/MockUniswapPair.sol +++ b/contracts/contracts/mocks/uniswap/v2/MockUniswapPair.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.0; -import { IUniswapV2Pair } from "../interfaces/uniswap/IUniswapV2Pair.sol"; +import { IUniswapV2Pair } from "../../../interfaces/uniswap/IUniswapV2Pair.sol"; contract MockUniswapPair is IUniswapV2Pair { address tok0; diff --git a/contracts/contracts/mocks/MockUniswapRouter.sol b/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol similarity index 94% rename from contracts/contracts/mocks/MockUniswapRouter.sol rename to contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol index 6e53b87130..97a7cb7dcc 100644 --- a/contracts/contracts/mocks/MockUniswapRouter.sol +++ b/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IUniswapV2Router } from "../interfaces/uniswap/IUniswapV2Router02.sol"; -import { Helpers } from "../utils/Helpers.sol"; -import { StableMath } from "../utils/StableMath.sol"; +import { IUniswapV2Router } from "../../../interfaces/uniswap/IUniswapV2Router02.sol"; +import { Helpers } from "../../../utils/Helpers.sol"; +import { StableMath } from "../../../utils/StableMath.sol"; // import "hardhat/console.sol"; diff --git a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol new file mode 100644 index 0000000000..31417df8b0 --- /dev/null +++ b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { IUniswapV3Helper } from '../../../interfaces/uniswap/v3/IUniswapV3Helper.sol'; +import { IMockUniswapV3Pool } from './MockUniswapV3Pool.sol'; + +contract MockNonfungiblePositionManager { + using SafeERC20 for IERC20; + + uint128 public mockTokensOwed0; + uint128 public mockTokensOwed1; + + struct MockPosition { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint128 token0Owed; + uint128 token1Owed; + uint128 liquidity; + address recipient; + } + + struct MintParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + } + + struct IncreaseLiquidityParams { + uint256 tokenId; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct CollectParams { + uint256 tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } + + mapping (uint256 => MockPosition) public mockPositions; + + uint256 public slippage = 100; + + IUniswapV3Helper internal uniswapV3Helper; + IMockUniswapV3Pool internal mockPool; + + uint256 internal tokenCount = 0; + + constructor (address _helper, address _mockPool) { + uniswapV3Helper = IUniswapV3Helper(_helper); + mockPool = IMockUniswapV3Pool(_mockPool); + } + + function positions(uint256 tokenId) external view returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) { + MockPosition memory p = mockPositions[tokenId]; + return ( + 0, + address(0), + address(0), + address(0), + 0, + 0, + 0, + 0, + 0, + 0, + p.token0Owed, + p.token1Owed + ); + } + + function setTokensOwed(uint256 tokenId, uint128 token0, uint128 token1) public { + MockPosition storage p = mockPositions[tokenId]; + p.token0Owed = token0; + p.token1Owed = token1; + } + + function mint(MintParams calldata params) + external + payable + returns ( + uint256 tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { + tokenCount += 1; + tokenId = tokenCount; + + mockPositions[tokenId] = MockPosition({ + token0: params.token0, + token1: params.token1, + recipient: params.recipient, + token0Owed: 0, + token1Owed: 0, + liquidity: 0, + fee: params.fee, + tickLower: params.tickLower, + tickUpper: params.tickUpper + }); + + MockPosition storage p = mockPositions[tokenId]; + + (liquidity) = uniswapV3Helper.getLiquidityForAmounts( + mockPool.mockSqrtPriceX96(), + uniswapV3Helper.getSqrtRatioAtTick(p.tickLower), + uniswapV3Helper.getSqrtRatioAtTick(p.tickUpper), + params.amount0Desired, + params.amount1Desired + ); + + (amount0, amount1) = uniswapV3Helper.getAmountsForLiquidity( + mockPool.mockSqrtPriceX96(), + uniswapV3Helper.getSqrtRatioAtTick(p.tickLower), + uniswapV3Helper.getSqrtRatioAtTick(p.tickUpper), + liquidity + ); + + IERC20(params.token0).safeTransferFrom(msg.sender, address(this), amount0); + IERC20(params.token1).safeTransferFrom(msg.sender, address(this), amount1); + + p.liquidity += liquidity; + + require(amount0 >= params.amount0Min, "V3 Liquidity error"); + require(amount1 >= params.amount1Min, "V3 Liquidity error"); + } + + function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable returns ( + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) { + MockPosition storage p = mockPositions[params.tokenId]; + + (liquidity) = uniswapV3Helper.getLiquidityForAmounts( + mockPool.mockSqrtPriceX96(), + uniswapV3Helper.getSqrtRatioAtTick(p.tickLower), + uniswapV3Helper.getSqrtRatioAtTick(p.tickUpper), + params.amount0Desired, + params.amount1Desired + ); + + (amount0, amount1) = uniswapV3Helper.getAmountsForLiquidity( + mockPool.mockSqrtPriceX96(), + uniswapV3Helper.getSqrtRatioAtTick(p.tickLower), + uniswapV3Helper.getSqrtRatioAtTick(p.tickUpper), + liquidity + ); + + IERC20(p.token0).safeTransferFrom(msg.sender, address(this), amount0); + IERC20(p.token1).safeTransferFrom(msg.sender, address(this), amount1); + + p.liquidity += liquidity; + + require(amount0 >= params.amount0Min, "V3 Liquidity error"); + require(amount1 >= params.amount1Min, "V3 Liquidity error"); + } + + function decreaseLiquidity(DecreaseLiquidityParams calldata params) + external + payable + returns (uint256 amount0, uint256 amount1) + { + MockPosition storage p = mockPositions[params.tokenId]; + + amount0 = params.amount0Min; + amount1 = params.amount1Min; + + IMintableERC20(p.token0).mint(amount0); + IMintableERC20(p.token1).mint(amount1); + + IERC20(p.token0).safeTransfer(p.recipient, amount0); + IERC20(p.token1).safeTransfer(p.recipient, amount1); + + p.liquidity -= params.liquidity; + } + + function collect(CollectParams calldata params) + external + payable + returns (uint256 amount0, uint256 amount1) + { + MockPosition storage p = mockPositions[params.tokenId]; + + amount0 = p.token0Owed; + amount1 = p.token1Owed; + + IMintableERC20(p.token0).mint(amount0); + IMintableERC20(p.token1).mint(amount1); + + IERC20(p.token0).safeTransfer(p.recipient, amount0); + IERC20(p.token1).safeTransfer(p.recipient, amount1); + + p.token0Owed = 0; + p.token1Owed = 0; + } + + function setSlippage(uint256 _slippage) public { + slippage = _slippage; + } +} + +interface IMintableERC20 { + function mint(uint256 value) external; + + function mintTo(address to, uint256 value) external; +} diff --git a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol new file mode 100644 index 0000000000..f2c2239f99 --- /dev/null +++ b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import { IUniswapV3Helper } from '../../../interfaces/uniswap/v3/IUniswapV3Helper.sol'; + +contract MockUniswapV3Pool { + address public immutable token0; + address public immutable token1; + uint24 public immutable fee; + + uint160 public mockSqrtPriceX96; + int24 public mockTick; + IUniswapV3Helper internal uniswapV3Helper; + + constructor (address _token0, address _token1, uint24 _fee, address _helper) { + token0 = _token0; + token1 = _token1; + fee = _fee; + uniswapV3Helper = IUniswapV3Helper(_helper); + } + + function slot0() public view returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ) { + return ( + mockSqrtPriceX96, + mockTick, + 0, + 0, + 0, + 0, + true + ); + } + + function setTick(int24 tick) public { + mockTick = tick; + mockSqrtPriceX96 = uniswapV3Helper.getSqrtRatioAtTick(tick); + } + + function setVal(uint160 sqrtPriceX96, int24 tick) public { + mockSqrtPriceX96 = sqrtPriceX96; + mockTick = tick; + } +} + +interface IMockUniswapV3Pool { + function setTick(int24 tick) external; + function mockSqrtPriceX96() external returns (uint160); +} \ No newline at end of file diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 7434542e7e..bb4bc0a5f4 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -88,8 +88,11 @@ contract ConvexLUSDMetaStrategyProxy is InitializeGovernedUpgradeabilityProxy { } /** - * @notice MorphoAaveStrategyProxy delegates calls to a MorphoCompoundStrategy implementation + * @notice MorphoAaveStrategyProxy delegates calls to a MorphoAaveStrategy implementation */ -contract MorphoAaveStrategyProxy is InitializeGovernedUpgradeabilityProxy { +contract MorphoAaveStrategyProxy is InitializeGovernedUpgradeabilityProxy {} -} +/** + * @notice UniV3_USDC_USDT_Proxy delegates calls to a GeneralizedUniswapV3Strategy implementation + */ +contract UniV3_USDC_USDT_Proxy is InitializeGovernedUpgradeabilityProxy {} diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol new file mode 100644 index 0000000000..84089427bc --- /dev/null +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol"; +import { IStrategy } from "../interfaces/IStrategy.sol"; +import { IVault } from "../interfaces/IVault.sol"; + +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import { INonfungiblePositionManager } from '../interfaces/uniswap/v3/INonfungiblePositionManager.sol'; +import { IUniswapV3Helper } from '../interfaces/uniswap/v3/IUniswapV3Helper.sol'; + +contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { + using SafeERC20 for IERC20; + + event OperatorChanged(address _address); + event ReserveStrategyChanged(address indexed token, address strategy); + event UniswapV3FeeCollected(uint256 indexed tokenId, uint256 amount0, uint256 amount1); + event UniswapV3LiquidityAdded(uint256 indexed tokenId, uint256 amount0Sent, uint256 amount1Sent, uint128 liquidityMinted); + event UniswapV3LiquidityRemoved(uint256 indexed tokenId, uint256 amount0Received, uint256 amount1Received, uint128 liquidityBurned); + event UniswapV3PositionClosed(uint256 indexed tokenId, uint256 amount0Received, uint256 amount1Received, uint128 liquidityBurned); + + // Address of operator + address public operatorAddr; + address public token0; + address public token1; + + uint24 public poolFee; + uint24 internal constant MAX_SLIPPAGE = 100; // 1% + + mapping(address => address) public reserveStrategy; + + INonfungiblePositionManager public nonfungiblePositionManager; + + struct Position { + uint256 tokenId; + uint128 liquidity; + int24 lowerTick; + int24 upperTick; + uint160 sqrtRatioAX96; + uint160 sqrtRatioBX96; + } + + mapping (int48 => uint256) internal ticksToTokenId; + mapping (uint256 => Position) internal tokenIdToPosition; + uint256[] internal allTokenIds; + uint256 internal currentPositionTokenId; + + IUniswapV3Helper internal uniswapV3Helper; + + // Future-proofing + uint256[50] private __gap; + + /** + * @dev Ensures that the caller is Governor, Strategist or Operator. + */ + modifier onlyGovernorOrStrategistOrOperator() { + require( + msg.sender == operatorAddr || msg.sender == IVault(vaultAddress).strategistAddr() || isGovernor(), + "Caller is not the Operator, Strategist or Governor" + ); + _; + } + + function initialize( + address _vaultAddress, + address _poolAddress, + address _nonfungiblePositionManager, + address _token0ReserveStrategy, + address _token1ReserveStrategy, + address _operator, + address _uniswapV3Helper + ) external onlyGovernor initializer { + nonfungiblePositionManager = INonfungiblePositionManager(_nonfungiblePositionManager); + IUniswapV3Pool pool = IUniswapV3Pool(_poolAddress); + uniswapV3Helper = IUniswapV3Helper(_uniswapV3Helper); + + token0 = pool.token0(); + token1 = pool.token1(); + poolFee = pool.fee(); + + address[] memory _assets = new address[](2); + _assets[0] = token0; + _assets[1] = token1; + + super._initialize( + _poolAddress, + _vaultAddress, + new address[](0), // No Reward tokens + _assets, // Asset addresses + _assets // Platform token addresses + ); + + _setReserveStrategy(_token0ReserveStrategy, _token1ReserveStrategy); + + } + + function setOperator(address _operator) external onlyGovernor { + require(_operator != address(0), "Invalid operator address"); + operatorAddr = _operator; + emit OperatorChanged(_operator); + } + + function setReserveStrategy(address _token0ReserveStrategy, address _token1ReserveStrategy) external onlyGovernorOrStrategistOrOperator nonReentrant { + _setReserveStrategy(_token0ReserveStrategy, _token1ReserveStrategy); + } + + function _setReserveStrategy(address _token0ReserveStrategy, address _token1ReserveStrategy) internal { + // require(IStrategy(_token0ReserveStrategy).supportsAsset(token0), "Invalid Reserve Strategy"); + // require(IStrategy(_token1ReserveStrategy).supportsAsset(token1), "Invalid Reserve Strategy"); + + reserveStrategy[token0] = _token0ReserveStrategy; + reserveStrategy[token1] = _token1ReserveStrategy; + + emit ReserveStrategyChanged(token0, _token0ReserveStrategy); + emit ReserveStrategyChanged(token1, _token1ReserveStrategy); + } + + function deposit(address _asset, uint256 _amount) + external + override + onlyVault + nonReentrant + { + IVault(vaultAddress).depositForUniswapV3(_asset, _amount); + } + + function depositAll() external override onlyVault nonReentrant { + uint256 token0Bal = IERC20(token0).balanceOf(address(this)); + uint256 token1Bal = IERC20(token1).balanceOf(address(this)); + if (token0Bal > 0) { + IVault(vaultAddress).depositForUniswapV3(token0, token0Bal); + } + if (token1Bal > 0) { + IVault(vaultAddress).depositForUniswapV3(token1, token1Bal); + } + } + + function withdraw( + address recipient, + address asset, + uint256 amount + ) external override onlyVault nonReentrant { + uint256 reserveBalance = IStrategy(reserveStrategy[asset]).checkBalance(asset); + if (reserveBalance >= amount) { + IVault(vaultAddress).withdrawForUniswapV3(recipient, asset, amount); + return; + } + + uint256 amountToPullOut = amount - reserveBalance; + // TODO: Remove liquidity from pool + } + + function withdrawAll() external override onlyVault nonReentrant { + if (currentPositionTokenId > 0) { + _closePosition(currentPositionTokenId); + } + } + + function collectRewardTokens() external override onlyHarvester nonReentrant { + for (uint256 i = 0; i < allTokenIds.length; i++) { + uint256 tokenId = allTokenIds[0]; + if (tokenIdToPosition[tokenId].liquidity > 0) { + _collectFeesForToken(tokenId); + } + } + } + + function getPendingRewards() external view returns (uint128 amount0, uint128 amount1) { + if (currentPositionTokenId > 0) { + (amount0, amount1) = _getTokensOwed(currentPositionTokenId); + } + } + + function _getTokensOwed(uint256 tokenId) internal view returns (uint128 tokensOwed0, uint128 tokensOwed1) { + (, , , , , , , , , , tokensOwed0, tokensOwed1) = nonfungiblePositionManager.positions(tokenId); + } + + function checkBalance(address _asset) + external + view + override + returns (uint256 balance) + { + require(_asset == token0 || _asset == token1, "Unsupported asset"); + // TODO: Should reserve strategy balance be included? Might result in double calculations + balance = IERC20(_asset).balanceOf(address(this)); + + (uint160 sqrtRatioX96, , , , , ,) = IUniswapV3Pool(platformAddress).slot0(); + + if (currentPositionTokenId > 0) { + Position memory p = tokenIdToPosition[currentPositionTokenId]; + balance += _checkAssetBalanceOfPosition(_asset, p, sqrtRatioX96); + } + + // for (uint256 i = 0; i < allTokenIds.length; i++) { + // // TODO: Should only current active position be checked? + // Position memory p = tokenIdToPosition[allTokenIds[i]]; + // balance += _checkAssetBalanceOfPosition(_asset, p, sqrtRatioX96); + // } + } + + function _checkAssetBalanceOfPosition(address asset, Position memory p, uint160 sqrtRatioX96) internal view returns (uint256 balance) { + if (asset == token0) { + (balance, ) = _checkBalanceOfPosition(p, sqrtRatioX96); + } else { + (, balance) = _checkBalanceOfPosition(p, sqrtRatioX96); + } + } + + function _checkBalanceOfPosition(Position memory p, uint160 sqrtRatioX96) internal view returns (uint256 amount0, uint256 amount1) { + if (p.liquidity == 0) { + // NOTE: Making the assumption that tokens owed for inactive positions + // will always be zero (should be case since fees are collecting after + // liquidity is removed) + return (0, 0); + } + + (amount0, amount1) = uniswapV3Helper.getAmountsForLiquidity( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + p.liquidity + ); + + (uint128 feeAmount0, uint128 feeAmount1) = _getTokensOwed(p.tokenId); + + amount0 += feeAmount0; + amount1 += feeAmount1; + } + + function rebalance(uint256 maxAmount0, uint256 maxAmount1, int24 lowerTick, int24 upperTick) external onlyGovernorOrStrategistOrOperator nonReentrant { + if (currentPositionTokenId > 0) { + _closePosition(currentPositionTokenId); + } + + IERC20 cToken0 = IERC20(token0); + IERC20 cToken1 = IERC20(token1); + IVault vault = IVault(vaultAddress); + + // Withdraw enough funds from Reserve strategies + uint256 token0Balance = cToken0.balanceOf(address(this)); + if (token0Balance < maxAmount0) { + vault.withdrawForUniswapV3(address(this), token0, maxAmount0 - token0Balance); + } + + uint256 token1Balance = cToken1.balanceOf(address(this)); + if (token1Balance < maxAmount1) { + vault.withdrawForUniswapV3(address(this), token1, maxAmount1 - token1Balance); + } + + // Provide liquidity + int48 tickKey = _getTickPositionKey(lowerTick, upperTick); + uint256 tokenId = ticksToTokenId[tickKey]; + + if (tokenId > 0) { + // Add liquidity to the position token + Position storage p = tokenIdToPosition[tokenId]; + _increaseLiquidityForPosition(p, maxAmount0, maxAmount1); + } else { + // Mint new position + (tokenId, , ,) = _mintPosition(maxAmount0, maxAmount1, lowerTick, upperTick); + } + + // Move any leftovers to Reserve + uint256 token0Dust = cToken0.balanceOf(address(this)); + if (token0Dust > 0) { + vault.depositForUniswapV3(token0, token0Dust); + } + + uint256 token1Dust = cToken1.balanceOf(address(this)); + if (token1Dust > 0) { + vault.depositForUniswapV3(token1, token1Dust); + } + + currentPositionTokenId = tokenId; + } + + function closeActivePosition() external onlyGovernorOrStrategistOrOperator nonReentrant { + require(currentPositionTokenId > 0, "No active position"); + _closePosition(currentPositionTokenId); + } + + function closePosition(uint256 tokenId) external onlyGovernorOrStrategistOrOperator nonReentrant { + require(tokenIdToPosition[tokenId].liquidity > 0, "Invalid position"); + _closePosition(tokenId); + } + + function _getTickPositionKey(int24 lowerTick, int24 upperTick) internal returns (int48 key) { + if (lowerTick > upperTick) (lowerTick, upperTick) = (upperTick, lowerTick); + key = int48(lowerTick) * 2**24; // Shift by 24 bits + key = key + int24(upperTick); + } + + function _closePosition(uint256 tokenId) internal returns (uint256 amount0, uint256 amount1) { + Position storage p = tokenIdToPosition[tokenId]; + + if (p.liquidity == 0) { + return (0, 0); + } + + // Remove all liquidity + (amount0, amount1) = _decreaseLiquidityForPosition(p, p.liquidity); + + // Collect all fees for position + (uint256 amount0Fee, uint256 amount1Fee) = _collectFeesForToken(tokenId); + + amount0 = amount0 + amount0Fee; + amount1 = amount1 + amount1Fee; + + if (tokenId == currentPositionTokenId) { + currentPositionTokenId = 0; + } + } + + function _collectFeesForToken(uint256 tokenId) internal returns (uint256 amount0, uint256 amount1) { + INonfungiblePositionManager.CollectParams memory params = INonfungiblePositionManager.CollectParams({ + tokenId: tokenId, + recipient: address(this), + amount0Max: type(uint128).max, + amount1Max: type(uint128).max + }); + + (amount0, amount1) = nonfungiblePositionManager.collect(params); + + emit UniswapV3FeeCollected(tokenId, amount0, amount1); + } + + function _mintPosition(uint256 maxAmount0, uint256 maxAmount1, int24 lowerTick, int24 upperTick) internal returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) { + INonfungiblePositionManager.MintParams memory params = + INonfungiblePositionManager.MintParams({ + token0: token0, + token1: token1, + fee: poolFee, + tickLower: lowerTick, + tickUpper: upperTick, + amount0Desired: maxAmount0, + amount1Desired: maxAmount1, + amount0Min: maxAmount0 * (10000 - MAX_SLIPPAGE), // 1% Slippage, + amount1Min: maxAmount1 * (10000 - MAX_SLIPPAGE), // 1% Slippage, + recipient: address(this), + deadline: block.timestamp + }); + + (tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager.mint(params); + + allTokenIds.push(tokenId); + ticksToTokenId[_getTickPositionKey(lowerTick, upperTick)] = tokenId; + tokenIdToPosition[tokenId] = Position({ + tokenId: tokenId, + liquidity: liquidity, + lowerTick: lowerTick, + upperTick: upperTick, + // 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? + sqrtRatioAX96: uniswapV3Helper.getSqrtRatioAtTick(lowerTick), + sqrtRatioBX96: uniswapV3Helper.getSqrtRatioAtTick(upperTick) + }); + + emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); + } + + function _increaseLiquidityForPosition(Position storage p, uint256 maxAmount0, uint256 maxAmount1) internal returns (uint128 liquidity, uint256 amount0, uint256 amount1) { + INonfungiblePositionManager.IncreaseLiquidityParams memory params = + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: p.tokenId, + amount0Desired: maxAmount0, + amount1Desired: maxAmount1, + amount0Min: maxAmount0 * (10000 - MAX_SLIPPAGE), // 1% Slippage, + amount1Min: maxAmount1 * (10000 - MAX_SLIPPAGE), // 1% Slippage, + deadline: block.timestamp + }); + + (liquidity, amount0, amount1) = nonfungiblePositionManager.increaseLiquidity(params); + + p.liquidity += liquidity; + + emit UniswapV3LiquidityRemoved(p.tokenId, amount0, amount1, liquidity); + } + + function _decreaseLiquidityForPosition(Position storage p, uint128 liquidity) internal returns (uint256 amount0, uint256 amount1) { + (uint160 sqrtRatioX96, , , , , ,) = IUniswapV3Pool(platformAddress).slot0(); + (uint256 exactAmount0, uint256 exactAmount1) = uniswapV3Helper.getAmountsForLiquidity( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + liquidity + ); + + INonfungiblePositionManager.DecreaseLiquidityParams memory params = + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: p.tokenId, + liquidity: liquidity, + amount0Min: exactAmount0 * (10000 - MAX_SLIPPAGE), // 1% Slippage + amount1Min: exactAmount1 * (10000 - MAX_SLIPPAGE), // 1% Slippage + deadline: block.timestamp + }); + + (amount0, amount1) = nonfungiblePositionManager.decreaseLiquidity(params); + + p.liquidity -= liquidity; + + emit UniswapV3LiquidityRemoved(p.tokenId, amount0, amount1, liquidity); + } + + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external returns (bytes4) { + // TODO: Should we reject unwanted NFTs being transfered to the strategy? + // Could use `INonfungiblePositionManager.positions(tokenId)` to see if the token0 and token1 are matching + return this.onERC721Received.selector; + } + + function safeApproveAllTokens() external override onlyGovernor nonReentrant { + IERC20(token0).safeApprove(address(nonfungiblePositionManager), type(uint256).max); + IERC20(token1).safeApprove(address(nonfungiblePositionManager), type(uint256).max); + } + + function resetAllowanceOfTokens() external onlyGovernor nonReentrant { + IERC20(token0).safeApprove(address(nonfungiblePositionManager), 0); + IERC20(token1).safeApprove(address(nonfungiblePositionManager), 0); + } + + function _abstractSetPToken(address _asset, address _pToken) + internal + override + { + IERC20(_asset).safeApprove(address(nonfungiblePositionManager), type(uint256).max); + } + + function supportsAsset(address _asset) external view override returns (bool) { + return _asset == token0 || _asset == token1; + } + + /** + * Unused/unnecessary inherited functions + */ + + + function setPTokenAddress(address _asset, address _pToken) external override onlyGovernor { + /** + * This function isn't overridable from `InitializableAbstractStrategy` due to + * missing `virtual` keyword. However, adding a function with same signature will + * hide the inherited function + */ + // The pool tokens can never change. + revert("Unsupported method"); + } + + function removePToken(uint256 _assetIndex) external override onlyGovernor { + /** + * This function isn't overridable from `InitializableAbstractStrategy` due to + * missing `virtual` keyword. However, adding a function with same signature will + * hide the inherited function + */ + // The pool tokens can never change. + revert("Unsupported method"); + } +} diff --git a/contracts/contracts/utils/InitializableAbstractStrategy.sol b/contracts/contracts/utils/InitializableAbstractStrategy.sol index 2279f3311a..0add108bcf 100644 --- a/contracts/contracts/utils/InitializableAbstractStrategy.sol +++ b/contracts/contracts/utils/InitializableAbstractStrategy.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import { Initializable } from "../utils/Initializable.sol"; import { Governable } from "../governance/Governable.sol"; @@ -11,7 +10,6 @@ import { IVault } from "../interfaces/IVault.sol"; abstract contract InitializableAbstractStrategy is Initializable, Governable { using SafeERC20 for IERC20; - using SafeMath for uint256; event PTokenAdded(address indexed _asset, address _pToken); event PTokenRemoved(address indexed _asset, address _pToken); @@ -89,7 +87,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { function _initialize( address _platformAddress, address _vaultAddress, - address[] calldata _rewardTokenAddresses, + address[] memory _rewardTokenAddresses, address[] memory _assets, address[] memory _pTokens ) internal { @@ -206,6 +204,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { */ function setPTokenAddress(address _asset, address _pToken) external + virtual onlyGovernor { _setPTokenAddress(_asset, _pToken); @@ -216,7 +215,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { * This method can only be called by the system Governor * @param _assetIndex Index of the asset to be removed */ - function removePToken(uint256 _assetIndex) external onlyGovernor { + function removePToken(uint256 _assetIndex) external virtual onlyGovernor { require(_assetIndex < assetsMapped.length, "Invalid index"); address asset = assetsMapped[_assetIndex]; address pToken = assetToPToken[asset]; diff --git a/contracts/contracts/utils/UniswapV3Helper.sol b/contracts/contracts/utils/UniswapV3Helper.sol new file mode 100644 index 0000000000..9309690f5c --- /dev/null +++ b/contracts/contracts/utils/UniswapV3Helper.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity =0.7.6; + +import '@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol'; +import '@uniswap/v3-core/contracts/libraries/TickMath.sol'; + +contract UniswapV3Helper { + function getAmountsForLiquidity( + uint160 sqrtRatioX96, + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96, + uint128 liquidity + ) internal pure returns (uint256 amount0, uint256 amount1) { + return LiquidityAmounts.getAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, liquidity); + } + + function getLiquidityForAmounts( + uint160 sqrtRatioX96, + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96, + uint256 amount0, + uint256 amount1 + ) internal pure returns (uint128 liquidity) { + return LiquidityAmounts.getLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1); + } + + function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { + return TickMath.getSqrtRatioAtTick(tick); + } +} diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index bedf84c789..e4a090101d 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -11,6 +11,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { StableMath } from "../utils/StableMath.sol"; import { IOracle } from "../interfaces/IOracle.sol"; +import { IUniswapV3Strategy } from "../interfaces/IUniswapV3Strategy.sol"; import "./VaultStorage.sol"; contract VaultAdmin is VaultStorage { @@ -38,6 +39,14 @@ contract VaultAdmin is VaultStorage { _; } + modifier onlyUniswapV3Strategies() { + require( + isUniswapV3Strategy[msg.sender], + "Caller is not Uniswap V3 Strategy" + ); + _; + } + /*************************************** Configuration ****************************************/ @@ -184,9 +193,18 @@ contract VaultAdmin is VaultStorage { * @param _addr Address of the strategy to add */ function approveStrategy(address _addr) external onlyGovernor { + _approveStrategy(_addr, false); + } + + function approveUniswapV3Strategy(address _addr) external onlyGovernor { + _approveStrategy(_addr, true); + } + + function _approveStrategy(address _addr, bool isUniswapV3) internal { require(!strategies[_addr].isSupported, "Strategy already approved"); - strategies[_addr] = Strategy({ isSupported: true, _deprecated: 0 }); + strategies[_addr] = Strategy({ isSupported: true, isUniswapV3Strategy: isUniswapV3, _deprecated: 0 }); allStrategies.push(_addr); + isUniswapV3Strategy[_addr] = isUniswapV3; emit StrategyApproved(_addr); } @@ -223,6 +241,7 @@ contract VaultAdmin is VaultStorage { // Mark the strategy as not supported strategies[_addr].isSupported = false; + isUniswapV3Strategy[_addr] = false; // Withdraw all assets IStrategy strategy = IStrategy(_addr); @@ -501,4 +520,30 @@ contract VaultAdmin is VaultStorage { strategy.withdrawAll(); } } + + /*************************************** + Uniswap V3 Utils + ****************************************/ + + function depositForUniswapV3(address asset, uint256 amount) external onlyUniswapV3Strategies nonReentrant { + _depositForUniswapV3(msg.sender, asset, amount); + } + + function _depositForUniswapV3(address v3Strategy, address asset, uint256 amount) internal { + require(strategies[v3Strategy].isSupported, "Strategy not approved"); + address reserveStrategy = IUniswapV3Strategy(v3Strategy).reserveStrategy(asset); + require(reserveStrategy != address(0), "Invalid Reserve Strategy address"); + IERC20(asset).safeTransfer(reserveStrategy, amount); + IStrategy(reserveStrategy).deposit(asset, amount); + } + + function withdrawForUniswapV3(address recipient, address asset, uint256 amount) external onlyUniswapV3Strategies nonReentrant { + _withdrawForUniswapV3(msg.sender, recipient, asset, amount); + } + + function _withdrawForUniswapV3(address v3Strategy, address recipient, address asset, uint256 amount) internal { + require(strategies[v3Strategy].isSupported, "Strategy not approved"); + address reserveStrategy = IUniswapV3Strategy(v3Strategy).reserveStrategy(asset); + IStrategy(reserveStrategy).withdraw(recipient, asset, amount); + } } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 3ee58e96b4..0b8cff0535 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -337,7 +337,8 @@ contract VaultCore is VaultStorage { // Iterate over all assets in the Vault and allocate to the appropriate // strategy for (uint256 i = 0; i < allAssets.length; i++) { - IERC20 asset = IERC20(allAssets[i]); + address assetAddr = allAssets[i]; + IERC20 asset = IERC20(assetAddr); uint256 assetBalance = asset.balanceOf(address(this)); // No balance, nothing to do here if (assetBalance == 0) continue; @@ -349,17 +350,26 @@ contract VaultCore is VaultStorage { ); address depositStrategyAddr = assetDefaultStrategies[ - address(asset) + assetAddr ]; if (depositStrategyAddr != address(0) && allocateAmount > 0) { - IStrategy strategy = IStrategy(depositStrategyAddr); + IStrategy strategy; + if (isUniswapV3Strategy[depositStrategyAddr]) { + IUniswapV3Strategy uniswapStrategy = IUniswapV3Strategy(depositStrategyAddr); + strategy = IStrategy(uniswapStrategy.reserveStrategy(assetAddr)); + } else { + strategy = IStrategy(depositStrategyAddr); + } + + require(address(strategy) != address(0), "Invalid deposit strategy"); + // Transfer asset to Strategy and call deposit method to // mint or take required action asset.safeTransfer(address(strategy), allocateAmount); - strategy.deposit(address(asset), allocateAmount); + strategy.deposit(assetAddr, allocateAmount); emit AssetAllocated( - address(asset), + assetAddr, depositStrategyAddr, allocateAmount ); diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index fef80b07b2..983d8d4dde 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -13,6 +13,7 @@ import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IStrategy } from "../interfaces/IStrategy.sol"; +import { IUniswapV3Strategy } from "../interfaces/IUniswapV3Strategy.sol"; import { Governable } from "../governance/Governable.sol"; import { OUSD } from "../token/OUSD.sol"; import { Initializable } from "../utils/Initializable.sol"; @@ -60,6 +61,7 @@ contract VaultStorage is Initializable, Governable { struct Strategy { bool isSupported; uint256 _deprecated; // Deprecated storage slot + bool isUniswapV3Strategy; } mapping(address => Strategy) internal strategies; address[] internal allStrategies; @@ -120,6 +122,9 @@ contract VaultStorage is Initializable, Governable { // How much net total OUSD is allowed to be minted by all strategies uint256 public netOusdMintForStrategyThreshold = 0; + // TODO: Should this be part of struct `Strategy`?? + mapping(address => bool) public isUniswapV3Strategy; + /** * @dev set the implementation for the admin, this needs to be in a base class else we cannot set it * @param newImpl address of the implementation diff --git a/contracts/deploy/000_mock.js b/contracts/deploy/000_mock.js index ee6306d48b..964b9e4ae8 100644 --- a/contracts/deploy/000_mock.js +++ b/contracts/deploy/000_mock.js @@ -26,6 +26,7 @@ const { abi: QUOTER_ABI, bytecode: QUOTER_BYTECODE, } = require("@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json"); +const { ethers } = require("hardhat"); const deployMocks = async ({ getNamedAccounts, deployments }) => { const { deploy } = deployments; @@ -327,11 +328,35 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => { args: [factory.address, weth.address], }); + // This is completely unrelated to the Uniswap V3 mocks above. + // This one focuses only on Uniswap V3 strategies + await deployMocksForUniswapV3Strategy(deploy, deployerAddr); + console.log("000_mock deploy done."); return true; }; +async function deployMocksForUniswapV3Strategy(deploy, deployerAddr) { + const v3Helper = await deploy("MockUniswapV3Helper", { + from: deployerAddr, + contract: "UniswapV3Helper" + }) + + const mockUSDT = await ethers.getContract("MockUSDT"); + const mockUSDC = await ethers.getContract("MockUSDC"); + + const mockPool = await deploy("MockUniswapV3Pool", { + from: deployerAddr, + args: [mockUSDC.address, mockUSDT.address, 500, v3Helper.address] + }) + + await deploy("MockNonfungiblePositionManager", { + from: deployerAddr, + args: [v3Helper.address, mockPool.address] + }) +} + deployMocks.id = "000_mock"; deployMocks.tags = ["mocks"]; deployMocks.skip = () => isMainnetOrFork; diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 3b533f1812..baba994fff 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -635,6 +635,15 @@ const configureStrategies = async (harvesterProxy) => { await withConfirmation( threePool.connect(sGovernor).setHarvesterAddress(harvesterProxy.address) ); + + const uniV3UsdcUsdtProxy = await ethers.getContract("UniV3_USDC_USDT_Proxy"); + const uniV3UsdcUsdt = await ethers.getContractAt( + "GeneralizedUniswapV3Strategy", + uniV3UsdcUsdtProxy.address + ); + await withConfirmation( + uniV3UsdcUsdt.connect(sGovernor).setHarvesterAddress(harvesterProxy.address) + ); }; const deployDripper = async () => { @@ -926,7 +935,7 @@ const deployBuyback = async () => { return cBuyback; }; -const deployVaultVaultChecker = async () => { +const deployVaultValueChecker = async () => { const vault = await ethers.getContract("VaultProxy"); const ousd = await ethers.getContract("OUSDProxy"); @@ -962,6 +971,65 @@ const deployWOusd = async () => { await wousd.connect(sGovernor).claimGovernance(); }; +const deployUniswapV3Strategy = async () => { + const { deployerAddr, governorAddr, operatorAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + const sGovernor = await ethers.provider.getSigner(governorAddr); + + const vault = await ethers.getContract("VaultProxy"); + const pool = await ethers.getContract("MockUniswapV3Pool"); + const manager = await ethers.getContract("MockNonfungiblePositionManager"); + const compStrat = await ethers.getContract("CompoundStrategyProxy"); + const v3Helper = await ethers.getContract("MockUniswapV3Helper"); + + const uniV3UsdcUsdtImpl = await deployWithConfirmation("UniV3_USDC_USDT_Strategy", [], "GeneralizedUniswapV3Strategy"); + await deployWithConfirmation("UniV3_USDC_USDT_Proxy"); + const uniV3UsdcUsdtProxy = await ethers.getContract("UniV3_USDC_USDT_Proxy"); + + await withConfirmation( + uniV3UsdcUsdtProxy["initialize(address,address,bytes)"]( + uniV3UsdcUsdtImpl.address, + deployerAddr, + [] + ) + ); + log("Initialized UniV3_USDC_USDT_Proxy"); + + const uniV3UsdcUsdtStrat = await ethers.getContractAt("GeneralizedUniswapV3Strategy", uniV3UsdcUsdtProxy.address); + await withConfirmation( + uniV3UsdcUsdtStrat.connect(sDeployer) + ["initialize(address,address,address,address,address,address,address)"]( + vault.address, + pool.address, + manager.address, + compStrat.address, + compStrat.address, + operatorAddr, + v3Helper.address + ) + ); + log("Initialized UniV3_USDC_USDT_Strategy"); + + await withConfirmation( + uniV3UsdcUsdtStrat.connect(sDeployer).transferGovernance(governorAddr) + ); + log(`UniV3_USDC_USDT_Strategy transferGovernance(${governorAddr}) called`); + + // On Mainnet the governance transfer gets executed separately, via the + // multi-sig wallet. On other networks, this migration script can claim + // governance by the governor. + if (!isMainnet) { + await withConfirmation( + uniV3UsdcUsdtStrat + .connect(sGovernor) // Claim governance with governor + .claimGovernance() + ); + log("Claimed governance for UniV3_USDC_USDT_Strategy"); + } + + return uniV3UsdcUsdtStrat; +} + const main = async () => { console.log("Running 001_core deployment..."); await deployOracles(); @@ -974,6 +1042,7 @@ const main = async () => { await deployConvexStrategy(); await deployConvexOUSDMetaStrategy(); await deployConvexLUSDMetaStrategy(); + await deployUniswapV3Strategy(); const harvesterProxy = await deployHarvester(); await configureVault(harvesterProxy); await configureStrategies(harvesterProxy); @@ -981,7 +1050,7 @@ const main = async () => { await deployFlipper(); await deployBuyback(); await deployUniswapV3Pool(); - await deployVaultVaultChecker(); + await deployVaultValueChecker(); await deployWOusd(); console.log("001_core deploy done."); return true; diff --git a/contracts/deploy/048_deposit_withdraw_tooling.js b/contracts/deploy/048_deposit_withdraw_tooling.js index 2c310fe384..ed59602727 100644 --- a/contracts/deploy/048_deposit_withdraw_tooling.js +++ b/contracts/deploy/048_deposit_withdraw_tooling.js @@ -2,7 +2,7 @@ const { deploymentWithProposal } = require("../utils/deploy"); const addresses = require("../utils/addresses"); module.exports = deploymentWithProposal( - { deployName: "048_deposit_withdraw_tooling", forceDeploy: false }, + { deployName: "048_deposit_withdraw_tooling", forceDeploy: false, forceSkip: true }, async ({ assetAddresses, deployWithConfirmation, diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js new file mode 100644 index 0000000000..ff3bd80884 --- /dev/null +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -0,0 +1,147 @@ +const { deploymentWithProposal } = require("../utils/deploy"); + +module.exports = deploymentWithProposal( + { + deployName: "049_uniswap_usdc_usdt_strategy", + forceDeploy: false, + }, + async ({ + assetAddresses, + deployWithConfirmation, + ethers, + getTxOpts, + withConfirmation, + }) => { + const { deployerAddr, governorAddr, operatorAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + // Current contracts + const cVaultProxy = await ethers.getContract("VaultProxy"); + const cVault = await ethers.getContractAt( + "Vault", + cVaultProxy.address + ); + const cVaultAdmin = await ethers.getContractAt( + "VaultAdmin", + cVaultProxy.address + ); + + // Deployer Actions + // ---------------- + + // 0. Deploy UniswapV3Helper + const dUniswapV3Helper = await deployWithConfirmation("UniswapV3Helper") + + // 0. Upgrade VaultAdmin + const dVaultAdmin = await deployWithConfirmation("VaultAdmin"); + const dVaultCore = await deployWithConfirmation("VaultCore"); + + // 1. Deploy new proxy + // New strategy will be living at a clean address + const dUniV3_USDC_USDT_Proxy = await deployWithConfirmation( + "UniV3_USDC_USDT_Proxy" + ); + const cUniV3_USDC_USDT_Proxy = await ethers.getContractAt( + "UniV3_USDC_USDT_Proxy", + dUniV3_USDC_USDT_Proxy.address + ); + + // 2. Deploy new implementation + const dUniV3_USDC_USDT_StrategyImpl = await deployWithConfirmation( + "GeneralizedUniswapV3Strategy" + ); + const cUniV3_USDC_USDT_Strategy = await ethers.getContractAt( + "GeneralizedUniswapV3Strategy", + dUniV3_USDC_USDT_Proxy.address + ); + + const cMorphoCompProxy = await ethers.getContract("MorphoCompoundStrategyProxy"); + + const cHarvesterProxy = await ethers.getContract("HarvesterProxy"); + const cHarvester = await ethers.getContractAt( + "Harvester", + cHarvesterProxy.address + ); + + console.log(governorAddr, await cMorphoCompProxy.governor()) + + // 3. Init the proxy to point at the implementation + await withConfirmation( + cUniV3_USDC_USDT_Proxy + .connect(sDeployer) + ["initialize(address,address,bytes)"]( + dUniV3_USDC_USDT_StrategyImpl.address, + deployerAddr, + [], + await getTxOpts() + ) + ); + + // 4. Init and configure new Uniswap V3 strategy + const initFunction = "initialize(address,address,address,address,address,address,address)"; + await withConfirmation( + cUniV3_USDC_USDT_Strategy.connect(sDeployer)[initFunction]( + cVaultProxy.address, // Vault + assetAddresses.UniV3_USDC_USDT_Pool, // Pool address + assetAddresses.UniV3PositionManager, // NonfungiblePositionManager + cMorphoCompProxy.address, // Reserve strategy for USDC + cMorphoCompProxy.address, // Reserve strategy for USDT + operatorAddr, + dUniswapV3Helper.address, + await getTxOpts() + ) + ); + + // 5. Transfer governance + await withConfirmation( + cUniV3_USDC_USDT_Strategy + .connect(sDeployer) + .transferGovernance(governorAddr, await getTxOpts()) + ); + + console.log("Uniswap V3 (USDC-USDT pool) strategy address: ", cUniV3_USDC_USDT_Strategy.address); + // Governance Actions + // ---------------- + return { + name: "Deploy new Uniswap V3 (USDC-USDT pool) strategy", + actions: [ + // 0. Set VaultCore implementation + { + contract: cVaultProxy, + signature: "upgradeTo(address)", + args: [dVaultCore.address], + }, + // 0. Set VaultAdmin implementation + { + contract: cVault, + signature: "setAdminImpl(address)", + args: [dVaultAdmin.address], + }, + // // 1. Accept governance of new cUniV3_USDC_USDT_Strategy + // { + // contract: cUniV3_USDC_USDT_Strategy, + // signature: "claimGovernance()", + // args: [], + // }, + // // 2. Add new strategy to vault + // { + // contract: cVaultAdmin, + // signature: "approveUniswapV3Strategy(address)", + // args: [cUniV3_USDC_USDT_Proxy.address], + // }, + // // 3. Set supported strategy on Harvester + // { + // contract: cHarvester, + // signature: "setSupportedStrategy(address,bool)", + // args: [cUniV3_USDC_USDT_Proxy.address, true], + // }, + // // 4. Set harvester address + // { + // contract: cUniV3_USDC_USDT_Strategy, + // signature: "setHarvesterAddress(address)", + // args: [cHarvesterProxy.address], + // }, + ], + }; + } +); diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index f0793b2677..b922fd5819 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -59,6 +59,7 @@ const MAINNET_GOVERNOR = "0x72426ba137dec62657306b12b1e869d43fec6ec7"; const MAINNET_MULTISIG = "0xbe2AB3d3d8F6a32b96414ebbd865dBD276d3d899"; const MAINNET_CLAIM_ADJUSTER = MAINNET_DEPLOYER; const MAINNET_STRATEGIST = "0xf14bbdf064e3f67f51cd9bd646ae3716ad938fdc"; +const MAINNET_OPERATOR = "0xf14bbdf064e3f67f51cd9bd646ae3716ad938fdc"; const mnemonic = "replace hover unaware super where filter stone fine garlic address matrix basic"; @@ -216,12 +217,20 @@ const isForkTest = module.exports = { solidity: { - version: "0.8.7", - settings: { - optimizer: { - enabled: true, + compilers: [ + { + version: "0.8.7", + settings: { + optimizer: { + enabled: true, + }, + }, }, - }, + { + // Uniswap V3 contracts use solc 0.7.6 + version: "0.7.6" + } + ] }, networks: { hardhat: { @@ -297,6 +306,12 @@ module.exports = { hardhat: process.env.FORK === "true" ? MAINNET_STRATEGIST : 0, mainnet: MAINNET_STRATEGIST, }, + operatorAddr: { + default: 3, + localhost: process.env.FORK === "true" ? MAINNET_OPERATOR : 3, + hardhat: process.env.FORK === "true" ? MAINNET_OPERATOR : 3, + mainnet: MAINNET_OPERATOR, + }, }, contractSizer: { alphaSort: true, diff --git a/contracts/node.sh b/contracts/node.sh index 1c283b763a..52adb733c2 100755 --- a/contracts/node.sh +++ b/contracts/node.sh @@ -2,7 +2,7 @@ trap "exit" INT TERM ERR trap "kill 0" EXIT #nodeWaitTimeout=120 -nodeWaitTimeout=1200 +nodeWaitTimeout=12000 RED='\033[0;31m' NO_COLOR='\033[0m' diff --git a/contracts/package.json b/contracts/package.json index b006a5f8b2..c66605165c 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -35,8 +35,8 @@ "@nomiclabs/hardhat-waffle": "^2.0.1", "@openzeppelin/contracts": "4.4.2", "@openzeppelin/hardhat-upgrades": "^1.10.0", - "@uniswap/v3-core": "^1.0.0", - "@uniswap/v3-periphery": "^1.1.1", + "@uniswap/v3-core": "^1.0.1", + "@uniswap/v3-periphery": "^1.4.3", "chai": "^4.3.4", "dotenv": "^10.0.0", "eslint": "^7.32.0", diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 30a75cbd73..686e9a45d8 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -112,6 +112,12 @@ async function defaultFixture() { const buyback = await ethers.getContract("Buyback"); + const UniV3_USDC_USDT_Proxy = await ethers.getContract("UniV3_USDC_USDT_Proxy") + const UniV3_USDC_USDT_Strategy = await ethers.getContractAt( + "GeneralizedUniswapV3Strategy", + UniV3_USDC_USDT_Proxy.address + ); + let usdt, dai, tusd, @@ -159,7 +165,9 @@ async function defaultFixture() { cvxBooster, cvxRewardPool, LUSDMetaStrategyProxy, - LUSDMetaStrategy; + LUSDMetaStrategy, + UniV3PositionManager, + UniV3_USDC_USDT_Pool; if (isFork) { usdt = await ethers.getContractAt(usdtAbi, addresses.mainnet.USDT); @@ -217,6 +225,17 @@ async function defaultFixture() { "MorphoAaveStrategy", morphoAaveStrategyProxy.address ); + + UniV3PositionManager = await ethers.getContractAt( + "INonfungiblePositionManager", + addresses.mainnet.UniV3PositionManager + ); + + UniV3_USDC_USDT_Pool = await ethers.getContractAt( + "IUniswapV3Pool", + addresses.mainnet.UniV3_USDC_USDT_Pool + ); + } else { usdt = await ethers.getContract("MockUSDT"); dai = await ethers.getContract("MockDAI"); @@ -295,6 +314,9 @@ async function defaultFixture() { "ConvexGeneralizedMetaStrategy", LUSDMetaStrategyProxy.address ); + + UniV3PositionManager = await ethers.getContract("MockNonfungiblePositionManager"); + UniV3_USDC_USDT_Pool = await ethers.getContract("MockUniswapV3Pool"); } if (!isFork) { const assetAddresses = await getAssetAddresses(deployments); @@ -315,6 +337,7 @@ async function defaultFixture() { let governor = signers[1]; const strategist = signers[0]; const adjuster = signers[0]; + const operator = signers[3]; const [matt, josh, anna, domen, daniel, franck] = signers.slice(4); @@ -351,6 +374,7 @@ async function defaultFixture() { domen, daniel, franck, + operator, // Contracts ousd, vault, @@ -422,6 +446,11 @@ async function defaultFixture() { flipper, buyback, wousd, + + // Uniswap V3 Strategy + UniV3PositionManager, + UniV3_USDC_USDT_Pool, + UniV3_USDC_USDT_Strategy }; } @@ -1170,6 +1199,53 @@ async function rebornFixture() { return fixture; } +async function uniswapV3Fixture() { + const fixture = await loadFixture(compoundVaultFixture); + + const { usdc, usdt, UniV3_USDC_USDT_Strategy } = fixture; + + // Approve Uniswap V3 Strategy + await _approveStrategy(fixture, UniV3_USDC_USDT_Strategy, true); + + // Change default strategy to Uniswap V3 for both USDT and USDC + await _setDefaultStrategy(fixture, usdc, UniV3_USDC_USDT_Strategy); + await _setDefaultStrategy(fixture, usdt, UniV3_USDC_USDT_Strategy); + + return fixture +} + +async function _approveStrategy(fixture, strategy, isUniswapV3) { + const { vault, harvester } = fixture; + const { governorAddr } = await getNamedAccounts(); + const sGovernor = await ethers.provider.getSigner(governorAddr); + + if (isUniswapV3) { + await vault + .connect(sGovernor) + .approveUniswapV3Strategy(strategy.address); + } else { + await vault + .connect(sGovernor) + .approveStrategy(strategy.address); + } + + await harvester + .connect(sGovernor) + .setSupportedStrategy(strategy.address, true); +} + +async function _setDefaultStrategy(fixture, asset, strategy) { + const { vault } = fixture; + const { governorAddr } = await getNamedAccounts(); + const sGovernor = await ethers.provider.getSigner(governorAddr); + await vault + .connect(sGovernor) + .setAssetDefaultStrategy( + asset.address, + strategy.address + ); +} + module.exports = { resetAllowance, defaultFixture, @@ -1188,6 +1264,7 @@ module.exports = { aaveVaultFixture, hackedVaultFixture, rebornFixture, + uniswapV3Fixture, withImpersonatedAccount, impersonateAndFundContract, impersonateAccount, diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index 6d855c5d93..f8265d5422 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -297,6 +297,9 @@ const getAssetAddresses = async (deployments) => { uniswapRouter: addresses.mainnet.uniswapRouter, uniswapV3Router: addresses.mainnet.uniswapV3Router, sushiswapRouter: addresses.mainnet.sushiswapRouter, + + UniV3PositionManager: addresses.mainnet.UniV3PositionManager, + UniV3_USDC_USDT_Pool: addresses.mainnet.UniV3_USDC_USDT_Pool, }; } else { const addressMap = { @@ -331,6 +334,9 @@ const getAssetAddresses = async (deployments) => { uniswapRouter: (await deployments.get("MockUniswapRouter")).address, uniswapV3Router: (await deployments.get("MockUniswapRouter")).address, sushiswapRouter: (await deployments.get("MockUniswapRouter")).address, + + UniV3PositionManager: (await deployments.get("MockNonfungiblePositionManager")).address, + UniV3_USDC_USDT_Pool: (await ethers.getContract("MockUniswapV3Pool")).address, }; try { diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js new file mode 100644 index 0000000000..a4374abb28 --- /dev/null +++ b/contracts/test/strategies/uniswap-v3.js @@ -0,0 +1,82 @@ +const { expect } = require("chai"); +const { uniswapV3Fixture } = require("../_fixture"); +const { loadFixture, units, ousdUnits, expectApproxSupply } = require("../helpers"); + +describe("Uniswap V3 Strategy", function () { + this.timeout(0); + + let fixture + let vault, harvester, ousd, usdc, usdt, dai, cusdc, cusdt, cdai + let reserveStrategy, uniV3Strategy, mockPool, mockPositionManager + let governor, strategist, operator, josh, matt, daniel, domen, franck + + beforeEach(async () => { + fixture = await loadFixture(uniswapV3Fixture) + reserveStrategy = fixture.compoundStrategy + uniV3Strategy = fixture.UniV3_USDC_USDT_Strategy + mockPool = fixture.UniV3_USDC_USDT_Pool + mockPositionManager = fixture.UniV3PositionManager + + ousd = fixture.ousd + usdc = fixture.usdc + usdt = fixture.usdt + dai = fixture.dai + cusdc = fixture.cusdc + cusdt = fixture.cusdt + cdai = fixture.cdai + vault = fixture.vault + harvester = fixture.harvester + governor = fixture.governor + strategist = fixture.strategist + operator = fixture.operator + josh = fixture.josh + matt = fixture.matt + daniel = fixture.daniel + domen = fixture.domen + franck = fixture.franck + }) + + const mint = async (user, amount, asset) => { + await asset.connect(user).mint(units(amount, asset)); + await asset.connect(user).approve(vault.address, units(amount, asset)); + await vault.connect(user).mint(asset.address, units(amount, asset), 0); + }; + + describe.only("Mint", function () { + it("Should deposit to reserve strategy", async () => { + // Vault has 200 DAI from fixtures + await expectApproxSupply(ousd, ousdUnits("200")); + await expect(vault).has.an.approxBalanceOf("200", dai); + + // Mint some OUSD with USDC + await mint(daniel, "10000", usdc); + await expectApproxSupply(ousd, ousdUnits("10200")); + + // Make sure it went to reserve strategy + console.log((await reserveStrategy.checkBalance(cusdc.address)).toString()); + // await expect(reserveStrategy).has.an.approxBalanceOf("10200", ); + }); + }) + + describe("Redeem", function () { + it("Should withdraw from reserve strategy", async () => { + + }) + }) + + describe("Rewards", function () { + it("Should show correct amount of fees", async () => { + + }) + }) + + describe("Rebalance", function () { + it("Should provide liquidity on given tick", async () => { + + }) + + it("Should close existing position", async () => { + + }) + }) +}) diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 1a69a1308d..dac533858d 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -138,4 +138,7 @@ addresses.mainnet.Flipper = "0xcecaD69d7D4Ed6D52eFcFA028aF8732F27e08F70"; addresses.mainnet.Morpho = "0x8888882f8f843896699869179fB6E4f7e3B58888"; addresses.mainnet.MorphoLens = "0x930f1b46e1d081ec1524efd95752be3ece51ef67"; +addresses.mainnet.UniV3PositionManager = "0xC36442b4a4522E871399CD717aBDD847Ab11FE88"; +addresses.mainnet.UniV3_USDC_USDT_Pool = "0x3416cf6c708da44db2624d63ea0aaef7113527c6"; + module.exports = addresses; diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index a882d6b498..f92d2b8ab0 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -50,7 +50,7 @@ const deployWithConfirmation = async ( ) => { // check that upgrade doesn't corrupt the storage slots if (!skipUpgradeSafety) { - await assertUpgradeIsSafe(hre, contractName); + await assertUpgradeIsSafe(hre, typeof contract == "string" ? contract : contractName); } const { deploy } = deployments; diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 0b23fed2ab..f7a1b0fee0 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -670,10 +670,10 @@ "@types/sinon-chai" "^3.2.3" "@types/web3" "1.0.19" -"@openzeppelin/contracts@3.4.1-solc-0.7-2": - version "3.4.1-solc-0.7-2" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92" - integrity sha512-tAG9LWg8+M2CMu7hIsqHPaTyG4uDzjr6mhvH96LvOpLZZj6tgzTluBt+LsCf1/QaYrlis6pITvpIaIhE+iZB+Q== +"@openzeppelin/contracts@3.4.2-solc-0.7": + version "3.4.2-solc-0.7" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz#38f4dbab672631034076ccdf2f3201fab1726635" + integrity sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA== "@openzeppelin/contracts@4.4.2": version "4.4.2" @@ -1024,22 +1024,26 @@ resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== -"@uniswap/v3-core@1.0.0", "@uniswap/v3-core@^1.0.0": +"@uniswap/v3-core@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0.tgz#6c24adacc4c25dceee0ba3ca142b35adbd7e359d" integrity sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA== -"@uniswap/v3-periphery@^1.1.1": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.2.0.tgz#fbe1a4bd23234a5b7f3e13e6abb8c051cb5b2898" - integrity sha512-4rNXGwNmiXldFowr7pAmU26XxVJpWzRUHYy7MHsCFx47fHkcAay5NQ+o89YGitLMWx+vOqFO0QGl2Tw9+XX4WA== +"@uniswap/v3-core@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.1.tgz#b6d2bdc6ba3c3fbd610bdc502395d86cd35264a0" + integrity sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ== + +"@uniswap/v3-periphery@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.4.3.tgz#a6da4632dbd46b139cc13a410e4ec09ad22bd19f" + integrity sha512-80c+wtVzl5JJT8UQskxVYYG3oZb4pkhY0zDe0ab/RX4+8f9+W5d8wI4BT0wLB0wFQTSnbW+QdBSpkHA/vRyGBA== dependencies: - "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@openzeppelin/contracts" "3.4.2-solc-0.7" "@uniswap/lib" "^4.0.1-alpha" "@uniswap/v2-core" "1.0.1" "@uniswap/v3-core" "1.0.0" base64-sol "1.0.1" - hardhat-watcher "^2.1.1" "@yarnpkg/lockfile@^1.1.0": version "1.1.0" @@ -2416,7 +2420,7 @@ chokidar@3.3.0: optionalDependencies: fsevents "~2.1.1" -chokidar@^3.4.0, chokidar@^3.4.3, chokidar@^3.5.2: +chokidar@^3.4.0, chokidar@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== @@ -4737,13 +4741,6 @@ hardhat-deploy@^0.9.1: murmur-128 "^0.2.1" qs "^6.9.4" -hardhat-watcher@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/hardhat-watcher/-/hardhat-watcher-2.1.1.tgz#8b05fec429ed45da11808bbf6054a90f3e34c51a" - integrity sha512-zilmvxAYD34IofBrwOliQn4z92UiDmt2c949DW4Gokf0vS0qk4YTfVCi/LmUBICThGygNANE3WfnRTpjCJGtDA== - dependencies: - chokidar "^3.4.3" - hardhat@^2.6.4: version "2.6.4" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.6.4.tgz#9ff3f139f697bfc4e14836a3fef3ca4c62357d65" diff --git a/package.json b/package.json index 46316b6cf3..32b8cdf3bf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "build:contracts": "(cd contracts && NODE_ENV=development yarn install && yarn run deploy)", "start": "cd dapp && yarn run start" }, - "dependencies": {}, "engines": { "node": "16.x" } From 2da919cd4e217ca3b21a69de2fd1d506340cbb7a Mon Sep 17 00:00:00 2001 From: Shahul Hameed Date: Wed, 1 Mar 2023 12:29:09 +0530 Subject: [PATCH 02/83] Switch to timelock --- .../deploy/048_deposit_withdraw_tooling.js | 2 +- .../deploy/049_uniswap_usdc_usdt_strategy.js | 58 +++++++++---------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/contracts/deploy/048_deposit_withdraw_tooling.js b/contracts/deploy/048_deposit_withdraw_tooling.js index 16fc73f04d..3e75458537 100644 --- a/contracts/deploy/048_deposit_withdraw_tooling.js +++ b/contracts/deploy/048_deposit_withdraw_tooling.js @@ -2,7 +2,7 @@ const { deploymentWithGovernanceProposal } = require("../utils/deploy"); const addresses = require("../utils/addresses"); const { isMainnet } = require("../test/helpers.js"); -module.exports = deploymentWithProposal( +module.exports = deploymentWithGovernanceProposal( { deployName: "048_deposit_withdraw_tooling", forceDeploy: false, diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index ff3bd80884..f1ec6e8ae9 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -1,6 +1,6 @@ -const { deploymentWithProposal } = require("../utils/deploy"); +const { deploymentWithGovernanceProposal } = require("../utils/deploy"); -module.exports = deploymentWithProposal( +module.exports = deploymentWithGovernanceProposal( { deployName: "049_uniswap_usdc_usdt_strategy", forceDeploy: false, @@ -12,7 +12,7 @@ module.exports = deploymentWithProposal( getTxOpts, withConfirmation, }) => { - const { deployerAddr, governorAddr, operatorAddr } = await getNamedAccounts(); + const { deployerAddr, operatorAddr, timelockAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); // Current contracts @@ -63,8 +63,6 @@ module.exports = deploymentWithProposal( cHarvesterProxy.address ); - console.log(governorAddr, await cMorphoCompProxy.governor()) - // 3. Init the proxy to point at the implementation await withConfirmation( cUniV3_USDC_USDT_Proxy @@ -96,7 +94,7 @@ module.exports = deploymentWithProposal( await withConfirmation( cUniV3_USDC_USDT_Strategy .connect(sDeployer) - .transferGovernance(governorAddr, await getTxOpts()) + .transferGovernance(timelockAddr, await getTxOpts()) ); console.log("Uniswap V3 (USDC-USDT pool) strategy address: ", cUniV3_USDC_USDT_Strategy.address); @@ -117,30 +115,30 @@ module.exports = deploymentWithProposal( signature: "setAdminImpl(address)", args: [dVaultAdmin.address], }, - // // 1. Accept governance of new cUniV3_USDC_USDT_Strategy - // { - // contract: cUniV3_USDC_USDT_Strategy, - // signature: "claimGovernance()", - // args: [], - // }, - // // 2. Add new strategy to vault - // { - // contract: cVaultAdmin, - // signature: "approveUniswapV3Strategy(address)", - // args: [cUniV3_USDC_USDT_Proxy.address], - // }, - // // 3. Set supported strategy on Harvester - // { - // contract: cHarvester, - // signature: "setSupportedStrategy(address,bool)", - // args: [cUniV3_USDC_USDT_Proxy.address, true], - // }, - // // 4. Set harvester address - // { - // contract: cUniV3_USDC_USDT_Strategy, - // signature: "setHarvesterAddress(address)", - // args: [cHarvesterProxy.address], - // }, + // 1. Accept governance of new cUniV3_USDC_USDT_Strategy + { + contract: cUniV3_USDC_USDT_Strategy, + signature: "claimGovernance()", + args: [], + }, + // 2. Add new strategy to vault + { + contract: cVaultAdmin, + signature: "approveUniswapV3Strategy(address)", + args: [cUniV3_USDC_USDT_Proxy.address], + }, + // 3. Set supported strategy on Harvester + { + contract: cHarvester, + signature: "setSupportedStrategy(address,bool)", + args: [cUniV3_USDC_USDT_Proxy.address, true], + }, + // 4. Set harvester address + { + contract: cUniV3_USDC_USDT_Strategy, + signature: "setHarvesterAddress(address)", + args: [cHarvesterProxy.address], + }, ], }; } From 8fd2464c01870d3c1b27fccded3dc50b23a7a909 Mon Sep 17 00:00:00 2001 From: Shahul Hameed Date: Wed, 1 Mar 2023 13:45:48 +0530 Subject: [PATCH 03/83] unblock myself --- contracts/node.sh | 2 +- contracts/utils/deploy.js | 2 +- contracts/utils/governor.js | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/node.sh b/contracts/node.sh index 52adb733c2..1c283b763a 100755 --- a/contracts/node.sh +++ b/contracts/node.sh @@ -2,7 +2,7 @@ trap "exit" INT TERM ERR trap "kill 0" EXIT #nodeWaitTimeout=120 -nodeWaitTimeout=12000 +nodeWaitTimeout=1200 RED='\033[0;31m' NO_COLOR='\033[0m' diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index 5739075c07..a2b62d661c 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -791,7 +791,7 @@ function deploymentWithProposal(opts, fn) { return; } - await sanityCheck(); + await sanityCheckOgvGovernance(); const proposal = await fn(tools); const propDescription = proposal.name; const propArgs = await proposeArgs(proposal.actions); diff --git a/contracts/utils/governor.js b/contracts/utils/governor.js index ab07160728..df2d47cfae 100644 --- a/contracts/utils/governor.js +++ b/contracts/utils/governor.js @@ -36,13 +36,13 @@ async function proposeArgs(governorArgsArray) { * @returns {Promise<*[]>} */ async function proposeGovernanceArgs(governorArgsArray) { - const args = await proposeArgs(governorArgsArray); + const [targets, sigs, calldata] = await proposeArgs(governorArgsArray); return [ - args[0], - Array(governorArgsArray).fill(BigNumber.from(0)), - args[1], - args[2], + targets, + Array(governorArgsArray.length).fill(BigNumber.from(0)), + sigs, + calldata, ]; } From 5c2d34aecbf336ae75587ed83fc21646001f7451 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 4 Mar 2023 16:41:55 +0530 Subject: [PATCH 04/83] More updates --- .../GeneralizedUniswapV3Strategy.json | 983 ++++++++++++++++++ brownie/scripts/uniswap_v3.py | 45 + brownie/uniswap-v3.py | 3 - brownie/world.py | 1 + contracts/contracts/interfaces/IVault.sol | 6 +- .../v3/INonfungiblePositionManager.sol | 5 +- .../uniswap/v3/IUniswapV3Helper.sol | 5 +- contracts/contracts/mocks/MockStrategy.sol | 66 ++ .../v3/MockNonfungiblePositionManager.sol | 84 +- .../mocks/uniswap/v3/MockUniswapV3Pool.sol | 44 +- contracts/contracts/proxies/Proxies.sol | 8 +- .../GeneralizedUniswapV3Strategy.sol | 475 +++++++-- contracts/contracts/utils/UniswapV3Helper.sol | 32 +- contracts/contracts/vault/VaultAdmin.sol | 47 +- contracts/contracts/vault/VaultCore.sol | 27 +- contracts/contracts/vault/VaultStorage.sol | 4 +- contracts/deploy/000_mock.js | 14 +- contracts/deploy/001_core.js | 39 +- .../deploy/049_uniswap_usdc_usdt_strategy.js | 22 +- contracts/hardhat.config.js | 10 +- contracts/test/_fixture.js | 81 +- contracts/test/helpers.js | 86 +- .../test/strategies/uniswap-v3.fork-test.js | 323 ++++++ contracts/test/strategies/uniswap-v3.js | 89 +- contracts/utils/addresses.js | 6 +- contracts/utils/deploy.js | 1 + 26 files changed, 2190 insertions(+), 316 deletions(-) create mode 100644 brownie/interfaces/GeneralizedUniswapV3Strategy.json create mode 100644 brownie/scripts/uniswap_v3.py delete mode 100644 brownie/uniswap-v3.py create mode 100644 contracts/contracts/mocks/MockStrategy.sol create mode 100644 contracts/test/strategies/uniswap-v3.fork-test.js diff --git a/brownie/interfaces/GeneralizedUniswapV3Strategy.json b/brownie/interfaces/GeneralizedUniswapV3Strategy.json new file mode 100644 index 0000000000..cfe4160f36 --- /dev/null +++ b/brownie/interfaces/GeneralizedUniswapV3Strategy.json @@ -0,0 +1,983 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_pToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousGovernor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newGovernor", + "type": "address" + } + ], + "name": "GovernorshipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_oldHarvesterAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_newHarvesterAddress", + "type": "address" + } + ], + "name": "HarvesterAddressesUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "OperatorChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_pToken", + "type": "address" + } + ], + "name": "PTokenAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_pToken", + "type": "address" + } + ], + "name": "PTokenRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousGovernor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newGovernor", + "type": "address" + } + ], + "name": "PendingGovernorshipTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "strategy", + "type": "address" + } + ], + "name": "ReserveStrategyChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address[]", + "name": "_oldAddresses", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "_newAddresses", + "type": "address[]" + } + ], + "name": "RewardTokenAddressesUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "rewardToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RewardTokenCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "UniswapV3FeeCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0Sent", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1Sent", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidityMinted", + "type": "uint128" + } + ], + "name": "UniswapV3LiquidityAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0Received", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1Received", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidityBurned", + "type": "uint128" + } + ], + "name": "UniswapV3LiquidityRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0Received", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1Received", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidityBurned", + "type": "uint128" + } + ], + "name": "UniswapV3PositionClosed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_pToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + }, + { + "inputs": [], + "name": "_deprecated_rewardLiquidationThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "_deprecated_rewardTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "assetToPToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + } + ], + "name": "checkBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claimGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "closeActivePosition", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "closePosition", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "collectRewardTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "depositAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getPendingRewards", + "outputs": [ + { + "internalType": "uint128", + "name": "amount0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRewardTokenAddresses", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governor", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "harvesterAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_vaultAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_poolAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_nonfungiblePositionManager", + "type": "address" + }, + { + "internalType": "address", + "name": "_token0ReserveStrategy", + "type": "address" + }, + { + "internalType": "address", + "name": "_token1ReserveStrategy", + "type": "address" + }, + { + "internalType": "address", + "name": "_operator", + "type": "address" + }, + { + "internalType": "address", + "name": "_uniswapV3Helper", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_platformAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_vaultAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_rewardTokenAddresses", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_assets", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_pTokens", + "type": "address[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "isGovernor", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nonfungiblePositionManager", + "outputs": [ + { + "internalType": "contract INonfungiblePositionManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onERC721Received", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "operatorAddr", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "platformAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolFee", + "outputs": [ + { + "internalType": "uint24", + "name": "", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "maxAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxAmount1", + "type": "uint256" + }, + { + "internalType": "int24", + "name": "lowerTick", + "type": "int24" + }, + { + "internalType": "int24", + "name": "upperTick", + "type": "int24" + } + ], + "name": "rebalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_assetIndex", + "type": "uint256" + } + ], + "name": "removePToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "reserveStrategy", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "resetAllowanceOfTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "rewardTokenAddresses", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "safeApproveAllTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_harvesterAddress", + "type": "address" + } + ], + "name": "setHarvesterAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_operator", + "type": "address" + } + ], + "name": "setOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "internalType": "address", + "name": "_pToken", + "type": "address" + } + ], + "name": "setPTokenAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token0ReserveStrategy", + "type": "address" + }, + { + "internalType": "address", + "name": "_token1ReserveStrategy", + "type": "address" + } + ], + "name": "setReserveStrategy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_rewardTokenAddresses", + "type": "address[]" + } + ], + "name": "setRewardTokenAddresses", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + } + ], + "name": "supportsAsset", + "outputs": [ + { + "internalType": "bool", + "name": "", + "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": [ + { + "internalType": "address", + "name": "_newGovernor", + "type": "address" + } + ], + "name": "transferGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "transferToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vaultAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/brownie/scripts/uniswap_v3.py b/brownie/scripts/uniswap_v3.py new file mode 100644 index 0000000000..20865775a6 --- /dev/null +++ b/brownie/scripts/uniswap_v3.py @@ -0,0 +1,45 @@ +from world import * +from brownie import interface, accounts + +user1 = accounts.at("0x0000000000000000000000000000000000000001", force=True) +user2 = accounts.at("0x0000000000000000000000000000000000000002", force=True) + +uni_usdc_usdt_proxy = "0xa863A50233FB5Aa5aFb515e6C3e6FB9c075AA594" +uni_usdc_usdt_strat = interface.GeneralizedUniswapV3Strategy(uni_usdc_usdt_proxy) + +def get_some_balance(user): + USDT_BAGS = '0x5754284f345afc66a98fbb0a0afe71e0f007b949' + USDT_BAGS_2 = '0x5041ed759dd4afc3a72b8192c143f72f4724081a' + USDC_BAGS = '0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf' + USDC_BAGS_2 = '0x0a59649758aa4d66e25f08dd01271e891fe52199' + + usdt.transfer(user.address, int(usdt.balanceOf(USDT_BAGS) / 10), {'from': USDT_BAGS}) + # usdt.transfer(user.address, int(usdt.balanceOf(USDT_BAGS_2) / 10), {'from': USDT_BAGS_2}) + usdc.transfer(user.address, int(usdc.balanceOf(USDC_BAGS) / 10), {'from': USDC_BAGS}) + # usdc.transfer(user.address, int(usdc.balanceOf(USDC_BAGS_2) / 10), {'from': USDC_BAGS_2}) + + usdt.approve(vault_core.address, int(0), {'from': user}) + usdc.approve(vault_core.address, int(0), {'from': user}) + print("Loaded wallets with some funds") + +def set_as_default_strategy(): + vault_admin.setAssetDefaultStrategy(usdt.address, uni_usdc_usdt_proxy, {'from': TIMELOCK}) + vault_admin.setAssetDefaultStrategy(usdc.address, uni_usdc_usdt_proxy, {'from': TIMELOCK}) + print("Uniswap V3 set as default strategy") + +def main(): + brownie.chain.snapshot() + + try: + get_some_balance(user1) + # get_some_balance(user2) + + set_as_default_strategy() + + print("Trying to mint") + # vault_core.mint(usdt.address, 10000 * 1000000, 0, {'from': user1}) + vault_core.mint(usdc.address, 10000 * 1000000, 0, {'from': user1}) + except Exception as e: + print("Exception", e) + + brownie.chain.revert() \ No newline at end of file diff --git a/brownie/uniswap-v3.py b/brownie/uniswap-v3.py deleted file mode 100644 index e93e08d2bb..0000000000 --- a/brownie/uniswap-v3.py +++ /dev/null @@ -1,3 +0,0 @@ -from world import * - - diff --git a/brownie/world.py b/brownie/world.py index a1be42dc23..00e661db26 100644 --- a/brownie/world.py +++ b/brownie/world.py @@ -55,6 +55,7 @@ def load_contract(name, address): stkaave = load_contract('stkaave', '0x4da27a545c0c5B758a6BA100e3a049001de870f5') strategist = brownie.accounts.at(STRATEGIST, force=True) +timelock = brownie.accounts.at(TIMELOCK, force=True) gova = brownie.accounts.at(GOVERNOR, force=True) governor = load_contract('governor', GOVERNOR) governor_five = load_contract('governor_five', GOVERNOR_FIVE) diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index 1bee56dec6..d4d3fd70e4 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -175,5 +175,9 @@ interface IVault { function depositForUniswapV3(address asset, uint256 amount) external; - function withdrawForUniswapV3(address recipient, address asset, uint256 amount) external; + function withdrawForUniswapV3( + address recipient, + address asset, + uint256 amount + ) external; } diff --git a/contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol b/contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol index ae3b4d6b1b..c117bbbf15 100644 --- a/contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol +++ b/contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol @@ -134,7 +134,10 @@ interface INonfungiblePositionManager { /// 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); + 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. diff --git a/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol b/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol index 7397e7d880..092f94a8d4 100644 --- a/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol +++ b/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol @@ -17,5 +17,8 @@ interface IUniswapV3Helper { uint256 amount1 ) external view returns (uint128 liquidity); - function getSqrtRatioAtTick(int24 tick) external view returns (uint160 sqrtPriceX96); + function getSqrtRatioAtTick(int24 tick) + external + view + returns (uint160 sqrtPriceX96); } diff --git a/contracts/contracts/mocks/MockStrategy.sol b/contracts/contracts/mocks/MockStrategy.sol new file mode 100644 index 0000000000..4b75d27839 --- /dev/null +++ b/contracts/contracts/mocks/MockStrategy.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MockStrategy { + using SafeERC20 for IERC20; + + address public vaultAddress; + mapping(address => bool) public supportsAsset; + address[] internal allAssets; + + modifier onlyVault() { + require(msg.sender == vaultAddress, "Not vault"); + _; + } + + constructor(address _vaultAddress, address[] memory assets) { + vaultAddress = _vaultAddress; + for (uint256 i = 0; i < assets.length; i++) { + address asset = assets[i]; + supportsAsset[asset] = true; + allAssets.push(asset); + } + } + + function deposit(address _asset, uint256 _amount) public onlyVault { + IERC20(_asset).safeTransferFrom(msg.sender, address(this), _amount); + } + + function depositAll() public onlyVault { + // Do nothing + } + + function withdraw(address _asset, uint256 _amount) public onlyVault { + IERC20(_asset).safeTransfer(vaultAddress, _amount); + } + + function checkBalance(address _asset) + external + view + returns (uint256 balance) + { + balance = IERC20(_asset).balanceOf(address(this)); + } + + function withdrawAll() public onlyVault { + for (uint256 i = 0; i < allAssets.length; i++) { + IERC20 asset = IERC20(allAssets[i]); + asset.safeTransfer(vaultAddress, asset.balanceOf(address(this))); + } + } + + function collectRewardTokens() external { + // Do nothing + } + + function getRewardTokenAddresses() + external + view + returns (address[] memory) + { + return new address[](0); + } +} diff --git a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol index 31417df8b0..9c1d956a85 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IUniswapV3Helper } from '../../../interfaces/uniswap/v3/IUniswapV3Helper.sol'; -import { IMockUniswapV3Pool } from './MockUniswapV3Pool.sol'; +import { IUniswapV3Helper } from "../../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; +import { IMockUniswapV3Pool } from "./MockUniswapV3Pool.sol"; contract MockNonfungiblePositionManager { using SafeERC20 for IERC20; @@ -63,7 +63,7 @@ contract MockNonfungiblePositionManager { uint128 amount1Max; } - mapping (uint256 => MockPosition) public mockPositions; + mapping(uint256 => MockPosition) public mockPositions; uint256 public slippage = 100; @@ -72,25 +72,29 @@ contract MockNonfungiblePositionManager { uint256 internal tokenCount = 0; - constructor (address _helper, address _mockPool) { + constructor(address _helper, address _mockPool) { uniswapV3Helper = IUniswapV3Helper(_helper); mockPool = IMockUniswapV3Pool(_mockPool); } - function positions(uint256 tokenId) external view returns ( - uint96 nonce, - address operator, - address token0, - address token1, - uint24 fee, - int24 tickLower, - int24 tickUpper, - uint128 liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ) { + function positions(uint256 tokenId) + external + view + returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) + { MockPosition memory p = mockPositions[tokenId]; return ( 0, @@ -108,7 +112,11 @@ contract MockNonfungiblePositionManager { ); } - function setTokensOwed(uint256 tokenId, uint128 token0, uint128 token1) public { + function setTokensOwed( + uint256 tokenId, + uint128 token0, + uint128 token1 + ) public { MockPosition storage p = mockPositions[tokenId]; p.token0Owed = token0; p.token1Owed = token1; @@ -122,7 +130,7 @@ contract MockNonfungiblePositionManager { uint128 liquidity, uint256 amount0, uint256 amount1 - ) + ) { tokenCount += 1; tokenId = tokenCount; @@ -156,8 +164,16 @@ contract MockNonfungiblePositionManager { liquidity ); - IERC20(params.token0).safeTransferFrom(msg.sender, address(this), amount0); - IERC20(params.token1).safeTransferFrom(msg.sender, address(this), amount1); + IERC20(params.token0).safeTransferFrom( + msg.sender, + address(this), + amount0 + ); + IERC20(params.token1).safeTransferFrom( + msg.sender, + address(this), + amount1 + ); p.liquidity += liquidity; @@ -165,11 +181,15 @@ contract MockNonfungiblePositionManager { require(amount1 >= params.amount1Min, "V3 Liquidity error"); } - function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable returns ( - uint128 liquidity, - uint256 amount0, - uint256 amount1 - ) { + function increaseLiquidity(IncreaseLiquidityParams calldata params) + external + payable + returns ( + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { MockPosition storage p = mockPositions[params.tokenId]; (liquidity) = uniswapV3Helper.getLiquidityForAmounts( @@ -199,7 +219,7 @@ contract MockNonfungiblePositionManager { function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable - returns (uint256 amount0, uint256 amount1) + returns (uint256 amount0, uint256 amount1) { MockPosition storage p = mockPositions[params.tokenId]; @@ -215,10 +235,10 @@ contract MockNonfungiblePositionManager { p.liquidity -= params.liquidity; } - function collect(CollectParams calldata params) - external - payable - returns (uint256 amount0, uint256 amount1) + function collect(CollectParams calldata params) + external + payable + returns (uint256 amount0, uint256 amount1) { MockPosition storage p = mockPositions[params.tokenId]; diff --git a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol index f2c2239f99..8a93d94cd6 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.0; -import { IUniswapV3Helper } from '../../../interfaces/uniswap/v3/IUniswapV3Helper.sol'; +import { IUniswapV3Helper } from "../../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; contract MockUniswapV3Pool { address public immutable token0; @@ -12,31 +12,32 @@ contract MockUniswapV3Pool { int24 public mockTick; IUniswapV3Helper internal uniswapV3Helper; - constructor (address _token0, address _token1, uint24 _fee, address _helper) { + constructor( + address _token0, + address _token1, + uint24 _fee, + address _helper + ) { token0 = _token0; token1 = _token1; fee = _fee; uniswapV3Helper = IUniswapV3Helper(_helper); } - function slot0() public view returns ( - uint160 sqrtPriceX96, - int24 tick, - uint16 observationIndex, - uint16 observationCardinality, - uint16 observationCardinalityNext, - uint8 feeProtocol, - bool unlocked - ) { - return ( - mockSqrtPriceX96, - mockTick, - 0, - 0, - 0, - 0, - true - ); + function slot0() + public + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ) + { + return (mockSqrtPriceX96, mockTick, 0, 0, 0, 0, true); } function setTick(int24 tick) public { @@ -52,5 +53,6 @@ contract MockUniswapV3Pool { interface IMockUniswapV3Pool { function setTick(int24 tick) external; + function mockSqrtPriceX96() external returns (uint160); -} \ No newline at end of file +} diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index bb4bc0a5f4..69f811f743 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -90,9 +90,13 @@ contract ConvexLUSDMetaStrategyProxy is InitializeGovernedUpgradeabilityProxy { /** * @notice MorphoAaveStrategyProxy delegates calls to a MorphoAaveStrategy implementation */ -contract MorphoAaveStrategyProxy is InitializeGovernedUpgradeabilityProxy {} +contract MorphoAaveStrategyProxy is InitializeGovernedUpgradeabilityProxy { + +} /** * @notice UniV3_USDC_USDT_Proxy delegates calls to a GeneralizedUniswapV3Strategy implementation */ -contract UniV3_USDC_USDT_Proxy is InitializeGovernedUpgradeabilityProxy {} +contract UniV3_USDC_USDT_Proxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index 84089427bc..01055749d6 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -9,18 +9,44 @@ import { IStrategy } from "../interfaces/IStrategy.sol"; import { IVault } from "../interfaces/IVault.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; -import { INonfungiblePositionManager } from '../interfaces/uniswap/v3/INonfungiblePositionManager.sol'; -import { IUniswapV3Helper } from '../interfaces/uniswap/v3/IUniswapV3Helper.sol'; +import { INonfungiblePositionManager } from "../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; +import { IUniswapV3Helper } from "../interfaces/uniswap/v3/IUniswapV3Helper.sol"; contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { using SafeERC20 for IERC20; event OperatorChanged(address _address); - event ReserveStrategyChanged(address indexed token, address strategy); - event UniswapV3FeeCollected(uint256 indexed tokenId, uint256 amount0, uint256 amount1); - event UniswapV3LiquidityAdded(uint256 indexed tokenId, uint256 amount0Sent, uint256 amount1Sent, uint128 liquidityMinted); - event UniswapV3LiquidityRemoved(uint256 indexed tokenId, uint256 amount0Received, uint256 amount1Received, uint128 liquidityBurned); - event UniswapV3PositionClosed(uint256 indexed tokenId, uint256 amount0Received, uint256 amount1Received, uint128 liquidityBurned); + event ReserveStrategiesChanged( + address token0Strategy, + address token1Strategy + ); + event UniswapV3FeeCollected( + uint256 indexed tokenId, + uint256 amount0, + uint256 amount1 + ); + 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 + ); + event UniswapV3PositionClosed( + uint256 indexed tokenId, + uint256 amount0Received, + uint256 amount1Received + ); // Address of operator address public operatorAddr; @@ -28,23 +54,28 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { address public token1; uint24 public poolFee; - uint24 internal constant MAX_SLIPPAGE = 100; // 1% + uint24 internal maxSlippage = 100; // 1% mapping(address => address) public reserveStrategy; INonfungiblePositionManager public nonfungiblePositionManager; struct Position { + bytes32 positionKey; uint256 tokenId; uint128 liquidity; int24 lowerTick; int24 upperTick; + bool exists; + // 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; } - mapping (int48 => uint256) internal ticksToTokenId; - mapping (uint256 => Position) internal tokenIdToPosition; + mapping(int48 => uint256) internal ticksToTokenId; + mapping(uint256 => Position) internal tokenIdToPosition; uint256[] internal allTokenIds; uint256 internal currentPositionTokenId; @@ -54,11 +85,25 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256[50] private __gap; /** - * @dev Ensures that the caller is Governor, Strategist or Operator. - */ + * @dev Ensures that the caller is Governor or Strategist. + */ + modifier onlyGovernorOrStrategist() { + require( + msg.sender == IVault(vaultAddress).strategistAddr() || + msg.sender == governor(), + "Caller is not the Operator, Strategist or Governor" + ); + _; + } + + /** + * @dev Ensures that the caller is Governor, Strategist or Operator. + */ modifier onlyGovernorOrStrategistOrOperator() { require( - msg.sender == operatorAddr || msg.sender == IVault(vaultAddress).strategistAddr() || isGovernor(), + msg.sender == operatorAddr || + msg.sender == IVault(vaultAddress).strategistAddr() || + msg.sender == governor(), "Caller is not the Operator, Strategist or Governor" ); _; @@ -73,7 +118,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { address _operator, address _uniswapV3Helper ) external onlyGovernor initializer { - nonfungiblePositionManager = INonfungiblePositionManager(_nonfungiblePositionManager); + nonfungiblePositionManager = INonfungiblePositionManager( + _nonfungiblePositionManager + ); IUniswapV3Pool pool = IUniswapV3Pool(_poolAddress); uniswapV3Helper = IUniswapV3Helper(_uniswapV3Helper); @@ -94,7 +141,16 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ); _setReserveStrategy(_token0ReserveStrategy, _token1ReserveStrategy); + } + function setMaxSlippage(uint24 _slippage) + external + onlyGovernorOrStrategist + { + require(_slippage <= 10000, "Invalid slippage value"); + // TODO: Should we make sure that Governor doesn't + // accidentally set slippage > 2% or something??? + maxSlippage = _slippage; } function setOperator(address _operator) external onlyGovernor { @@ -103,19 +159,33 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { emit OperatorChanged(_operator); } - function setReserveStrategy(address _token0ReserveStrategy, address _token1ReserveStrategy) external onlyGovernorOrStrategistOrOperator nonReentrant { + function setReserveStrategy( + address _token0ReserveStrategy, + address _token1ReserveStrategy + ) external onlyGovernorOrStrategistOrOperator nonReentrant { _setReserveStrategy(_token0ReserveStrategy, _token1ReserveStrategy); } - function _setReserveStrategy(address _token0ReserveStrategy, address _token1ReserveStrategy) internal { - // require(IStrategy(_token0ReserveStrategy).supportsAsset(token0), "Invalid Reserve Strategy"); - // require(IStrategy(_token1ReserveStrategy).supportsAsset(token1), "Invalid Reserve Strategy"); + function _setReserveStrategy( + address _token0ReserveStrategy, + address _token1ReserveStrategy + ) internal { + require( + IStrategy(_token0ReserveStrategy).supportsAsset(token0), + "Invalid Reserve Strategy" + ); + require( + IStrategy(_token1ReserveStrategy).supportsAsset(token1), + "Invalid Reserve Strategy" + ); reserveStrategy[token0] = _token0ReserveStrategy; reserveStrategy[token1] = _token1ReserveStrategy; - emit ReserveStrategyChanged(token0, _token0ReserveStrategy); - emit ReserveStrategyChanged(token1, _token1ReserveStrategy); + emit ReserveStrategiesChanged( + _token0ReserveStrategy, + _token1ReserveStrategy + ); } function deposit(address _asset, uint256 _amount) @@ -128,6 +198,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } function depositAll() external override onlyVault nonReentrant { + _depositAll(); + } + + function _depositAll() internal { uint256 token0Bal = IERC20(token0).balanceOf(address(this)); uint256 token1Bal = IERC20(token1).balanceOf(address(this)); if (token0Bal > 0) { @@ -143,23 +217,41 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { address asset, uint256 amount ) external override onlyVault nonReentrant { - uint256 reserveBalance = IStrategy(reserveStrategy[asset]).checkBalance(asset); - if (reserveBalance >= amount) { - IVault(vaultAddress).withdrawForUniswapV3(recipient, asset, amount); - return; - } + uint256 reserveBalance = IStrategy(reserveStrategy[asset]).checkBalance( + asset + ); - uint256 amountToPullOut = amount - reserveBalance; - // TODO: Remove liquidity from pool + // TODO: Remove liquidity from pool instead? + require(reserveBalance >= amount, "Liquidity error"); + + IVault(vaultAddress).withdrawForUniswapV3(recipient, asset, amount); } function withdrawAll() external override onlyVault nonReentrant { if (currentPositionTokenId > 0) { _closePosition(currentPositionTokenId); } + + IERC20 cToken0 = IERC20(token0); + IERC20 cToken1 = IERC20(token1); + + uint256 token0Balance = cToken0.balanceOf(address(this)); + if (token0Balance >= 0) { + cToken0.safeTransfer(vaultAddress, token0Balance); + } + + uint256 token1Balance = cToken1.balanceOf(address(this)); + if (token1Balance >= 0) { + cToken1.safeTransfer(vaultAddress, token1Balance); + } } - function collectRewardTokens() external override onlyHarvester nonReentrant { + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { for (uint256 i = 0; i < allTokenIds.length; i++) { uint256 tokenId = allTokenIds[0]; if (tokenIdToPosition[tokenId].liquidity > 0) { @@ -168,14 +260,25 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } } - function getPendingRewards() external view returns (uint128 amount0, uint128 amount1) { - if (currentPositionTokenId > 0) { - (amount0, amount1) = _getTokensOwed(currentPositionTokenId); - } + function getPendingRewards() + external + view + returns (uint128 amount0, uint128 amount1) + { + Position memory p = tokenIdToPosition[currentPositionTokenId]; + + (amount0, amount1) = _getTokensOwed(p); } - function _getTokensOwed(uint256 tokenId) internal view returns (uint128 tokensOwed0, uint128 tokensOwed1) { - (, , , , , , , , , , tokensOwed0, tokensOwed1) = nonfungiblePositionManager.positions(tokenId); + function _getTokensOwed(Position memory p) + internal + view + returns (uint128 tokensOwed0, uint128 tokensOwed1) + { + if (!p.exists) return (0, 0); + + (, , , tokensOwed0, tokensOwed1) = IUniswapV3Pool(platformAddress) + .positions(p.positionKey); } function checkBalance(address _asset) @@ -188,7 +291,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // TODO: Should reserve strategy balance be included? Might result in double calculations balance = IERC20(_asset).balanceOf(address(this)); - (uint160 sqrtRatioX96, , , , , ,) = IUniswapV3Pool(platformAddress).slot0(); + (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(platformAddress) + .slot0(); if (currentPositionTokenId > 0) { Position memory p = tokenIdToPosition[currentPositionTokenId]; @@ -196,13 +300,17 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } // for (uint256 i = 0; i < allTokenIds.length; i++) { - // // TODO: Should only current active position be checked? + // // TODO: Should only current active position be checked? // Position memory p = tokenIdToPosition[allTokenIds[i]]; // balance += _checkAssetBalanceOfPosition(_asset, p, sqrtRatioX96); // } } - function _checkAssetBalanceOfPosition(address asset, Position memory p, uint160 sqrtRatioX96) internal view returns (uint256 balance) { + function _checkAssetBalanceOfPosition( + address asset, + Position memory p, + uint160 sqrtRatioX96 + ) internal view returns (uint256 balance) { if (asset == token0) { (balance, ) = _checkBalanceOfPosition(p, sqrtRatioX96); } else { @@ -210,91 +318,145 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } } - function _checkBalanceOfPosition(Position memory p, uint160 sqrtRatioX96) internal view returns (uint256 amount0, uint256 amount1) { + function _checkBalanceOfPosition(Position memory p, uint160 sqrtRatioX96) + internal + view + returns (uint256 amount0, uint256 amount1) + { if (p.liquidity == 0) { // NOTE: Making the assumption that tokens owed for inactive positions - // will always be zero (should be case since fees are collecting after + // will always be zero (should be case since fees are collecting after // liquidity is removed) return (0, 0); } (amount0, amount1) = uniswapV3Helper.getAmountsForLiquidity( sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, p.liquidity ); - (uint128 feeAmount0, uint128 feeAmount1) = _getTokensOwed(p.tokenId); + (uint128 feeAmount0, uint128 feeAmount1) = _getTokensOwed(p); amount0 += feeAmount0; amount1 += feeAmount1; } - function rebalance(uint256 maxAmount0, uint256 maxAmount1, int24 lowerTick, int24 upperTick) external onlyGovernorOrStrategistOrOperator nonReentrant { - if (currentPositionTokenId > 0) { - _closePosition(currentPositionTokenId); - } - + function _withdrawForLiquidity(uint256 minAmount0, uint256 minAmount1) + internal + { IERC20 cToken0 = IERC20(token0); IERC20 cToken1 = IERC20(token1); IVault vault = IVault(vaultAddress); // Withdraw enough funds from Reserve strategies uint256 token0Balance = cToken0.balanceOf(address(this)); - if (token0Balance < maxAmount0) { - vault.withdrawForUniswapV3(address(this), token0, maxAmount0 - token0Balance); + if (token0Balance < minAmount0) { + vault.withdrawForUniswapV3( + address(this), + token0, + minAmount0 - token0Balance + ); } uint256 token1Balance = cToken1.balanceOf(address(this)); - if (token1Balance < maxAmount1) { - vault.withdrawForUniswapV3(address(this), token1, maxAmount1 - token1Balance); + if (token1Balance < minAmount1) { + vault.withdrawForUniswapV3( + address(this), + token1, + minAmount1 - token1Balance + ); } + } - // Provide liquidity + function rebalance( + uint256 maxAmount0, + uint256 maxAmount1, + int24 lowerTick, + int24 upperTick + ) external onlyGovernorOrStrategistOrOperator nonReentrant { int48 tickKey = _getTickPositionKey(lowerTick, upperTick); uint256 tokenId = ticksToTokenId[tickKey]; + if (currentPositionTokenId > 0) { + // Close any active position + _closePosition(currentPositionTokenId); + } + + // Withdraw enough funds from Reserve strategies + _withdrawForLiquidity(maxAmount0, maxAmount1); + + // Provide liquidity if (tokenId > 0) { // Add liquidity to the position token Position storage p = tokenIdToPosition[tokenId]; _increaseLiquidityForPosition(p, maxAmount0, maxAmount1); } else { // Mint new position - (tokenId, , ,) = _mintPosition(maxAmount0, maxAmount1, lowerTick, upperTick); + (tokenId, , , ) = _mintPosition( + maxAmount0, + maxAmount1, + lowerTick, + upperTick + ); } + // Mark it as active position + currentPositionTokenId = tokenId; + // Move any leftovers to Reserve - uint256 token0Dust = cToken0.balanceOf(address(this)); - if (token0Dust > 0) { - vault.depositForUniswapV3(token0, token0Dust); - } + _depositAll(); + } - uint256 token1Dust = cToken1.balanceOf(address(this)); - if (token1Dust > 0) { - vault.depositForUniswapV3(token1, token1Dust); - } + function increaseLiquidityForActivePosition( + uint256 maxAmount0, + uint256 maxAmount1 + ) external onlyGovernorOrStrategistOrOperator nonReentrant { + require(currentPositionTokenId > 0, "No active position"); - currentPositionTokenId = tokenId; + // Withdraw enough funds from Reserve strategies + _withdrawForLiquidity(maxAmount0, maxAmount1); + + Position storage p = tokenIdToPosition[currentPositionTokenId]; + _increaseLiquidityForPosition(p, maxAmount0, maxAmount1); + + // Deposit all dust back to reserve strategies + _depositAll(); } - function closeActivePosition() external onlyGovernorOrStrategistOrOperator nonReentrant { + function closeActivePosition() + external + onlyGovernorOrStrategistOrOperator + nonReentrant + { require(currentPositionTokenId > 0, "No active position"); _closePosition(currentPositionTokenId); } - function closePosition(uint256 tokenId) external onlyGovernorOrStrategistOrOperator nonReentrant { - require(tokenIdToPosition[tokenId].liquidity > 0, "Invalid position"); + function closePosition(uint256 tokenId) + external + onlyGovernorOrStrategistOrOperator + nonReentrant + { + require(tokenIdToPosition[tokenId].exists, "Invalid position"); _closePosition(tokenId); } - function _getTickPositionKey(int24 lowerTick, int24 upperTick) internal returns (int48 key) { - if (lowerTick > upperTick) (lowerTick, upperTick) = (upperTick, lowerTick); + function _getTickPositionKey(int24 lowerTick, int24 upperTick) + internal + returns (int48 key) + { + if (lowerTick > upperTick) + (lowerTick, upperTick) = (upperTick, lowerTick); key = int48(lowerTick) * 2**24; // Shift by 24 bits key = key + int24(upperTick); } - function _closePosition(uint256 tokenId) internal returns (uint256 amount0, uint256 amount1) { + function _closePosition(uint256 tokenId) + internal + returns (uint256 amount0, uint256 amount1) + { Position storage p = tokenIdToPosition[tokenId]; if (p.liquidity == 0) { @@ -305,7 +467,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { (amount0, amount1) = _decreaseLiquidityForPosition(p, p.liquidity); // Collect all fees for position - (uint256 amount0Fee, uint256 amount1Fee) = _collectFeesForToken(tokenId); + (uint256 amount0Fee, uint256 amount1Fee) = _collectFeesForToken( + tokenId + ); amount0 = amount0 + amount0Fee; amount1 = amount1 + amount1Fee; @@ -313,24 +477,43 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { if (tokenId == currentPositionTokenId) { currentPositionTokenId = 0; } + + emit UniswapV3PositionClosed(tokenId, amount0, amount1); } - function _collectFeesForToken(uint256 tokenId) internal returns (uint256 amount0, uint256 amount1) { - INonfungiblePositionManager.CollectParams memory params = INonfungiblePositionManager.CollectParams({ - tokenId: tokenId, - recipient: address(this), - amount0Max: type(uint128).max, - amount1Max: type(uint128).max - }); + function _collectFeesForToken(uint256 tokenId) + internal + returns (uint256 amount0, uint256 amount1) + { + INonfungiblePositionManager.CollectParams + memory params = INonfungiblePositionManager.CollectParams({ + tokenId: tokenId, + recipient: address(this), + amount0Max: type(uint128).max, + amount1Max: type(uint128).max + }); (amount0, amount1) = nonfungiblePositionManager.collect(params); emit UniswapV3FeeCollected(tokenId, amount0, amount1); } - function _mintPosition(uint256 maxAmount0, uint256 maxAmount1, int24 lowerTick, int24 upperTick) internal returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) { - INonfungiblePositionManager.MintParams memory params = - INonfungiblePositionManager.MintParams({ + function _mintPosition( + uint256 maxAmount0, + uint256 maxAmount1, + int24 lowerTick, + int24 upperTick + ) + internal + returns ( + uint256 tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { + INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager + .MintParams({ token0: token0, token1: token1, fee: poolFee, @@ -338,68 +521,110 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { tickUpper: upperTick, amount0Desired: maxAmount0, amount1Desired: maxAmount1, - amount0Min: maxAmount0 * (10000 - MAX_SLIPPAGE), // 1% Slippage, - amount1Min: maxAmount1 * (10000 - MAX_SLIPPAGE), // 1% Slippage, + amount0Min: maxSlippage == 0 + ? 0 + : (maxAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, + amount1Min: maxSlippage == 0 + ? 0 + : (maxAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, recipient: address(this), deadline: block.timestamp }); - (tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager.mint(params); + (tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager + .mint(params); allTokenIds.push(tokenId); ticksToTokenId[_getTickPositionKey(lowerTick, upperTick)] = tokenId; tokenIdToPosition[tokenId] = Position({ + exists: true, tokenId: tokenId, liquidity: liquidity, lowerTick: lowerTick, upperTick: upperTick, - // 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? sqrtRatioAX96: uniswapV3Helper.getSqrtRatioAtTick(lowerTick), - sqrtRatioBX96: uniswapV3Helper.getSqrtRatioAtTick(upperTick) + sqrtRatioBX96: uniswapV3Helper.getSqrtRatioAtTick(upperTick), + positionKey: keccak256( + abi.encodePacked( + address(nonfungiblePositionManager), + lowerTick, + upperTick + ) + ) }); - + + emit UniswapV3PositionMinted(tokenId, lowerTick, upperTick); emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); } - function _increaseLiquidityForPosition(Position storage p, uint256 maxAmount0, uint256 maxAmount1) internal returns (uint128 liquidity, uint256 amount0, uint256 amount1) { - INonfungiblePositionManager.IncreaseLiquidityParams memory params = - INonfungiblePositionManager.IncreaseLiquidityParams({ + function _increaseLiquidityForPosition( + Position storage p, + uint256 maxAmount0, + uint256 maxAmount1 + ) + internal + returns ( + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { + require(p.exists, "Unknown position"); + + INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager + .IncreaseLiquidityParams({ tokenId: p.tokenId, amount0Desired: maxAmount0, amount1Desired: maxAmount1, - amount0Min: maxAmount0 * (10000 - MAX_SLIPPAGE), // 1% Slippage, - amount1Min: maxAmount1 * (10000 - MAX_SLIPPAGE), // 1% Slippage, + amount0Min: maxSlippage == 0 + ? 0 + : (maxAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, + amount1Min: maxSlippage == 0 + ? 0 + : (maxAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, deadline: block.timestamp }); - (liquidity, amount0, amount1) = nonfungiblePositionManager.increaseLiquidity(params); + (liquidity, amount0, amount1) = nonfungiblePositionManager + .increaseLiquidity(params); p.liquidity += liquidity; - emit UniswapV3LiquidityRemoved(p.tokenId, amount0, amount1, liquidity); + emit UniswapV3LiquidityAdded(p.tokenId, amount0, amount1, liquidity); } - function _decreaseLiquidityForPosition(Position storage p, uint128 liquidity) internal returns (uint256 amount0, uint256 amount1) { - (uint160 sqrtRatioX96, , , , , ,) = IUniswapV3Pool(platformAddress).slot0(); - (uint256 exactAmount0, uint256 exactAmount1) = uniswapV3Helper.getAmountsForLiquidity( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - liquidity - ); + function _decreaseLiquidityForPosition( + Position storage p, + uint128 liquidity + ) internal returns (uint256 amount0, uint256 amount1) { + require(p.exists, "Unknown position"); + + (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(platformAddress) + .slot0(); + (uint256 exactAmount0, uint256 exactAmount1) = uniswapV3Helper + .getAmountsForLiquidity( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + liquidity + ); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = - INonfungiblePositionManager.DecreaseLiquidityParams({ + INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager + .DecreaseLiquidityParams({ tokenId: p.tokenId, liquidity: liquidity, - amount0Min: exactAmount0 * (10000 - MAX_SLIPPAGE), // 1% Slippage - amount1Min: exactAmount1 * (10000 - MAX_SLIPPAGE), // 1% Slippage + amount0Min: maxSlippage == 0 + ? 0 + : (exactAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, + amount1Min: maxSlippage == 0 + ? 0 + : (exactAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, deadline: block.timestamp }); - (amount0, amount1) = nonfungiblePositionManager.decreaseLiquidity(params); + (amount0, amount1) = nonfungiblePositionManager.decreaseLiquidity( + params + ); p.liquidity -= liquidity; @@ -417,9 +642,20 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { return this.onERC721Received.selector; } - function safeApproveAllTokens() external override onlyGovernor nonReentrant { - IERC20(token0).safeApprove(address(nonfungiblePositionManager), type(uint256).max); - IERC20(token1).safeApprove(address(nonfungiblePositionManager), type(uint256).max); + function safeApproveAllTokens() + external + override + onlyGovernor + nonReentrant + { + IERC20(token0).safeApprove( + address(nonfungiblePositionManager), + type(uint256).max + ); + IERC20(token1).safeApprove( + address(nonfungiblePositionManager), + type(uint256).max + ); } function resetAllowanceOfTokens() external onlyGovernor nonReentrant { @@ -431,10 +667,18 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { internal override { - IERC20(_asset).safeApprove(address(nonfungiblePositionManager), type(uint256).max); + IERC20(_asset).safeApprove( + address(nonfungiblePositionManager), + type(uint256).max + ); } - function supportsAsset(address _asset) external view override returns (bool) { + function supportsAsset(address _asset) + external + view + override + returns (bool) + { return _asset == token0 || _asset == token1; } @@ -442,8 +686,11 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * Unused/unnecessary inherited functions */ - - function setPTokenAddress(address _asset, address _pToken) external override onlyGovernor { + function setPTokenAddress(address _asset, address _pToken) + external + override + onlyGovernor + { /** * This function isn't overridable from `InitializableAbstractStrategy` due to * missing `virtual` keyword. However, adding a function with same signature will diff --git a/contracts/contracts/utils/UniswapV3Helper.sol b/contracts/contracts/utils/UniswapV3Helper.sol index 9309690f5c..20d1cdd1f3 100644 --- a/contracts/contracts/utils/UniswapV3Helper.sol +++ b/contracts/contracts/utils/UniswapV3Helper.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity =0.7.6; -import '@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol'; -import '@uniswap/v3-core/contracts/libraries/TickMath.sol'; +import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; contract UniswapV3Helper { function getAmountsForLiquidity( @@ -10,8 +11,14 @@ contract UniswapV3Helper { uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity - ) internal pure returns (uint256 amount0, uint256 amount1) { - return LiquidityAmounts.getAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, liquidity); + ) external pure returns (uint256 amount0, uint256 amount1) { + return + LiquidityAmounts.getAmountsForLiquidity( + sqrtRatioX96, + sqrtRatioAX96, + sqrtRatioBX96, + liquidity + ); } function getLiquidityForAmounts( @@ -20,11 +27,22 @@ contract UniswapV3Helper { uint160 sqrtRatioBX96, uint256 amount0, uint256 amount1 - ) internal pure returns (uint128 liquidity) { - return LiquidityAmounts.getLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1); + ) external pure returns (uint128 liquidity) { + return + LiquidityAmounts.getLiquidityForAmounts( + sqrtRatioX96, + sqrtRatioAX96, + sqrtRatioBX96, + amount0, + amount1 + ); } - function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { + function getSqrtRatioAtTick(int24 tick) + external + pure + returns (uint160 sqrtPriceX96) + { return TickMath.getSqrtRatioAtTick(tick); } } diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index e4a090101d..a4b3d1df6a 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -40,8 +40,9 @@ contract VaultAdmin is VaultStorage { } modifier onlyUniswapV3Strategies() { + Strategy memory strategy = strategies[msg.sender]; require( - isUniswapV3Strategy[msg.sender], + strategy.isSupported && strategy.isUniswapV3Strategy, "Caller is not Uniswap V3 Strategy" ); _; @@ -202,9 +203,12 @@ contract VaultAdmin is VaultStorage { function _approveStrategy(address _addr, bool isUniswapV3) internal { require(!strategies[_addr].isSupported, "Strategy already approved"); - strategies[_addr] = Strategy({ isSupported: true, isUniswapV3Strategy: isUniswapV3, _deprecated: 0 }); + strategies[_addr] = Strategy({ + isSupported: true, + _deprecated: 0, + isUniswapV3Strategy: isUniswapV3 + }); allStrategies.push(_addr); - isUniswapV3Strategy[_addr] = isUniswapV3; emit StrategyApproved(_addr); } @@ -241,7 +245,6 @@ contract VaultAdmin is VaultStorage { // Mark the strategy as not supported strategies[_addr].isSupported = false; - isUniswapV3Strategy[_addr] = false; // Withdraw all assets IStrategy strategy = IStrategy(_addr); @@ -525,25 +528,47 @@ contract VaultAdmin is VaultStorage { Uniswap V3 Utils ****************************************/ - function depositForUniswapV3(address asset, uint256 amount) external onlyUniswapV3Strategies nonReentrant { + function depositForUniswapV3(address asset, uint256 amount) + external + onlyUniswapV3Strategies + nonReentrant + { _depositForUniswapV3(msg.sender, asset, amount); } - function _depositForUniswapV3(address v3Strategy, address asset, uint256 amount) internal { + function _depositForUniswapV3( + address v3Strategy, + address asset, + uint256 amount + ) internal { require(strategies[v3Strategy].isSupported, "Strategy not approved"); - address reserveStrategy = IUniswapV3Strategy(v3Strategy).reserveStrategy(asset); - require(reserveStrategy != address(0), "Invalid Reserve Strategy address"); + address reserveStrategy = IUniswapV3Strategy(v3Strategy) + .reserveStrategy(asset); + require( + reserveStrategy != address(0), + "Invalid Reserve Strategy address" + ); IERC20(asset).safeTransfer(reserveStrategy, amount); IStrategy(reserveStrategy).deposit(asset, amount); } - function withdrawForUniswapV3(address recipient, address asset, uint256 amount) external onlyUniswapV3Strategies nonReentrant { + function withdrawForUniswapV3( + address recipient, + address asset, + uint256 amount + ) external onlyUniswapV3Strategies nonReentrant { _withdrawForUniswapV3(msg.sender, recipient, asset, amount); } - function _withdrawForUniswapV3(address v3Strategy, address recipient, address asset, uint256 amount) internal { + function _withdrawForUniswapV3( + address v3Strategy, + address recipient, + address asset, + uint256 amount + ) internal { require(strategies[v3Strategy].isSupported, "Strategy not approved"); - address reserveStrategy = IUniswapV3Strategy(v3Strategy).reserveStrategy(asset); + address reserveStrategy = IUniswapV3Strategy(v3Strategy) + .reserveStrategy(asset); IStrategy(reserveStrategy).withdraw(recipient, asset, amount); } } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 0b8cff0535..6da2cd3729 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -349,21 +349,32 @@ contract VaultCore is VaultStorage { vaultBufferModifier ); - address depositStrategyAddr = assetDefaultStrategies[ - assetAddr - ]; + address depositStrategyAddr = assetDefaultStrategies[assetAddr]; if (depositStrategyAddr != address(0) && allocateAmount > 0) { IStrategy strategy; - if (isUniswapV3Strategy[depositStrategyAddr]) { - IUniswapV3Strategy uniswapStrategy = IUniswapV3Strategy(depositStrategyAddr); - strategy = IStrategy(uniswapStrategy.reserveStrategy(assetAddr)); + if (strategies[depositStrategyAddr].isUniswapV3Strategy) { + IUniswapV3Strategy uniswapStrategy = IUniswapV3Strategy( + depositStrategyAddr + ); + + address reserveStrategyAddr = IUniswapV3Strategy( + depositStrategyAddr + ).reserveStrategy(assetAddr); + + require( + // Defensive check to make sure the address(0) or unsupported strategy + // isn't returned by `IUniswapV3Strategy.reserveStrategy()` + strategies[reserveStrategyAddr].isSupported, + "Invalid reserve strategy for asset" + ); + + // For Uniswap V3 Strategies, always deposit to reserve strategies + strategy = IStrategy(reserveStrategyAddr); } else { strategy = IStrategy(depositStrategyAddr); } - require(address(strategy) != address(0), "Invalid deposit strategy"); - // Transfer asset to Strategy and call deposit method to // mint or take required action asset.safeTransfer(address(strategy), allocateAmount); diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 983d8d4dde..6c2e24df34 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -61,6 +61,7 @@ contract VaultStorage is Initializable, Governable { struct Strategy { bool isSupported; uint256 _deprecated; // Deprecated storage slot + // Set to true if the Strategy is an instance of `GeneralizedUniswapV3Strategy` bool isUniswapV3Strategy; } mapping(address => Strategy) internal strategies; @@ -122,9 +123,6 @@ contract VaultStorage is Initializable, Governable { // How much net total OUSD is allowed to be minted by all strategies uint256 public netOusdMintForStrategyThreshold = 0; - // TODO: Should this be part of struct `Strategy`?? - mapping(address => bool) public isUniswapV3Strategy; - /** * @dev set the implementation for the admin, this needs to be in a base class else we cannot set it * @param newImpl address of the implementation diff --git a/contracts/deploy/000_mock.js b/contracts/deploy/000_mock.js index 964b9e4ae8..303f15a656 100644 --- a/contracts/deploy/000_mock.js +++ b/contracts/deploy/000_mock.js @@ -338,23 +338,23 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => { }; async function deployMocksForUniswapV3Strategy(deploy, deployerAddr) { - const v3Helper = await deploy("MockUniswapV3Helper", { + const v3Helper = await deploy("UniswapV3Helper", { from: deployerAddr, - contract: "UniswapV3Helper" - }) + contract: "UniswapV3Helper", + }); const mockUSDT = await ethers.getContract("MockUSDT"); const mockUSDC = await ethers.getContract("MockUSDC"); const mockPool = await deploy("MockUniswapV3Pool", { from: deployerAddr, - args: [mockUSDC.address, mockUSDT.address, 500, v3Helper.address] - }) + args: [mockUSDC.address, mockUSDT.address, 500, v3Helper.address], + }); await deploy("MockNonfungiblePositionManager", { from: deployerAddr, - args: [v3Helper.address, mockPool.address] - }) + args: [v3Helper.address, mockPool.address], + }); } deployMocks.id = "000_mock"; diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index baba994fff..4e43f3f5b2 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -973,16 +973,37 @@ const deployWOusd = async () => { const deployUniswapV3Strategy = async () => { const { deployerAddr, governorAddr, operatorAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); const sGovernor = await ethers.provider.getSigner(governorAddr); const vault = await ethers.getContract("VaultProxy"); const pool = await ethers.getContract("MockUniswapV3Pool"); const manager = await ethers.getContract("MockNonfungiblePositionManager"); - const compStrat = await ethers.getContract("CompoundStrategyProxy"); const v3Helper = await ethers.getContract("MockUniswapV3Helper"); - const uniV3UsdcUsdtImpl = await deployWithConfirmation("UniV3_USDC_USDT_Strategy", [], "GeneralizedUniswapV3Strategy"); + const mockUSDT = await ethers.getContract("MockUSDT"); + const mockUSDC = await ethers.getContract("MockUSDC"); + const mockDAI = await ethers.getContract("MockDAI"); + + await deployWithConfirmation("MockStrategy", [ + vault.address, + [mockUSDC.address, mockUSDT.address], + ]); + + await deployWithConfirmation( + "MockStrategyDAI", + [vault.address, [mockDAI.address]], + "MockStrategy" + ); + + const mockStrat = await ethers.getContract("MockStrategy"); + + const uniV3UsdcUsdtImpl = await deployWithConfirmation( + "UniV3_USDC_USDT_Strategy", + [], + "GeneralizedUniswapV3Strategy" + ); await deployWithConfirmation("UniV3_USDC_USDT_Proxy"); const uniV3UsdcUsdtProxy = await ethers.getContract("UniV3_USDC_USDT_Proxy"); @@ -995,15 +1016,19 @@ const deployUniswapV3Strategy = async () => { ); log("Initialized UniV3_USDC_USDT_Proxy"); - const uniV3UsdcUsdtStrat = await ethers.getContractAt("GeneralizedUniswapV3Strategy", uniV3UsdcUsdtProxy.address); + const uniV3UsdcUsdtStrat = await ethers.getContractAt( + "GeneralizedUniswapV3Strategy", + uniV3UsdcUsdtProxy.address + ); await withConfirmation( - uniV3UsdcUsdtStrat.connect(sDeployer) + uniV3UsdcUsdtStrat + .connect(sDeployer) ["initialize(address,address,address,address,address,address,address)"]( vault.address, pool.address, manager.address, - compStrat.address, - compStrat.address, + mockStrat.address, + mockStrat.address, operatorAddr, v3Helper.address ) @@ -1028,7 +1053,7 @@ const deployUniswapV3Strategy = async () => { } return uniV3UsdcUsdtStrat; -} +}; const main = async () => { console.log("Running 001_core deployment..."); diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index f1ec6e8ae9..3be7af6012 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -12,15 +12,13 @@ module.exports = deploymentWithGovernanceProposal( getTxOpts, withConfirmation, }) => { - const { deployerAddr, operatorAddr, timelockAddr } = await getNamedAccounts(); + const { deployerAddr, operatorAddr, timelockAddr } = + await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); // Current contracts const cVaultProxy = await ethers.getContract("VaultProxy"); - const cVault = await ethers.getContractAt( - "Vault", - cVaultProxy.address - ); + const cVault = await ethers.getContractAt("Vault", cVaultProxy.address); const cVaultAdmin = await ethers.getContractAt( "VaultAdmin", cVaultProxy.address @@ -30,7 +28,7 @@ module.exports = deploymentWithGovernanceProposal( // ---------------- // 0. Deploy UniswapV3Helper - const dUniswapV3Helper = await deployWithConfirmation("UniswapV3Helper") + const dUniswapV3Helper = await deployWithConfirmation("UniswapV3Helper"); // 0. Upgrade VaultAdmin const dVaultAdmin = await deployWithConfirmation("VaultAdmin"); @@ -55,7 +53,9 @@ module.exports = deploymentWithGovernanceProposal( dUniV3_USDC_USDT_Proxy.address ); - const cMorphoCompProxy = await ethers.getContract("MorphoCompoundStrategyProxy"); + const cMorphoCompProxy = await ethers.getContract( + "MorphoCompoundStrategyProxy" + ); const cHarvesterProxy = await ethers.getContract("HarvesterProxy"); const cHarvester = await ethers.getContractAt( @@ -76,7 +76,8 @@ module.exports = deploymentWithGovernanceProposal( ); // 4. Init and configure new Uniswap V3 strategy - const initFunction = "initialize(address,address,address,address,address,address,address)"; + const initFunction = + "initialize(address,address,address,address,address,address,address)"; await withConfirmation( cUniV3_USDC_USDT_Strategy.connect(sDeployer)[initFunction]( cVaultProxy.address, // Vault @@ -97,7 +98,10 @@ module.exports = deploymentWithGovernanceProposal( .transferGovernance(timelockAddr, await getTxOpts()) ); - console.log("Uniswap V3 (USDC-USDT pool) strategy address: ", cUniV3_USDC_USDT_Strategy.address); + console.log( + "Uniswap V3 (USDC-USDT pool) strategy address: ", + cUniV3_USDC_USDT_Strategy.address + ); // Governance Actions // ---------------- return { diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index 2d875b0a63..c9dd10c8ab 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -232,9 +232,9 @@ module.exports = { }, { // Uniswap V3 contracts use solc 0.7.6 - version: "0.7.6" - } - ] + version: "0.7.6", + }, + ], }, networks: { hardhat: { @@ -262,7 +262,7 @@ module.exports = { }), }, localhost: { - timeout: 60000, + timeout: 0, }, mainnet: { url: `${process.env.PROVIDER_URL}`, @@ -274,7 +274,7 @@ module.exports = { }, mocha: { bail: process.env.BAIL === "true", - timeout: 40000, + timeout: 0, }, throwOnTransactionFailures: true, namedAccounts: { diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index bb2ec9e270..9e03b3f59e 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -33,7 +33,7 @@ async function defaultFixture() { keepExistingDeployments: Boolean(isForkWithLocalNode), }); - const { governorAddr, timelockAddr } = await getNamedAccounts(); + const { governorAddr, timelockAddr, operatorAddr } = await getNamedAccounts(); const ousdProxy = await ethers.getContract("OUSDProxy"); const vaultProxy = await ethers.getContract("VaultProxy"); @@ -112,11 +112,14 @@ async function defaultFixture() { const buyback = await ethers.getContract("Buyback"); - const UniV3_USDC_USDT_Proxy = await ethers.getContract("UniV3_USDC_USDT_Proxy") + const UniV3_USDC_USDT_Proxy = await ethers.getContract( + "UniV3_USDC_USDT_Proxy" + ); const UniV3_USDC_USDT_Strategy = await ethers.getContractAt( "GeneralizedUniswapV3Strategy", UniV3_USDC_USDT_Proxy.address ); + const UniV3Helper = await ethers.getContract("UniswapV3Helper"); let usdt, dai, @@ -167,7 +170,9 @@ async function defaultFixture() { LUSDMetaStrategyProxy, LUSDMetaStrategy, UniV3PositionManager, - UniV3_USDC_USDT_Pool; + UniV3_USDC_USDT_Pool, + mockStrategy, + mockStrategyDAI; if (isFork) { usdt = await ethers.getContractAt(usdtAbi, addresses.mainnet.USDT); @@ -235,7 +240,6 @@ async function defaultFixture() { "IUniswapV3Pool", addresses.mainnet.UniV3_USDC_USDT_Pool ); - } else { usdt = await ethers.getContract("MockUSDT"); dai = await ethers.getContract("MockDAI"); @@ -315,8 +319,12 @@ async function defaultFixture() { LUSDMetaStrategyProxy.address ); - UniV3PositionManager = await ethers.getContract("MockNonfungiblePositionManager"); + UniV3PositionManager = await ethers.getContract( + "MockNonfungiblePositionManager" + ); UniV3_USDC_USDT_Pool = await ethers.getContract("MockUniswapV3Pool"); + mockStrategy = await ethers.getContract("MockStrategy"); + mockStrategyDAI = await ethers.getContract("MockStrategyDAI"); } if (!isFork) { const assetAddresses = await getAssetAddresses(deployments); @@ -337,7 +345,7 @@ async function defaultFixture() { let governor = signers[1]; const strategist = signers[0]; const adjuster = signers[0]; - const operator = signers[3]; + let operator = signers[3]; let timelock; const [matt, josh, anna, domen, daniel, franck] = signers.slice(4); @@ -345,6 +353,7 @@ async function defaultFixture() { if (isFork) { governor = await impersonateAndFundContract(governorAddr); timelock = await impersonateAndFundContract(timelockAddr); + operator = await impersonateAndFundContract(operatorAddr); } await fundAccounts(); if (isFork) { @@ -453,7 +462,10 @@ async function defaultFixture() { // Uniswap V3 Strategy UniV3PositionManager, UniV3_USDC_USDT_Pool, - UniV3_USDC_USDT_Strategy + UniV3_USDC_USDT_Strategy, + UniV3Helper, + mockStrategy, + mockStrategyDAI, }; } @@ -1205,33 +1217,49 @@ async function rebornFixture() { } async function uniswapV3Fixture() { - const fixture = await loadFixture(compoundVaultFixture); + const fixture = await loadFixture(defaultFixture); - const { usdc, usdt, UniV3_USDC_USDT_Strategy } = fixture; + const { + usdc, + usdt, + dai, + UniV3_USDC_USDT_Strategy, + mockStrategy, + mockStrategyDAI, + } = fixture; + + if (!isFork) { + // Approve mockStrategy + await _approveStrategy(fixture, mockStrategy); + await _approveStrategy(fixture, mockStrategyDAI); + + // Approve Uniswap V3 Strategy + await _approveStrategy(fixture, UniV3_USDC_USDT_Strategy, true); + } - // Approve Uniswap V3 Strategy - await _approveStrategy(fixture, UniV3_USDC_USDT_Strategy, true); - // Change default strategy to Uniswap V3 for both USDT and USDC await _setDefaultStrategy(fixture, usdc, UniV3_USDC_USDT_Strategy); await _setDefaultStrategy(fixture, usdt, UniV3_USDC_USDT_Strategy); - return fixture + if (!isFork) { + // And a different one for DAI + await _setDefaultStrategy(fixture, dai, mockStrategyDAI); + } + + return fixture; } async function _approveStrategy(fixture, strategy, isUniswapV3) { const { vault, harvester } = fixture; - const { governorAddr } = await getNamedAccounts(); - const sGovernor = await ethers.provider.getSigner(governorAddr); + const { governorAddr, timelockAddr } = await getNamedAccounts(); + const sGovernor = await ethers.provider.getSigner( + isFork ? timelockAddr : governorAddr + ); if (isUniswapV3) { - await vault - .connect(sGovernor) - .approveUniswapV3Strategy(strategy.address); + await vault.connect(sGovernor).approveUniswapV3Strategy(strategy.address); } else { - await vault - .connect(sGovernor) - .approveStrategy(strategy.address); + await vault.connect(sGovernor).approveStrategy(strategy.address); } await harvester @@ -1241,14 +1269,13 @@ async function _approveStrategy(fixture, strategy, isUniswapV3) { async function _setDefaultStrategy(fixture, asset, strategy) { const { vault } = fixture; - const { governorAddr } = await getNamedAccounts(); - const sGovernor = await ethers.provider.getSigner(governorAddr); + const { governorAddr, timelockAddr } = await getNamedAccounts(); + const sGovernor = await ethers.provider.getSigner( + isFork ? timelockAddr : governorAddr + ); await vault .connect(sGovernor) - .setAssetDefaultStrategy( - asset.address, - strategy.address - ); + .setAssetDefaultStrategy(asset.address, strategy.address); } module.exports = { diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index f8265d5422..f46b2b5db1 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -35,28 +35,95 @@ chai.Assertion.addMethod( } ); +chai.Assertion.addMethod("totalSupplyOf", async function (expected, message) { + const contract = this._obj; + const actual = await contract.totalSupply(); + if (!BigNumber.isBigNumber(expected)) { + expected = parseUnits(expected, await decimalsFor(contract)); + } + chai.expect(actual).to.equal(expected, message); +}); + +chai.Assertion.addMethod( + "approxTotalSupplyOf", + async function (expected, message) { + const contract = this._obj; + const actual = await contract.totalSupply(); + if (!BigNumber.isBigNumber(expected)) { + expected = parseUnits(expected, await decimalsFor(contract)); + } + chai.expect(actual).to.approxEqualTolerance(expected, 1, message); + } +); + chai.Assertion.addMethod( "approxBalanceOf", async function (expected, contract, message) { - var user = this._obj; - var address = user.address || user.getAddress(); // supports contracts too + const user = this._obj; + const address = user.address || user.getAddress(); // supports contracts too const actual = await contract.balanceOf(address); - expected = parseUnits(expected, await decimalsFor(contract)); + if (!BigNumber.isBigNumber(expected)) { + expected = parseUnits(expected, await decimalsFor(contract)); + } chai.expect(actual).to.approxEqual(expected, message); } ); +chai.Assertion.addMethod( + "approxBalanceWithToleranceOf", + async function (expected, contract, tolerancePct = 1, message = undefined) { + const user = this._obj; + const address = user.address || user.getAddress(); // supports contracts too + const actual = await contract.balanceOf(address); + if (!BigNumber.isBigNumber(expected)) { + expected = parseUnits(expected, await decimalsFor(contract)); + } + chai + .expect(actual) + .to.approxEqualTolerance(expected, tolerancePct, message); + } +); + chai.Assertion.addMethod( "balanceOf", async function (expected, contract, message) { - var user = this._obj; - var address = user.address || user.getAddress(); // supports contracts too + const user = this._obj; + const address = user.address || user.getAddress(); // supports contracts too const actual = await contract.balanceOf(address); - expected = parseUnits(expected, await decimalsFor(contract)); + if (!BigNumber.isBigNumber(expected)) { + expected = parseUnits(expected, await decimalsFor(contract)); + } chai.expect(actual).to.equal(expected, message); } ); +chai.Assertion.addMethod( + "assetBalanceOf", + async function (expected, asset, message) { + const strategy = this._obj; + const assetAddress = asset.address || asset.getAddress(); + const actual = await strategy.checkBalance(assetAddress); + if (!BigNumber.isBigNumber(expected)) { + expected = parseUnits(expected, await decimalsFor(asset)); + } + chai.expect(actual).to.approxEqualTolerance(expected, 1, message); + } +); + +chai.Assertion.addMethod("emittedEvent", async function (eventName, args) { + const tx = this._obj; + const { events } = await tx.wait(); + const log = events.find((e) => e.event == eventName); + chai.expect(log).to.not.be.undefined; + + if (Array.isArray(args)) { + chai.expect(log.args).to.equal(args.length, "Invalid event arg count"); + for (let i = 0; i < args.length; i++) { + chai.expect(log.args[i]).to.equal(args[i]); + } + } +}); + const DECIMAL_CACHE = {}; async function decimalsFor(contract) { if (DECIMAL_CACHE[contract.address] != undefined) { @@ -335,8 +402,11 @@ const getAssetAddresses = async (deployments) => { uniswapV3Router: (await deployments.get("MockUniswapRouter")).address, sushiswapRouter: (await deployments.get("MockUniswapRouter")).address, - UniV3PositionManager: (await deployments.get("MockNonfungiblePositionManager")).address, - UniV3_USDC_USDT_Pool: (await ethers.getContract("MockUniswapV3Pool")).address, + UniV3PositionManager: ( + await deployments.get("MockNonfungiblePositionManager") + ).address, + UniV3_USDC_USDT_Pool: (await ethers.getContract("MockUniswapV3Pool")) + .address, }; try { diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js new file mode 100644 index 0000000000..0ff237c4ba --- /dev/null +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -0,0 +1,323 @@ +const { expect } = require("chai"); +const { uniswapV3Fixture } = require("../_fixture"); +const { + forkOnlyDescribe, + loadFixture, + units, + ousdUnits, + ousdUnitsFormat, + expectApproxSupply, + usdcUnits, + usdcUnitsFormat, + usdtUnits, + usdtUnitsFormat, + daiUnits, + daiUnitsFormat, + advanceTime, + advanceBlocks, +} = require("../helpers"); +const { BigNumber } = require("ethers"); + +forkOnlyDescribe("Uniswap V3 Strategy", function () { + this.timeout(0); + + let fixture; + let vault, harvester, ousd, usdc, usdt, dai; + let reserveStrategy, strategy, pool, positionManager, v3Helper; + let governor, strategist, operator, josh, matt, daniel, domen, franck; + + beforeEach(async () => { + fixture = await loadFixture(uniswapV3Fixture); + reserveStrategy = fixture.morphoCompoundStrategy; + strategy = fixture.UniV3_USDC_USDT_Strategy; + pool = fixture.UniV3_USDC_USDT_Pool; + positionManager = fixture.UniV3PositionManager; + v3Helper = fixture.UniV3Helper; + + ousd = fixture.ousd; + usdc = fixture.usdc; + usdt = fixture.usdt; + dai = fixture.dai; + vault = fixture.vault; + harvester = fixture.harvester; + governor = fixture.governor; + strategist = fixture.strategist; + operator = fixture.operator; + josh = fixture.josh; + matt = fixture.matt; + daniel = fixture.daniel; + domen = fixture.domen; + franck = fixture.franck; + }); + + describe("Mint", function () { + const mintTest = async (user, amount, asset) => { + const ousdAmount = ousdUnits(amount); + const tokenAmount = await units(amount, asset); + + const currentSupply = await ousd.totalSupply(); + const ousdBalance = await ousd.balanceOf(user.address); + const tokenBalance = await asset.balanceOf(user.address); + const reserveTokenBalance = await reserveStrategy.checkBalance( + asset.address + ); + + // await asset.connect(user).approve(vault.address, tokenAmount) + await vault.connect(user).mint(asset.address, tokenAmount, 0); + + await expect(ousd).to.have.an.approxTotalSupplyOf( + currentSupply.add(ousdAmount), + "Total supply mismatch" + ); + if (asset == dai) { + // DAI is unsupported and should not be deposited in reserve strategy + await expect(reserveStrategy).to.have.an.assetBalanceOf( + reserveTokenBalance, + asset, + "Expected reserve strategy to not support DAI" + ); + } else { + await expect(reserveStrategy).to.have.an.assetBalanceOf( + reserveTokenBalance.add(tokenAmount), + asset, + "Expected reserve strategy to have received the other token" + ); + } + + await expect(user).to.have.an.approxBalanceWithToleranceOf( + ousdBalance.add(ousdAmount), + ousd, + 1, + "Should've minted equivalent OUSD" + ); + await expect(user).to.have.an.approxBalanceWithToleranceOf( + tokenBalance.sub(tokenAmount), + asset, + 1, + "Should've deposoited equivaluent other token" + ); + }; + + it("with USDC", async () => { + await mintTest(daniel, "30000", usdc); + }); + it("with USDT", async () => { + await mintTest(domen, "30000", usdt); + }); + it("with DAI", async () => { + await mintTest(franck, "30000", dai); + }); + }); + + describe("Redeem", function () { + const redeemTest = async (user, amount) => { + const ousdAmount = ousdUnits(amount); + + let ousdBalance = await ousd.balanceOf(user.address); + if (ousdBalance.lt(ousdAmount)) { + // Mint some OUSD + await vault.connect(user).mint(dai.address, daiUnits(amount), 0); + ousdBalance = await ousd.balanceOf(user.address); + } + + const currentSupply = await ousd.totalSupply(); + const usdcBalance = await usdc.balanceOf(user.address); + const usdtBalance = await usdt.balanceOf(user.address); + const daiBalance = await dai.balanceOf(user.address); + + await vault.connect(user).redeem(ousdAmount, 0); + + await expect(ousd).to.have.an.approxTotalSupplyOf( + currentSupply.sub(ousdAmount), + "Total supply mismatch" + ); + await expect(user).to.have.an.approxBalanceWithToleranceOf( + ousdBalance.sub(ousdAmount), + ousd, + 1, + "Should've burned equivalent OUSD" + ); + + const balanceDiff = + parseFloat( + usdcUnitsFormat((await usdc.balanceOf(user.address)) - usdcBalance) + ) + + parseFloat( + usdtUnitsFormat((await usdt.balanceOf(user.address)) - usdtBalance) + ) + + parseFloat( + daiUnitsFormat((await dai.balanceOf(user.address)) - daiBalance) + ); + + await expect(balanceDiff).to.approxEqualTolerance( + amount, + 1, + "Should've redeemed equivaluent other token" + ); + }; + + it("Should withdraw from reserve strategy", async () => { + redeemTest(josh, "10000"); + }); + }); + + describe("Rewards", function () { + it("Should show correct amount of fees", async () => {}); + }); + + describe.only("Uniswap V3 positions", function () { + const findMaxDepositableAmount = async ( + lowerTick, + upperTick, + usdcAmount, + usdtAmount + ) => { + const [sqrtRatioX96] = await pool.slot0(); + const sqrtRatioAX96 = await v3Helper.getSqrtRatioAtTick(lowerTick); + const sqrtRatioBX96 = await v3Helper.getSqrtRatioAtTick(upperTick); + + const liquidity = await v3Helper.getLiquidityForAmounts( + sqrtRatioX96, + sqrtRatioAX96, + sqrtRatioBX96, + usdcAmount, + usdtAmount + ); + + const [maxAmount0, maxAmount1] = await v3Helper.getAmountsForLiquidity( + sqrtRatioX96, + sqrtRatioAX96, + sqrtRatioBX96, + liquidity + ); + + return [maxAmount0, maxAmount1]; + }; + + const mintLiquidity = async ( + lowerTick, + upperTick, + usdcAmount, + usdtAmount + ) => { + const [maxUSDC, maxUSDT] = await findMaxDepositableAmount( + lowerTick, + upperTick, + BigNumber.from(usdcAmount).mul(10 ** 6), + BigNumber.from(usdtAmount).mul(10 ** 6) + ); + + const tx = await strategy + .connect(operator) + .rebalance(maxUSDC, maxUSDT, lowerTick, upperTick); + + const { events } = await tx.wait(); + + const [tokenId, amount0Minted, amount1Minted, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + + return { + tokenId, + amount0Minted, + amount1Minted, + liquidityMinted, + tx, + }; + }; + + it("Should mint position", async () => { + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick - 1000; + const upperTick = activeTick + 1000; + + const { tokenId, tx } = await mintLiquidity( + lowerTick, + upperTick, + "1000000", + "1000000" + ); + + // Check events + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + await expect(tx).to.have.emittedEvent("UniswapV3LiquidityAdded"); + + // Check minted position data + const nfp = await positionManager.positions(tokenId); + expect(nfp.token0).to.equal(usdc.address, "Invalid token0 address"); + expect(nfp.token1).to.equal(usdt.address, "Invalid token1 address"); + expect(nfp.tickLower).to.equal(lowerTick, "Invalid lower tick"); + expect(nfp.tickUpper).to.equal(upperTick, "Invalid upper tick"); + + // TODO: Check storage values in Strategy + }); + + it("Should increase liquidity of existing position", async () => { + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick - 1002; + const upperTick = activeTick + 1002; + + // Mint position + await mintLiquidity(lowerTick, upperTick, "1000000", "1000000"); + + // Rebalance again to increase liquidity + const { tokenId, tx } = await mintLiquidity( + lowerTick, + upperTick, + "1000000", + "1000000" + ); + + // Check events + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + await expect(tx).to.have.emittedEvent("UniswapV3LiquidityAdded"); + + // Check minted position data + const nfp = await positionManager.positions(tokenId); + expect(nfp.token0).to.equal(usdc.address, "Invalid token0 address"); + expect(nfp.token1).to.equal(usdt.address, "Invalid token1 address"); + expect(nfp.tickLower).to.equal(lowerTick, "Invalid lower tick"); + expect(nfp.tickUpper).to.equal(upperTick, "Invalid upper tick"); + + // TODO: Check storage values in Strategy + }); + + // Mint + // Increase + // Close + + // it("Should be able close existing positions", async () => { + // const [, activeTick] = await pool.slot0(); + // const lowerTick = activeTick - 7 + // const upperTick = activeTick + 7 + + // const { tokenId, tx } = await mintLiquidity( + // lowerTick, + // upperTick, + // '1000000', + // '1000000' + // ) + + // // Check events + // await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted") + // await expect(tx).to.have.emittedEvent("UniswapV3LiquidityAdded") + + // // Check minted position data + // const nfp = await positionManager.positions(tokenId) + // expect(nfp.token0).to.equal(usdc.address, "Invalid token0 address") + // expect(nfp.token1).to.equal(usdt.address, "Invalid token1 address") + // expect(nfp.tickLower).to.equal(lowerTick, "Invalid lower tick") + // expect(nfp.tickUpper).to.equal(upperTick, "Invalid upper tick") + + // // TODO: Check storage values in Strategy + // }) + + // describe.only("Liquidity management", function () { + + // }) + + // it("Should provide liquidity on given tick", async () => { + // }); + + // it("Should close existing position", async () => {}); + }); +}); diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index a4374abb28..9e61297290 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -1,40 +1,42 @@ const { expect } = require("chai"); const { uniswapV3Fixture } = require("../_fixture"); -const { loadFixture, units, ousdUnits, expectApproxSupply } = require("../helpers"); +const { + loadFixture, + units, + ousdUnits, + expectApproxSupply, +} = require("../helpers"); describe("Uniswap V3 Strategy", function () { this.timeout(0); - let fixture - let vault, harvester, ousd, usdc, usdt, dai, cusdc, cusdt, cdai - let reserveStrategy, uniV3Strategy, mockPool, mockPositionManager - let governor, strategist, operator, josh, matt, daniel, domen, franck + let fixture; + let vault, harvester, ousd, usdc, usdt, dai; + let reserveStrategy, uniV3Strategy, mockPool, mockPositionManager; + let governor, strategist, operator, josh, matt, daniel, domen, franck; beforeEach(async () => { - fixture = await loadFixture(uniswapV3Fixture) - reserveStrategy = fixture.compoundStrategy - uniV3Strategy = fixture.UniV3_USDC_USDT_Strategy - mockPool = fixture.UniV3_USDC_USDT_Pool - mockPositionManager = fixture.UniV3PositionManager + fixture = await loadFixture(uniswapV3Fixture); + reserveStrategy = fixture.mockStrategy; + uniV3Strategy = fixture.UniV3_USDC_USDT_Strategy; + mockPool = fixture.UniV3_USDC_USDT_Pool; + mockPositionManager = fixture.UniV3PositionManager; - ousd = fixture.ousd - usdc = fixture.usdc - usdt = fixture.usdt - dai = fixture.dai - cusdc = fixture.cusdc - cusdt = fixture.cusdt - cdai = fixture.cdai - vault = fixture.vault - harvester = fixture.harvester - governor = fixture.governor - strategist = fixture.strategist - operator = fixture.operator - josh = fixture.josh - matt = fixture.matt - daniel = fixture.daniel - domen = fixture.domen - franck = fixture.franck - }) + ousd = fixture.ousd; + usdc = fixture.usdc; + usdt = fixture.usdt; + dai = fixture.dai; + vault = fixture.vault; + harvester = fixture.harvester; + governor = fixture.governor; + strategist = fixture.strategist; + operator = fixture.operator; + josh = fixture.josh; + matt = fixture.matt; + daniel = fixture.daniel; + domen = fixture.domen; + franck = fixture.franck; + }); const mint = async (user, amount, asset) => { await asset.connect(user).mint(units(amount, asset)); @@ -52,31 +54,24 @@ describe("Uniswap V3 Strategy", function () { await mint(daniel, "10000", usdc); await expectApproxSupply(ousd, ousdUnits("10200")); + console.log(await usdc.balanceOf(reserveStrategy.address)); + // Make sure it went to reserve strategy - console.log((await reserveStrategy.checkBalance(cusdc.address)).toString()); - // await expect(reserveStrategy).has.an.approxBalanceOf("10200", ); + await expect(reserveStrategy).has.an.approxBalanceOf("10000", usdc); }); - }) + }); describe("Redeem", function () { - it("Should withdraw from reserve strategy", async () => { - - }) - }) + it("Should withdraw from reserve strategy", async () => {}); + }); describe("Rewards", function () { - it("Should show correct amount of fees", async () => { - - }) - }) + it("Should show correct amount of fees", async () => {}); + }); describe("Rebalance", function () { - it("Should provide liquidity on given tick", async () => { - - }) - - it("Should close existing position", async () => { + it("Should provide liquidity on given tick", async () => {}); - }) - }) -}) + it("Should close existing position", async () => {}); + }); +}); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 26cc6446c6..519bf596b7 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -138,8 +138,10 @@ addresses.mainnet.Flipper = "0xcecaD69d7D4Ed6D52eFcFA028aF8732F27e08F70"; addresses.mainnet.Morpho = "0x8888882f8f843896699869179fB6E4f7e3B58888"; addresses.mainnet.MorphoLens = "0x930f1b46e1d081ec1524efd95752be3ece51ef67"; -addresses.mainnet.UniV3PositionManager = "0xC36442b4a4522E871399CD717aBDD847Ab11FE88"; -addresses.mainnet.UniV3_USDC_USDT_Pool = "0x3416cf6c708da44db2624d63ea0aaef7113527c6"; +addresses.mainnet.UniV3PositionManager = + "0xC36442b4a4522E871399CD717aBDD847Ab11FE88"; +addresses.mainnet.UniV3_USDC_USDT_Pool = + "0x3416cf6c708da44db2624d63ea0aaef7113527c6"; // OUSD Governance addresses.mainnet.GovernorFive = "0x3cdd07c16614059e66344a7b579dab4f9516c0b6"; diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index b5147c1978..7722527174 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -15,6 +15,7 @@ const { getAssetAddresses, isSmokeTest, isForkTest, + isTest, } = require("../test/helpers.js"); const { From 861726de197942da0802b76025b6c411724cc64d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 4 Mar 2023 18:32:50 +0530 Subject: [PATCH 05/83] Add some comments --- .../GeneralizedUniswapV3Strategy.sol | 438 +++++++++++++----- .../utils/InitializableAbstractStrategy.sol | 14 +- 2 files changed, 338 insertions(+), 114 deletions(-) diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index 01055749d6..45ec77e5d8 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -48,25 +48,31 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 amount1Received ); - // Address of operator + // The address that can manage the positions on Uniswap V3 address public operatorAddr; - address public token0; - address public token1; + address public constant token0; // Token0 of Uniswap V3 Pool + address public constant token1; // Token1 of Uniswap V3 Pool - uint24 public poolFee; - uint24 internal maxSlippage = 100; // 1% + uint24 public constant poolFee; // Uniswap V3 Pool Fee + uint24 internal maxSlippage = 100; // 1%; Slippage tolerance when providing liquidity + // Address mapping of (Asset -> Strategy). When the funds are + // not deployed in Uniswap V3 Pool, they will be deposited + // to these reserve strategies mapping(address => address) public reserveStrategy; - INonfungiblePositionManager public nonfungiblePositionManager; + // Uniswap V3's PositionManager + INonfungiblePositionManager public constant nonfungiblePositionManager; + // Represents a position minted by this contract struct Position { - bytes32 positionKey; - uint256 tokenId; - uint128 liquidity; - int24 lowerTick; - int24 upperTick; - bool exists; + bytes32 positionKey; // Required to read collectible fees from the V3 Pool + uint256 tokenId; // ERC721 token Id of the minted position + uint128 liquidity; // Amount of liquidity deployed + int24 lowerTick; // Lower tick index + int24 upperTick; // Upper tick index + bool exists; // True, if position is minted + // 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? @@ -74,11 +80,14 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint160 sqrtRatioBX96; } + // A lookup table to find token IDs of position using f(lowerTick, upperTick) mapping(int48 => uint256) internal ticksToTokenId; + // Maps tokenIDs to their Position object mapping(uint256 => Position) internal tokenIdToPosition; - uint256[] internal allTokenIds; + // Token ID of the position that's being used to provide LP at the time uint256 internal currentPositionTokenId; + // A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch IUniswapV3Helper internal uniswapV3Helper; // Future-proofing @@ -109,6 +118,16 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _; } + /** + * @dev Initialize the contract + * @param _vaultAddress OUSD Vault + * @param _poolAddress Uniswap V3 Pool + * @param _nonfungiblePositionManager Uniswap V3's Position Manager + * @param _token0ReserveStrategy Reserve Strategy for token0 + * @param _token1ReserveStrategy Reserve Strategy for token1 + * @param _operator Address that can manage LP positions on the V3 pool + * @param _uniswapV3Helper Deployed UniswapV3Helper contract + */ function initialize( address _vaultAddress, address _poolAddress, @@ -143,6 +162,15 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _setReserveStrategy(_token0ReserveStrategy, _token1ReserveStrategy); } + /*************************************** + Admin Utils + ****************************************/ + + /** + * @notice Change the slippage tolerance + * @dev Can only be called by Governor or Strategist + * @param _slippage The new value to be set + */ function setMaxSlippage(uint24 _slippage) external onlyGovernorOrStrategist @@ -153,12 +181,22 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { maxSlippage = _slippage; } + /** + * @notice Change the address of the operator + * @dev Can only be called by the Governor + * @param _operator The new value to be set + */ function setOperator(address _operator) external onlyGovernor { require(_operator != address(0), "Invalid operator address"); operatorAddr = _operator; emit OperatorChanged(_operator); } + /** + * @notice Change the reserve strategies of the supported assets + * @param _token0ReserveStrategy The new reserve strategy for token0 + * @param _token1ReserveStrategy The new reserve strategy for token1 + */ function setReserveStrategy( address _token0ReserveStrategy, address _token1ReserveStrategy @@ -166,6 +204,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _setReserveStrategy(_token0ReserveStrategy, _token1ReserveStrategy); } + /** + * @notice Change the reserve strategies of the supported assets + * @dev Will throw if the strategies don't support the assets + * @param _token0ReserveStrategy The new reserve strategy for token0 + * @param _token1ReserveStrategy The new reserve strategy for token1 + */ function _setReserveStrategy( address _token0ReserveStrategy, address _token1ReserveStrategy @@ -188,19 +232,30 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ); } + /*************************************** + Deposit/Withdraw + ****************************************/ + + /// @inheritdoc InitializableAbstractStrategy function deposit(address _asset, uint256 _amount) external override onlyVault nonReentrant { + require(_asset == token0 || _asset == token1, "Unsupported asset"); IVault(vaultAddress).depositForUniswapV3(_asset, _amount); + // Not emitting Deposit event since the Reserve strategy would do so } + /// @inheritdoc InitializableAbstractStrategy function depositAll() external override onlyVault nonReentrant { _depositAll(); } + /** + * @notice Deposits all undeployed balances of the contract to the reserve strategies + */ function _depositAll() internal { uint256 token0Bal = IERC20(token0).balanceOf(address(this)); uint256 token1Bal = IERC20(token1).balanceOf(address(this)); @@ -210,23 +265,47 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { if (token1Bal > 0) { IVault(vaultAddress).depositForUniswapV3(token1, token1Bal); } + + // Not emitting Deposit events since the Reserve strategies would do so } + /** + * @notice Withdraws asset from the reserve strategy + * @inheritdoc InitializableAbstractStrategy + */ function withdraw( address recipient, - address asset, + address _asset, uint256 amount ) external override onlyVault nonReentrant { - uint256 reserveBalance = IStrategy(reserveStrategy[asset]).checkBalance( - asset - ); + require(_asset == token0 || _asset == token1, "Unsupported asset"); + + IERC20 asset = IERC20(_asset); + uint256 selfBalance = asset.balanceOf(address(this)); - // TODO: Remove liquidity from pool instead? - require(reserveBalance >= amount, "Liquidity error"); + if (selfBalance < amount) { + // Try to pull remaining amount from reserve strategy + // This might throw if there isn't enough in reserve strategy as well + IVault(vaultAddress).withdrawForUniswapV3(recipient, asset, amount - selfBalance); - IVault(vaultAddress).withdrawForUniswapV3(recipient, asset, amount); + // TODO: Remove liquidity from V3 pool instead? + + // Transfer all of unused balance + asset.safeTransfer(recipient, selfBalance); + + // Emit event for only the amount transferred out from this strategy + emit Withdrawal(asset, asset, selfBalance); + } else { + // Transfer requested amount + asset.safeTransfer(recipient, amount); + emit Withdrawal(asset, asset, amount); + } } + /** + * @notice Closes active LP position, if any, and transfer all token balance to Vault + * @inheritdoc InitializableAbstractStrategy + */ function withdrawAll() external override onlyVault nonReentrant { if (currentPositionTokenId > 0) { _closePosition(currentPositionTokenId); @@ -238,28 +317,80 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 token0Balance = cToken0.balanceOf(address(this)); if (token0Balance >= 0) { cToken0.safeTransfer(vaultAddress, token0Balance); + emit Withdrawal(token0, token0, token0Balance); } uint256 token1Balance = cToken1.balanceOf(address(this)); if (token1Balance >= 0) { cToken1.safeTransfer(vaultAddress, token1Balance); + emit Withdrawal(token1, token1, token1Balance); } } + /** + * @dev Checks if there's enough balance left in the contract to provide liquidity. + * If not, tries to pull it from reserve strategies + * @param minAmount0 Minimum amount of token0 needed + * @param minAmount1 Minimum amount of token1 needed + */ + function _withdrawForLiquidity(uint256 minAmount0, uint256 minAmount1) + internal + { + IERC20 cToken0 = IERC20(token0); + IERC20 cToken1 = IERC20(token1); + IVault vault = IVault(vaultAddress); + + // Withdraw enough funds from Reserve strategies + uint256 token0Balance = cToken0.balanceOf(address(this)); + if (token0Balance < minAmount0) { + vault.withdrawForUniswapV3( + address(this), + token0, + minAmount0 - token0Balance + ); + } + + uint256 token1Balance = cToken1.balanceOf(address(this)); + if (token1Balance < minAmount1) { + vault.withdrawForUniswapV3( + address(this), + token1, + minAmount1 - token1Balance + ); + } + } + + /*************************************** + Balances and Fees + ****************************************/ + + /** + * @notice Collect accumulated fees from the active position + * @dev Doesn't send to vault or harvester + */ function collectRewardTokens() external override onlyHarvester nonReentrant { - for (uint256 i = 0; i < allTokenIds.length; i++) { - uint256 tokenId = allTokenIds[0]; - if (tokenIdToPosition[tokenId].liquidity > 0) { - _collectFeesForToken(tokenId); - } + if (currentPositionTokenId > 0) { + _collectFeesForToken(currentPositionTokenId); } + + // for (uint256 i = 0; i < allTokenIds.length; i++) { + // uint256 tokenId = allTokenIds[0]; + // if (tokenIdToPosition[tokenId].liquidity > 0) { + // _collectFeesForToken(tokenId); + // } + // } } + /** + * @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 getPendingRewards() external view @@ -270,6 +401,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { (amount0, amount1) = _getTokensOwed(p); } + /** + * @notice Fetches the fees generated by the position on V3 pool + * @param p Position struct + * @return tokensOwed0 Amount of token0 ready to be collected as fee + * @return tokensOwed1 Amount of token1 ready to be collected as fee + */ function _getTokensOwed(Position memory p) internal view @@ -281,6 +418,37 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { .positions(p.positionKey); } + /** + * @notice Collects the fees generated by the position on V3 pool + * @param tokenId Token ID of the position to collect fees of. + * @return amount0 Amount of token0 collected as fee + * @return amount1 Amount of token1 collected as fee + */ + function _collectFeesForToken(uint256 tokenId) + internal + returns (uint256 amount0, uint256 amount1) + { + INonfungiblePositionManager.CollectParams + memory params = INonfungiblePositionManager.CollectParams({ + tokenId: tokenId, + recipient: address(this), + amount0Max: type(uint128).max, + amount1Max: type(uint128).max + }); + + (amount0, amount1) = nonfungiblePositionManager.collect(params); + + emit RewardTokenCollected(address(this), token0, amount0); + emit RewardTokenCollected(address(this), token1, amount1); + + emit UniswapV3FeeCollected(tokenId, amount0, amount1); + } + + /** + * @dev Only checks the active LP position. + * Doesn't return the balance held in the reserve strategies. + * @inheritdoc InitializableAbstractStrategy + */ function checkBalance(address _asset) external view @@ -288,7 +456,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { returns (uint256 balance) { require(_asset == token0 || _asset == token1, "Unsupported asset"); - // TODO: Should reserve strategy balance be included? Might result in double calculations + balance = IERC20(_asset).balanceOf(address(this)); (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(platformAddress) @@ -300,12 +468,18 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } // for (uint256 i = 0; i < allTokenIds.length; i++) { - // // TODO: Should only current active position be checked? // Position memory p = tokenIdToPosition[allTokenIds[i]]; // balance += _checkAssetBalanceOfPosition(_asset, p, sqrtRatioX96); // } } + /** + * @dev Get the balance of an asset held in a LP position along with fees generated + * @param asset Address of the asset + * @param p Position object + * @param sqrtRatioX96 Price ratio of the current tick of the pool + * @return balance Total amount of the asset available + */ function _checkAssetBalanceOfPosition( address asset, Position memory p, @@ -318,6 +492,13 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } } + /** + * @notice Get the balance of both assets held in a LP position along with fees generated + * @param p Position object + * @param sqrtRatioX96 Price ratio of the current tick of the pool + * @return amount0 Total amount of the token0 available + * @return amount1 Total amount of the token1 available + */ function _checkBalanceOfPosition(Position memory p, uint160 sqrtRatioX96) internal view @@ -343,36 +524,42 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { amount1 += feeAmount1; } - function _withdrawForLiquidity(uint256 minAmount0, uint256 minAmount1) - internal - { - IERC20 cToken0 = IERC20(token0); - IERC20 cToken1 = IERC20(token1); - IVault vault = IVault(vaultAddress); - // Withdraw enough funds from Reserve strategies - uint256 token0Balance = cToken0.balanceOf(address(this)); - if (token0Balance < minAmount0) { - vault.withdrawForUniswapV3( - address(this), - token0, - minAmount0 - token0Balance - ); - } + /*************************************** + Pool Liquidity Management + ****************************************/ - uint256 token1Balance = cToken1.balanceOf(address(this)); - if (token1Balance < minAmount1) { - vault.withdrawForUniswapV3( - address(this), - token1, - minAmount1 - token1Balance - ); - } + /** + * @notice Returns a unique ID based on lowerTick and upperTick + * @dev Basically concats the lower tick and upper tick values. Shifts the value + * of lowerTick by 24 bits and then adds the upperTick value to avoid overlaps. + * So, the result is smaller in size (int48 rather than bytes32 when using keccak256) + * @param lowerTick Lower tick index + * @param upperTick Upper tick index + * @param key A unique identifier to be used with ticksToTokenId + */ + function _getTickPositionKey(int24 lowerTick, int24 upperTick) + internal + returns (int48 key) + { + if (lowerTick > upperTick) + (lowerTick, upperTick) = (upperTick, lowerTick); + key = int48(lowerTick) * 2**24; // Shift by 24 bits + key = key + int24(upperTick); } + /** + * @notice Closes active LP position if any and then provides liquidity to the requested position. + * Mints new position, if it doesn't exist already. + * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them + * @param desiredAmount0 Desired amount of token0 to provide liquidity + * @param desiredAmount1 Desired amount of token1 to provide liquidity + * @param lowerTick Lower tick index + * @param upperTick Upper tick index + */ function rebalance( - uint256 maxAmount0, - uint256 maxAmount1, + uint256 desiredAmount0, + uint256 desiredAmount1, int24 lowerTick, int24 upperTick ) external onlyGovernorOrStrategistOrOperator nonReentrant { @@ -385,18 +572,18 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } // Withdraw enough funds from Reserve strategies - _withdrawForLiquidity(maxAmount0, maxAmount1); + _withdrawForLiquidity(desiredAmount0, desiredAmount1); // Provide liquidity if (tokenId > 0) { // Add liquidity to the position token Position storage p = tokenIdToPosition[tokenId]; - _increaseLiquidityForPosition(p, maxAmount0, maxAmount1); + _increaseLiquidityForPosition(p, desiredAmount0, desiredAmount1); } else { // Mint new position (tokenId, , , ) = _mintPosition( - maxAmount0, - maxAmount1, + desiredAmount0, + desiredAmount1, lowerTick, upperTick ); @@ -409,22 +596,31 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _depositAll(); } + /** + * @notice Increases liquidity of the active position. + * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them + * @param desiredAmount0 Desired amount of token0 to provide liquidity + * @param desiredAmount1 Desired amount of token1 to provide liquidity + */ function increaseLiquidityForActivePosition( - uint256 maxAmount0, - uint256 maxAmount1 + uint256 desiredAmount0, + uint256 desiredAmount1 ) external onlyGovernorOrStrategistOrOperator nonReentrant { require(currentPositionTokenId > 0, "No active position"); // Withdraw enough funds from Reserve strategies - _withdrawForLiquidity(maxAmount0, maxAmount1); + _withdrawForLiquidity(desiredAmount0, desiredAmount1); Position storage p = tokenIdToPosition[currentPositionTokenId]; - _increaseLiquidityForPosition(p, maxAmount0, maxAmount1); + _increaseLiquidityForPosition(p, desiredAmount0, desiredAmount1); // Deposit all dust back to reserve strategies _depositAll(); } + /** + * @notice Removes all liquidity from active position and collects the fees + */ function closeActivePosition() external onlyGovernorOrStrategistOrOperator @@ -434,6 +630,11 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _closePosition(currentPositionTokenId); } + /** + * @notice Removes all liquidity from specified position and collects the fees + * @dev Must be a position minted by this contract + * @param tokenId ERC721 token ID of the position to liquidate + */ function closePosition(uint256 tokenId) external onlyGovernorOrStrategistOrOperator @@ -443,16 +644,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _closePosition(tokenId); } - function _getTickPositionKey(int24 lowerTick, int24 upperTick) - internal - returns (int48 key) - { - if (lowerTick > upperTick) - (lowerTick, upperTick) = (upperTick, lowerTick); - key = int48(lowerTick) * 2**24; // Shift by 24 bits - key = key + int24(upperTick); - } - + /** + * @notice Closes the position denoted by the tokenId and and collects all fees + * @param tokenId ERC721 token ID of the position to liquidate + * @param amount0 Amount of token0 received after removing liquidity + * @param amount1 Amount of token1 received after removing liquidity + */ function _closePosition(uint256 tokenId) internal returns (uint256 amount0, uint256 amount1) @@ -481,26 +678,20 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { emit UniswapV3PositionClosed(tokenId, amount0, amount1); } - function _collectFeesForToken(uint256 tokenId) - internal - returns (uint256 amount0, uint256 amount1) - { - INonfungiblePositionManager.CollectParams - memory params = INonfungiblePositionManager.CollectParams({ - tokenId: tokenId, - recipient: address(this), - amount0Max: type(uint128).max, - amount1Max: type(uint128).max - }); - - (amount0, amount1) = nonfungiblePositionManager.collect(params); - - emit UniswapV3FeeCollected(tokenId, amount0, amount1); - } - + /** + * @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 lowerTick Lower tick index + * @param upperTick Upper tick index + * @return tokenId ERC721 token ID of the position minted + * @return liquidity Amount of liquidity added to the pool + * @return amount0 Amount of token0 added to the position + * @return amount1 Amount of token1 added to the position + */ function _mintPosition( - uint256 maxAmount0, - uint256 maxAmount1, + uint256 desiredAmount0, + uint256 desiredAmount1, int24 lowerTick, int24 upperTick ) @@ -519,14 +710,14 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { fee: poolFee, tickLower: lowerTick, tickUpper: upperTick, - amount0Desired: maxAmount0, - amount1Desired: maxAmount1, + amount0Desired: desiredAmount0, + amount1Desired: desiredAmount1, amount0Min: maxSlippage == 0 ? 0 - : (maxAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, + : (desiredAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, amount1Min: maxSlippage == 0 ? 0 - : (maxAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, + : (desiredAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, recipient: address(this), deadline: block.timestamp }); @@ -557,10 +748,19 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); } + /** + * @notice Increases liquidity of the position in the pool + * @param p Position object + * @param desiredAmount0 Desired amount of token0 to provide liquidity + * @param desiredAmount1 Desired amount of token1 to provide liquidity + * @return liquidity Amount of liquidity added to the pool + * @return amount0 Amount of token0 added to the position + * @return amount1 Amount of token1 added to the position + */ function _increaseLiquidityForPosition( Position storage p, - uint256 maxAmount0, - uint256 maxAmount1 + uint256 desiredAmount0, + uint256 desiredAmount1 ) internal returns ( @@ -574,14 +774,14 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager .IncreaseLiquidityParams({ tokenId: p.tokenId, - amount0Desired: maxAmount0, - amount1Desired: maxAmount1, + amount0Desired: desiredAmount0, + amount1Desired: desiredAmount1, amount0Min: maxSlippage == 0 ? 0 - : (maxAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, + : (desiredAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, amount1Min: maxSlippage == 0 ? 0 - : (maxAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, + : (desiredAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, deadline: block.timestamp }); @@ -593,6 +793,13 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { emit UniswapV3LiquidityAdded(p.tokenId, amount0, amount1, liquidity); } + /** + * @notice Removes liquidity of the position in the pool + * @param p Position object + * @param liquidity Amount of liquidity to remove form the position + * @return amount0 Amount of token0 received after liquidation + * @return amount1 Amount of token1 received after liquidation + */ function _decreaseLiquidityForPosition( Position storage p, uint128 liquidity @@ -631,6 +838,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { emit UniswapV3LiquidityRemoved(p.tokenId, amount0, amount1, liquidity); } + /*************************************** + ERC721 management + ****************************************/ + + /// Callback function for whenever a NFT is transferred to this contract + /// Ref: https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#IERC721Receiver-onERC721Received-address-address-uint256-bytes- function onERC721Received( address, address, @@ -642,6 +855,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { return this.onERC721Received.selector; } + + /*************************************** + Inherited functions + ****************************************/ + + /// @inheritdoc InitializableAbstractStrategy function safeApproveAllTokens() external override @@ -658,11 +877,15 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ); } + /** + * Removes all allowance of both the tokens from NonfungiblePositionManager + */ function resetAllowanceOfTokens() external onlyGovernor nonReentrant { IERC20(token0).safeApprove(address(nonfungiblePositionManager), 0); IERC20(token1).safeApprove(address(nonfungiblePositionManager), 0); } + /// @inheritdoc InitializableAbstractStrategy function _abstractSetPToken(address _asset, address _pToken) internal override @@ -673,6 +896,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ); } + /// @inheritdoc InitializableAbstractStrategy function supportsAsset(address _asset) external view @@ -682,30 +906,20 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { return _asset == token0 || _asset == token1; } - /** - * Unused/unnecessary inherited functions - */ + /*************************************** + Hidden functions + ****************************************/ function setPTokenAddress(address _asset, address _pToken) external override onlyGovernor { - /** - * This function isn't overridable from `InitializableAbstractStrategy` due to - * missing `virtual` keyword. However, adding a function with same signature will - * hide the inherited function - */ // The pool tokens can never change. revert("Unsupported method"); } function removePToken(uint256 _assetIndex) external override onlyGovernor { - /** - * This function isn't overridable from `InitializableAbstractStrategy` due to - * missing `virtual` keyword. However, adding a function with same signature will - * hide the inherited function - */ // The pool tokens can never change. revert("Unsupported method"); } diff --git a/contracts/contracts/utils/InitializableAbstractStrategy.sol b/contracts/contracts/utils/InitializableAbstractStrategy.sol index 0add108bcf..294a0a803b 100644 --- a/contracts/contracts/utils/InitializableAbstractStrategy.sol +++ b/contracts/contracts/utils/InitializableAbstractStrategy.sol @@ -103,7 +103,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { } /** - * @dev Collect accumulated reward token and send to Vault. + * @dev Collect accumulated reward token and send to Harvester. */ function collectRewardTokens() external virtual onlyHarvester nonReentrant { _collectRewardTokens(); @@ -280,10 +280,20 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { Abstract ****************************************/ + /** + * @dev Callback function that's invoked everytime + * PToken address of an supported asset is updated. + * @param _asset Address for the asset + * @param _pToken Adress of the platform token + */ function _abstractSetPToken(address _asset, address _pToken) internal virtual; + /** + * @dev Approve all the assets supported by the strategy + * to be moved around the platform. + */ function safeApproveAllTokens() external virtual; /** @@ -316,7 +326,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { function withdrawAll() external virtual; /** - * @dev Get the total asset value held in the platform. + * @notice Get the total asset value held in the platform. * This includes any interest that was generated since depositing. * @param _asset Address of the asset * @return balance Total value of the asset in the platform From 2bc18cb5ca23d4bfaaaeb1ca803c5e4205a61a8f Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 4 Mar 2023 19:05:15 +0530 Subject: [PATCH 06/83] Document more functions --- .../GeneralizedUniswapV3Strategy.sol | 38 +++++++------------ .../utils/InitializableAbstractStrategy.sol | 4 +- contracts/contracts/utils/UniswapV3Helper.sol | 5 +++ contracts/contracts/vault/VaultAdmin.sol | 35 +++++++++++++++++ 4 files changed, 55 insertions(+), 27 deletions(-) diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index 45ec77e5d8..bda71abe6e 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -72,7 +72,6 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { int24 lowerTick; // Lower tick index int24 upperTick; // Upper tick index bool exists; // True, if position is minted - // 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? @@ -272,7 +271,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { /** * @notice Withdraws asset from the reserve strategy * @inheritdoc InitializableAbstractStrategy - */ + */ function withdraw( address recipient, address _asset, @@ -286,7 +285,11 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { if (selfBalance < amount) { // Try to pull remaining amount from reserve strategy // This might throw if there isn't enough in reserve strategy as well - IVault(vaultAddress).withdrawForUniswapV3(recipient, asset, amount - selfBalance); + IVault(vaultAddress).withdrawForUniswapV3( + recipient, + asset, + amount - selfBalance + ); // TODO: Remove liquidity from V3 pool instead? @@ -305,7 +308,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { /** * @notice Closes active LP position, if any, and transfer all token balance to Vault * @inheritdoc InitializableAbstractStrategy - */ + */ function withdrawAll() external override onlyVault nonReentrant { if (currentPositionTokenId > 0) { _closePosition(currentPositionTokenId); @@ -332,7 +335,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * If not, tries to pull it from reserve strategies * @param minAmount0 Minimum amount of token0 needed * @param minAmount1 Minimum amount of token1 needed - */ + */ function _withdrawForLiquidity(uint256 minAmount0, uint256 minAmount1) internal { @@ -377,13 +380,6 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { if (currentPositionTokenId > 0) { _collectFeesForToken(currentPositionTokenId); } - - // for (uint256 i = 0; i < allTokenIds.length; i++) { - // uint256 tokenId = allTokenIds[0]; - // if (tokenIdToPosition[tokenId].liquidity > 0) { - // _collectFeesForToken(tokenId); - // } - // } } /** @@ -445,10 +441,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } /** - * @dev Only checks the active LP position. + * @dev Only checks the active LP position. * Doesn't return the balance held in the reserve strategies. * @inheritdoc InitializableAbstractStrategy - */ + */ function checkBalance(address _asset) external view @@ -466,11 +462,6 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { Position memory p = tokenIdToPosition[currentPositionTokenId]; balance += _checkAssetBalanceOfPosition(_asset, p, sqrtRatioX96); } - - // for (uint256 i = 0; i < allTokenIds.length; i++) { - // Position memory p = tokenIdToPosition[allTokenIds[i]]; - // balance += _checkAssetBalanceOfPosition(_asset, p, sqrtRatioX96); - // } } /** @@ -524,14 +515,13 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { amount1 += feeAmount1; } - /*************************************** Pool Liquidity Management ****************************************/ /** * @notice Returns a unique ID based on lowerTick and upperTick - * @dev Basically concats the lower tick and upper tick values. Shifts the value + * @dev Basically concats the lower tick and upper tick values. Shifts the value * of lowerTick by 24 bits and then adds the upperTick value to avoid overlaps. * So, the result is smaller in size (int48 rather than bytes32 when using keccak256) * @param lowerTick Lower tick index @@ -725,7 +715,6 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { (tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager .mint(params); - allTokenIds.push(tokenId); ticksToTokenId[_getTickPositionKey(lowerTick, upperTick)] = tokenId; tokenIdToPosition[tokenId] = Position({ exists: true, @@ -841,7 +830,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { /*************************************** ERC721 management ****************************************/ - + /// Callback function for whenever a NFT is transferred to this contract /// Ref: https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#IERC721Receiver-onERC721Received-address-address-uint256-bytes- function onERC721Received( @@ -855,11 +844,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { return this.onERC721Received.selector; } - /*************************************** Inherited functions ****************************************/ - + /// @inheritdoc InitializableAbstractStrategy function safeApproveAllTokens() external diff --git a/contracts/contracts/utils/InitializableAbstractStrategy.sol b/contracts/contracts/utils/InitializableAbstractStrategy.sol index 294a0a803b..e2db5e1dbc 100644 --- a/contracts/contracts/utils/InitializableAbstractStrategy.sol +++ b/contracts/contracts/utils/InitializableAbstractStrategy.sol @@ -281,7 +281,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { ****************************************/ /** - * @dev Callback function that's invoked everytime + * @dev Callback function that's invoked everytime * PToken address of an supported asset is updated. * @param _asset Address for the asset * @param _pToken Adress of the platform token @@ -291,7 +291,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { virtual; /** - * @dev Approve all the assets supported by the strategy + * @dev Approve all the assets supported by the strategy * to be moved around the platform. */ function safeApproveAllTokens() external virtual; diff --git a/contracts/contracts/utils/UniswapV3Helper.sol b/contracts/contracts/utils/UniswapV3Helper.sol index 20d1cdd1f3..a5db5e0cca 100644 --- a/contracts/contracts/utils/UniswapV3Helper.sol +++ b/contracts/contracts/utils/UniswapV3Helper.sol @@ -5,6 +5,11 @@ import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +/** + * @dev Uniswap V3 Contracts use Solidity v0.7.6 and OUSD contracts are on 0.8.6. + * So, the libraries cannot be directly imported into OUSD contracts. + * This contract (on v0.7.6) just proxies the calls to the Uniswap Libraries. + */ contract UniswapV3Helper { function getAmountsForLiquidity( uint160 sqrtRatioX96, diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index a4b3d1df6a..7bb38a1f34 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -197,10 +197,19 @@ contract VaultAdmin is VaultStorage { _approveStrategy(_addr, false); } + /** + * @dev Add a strategy to the Vault and mark it as Uniswap V3 strategy + * @param _addr Address of the strategy to add + */ function approveUniswapV3Strategy(address _addr) external onlyGovernor { _approveStrategy(_addr, true); } + /** + * @dev Add a strategy to the Vault + * @param _addr Address of the strategy to add + * @param isUniswapV3 Set to true, if the strategy is an instance of GeneralizedUniswapV3Strategy + */ function _approveStrategy(address _addr, bool isUniswapV3) internal { require(!strategies[_addr].isSupported, "Strategy already approved"); strategies[_addr] = Strategy({ @@ -528,6 +537,12 @@ contract VaultAdmin is VaultStorage { Uniswap V3 Utils ****************************************/ + /** + * @dev Deposits token to the reserve strategy + * @dev Only callable by whitelisted Uniswap V3 strategies + * @param asset The asset to deposit + * @param amount Amount of tokens to deposit + */ function depositForUniswapV3(address asset, uint256 amount) external onlyUniswapV3Strategies @@ -536,6 +551,12 @@ contract VaultAdmin is VaultStorage { _depositForUniswapV3(msg.sender, asset, amount); } + /** + * @dev Deposits token to the reserve strategy + * @param v3Strategy Uniswap V3 Strategy that's depositing the tokens + * @param asset The asset to deposit + * @param amount Amount of tokens to deposit + */ function _depositForUniswapV3( address v3Strategy, address asset, @@ -552,6 +573,13 @@ contract VaultAdmin is VaultStorage { IStrategy(reserveStrategy).deposit(asset, amount); } + /** + * @notice Moves tokens from reserve strategy to the recipient + * @dev Only callable by whitelisted Uniswap V3 strategies + * @param recipient Receiver of the funds + * @param asset The asset to move + * @param amount Amount of tokens to move + */ function withdrawForUniswapV3( address recipient, address asset, @@ -560,6 +588,13 @@ contract VaultAdmin is VaultStorage { _withdrawForUniswapV3(msg.sender, recipient, asset, amount); } + /** + * @notice Moves tokens from reserve strategy to the recipient + * @param v3Strategy Uniswap V3 Strategy that's requesting the withdraw + * @param recipient Receiver of the funds + * @param asset The asset to move + * @param amount Amount of tokens to move + */ function _withdrawForUniswapV3( address v3Strategy, address recipient, From 700fd576c86f4eb9ccf92de8eadabcad3041f8c2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 4 Mar 2023 19:07:02 +0530 Subject: [PATCH 07/83] Fix bugs --- .../strategies/GeneralizedUniswapV3Strategy.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index bda71abe6e..a884a3074a 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -50,10 +50,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // The address that can manage the positions on Uniswap V3 address public operatorAddr; - address public constant token0; // Token0 of Uniswap V3 Pool - address public constant token1; // Token1 of Uniswap V3 Pool + address public token0; // Token0 of Uniswap V3 Pool + address public token1; // Token1 of Uniswap V3 Pool - uint24 public constant poolFee; // Uniswap V3 Pool Fee + uint24 public poolFee; // Uniswap V3 Pool Fee uint24 internal maxSlippage = 100; // 1%; Slippage tolerance when providing liquidity // Address mapping of (Asset -> Strategy). When the funds are @@ -62,7 +62,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { mapping(address => address) public reserveStrategy; // Uniswap V3's PositionManager - INonfungiblePositionManager public constant nonfungiblePositionManager; + INonfungiblePositionManager public nonfungiblePositionManager; // Represents a position minted by this contract struct Position { @@ -287,7 +287,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // This might throw if there isn't enough in reserve strategy as well IVault(vaultAddress).withdrawForUniswapV3( recipient, - asset, + _asset, amount - selfBalance ); @@ -297,11 +297,11 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { asset.safeTransfer(recipient, selfBalance); // Emit event for only the amount transferred out from this strategy - emit Withdrawal(asset, asset, selfBalance); + emit Withdrawal(_asset, _asset, selfBalance); } else { // Transfer requested amount asset.safeTransfer(recipient, amount); - emit Withdrawal(asset, asset, amount); + emit Withdrawal(_asset, _asset, amount); } } From e30b62fdbb761f27a011fd41fe9d187c0a8adf7b Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 4 Mar 2023 20:13:43 +0530 Subject: [PATCH 08/83] Update fork tests --- .../GeneralizedUniswapV3Strategy.sol | 28 +++-- contracts/contracts/vault/VaultAdmin.sol | 2 +- .../test/strategies/uniswap-v3.fork-test.js | 109 ++++++++---------- 3 files changed, 67 insertions(+), 72 deletions(-) diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index a884a3074a..4d3798bce0 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -54,7 +54,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { address public token1; // Token1 of Uniswap V3 Pool uint24 public poolFee; // Uniswap V3 Pool Fee - uint24 internal maxSlippage = 100; // 1%; Slippage tolerance when providing liquidity + uint24 public maxSlippage = 100; // 1%; Slippage tolerance when providing liquidity // Address mapping of (Asset -> Strategy). When the funds are // not deployed in Uniswap V3 Pool, they will be deposited @@ -82,9 +82,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // A lookup table to find token IDs of position using f(lowerTick, upperTick) mapping(int48 => uint256) internal ticksToTokenId; // Maps tokenIDs to their Position object - mapping(uint256 => Position) internal tokenIdToPosition; + mapping(uint256 => Position) public tokenIdToPosition; // Token ID of the position that's being used to provide LP at the time - uint256 internal currentPositionTokenId; + uint256 public currentPositionTokenId; // A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch IUniswapV3Helper internal uniswapV3Helper; @@ -336,7 +336,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @param minAmount0 Minimum amount of token0 needed * @param minAmount1 Minimum amount of token1 needed */ - function _withdrawForLiquidity(uint256 minAmount0, uint256 minAmount1) + function _ensureAssetBalances(uint256 minAmount0, uint256 minAmount1) internal { IERC20 cToken0 = IERC20(token0); @@ -544,8 +544,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them * @param desiredAmount0 Desired amount of token0 to provide liquidity * @param desiredAmount1 Desired amount of token1 to provide liquidity - * @param lowerTick Lower tick index - * @param upperTick Upper tick index + * @param lowerTick Desired lower tick index + * @param upperTick Desired upper tick index */ function rebalance( uint256 desiredAmount0, @@ -562,7 +562,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } // Withdraw enough funds from Reserve strategies - _withdrawForLiquidity(desiredAmount0, desiredAmount1); + _ensureAssetBalances(desiredAmount0, desiredAmount1); // Provide liquidity if (tokenId > 0) { @@ -599,7 +599,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { require(currentPositionTokenId > 0, "No active position"); // Withdraw enough funds from Reserve strategies - _withdrawForLiquidity(desiredAmount0, desiredAmount1); + _ensureAssetBalances(desiredAmount0, desiredAmount1); Position storage p = tokenIdToPosition[currentPositionTokenId]; _increaseLiquidityForPosition(p, desiredAmount0, desiredAmount1); @@ -855,6 +855,14 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { onlyGovernor nonReentrant { + IERC20(token0).safeApprove( + vaultAddress, + type(uint256).max + ); + IERC20(token1).safeApprove( + vaultAddress, + type(uint256).max + ); IERC20(token0).safeApprove( address(nonfungiblePositionManager), type(uint256).max @@ -878,6 +886,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { internal override { + IERC20(_asset).safeApprove( + vaultAddress, + type(uint256).max + ); IERC20(_asset).safeApprove( address(nonfungiblePositionManager), type(uint256).max diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index 7bb38a1f34..13fd01c957 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -569,7 +569,7 @@ contract VaultAdmin is VaultStorage { reserveStrategy != address(0), "Invalid Reserve Strategy address" ); - IERC20(asset).safeTransfer(reserveStrategy, amount); + IERC20(asset).safeTransferFrom(v3Strategy, reserveStrategy, amount); IStrategy(reserveStrategy).deposit(asset, amount); } diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 0ff237c4ba..7ad87ca58e 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -165,7 +165,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { it("Should show correct amount of fees", async () => {}); }); - describe.only("Uniswap V3 positions", function () { + describe.only("Uniswap V3 LP positions", function () { const findMaxDepositableAmount = async ( lowerTick, upperTick, @@ -226,15 +226,18 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { }; it("Should mint position", async () => { + const usdcBalBefore = await strategy.checkBalance(usdc.address); + const usdtBalBefore = await strategy.checkBalance(usdt.address); + const [, activeTick] = await pool.slot0(); const lowerTick = activeTick - 1000; const upperTick = activeTick + 1000; - const { tokenId, tx } = await mintLiquidity( + const { tokenId, amount0Minted, amount1Minted, liquidityMinted, tx } = await mintLiquidity( lowerTick, upperTick, - "1000000", - "1000000" + "100000", + "100000" ); // Check events @@ -248,76 +251,56 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(nfp.tickLower).to.equal(lowerTick, "Invalid lower tick"); expect(nfp.tickUpper).to.equal(upperTick, "Invalid upper tick"); - // TODO: Check storage values in Strategy + // Check Strategy balance + const usdcBalAfter = await strategy.checkBalance(usdc.address); + const usdtBalAfter = await strategy.checkBalance(usdt.address); + expect(usdcBalAfter).gte(usdcBalBefore, "Expected USDC balance to have increased"); + expect(usdtBalAfter).gte(usdtBalBefore, "Expected USDT balance to have increased"); + expect(usdcBalAfter).to.approxEqual(usdcBalBefore.add(amount0Minted), "Deposited USDC mismatch"); + expect(usdtBalAfter).to.approxEqual(usdtBalBefore.add(amount1Minted), "Deposited USDT mismatch"); + + // Check data on strategy + const storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.exists).to.be.true; + expect(storedPosition.tokenId).to.equal(tokenId); + expect(storedPosition.lowerTick).to.equal(lowerTick); + expect(storedPosition.upperTick).to.equal(upperTick); + expect(storedPosition.liquidity).to.equal(liquidityMinted); + expect(await strategy.currentPositionTokenId()).to.equal(tokenId); }); it("Should increase liquidity of existing position", async () => { + const usdcBalBefore = await strategy.checkBalance(usdc.address); + const usdtBalBefore = await strategy.checkBalance(usdt.address); + const [, activeTick] = await pool.slot0(); - const lowerTick = activeTick - 1002; - const upperTick = activeTick + 1002; + const lowerTick = activeTick - 1003; + const upperTick = activeTick + 1005; + + const amount = "100000" + const amountUnits = BigNumber.from(amount).mul(10**6) // Mint position - await mintLiquidity(lowerTick, upperTick, "1000000", "1000000"); + const { tokenId, tx } = await mintLiquidity(lowerTick, upperTick, amount, amount); + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + const storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.exists).to.be.true; + expect(await strategy.currentPositionTokenId()).to.equal(tokenId); // Rebalance again to increase liquidity - const { tokenId, tx } = await mintLiquidity( - lowerTick, - upperTick, - "1000000", - "1000000" + const tx2 = await strategy.connect(operator).increaseLiquidityForActivePosition( + amountUnits, + amountUnits ); + await expect(tx2).to.have.emittedEvent("UniswapV3LiquidityAdded"); - // Check events - await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); - await expect(tx).to.have.emittedEvent("UniswapV3LiquidityAdded"); - - // Check minted position data - const nfp = await positionManager.positions(tokenId); - expect(nfp.token0).to.equal(usdc.address, "Invalid token0 address"); - expect(nfp.token1).to.equal(usdt.address, "Invalid token1 address"); - expect(nfp.tickLower).to.equal(lowerTick, "Invalid lower tick"); - expect(nfp.tickUpper).to.equal(upperTick, "Invalid upper tick"); - - // TODO: Check storage values in Strategy + // Check balance on strategy + const usdcBalAfter = await strategy.checkBalance(usdc.address); + const usdtBalAfter = await strategy.checkBalance(usdt.address); + expect(usdcBalAfter).to.approxEqualTolerance(usdcBalBefore.add(amountUnits.mul(2)), 1, "Deposited USDC mismatch"); + expect(usdtBalAfter).to.approxEqualTolerance(usdtBalBefore.add(amountUnits.mul(2)), 1, "Deposited USDT mismatch"); }); - // Mint - // Increase - // Close - - // it("Should be able close existing positions", async () => { - // const [, activeTick] = await pool.slot0(); - // const lowerTick = activeTick - 7 - // const upperTick = activeTick + 7 - - // const { tokenId, tx } = await mintLiquidity( - // lowerTick, - // upperTick, - // '1000000', - // '1000000' - // ) - - // // Check events - // await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted") - // await expect(tx).to.have.emittedEvent("UniswapV3LiquidityAdded") - - // // Check minted position data - // const nfp = await positionManager.positions(tokenId) - // expect(nfp.token0).to.equal(usdc.address, "Invalid token0 address") - // expect(nfp.token1).to.equal(usdt.address, "Invalid token1 address") - // expect(nfp.tickLower).to.equal(lowerTick, "Invalid lower tick") - // expect(nfp.tickUpper).to.equal(upperTick, "Invalid upper tick") - - // // TODO: Check storage values in Strategy - // }) - - // describe.only("Liquidity management", function () { - - // }) - - // it("Should provide liquidity on given tick", async () => { - // }); - - // it("Should close existing position", async () => {}); + it("Should close active LP position", async () => {}) }); }); From a08eb2f929edd169c387005784534acba1a1c557 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 4 Mar 2023 20:14:07 +0530 Subject: [PATCH 09/83] Lint --- .../test/strategies/uniswap-v3.fork-test.js | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 7ad87ca58e..b65ec36a67 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -233,12 +233,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const lowerTick = activeTick - 1000; const upperTick = activeTick + 1000; - const { tokenId, amount0Minted, amount1Minted, liquidityMinted, tx } = await mintLiquidity( - lowerTick, - upperTick, - "100000", - "100000" - ); + const { tokenId, amount0Minted, amount1Minted, liquidityMinted, tx } = + await mintLiquidity(lowerTick, upperTick, "100000", "100000"); // Check events await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); @@ -254,10 +250,22 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Check Strategy balance const usdcBalAfter = await strategy.checkBalance(usdc.address); const usdtBalAfter = await strategy.checkBalance(usdt.address); - expect(usdcBalAfter).gte(usdcBalBefore, "Expected USDC balance to have increased"); - expect(usdtBalAfter).gte(usdtBalBefore, "Expected USDT balance to have increased"); - expect(usdcBalAfter).to.approxEqual(usdcBalBefore.add(amount0Minted), "Deposited USDC mismatch"); - expect(usdtBalAfter).to.approxEqual(usdtBalBefore.add(amount1Minted), "Deposited USDT mismatch"); + expect(usdcBalAfter).gte( + usdcBalBefore, + "Expected USDC balance to have increased" + ); + expect(usdtBalAfter).gte( + usdtBalBefore, + "Expected USDT balance to have increased" + ); + expect(usdcBalAfter).to.approxEqual( + usdcBalBefore.add(amount0Minted), + "Deposited USDC mismatch" + ); + expect(usdtBalAfter).to.approxEqual( + usdtBalBefore.add(amount1Minted), + "Deposited USDT mismatch" + ); // Check data on strategy const storedPosition = await strategy.tokenIdToPosition(tokenId); @@ -277,30 +285,42 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const lowerTick = activeTick - 1003; const upperTick = activeTick + 1005; - const amount = "100000" - const amountUnits = BigNumber.from(amount).mul(10**6) + const amount = "100000"; + const amountUnits = BigNumber.from(amount).mul(10 ** 6); // Mint position - const { tokenId, tx } = await mintLiquidity(lowerTick, upperTick, amount, amount); + const { tokenId, tx } = await mintLiquidity( + lowerTick, + upperTick, + amount, + amount + ); await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); const storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.exists).to.be.true; expect(await strategy.currentPositionTokenId()).to.equal(tokenId); // Rebalance again to increase liquidity - const tx2 = await strategy.connect(operator).increaseLiquidityForActivePosition( - amountUnits, - amountUnits - ); + const tx2 = await strategy + .connect(operator) + .increaseLiquidityForActivePosition(amountUnits, amountUnits); await expect(tx2).to.have.emittedEvent("UniswapV3LiquidityAdded"); // Check balance on strategy const usdcBalAfter = await strategy.checkBalance(usdc.address); const usdtBalAfter = await strategy.checkBalance(usdt.address); - expect(usdcBalAfter).to.approxEqualTolerance(usdcBalBefore.add(amountUnits.mul(2)), 1, "Deposited USDC mismatch"); - expect(usdtBalAfter).to.approxEqualTolerance(usdtBalBefore.add(amountUnits.mul(2)), 1, "Deposited USDT mismatch"); + expect(usdcBalAfter).to.approxEqualTolerance( + usdcBalBefore.add(amountUnits.mul(2)), + 1, + "Deposited USDC mismatch" + ); + expect(usdtBalAfter).to.approxEqualTolerance( + usdtBalBefore.add(amountUnits.mul(2)), + 1, + "Deposited USDT mismatch" + ); }); - it("Should close active LP position", async () => {}) + it("Should close active LP position", async () => {}); }); }); From d4f982cf5a6b7ac7eb2ea9dc631fe39af75bf3a4 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 4 Mar 2023 23:28:50 +0530 Subject: [PATCH 10/83] Add more fork tests --- .../GeneralizedUniswapV3Strategy.json | 2 +- .../uniswap/v3/IUniswapV3Helper.sol | 15 + .../mocks/uniswap/v3/MockUniswapV3Pool.sol | 1 + .../GeneralizedUniswapV3Strategy.sol | 159 +++----- contracts/contracts/utils/UniswapV3Helper.sol | 247 ++++++++++++- contracts/test/_fixture.js | 7 + contracts/test/helpers.js | 1 + .../test/strategies/uniswap-v3.fork-test.js | 343 ++++++++++++------ contracts/utils/addresses.js | 2 + 9 files changed, 539 insertions(+), 238 deletions(-) diff --git a/brownie/interfaces/GeneralizedUniswapV3Strategy.json b/brownie/interfaces/GeneralizedUniswapV3Strategy.json index cfe4160f36..d27652d480 100644 --- a/brownie/interfaces/GeneralizedUniswapV3Strategy.json +++ b/brownie/interfaces/GeneralizedUniswapV3Strategy.json @@ -609,7 +609,7 @@ }, { "inputs": [], - "name": "nonfungiblePositionManager", + "name": "positionManager", "outputs": [ { "internalType": "contract INonfungiblePositionManager", diff --git a/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol b/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol index 092f94a8d4..7cdf3abe07 100644 --- a/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol +++ b/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.0; +import { INonfungiblePositionManager } from "./INonfungiblePositionManager.sol"; + interface IUniswapV3Helper { function getAmountsForLiquidity( uint160 sqrtRatioX96, @@ -21,4 +23,17 @@ interface IUniswapV3Helper { external view returns (uint160 sqrtPriceX96); + + function positionFees( + INonfungiblePositionManager positionManager, + address poolAddress, + uint256 tokenId + ) external view returns (uint256 amount0, uint256 amount1); + + function positionValue( + INonfungiblePositionManager positionManager, + address poolAddress, + uint256 tokenId, + uint160 sqrtRatioX96 + ) external view returns (uint256 amount0, uint256 amount1); } diff --git a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol index 8a93d94cd6..6ee8f970a0 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import { IUniswapV3Helper } from "../../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; +import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; contract MockUniswapV3Pool { address public immutable token0; diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index 4d3798bce0..7c6d8f8ea4 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -62,7 +62,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { mapping(address => address) public reserveStrategy; // Uniswap V3's PositionManager - INonfungiblePositionManager public nonfungiblePositionManager; + INonfungiblePositionManager public positionManager; // Represents a position minted by this contract struct Position { @@ -136,7 +136,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { address _operator, address _uniswapV3Helper ) external onlyGovernor initializer { - nonfungiblePositionManager = INonfungiblePositionManager( + positionManager = INonfungiblePositionManager( _nonfungiblePositionManager ); IUniswapV3Pool pool = IUniswapV3Pool(_poolAddress); @@ -387,31 +387,16 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @return amount0 Amount of token0 ready to be collected as fee * @return amount1 Amount of token1 ready to be collected as fee */ - function getPendingRewards() + function getPendingFees() external view - returns (uint128 amount0, uint128 amount1) - { - Position memory p = tokenIdToPosition[currentPositionTokenId]; - - (amount0, amount1) = _getTokensOwed(p); - } - - /** - * @notice Fetches the fees generated by the position on V3 pool - * @param p Position struct - * @return tokensOwed0 Amount of token0 ready to be collected as fee - * @return tokensOwed1 Amount of token1 ready to be collected as fee - */ - function _getTokensOwed(Position memory p) - internal - view - returns (uint128 tokensOwed0, uint128 tokensOwed1) + returns (uint256 amount0, uint256 amount1) { - if (!p.exists) return (0, 0); - - (, , , tokensOwed0, tokensOwed1) = IUniswapV3Pool(platformAddress) - .positions(p.positionKey); + (amount0, amount1) = uniswapV3Helper.positionFees( + positionManager, + platformAddress, + currentPositionTokenId + ); } /** @@ -432,10 +417,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { amount1Max: type(uint128).max }); - (amount0, amount1) = nonfungiblePositionManager.collect(params); - - emit RewardTokenCollected(address(this), token0, amount0); - emit RewardTokenCollected(address(this), token1, amount1); + (amount0, amount1) = positionManager.collect(params); emit UniswapV3FeeCollected(tokenId, amount0, amount1); } @@ -459,60 +441,24 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { .slot0(); if (currentPositionTokenId > 0) { - Position memory p = tokenIdToPosition[currentPositionTokenId]; - balance += _checkAssetBalanceOfPosition(_asset, p, sqrtRatioX96); - } - } + require( + tokenIdToPosition[currentPositionTokenId].exists, + "Invalid token" + ); - /** - * @dev Get the balance of an asset held in a LP position along with fees generated - * @param asset Address of the asset - * @param p Position object - * @param sqrtRatioX96 Price ratio of the current tick of the pool - * @return balance Total amount of the asset available - */ - function _checkAssetBalanceOfPosition( - address asset, - Position memory p, - uint160 sqrtRatioX96 - ) internal view returns (uint256 balance) { - if (asset == token0) { - (balance, ) = _checkBalanceOfPosition(p, sqrtRatioX96); - } else { - (, balance) = _checkBalanceOfPosition(p, sqrtRatioX96); - } - } + (uint256 amount0, uint256 amount1) = uniswapV3Helper.positionValue( + positionManager, + platformAddress, + currentPositionTokenId, + sqrtRatioX96 + ); - /** - * @notice Get the balance of both assets held in a LP position along with fees generated - * @param p Position object - * @param sqrtRatioX96 Price ratio of the current tick of the pool - * @return amount0 Total amount of the token0 available - * @return amount1 Total amount of the token1 available - */ - function _checkBalanceOfPosition(Position memory p, uint160 sqrtRatioX96) - internal - view - returns (uint256 amount0, uint256 amount1) - { - if (p.liquidity == 0) { - // NOTE: Making the assumption that tokens owed for inactive positions - // will always be zero (should be case since fees are collecting after - // liquidity is removed) - return (0, 0); + if (_asset == token0) { + balance += amount0; + } else if (_asset == token1) { + balance += amount1; + } } - - (amount0, amount1) = uniswapV3Helper.getAmountsForLiquidity( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - p.liquidity - ); - - (uint128 feeAmount0, uint128 feeAmount1) = _getTokensOwed(p); - - amount0 += feeAmount0; - amount1 += feeAmount1; } /*************************************** @@ -632,6 +578,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { { require(tokenIdToPosition[tokenId].exists, "Invalid position"); _closePosition(tokenId); + + // Deposit all dust back to reserve strategies + _depositAll(); } /** @@ -712,8 +661,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { deadline: block.timestamp }); - (tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager - .mint(params); + (tokenId, liquidity, amount0, amount1) = positionManager.mint(params); ticksToTokenId[_getTickPositionKey(lowerTick, upperTick)] = tokenId; tokenIdToPosition[tokenId] = Position({ @@ -725,11 +673,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { sqrtRatioAX96: uniswapV3Helper.getSqrtRatioAtTick(lowerTick), sqrtRatioBX96: uniswapV3Helper.getSqrtRatioAtTick(upperTick), positionKey: keccak256( - abi.encodePacked( - address(nonfungiblePositionManager), - lowerTick, - upperTick - ) + abi.encodePacked(address(positionManager), lowerTick, upperTick) ) }); @@ -774,8 +718,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { deadline: block.timestamp }); - (liquidity, amount0, amount1) = nonfungiblePositionManager - .increaseLiquidity(params); + (liquidity, amount0, amount1) = positionManager.increaseLiquidity( + params + ); p.liquidity += liquidity; @@ -818,9 +763,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { deadline: block.timestamp }); - (amount0, amount1) = nonfungiblePositionManager.decreaseLiquidity( - params - ); + (amount0, amount1) = positionManager.decreaseLiquidity(params); p.liquidity -= liquidity; @@ -855,30 +798,18 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { onlyGovernor nonReentrant { - IERC20(token0).safeApprove( - vaultAddress, - type(uint256).max - ); - IERC20(token1).safeApprove( - vaultAddress, - type(uint256).max - ); - IERC20(token0).safeApprove( - address(nonfungiblePositionManager), - type(uint256).max - ); - IERC20(token1).safeApprove( - address(nonfungiblePositionManager), - type(uint256).max - ); + IERC20(token0).safeApprove(vaultAddress, type(uint256).max); + IERC20(token1).safeApprove(vaultAddress, type(uint256).max); + IERC20(token0).safeApprove(address(positionManager), type(uint256).max); + IERC20(token1).safeApprove(address(positionManager), type(uint256).max); } /** * Removes all allowance of both the tokens from NonfungiblePositionManager */ function resetAllowanceOfTokens() external onlyGovernor nonReentrant { - IERC20(token0).safeApprove(address(nonfungiblePositionManager), 0); - IERC20(token1).safeApprove(address(nonfungiblePositionManager), 0); + IERC20(token0).safeApprove(address(positionManager), 0); + IERC20(token1).safeApprove(address(positionManager), 0); } /// @inheritdoc InitializableAbstractStrategy @@ -886,14 +817,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { internal override { - IERC20(_asset).safeApprove( - vaultAddress, - type(uint256).max - ); - IERC20(_asset).safeApprove( - address(nonfungiblePositionManager), - type(uint256).max - ); + IERC20(_asset).safeApprove(vaultAddress, type(uint256).max); + IERC20(_asset).safeApprove(address(positionManager), type(uint256).max); } /// @inheritdoc InitializableAbstractStrategy diff --git a/contracts/contracts/utils/UniswapV3Helper.sol b/contracts/contracts/utils/UniswapV3Helper.sol index a5db5e0cca..322b2f3d32 100644 --- a/contracts/contracts/utils/UniswapV3Helper.sol +++ b/contracts/contracts/utils/UniswapV3Helper.sol @@ -1,9 +1,13 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity =0.7.6; -import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/libraries/FixedPoint128.sol"; import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; -import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/libraries/Tick.sol"; +import "@uniswap/v3-periphery/contracts/libraries/PositionKey.sol"; +import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; +import { INonfungiblePositionManager } from "../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; /** * @dev Uniswap V3 Contracts use Solidity v0.7.6 and OUSD contracts are on 0.8.6. @@ -50,4 +54,243 @@ contract UniswapV3Helper { { return TickMath.getSqrtRatioAtTick(tick); } + + function positionFees( + INonfungiblePositionManager positionManager, + address poolAddress, + uint256 tokenId + ) external view returns (uint256 amount0, uint256 amount1) { + return PositionValue.fees(positionManager, poolAddress, tokenId); + } + + function positionValue( + INonfungiblePositionManager positionManager, + address poolAddress, + uint256 tokenId, + uint160 sqrtRatioX96 + ) external view returns (uint256 amount0, uint256 amount1) { + return + PositionValue.total( + positionManager, + poolAddress, + tokenId, + sqrtRatioX96 + ); + } +} + +/// @dev Couldn't import this library directly either because of issues with OpenZeppelin versioning +/// @title Returns information about the token value held in a Uniswap V3 NFT +library PositionValue { + /// @notice Returns the total amounts of token0 and token1, i.e. the sum of fees and principal + /// that a given nonfungible position manager token is worth + /// @param positionManager The Uniswap V3 NonfungiblePositionManager + /// @param poolAddress The Uniswap V3 Pool + /// @param tokenId The tokenId of the token for which to get the total value + /// @param sqrtRatioX96 The square root price X96 for which to calculate the principal amounts + /// @return amount0 The total amount of token0 including principal and fees + /// @return amount1 The total amount of token1 including principal and fees + function total( + INonfungiblePositionManager positionManager, + address poolAddress, + uint256 tokenId, + uint160 sqrtRatioX96 + ) internal view returns (uint256 amount0, uint256 amount1) { + (uint256 amount0Principal, uint256 amount1Principal) = principal( + positionManager, + tokenId, + sqrtRatioX96 + ); + (uint256 amount0Fee, uint256 amount1Fee) = fees( + positionManager, + poolAddress, + tokenId + ); + return (amount0Principal + amount0Fee, amount1Principal + amount1Fee); + } + + /// @notice Calculates the principal (currently acting as liquidity) owed to the token owner in the event + /// that the position is burned + /// @param positionManager The Uniswap V3 NonfungiblePositionManager + /// @param tokenId The tokenId of the token for which to get the total principal owed + /// @param sqrtRatioX96 The square root price X96 for which to calculate the principal amounts + /// @return amount0 The principal amount of token0 + /// @return amount1 The principal amount of token1 + function principal( + INonfungiblePositionManager positionManager, + uint256 tokenId, + uint160 sqrtRatioX96 + ) internal view returns (uint256 amount0, uint256 amount1) { + ( + , + , + , + , + , + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + , + , + , + + ) = positionManager.positions(tokenId); + + return + LiquidityAmounts.getAmountsForLiquidity( + sqrtRatioX96, + TickMath.getSqrtRatioAtTick(tickLower), + TickMath.getSqrtRatioAtTick(tickUpper), + liquidity + ); + } + + struct FeeParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint128 liquidity; + uint256 positionFeeGrowthInside0LastX128; + uint256 positionFeeGrowthInside1LastX128; + uint256 tokensOwed0; + uint256 tokensOwed1; + } + + /// @notice Calculates the total fees owed to the token owner + /// @param positionManager The Uniswap V3 NonfungiblePositionManager + /// @param poolAddress The Uniswap V3 Pool + /// @param tokenId The tokenId of the token for which to get the total fees owed + /// @return amount0 The amount of fees owed in token0 + /// @return amount1 The amount of fees owed in token1 + function fees( + INonfungiblePositionManager positionManager, + address poolAddress, + uint256 tokenId + ) internal view returns (uint256 amount0, uint256 amount1) { + ( + , + , + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 positionFeeGrowthInside0LastX128, + uint256 positionFeeGrowthInside1LastX128, + uint256 tokensOwed0, + uint256 tokensOwed1 + ) = positionManager.positions(tokenId); + + return + _fees( + poolAddress, + FeeParams({ + token0: token0, + token1: token1, + fee: fee, + tickLower: tickLower, + tickUpper: tickUpper, + liquidity: liquidity, + positionFeeGrowthInside0LastX128: positionFeeGrowthInside0LastX128, + positionFeeGrowthInside1LastX128: positionFeeGrowthInside1LastX128, + tokensOwed0: tokensOwed0, + tokensOwed1: tokensOwed1 + }) + ); + } + + function _fees(address poolAddress, FeeParams memory feeParams) + private + view + returns (uint256 amount0, uint256 amount1) + { + ( + uint256 poolFeeGrowthInside0LastX128, + uint256 poolFeeGrowthInside1LastX128 + ) = _getFeeGrowthInside( + IUniswapV3Pool(poolAddress), + feeParams.tickLower, + feeParams.tickUpper + ); + + amount0 = + FullMath.mulDiv( + poolFeeGrowthInside0LastX128 - + feeParams.positionFeeGrowthInside0LastX128, + feeParams.liquidity, + FixedPoint128.Q128 + ) + + feeParams.tokensOwed0; + + amount1 = + FullMath.mulDiv( + poolFeeGrowthInside1LastX128 - + feeParams.positionFeeGrowthInside1LastX128, + feeParams.liquidity, + FixedPoint128.Q128 + ) + + feeParams.tokensOwed1; + } + + function _getFeeGrowthInside( + IUniswapV3Pool pool, + int24 tickLower, + int24 tickUpper + ) + private + view + returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) + { + (, int24 tickCurrent, , , , , ) = pool.slot0(); + ( + , + , + uint256 lowerFeeGrowthOutside0X128, + uint256 lowerFeeGrowthOutside1X128, + , + , + , + + ) = pool.ticks(tickLower); + ( + , + , + uint256 upperFeeGrowthOutside0X128, + uint256 upperFeeGrowthOutside1X128, + , + , + , + + ) = pool.ticks(tickUpper); + + if (tickCurrent < tickLower) { + feeGrowthInside0X128 = + lowerFeeGrowthOutside0X128 - + upperFeeGrowthOutside0X128; + feeGrowthInside1X128 = + lowerFeeGrowthOutside1X128 - + upperFeeGrowthOutside1X128; + } else if (tickCurrent < tickUpper) { + uint256 feeGrowthGlobal0X128 = pool.feeGrowthGlobal0X128(); + uint256 feeGrowthGlobal1X128 = pool.feeGrowthGlobal1X128(); + feeGrowthInside0X128 = + feeGrowthGlobal0X128 - + lowerFeeGrowthOutside0X128 - + upperFeeGrowthOutside0X128; + feeGrowthInside1X128 = + feeGrowthGlobal1X128 - + lowerFeeGrowthOutside1X128 - + upperFeeGrowthOutside1X128; + } else { + feeGrowthInside0X128 = + upperFeeGrowthOutside0X128 - + lowerFeeGrowthOutside0X128; + feeGrowthInside1X128 = + upperFeeGrowthOutside1X128 - + lowerFeeGrowthOutside1X128; + } + } } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 9e03b3f59e..e484a3ff3d 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -171,6 +171,7 @@ async function defaultFixture() { LUSDMetaStrategy, UniV3PositionManager, UniV3_USDC_USDT_Pool, + UniV3SwapRouter, mockStrategy, mockStrategyDAI; @@ -240,6 +241,11 @@ async function defaultFixture() { "IUniswapV3Pool", addresses.mainnet.UniV3_USDC_USDT_Pool ); + + UniV3SwapRouter = await ethers.getContractAt( + "ISwapRouter", + addresses.mainnet.UniV3SwapRouter + ); } else { usdt = await ethers.getContract("MockUSDT"); dai = await ethers.getContract("MockDAI"); @@ -464,6 +470,7 @@ async function defaultFixture() { UniV3_USDC_USDT_Pool, UniV3_USDC_USDT_Strategy, UniV3Helper, + UniV3SwapRouter, mockStrategy, mockStrategyDAI, }; diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index f46b2b5db1..b4734a6a88 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -367,6 +367,7 @@ const getAssetAddresses = async (deployments) => { UniV3PositionManager: addresses.mainnet.UniV3PositionManager, UniV3_USDC_USDT_Pool: addresses.mainnet.UniV3_USDC_USDT_Pool, + UniV3SwapRouter: addresses.mainnet.UniV3SwapRouter, }; } else { const addressMap = { diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index b65ec36a67..6c44fbb2a3 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -15,6 +15,7 @@ const { daiUnitsFormat, advanceTime, advanceBlocks, + getBlockTimestamp, } = require("../helpers"); const { BigNumber } = require("ethers"); @@ -23,8 +24,16 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { let fixture; let vault, harvester, ousd, usdc, usdt, dai; - let reserveStrategy, strategy, pool, positionManager, v3Helper; - let governor, strategist, operator, josh, matt, daniel, domen, franck; + let reserveStrategy, strategy, pool, positionManager, v3Helper, swapRouter; + let timelock, + governor, + strategist, + operator, + josh, + matt, + daniel, + domen, + franck; beforeEach(async () => { fixture = await loadFixture(uniswapV3Fixture); @@ -33,6 +42,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { pool = fixture.UniV3_USDC_USDT_Pool; positionManager = fixture.UniV3PositionManager; v3Helper = fixture.UniV3Helper; + swapRouter = fixture.UniV3SwapRouter; ousd = fixture.ousd; usdc = fixture.usdc; @@ -43,6 +53,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { governor = fixture.governor; strategist = fixture.strategist; operator = fixture.operator; + timelock = fixture.timelock; josh = fixture.josh; matt = fixture.matt; daniel = fixture.daniel; @@ -50,122 +61,11 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { franck = fixture.franck; }); - describe("Mint", function () { - const mintTest = async (user, amount, asset) => { - const ousdAmount = ousdUnits(amount); - const tokenAmount = await units(amount, asset); - - const currentSupply = await ousd.totalSupply(); - const ousdBalance = await ousd.balanceOf(user.address); - const tokenBalance = await asset.balanceOf(user.address); - const reserveTokenBalance = await reserveStrategy.checkBalance( - asset.address - ); - - // await asset.connect(user).approve(vault.address, tokenAmount) - await vault.connect(user).mint(asset.address, tokenAmount, 0); - - await expect(ousd).to.have.an.approxTotalSupplyOf( - currentSupply.add(ousdAmount), - "Total supply mismatch" - ); - if (asset == dai) { - // DAI is unsupported and should not be deposited in reserve strategy - await expect(reserveStrategy).to.have.an.assetBalanceOf( - reserveTokenBalance, - asset, - "Expected reserve strategy to not support DAI" - ); - } else { - await expect(reserveStrategy).to.have.an.assetBalanceOf( - reserveTokenBalance.add(tokenAmount), - asset, - "Expected reserve strategy to have received the other token" - ); - } - - await expect(user).to.have.an.approxBalanceWithToleranceOf( - ousdBalance.add(ousdAmount), - ousd, - 1, - "Should've minted equivalent OUSD" - ); - await expect(user).to.have.an.approxBalanceWithToleranceOf( - tokenBalance.sub(tokenAmount), - asset, - 1, - "Should've deposoited equivaluent other token" - ); - }; - - it("with USDC", async () => { - await mintTest(daniel, "30000", usdc); - }); - it("with USDT", async () => { - await mintTest(domen, "30000", usdt); - }); - it("with DAI", async () => { - await mintTest(franck, "30000", dai); - }); - }); + describe("Uniswap V3 LP positions", function () { + // NOTE: These tests all work on the assumption that the strategy + // has no active position, which might not be true after deployment. + // Gotta update the tests before that - describe("Redeem", function () { - const redeemTest = async (user, amount) => { - const ousdAmount = ousdUnits(amount); - - let ousdBalance = await ousd.balanceOf(user.address); - if (ousdBalance.lt(ousdAmount)) { - // Mint some OUSD - await vault.connect(user).mint(dai.address, daiUnits(amount), 0); - ousdBalance = await ousd.balanceOf(user.address); - } - - const currentSupply = await ousd.totalSupply(); - const usdcBalance = await usdc.balanceOf(user.address); - const usdtBalance = await usdt.balanceOf(user.address); - const daiBalance = await dai.balanceOf(user.address); - - await vault.connect(user).redeem(ousdAmount, 0); - - await expect(ousd).to.have.an.approxTotalSupplyOf( - currentSupply.sub(ousdAmount), - "Total supply mismatch" - ); - await expect(user).to.have.an.approxBalanceWithToleranceOf( - ousdBalance.sub(ousdAmount), - ousd, - 1, - "Should've burned equivalent OUSD" - ); - - const balanceDiff = - parseFloat( - usdcUnitsFormat((await usdc.balanceOf(user.address)) - usdcBalance) - ) + - parseFloat( - usdtUnitsFormat((await usdt.balanceOf(user.address)) - usdtBalance) - ) + - parseFloat( - daiUnitsFormat((await dai.balanceOf(user.address)) - daiBalance) - ); - - await expect(balanceDiff).to.approxEqualTolerance( - amount, - 1, - "Should've redeemed equivaluent other token" - ); - }; - - it("Should withdraw from reserve strategy", async () => { - redeemTest(josh, "10000"); - }); - }); - - describe("Rewards", function () { - it("Should show correct amount of fees", async () => {}); - }); - - describe.only("Uniswap V3 LP positions", function () { const findMaxDepositableAmount = async ( lowerTick, upperTick, @@ -321,6 +221,213 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { ); }); - it("Should close active LP position", async () => {}); + it("Should close LP position", async () => { + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick - 1003; + const upperTick = activeTick + 1005; + + const amount = "100000"; + + // Mint position + const { tokenId, tx } = await mintLiquidity( + lowerTick, + upperTick, + amount, + amount + ); + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + const storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.exists).to.be.true; + expect(await strategy.currentPositionTokenId()).to.equal(tokenId); + + // Remove liquidity + const tx2 = await strategy.connect(operator).closePosition(tokenId); + await expect(tx2).to.have.emittedEvent("UniswapV3LiquidityRemoved"); + + expect(await strategy.currentPositionTokenId()).to.equal( + BigNumber.from(0), + "Should have no active position" + ); + + // Check balance on strategy + const usdcBalAfter = await strategy.checkBalance(usdc.address); + const usdtBalAfter = await strategy.checkBalance(usdt.address); + expect(usdcBalAfter).to.equal( + BigNumber.from(0), + "Expected to have liquidated all USDC" + ); + expect(usdtBalAfter).to.equal( + BigNumber.from(0), + "Expected to have liquidated all USDT" + ); + }); + + async function _swap(user, amount, zeroForOne) { + const [, activeTick] = await pool.slot0(); + const sqrtPriceLimitX96 = await v3Helper.getSqrtRatioAtTick( + activeTick + (zeroForOne ? -2 : 2) + ); + const swapAmount = BigNumber.from(amount).mul(10 ** 6); + usdc.connect(user).approve(swapRouter.address, swapAmount.mul(10)); + usdt.connect(user).approve(swapRouter.address, swapAmount.mul(10)); + await swapRouter.connect(user).exactInputSingle([ + zeroForOne ? usdc.address : usdt.address, // tokenIn + zeroForOne ? usdt.address : usdc.address, // tokenOut + 100, // fee + user.address, // recipient + (await getBlockTimestamp()) + 5, // deadline + swapAmount, // amountIn + 0, // amountOutMinimum + sqrtPriceLimitX96, + ]); + } + + it("Should collect rewards", async () => { + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick - 12; + const upperTick = activeTick + 49; + + // Mint position + const amount = "100000"; + const { tokenId, tx } = await mintLiquidity( + lowerTick, + upperTick, + amount, + amount + ); + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + const storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.exists).to.be.true; + expect(await strategy.currentPositionTokenId()).to.equal(tokenId); + + // Do some big swaps + await _swap(matt, "1000000", true); + await _swap(josh, "1000000", false); + await _swap(franck, "1000000", true); + await _swap(daniel, "1000000", false); + await _swap(domen, "1000000", true); + + // Check reward amounts + let [fee0, fee1] = await strategy.getPendingFees(); + expect(fee0).to.be.gt(0); + expect(fee1).to.be.gt(0); + + // Harvest rewards + await harvester.connect(timelock)["harvest(address)"](strategy.address); + [fee0, fee1] = await strategy.getPendingFees(); + expect(fee0).to.equal(0); + expect(fee1).to.equal(0); + }); + }); + + describe("Mint", function () { + const mintTest = async (user, amount, asset) => { + const ousdAmount = ousdUnits(amount); + const tokenAmount = await units(amount, asset); + + const currentSupply = await ousd.totalSupply(); + const ousdBalance = await ousd.balanceOf(user.address); + const tokenBalance = await asset.balanceOf(user.address); + const reserveTokenBalance = await reserveStrategy.checkBalance( + asset.address + ); + + // await asset.connect(user).approve(vault.address, tokenAmount) + await vault.connect(user).mint(asset.address, tokenAmount, 0); + + await expect(ousd).to.have.an.approxTotalSupplyOf( + currentSupply.add(ousdAmount), + "Total supply mismatch" + ); + if (asset == dai) { + // DAI is unsupported and should not be deposited in reserve strategy + await expect(reserveStrategy).to.have.an.assetBalanceOf( + reserveTokenBalance, + asset, + "Expected reserve strategy to not support DAI" + ); + } else { + await expect(reserveStrategy).to.have.an.assetBalanceOf( + reserveTokenBalance.add(tokenAmount), + asset, + "Expected reserve strategy to have received the other token" + ); + } + + await expect(user).to.have.an.approxBalanceWithToleranceOf( + ousdBalance.add(ousdAmount), + ousd, + 1, + "Should've minted equivalent OUSD" + ); + await expect(user).to.have.an.approxBalanceWithToleranceOf( + tokenBalance.sub(tokenAmount), + asset, + 1, + "Should've deposoited equivaluent other token" + ); + }; + + it("with USDC", async () => { + await mintTest(daniel, "30000", usdc); + }); + it("with USDT", async () => { + await mintTest(domen, "30000", usdt); + }); + it("with DAI", async () => { + await mintTest(franck, "30000", dai); + }); + }); + + describe("Redeem", function () { + const redeemTest = async (user, amount) => { + const ousdAmount = ousdUnits(amount); + + let ousdBalance = await ousd.balanceOf(user.address); + if (ousdBalance.lt(ousdAmount)) { + // Mint some OUSD + await vault.connect(user).mint(dai.address, daiUnits(amount), 0); + ousdBalance = await ousd.balanceOf(user.address); + } + + const currentSupply = await ousd.totalSupply(); + const usdcBalance = await usdc.balanceOf(user.address); + const usdtBalance = await usdt.balanceOf(user.address); + const daiBalance = await dai.balanceOf(user.address); + + await vault.connect(user).redeem(ousdAmount, 0); + + await expect(ousd).to.have.an.approxTotalSupplyOf( + currentSupply.sub(ousdAmount), + "Total supply mismatch" + ); + await expect(user).to.have.an.approxBalanceWithToleranceOf( + ousdBalance.sub(ousdAmount), + ousd, + 1, + "Should've burned equivalent OUSD" + ); + + const balanceDiff = + parseFloat( + usdcUnitsFormat((await usdc.balanceOf(user.address)) - usdcBalance) + ) + + parseFloat( + usdtUnitsFormat((await usdt.balanceOf(user.address)) - usdtBalance) + ) + + parseFloat( + daiUnitsFormat((await dai.balanceOf(user.address)) - daiBalance) + ); + + await expect(balanceDiff).to.approxEqualTolerance( + amount, + 1, + "Should've redeemed equivaluent other token" + ); + }; + + it("Should withdraw from reserve strategy", async () => { + redeemTest(josh, "10000"); + }); }); }); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 519bf596b7..7fc67d697a 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -142,6 +142,8 @@ addresses.mainnet.UniV3PositionManager = "0xC36442b4a4522E871399CD717aBDD847Ab11FE88"; addresses.mainnet.UniV3_USDC_USDT_Pool = "0x3416cf6c708da44db2624d63ea0aaef7113527c6"; +addresses.mainnet.UniV3SwapRouter = + "0xE592427A0AEce92De3Edee1F18E0157C05861564"; // OUSD Governance addresses.mainnet.GovernorFive = "0x3cdd07c16614059e66344a7b579dab4f9516c0b6"; From 91fd51d793aa5e130508923322929eaa19820353 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 5 Mar 2023 12:13:31 +0530 Subject: [PATCH 11/83] Linter/prettier --- .../GeneralizedUniswapV3Strategy.sol | 19 ++- contracts/contracts/vault/VaultCore.sol | 4 - .../test/strategies/uniswap-v3.fork-test.js | 14 +- contracts/test/strategies/uniswap-v3.js | 135 ++++++++---------- 4 files changed, 74 insertions(+), 98 deletions(-) diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index 7c6d8f8ea4..d975b9b909 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -159,6 +159,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ); _setReserveStrategy(_token0ReserveStrategy, _token1ReserveStrategy); + _setOperator(_operator); } /*************************************** @@ -186,6 +187,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @param _operator The new value to be set */ function setOperator(address _operator) external onlyGovernor { + _setOperator(_operator); + } + + function _setOperator(address _operator) internal { require(_operator != address(0), "Invalid operator address"); operatorAddr = _operator; emit OperatorChanged(_operator); @@ -775,6 +780,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ****************************************/ /// Callback function for whenever a NFT is transferred to this contract + // solhint-disable-next-line max-line-length /// Ref: https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#IERC721Receiver-onERC721Received-address-address-uint256-bytes- function onERC721Received( address, @@ -813,10 +819,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } /// @inheritdoc InitializableAbstractStrategy - function _abstractSetPToken(address _asset, address _pToken) - internal - override - { + function _abstractSetPToken(address _asset, address) internal override { IERC20(_asset).safeApprove(vaultAddress, type(uint256).max); IERC20(_asset).safeApprove(address(positionManager), type(uint256).max); } @@ -835,16 +838,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { Hidden functions ****************************************/ - function setPTokenAddress(address _asset, address _pToken) - external - override - onlyGovernor - { + function setPTokenAddress(address, address) external override onlyGovernor { // The pool tokens can never change. revert("Unsupported method"); } - function removePToken(uint256 _assetIndex) external override onlyGovernor { + function removePToken(uint256) external override onlyGovernor { // The pool tokens can never change. revert("Unsupported method"); } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 6da2cd3729..e58b38f781 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -354,10 +354,6 @@ contract VaultCore is VaultStorage { if (depositStrategyAddr != address(0) && allocateAmount > 0) { IStrategy strategy; if (strategies[depositStrategyAddr].isUniswapV3Strategy) { - IUniswapV3Strategy uniswapStrategy = IUniswapV3Strategy( - depositStrategyAddr - ); - address reserveStrategyAddr = IUniswapV3Strategy( depositStrategyAddr ).reserveStrategy(assetAddr); diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 6c44fbb2a3..8504b126f3 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -5,16 +5,10 @@ const { loadFixture, units, ousdUnits, - ousdUnitsFormat, - expectApproxSupply, - usdcUnits, usdcUnitsFormat, - usdtUnits, usdtUnitsFormat, daiUnits, daiUnitsFormat, - advanceTime, - advanceBlocks, getBlockTimestamp, } = require("../helpers"); const { BigNumber } = require("ethers"); @@ -26,8 +20,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { let vault, harvester, ousd, usdc, usdt, dai; let reserveStrategy, strategy, pool, positionManager, v3Helper, swapRouter; let timelock, - governor, - strategist, + // governor, + // strategist, operator, josh, matt, @@ -50,8 +44,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { dai = fixture.dai; vault = fixture.vault; harvester = fixture.harvester; - governor = fixture.governor; - strategist = fixture.strategist; + // governor = fixture.governor; + // strategist = fixture.strategist; operator = fixture.operator; timelock = fixture.timelock; josh = fixture.josh; diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 9e61297290..4af0166d0b 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -1,77 +1,64 @@ -const { expect } = require("chai"); -const { uniswapV3Fixture } = require("../_fixture"); -const { - loadFixture, - units, - ousdUnits, - expectApproxSupply, -} = require("../helpers"); +// const { expect } = require("chai"); +// const { uniswapV3Fixture } = require("../_fixture"); +// const { +// loadFixture, +// units, +// ousdUnits, +// expectApproxSupply, +// } = require("../helpers"); describe("Uniswap V3 Strategy", function () { - this.timeout(0); - - let fixture; - let vault, harvester, ousd, usdc, usdt, dai; - let reserveStrategy, uniV3Strategy, mockPool, mockPositionManager; - let governor, strategist, operator, josh, matt, daniel, domen, franck; - - beforeEach(async () => { - fixture = await loadFixture(uniswapV3Fixture); - reserveStrategy = fixture.mockStrategy; - uniV3Strategy = fixture.UniV3_USDC_USDT_Strategy; - mockPool = fixture.UniV3_USDC_USDT_Pool; - mockPositionManager = fixture.UniV3PositionManager; - - ousd = fixture.ousd; - usdc = fixture.usdc; - usdt = fixture.usdt; - dai = fixture.dai; - vault = fixture.vault; - harvester = fixture.harvester; - governor = fixture.governor; - strategist = fixture.strategist; - operator = fixture.operator; - josh = fixture.josh; - matt = fixture.matt; - daniel = fixture.daniel; - domen = fixture.domen; - franck = fixture.franck; - }); - - const mint = async (user, amount, asset) => { - await asset.connect(user).mint(units(amount, asset)); - await asset.connect(user).approve(vault.address, units(amount, asset)); - await vault.connect(user).mint(asset.address, units(amount, asset), 0); - }; - - describe.only("Mint", function () { - it("Should deposit to reserve strategy", async () => { - // Vault has 200 DAI from fixtures - await expectApproxSupply(ousd, ousdUnits("200")); - await expect(vault).has.an.approxBalanceOf("200", dai); - - // Mint some OUSD with USDC - await mint(daniel, "10000", usdc); - await expectApproxSupply(ousd, ousdUnits("10200")); - - console.log(await usdc.balanceOf(reserveStrategy.address)); - - // Make sure it went to reserve strategy - await expect(reserveStrategy).has.an.approxBalanceOf("10000", usdc); - }); - }); - - describe("Redeem", function () { - it("Should withdraw from reserve strategy", async () => {}); - }); - - describe("Rewards", function () { - it("Should show correct amount of fees", async () => {}); - }); - - describe("Rebalance", function () { - it("Should provide liquidity on given tick", async () => {}); - - it("Should close existing position", async () => {}); - }); + // let fixture; + // let vault, harvester, ousd, usdc, usdt, dai; + // let reserveStrategy, strategy, mockPool, mockPositionManager; + // let governor, strategist, operator, josh, matt, daniel, domen, franck; + // beforeEach(async () => { + // fixture = await loadFixture(uniswapV3Fixture); + // reserveStrategy = fixture.mockStrategy; + // strategy = fixture.UniV3_USDC_USDT_Strategy; + // mockPool = fixture.UniV3_USDC_USDT_Pool; + // mockPositionManager = fixture.UniV3PositionManager; + // ousd = fixture.ousd; + // usdc = fixture.usdc; + // usdt = fixture.usdt; + // dai = fixture.dai; + // vault = fixture.vault; + // harvester = fixture.harvester; + // governor = fixture.governor; + // strategist = fixture.strategist; + // operator = fixture.operator; + // josh = fixture.josh; + // matt = fixture.matt; + // daniel = fixture.daniel; + // domen = fixture.domen; + // franck = fixture.franck; + // }); + // const mint = async (user, amount, asset) => { + // await asset.connect(user).mint(units(amount, asset)); + // await asset.connect(user).approve(vault.address, units(amount, asset)); + // await vault.connect(user).mint(asset.address, units(amount, asset), 0); + // }; + // describe("Mint", function () { + // it("Should deposit to reserve strategy", async () => { + // // Vault has 200 DAI from fixtures + // await expectApproxSupply(ousd, ousdUnits("200")); + // await expect(vault).has.an.approxBalanceOf("200", dai); + // // Mint some OUSD with USDC + // await mint(daniel, "10000", usdc); + // await expectApproxSupply(ousd, ousdUnits("10200")); + // console.log(await usdc.balanceOf(reserveStrategy.address)); + // // Make sure it went to reserve strategy + // await expect(reserveStrategy).has.an.approxBalanceOf("10000", usdc); + // }); + // }); + // describe("Redeem", function () { + // it("Should withdraw from reserve strategy", async () => {}); + // }); + // describe("Rewards", function () { + // it("Should show correct amount of fees", async () => {}); + // }); + // describe("Rebalance", function () { + // it("Should provide liquidity on given tick", async () => {}); + // it("Should close existing position", async () => {}); + // }); }); From 86523a5b64a63cdf4f157ee4098cc4768161b9a2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 6 Mar 2023 15:25:08 +0530 Subject: [PATCH 12/83] Slither fixes --- .../strategies/GeneralizedUniswapV3Strategy.sol | 4 ++-- contracts/contracts/vault/VaultCore.sol | 11 +++++++++-- contracts/test/vault/vault.fork-test.js | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index d975b9b909..72e858ce79 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -323,13 +323,13 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { IERC20 cToken1 = IERC20(token1); uint256 token0Balance = cToken0.balanceOf(address(this)); - if (token0Balance >= 0) { + if (token0Balance > 0) { cToken0.safeTransfer(vaultAddress, token0Balance); emit Withdrawal(token0, token0, token0Balance); } uint256 token1Balance = cToken1.balanceOf(address(this)); - if (token1Balance >= 0) { + if (token1Balance > 0) { cToken1.safeTransfer(vaultAddress, token1Balance); emit Withdrawal(token1, token1, token1Balance); } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index e58b38f781..92c34bdaa1 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -353,17 +353,24 @@ contract VaultCore is VaultStorage { if (depositStrategyAddr != address(0) && allocateAmount > 0) { IStrategy strategy; + + // `strategies` is initialized in `VaultAdmin` + // slither-disable-next-line uninitialized-state if (strategies[depositStrategyAddr].isUniswapV3Strategy) { address reserveStrategyAddr = IUniswapV3Strategy( depositStrategyAddr ).reserveStrategy(assetAddr); + // Defensive check to make sure the address(0) or unsupported strategy + // isn't returned by `IUniswapV3Strategy.reserveStrategy()` + + // `strategies` is initialized in `VaultAdmin`. + // slither-disable-start uninitialized-state require( - // Defensive check to make sure the address(0) or unsupported strategy - // isn't returned by `IUniswapV3Strategy.reserveStrategy()` strategies[reserveStrategyAddr].isSupported, "Invalid reserve strategy for asset" ); + // slither-disable-end uninitialized-state // For Uniswap V3 Strategies, always deposit to reserve strategies strategy = IStrategy(reserveStrategyAddr); diff --git a/contracts/test/vault/vault.fork-test.js b/contracts/test/vault/vault.fork-test.js index 80dc6bdc6f..200153a346 100644 --- a/contracts/test/vault/vault.fork-test.js +++ b/contracts/test/vault/vault.fork-test.js @@ -301,6 +301,7 @@ forkOnlyDescribe("ForkTest: Vault", function () { "0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAaveStrategy // TODO: Hard-code these after deploy //"0x7A192DD9Cc4Ea9bdEdeC9992df74F1DA55e60a19", // LUSD MetaStrategy + "0xa863A50233FB5Aa5aFb515e6C3e6FB9c075AA594" // USDC<>USDT Uniswap V3 Strategy ]; for (const s of strategies) { From cc29fc821aedb38a06f5247a35e0a1deb20d4ba0 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 8 Mar 2023 21:04:27 +0530 Subject: [PATCH 13/83] Updates --- contracts/contracts/mocks/MockStrategy.sol | 10 +- .../GeneralizedUniswapV3Strategy.sol | 302 ++++++++++++------ contracts/contracts/vault/VaultCore.sol | 17 +- contracts/deploy/000_mock.js | 2 +- contracts/deploy/001_core.js | 3 +- contracts/deploy/004_single_asset_staking.js | 1 + contracts/deploy/005_compensation_claims.js | 1 + contracts/deploy/006_liquidity_reward.js | 1 + contracts/test/_fixture.js | 64 ++-- contracts/test/strategies/uniswap-v3.js | 169 ++++++---- contracts/test/vault/index.js | 3 - contracts/test/vault/vault.fork-test.js | 2 +- contracts/utils/deploy.js | 2 +- 13 files changed, 375 insertions(+), 202 deletions(-) diff --git a/contracts/contracts/mocks/MockStrategy.sol b/contracts/contracts/mocks/MockStrategy.sol index 4b75d27839..ca39d89b9d 100644 --- a/contracts/contracts/mocks/MockStrategy.sol +++ b/contracts/contracts/mocks/MockStrategy.sol @@ -26,7 +26,7 @@ contract MockStrategy { } function deposit(address _asset, uint256 _amount) public onlyVault { - IERC20(_asset).safeTransferFrom(msg.sender, address(this), _amount); + // Do nothing } function depositAll() public onlyVault { @@ -37,6 +37,14 @@ contract MockStrategy { IERC20(_asset).safeTransfer(vaultAddress, _amount); } + function withdraw( + address _recipient, + address _asset, + uint256 _amount + ) external onlyVault { + IERC20(_asset).safeTransfer(_recipient, _amount); + } + function checkBalance(address _asset) external view diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index 72e858ce79..5816bf592c 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -99,7 +99,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { require( msg.sender == IVault(vaultAddress).strategistAddr() || msg.sender == governor(), - "Caller is not the Operator, Strategist or Governor" + "Caller is not the Strategist or Governor" ); _; } @@ -166,21 +166,6 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { Admin Utils ****************************************/ - /** - * @notice Change the slippage tolerance - * @dev Can only be called by Governor or Strategist - * @param _slippage The new value to be set - */ - function setMaxSlippage(uint24 _slippage) - external - onlyGovernorOrStrategist - { - require(_slippage <= 10000, "Invalid slippage value"); - // TODO: Should we make sure that Governor doesn't - // accidentally set slippage > 2% or something??? - maxSlippage = _slippage; - } - /** * @notice Change the address of the operator * @dev Can only be called by the Governor @@ -288,25 +273,86 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 selfBalance = asset.balanceOf(address(this)); if (selfBalance < amount) { - // Try to pull remaining amount from reserve strategy - // This might throw if there isn't enough in reserve strategy as well - IVault(vaultAddress).withdrawForUniswapV3( - recipient, - _asset, - amount - selfBalance - ); + Position storage p = tokenIdToPosition[currentPositionTokenId]; + require(p.exists && p.liquidity > 0, "Liquidity error"); + + // Figure out liquidity to burn + ( + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) = _calculateLiquidityToWithdraw( + p, + _asset, + (amount - selfBalance) + ); + + // Liquidiate active position + _decreaseLiquidityForPosition(p, liquidity, minAmount0, minAmount1); + } - // TODO: Remove liquidity from V3 pool instead? + // Transfer requested amount + asset.safeTransfer(recipient, amount); + emit Withdrawal(_asset, _asset, amount); + } - // Transfer all of unused balance - asset.safeTransfer(recipient, selfBalance); + /** + * @notice Calculates the amount liquidity that needs to be removed + * to Withdraw specified amount of the given asset. + * + * @param p Position object + * @param asset Token needed + * @param amount Minimum amount to liquidate + * + * @return liquidity Liquidity to burn + * @return minAmount0 Minimum amount0 to expect + * @return minAmount1 Minimum amount1 to expect + */ + function _calculateLiquidityToWithdraw( + Position memory p, + address asset, + uint256 amount + ) + internal + view + returns ( + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) + { + (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(platformAddress) + .slot0(); + + // Total amount in Liquidity pools + (uint256 totalAmount0, uint256 totalAmount1) = uniswapV3Helper + .getAmountsForLiquidity( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + p.liquidity + ); - // Emit event for only the amount transferred out from this strategy - emit Withdrawal(_asset, _asset, selfBalance); + if (asset == token0) { + minAmount0 = amount; + minAmount1 = totalAmount1 / (totalAmount0 / amount); + liquidity = uniswapV3Helper.getLiquidityForAmounts( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + amount, + minAmount1 + ); } else { - // Transfer requested amount - asset.safeTransfer(recipient, amount); - emit Withdrawal(_asset, _asset, amount); + minAmount0 = totalAmount0 / (totalAmount1 / amount); + minAmount1 = amount; + liquidity = uniswapV3Helper.getLiquidityForAmounts( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + minAmount0, + amount + ); } } @@ -316,21 +362,26 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { */ function withdrawAll() external override onlyVault nonReentrant { if (currentPositionTokenId > 0) { - _closePosition(currentPositionTokenId); + // TODO: This method is only callable from Vault directly + // and by Governor or Strategist indirectly. + // Changing the Vault code to pass a minAmount0 and minAmount1 will + // make things complex. We could perhaps make sure that there're no + // active position when withdrawingAll rather than passing zero values? + _closePosition(currentPositionTokenId, 0, 0); } - IERC20 cToken0 = IERC20(token0); - IERC20 cToken1 = IERC20(token1); + IERC20 token0Contract = IERC20(token0); + IERC20 token1Contract = IERC20(token1); - uint256 token0Balance = cToken0.balanceOf(address(this)); + uint256 token0Balance = token0Contract.balanceOf(address(this)); if (token0Balance > 0) { - cToken0.safeTransfer(vaultAddress, token0Balance); + token0Contract.safeTransfer(vaultAddress, token0Balance); emit Withdrawal(token0, token0, token0Balance); } - uint256 token1Balance = cToken1.balanceOf(address(this)); + uint256 token1Balance = token1Contract.balanceOf(address(this)); if (token1Balance > 0) { - cToken1.safeTransfer(vaultAddress, token1Balance); + token1Contract.safeTransfer(vaultAddress, token1Balance); emit Withdrawal(token1, token1, token1Balance); } } @@ -344,12 +395,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { function _ensureAssetBalances(uint256 minAmount0, uint256 minAmount1) internal { - IERC20 cToken0 = IERC20(token0); - IERC20 cToken1 = IERC20(token1); + IERC20 token0Contract = IERC20(token0); + IERC20 token1Contract = IERC20(token1); IVault vault = IVault(vaultAddress); // Withdraw enough funds from Reserve strategies - uint256 token0Balance = cToken0.balanceOf(address(this)); + uint256 token0Balance = token0Contract.balanceOf(address(this)); if (token0Balance < minAmount0) { vault.withdrawForUniswapV3( address(this), @@ -358,7 +409,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ); } - uint256 token1Balance = cToken1.balanceOf(address(this)); + uint256 token1Balance = token1Contract.balanceOf(address(this)); if (token1Balance < minAmount1) { vault.withdrawForUniswapV3( address(this), @@ -376,10 +427,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @notice Collect accumulated fees from the active position * @dev Doesn't send to vault or harvester */ - function collectRewardTokens() + function collectFees() external - override - onlyHarvester + onlyGovernorOrStrategistOrOperator nonReentrant { if (currentPositionTokenId > 0) { @@ -495,12 +545,20 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them * @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 + * @param minRedeemAmount0 Min amount of token0 received from closing active position + * @param minRedeemAmount1 Min amount of token1 received from closing active position * @param lowerTick Desired lower tick index * @param upperTick Desired upper tick index */ function rebalance( uint256 desiredAmount0, uint256 desiredAmount1, + uint256 minAmount0, + uint256 minAmount1, + uint256 minRedeemAmount0, + uint256 minRedeemAmount1, int24 lowerTick, int24 upperTick ) external onlyGovernorOrStrategistOrOperator nonReentrant { @@ -509,7 +567,11 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { if (currentPositionTokenId > 0) { // Close any active position - _closePosition(currentPositionTokenId); + _closePosition( + currentPositionTokenId, + minRedeemAmount0, + minRedeemAmount1 + ); } // Withdraw enough funds from Reserve strategies @@ -519,12 +581,20 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { if (tokenId > 0) { // Add liquidity to the position token Position storage p = tokenIdToPosition[tokenId]; - _increaseLiquidityForPosition(p, desiredAmount0, desiredAmount1); + _increaseLiquidityForPosition( + p, + desiredAmount0, + desiredAmount1, + minAmount0, + minAmount1 + ); } else { // Mint new position (tokenId, , , ) = _mintPosition( desiredAmount0, desiredAmount1, + minAmount0, + minAmount1, lowerTick, upperTick ); @@ -542,10 +612,14 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them * @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 increaseLiquidityForActivePosition( uint256 desiredAmount0, - uint256 desiredAmount1 + uint256 desiredAmount1, + uint256 minAmount0, + uint256 minAmount1 ) external onlyGovernorOrStrategistOrOperator nonReentrant { require(currentPositionTokenId > 0, "No active position"); @@ -553,7 +627,13 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _ensureAssetBalances(desiredAmount0, desiredAmount1); Position storage p = tokenIdToPosition[currentPositionTokenId]; - _increaseLiquidityForPosition(p, desiredAmount0, desiredAmount1); + _increaseLiquidityForPosition( + p, + desiredAmount0, + desiredAmount1, + minAmount0, + minAmount1 + ); // Deposit all dust back to reserve strategies _depositAll(); @@ -561,14 +641,16 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { /** * @notice Removes all liquidity from active position and collects the fees + * @param minAmount0 Min amount of token0 to receive back + * @param minAmount1 Min amount of token1 to receive back */ - function closeActivePosition() + function closeActivePosition(uint256 minAmount0, uint256 minAmount1) external onlyGovernorOrStrategistOrOperator nonReentrant { require(currentPositionTokenId > 0, "No active position"); - _closePosition(currentPositionTokenId); + _closePosition(currentPositionTokenId, minAmount0, minAmount1); } /** @@ -576,13 +658,13 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @dev Must be a position minted by this contract * @param tokenId ERC721 token ID of the position to liquidate */ - function closePosition(uint256 tokenId) - external - onlyGovernorOrStrategistOrOperator - nonReentrant - { + function closePosition( + uint256 tokenId, + uint256 minAmount0, + uint256 minAmount1 + ) external onlyGovernorOrStrategistOrOperator nonReentrant { require(tokenIdToPosition[tokenId].exists, "Invalid position"); - _closePosition(tokenId); + _closePosition(tokenId, minAmount0, minAmount1); // Deposit all dust back to reserve strategies _depositAll(); @@ -591,13 +673,16 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { /** * @notice Closes the position denoted by the tokenId and and collects all fees * @param tokenId ERC721 token ID of the position to liquidate - * @param amount0 Amount of token0 received after removing liquidity - * @param amount1 Amount of token1 received after removing liquidity + * @param minAmount0 Min amount of token0 to receive back + * @param minAmount1 Min amount of token1 to receive back + * @return amount0 Amount of token0 received after removing liquidity + * @return amount1 Amount of token1 received after removing liquidity */ - function _closePosition(uint256 tokenId) - internal - returns (uint256 amount0, uint256 amount1) - { + function _closePosition( + uint256 tokenId, + uint256 minAmount0, + uint256 minAmount1 + ) internal returns (uint256 amount0, uint256 amount1) { Position storage p = tokenIdToPosition[tokenId]; if (p.liquidity == 0) { @@ -605,7 +690,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } // Remove all liquidity - (amount0, amount1) = _decreaseLiquidityForPosition(p, p.liquidity); + (amount0, amount1) = _decreaseLiquidityForPosition( + p, + p.liquidity, + minAmount0, + minAmount1 + ); // Collect all fees for position (uint256 amount0Fee, uint256 amount1Fee) = _collectFeesForToken( @@ -624,10 +714,14 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { /** * @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 * @param lowerTick Lower tick index * @param upperTick Upper tick index + * * @return tokenId ERC721 token ID of the position minted * @return liquidity Amount of liquidity added to the pool * @return amount0 Amount of token0 added to the position @@ -636,6 +730,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { function _mintPosition( uint256 desiredAmount0, uint256 desiredAmount1, + uint256 minAmount0, + uint256 minAmount1, int24 lowerTick, int24 upperTick ) @@ -647,8 +743,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 amount1 ) { - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager - .MintParams({ + INonfungiblePositionManager.MintParams + memory params = INonfungiblePositionManager.MintParams({ token0: token0, token1: token1, fee: poolFee, @@ -656,12 +752,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { tickUpper: upperTick, amount0Desired: desiredAmount0, amount1Desired: desiredAmount1, - amount0Min: maxSlippage == 0 - ? 0 - : (desiredAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, - amount1Min: maxSlippage == 0 - ? 0 - : (desiredAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, + amount0Min: minAmount0, + amount1Min: minAmount1, recipient: address(this), deadline: block.timestamp }); @@ -691,6 +783,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @param p Position object * @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 * @return liquidity Amount of liquidity added to the pool * @return amount0 Amount of token0 added to the position * @return amount1 Amount of token1 added to the position @@ -698,7 +792,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { function _increaseLiquidityForPosition( Position storage p, uint256 desiredAmount0, - uint256 desiredAmount1 + uint256 desiredAmount1, + uint256 minAmount0, + uint256 minAmount1 ) internal returns ( @@ -709,19 +805,16 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { { require(p.exists, "Unknown position"); - INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager - .IncreaseLiquidityParams({ - tokenId: p.tokenId, - amount0Desired: desiredAmount0, - amount1Desired: desiredAmount1, - amount0Min: maxSlippage == 0 - ? 0 - : (desiredAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, - amount1Min: maxSlippage == 0 - ? 0 - : (desiredAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, - deadline: block.timestamp - }); + INonfungiblePositionManager.IncreaseLiquidityParams + memory params = INonfungiblePositionManager + .IncreaseLiquidityParams({ + tokenId: p.tokenId, + amount0Desired: desiredAmount0, + amount1Desired: desiredAmount1, + amount0Min: minAmount0, + amount1Min: minAmount1, + deadline: block.timestamp + }); (liquidity, amount0, amount1) = positionManager.increaseLiquidity( params @@ -736,12 +829,16 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @notice Removes liquidity of the position in the pool * @param p Position object * @param liquidity Amount of liquidity to remove form the position + * @param minAmount0 Min amount of token0 to withdraw + * @param minAmount1 Min amount of token1 to withdraw * @return amount0 Amount of token0 received after liquidation * @return amount1 Amount of token1 received after liquidation */ function _decreaseLiquidityForPosition( Position storage p, - uint128 liquidity + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 ) internal returns (uint256 amount0, uint256 amount1) { require(p.exists, "Unknown position"); @@ -755,18 +852,15 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { liquidity ); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: p.tokenId, - liquidity: liquidity, - amount0Min: maxSlippage == 0 - ? 0 - : (exactAmount0 * (10000 - maxSlippage)) / 10000, // Price Slippage, - amount1Min: maxSlippage == 0 - ? 0 - : (exactAmount1 * (10000 - maxSlippage)) / 10000, // Price Slippage, - deadline: block.timestamp - }); + INonfungiblePositionManager.DecreaseLiquidityParams + memory params = INonfungiblePositionManager + .DecreaseLiquidityParams({ + tokenId: p.tokenId, + liquidity: liquidity, + amount0Min: minAmount0, + amount1Min: minAmount1, + deadline: block.timestamp + }); (amount0, amount1) = positionManager.decreaseLiquidity(params); @@ -847,4 +941,14 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // The pool tokens can never change. revert("Unsupported method"); } + + /// @inheritdoc InitializableAbstractStrategy + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + // Do nothing + } } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 92c34bdaa1..9675ef3169 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -194,17 +194,28 @@ contract VaultCore is VaultStorage { for (uint256 i = 0; i < allAssets.length; i++) { if (outputs[i] == 0) continue; - IERC20 asset = IERC20(allAssets[i]); + address assetAddr = allAssets[i]; + IERC20 asset = IERC20(assetAddr); if (asset.balanceOf(address(this)) >= outputs[i]) { // Use Vault funds first if sufficient asset.safeTransfer(msg.sender, outputs[i]); } else { - address strategyAddr = assetDefaultStrategies[allAssets[i]]; + address strategyAddr = assetDefaultStrategies[assetAddr]; + + // `strategies` is initialized in `VaultAdmin` + // slither-disable-next-line uninitialized-state + if (strategies[strategyAddr].isUniswapV3Strategy) { + // In case of Uniswap Strategy, withdraw from + // Reserve strategy directly + strategyAddr = IUniswapV3Strategy(strategyAddr) + .reserveStrategy(assetAddr); + } + if (strategyAddr != address(0)) { // Nothing in Vault, but something in Strategy, send from there IStrategy strategy = IStrategy(strategyAddr); - strategy.withdraw(msg.sender, allAssets[i], outputs[i]); + strategy.withdraw(msg.sender, assetAddr, outputs[i]); } else { // Cant find funds anywhere revert("Liquidity error"); diff --git a/contracts/deploy/000_mock.js b/contracts/deploy/000_mock.js index 303f15a656..486c14e006 100644 --- a/contracts/deploy/000_mock.js +++ b/contracts/deploy/000_mock.js @@ -358,7 +358,7 @@ async function deployMocksForUniswapV3Strategy(deploy, deployerAddr) { } deployMocks.id = "000_mock"; -deployMocks.tags = ["mocks"]; +deployMocks.tags = ["mocks", "unit_tests"]; deployMocks.skip = () => isMainnetOrFork; module.exports = deployMocks; diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 4e43f3f5b2..1e3bba5209 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -980,7 +980,7 @@ const deployUniswapV3Strategy = async () => { const vault = await ethers.getContract("VaultProxy"); const pool = await ethers.getContract("MockUniswapV3Pool"); const manager = await ethers.getContract("MockNonfungiblePositionManager"); - const v3Helper = await ethers.getContract("MockUniswapV3Helper"); + const v3Helper = await ethers.getContract("UniswapV3Helper"); const mockUSDT = await ethers.getContract("MockUSDT"); const mockUSDC = await ethers.getContract("MockUSDC"); @@ -1083,6 +1083,7 @@ const main = async () => { main.id = "001_core"; main.dependencies = ["mocks"]; +main.tags = ["core", "unit_tests"]; main.skip = () => isFork; module.exports = main; diff --git a/contracts/deploy/004_single_asset_staking.js b/contracts/deploy/004_single_asset_staking.js index 37849c2b34..858c15c26b 100644 --- a/contracts/deploy/004_single_asset_staking.js +++ b/contracts/deploy/004_single_asset_staking.js @@ -191,6 +191,7 @@ const singleAssetStaking = async ({ getNamedAccounts, deployments }) => { singleAssetStaking.id = deployName; singleAssetStaking.dependencies = ["core"]; +singleAssetStaking.tags = ["unit_tests"]; singleAssetStaking.skip = () => isFork; module.exports = singleAssetStaking; diff --git a/contracts/deploy/005_compensation_claims.js b/contracts/deploy/005_compensation_claims.js index 3840901e1c..2a7c2c6bbe 100644 --- a/contracts/deploy/005_compensation_claims.js +++ b/contracts/deploy/005_compensation_claims.js @@ -77,6 +77,7 @@ const compensationClaimsDeploy = async ({ getNamedAccounts }) => { compensationClaimsDeploy.id = deployName; compensationClaimsDeploy.dependencies = ["core"]; +compensationClaimsDeploy.tags = ["unit_tests"]; compensationClaimsDeploy.skip = () => isFork; module.exports = compensationClaimsDeploy; diff --git a/contracts/deploy/006_liquidity_reward.js b/contracts/deploy/006_liquidity_reward.js index c66c0c2ae7..19ff7d35fa 100644 --- a/contracts/deploy/006_liquidity_reward.js +++ b/contracts/deploy/006_liquidity_reward.js @@ -158,6 +158,7 @@ const liquidityReward = async ({ getNamedAccounts, deployments }) => { liquidityReward.id = deployName; liquidityReward.dependencies = ["core"]; +liquidityReward.tags = ["unit_tests"]; // Liquidity mining will get deployed to Mainnet at a later date. liquidityReward.skip = () => isMainnet || isFork; diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index e484a3ff3d..cc713982e0 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -29,8 +29,8 @@ const threepoolLPAbi = require("./abi/threepoolLP.json"); const threepoolSwapAbi = require("./abi/threepoolSwap.json"); async function defaultFixture() { - await deployments.fixture(undefined, { - keepExistingDeployments: Boolean(isForkWithLocalNode), + await deployments.fixture(isFork ? undefined : ["unit_tests"], { + keepExistingDeployments: true, // Boolean(isForkWithLocalNode), }); const { governorAddr, timelockAddr, operatorAddr } = await getNamedAccounts(); @@ -1223,37 +1223,39 @@ async function rebornFixture() { return fixture; } -async function uniswapV3Fixture() { - const fixture = await loadFixture(defaultFixture); - - const { - usdc, - usdt, - dai, - UniV3_USDC_USDT_Strategy, - mockStrategy, - mockStrategyDAI, - } = fixture; - - if (!isFork) { - // Approve mockStrategy - await _approveStrategy(fixture, mockStrategy); - await _approveStrategy(fixture, mockStrategyDAI); - - // Approve Uniswap V3 Strategy - await _approveStrategy(fixture, UniV3_USDC_USDT_Strategy, true); - } +function uniswapV3FixturSetup() { + return deployments.createFixture(async () => { + const fixture = await defaultFixture(); + + const { + usdc, + usdt, + dai, + UniV3_USDC_USDT_Strategy, + mockStrategy, + mockStrategyDAI, + } = fixture; + + if (!isFork) { + // Approve mockStrategy + await _approveStrategy(fixture, mockStrategy); + await _approveStrategy(fixture, mockStrategyDAI); + + // Approve Uniswap V3 Strategy + await _approveStrategy(fixture, UniV3_USDC_USDT_Strategy, true); + } - // Change default strategy to Uniswap V3 for both USDT and USDC - await _setDefaultStrategy(fixture, usdc, UniV3_USDC_USDT_Strategy); - await _setDefaultStrategy(fixture, usdt, UniV3_USDC_USDT_Strategy); + // Change default strategy to Uniswap V3 for both USDT and USDC + await _setDefaultStrategy(fixture, usdc, UniV3_USDC_USDT_Strategy); + await _setDefaultStrategy(fixture, usdt, UniV3_USDC_USDT_Strategy); - if (!isFork) { - // And a different one for DAI - await _setDefaultStrategy(fixture, dai, mockStrategyDAI); - } + if (!isFork) { + // And a different one for DAI + await _setDefaultStrategy(fixture, dai, mockStrategyDAI); + } - return fixture; + return fixture; + }); } async function _approveStrategy(fixture, strategy, isUniswapV3) { @@ -1303,7 +1305,7 @@ module.exports = { aaveVaultFixture, hackedVaultFixture, rebornFixture, - uniswapV3Fixture, + uniswapV3FixturSetup, withImpersonatedAccount, impersonateAndFundContract, impersonateAccount, diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 4af0166d0b..c3e1a0d977 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -1,64 +1,111 @@ -// const { expect } = require("chai"); -// const { uniswapV3Fixture } = require("../_fixture"); -// const { -// loadFixture, -// units, -// ousdUnits, -// expectApproxSupply, -// } = require("../helpers"); +const { expect } = require("chai"); +const { uniswapV3FixturSetup } = require("../_fixture"); +const { + loadFixture, + units, + ousdUnits, + expectApproxSupply, +} = require("../helpers"); + +const uniswapV3Fixture = uniswapV3FixturSetup(); describe("Uniswap V3 Strategy", function () { - // let fixture; - // let vault, harvester, ousd, usdc, usdt, dai; - // let reserveStrategy, strategy, mockPool, mockPositionManager; - // let governor, strategist, operator, josh, matt, daniel, domen, franck; - // beforeEach(async () => { - // fixture = await loadFixture(uniswapV3Fixture); - // reserveStrategy = fixture.mockStrategy; - // strategy = fixture.UniV3_USDC_USDT_Strategy; - // mockPool = fixture.UniV3_USDC_USDT_Pool; - // mockPositionManager = fixture.UniV3PositionManager; - // ousd = fixture.ousd; - // usdc = fixture.usdc; - // usdt = fixture.usdt; - // dai = fixture.dai; - // vault = fixture.vault; - // harvester = fixture.harvester; - // governor = fixture.governor; - // strategist = fixture.strategist; - // operator = fixture.operator; - // josh = fixture.josh; - // matt = fixture.matt; - // daniel = fixture.daniel; - // domen = fixture.domen; - // franck = fixture.franck; - // }); - // const mint = async (user, amount, asset) => { - // await asset.connect(user).mint(units(amount, asset)); - // await asset.connect(user).approve(vault.address, units(amount, asset)); - // await vault.connect(user).mint(asset.address, units(amount, asset), 0); - // }; - // describe("Mint", function () { - // it("Should deposit to reserve strategy", async () => { - // // Vault has 200 DAI from fixtures - // await expectApproxSupply(ousd, ousdUnits("200")); - // await expect(vault).has.an.approxBalanceOf("200", dai); - // // Mint some OUSD with USDC - // await mint(daniel, "10000", usdc); - // await expectApproxSupply(ousd, ousdUnits("10200")); - // console.log(await usdc.balanceOf(reserveStrategy.address)); - // // Make sure it went to reserve strategy - // await expect(reserveStrategy).has.an.approxBalanceOf("10000", usdc); - // }); - // }); - // describe("Redeem", function () { - // it("Should withdraw from reserve strategy", async () => {}); - // }); - // describe("Rewards", function () { - // it("Should show correct amount of fees", async () => {}); - // }); - // describe("Rebalance", function () { - // it("Should provide liquidity on given tick", async () => {}); - // it("Should close existing position", async () => {}); - // }); + let fixture; + let vault, harvester, ousd, usdc, usdt, dai; + let reserveStrategy, strategy, mockPool, mockPositionManager; + let governor, strategist, operator, josh, matt, daniel, domen, franck; + + beforeEach(async () => { + fixture = await uniswapV3Fixture(); + reserveStrategy = fixture.mockStrategy; + strategy = fixture.UniV3_USDC_USDT_Strategy; + mockPool = fixture.UniV3_USDC_USDT_Pool; + mockPositionManager = fixture.UniV3PositionManager; + ousd = fixture.ousd; + usdc = fixture.usdc; + usdt = fixture.usdt; + dai = fixture.dai; + vault = fixture.vault; + harvester = fixture.harvester; + governor = fixture.governor; + strategist = fixture.strategist; + operator = fixture.operator; + josh = fixture.josh; + matt = fixture.matt; + daniel = fixture.daniel; + domen = fixture.domen; + franck = fixture.franck; + }); + + const mint = async (user, amount, asset) => { + await asset.connect(user).mint(units(amount, asset)); + await asset.connect(user).approve(vault.address, units(amount, asset)); + await vault.connect(user).mint(asset.address, units(amount, asset), 0); + }; + + for (const assetSymbol of ["USDC", "USDT"]) { + describe(`Mint w/ ${assetSymbol}`, function () { + let asset; + beforeEach(() => { + asset = assetSymbol == "USDT" ? usdt : usdc; + }); + + it("Should mint w/o allocate", async () => { + // Vault has 200 DAI from fixtures + await expectApproxSupply(ousd, ousdUnits("200")); + await expect(vault).has.an.approxBalanceOf("200", dai); + // Mint some OUSD with USDC/USDT + await mint(daniel, "10000", asset); + await expectApproxSupply(ousd, ousdUnits("10200")); + // Make sure it's in vault + await expect(vault).has.an.approxBalanceOf("10000", asset); + }); + + it("Should mint and allocate to reserve strategy", async () => { + // Vault has 200 DAI from fixtures + await expectApproxSupply(ousd, ousdUnits("200")); + await expect(vault).has.an.approxBalanceOf("200", dai); + // Mint some OUSD with USDC/USDT + await mint(franck, "30000", asset); + await expectApproxSupply(ousd, ousdUnits("30200")); + // Make sure it went to reserve strategy + await expect(reserveStrategy).has.an.approxBalanceOf("30000", asset); + }); + }); + } + + describe("Redeem", function () { + it("Should withdraw from vault balance", async () => { + // Vault has 200 DAI from fixtures + await expectApproxSupply(ousd, ousdUnits("200")); + await expect(vault).has.an.approxBalanceOf("200", dai); + // Mint some OUSD with USDC + await mint(domen, "10000", usdc); + + // Try redeem + await vault.connect(domen).redeem(ousdUnits("10000"), 0); + await expectApproxSupply(ousd, ousdUnits("200")); + }); + + it("Should withdraw from reserve strategy", async () => { + // Vault has 200 DAI from fixtures + await expectApproxSupply(ousd, ousdUnits("200")); + await expect(vault).has.an.approxBalanceOf("200", dai); + // Mint some OUSD with USDT + await mint(matt, "30000", usdt); + await expectApproxSupply(ousd, ousdUnits("30200")); + await expect(reserveStrategy).has.an.approxBalanceOf("30000", usdt); + + // Try redeem + await vault.connect(matt).redeem(ousdUnits("30000"), 0); + await expectApproxSupply(ousd, ousdUnits("200")); + }); + }); + describe("Rewards", function () { + it("Should show correct amount of fees", async () => {}); + }); + describe("Rebalance", function () { + it("Should provide liquidity on given tick", async () => {}); + it("Should close existing position", async () => {}); + }); }); diff --git a/contracts/test/vault/index.js b/contracts/test/vault/index.js index c19701d07b..3581c79d76 100644 --- a/contracts/test/vault/index.js +++ b/contracts/test/vault/index.js @@ -1,7 +1,6 @@ const { defaultFixture } = require("../_fixture"); const chai = require("chai"); const hre = require("hardhat"); -const { solidity } = require("ethereum-waffle"); const { utils } = require("ethers"); const { @@ -16,8 +15,6 @@ const { isFork, } = require("../helpers"); -// Support BigNumber and all that with ethereum-waffle -chai.use(solidity); const expect = chai.expect; describe("Vault", function () { diff --git a/contracts/test/vault/vault.fork-test.js b/contracts/test/vault/vault.fork-test.js index 200153a346..7e9932ebeb 100644 --- a/contracts/test/vault/vault.fork-test.js +++ b/contracts/test/vault/vault.fork-test.js @@ -301,7 +301,7 @@ forkOnlyDescribe("ForkTest: Vault", function () { "0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAaveStrategy // TODO: Hard-code these after deploy //"0x7A192DD9Cc4Ea9bdEdeC9992df74F1DA55e60a19", // LUSD MetaStrategy - "0xa863A50233FB5Aa5aFb515e6C3e6FB9c075AA594" // USDC<>USDT Uniswap V3 Strategy + "0xa863A50233FB5Aa5aFb515e6C3e6FB9c075AA594", // USDC<>USDT Uniswap V3 Strategy ]; for (const s of strategies) { diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index 7722527174..0be336345d 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -3,7 +3,7 @@ // const hre = require("hardhat"); -const { utils, BigNumber } = require("ethers"); +const { utils } = require("ethers"); const { advanceTime, From bd8d3f9c6c60826399a18c2400af1b3c4fdc9786 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 9 Mar 2023 19:54:03 +0530 Subject: [PATCH 14/83] Few more tweaks --- contracts/contracts/interfaces/IVault.sol | 2 + .../GeneralizedUniswapV3Strategy.sol | 167 ++++++++++++------ contracts/contracts/vault/VaultAdmin.sol | 11 +- 3 files changed, 129 insertions(+), 51 deletions(-) diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index d4d3fd70e4..fa1d69ff2b 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -78,6 +78,8 @@ interface IVault { function removeStrategy(address _addr) external; + function isStrategySupported(address _addr) external view returns (bool); + function setAssetDefaultStrategy(address _asset, address _strategy) external; diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index 5816bf592c..bdecb56e37 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -16,9 +16,13 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { using SafeERC20 for IERC20; event OperatorChanged(address _address); - event ReserveStrategiesChanged( - address token0Strategy, - address token1Strategy + event ReserveStrategyChanged( + address asset, + address reserveStrategy + ); + event MinDepositThresholdChanged( + address asset, + uint256 minDepositThreshold ); event UniswapV3FeeCollected( uint256 indexed tokenId, @@ -56,10 +60,23 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint24 public poolFee; // Uniswap V3 Pool Fee uint24 public maxSlippage = 100; // 1%; Slippage tolerance when providing liquidity - // Address mapping of (Asset -> Strategy). When the funds are - // not deployed in Uniswap V3 Pool, they will be deposited - // to these reserve strategies - mapping(address => address) public reserveStrategy; + // Represents both tokens supported by the strategy + struct PoolToken { + bool isSupported; // True if asset is either token0 or token1 + + // When the funds are not deployed in Uniswap V3 Pool, they will + // be deposited to these reserve strategies + address reserveStrategy; + + // Deposits to reserve strategy when contract balance exceeds this amount + uint256 minDepositThreshold; + } + mapping(address => PoolToken) public poolTokens; + + // // Address mapping of (Asset -> Strategy). When the funds are + // // not deployed in Uniswap V3 Pool, they will be deposited + // // to these reserve strategies + // mapping(address => address) public reserveStrategy; // Uniswap V3's PositionManager INonfungiblePositionManager public positionManager; @@ -90,8 +107,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { IUniswapV3Helper internal uniswapV3Helper; // Future-proofing - uint256[50] private __gap; + uint256[100] private __gap; + /*************************************** + Modifiers + ****************************************/ + /** * @dev Ensures that the caller is Governor or Strategist. */ @@ -117,6 +138,18 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _; } + /** + * @dev Ensures that the asset address is either token0 or token1. + */ + modifier onlyPoolTokens(address addr) { + require(poolTokens[addr].isSupported, "Unsupported asset"); + _; + } + + /*************************************** + Initializer + ****************************************/ + /** * @dev Initialize the contract * @param _vaultAddress OUSD Vault @@ -158,69 +191,108 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _assets // Platform token addresses ); - _setReserveStrategy(_token0ReserveStrategy, _token1ReserveStrategy); + poolTokens[token0] = PoolToken({ + isSupported: true, + reserveStrategy: address(0), // Set below using `_setReserveStrategy()` + minDepositThreshold: 0 + }); + _setReserveStrategy(token0, _token0ReserveStrategy); + + poolTokens[token1] = PoolToken({ + isSupported: true, + reserveStrategy: address(0), // Set below using `_setReserveStrategy() + minDepositThreshold: 0 + }); + _setReserveStrategy(token1, _token1ReserveStrategy); + _setOperator(_operator); } /*************************************** Admin Utils ****************************************/ - + /** * @notice Change the address of the operator - * @dev Can only be called by the Governor + * @dev Can only be called by the Governor or Strategist * @param _operator The new value to be set */ - function setOperator(address _operator) external onlyGovernor { + function setOperator(address _operator) external onlyGovernorOrStrategist { _setOperator(_operator); } function _setOperator(address _operator) internal { - require(_operator != address(0), "Invalid operator address"); operatorAddr = _operator; emit OperatorChanged(_operator); } /** * @notice Change the reserve strategies of the supported assets - * @param _token0ReserveStrategy The new reserve strategy for token0 - * @param _token1ReserveStrategy The new reserve strategy for token1 + * @param _asset Asset to set the reserve strategy for + * @param _reserveStrategy The new reserve strategy for token */ function setReserveStrategy( - address _token0ReserveStrategy, - address _token1ReserveStrategy - ) external onlyGovernorOrStrategistOrOperator nonReentrant { - _setReserveStrategy(_token0ReserveStrategy, _token1ReserveStrategy); + address _asset, + address _reserveStrategy + ) external onlyGovernorOrStrategist nonReentrant { + _setReserveStrategy(_asset, _reserveStrategy); } /** - * @notice Change the reserve strategies of the supported assets - * @dev Will throw if the strategies don't support the assets - * @param _token0ReserveStrategy The new reserve strategy for token0 - * @param _token1ReserveStrategy The new reserve strategy for token1 + * @notice Change the reserve strategy of the supported asset + * @dev Will throw if the strategies don't support the assets or if + * strategy is unsupported by the vault + * @param _asset Asset to set the reserve strategy for + * @param _reserveStrategy The new reserve strategy for token */ function _setReserveStrategy( - address _token0ReserveStrategy, - address _token1ReserveStrategy - ) internal { + address _asset, + address _reserveStrategy + ) internal onlyPoolTokens(_asset) { require( - IStrategy(_token0ReserveStrategy).supportsAsset(token0), - "Invalid Reserve Strategy" + IVault(vaultAddress).isStrategySupported(_reserveStrategy), + "Unsupported strategy" ); + require( - IStrategy(_token1ReserveStrategy).supportsAsset(token1), - "Invalid Reserve Strategy" + IStrategy(_reserveStrategy).supportsAsset(_asset), + "Invalid strategy for asset" ); - reserveStrategy[token0] = _token0ReserveStrategy; - reserveStrategy[token1] = _token1ReserveStrategy; + PoolToken storage token = poolTokens[_asset]; + token.reserveStrategy = _reserveStrategy; - emit ReserveStrategiesChanged( - _token0ReserveStrategy, - _token1ReserveStrategy + emit ReserveStrategyChanged( + _asset, + _reserveStrategy ); } + /** + * @notice Get reserve strategy of the given asset + * @param _asset Address of the asset + * @return reserveStrategyAddr Reserve strategy address + */ + function reserveStrategy(address _asset) + external view onlyPoolTokens(_asset) + returns (address reserveStrategyAddr) + { + return poolTokens[_asset].reserveStrategy; + } + + /** + * @notice Change the minimum deposit threshold for the supported asset + * @param _asset Asset to set the threshold + * @param _minThreshold The new deposit threshold value + */ + function setMinDepositThreshold(address _asset, uint256 _minThreshold) + external onlyGovernorOrStrategist onlyPoolTokens(_asset) + { + PoolToken storage token = poolTokens[_asset]; + token.minDepositThreshold = _minThreshold; + emit MinDepositThresholdChanged(_asset, _minThreshold); + } + /*************************************** Deposit/Withdraw ****************************************/ @@ -230,9 +302,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { external override onlyVault + onlyPoolTokens(_asset) nonReentrant { - require(_asset == token0 || _asset == token1, "Unsupported asset"); IVault(vaultAddress).depositForUniswapV3(_asset, _amount); // Not emitting Deposit event since the Reserve strategy would do so } @@ -248,13 +320,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { function _depositAll() internal { uint256 token0Bal = IERC20(token0).balanceOf(address(this)); uint256 token1Bal = IERC20(token1).balanceOf(address(this)); - if (token0Bal > 0) { + if (token0Bal > 0 && token0Bal >= poolTokens[token0].minDepositThreshold) { IVault(vaultAddress).depositForUniswapV3(token0, token0Bal); } - if (token1Bal > 0) { + if (token1Bal > 0 && token1Bal >= poolTokens[token1].minDepositThreshold) { IVault(vaultAddress).depositForUniswapV3(token1, token1Bal); } - // Not emitting Deposit events since the Reserve strategies would do so } @@ -266,9 +337,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { address recipient, address _asset, uint256 amount - ) external override onlyVault nonReentrant { - require(_asset == token0 || _asset == token1, "Unsupported asset"); - + ) external override onlyVault onlyPoolTokens(_asset) nonReentrant { IERC20 asset = IERC20(_asset); uint256 selfBalance = asset.balanceOf(address(this)); @@ -301,7 +370,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * to Withdraw specified amount of the given asset. * * @param p Position object - * @param asset Token needed + * @param _asset Token needed * @param amount Minimum amount to liquidate * * @return liquidity Liquidity to burn @@ -310,11 +379,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { */ function _calculateLiquidityToWithdraw( Position memory p, - address asset, + address _asset, uint256 amount ) - internal - view + internal view onlyPoolTokens(_asset) returns ( uint128 liquidity, uint256 minAmount0, @@ -333,7 +401,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { p.liquidity ); - if (asset == token0) { + if (_asset == token0) { minAmount0 = amount; minAmount1 = totalAmount1 / (totalAmount0 / amount); liquidity = uniswapV3Helper.getLiquidityForAmounts( @@ -486,10 +554,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { external view override + onlyPoolTokens(_asset) returns (uint256 balance) { - require(_asset == token0 || _asset == token1, "Unsupported asset"); - balance = IERC20(_asset).balanceOf(address(this)); (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(platformAddress) @@ -527,7 +594,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * So, the result is smaller in size (int48 rather than bytes32 when using keccak256) * @param lowerTick Lower tick index * @param upperTick Upper tick index - * @param key A unique identifier to be used with ticksToTokenId + * @return key A unique identifier to be used with ticksToTokenId */ function _getTickPositionKey(int24 lowerTick, int24 upperTick) internal diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index 13fd01c957..3ed7bb402c 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -225,7 +225,6 @@ contract VaultAdmin is VaultStorage { * @dev Remove a strategy from the Vault. * @param _addr Address of the strategy to remove */ - function removeStrategy(address _addr) external onlyGovernor { require(strategies[_addr].isSupported, "Strategy not approved"); @@ -263,6 +262,16 @@ contract VaultAdmin is VaultStorage { } } + /** + * @notice Checks if the address is a supported strategy + * @param _addr Address of the strategy to check + * @return supported True, if strategy is recognized by the vault + */ + function isStrategySupported(address _addr) + external view returns (bool supported) { + supported = strategies[_addr].isSupported; + } + /** * @dev Move assets from one Strategy to another * @param _strategyFromAddress Address of Strategy to move assets from. From 2fb3fe5d74e2b6c4aa1cf26c04a024711317a62f Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Tue, 14 Mar 2023 19:01:06 +0530 Subject: [PATCH 15/83] checkpoint --- .../interfaces/IUniswapV3Strategy.sol | 2 + contracts/contracts/interfaces/IVault.sol | 3 +- .../mocks/uniswap/v2/MockUniswapRouter.sol | 24 ++ .../GeneralizedUniswapV3Strategy.sol | 313 +++++++++++++++--- contracts/contracts/vault/VaultAdmin.sol | 24 ++ contracts/contracts/vault/VaultStorage.sol | 1 + contracts/deploy/001_core.js | 4 +- .../deploy/049_uniswap_usdc_usdt_strategy.js | 1 + .../test/strategies/uniswap-v3.fork-test.js | 17 +- contracts/test/strategies/uniswap-v3.js | 1 - 10 files changed, 326 insertions(+), 64 deletions(-) diff --git a/contracts/contracts/interfaces/IUniswapV3Strategy.sol b/contracts/contracts/interfaces/IUniswapV3Strategy.sol index ff13315fb9..157322cd36 100644 --- a/contracts/contracts/interfaces/IUniswapV3Strategy.sol +++ b/contracts/contracts/interfaces/IUniswapV3Strategy.sol @@ -5,4 +5,6 @@ import { IStrategy } from "./IStrategy.sol"; interface IUniswapV3Strategy is IStrategy { function reserveStrategy(address token) external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); } diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index fa1d69ff2b..1968c8a657 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -177,8 +177,7 @@ interface IVault { function depositForUniswapV3(address asset, uint256 amount) external; - function withdrawForUniswapV3( - address recipient, + function withdrawAssetForUniswapV3( address asset, uint256 amount ) external; diff --git a/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol b/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol index 97a7cb7dcc..83e507832d 100644 --- a/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol +++ b/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol @@ -121,4 +121,28 @@ contract MockUniswapRouter is IUniswapV2Router { function WETH() external pure override returns (address) { return address(0); } + + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut) { + amountOut = params.amountIn.scaleBy( + Helpers.getDecimals(params.tokenIn), + Helpers.getDecimals(params.tokenOut) + ); + IERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + IERC20(params.tokenOut).transfer(params.recipient, amountOut); + require( + amountOut >= params.amountOutMinimum, + "UniswapMock: amountOut less than amountOutMinimum" + ); + } } diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index bdecb56e37..0471c7cf59 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -11,6 +11,7 @@ import { IVault } from "../interfaces/IVault.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import { INonfungiblePositionManager } from "../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; import { IUniswapV3Helper } from "../interfaces/uniswap/v3/IUniswapV3Helper.sol"; +import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { using SafeERC20 for IERC20; @@ -51,6 +52,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 amount0Received, uint256 amount1Received ); + event SwapsPauseStatusChanged(bool paused); + event MaxSwapSlippageChanged(uint24 maxSlippage); + event AssetSwappedForRebalancing(address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut); // The address that can manage the positions on Uniswap V3 address public operatorAddr; @@ -58,7 +62,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { address public token1; // Token1 of Uniswap V3 Pool uint24 public poolFee; // Uniswap V3 Pool Fee - uint24 public maxSlippage = 100; // 1%; Slippage tolerance when providing liquidity + uint24 public maxSwapSlippage = 100; // 1%; Reverts if swap slippage is higher than this + bool public swapsPaused = false; // True if Swaps are paused + + uint256 public maxTVL; // In USD, 18 decimals // Represents both tokens supported by the strategy struct PoolToken { @@ -70,14 +77,11 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // Deposits to reserve strategy when contract balance exceeds this amount uint256 minDepositThreshold; + + // uint256 minSwapPrice; // Min swap price for the token } mapping(address => PoolToken) public poolTokens; - // // Address mapping of (Asset -> Strategy). When the funds are - // // not deployed in Uniswap V3 Pool, they will be deposited - // // to these reserve strategies - // mapping(address => address) public reserveStrategy; - // Uniswap V3's PositionManager INonfungiblePositionManager public positionManager; @@ -106,6 +110,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch IUniswapV3Helper internal uniswapV3Helper; + ISwapRouter internal swapRouter; + // Future-proofing uint256[100] private __gap; @@ -159,6 +165,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @param _token1ReserveStrategy Reserve Strategy for token1 * @param _operator Address that can manage LP positions on the V3 pool * @param _uniswapV3Helper Deployed UniswapV3Helper contract + * @param _swapRouter Uniswap SwapRouter contract */ function initialize( address _vaultAddress, @@ -167,13 +174,15 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { address _token0ReserveStrategy, address _token1ReserveStrategy, address _operator, - address _uniswapV3Helper + address _uniswapV3Helper, + address _swapRouter ) external onlyGovernor initializer { positionManager = INonfungiblePositionManager( _nonfungiblePositionManager ); IUniswapV3Pool pool = IUniswapV3Pool(_poolAddress); uniswapV3Helper = IUniswapV3Helper(_uniswapV3Helper); + swapRouter = ISwapRouter(_swapRouter); token0 = pool.token0(); token1 = pool.token1(); @@ -259,8 +268,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { "Invalid strategy for asset" ); - PoolToken storage token = poolTokens[_asset]; - token.reserveStrategy = _reserveStrategy; + poolTokens[_asset].reserveStrategy = _reserveStrategy; emit ReserveStrategyChanged( _asset, @@ -293,6 +301,21 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { emit MinDepositThresholdChanged(_asset, _minThreshold); } + function setSwapsPaused(bool _paused) external onlyGovernorOrStrategist { + swapsPaused = _paused; + emit SwapsPauseStatusChanged(_paused); + } + + function setMaxSwapSlippage(uint24 _maxSlippage) external onlyGovernorOrStrategist { + maxSwapSlippage = _maxSlippage; + // emit SwapsPauseStatusChanged(_paused); + } + + // function setMinSwapPrice(address _asset, uint256 _price) external onlyGovernorOrStrategist { + // maxSwapSlippage = _maxSlippage; + // // emit SwapsPauseStatusChanged(_paused); + // } + /*************************************** Deposit/Withdraw ****************************************/ @@ -305,8 +328,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { onlyPoolTokens(_asset) nonReentrant { - IVault(vaultAddress).depositForUniswapV3(_asset, _amount); - // Not emitting Deposit event since the Reserve strategy would do so + if (_amount > poolTokens[_asset].minDepositThreshold) { + IVault(vaultAddress).depositForUniswapV3(_asset, _amount); + // Not emitting Deposit event since the Reserve strategy would do so + } } /// @inheritdoc InitializableAbstractStrategy @@ -454,37 +479,122 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } } + function _getToken1ForToken0(uint256 amount0) internal { + + } + /** * @dev Checks if there's enough balance left in the contract to provide liquidity. * If not, tries to pull it from reserve strategies - * @param minAmount0 Minimum amount of token0 needed - * @param minAmount1 Minimum amount of token1 needed + * @param desiredAmount0 Minimum amount of token0 needed + * @param desiredAmount1 Minimum amount of token1 needed */ - function _ensureAssetBalances(uint256 minAmount0, uint256 minAmount1) + function _ensureAssetBalances(uint256 desiredAmount0, uint256 desiredAmount1) internal { - IERC20 token0Contract = IERC20(token0); - IERC20 token1Contract = IERC20(token1); IVault vault = IVault(vaultAddress); // Withdraw enough funds from Reserve strategies - uint256 token0Balance = token0Contract.balanceOf(address(this)); - if (token0Balance < minAmount0) { - vault.withdrawForUniswapV3( - address(this), + uint256 token0Balance = IERC20(token0).balanceOf(address(this)); + if (token0Balance < desiredAmount0) { + vault.withdrawAssetForUniswapV3( token0, - minAmount0 - token0Balance + desiredAmount0 - token0Balance ); } - uint256 token1Balance = token1Contract.balanceOf(address(this)); - if (token1Balance < minAmount1) { - vault.withdrawForUniswapV3( - address(this), + uint256 token1Balance = IERC20(token1).balanceOf(address(this)); + if (token1Balance < desiredAmount1) { + vault.withdrawAssetForUniswapV3( token1, - minAmount1 - token1Balance + desiredAmount1 - token1Balance ); } + + // TODO: Check value of assets moved here + } + + function _ensureAssetsBySwapping( + uint256 desiredAmount0, + uint256 desiredAmount1, + uint256 swapAmountIn, + uint256 swapMinAmountOut, + uint160 sqrtPriceLimitX96, + bool swapZeroForOne + ) internal { + require(!swapsPaused, "Swaps are paused"); + IERC20 t0Contract = IERC20(token0); + IERC20 t1Contract = IERC20(token1); + + uint256 token0Balance = t0Contract.balanceOf(address(this)); + uint256 token1Balance = t1Contract.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 = IStrategy(poolTokens[token1].reserveStrategy).checkBalance(token1); + + // Only swap when asset isn't available in reserve as well + require(token1Needed > 0 && 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; + + // Approve for swaps + t0Contract.safeApprove(address(swapRouter), swapAmountIn); + } else { + // Amount available in reserve strategies + uint256 t0ReserveBal = IStrategy(poolTokens[token0].reserveStrategy).checkBalance(token0); + + // Only swap when asset isn't available in reserve as well + require(token0Needed > 0 && 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; + + // Approve for swaps + t1Contract.safeApprove(address(swapRouter), swapAmountIn); + } + + // TODO: Check value of token0Needed and token1Needed + + // Fund strategy from reserve strategies + if (token0Needed > 0) { + IVault(vaultAddress).withdrawAssetForUniswapV3(token0, token0Needed); + } + + if (token1Needed > 0) { + IVault(vaultAddress).withdrawAssetForUniswapV3(token0, token0Needed); + } + + // TODO: Slippage/price check + + // 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 + ); + + // TODO: Check value of assets moved here } /*************************************** @@ -610,25 +720,99 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @notice Closes active LP position if any and then provides liquidity to the requested position. * Mints new position, if it doesn't exist already. * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them - * @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 - * @param minRedeemAmount0 Min amount of token0 received from closing active position - * @param minRedeemAmount1 Min amount of token1 received from closing active position + * @param desiredAmounts Amounts of token0 and token1 to use to provide liquidity + * @param minAmounts Min amounts of token0 and token1 to deposit/expect + * @param minRedeemAmounts Min amount of token0 and token1 received from closing active position + * @param lowerTick Desired lower tick index + * @param upperTick Desired upper tick index + */ + function _rebalance( + uint256[2] calldata desiredAmounts, + uint256[2] calldata minAmounts, + uint256[2] calldata minRedeemAmounts, + int24 lowerTick, + int24 upperTick + ) internal { + require(lowerTick < upperTick, "Invalid tick range"); + + int48 tickKey = _getTickPositionKey(lowerTick, upperTick); + uint256 tokenId = ticksToTokenId[tickKey]; + + if (currentPositionTokenId > 0) { + // Close any active position + _closePosition( + currentPositionTokenId, + minRedeemAmounts[0], + minRedeemAmounts[1] + ); + } + + // Withdraw enough funds from Reserve strategies + _ensureAssetBalances(desiredAmounts[0], desiredAmounts[1]); + + // Provide liquidity + if (tokenId > 0) { + // Add liquidity to the position token + Position storage p = tokenIdToPosition[tokenId]; + _increaseLiquidityForPosition( + p, + desiredAmounts[0], + desiredAmounts[1], + minAmounts[0], + minAmounts[1] + ); + } else { + // Mint new position + (tokenId, , , ) = _mintPosition( + desiredAmounts[0], + desiredAmounts[1], + minAmounts[0], + minAmounts[1], + lowerTick, + upperTick + ); + } + + // Mark it as active position + currentPositionTokenId = tokenId; + + // Move any leftovers to Reserve + _depositAll(); + } + + /** + * @notice Closes active LP position if any and then provides liquidity to the requested position. + * Mints new position, if it doesn't exist already. + * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them + * @param desiredAmounts Amounts of token0 and token1 to use to provide liquidity + * @param minAmounts Min amounts of token0 and token1 to deposit/expect + * @param minRedeemAmounts Min amount of token0 and token1 received from closing active position * @param lowerTick Desired lower tick index * @param upperTick Desired upper tick index */ function rebalance( - uint256 desiredAmount0, - uint256 desiredAmount1, - uint256 minAmount0, - uint256 minAmount1, - uint256 minRedeemAmount0, - uint256 minRedeemAmount1, + uint256[2] calldata desiredAmounts, + uint256[2] calldata minAmounts, + uint256[2] calldata minRedeemAmounts, int24 lowerTick, int24 upperTick ) external onlyGovernorOrStrategistOrOperator nonReentrant { + _rebalance(desiredAmounts, minAmounts, minRedeemAmounts, lowerTick, upperTick); + } + + function _swapAndRebalance( + uint256[2] calldata desiredAmounts, + uint256[2] calldata minAmounts, + uint256[2] calldata minRedeemAmounts, + int24 lowerTick, + int24 upperTick, + uint256 swapAmountIn, + uint256 swapMinAmountOut, + uint160 sqrtPriceLimitX96, + bool swapZeroForOne + ) internal { + require(lowerTick < upperTick, "Invalid tick range"); + int48 tickKey = _getTickPositionKey(lowerTick, upperTick); uint256 tokenId = ticksToTokenId[tickKey]; @@ -636,13 +820,13 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // Close any active position _closePosition( currentPositionTokenId, - minRedeemAmount0, - minRedeemAmount1 + minRedeemAmounts[0], + minRedeemAmounts[1] ); } - // Withdraw enough funds from Reserve strategies - _ensureAssetBalances(desiredAmount0, desiredAmount1); + // Withdraw enough funds from Reserve strategies and swap to desired amounts + _ensureAssetsBySwapping(desiredAmounts[0], desiredAmounts[1], swapAmountIn, swapMinAmountOut, sqrtPriceLimitX96, swapZeroForOne); // Provide liquidity if (tokenId > 0) { @@ -650,18 +834,18 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { Position storage p = tokenIdToPosition[tokenId]; _increaseLiquidityForPosition( p, - desiredAmount0, - desiredAmount1, - minAmount0, - minAmount1 + desiredAmounts[0], + desiredAmounts[1], + minAmounts[0], + minAmounts[1] ); } else { // Mint new position (tokenId, , , ) = _mintPosition( - desiredAmount0, - desiredAmount1, - minAmount0, - minAmount1, + desiredAmounts[0], + desiredAmounts[1], + minAmounts[0], + minAmounts[1], lowerTick, upperTick ); @@ -674,6 +858,20 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _depositAll(); } + function swapAndRebalance( + uint256[2] calldata desiredAmounts, + uint256[2] calldata minAmounts, + uint256[2] calldata minRedeemAmounts, + int24 lowerTick, + int24 upperTick, + uint256 swapAmountIn, + uint256 swapMinAmountOut, + uint160 sqrtPriceLimitX96, + bool swapZeroForOne + ) external onlyGovernorOrStrategistOrOperator nonReentrant { + _swapAndRebalance(desiredAmounts, minAmounts, minRedeemAmounts, lowerTick, upperTick, swapAmountIn, swapMinAmountOut, sqrtPriceLimitX96, swapZeroForOne); + } + /** * @notice Increases liquidity of the active position. * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them @@ -690,8 +888,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ) external onlyGovernorOrStrategistOrOperator nonReentrant { require(currentPositionTokenId > 0, "No active position"); - // Withdraw enough funds from Reserve strategies - _ensureAssetBalances(desiredAmount0, desiredAmount1); + // // Withdraw enough funds from Reserve strategies + // _ensureAssetBalances(desiredAmount0, desiredAmount1); Position storage p = tokenIdToPosition[currentPositionTokenId]; _increaseLiquidityForPosition( @@ -716,8 +914,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { onlyGovernorOrStrategistOrOperator nonReentrant { - require(currentPositionTokenId > 0, "No active position"); _closePosition(currentPositionTokenId, minAmount0, minAmount1); + + // Deposit all dust back to reserve strategies + _depositAll(); } /** @@ -730,7 +930,6 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 minAmount0, uint256 minAmount1 ) external onlyGovernorOrStrategistOrOperator nonReentrant { - require(tokenIdToPosition[tokenId].exists, "Invalid position"); _closePosition(tokenId, minAmount0, minAmount1); // Deposit all dust back to reserve strategies @@ -751,6 +950,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 minAmount1 ) internal returns (uint256 amount0, uint256 amount1) { Position storage p = tokenIdToPosition[tokenId]; + require(p.exists, "Invalid position"); if (p.liquidity == 0) { return (0, 0); @@ -810,6 +1010,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 amount1 ) { + int48 tickKey = _getTickPositionKey(lowerTick, upperTick); + require(ticksToTokenId[tickKey] != 0, "Duplicate position mint"); + INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ token0: token0, @@ -827,7 +1030,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { (tokenId, liquidity, amount0, amount1) = positionManager.mint(params); - ticksToTokenId[_getTickPositionKey(lowerTick, upperTick)] = tokenId; + ticksToTokenId[tickKey] = tokenId; tokenIdToPosition[tokenId] = Position({ exists: true, tokenId: tokenId, diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index 3ed7bb402c..ab0ef3eee1 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -615,4 +615,28 @@ contract VaultAdmin is VaultStorage { .reserveStrategy(asset); IStrategy(reserveStrategy).withdraw(recipient, asset, amount); } + + /** + * @notice Moves tokens from reserve strategy to the Uniswap V3 Strategy + * @dev Only callable by whitelisted Uniswap V3 strategies + * @param asset Address of the token + * @param amount Amount of token1 required + */ + function withdrawAssetForUniswapV3( + address asset, + uint256 amount + ) external onlyUniswapV3Strategies nonReentrant { + IUniswapV3Strategy v3Strategy = IUniswapV3Strategy(msg.sender); + require(amount > 0, "Invalid amount specified"); + + address reserveStrategyAddr = IUniswapV3Strategy(v3Strategy) + .reserveStrategy(asset); + require(strategies[reserveStrategyAddr].isSupported, "Unknown reserve strategy"); + + IStrategy reserveStrategy = IStrategy(reserveStrategyAddr); + require(reserveStrategy.supportsAsset(asset), "Unsupported asset"); + reserveStrategy.withdraw(msg.sender, asset, amount); + + emit AssetTransferredToUniswapV3Strategy(msg.sender, asset, amount); + } } diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 6c2e24df34..63fb386385 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -49,6 +49,7 @@ contract VaultStorage is Initializable, Governable { event TrusteeFeeBpsChanged(uint256 _basis); event TrusteeAddressChanged(address _address); event NetOusdMintForStrategyThresholdChanged(uint256 _threshold); + event AssetTransferredToUniswapV3Strategy(address indexed strategy, address indexed asset, uint256 amount); // Assets supported by the Vault, i.e. Stablecoins struct Asset { diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 1e3bba5209..75f717d7ec 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -985,6 +985,7 @@ const deployUniswapV3Strategy = async () => { const mockUSDT = await ethers.getContract("MockUSDT"); const mockUSDC = await ethers.getContract("MockUSDC"); const mockDAI = await ethers.getContract("MockDAI"); + const mockRouter = await ethers.getContract("MockUniswapRouter"); await deployWithConfirmation("MockStrategy", [ vault.address, @@ -1030,7 +1031,8 @@ const deployUniswapV3Strategy = async () => { mockStrat.address, mockStrat.address, operatorAddr, - v3Helper.address + v3Helper.address, + mockRouter.address ) ); log("Initialized UniV3_USDC_USDT_Strategy"); diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index 3be7af6012..748f03a88d 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -87,6 +87,7 @@ module.exports = deploymentWithGovernanceProposal( cMorphoCompProxy.address, // Reserve strategy for USDT operatorAddr, dUniswapV3Helper.address, + assetAddresses.UniV3SwapRouter, await getTxOpts() ) ); diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 8504b126f3..f238936ffc 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -1,8 +1,7 @@ const { expect } = require("chai"); -const { uniswapV3Fixture } = require("../_fixture"); +const { uniswapV3FixturSetup } = require("../_fixture"); const { forkOnlyDescribe, - loadFixture, units, ousdUnits, usdcUnitsFormat, @@ -13,6 +12,8 @@ const { } = require("../helpers"); const { BigNumber } = require("ethers"); +const uniswapV3Fixture = uniswapV3FixturSetup(); + forkOnlyDescribe("Uniswap V3 Strategy", function () { this.timeout(0); @@ -30,7 +31,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { franck; beforeEach(async () => { - fixture = await loadFixture(uniswapV3Fixture); + fixture = await uniswapV3Fixture(); reserveStrategy = fixture.morphoCompoundStrategy; strategy = fixture.UniV3_USDC_USDT_Strategy; pool = fixture.UniV3_USDC_USDT_Pool; @@ -103,7 +104,13 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const tx = await strategy .connect(operator) - .rebalance(maxUSDC, maxUSDT, lowerTick, upperTick); + .rebalance( + [maxUSDC, maxUSDT], + [maxUSDC.mul(9900).div(10000), maxUSDT.mul(9900).div(10000)], + [0, 0], + lowerTick, + upperTick + ); const { events } = await tx.wait(); @@ -119,7 +126,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { }; }; - it("Should mint position", async () => { + it.only("Should mint position", async () => { const usdcBalBefore = await strategy.checkBalance(usdc.address); const usdtBalBefore = await strategy.checkBalance(usdt.address); diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index c3e1a0d977..68d2c77206 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -1,7 +1,6 @@ const { expect } = require("chai"); const { uniswapV3FixturSetup } = require("../_fixture"); const { - loadFixture, units, ousdUnits, expectApproxSupply, From 8b55689c370856e35970e89c4f84de2d12affaf7 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 15 Mar 2023 00:00:39 +0530 Subject: [PATCH 16/83] Start with splitting code --- .../GeneralizedUniswapV3Strategy.sol | 459 +++++++----------- .../contracts/utils/UniswapV3StrategyLib.sol | 236 +++++++++ contracts/deploy/001_core.js | 9 +- .../deploy/025_resolution_upgrade_end.js | 1 + contracts/deploy/032_convex_rewards.js | 1 + .../deploy/036_multiple_rewards_strategies.js | 5 + contracts/deploy/042_ogv_buyback.js | 1 + contracts/deploy/046_value_value_checker.js | 1 + .../deploy/049_uniswap_usdc_usdt_strategy.js | 14 +- contracts/tasks/storageSlots.js | 22 +- contracts/test/_fixture.js | 10 +- .../test/strategies/uniswap-v3.fork-test.js | 6 +- contracts/utils/deploy.js | 5 +- 13 files changed, 463 insertions(+), 307 deletions(-) create mode 100644 contracts/contracts/utils/UniswapV3StrategyLib.sol diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index 0471c7cf59..12fc96c4a5 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -13,6 +13,8 @@ import { INonfungiblePositionManager } from "../interfaces/uniswap/v3/INonfungib import { IUniswapV3Helper } from "../interfaces/uniswap/v3/IUniswapV3Helper.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import { UniswapV3StrategyLib } from "../utils/UniswapV3StrategyLib.sol"; + contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { using SafeERC20 for IERC20; @@ -25,23 +27,23 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { address asset, uint256 minDepositThreshold ); - event UniswapV3FeeCollected( - uint256 indexed tokenId, - uint256 amount0, - uint256 amount1 - ); - event UniswapV3LiquidityAdded( - uint256 indexed tokenId, - uint256 amount0Sent, - uint256 amount1Sent, - uint128 liquidityMinted - ); - event UniswapV3LiquidityRemoved( - uint256 indexed tokenId, - uint256 amount0Received, - uint256 amount1Received, - uint128 liquidityBurned - ); + // event UniswapV3FeeCollected( + // uint256 indexed tokenId, + // uint256 amount0, + // uint256 amount1 + // ); + // 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, @@ -80,30 +82,46 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // uint256 minSwapPrice; // Min swap price for the token } + + struct SwapAndRebalanceParams { + uint256 desiredAmount0; + uint256 desiredAmount1; + uint256 minAmount0; + uint256 minAmount1; + uint256 minRedeemAmount0; + uint256 minRedeemAmount1; + int24 lowerTick; + int24 upperTick; + uint256 swapAmountIn; + uint256 swapMinAmountOut; + uint160 sqrtPriceLimitX96; + bool swapZeroForOne; + } + mapping(address => PoolToken) public poolTokens; // Uniswap V3's PositionManager INonfungiblePositionManager public positionManager; - // Represents a position minted by this contract - struct Position { - bytes32 positionKey; // Required to read collectible fees from the V3 Pool - uint256 tokenId; // ERC721 token Id of the minted position - uint128 liquidity; // Amount of liquidity deployed - int24 lowerTick; // Lower tick index - int24 upperTick; // Upper tick index - bool exists; // True, if position is minted - // 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; - } + // // Represents a position minted by this contract + // struct Position { + // bytes32 positionKey; // Required to read collectible fees from the V3 Pool + // uint256 tokenId; // ERC721 token Id of the minted position + // uint128 liquidity; // Amount of liquidity deployed + // int24 lowerTick; // Lower tick index + // int24 upperTick; // Upper tick index + // bool exists; // True, if position is minted + // // 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; + // } // A lookup table to find token IDs of position using f(lowerTick, upperTick) mapping(int48 => uint256) internal ticksToTokenId; // Maps tokenIDs to their Position object - mapping(uint256 => Position) public tokenIdToPosition; + mapping(uint256 => UniswapV3StrategyLib.Position) public tokenIdToPosition; // Token ID of the position that's being used to provide LP at the time uint256 public currentPositionTokenId; @@ -257,16 +275,16 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { function _setReserveStrategy( address _asset, address _reserveStrategy - ) internal onlyPoolTokens(_asset) { - require( - IVault(vaultAddress).isStrategySupported(_reserveStrategy), - "Unsupported strategy" - ); + ) internal { + // require( + // IVault(vaultAddress).isStrategySupported(_reserveStrategy), + // "Unsupported strategy" + // ); - require( - IStrategy(_reserveStrategy).supportsAsset(_asset), - "Invalid strategy for asset" - ); + // require( + // IStrategy(_reserveStrategy).supportsAsset(_asset), + // "Invalid strategy for asset" + // ); poolTokens[_asset].reserveStrategy = _reserveStrategy; @@ -354,8 +372,36 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // Not emitting Deposit events since the Reserve strategies would do so } + function _withdrawAssetFromCurrentPosition(address _asset, uint256 amount) internal { + UniswapV3StrategyLib.Position storage p = tokenIdToPosition[currentPositionTokenId]; + require(p.exists && p.liquidity > 0, "Liquidity error"); + + // Figure out liquidity to burn + ( + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) = UniswapV3StrategyLib.calculateLiquidityToWithdraw( + platformAddress, + address(uniswapV3Helper), + p, + _asset, + amount + ); + + // Liquidiate active position + UniswapV3StrategyLib.decreaseLiquidityForPosition( + platformAddress, + address(positionManager), + address(uniswapV3Helper), + p, + liquidity, + minAmount0, + minAmount1 + ); + } + /** - * @notice Withdraws asset from the reserve strategy * @inheritdoc InitializableAbstractStrategy */ function withdraw( @@ -367,22 +413,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 selfBalance = asset.balanceOf(address(this)); if (selfBalance < amount) { - Position storage p = tokenIdToPosition[currentPositionTokenId]; - require(p.exists && p.liquidity > 0, "Liquidity error"); - - // Figure out liquidity to burn - ( - uint128 liquidity, - uint256 minAmount0, - uint256 minAmount1 - ) = _calculateLiquidityToWithdraw( - p, - _asset, - (amount - selfBalance) - ); - - // Liquidiate active position - _decreaseLiquidityForPosition(p, liquidity, minAmount0, minAmount1); + _withdrawAssetFromCurrentPosition(_asset, amount - selfBalance); } // Transfer requested amount @@ -390,65 +421,6 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { emit Withdrawal(_asset, _asset, amount); } - /** - * @notice Calculates the amount liquidity that needs to be removed - * to Withdraw specified amount of the given asset. - * - * @param p Position object - * @param _asset Token needed - * @param amount Minimum amount to liquidate - * - * @return liquidity Liquidity to burn - * @return minAmount0 Minimum amount0 to expect - * @return minAmount1 Minimum amount1 to expect - */ - function _calculateLiquidityToWithdraw( - Position memory p, - address _asset, - uint256 amount - ) - internal view onlyPoolTokens(_asset) - returns ( - uint128 liquidity, - uint256 minAmount0, - uint256 minAmount1 - ) - { - (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(platformAddress) - .slot0(); - - // Total amount in Liquidity pools - (uint256 totalAmount0, uint256 totalAmount1) = uniswapV3Helper - .getAmountsForLiquidity( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - p.liquidity - ); - - if (_asset == token0) { - minAmount0 = amount; - minAmount1 = totalAmount1 / (totalAmount0 / amount); - liquidity = uniswapV3Helper.getLiquidityForAmounts( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - amount, - minAmount1 - ); - } else { - minAmount0 = totalAmount0 / (totalAmount1 / amount); - minAmount1 = amount; - liquidity = uniswapV3Helper.getLiquidityForAmounts( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - minAmount0, - amount - ); - } - } - /** * @notice Closes active LP position, if any, and transfer all token balance to Vault * @inheritdoc InitializableAbstractStrategy @@ -611,7 +583,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { nonReentrant { if (currentPositionTokenId > 0) { - _collectFeesForToken(currentPositionTokenId); + UniswapV3StrategyLib.collectFeesForToken(address(positionManager), currentPositionTokenId); } } @@ -632,29 +604,6 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ); } - /** - * @notice Collects the fees generated by the position on V3 pool - * @param tokenId Token ID of the position to collect fees of. - * @return amount0 Amount of token0 collected as fee - * @return amount1 Amount of token1 collected as fee - */ - function _collectFeesForToken(uint256 tokenId) - internal - returns (uint256 amount0, uint256 amount1) - { - INonfungiblePositionManager.CollectParams - memory params = INonfungiblePositionManager.CollectParams({ - tokenId: tokenId, - recipient: address(this), - amount0Max: type(uint128).max, - amount1Max: type(uint128).max - }); - - (amount0, amount1) = positionManager.collect(params); - - emit UniswapV3FeeCollected(tokenId, amount0, amount1); - } - /** * @dev Only checks the active LP position. * Doesn't return the balance held in the reserve strategies. @@ -753,8 +702,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // Provide liquidity if (tokenId > 0) { // Add liquidity to the position token - Position storage p = tokenIdToPosition[tokenId]; - _increaseLiquidityForPosition( + UniswapV3StrategyLib.Position storage p = tokenIdToPosition[tokenId]; + UniswapV3StrategyLib.increaseLiquidityForPosition( + address(positionManager), p, desiredAmounts[0], desiredAmounts[1], @@ -800,54 +750,62 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _rebalance(desiredAmounts, minAmounts, minRedeemAmounts, lowerTick, upperTick); } - function _swapAndRebalance( - uint256[2] calldata desiredAmounts, - uint256[2] calldata minAmounts, - uint256[2] calldata minRedeemAmounts, - int24 lowerTick, - int24 upperTick, - uint256 swapAmountIn, - uint256 swapMinAmountOut, - uint160 sqrtPriceLimitX96, - bool swapZeroForOne - ) internal { - require(lowerTick < upperTick, "Invalid tick range"); + function swapAndRebalance( + SwapAndRebalanceParams calldata params + // uint256[2] calldata desiredAmounts, + // uint256[2] calldata minAmounts, + // uint256[2] calldata minRedeemAmounts, + // int24 lowerTick, + // int24 upperTick, + // uint256 swapAmountIn, + // uint256 swapMinAmountOut, + // uint160 sqrtPriceLimitX96, + // bool swapZeroForOne + ) external onlyGovernorOrStrategistOrOperator nonReentrant { + require(params.lowerTick < params.upperTick, "Invalid tick range"); - int48 tickKey = _getTickPositionKey(lowerTick, upperTick); - uint256 tokenId = ticksToTokenId[tickKey]; + uint256 tokenId = ticksToTokenId[_getTickPositionKey(params.lowerTick, params.upperTick)]; if (currentPositionTokenId > 0) { // Close any active position _closePosition( currentPositionTokenId, - minRedeemAmounts[0], - minRedeemAmounts[1] + params.minRedeemAmount0, + params.minRedeemAmount1 ); } // Withdraw enough funds from Reserve strategies and swap to desired amounts - _ensureAssetsBySwapping(desiredAmounts[0], desiredAmounts[1], swapAmountIn, swapMinAmountOut, sqrtPriceLimitX96, swapZeroForOne); + _ensureAssetsBySwapping( + params.desiredAmount0, + params.desiredAmount1, + params.swapAmountIn, + params.swapMinAmountOut, + params.sqrtPriceLimitX96, + params.swapZeroForOne + ); // Provide liquidity if (tokenId > 0) { // Add liquidity to the position token - Position storage p = tokenIdToPosition[tokenId]; - _increaseLiquidityForPosition( + UniswapV3StrategyLib.Position storage p = tokenIdToPosition[tokenId]; + UniswapV3StrategyLib.increaseLiquidityForPosition( + address(positionManager), p, - desiredAmounts[0], - desiredAmounts[1], - minAmounts[0], - minAmounts[1] + params.desiredAmount0, + params.desiredAmount1, + params.minAmount0, + params.minAmount1 ); } else { // Mint new position (tokenId, , , ) = _mintPosition( - desiredAmounts[0], - desiredAmounts[1], - minAmounts[0], - minAmounts[1], - lowerTick, - upperTick + params.desiredAmount0, + params.desiredAmount1, + params.minAmount0, + params.minAmount1, + params.lowerTick, + params.upperTick ); } @@ -858,19 +816,19 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { _depositAll(); } - function swapAndRebalance( - uint256[2] calldata desiredAmounts, - uint256[2] calldata minAmounts, - uint256[2] calldata minRedeemAmounts, - int24 lowerTick, - int24 upperTick, - uint256 swapAmountIn, - uint256 swapMinAmountOut, - uint160 sqrtPriceLimitX96, - bool swapZeroForOne - ) external onlyGovernorOrStrategistOrOperator nonReentrant { - _swapAndRebalance(desiredAmounts, minAmounts, minRedeemAmounts, lowerTick, upperTick, swapAmountIn, swapMinAmountOut, sqrtPriceLimitX96, swapZeroForOne); - } + // function swapAndRebalance( + // uint256[2] calldata desiredAmounts, + // uint256[2] calldata minAmounts, + // uint256[2] calldata minRedeemAmounts, + // int24 lowerTick, + // int24 upperTick, + // uint256 swapAmountIn, + // uint256 swapMinAmountOut, + // uint160 sqrtPriceLimitX96, + // bool swapZeroForOne + // ) external onlyGovernorOrStrategistOrOperator nonReentrant { + // _swapAndRebalance(desiredAmounts, minAmounts, minRedeemAmounts, lowerTick, upperTick, swapAmountIn, swapMinAmountOut, sqrtPriceLimitX96, swapZeroForOne); + // } /** * @notice Increases liquidity of the active position. @@ -888,12 +846,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ) external onlyGovernorOrStrategistOrOperator nonReentrant { require(currentPositionTokenId > 0, "No active position"); - // // Withdraw enough funds from Reserve strategies - // _ensureAssetBalances(desiredAmount0, desiredAmount1); + // Withdraw enough funds from Reserve strategies + _ensureAssetBalances(desiredAmount0, desiredAmount1); - Position storage p = tokenIdToPosition[currentPositionTokenId]; - _increaseLiquidityForPosition( - p, + UniswapV3StrategyLib.increaseLiquidityForPosition( + address(positionManager), + tokenIdToPosition[currentPositionTokenId], desiredAmount0, desiredAmount1, minAmount0, @@ -949,7 +907,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 minAmount0, uint256 minAmount1 ) internal returns (uint256 amount0, uint256 amount1) { - Position storage p = tokenIdToPosition[tokenId]; + UniswapV3StrategyLib.Position storage p = tokenIdToPosition[tokenId]; require(p.exists, "Invalid position"); if (p.liquidity == 0) { @@ -957,26 +915,30 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } // Remove all liquidity - (amount0, amount1) = _decreaseLiquidityForPosition( + (amount0, amount1) = UniswapV3StrategyLib.decreaseLiquidityForPosition( + platformAddress, + address(positionManager), + address(uniswapV3Helper), p, p.liquidity, minAmount0, minAmount1 ); - // Collect all fees for position - (uint256 amount0Fee, uint256 amount1Fee) = _collectFeesForToken( - tokenId - ); - - amount0 = amount0 + amount0Fee; - amount1 = amount1 + amount1Fee; - if (tokenId == currentPositionTokenId) { currentPositionTokenId = 0; } emit UniswapV3PositionClosed(tokenId, amount0, amount1); + + // Collect all fees for position + (uint256 amount0Fee, uint256 amount1Fee) = UniswapV3StrategyLib.collectFeesForToken( + address(positionManager), + p.tokenId + ); + + amount0 = amount0 + amount0Fee; + amount1 = amount1 + amount1Fee; } /** @@ -1011,7 +973,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ) { int48 tickKey = _getTickPositionKey(lowerTick, upperTick); - require(ticksToTokenId[tickKey] != 0, "Duplicate position mint"); + require(ticksToTokenId[tickKey] == 0, "Duplicate position mint"); INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ @@ -1031,7 +993,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { (tokenId, liquidity, amount0, amount1) = positionManager.mint(params); ticksToTokenId[tickKey] = tokenId; - tokenIdToPosition[tokenId] = Position({ + tokenIdToPosition[tokenId] = UniswapV3StrategyLib.Position({ exists: true, tokenId: tokenId, liquidity: liquidity, @@ -1045,98 +1007,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { }); emit UniswapV3PositionMinted(tokenId, lowerTick, upperTick); - emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); - } - - /** - * @notice Increases liquidity of the position in the pool - * @param p Position object - * @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 - * @return liquidity Amount of liquidity added to the pool - * @return amount0 Amount of token0 added to the position - * @return amount1 Amount of token1 added to the position - */ - function _increaseLiquidityForPosition( - Position storage p, - uint256 desiredAmount0, - uint256 desiredAmount1, - uint256 minAmount0, - uint256 minAmount1 - ) - internal - returns ( - uint128 liquidity, - uint256 amount0, - uint256 amount1 - ) - { - require(p.exists, "Unknown position"); - - INonfungiblePositionManager.IncreaseLiquidityParams - memory params = INonfungiblePositionManager - .IncreaseLiquidityParams({ - tokenId: p.tokenId, - amount0Desired: desiredAmount0, - amount1Desired: desiredAmount1, - amount0Min: minAmount0, - amount1Min: minAmount1, - deadline: block.timestamp - }); - - (liquidity, amount0, amount1) = positionManager.increaseLiquidity( - params - ); - - p.liquidity += liquidity; - - emit UniswapV3LiquidityAdded(p.tokenId, amount0, amount1, liquidity); - } - - /** - * @notice Removes liquidity of the position in the pool - * @param p Position object - * @param liquidity Amount of liquidity to remove form the position - * @param minAmount0 Min amount of token0 to withdraw - * @param minAmount1 Min amount of token1 to withdraw - * @return amount0 Amount of token0 received after liquidation - * @return amount1 Amount of token1 received after liquidation - */ - function _decreaseLiquidityForPosition( - Position storage p, - uint128 liquidity, - uint256 minAmount0, - uint256 minAmount1 - ) internal returns (uint256 amount0, uint256 amount1) { - require(p.exists, "Unknown position"); - - (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(platformAddress) - .slot0(); - (uint256 exactAmount0, uint256 exactAmount1) = uniswapV3Helper - .getAmountsForLiquidity( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - liquidity - ); - - INonfungiblePositionManager.DecreaseLiquidityParams - memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: p.tokenId, - liquidity: liquidity, - amount0Min: minAmount0, - amount1Min: minAmount1, - deadline: block.timestamp - }); - - (amount0, amount1) = positionManager.decreaseLiquidity(params); - - p.liquidity -= liquidity; - - emit UniswapV3LiquidityRemoved(p.tokenId, amount0, amount1, liquidity); + emit UniswapV3StrategyLib.UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); } /*************************************** diff --git a/contracts/contracts/utils/UniswapV3StrategyLib.sol b/contracts/contracts/utils/UniswapV3StrategyLib.sol new file mode 100644 index 0000000000..29611e3c10 --- /dev/null +++ b/contracts/contracts/utils/UniswapV3StrategyLib.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; +import "../interfaces/uniswap/v3/IUniswapV3Helper.sol"; + +library UniswapV3StrategyLib { + // Represents a position minted by UniswapV3Strategy contract + struct Position { + bytes32 positionKey; // Required to read collectible fees from the V3 Pool + uint256 tokenId; // ERC721 token Id of the minted position + uint128 liquidity; // Amount of liquidity deployed + int24 lowerTick; // Lower tick index + int24 upperTick; // Upper tick index + bool exists; // True, if position is minted + // 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; + } + + event UniswapV3LiquidityAdded( + uint256 indexed tokenId, + uint256 amount0Sent, + uint256 amount1Sent, + uint128 liquidityMinted + ); + event UniswapV3LiquidityRemoved( + uint256 indexed tokenId, + uint256 amount0Received, + uint256 amount1Received, + uint128 liquidityBurned + ); + event UniswapV3PositionClosed( + uint256 indexed tokenId, + uint256 amount0Received, + uint256 amount1Received + ); + event UniswapV3FeeCollected( + uint256 indexed tokenId, + uint256 amount0, + uint256 amount1 + ); + + /** + * @notice Increases liquidity of the position in the pool + * @param positionManager Uniswap V3 Position manager + * @param p Position object + * @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 + * @return liquidity Amount of liquidity added to the pool + * @return amount0 Amount of token0 added to the position + * @return amount1 Amount of token1 added to the position + */ + function increaseLiquidityForPosition( + address positionManager, + UniswapV3StrategyLib.Position storage p, + uint256 desiredAmount0, + uint256 desiredAmount1, + uint256 minAmount0, + uint256 minAmount1 + ) + external + returns ( + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { + require(p.exists, "Unknown position"); + + INonfungiblePositionManager.IncreaseLiquidityParams + memory params = INonfungiblePositionManager + .IncreaseLiquidityParams({ + tokenId: p.tokenId, + amount0Desired: desiredAmount0, + amount1Desired: desiredAmount1, + amount0Min: minAmount0, + amount1Min: minAmount1, + deadline: block.timestamp + }); + + (liquidity, amount0, amount1) = INonfungiblePositionManager(positionManager).increaseLiquidity( + params + ); + + p.liquidity += liquidity; + + emit UniswapV3LiquidityAdded(p.tokenId, amount0, amount1, liquidity); + } + + /** + * @notice Removes liquidity of the position in the pool + * + * @param poolAddress Uniswap V3 pool address + * @param positionManager Uniswap V3 Position manager + * @param v3Helper Uniswap V3 helper contract + * @param p Position object reference + * @param liquidity Amount of liquidity to remove form the position + * @param minAmount0 Min amount of token0 to withdraw + * @param minAmount1 Min amount of token1 to withdraw + * + * @return amount0 Amount of token0 received after liquidation + * @return amount1 Amount of token1 received after liquidation + */ + function decreaseLiquidityForPosition( + address poolAddress, + address positionManager, + address v3Helper, + Position storage p, + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) external returns (uint256 amount0, uint256 amount1) { + require(p.exists, "Unknown position"); + + (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(poolAddress) + .slot0(); + (uint256 exactAmount0, uint256 exactAmount1) = IUniswapV3Helper(v3Helper) + .getAmountsForLiquidity( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + liquidity + ); + + INonfungiblePositionManager.DecreaseLiquidityParams + memory params = INonfungiblePositionManager + .DecreaseLiquidityParams({ + tokenId: p.tokenId, + liquidity: liquidity, + amount0Min: minAmount0, + amount1Min: minAmount1, + deadline: block.timestamp + }); + + (amount0, amount1) = INonfungiblePositionManager(positionManager).decreaseLiquidity(params); + + p.liquidity -= liquidity; + + emit UniswapV3LiquidityRemoved(p.tokenId, amount0, amount1, liquidity); + } + + /** + * @notice Collects the fees generated by the position on V3 pool + * @param positionManager Uniswap V3 Position manager + * @param tokenId Token ID of the position to collect fees of. + * @return amount0 Amount of token0 collected as fee + * @return amount1 Amount of token1 collected as fee + */ + function collectFeesForToken(address positionManager, uint256 tokenId) + external + returns (uint256 amount0, uint256 amount1) + { + INonfungiblePositionManager.CollectParams + memory params = INonfungiblePositionManager.CollectParams({ + tokenId: tokenId, + recipient: address(this), + amount0Max: type(uint128).max, + amount1Max: type(uint128).max + }); + + (amount0, amount1) = INonfungiblePositionManager(positionManager).collect(params); + + emit UniswapV3FeeCollected(tokenId, amount0, amount1); + } + + /** + * @notice Calculates the amount liquidity that needs to be removed + * to Withdraw specified amount of the given asset. + * + * @param poolAddress Uniswap V3 pool address + * @param v3Helper Uniswap V3 helper contract + * @param p Position object + * @param _asset Token needed + * @param amount Minimum amount to liquidate + * + * @return liquidity Liquidity to burn + * @return minAmount0 Minimum amount0 to expect + * @return minAmount1 Minimum amount1 to expect + */ + function calculateLiquidityToWithdraw( + address poolAddress, + address v3Helper, + UniswapV3StrategyLib.Position memory p, + address _asset, + uint256 amount + ) + external view + returns ( + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) + { + IUniswapV3Helper uniswapV3Helper = IUniswapV3Helper(v3Helper); + IUniswapV3Pool pool = IUniswapV3Pool(poolAddress); + (uint160 sqrtRatioX96, , , , , , ) = pool + .slot0(); + + // Total amount in Liquidity pools + (uint256 totalAmount0, uint256 totalAmount1) = uniswapV3Helper + .getAmountsForLiquidity( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + p.liquidity + ); + + if (_asset == pool.token0()) { + minAmount0 = amount; + minAmount1 = totalAmount1 / (totalAmount0 / amount); + liquidity = uniswapV3Helper.getLiquidityForAmounts( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + amount, + minAmount1 + ); + } else if (_asset == pool.token1()) { + minAmount0 = totalAmount0 / (totalAmount1 / amount); + minAmount1 = amount; + liquidity = uniswapV3Helper.getLiquidityForAmounts( + sqrtRatioX96, + p.sqrtRatioAX96, + p.sqrtRatioBX96, + minAmount0, + amount + ); + } + } +} \ No newline at end of file diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 75f717d7ec..38b568236e 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -1000,10 +1000,17 @@ const deployUniswapV3Strategy = async () => { const mockStrat = await ethers.getContract("MockStrategy"); + const UniswapV3StrategyLib = await deployWithConfirmation( + "UniswapV3StrategyLib" + ); + const uniV3UsdcUsdtImpl = await deployWithConfirmation( "UniV3_USDC_USDT_Strategy", [], - "GeneralizedUniswapV3Strategy" + "GeneralizedUniswapV3Strategy", + { + UniswapV3StrategyLib: UniswapV3StrategyLib.address, + } ); await deployWithConfirmation("UniV3_USDC_USDT_Proxy"); const uniV3UsdcUsdtProxy = await ethers.getContract("UniV3_USDC_USDT_Proxy"); diff --git a/contracts/deploy/025_resolution_upgrade_end.js b/contracts/deploy/025_resolution_upgrade_end.js index 1b83ccb7ff..dbfd1234ea 100644 --- a/contracts/deploy/025_resolution_upgrade_end.js +++ b/contracts/deploy/025_resolution_upgrade_end.js @@ -7,6 +7,7 @@ module.exports = deploymentWithProposal( "OUSD", undefined, undefined, + undefined, true ); const cOUSDProxy = await ethers.getContract("OUSDProxy"); diff --git a/contracts/deploy/032_convex_rewards.js b/contracts/deploy/032_convex_rewards.js index ba05f63942..935000c808 100644 --- a/contracts/deploy/032_convex_rewards.js +++ b/contracts/deploy/032_convex_rewards.js @@ -27,6 +27,7 @@ module.exports = deploymentWithProposal( "ConvexStrategy", undefined, undefined, + undefined, true // Disable storage slot checking. We are intentionally renaming a slot. ); const cConvexStrategyProxy = await ethers.getContract( diff --git a/contracts/deploy/036_multiple_rewards_strategies.js b/contracts/deploy/036_multiple_rewards_strategies.js index c28fd312ca..d7414b557f 100644 --- a/contracts/deploy/036_multiple_rewards_strategies.js +++ b/contracts/deploy/036_multiple_rewards_strategies.js @@ -17,12 +17,14 @@ module.exports = deploymentWithProposal( "VaultAdmin", undefined, undefined, + undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); const dVaultCore = await deployWithConfirmation( "VaultCore", undefined, undefined, + undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); @@ -48,18 +50,21 @@ module.exports = deploymentWithProposal( "ConvexStrategy", undefined, undefined, + undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); const dCompoundStrategyImpl = await deployWithConfirmation( "CompoundStrategy", undefined, undefined, + undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); const dAaveStrategyImpl = await deployWithConfirmation( "AaveStrategy", undefined, undefined, + undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); diff --git a/contracts/deploy/042_ogv_buyback.js b/contracts/deploy/042_ogv_buyback.js index 626e8e1d42..7998f49859 100644 --- a/contracts/deploy/042_ogv_buyback.js +++ b/contracts/deploy/042_ogv_buyback.js @@ -41,6 +41,7 @@ module.exports = deploymentWithProposal( assetAddresses.RewardsSource, ], "Buyback", + undefined, true ); const cBuyback = await ethers.getContract("Buyback"); diff --git a/contracts/deploy/046_value_value_checker.js b/contracts/deploy/046_value_value_checker.js index 1e7ee74ac6..9e64ef5c54 100644 --- a/contracts/deploy/046_value_value_checker.js +++ b/contracts/deploy/046_value_value_checker.js @@ -21,6 +21,7 @@ module.exports = deploymentWithProposal( "VaultValueChecker", [cVaultProxy.address, cOUSDProxy.address], undefined, + undefined, true // Incompatibable storage layout ); const vaultValueChecker = await ethers.getContract("VaultValueChecker"); diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index 748f03a88d..b08aeb27fd 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -27,8 +27,11 @@ module.exports = deploymentWithGovernanceProposal( // Deployer Actions // ---------------- - // 0. Deploy UniswapV3Helper + // 0. Deploy UniswapV3Helper and UniswapV3StrategyLib const dUniswapV3Helper = await deployWithConfirmation("UniswapV3Helper"); + const dUniswapV3StrategyLib = await deployWithConfirmation( + "UniswapV3StrategyLib" + ); // 0. Upgrade VaultAdmin const dVaultAdmin = await deployWithConfirmation("VaultAdmin"); @@ -46,7 +49,12 @@ module.exports = deploymentWithGovernanceProposal( // 2. Deploy new implementation const dUniV3_USDC_USDT_StrategyImpl = await deployWithConfirmation( - "GeneralizedUniswapV3Strategy" + "GeneralizedUniswapV3Strategy", + [], + undefined, + { + UniswapV3StrategyLib: dUniswapV3StrategyLib.address, + } ); const cUniV3_USDC_USDT_Strategy = await ethers.getContractAt( "GeneralizedUniswapV3Strategy", @@ -77,7 +85,7 @@ module.exports = deploymentWithGovernanceProposal( // 4. Init and configure new Uniswap V3 strategy const initFunction = - "initialize(address,address,address,address,address,address,address)"; + "initialize(address,address,address,address,address,address,address,address)"; await withConfirmation( cUniV3_USDC_USDT_Strategy.connect(sDeployer)[initFunction]( cVaultProxy.address, // Vault diff --git a/contracts/tasks/storageSlots.js b/contracts/tasks/storageSlots.js index d82a76472e..04a8eca418 100644 --- a/contracts/tasks/storageSlots.js +++ b/contracts/tasks/storageSlots.js @@ -27,9 +27,11 @@ const getStorageFileLocation = (hre, contractName) => { return `${layoutFolder}${contractName}.json`; }; -const getStorageLayoutForContract = async (hre, contractName) => { +const getStorageLayoutForContract = async (hre, contractName, libraries) => { const validations = await readValidations(hre); - const implFactory = await hre.ethers.getContractFactory(contractName); + const implFactory = await hre.ethers.getContractFactory(contractName, { + libraries, + }); const unlinkedBytecode = getUnlinkedBytecode( validations, implFactory.bytecode @@ -50,8 +52,12 @@ const loadPreviousStorageLayoutForContract = async (hre, contractName) => { return JSON.parse(await promises.readFile(location, "utf8")); }; -const storeStorageLayoutForContract = async (hre, contractName) => { - const layout = await getStorageLayoutForContract(hre, contractName); +const storeStorageLayoutForContract = async (hre, contractName, libraries) => { + const layout = await getStorageLayoutForContract( + hre, + contractName, + libraries + ); const storageLayoutFile = getStorageFileLocation(hre, contractName); // pretty print storage layout for the contract @@ -116,13 +122,17 @@ const showStorageLayout = async (taskArguments, hre) => { visualizeLayoutData(layout); }; -const assertUpgradeIsSafe = async (hre, contractName) => { +const assertUpgradeIsSafe = async (hre, contractName, libraries) => { if (!isContractEligible(contractName)) { console.debug(`Skipping storage slot validation of ${contractName}.`); return true; } - const layout = await getStorageLayoutForContract(hre, contractName); + const layout = await getStorageLayoutForContract( + hre, + contractName, + libraries + ); const oldLayout = await loadPreviousStorageLayoutForContract( hre, diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index cc713982e0..d4a446de94 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -112,11 +112,19 @@ async function defaultFixture() { const buyback = await ethers.getContract("Buyback"); + const UniV3Lib = await ethers.getContract("UniswapV3StrategyLib"); const UniV3_USDC_USDT_Proxy = await ethers.getContract( "UniV3_USDC_USDT_Proxy" ); - const UniV3_USDC_USDT_Strategy = await ethers.getContractAt( + const UniswapV3StrategyFactory = await ethers.getContractFactory( "GeneralizedUniswapV3Strategy", + { libraries: { UniswapV3StrategyLib: UniV3Lib.address } } + ); + const UniV3_USDC_USDT_Strategy = await ethers.getContractAt( + [ + ...UniV3Lib.interface.format("full").filter((e) => e.startsWith("event")), + ...UniswapV3StrategyFactory.interface.format("full"), + ], UniV3_USDC_USDT_Proxy.address ); const UniV3Helper = await ethers.getContract("UniswapV3Helper"); diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index f238936ffc..d7371ccea7 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -102,6 +102,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { BigNumber.from(usdtAmount).mul(10 ** 6) ); + console.log("Rebalance in process...") + const tx = await strategy .connect(operator) .rebalance( @@ -112,6 +114,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { upperTick ); + console.log("Rebalance done") + const { events } = await tx.wait(); const [tokenId, amount0Minted, amount1Minted, liquidityMinted] = @@ -126,7 +130,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { }; }; - it.only("Should mint position", async () => { + it("Should mint position", async () => { const usdcBalBefore = await strategy.checkBalance(usdc.address); const usdtBalBefore = await strategy.checkBalance(usdt.address); diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index 0be336345d..dfb49ff4ae 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -50,13 +50,15 @@ const deployWithConfirmation = async ( contractName, args, contract, + libraries, skipUpgradeSafety = false ) => { // check that upgrade doesn't corrupt the storage slots if (!skipUpgradeSafety) { await assertUpgradeIsSafe( hre, - typeof contract == "string" ? contract : contractName + typeof contract == "string" ? contract : contractName, + libraries ); } @@ -70,6 +72,7 @@ const deployWithConfirmation = async ( args, contract, fieldsToCompare: null, + libraries, ...(await getTxOpts()), }) ); From 8524569d50156ad2c2e0339156ef9013afc4344c Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 15 Mar 2023 00:02:13 +0530 Subject: [PATCH 17/83] prettify --- .../interfaces/IUniswapV3Strategy.sol | 2 + contracts/contracts/interfaces/IVault.sol | 5 +- .../mocks/uniswap/v2/MockUniswapRouter.sol | 12 +- .../GeneralizedUniswapV3Strategy.sol | 183 +++++++++++------- .../contracts/utils/UniswapV3StrategyLib.sol | 27 +-- contracts/contracts/vault/VaultAdmin.sol | 21 +- contracts/contracts/vault/VaultStorage.sol | 6 +- .../test/strategies/uniswap-v3.fork-test.js | 8 +- contracts/test/strategies/uniswap-v3.js | 6 +- 9 files changed, 168 insertions(+), 102 deletions(-) diff --git a/contracts/contracts/interfaces/IUniswapV3Strategy.sol b/contracts/contracts/interfaces/IUniswapV3Strategy.sol index 157322cd36..9b7bcd738e 100644 --- a/contracts/contracts/interfaces/IUniswapV3Strategy.sol +++ b/contracts/contracts/interfaces/IUniswapV3Strategy.sol @@ -5,6 +5,8 @@ import { IStrategy } from "./IStrategy.sol"; interface IUniswapV3Strategy is IStrategy { function reserveStrategy(address token) external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); } diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index 1968c8a657..9066803e44 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -177,8 +177,5 @@ interface IVault { function depositForUniswapV3(address asset, uint256 amount) external; - function withdrawAssetForUniswapV3( - address asset, - uint256 amount - ) external; + function withdrawAssetForUniswapV3(address asset, uint256 amount) external; } diff --git a/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol b/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol index 83e507832d..51a7123f9b 100644 --- a/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol +++ b/contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol @@ -133,12 +133,20 @@ contract MockUniswapRouter is IUniswapV2Router { uint160 sqrtPriceLimitX96; } - function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut) { + function exactInputSingle(ExactInputSingleParams calldata params) + external + payable + returns (uint256 amountOut) + { amountOut = params.amountIn.scaleBy( Helpers.getDecimals(params.tokenIn), Helpers.getDecimals(params.tokenOut) ); - IERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + IERC20(params.tokenIn).transferFrom( + msg.sender, + address(this), + params.amountIn + ); IERC20(params.tokenOut).transfer(params.recipient, amountOut); require( amountOut >= params.amountOutMinimum, diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol index 12fc96c4a5..78d41e3b8c 100644 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol @@ -19,10 +19,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { using SafeERC20 for IERC20; event OperatorChanged(address _address); - event ReserveStrategyChanged( - address asset, - address reserveStrategy - ); + event ReserveStrategyChanged(address asset, address reserveStrategy); event MinDepositThresholdChanged( address asset, uint256 minDepositThreshold @@ -56,7 +53,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { ); event SwapsPauseStatusChanged(bool paused); event MaxSwapSlippageChanged(uint24 maxSlippage); - event AssetSwappedForRebalancing(address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut); + event AssetSwappedForRebalancing( + address indexed tokenIn, + address indexed tokenOut, + uint256 amountIn, + uint256 amountOut + ); // The address that can manage the positions on Uniswap V3 address public operatorAddr; @@ -72,11 +74,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // Represents both tokens supported by the strategy struct PoolToken { bool isSupported; // True if asset is either token0 or token1 - // When the funds are not deployed in Uniswap V3 Pool, they will // be deposited to these reserve strategies address reserveStrategy; - // Deposits to reserve strategy when contract balance exceeds this amount uint256 minDepositThreshold; @@ -136,7 +136,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { /*************************************** Modifiers ****************************************/ - + /** * @dev Ensures that the caller is Governor or Strategist. */ @@ -173,7 +173,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { /*************************************** Initializer ****************************************/ - + /** * @dev Initialize the contract * @param _vaultAddress OUSD Vault @@ -238,7 +238,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { /*************************************** Admin Utils ****************************************/ - + /** * @notice Change the address of the operator * @dev Can only be called by the Governor or Strategist @@ -258,24 +258,24 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @param _asset Asset to set the reserve strategy for * @param _reserveStrategy The new reserve strategy for token */ - function setReserveStrategy( - address _asset, - address _reserveStrategy - ) external onlyGovernorOrStrategist nonReentrant { + function setReserveStrategy(address _asset, address _reserveStrategy) + external + onlyGovernorOrStrategist + nonReentrant + { _setReserveStrategy(_asset, _reserveStrategy); } /** * @notice Change the reserve strategy of the supported asset - * @dev Will throw if the strategies don't support the assets or if + * @dev Will throw if the strategies don't support the assets or if * strategy is unsupported by the vault * @param _asset Asset to set the reserve strategy for * @param _reserveStrategy The new reserve strategy for token */ - function _setReserveStrategy( - address _asset, - address _reserveStrategy - ) internal { + function _setReserveStrategy(address _asset, address _reserveStrategy) + internal + { // require( // IVault(vaultAddress).isStrategySupported(_reserveStrategy), // "Unsupported strategy" @@ -288,10 +288,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { poolTokens[_asset].reserveStrategy = _reserveStrategy; - emit ReserveStrategyChanged( - _asset, - _reserveStrategy - ); + emit ReserveStrategyChanged(_asset, _reserveStrategy); } /** @@ -299,9 +296,11 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @param _asset Address of the asset * @return reserveStrategyAddr Reserve strategy address */ - function reserveStrategy(address _asset) - external view onlyPoolTokens(_asset) - returns (address reserveStrategyAddr) + function reserveStrategy(address _asset) + external + view + onlyPoolTokens(_asset) + returns (address reserveStrategyAddr) { return poolTokens[_asset].reserveStrategy; } @@ -312,7 +311,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @param _minThreshold The new deposit threshold value */ function setMinDepositThreshold(address _asset, uint256 _minThreshold) - external onlyGovernorOrStrategist onlyPoolTokens(_asset) + external + onlyGovernorOrStrategist + onlyPoolTokens(_asset) { PoolToken storage token = poolTokens[_asset]; token.minDepositThreshold = _minThreshold; @@ -324,7 +325,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { emit SwapsPauseStatusChanged(_paused); } - function setMaxSwapSlippage(uint24 _maxSlippage) external onlyGovernorOrStrategist { + function setMaxSwapSlippage(uint24 _maxSlippage) + external + onlyGovernorOrStrategist + { maxSwapSlippage = _maxSlippage; // emit SwapsPauseStatusChanged(_paused); } @@ -363,17 +367,25 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { function _depositAll() internal { uint256 token0Bal = IERC20(token0).balanceOf(address(this)); uint256 token1Bal = IERC20(token1).balanceOf(address(this)); - if (token0Bal > 0 && token0Bal >= poolTokens[token0].minDepositThreshold) { + if ( + token0Bal > 0 && token0Bal >= poolTokens[token0].minDepositThreshold + ) { IVault(vaultAddress).depositForUniswapV3(token0, token0Bal); } - if (token1Bal > 0 && token1Bal >= poolTokens[token1].minDepositThreshold) { + if ( + token1Bal > 0 && token1Bal >= poolTokens[token1].minDepositThreshold + ) { IVault(vaultAddress).depositForUniswapV3(token1, token1Bal); } // Not emitting Deposit events since the Reserve strategies would do so } - function _withdrawAssetFromCurrentPosition(address _asset, uint256 amount) internal { - UniswapV3StrategyLib.Position storage p = tokenIdToPosition[currentPositionTokenId]; + function _withdrawAssetFromCurrentPosition(address _asset, uint256 amount) + internal + { + UniswapV3StrategyLib.Position storage p = tokenIdToPosition[ + currentPositionTokenId + ]; require(p.exists && p.liquidity > 0, "Liquidity error"); // Figure out liquidity to burn @@ -394,9 +406,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { platformAddress, address(positionManager), address(uniswapV3Helper), - p, - liquidity, - minAmount0, + p, + liquidity, + minAmount0, minAmount1 ); } @@ -451,9 +463,7 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { } } - function _getToken1ForToken0(uint256 amount0) internal { - - } + function _getToken1ForToken0(uint256 amount0) internal {} /** * @dev Checks if there's enough balance left in the contract to provide liquidity. @@ -461,9 +471,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { * @param desiredAmount0 Minimum amount of token0 needed * @param desiredAmount1 Minimum amount of token1 needed */ - function _ensureAssetBalances(uint256 desiredAmount0, uint256 desiredAmount1) - internal - { + function _ensureAssetBalances( + uint256 desiredAmount0, + uint256 desiredAmount1 + ) internal { IVault vault = IVault(vaultAddress); // Withdraw enough funds from Reserve strategies @@ -501,15 +512,23 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { uint256 token0Balance = t0Contract.balanceOf(address(this)); uint256 token1Balance = t1Contract.balanceOf(address(this)); - uint256 token0Needed = desiredAmount0 > token0Balance ? desiredAmount0 - token0Balance : 0; - uint256 token1Needed = desiredAmount1 > token1Balance ? desiredAmount1 - token1Balance : 0; + uint256 token0Needed = desiredAmount0 > token0Balance + ? desiredAmount0 - token0Balance + : 0; + uint256 token1Needed = desiredAmount1 > token1Balance + ? desiredAmount1 - token1Balance + : 0; if (swapZeroForOne) { // Amount available in reserve strategies - uint256 t1ReserveBal = IStrategy(poolTokens[token1].reserveStrategy).checkBalance(token1); + uint256 t1ReserveBal = IStrategy(poolTokens[token1].reserveStrategy) + .checkBalance(token1); // Only swap when asset isn't available in reserve as well - require(token1Needed > 0 && token1Needed < t1ReserveBal, "Cannot swap when the asset is available in reserve"); + require( + token1Needed > 0 && 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 @@ -519,10 +538,14 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { t0Contract.safeApprove(address(swapRouter), swapAmountIn); } else { // Amount available in reserve strategies - uint256 t0ReserveBal = IStrategy(poolTokens[token0].reserveStrategy).checkBalance(token0); + uint256 t0ReserveBal = IStrategy(poolTokens[token0].reserveStrategy) + .checkBalance(token0); // Only swap when asset isn't available in reserve as well - require(token0Needed > 0 && token0Needed < t0ReserveBal, "Cannot swap when the asset is available in reserve"); + require( + token0Needed > 0 && 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 @@ -536,11 +559,17 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // Fund strategy from reserve strategies if (token0Needed > 0) { - IVault(vaultAddress).withdrawAssetForUniswapV3(token0, token0Needed); + IVault(vaultAddress).withdrawAssetForUniswapV3( + token0, + token0Needed + ); } if (token1Needed > 0) { - IVault(vaultAddress).withdrawAssetForUniswapV3(token0, token0Needed); + IVault(vaultAddress).withdrawAssetForUniswapV3( + token0, + token0Needed + ); } // TODO: Slippage/price check @@ -583,7 +612,10 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { nonReentrant { if (currentPositionTokenId > 0) { - UniswapV3StrategyLib.collectFeesForToken(address(positionManager), currentPositionTokenId); + UniswapV3StrategyLib.collectFeesForToken( + address(positionManager), + currentPositionTokenId + ); } } @@ -702,7 +734,9 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // Provide liquidity if (tokenId > 0) { // Add liquidity to the position token - UniswapV3StrategyLib.Position storage p = tokenIdToPosition[tokenId]; + UniswapV3StrategyLib.Position storage p = tokenIdToPosition[ + tokenId + ]; UniswapV3StrategyLib.increaseLiquidityForPosition( address(positionManager), p, @@ -747,11 +781,17 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { int24 lowerTick, int24 upperTick ) external onlyGovernorOrStrategistOrOperator nonReentrant { - _rebalance(desiredAmounts, minAmounts, minRedeemAmounts, lowerTick, upperTick); + _rebalance( + desiredAmounts, + minAmounts, + minRedeemAmounts, + lowerTick, + upperTick + ); } - function swapAndRebalance( - SwapAndRebalanceParams calldata params + function swapAndRebalance(SwapAndRebalanceParams calldata params) + external // uint256[2] calldata desiredAmounts, // uint256[2] calldata minAmounts, // uint256[2] calldata minRedeemAmounts, @@ -761,10 +801,14 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // uint256 swapMinAmountOut, // uint160 sqrtPriceLimitX96, // bool swapZeroForOne - ) external onlyGovernorOrStrategistOrOperator nonReentrant { + onlyGovernorOrStrategistOrOperator + nonReentrant + { require(params.lowerTick < params.upperTick, "Invalid tick range"); - uint256 tokenId = ticksToTokenId[_getTickPositionKey(params.lowerTick, params.upperTick)]; + uint256 tokenId = ticksToTokenId[ + _getTickPositionKey(params.lowerTick, params.upperTick) + ]; if (currentPositionTokenId > 0) { // Close any active position @@ -777,18 +821,20 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { // Withdraw enough funds from Reserve strategies and swap to desired amounts _ensureAssetsBySwapping( - params.desiredAmount0, - params.desiredAmount1, - params.swapAmountIn, - params.swapMinAmountOut, - params.sqrtPriceLimitX96, + params.desiredAmount0, + params.desiredAmount1, + params.swapAmountIn, + params.swapMinAmountOut, + params.sqrtPriceLimitX96, params.swapZeroForOne ); // Provide liquidity if (tokenId > 0) { // Add liquidity to the position token - UniswapV3StrategyLib.Position storage p = tokenIdToPosition[tokenId]; + UniswapV3StrategyLib.Position storage p = tokenIdToPosition[ + tokenId + ]; UniswapV3StrategyLib.increaseLiquidityForPosition( address(positionManager), p, @@ -932,10 +978,8 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { emit UniswapV3PositionClosed(tokenId, amount0, amount1); // Collect all fees for position - (uint256 amount0Fee, uint256 amount1Fee) = UniswapV3StrategyLib.collectFeesForToken( - address(positionManager), - p.tokenId - ); + (uint256 amount0Fee, uint256 amount1Fee) = UniswapV3StrategyLib + .collectFeesForToken(address(positionManager), p.tokenId); amount0 = amount0 + amount0Fee; amount1 = amount1 + amount1Fee; @@ -1007,7 +1051,12 @@ contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { }); emit UniswapV3PositionMinted(tokenId, lowerTick, upperTick); - emit UniswapV3StrategyLib.UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); + emit UniswapV3StrategyLib.UniswapV3LiquidityAdded( + tokenId, + amount0, + amount1, + liquidity + ); } /*************************************** diff --git a/contracts/contracts/utils/UniswapV3StrategyLib.sol b/contracts/contracts/utils/UniswapV3StrategyLib.sol index 29611e3c10..af08a1204c 100644 --- a/contracts/contracts/utils/UniswapV3StrategyLib.sol +++ b/contracts/contracts/utils/UniswapV3StrategyLib.sol @@ -84,9 +84,9 @@ library UniswapV3StrategyLib { deadline: block.timestamp }); - (liquidity, amount0, amount1) = INonfungiblePositionManager(positionManager).increaseLiquidity( - params - ); + (liquidity, amount0, amount1) = INonfungiblePositionManager( + positionManager + ).increaseLiquidity(params); p.liquidity += liquidity; @@ -95,7 +95,7 @@ library UniswapV3StrategyLib { /** * @notice Removes liquidity of the position in the pool - * + * * @param poolAddress Uniswap V3 pool address * @param positionManager Uniswap V3 Position manager * @param v3Helper Uniswap V3 helper contract @@ -120,8 +120,9 @@ library UniswapV3StrategyLib { (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(poolAddress) .slot0(); - (uint256 exactAmount0, uint256 exactAmount1) = IUniswapV3Helper(v3Helper) - .getAmountsForLiquidity( + (uint256 exactAmount0, uint256 exactAmount1) = IUniswapV3Helper( + v3Helper + ).getAmountsForLiquidity( sqrtRatioX96, p.sqrtRatioAX96, p.sqrtRatioBX96, @@ -138,7 +139,8 @@ library UniswapV3StrategyLib { deadline: block.timestamp }); - (amount0, amount1) = INonfungiblePositionManager(positionManager).decreaseLiquidity(params); + (amount0, amount1) = INonfungiblePositionManager(positionManager) + .decreaseLiquidity(params); p.liquidity -= liquidity; @@ -164,7 +166,8 @@ library UniswapV3StrategyLib { amount1Max: type(uint128).max }); - (amount0, amount1) = INonfungiblePositionManager(positionManager).collect(params); + (amount0, amount1) = INonfungiblePositionManager(positionManager) + .collect(params); emit UniswapV3FeeCollected(tokenId, amount0, amount1); } @@ -190,7 +193,8 @@ library UniswapV3StrategyLib { address _asset, uint256 amount ) - external view + external + view returns ( uint128 liquidity, uint256 minAmount0, @@ -199,8 +203,7 @@ library UniswapV3StrategyLib { { IUniswapV3Helper uniswapV3Helper = IUniswapV3Helper(v3Helper); IUniswapV3Pool pool = IUniswapV3Pool(poolAddress); - (uint160 sqrtRatioX96, , , , , , ) = pool - .slot0(); + (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); // Total amount in Liquidity pools (uint256 totalAmount0, uint256 totalAmount1) = uniswapV3Helper @@ -233,4 +236,4 @@ library UniswapV3StrategyLib { ); } } -} \ No newline at end of file +} diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index ab0ef3eee1..d86d30dcc1 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -267,8 +267,11 @@ contract VaultAdmin is VaultStorage { * @param _addr Address of the strategy to check * @return supported True, if strategy is recognized by the vault */ - function isStrategySupported(address _addr) - external view returns (bool supported) { + function isStrategySupported(address _addr) + external + view + returns (bool supported) + { supported = strategies[_addr].isSupported; } @@ -622,16 +625,20 @@ contract VaultAdmin is VaultStorage { * @param asset Address of the token * @param amount Amount of token1 required */ - function withdrawAssetForUniswapV3( - address asset, - uint256 amount - ) external onlyUniswapV3Strategies nonReentrant { + function withdrawAssetForUniswapV3(address asset, uint256 amount) + external + onlyUniswapV3Strategies + nonReentrant + { IUniswapV3Strategy v3Strategy = IUniswapV3Strategy(msg.sender); require(amount > 0, "Invalid amount specified"); address reserveStrategyAddr = IUniswapV3Strategy(v3Strategy) .reserveStrategy(asset); - require(strategies[reserveStrategyAddr].isSupported, "Unknown reserve strategy"); + require( + strategies[reserveStrategyAddr].isSupported, + "Unknown reserve strategy" + ); IStrategy reserveStrategy = IStrategy(reserveStrategyAddr); require(reserveStrategy.supportsAsset(asset), "Unsupported asset"); diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 63fb386385..d473a8ee4b 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -49,7 +49,11 @@ contract VaultStorage is Initializable, Governable { event TrusteeFeeBpsChanged(uint256 _basis); event TrusteeAddressChanged(address _address); event NetOusdMintForStrategyThresholdChanged(uint256 _threshold); - event AssetTransferredToUniswapV3Strategy(address indexed strategy, address indexed asset, uint256 amount); + event AssetTransferredToUniswapV3Strategy( + address indexed strategy, + address indexed asset, + uint256 amount + ); // Assets supported by the Vault, i.e. Stablecoins struct Asset { diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index d7371ccea7..70f9f4cd0a 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -102,19 +102,19 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { BigNumber.from(usdtAmount).mul(10 ** 6) ); - console.log("Rebalance in process...") + console.log("Rebalance in process..."); const tx = await strategy .connect(operator) .rebalance( - [maxUSDC, maxUSDT], + [maxUSDC, maxUSDT], [maxUSDC.mul(9900).div(10000), maxUSDT.mul(9900).div(10000)], [0, 0], - lowerTick, + lowerTick, upperTick ); - console.log("Rebalance done") + console.log("Rebalance done"); const { events } = await tx.wait(); diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 68d2c77206..5f80da3134 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -1,10 +1,6 @@ const { expect } = require("chai"); const { uniswapV3FixturSetup } = require("../_fixture"); -const { - units, - ousdUnits, - expectApproxSupply, -} = require("../helpers"); +const { units, ousdUnits, expectApproxSupply } = require("../helpers"); const uniswapV3Fixture = uniswapV3FixturSetup(); From b1712f63cffe90d4956d907ca97c4629305ba342 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 16 Mar 2023 23:39:01 +0530 Subject: [PATCH 18/83] Split contracts and add swapAndRebalance method --- .../interfaces/IUniswapV3Strategy.sol | 4 +- contracts/contracts/interfaces/IVault.sol | 4 +- .../v3/MockNonfungiblePositionManager.sol | 28 +- .../mocks/uniswap/v3/MockUniswapV3Pool.sol | 6 +- .../GeneralizedUniswapV3Strategy.sol | 1144 ----------------- .../strategies/uniswap/UniswapV3Library.sol | 52 + .../uniswap/UniswapV3LiquidityManager.sol | 781 +++++++++++ .../strategies/uniswap/UniswapV3Strategy.sol | 476 +++++++ .../uniswap/UniswapV3StrategyStorage.sol | 187 +++ contracts/contracts/vault/VaultAdmin.sol | 46 +- contracts/deploy/001_core.js | 2 +- .../deploy/049_uniswap_usdc_usdt_strategy.js | 47 +- contracts/test/_fixture.js | 26 +- .../test/strategies/uniswap-v3.fork-test.js | 245 +++- 14 files changed, 1794 insertions(+), 1254 deletions(-) delete mode 100644 contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol create mode 100644 contracts/contracts/strategies/uniswap/UniswapV3Library.sol create mode 100644 contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol create mode 100644 contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol create mode 100644 contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol diff --git a/contracts/contracts/interfaces/IUniswapV3Strategy.sol b/contracts/contracts/interfaces/IUniswapV3Strategy.sol index 9b7bcd738e..ab7bf50f75 100644 --- a/contracts/contracts/interfaces/IUniswapV3Strategy.sol +++ b/contracts/contracts/interfaces/IUniswapV3Strategy.sol @@ -4,9 +4,7 @@ pragma solidity ^0.8.0; import { IStrategy } from "./IStrategy.sol"; interface IUniswapV3Strategy is IStrategy { - function reserveStrategy(address token) external view returns (address); - function token0() external view returns (address); - function token1() external view returns (address); + function reserveStrategy(address token) external view returns (address); } diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index 9066803e44..88a31e3af7 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -175,7 +175,7 @@ interface IVault { function netOusdMintedForStrategy() external view returns (int256); - function depositForUniswapV3(address asset, uint256 amount) external; + function depositToUniswapV3Reserve(address asset, uint256 amount) external; - function withdrawAssetForUniswapV3(address asset, uint256 amount) external; + function withdrawFromUniswapV3Reserve(address asset, uint256 amount) external; } diff --git a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol index 9c1d956a85..582463b4c3 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol @@ -67,13 +67,13 @@ contract MockNonfungiblePositionManager { uint256 public slippage = 100; - IUniswapV3Helper internal uniswapV3Helper; + IUniswapV3Helper internal helper; IMockUniswapV3Pool internal mockPool; uint256 internal tokenCount = 0; constructor(address _helper, address _mockPool) { - uniswapV3Helper = IUniswapV3Helper(_helper); + helper = IUniswapV3Helper(_helper); mockPool = IMockUniswapV3Pool(_mockPool); } @@ -149,18 +149,18 @@ contract MockNonfungiblePositionManager { MockPosition storage p = mockPositions[tokenId]; - (liquidity) = uniswapV3Helper.getLiquidityForAmounts( + (liquidity) = helper.getLiquidityForAmounts( mockPool.mockSqrtPriceX96(), - uniswapV3Helper.getSqrtRatioAtTick(p.tickLower), - uniswapV3Helper.getSqrtRatioAtTick(p.tickUpper), + helper.getSqrtRatioAtTick(p.tickLower), + helper.getSqrtRatioAtTick(p.tickUpper), params.amount0Desired, params.amount1Desired ); - (amount0, amount1) = uniswapV3Helper.getAmountsForLiquidity( + (amount0, amount1) = helper.getAmountsForLiquidity( mockPool.mockSqrtPriceX96(), - uniswapV3Helper.getSqrtRatioAtTick(p.tickLower), - uniswapV3Helper.getSqrtRatioAtTick(p.tickUpper), + helper.getSqrtRatioAtTick(p.tickLower), + helper.getSqrtRatioAtTick(p.tickUpper), liquidity ); @@ -192,18 +192,18 @@ contract MockNonfungiblePositionManager { { MockPosition storage p = mockPositions[params.tokenId]; - (liquidity) = uniswapV3Helper.getLiquidityForAmounts( + (liquidity) = helper.getLiquidityForAmounts( mockPool.mockSqrtPriceX96(), - uniswapV3Helper.getSqrtRatioAtTick(p.tickLower), - uniswapV3Helper.getSqrtRatioAtTick(p.tickUpper), + helper.getSqrtRatioAtTick(p.tickLower), + helper.getSqrtRatioAtTick(p.tickUpper), params.amount0Desired, params.amount1Desired ); - (amount0, amount1) = uniswapV3Helper.getAmountsForLiquidity( + (amount0, amount1) = helper.getAmountsForLiquidity( mockPool.mockSqrtPriceX96(), - uniswapV3Helper.getSqrtRatioAtTick(p.tickLower), - uniswapV3Helper.getSqrtRatioAtTick(p.tickUpper), + helper.getSqrtRatioAtTick(p.tickLower), + helper.getSqrtRatioAtTick(p.tickUpper), liquidity ); diff --git a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol index 6ee8f970a0..b959b693b1 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol @@ -11,7 +11,7 @@ contract MockUniswapV3Pool { uint160 public mockSqrtPriceX96; int24 public mockTick; - IUniswapV3Helper internal uniswapV3Helper; + IUniswapV3Helper internal helper; constructor( address _token0, @@ -22,7 +22,7 @@ contract MockUniswapV3Pool { token0 = _token0; token1 = _token1; fee = _fee; - uniswapV3Helper = IUniswapV3Helper(_helper); + helper = IUniswapV3Helper(_helper); } function slot0() @@ -43,7 +43,7 @@ contract MockUniswapV3Pool { function setTick(int24 tick) public { mockTick = tick; - mockSqrtPriceX96 = uniswapV3Helper.getSqrtRatioAtTick(tick); + mockSqrtPriceX96 = helper.getSqrtRatioAtTick(tick); } function setVal(uint160 sqrtPriceX96, int24 tick) public { diff --git a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol b/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol deleted file mode 100644 index 78d41e3b8c..0000000000 --- a/contracts/contracts/strategies/GeneralizedUniswapV3Strategy.sol +++ /dev/null @@ -1,1144 +0,0 @@ -// SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.8.0; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import { InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol"; -import { IStrategy } from "../interfaces/IStrategy.sol"; -import { IVault } from "../interfaces/IVault.sol"; - -import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; -import { INonfungiblePositionManager } from "../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; -import { IUniswapV3Helper } from "../interfaces/uniswap/v3/IUniswapV3Helper.sol"; -import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; - -import { UniswapV3StrategyLib } from "../utils/UniswapV3StrategyLib.sol"; - -contract GeneralizedUniswapV3Strategy is InitializableAbstractStrategy { - using SafeERC20 for IERC20; - - event OperatorChanged(address _address); - event ReserveStrategyChanged(address asset, address reserveStrategy); - event MinDepositThresholdChanged( - address asset, - uint256 minDepositThreshold - ); - // event UniswapV3FeeCollected( - // uint256 indexed tokenId, - // uint256 amount0, - // uint256 amount1 - // ); - // 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 - ); - event UniswapV3PositionClosed( - uint256 indexed tokenId, - uint256 amount0Received, - uint256 amount1Received - ); - event SwapsPauseStatusChanged(bool paused); - event MaxSwapSlippageChanged(uint24 maxSlippage); - event AssetSwappedForRebalancing( - address indexed tokenIn, - address indexed tokenOut, - uint256 amountIn, - uint256 amountOut - ); - - // The address that can manage the positions on Uniswap V3 - address public operatorAddr; - address public token0; // Token0 of Uniswap V3 Pool - address public token1; // Token1 of Uniswap V3 Pool - - uint24 public poolFee; // Uniswap V3 Pool Fee - uint24 public maxSwapSlippage = 100; // 1%; Reverts if swap slippage is higher than this - bool public swapsPaused = false; // True if Swaps are paused - - uint256 public maxTVL; // In USD, 18 decimals - - // Represents both tokens supported by the strategy - struct PoolToken { - bool isSupported; // True if asset is either token0 or token1 - // When the funds are not deployed in Uniswap V3 Pool, they will - // be deposited to these reserve strategies - address reserveStrategy; - // Deposits to reserve strategy when contract balance exceeds this amount - uint256 minDepositThreshold; - - // uint256 minSwapPrice; // Min swap price for the token - } - - struct SwapAndRebalanceParams { - uint256 desiredAmount0; - uint256 desiredAmount1; - uint256 minAmount0; - uint256 minAmount1; - uint256 minRedeemAmount0; - uint256 minRedeemAmount1; - int24 lowerTick; - int24 upperTick; - uint256 swapAmountIn; - uint256 swapMinAmountOut; - uint160 sqrtPriceLimitX96; - bool swapZeroForOne; - } - - mapping(address => PoolToken) public poolTokens; - - // Uniswap V3's PositionManager - INonfungiblePositionManager public positionManager; - - // // Represents a position minted by this contract - // struct Position { - // bytes32 positionKey; // Required to read collectible fees from the V3 Pool - // uint256 tokenId; // ERC721 token Id of the minted position - // uint128 liquidity; // Amount of liquidity deployed - // int24 lowerTick; // Lower tick index - // int24 upperTick; // Upper tick index - // bool exists; // True, if position is minted - // // 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; - // } - - // A lookup table to find token IDs of position using f(lowerTick, upperTick) - mapping(int48 => uint256) internal ticksToTokenId; - // Maps tokenIDs to their Position object - mapping(uint256 => UniswapV3StrategyLib.Position) public tokenIdToPosition; - // Token ID of the position that's being used to provide LP at the time - uint256 public currentPositionTokenId; - - // A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch - IUniswapV3Helper internal uniswapV3Helper; - - ISwapRouter internal swapRouter; - - // Future-proofing - uint256[100] private __gap; - - /*************************************** - Modifiers - ****************************************/ - - /** - * @dev Ensures that the caller is Governor or Strategist. - */ - modifier onlyGovernorOrStrategist() { - require( - msg.sender == IVault(vaultAddress).strategistAddr() || - msg.sender == governor(), - "Caller is not the Strategist or Governor" - ); - _; - } - - /** - * @dev Ensures that the caller is Governor, Strategist or Operator. - */ - modifier onlyGovernorOrStrategistOrOperator() { - require( - msg.sender == operatorAddr || - msg.sender == IVault(vaultAddress).strategistAddr() || - msg.sender == governor(), - "Caller is not the Operator, Strategist or Governor" - ); - _; - } - - /** - * @dev Ensures that the asset address is either token0 or token1. - */ - modifier onlyPoolTokens(address addr) { - require(poolTokens[addr].isSupported, "Unsupported asset"); - _; - } - - /*************************************** - Initializer - ****************************************/ - - /** - * @dev Initialize the contract - * @param _vaultAddress OUSD Vault - * @param _poolAddress Uniswap V3 Pool - * @param _nonfungiblePositionManager Uniswap V3's Position Manager - * @param _token0ReserveStrategy Reserve Strategy for token0 - * @param _token1ReserveStrategy Reserve Strategy for token1 - * @param _operator Address that can manage LP positions on the V3 pool - * @param _uniswapV3Helper Deployed UniswapV3Helper contract - * @param _swapRouter Uniswap SwapRouter contract - */ - function initialize( - address _vaultAddress, - address _poolAddress, - address _nonfungiblePositionManager, - address _token0ReserveStrategy, - address _token1ReserveStrategy, - address _operator, - address _uniswapV3Helper, - address _swapRouter - ) external onlyGovernor initializer { - positionManager = INonfungiblePositionManager( - _nonfungiblePositionManager - ); - IUniswapV3Pool pool = IUniswapV3Pool(_poolAddress); - uniswapV3Helper = IUniswapV3Helper(_uniswapV3Helper); - swapRouter = ISwapRouter(_swapRouter); - - token0 = pool.token0(); - token1 = pool.token1(); - poolFee = pool.fee(); - - address[] memory _assets = new address[](2); - _assets[0] = token0; - _assets[1] = token1; - - super._initialize( - _poolAddress, - _vaultAddress, - new address[](0), // No Reward tokens - _assets, // Asset addresses - _assets // Platform token addresses - ); - - poolTokens[token0] = PoolToken({ - isSupported: true, - reserveStrategy: address(0), // Set below using `_setReserveStrategy()` - minDepositThreshold: 0 - }); - _setReserveStrategy(token0, _token0ReserveStrategy); - - poolTokens[token1] = PoolToken({ - isSupported: true, - reserveStrategy: address(0), // Set below using `_setReserveStrategy() - minDepositThreshold: 0 - }); - _setReserveStrategy(token1, _token1ReserveStrategy); - - _setOperator(_operator); - } - - /*************************************** - Admin Utils - ****************************************/ - - /** - * @notice Change the address of the operator - * @dev Can only be called by the Governor or Strategist - * @param _operator The new value to be set - */ - function setOperator(address _operator) external onlyGovernorOrStrategist { - _setOperator(_operator); - } - - function _setOperator(address _operator) internal { - operatorAddr = _operator; - emit OperatorChanged(_operator); - } - - /** - * @notice Change the reserve strategies of the supported assets - * @param _asset Asset to set the reserve strategy for - * @param _reserveStrategy The new reserve strategy for token - */ - function setReserveStrategy(address _asset, address _reserveStrategy) - external - onlyGovernorOrStrategist - nonReentrant - { - _setReserveStrategy(_asset, _reserveStrategy); - } - - /** - * @notice Change the reserve strategy of the supported asset - * @dev Will throw if the strategies don't support the assets or if - * strategy is unsupported by the vault - * @param _asset Asset to set the reserve strategy for - * @param _reserveStrategy The new reserve strategy for token - */ - function _setReserveStrategy(address _asset, address _reserveStrategy) - internal - { - // require( - // IVault(vaultAddress).isStrategySupported(_reserveStrategy), - // "Unsupported strategy" - // ); - - // require( - // IStrategy(_reserveStrategy).supportsAsset(_asset), - // "Invalid strategy for asset" - // ); - - poolTokens[_asset].reserveStrategy = _reserveStrategy; - - emit ReserveStrategyChanged(_asset, _reserveStrategy); - } - - /** - * @notice Get reserve strategy of the given asset - * @param _asset Address of the asset - * @return reserveStrategyAddr Reserve strategy address - */ - function reserveStrategy(address _asset) - external - view - onlyPoolTokens(_asset) - returns (address reserveStrategyAddr) - { - return poolTokens[_asset].reserveStrategy; - } - - /** - * @notice Change the minimum deposit threshold for the supported asset - * @param _asset Asset to set the threshold - * @param _minThreshold The new deposit threshold value - */ - function setMinDepositThreshold(address _asset, uint256 _minThreshold) - external - onlyGovernorOrStrategist - onlyPoolTokens(_asset) - { - PoolToken storage token = poolTokens[_asset]; - token.minDepositThreshold = _minThreshold; - emit MinDepositThresholdChanged(_asset, _minThreshold); - } - - function setSwapsPaused(bool _paused) external onlyGovernorOrStrategist { - swapsPaused = _paused; - emit SwapsPauseStatusChanged(_paused); - } - - function setMaxSwapSlippage(uint24 _maxSlippage) - external - onlyGovernorOrStrategist - { - maxSwapSlippage = _maxSlippage; - // emit SwapsPauseStatusChanged(_paused); - } - - // function setMinSwapPrice(address _asset, uint256 _price) external onlyGovernorOrStrategist { - // maxSwapSlippage = _maxSlippage; - // // emit SwapsPauseStatusChanged(_paused); - // } - - /*************************************** - Deposit/Withdraw - ****************************************/ - - /// @inheritdoc InitializableAbstractStrategy - function deposit(address _asset, uint256 _amount) - external - override - onlyVault - onlyPoolTokens(_asset) - nonReentrant - { - if (_amount > poolTokens[_asset].minDepositThreshold) { - IVault(vaultAddress).depositForUniswapV3(_asset, _amount); - // Not emitting Deposit event since the Reserve strategy would do so - } - } - - /// @inheritdoc InitializableAbstractStrategy - function depositAll() external override onlyVault nonReentrant { - _depositAll(); - } - - /** - * @notice Deposits all undeployed balances of the contract to the reserve strategies - */ - function _depositAll() internal { - uint256 token0Bal = IERC20(token0).balanceOf(address(this)); - uint256 token1Bal = IERC20(token1).balanceOf(address(this)); - if ( - token0Bal > 0 && token0Bal >= poolTokens[token0].minDepositThreshold - ) { - IVault(vaultAddress).depositForUniswapV3(token0, token0Bal); - } - if ( - token1Bal > 0 && token1Bal >= poolTokens[token1].minDepositThreshold - ) { - IVault(vaultAddress).depositForUniswapV3(token1, token1Bal); - } - // Not emitting Deposit events since the Reserve strategies would do so - } - - function _withdrawAssetFromCurrentPosition(address _asset, uint256 amount) - internal - { - UniswapV3StrategyLib.Position storage p = tokenIdToPosition[ - currentPositionTokenId - ]; - require(p.exists && p.liquidity > 0, "Liquidity error"); - - // Figure out liquidity to burn - ( - uint128 liquidity, - uint256 minAmount0, - uint256 minAmount1 - ) = UniswapV3StrategyLib.calculateLiquidityToWithdraw( - platformAddress, - address(uniswapV3Helper), - p, - _asset, - amount - ); - - // Liquidiate active position - UniswapV3StrategyLib.decreaseLiquidityForPosition( - platformAddress, - address(positionManager), - address(uniswapV3Helper), - p, - liquidity, - minAmount0, - minAmount1 - ); - } - - /** - * @inheritdoc InitializableAbstractStrategy - */ - function withdraw( - address recipient, - address _asset, - uint256 amount - ) external override onlyVault onlyPoolTokens(_asset) nonReentrant { - IERC20 asset = IERC20(_asset); - uint256 selfBalance = asset.balanceOf(address(this)); - - if (selfBalance < amount) { - _withdrawAssetFromCurrentPosition(_asset, amount - selfBalance); - } - - // Transfer requested amount - asset.safeTransfer(recipient, amount); - emit Withdrawal(_asset, _asset, amount); - } - - /** - * @notice Closes active LP position, if any, and transfer all token balance to Vault - * @inheritdoc InitializableAbstractStrategy - */ - function withdrawAll() external override onlyVault nonReentrant { - if (currentPositionTokenId > 0) { - // TODO: This method is only callable from Vault directly - // and by Governor or Strategist indirectly. - // Changing the Vault code to pass a minAmount0 and minAmount1 will - // make things complex. We could perhaps make sure that there're no - // active position when withdrawingAll rather than passing zero values? - _closePosition(currentPositionTokenId, 0, 0); - } - - IERC20 token0Contract = IERC20(token0); - IERC20 token1Contract = IERC20(token1); - - uint256 token0Balance = token0Contract.balanceOf(address(this)); - if (token0Balance > 0) { - token0Contract.safeTransfer(vaultAddress, token0Balance); - emit Withdrawal(token0, token0, token0Balance); - } - - uint256 token1Balance = token1Contract.balanceOf(address(this)); - if (token1Balance > 0) { - token1Contract.safeTransfer(vaultAddress, token1Balance); - emit Withdrawal(token1, token1, token1Balance); - } - } - - function _getToken1ForToken0(uint256 amount0) internal {} - - /** - * @dev Checks if there's enough balance left in the contract to provide liquidity. - * If not, tries to pull it from reserve strategies - * @param desiredAmount0 Minimum amount of token0 needed - * @param desiredAmount1 Minimum amount of token1 needed - */ - function _ensureAssetBalances( - uint256 desiredAmount0, - uint256 desiredAmount1 - ) internal { - IVault vault = IVault(vaultAddress); - - // Withdraw enough funds from Reserve strategies - uint256 token0Balance = IERC20(token0).balanceOf(address(this)); - if (token0Balance < desiredAmount0) { - vault.withdrawAssetForUniswapV3( - token0, - desiredAmount0 - token0Balance - ); - } - - uint256 token1Balance = IERC20(token1).balanceOf(address(this)); - if (token1Balance < desiredAmount1) { - vault.withdrawAssetForUniswapV3( - token1, - desiredAmount1 - token1Balance - ); - } - - // TODO: Check value of assets moved here - } - - function _ensureAssetsBySwapping( - uint256 desiredAmount0, - uint256 desiredAmount1, - uint256 swapAmountIn, - uint256 swapMinAmountOut, - uint160 sqrtPriceLimitX96, - bool swapZeroForOne - ) internal { - require(!swapsPaused, "Swaps are paused"); - IERC20 t0Contract = IERC20(token0); - IERC20 t1Contract = IERC20(token1); - - uint256 token0Balance = t0Contract.balanceOf(address(this)); - uint256 token1Balance = t1Contract.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 = IStrategy(poolTokens[token1].reserveStrategy) - .checkBalance(token1); - - // Only swap when asset isn't available in reserve as well - require( - token1Needed > 0 && 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; - - // Approve for swaps - t0Contract.safeApprove(address(swapRouter), swapAmountIn); - } else { - // Amount available in reserve strategies - uint256 t0ReserveBal = IStrategy(poolTokens[token0].reserveStrategy) - .checkBalance(token0); - - // Only swap when asset isn't available in reserve as well - require( - token0Needed > 0 && 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; - - // Approve for swaps - t1Contract.safeApprove(address(swapRouter), swapAmountIn); - } - - // TODO: Check value of token0Needed and token1Needed - - // Fund strategy from reserve strategies - if (token0Needed > 0) { - IVault(vaultAddress).withdrawAssetForUniswapV3( - token0, - token0Needed - ); - } - - if (token1Needed > 0) { - IVault(vaultAddress).withdrawAssetForUniswapV3( - token0, - token0Needed - ); - } - - // TODO: Slippage/price check - - // 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 - ); - - // TODO: Check value of assets moved here - } - - /*************************************** - Balances and Fees - ****************************************/ - - /** - * @notice Collect accumulated fees from the active position - * @dev Doesn't send to vault or harvester - */ - function collectFees() - external - onlyGovernorOrStrategistOrOperator - nonReentrant - { - if (currentPositionTokenId > 0) { - UniswapV3StrategyLib.collectFeesForToken( - address(positionManager), - currentPositionTokenId - ); - } - } - - /** - * @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) = uniswapV3Helper.positionFees( - positionManager, - platformAddress, - currentPositionTokenId - ); - } - - /** - * @dev Only checks the active LP position. - * Doesn't return the balance held in the reserve strategies. - * @inheritdoc InitializableAbstractStrategy - */ - function checkBalance(address _asset) - external - view - override - onlyPoolTokens(_asset) - returns (uint256 balance) - { - balance = IERC20(_asset).balanceOf(address(this)); - - (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(platformAddress) - .slot0(); - - if (currentPositionTokenId > 0) { - require( - tokenIdToPosition[currentPositionTokenId].exists, - "Invalid token" - ); - - (uint256 amount0, uint256 amount1) = uniswapV3Helper.positionValue( - positionManager, - platformAddress, - currentPositionTokenId, - sqrtRatioX96 - ); - - if (_asset == token0) { - balance += amount0; - } else if (_asset == token1) { - balance += amount1; - } - } - } - - /*************************************** - Pool Liquidity Management - ****************************************/ - - /** - * @notice Returns a unique ID based on lowerTick and upperTick - * @dev Basically concats the lower tick and upper tick values. Shifts the value - * of lowerTick by 24 bits and then adds the upperTick value to avoid overlaps. - * So, the result is smaller in size (int48 rather than bytes32 when using keccak256) - * @param lowerTick Lower tick index - * @param upperTick Upper tick index - * @return key A unique identifier to be used with ticksToTokenId - */ - function _getTickPositionKey(int24 lowerTick, int24 upperTick) - internal - returns (int48 key) - { - if (lowerTick > upperTick) - (lowerTick, upperTick) = (upperTick, lowerTick); - key = int48(lowerTick) * 2**24; // Shift by 24 bits - key = key + int24(upperTick); - } - - /** - * @notice Closes active LP position if any and then provides liquidity to the requested position. - * Mints new position, if it doesn't exist already. - * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them - * @param desiredAmounts Amounts of token0 and token1 to use to provide liquidity - * @param minAmounts Min amounts of token0 and token1 to deposit/expect - * @param minRedeemAmounts Min amount of token0 and token1 received from closing active position - * @param lowerTick Desired lower tick index - * @param upperTick Desired upper tick index - */ - function _rebalance( - uint256[2] calldata desiredAmounts, - uint256[2] calldata minAmounts, - uint256[2] calldata minRedeemAmounts, - int24 lowerTick, - int24 upperTick - ) internal { - require(lowerTick < upperTick, "Invalid tick range"); - - int48 tickKey = _getTickPositionKey(lowerTick, upperTick); - uint256 tokenId = ticksToTokenId[tickKey]; - - if (currentPositionTokenId > 0) { - // Close any active position - _closePosition( - currentPositionTokenId, - minRedeemAmounts[0], - minRedeemAmounts[1] - ); - } - - // Withdraw enough funds from Reserve strategies - _ensureAssetBalances(desiredAmounts[0], desiredAmounts[1]); - - // Provide liquidity - if (tokenId > 0) { - // Add liquidity to the position token - UniswapV3StrategyLib.Position storage p = tokenIdToPosition[ - tokenId - ]; - UniswapV3StrategyLib.increaseLiquidityForPosition( - address(positionManager), - p, - desiredAmounts[0], - desiredAmounts[1], - minAmounts[0], - minAmounts[1] - ); - } else { - // Mint new position - (tokenId, , , ) = _mintPosition( - desiredAmounts[0], - desiredAmounts[1], - minAmounts[0], - minAmounts[1], - lowerTick, - upperTick - ); - } - - // Mark it as active position - currentPositionTokenId = tokenId; - - // Move any leftovers to Reserve - _depositAll(); - } - - /** - * @notice Closes active LP position if any and then provides liquidity to the requested position. - * Mints new position, if it doesn't exist already. - * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them - * @param desiredAmounts Amounts of token0 and token1 to use to provide liquidity - * @param minAmounts Min amounts of token0 and token1 to deposit/expect - * @param minRedeemAmounts Min amount of token0 and token1 received from closing active position - * @param lowerTick Desired lower tick index - * @param upperTick Desired upper tick index - */ - function rebalance( - uint256[2] calldata desiredAmounts, - uint256[2] calldata minAmounts, - uint256[2] calldata minRedeemAmounts, - int24 lowerTick, - int24 upperTick - ) external onlyGovernorOrStrategistOrOperator nonReentrant { - _rebalance( - desiredAmounts, - minAmounts, - minRedeemAmounts, - lowerTick, - upperTick - ); - } - - function swapAndRebalance(SwapAndRebalanceParams calldata params) - external - // uint256[2] calldata desiredAmounts, - // uint256[2] calldata minAmounts, - // uint256[2] calldata minRedeemAmounts, - // int24 lowerTick, - // int24 upperTick, - // uint256 swapAmountIn, - // uint256 swapMinAmountOut, - // uint160 sqrtPriceLimitX96, - // bool swapZeroForOne - onlyGovernorOrStrategistOrOperator - nonReentrant - { - require(params.lowerTick < params.upperTick, "Invalid tick range"); - - uint256 tokenId = ticksToTokenId[ - _getTickPositionKey(params.lowerTick, params.upperTick) - ]; - - if (currentPositionTokenId > 0) { - // Close any active position - _closePosition( - currentPositionTokenId, - params.minRedeemAmount0, - params.minRedeemAmount1 - ); - } - - // Withdraw enough funds from Reserve strategies and swap to desired amounts - _ensureAssetsBySwapping( - params.desiredAmount0, - params.desiredAmount1, - params.swapAmountIn, - params.swapMinAmountOut, - params.sqrtPriceLimitX96, - params.swapZeroForOne - ); - - // Provide liquidity - if (tokenId > 0) { - // Add liquidity to the position token - UniswapV3StrategyLib.Position storage p = tokenIdToPosition[ - tokenId - ]; - UniswapV3StrategyLib.increaseLiquidityForPosition( - address(positionManager), - p, - params.desiredAmount0, - params.desiredAmount1, - params.minAmount0, - params.minAmount1 - ); - } else { - // Mint new position - (tokenId, , , ) = _mintPosition( - params.desiredAmount0, - params.desiredAmount1, - params.minAmount0, - params.minAmount1, - params.lowerTick, - params.upperTick - ); - } - - // Mark it as active position - currentPositionTokenId = tokenId; - - // Move any leftovers to Reserve - _depositAll(); - } - - // function swapAndRebalance( - // uint256[2] calldata desiredAmounts, - // uint256[2] calldata minAmounts, - // uint256[2] calldata minRedeemAmounts, - // int24 lowerTick, - // int24 upperTick, - // uint256 swapAmountIn, - // uint256 swapMinAmountOut, - // uint160 sqrtPriceLimitX96, - // bool swapZeroForOne - // ) external onlyGovernorOrStrategistOrOperator nonReentrant { - // _swapAndRebalance(desiredAmounts, minAmounts, minRedeemAmounts, lowerTick, upperTick, swapAmountIn, swapMinAmountOut, sqrtPriceLimitX96, swapZeroForOne); - // } - - /** - * @notice Increases liquidity of the active position. - * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them - * @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 increaseLiquidityForActivePosition( - uint256 desiredAmount0, - uint256 desiredAmount1, - uint256 minAmount0, - uint256 minAmount1 - ) external onlyGovernorOrStrategistOrOperator nonReentrant { - require(currentPositionTokenId > 0, "No active position"); - - // Withdraw enough funds from Reserve strategies - _ensureAssetBalances(desiredAmount0, desiredAmount1); - - UniswapV3StrategyLib.increaseLiquidityForPosition( - address(positionManager), - tokenIdToPosition[currentPositionTokenId], - desiredAmount0, - desiredAmount1, - minAmount0, - minAmount1 - ); - - // Deposit all dust back to reserve strategies - _depositAll(); - } - - /** - * @notice Removes all liquidity from active position and collects the fees - * @param minAmount0 Min amount of token0 to receive back - * @param minAmount1 Min amount of token1 to receive back - */ - function closeActivePosition(uint256 minAmount0, uint256 minAmount1) - external - onlyGovernorOrStrategistOrOperator - nonReentrant - { - _closePosition(currentPositionTokenId, minAmount0, minAmount1); - - // Deposit all dust back to reserve strategies - _depositAll(); - } - - /** - * @notice Removes all liquidity from specified position and collects the fees - * @dev Must be a position minted by this contract - * @param tokenId ERC721 token ID of the position to liquidate - */ - function closePosition( - uint256 tokenId, - uint256 minAmount0, - uint256 minAmount1 - ) external onlyGovernorOrStrategistOrOperator nonReentrant { - _closePosition(tokenId, minAmount0, minAmount1); - - // Deposit all dust back to reserve strategies - _depositAll(); - } - - /** - * @notice Closes the position denoted by the tokenId and and collects all fees - * @param tokenId ERC721 token ID of the position to liquidate - * @param minAmount0 Min amount of token0 to receive back - * @param minAmount1 Min amount of token1 to receive back - * @return amount0 Amount of token0 received after removing liquidity - * @return amount1 Amount of token1 received after removing liquidity - */ - function _closePosition( - uint256 tokenId, - uint256 minAmount0, - uint256 minAmount1 - ) internal returns (uint256 amount0, uint256 amount1) { - UniswapV3StrategyLib.Position storage p = tokenIdToPosition[tokenId]; - require(p.exists, "Invalid position"); - - if (p.liquidity == 0) { - return (0, 0); - } - - // Remove all liquidity - (amount0, amount1) = UniswapV3StrategyLib.decreaseLiquidityForPosition( - platformAddress, - address(positionManager), - address(uniswapV3Helper), - p, - p.liquidity, - minAmount0, - minAmount1 - ); - - if (tokenId == currentPositionTokenId) { - currentPositionTokenId = 0; - } - - emit UniswapV3PositionClosed(tokenId, amount0, amount1); - - // Collect all fees for position - (uint256 amount0Fee, uint256 amount1Fee) = UniswapV3StrategyLib - .collectFeesForToken(address(positionManager), p.tokenId); - - amount0 = amount0 + amount0Fee; - amount1 = amount1 + amount1Fee; - } - - /** - * @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 - * @param lowerTick Lower tick index - * @param upperTick Upper tick index - * - * @return tokenId ERC721 token ID of the position minted - * @return liquidity Amount of liquidity added to the pool - * @return amount0 Amount of token0 added to the position - * @return amount1 Amount of token1 added to the position - */ - function _mintPosition( - uint256 desiredAmount0, - uint256 desiredAmount1, - uint256 minAmount0, - uint256 minAmount1, - int24 lowerTick, - int24 upperTick - ) - internal - returns ( - uint256 tokenId, - uint128 liquidity, - uint256 amount0, - uint256 amount1 - ) - { - int48 tickKey = _getTickPositionKey(lowerTick, upperTick); - require(ticksToTokenId[tickKey] == 0, "Duplicate position mint"); - - INonfungiblePositionManager.MintParams - memory params = INonfungiblePositionManager.MintParams({ - token0: token0, - token1: token1, - fee: poolFee, - tickLower: lowerTick, - tickUpper: upperTick, - amount0Desired: desiredAmount0, - amount1Desired: desiredAmount1, - amount0Min: minAmount0, - amount1Min: minAmount1, - recipient: address(this), - deadline: block.timestamp - }); - - (tokenId, liquidity, amount0, amount1) = positionManager.mint(params); - - ticksToTokenId[tickKey] = tokenId; - tokenIdToPosition[tokenId] = UniswapV3StrategyLib.Position({ - exists: true, - tokenId: tokenId, - liquidity: liquidity, - lowerTick: lowerTick, - upperTick: upperTick, - sqrtRatioAX96: uniswapV3Helper.getSqrtRatioAtTick(lowerTick), - sqrtRatioBX96: uniswapV3Helper.getSqrtRatioAtTick(upperTick), - positionKey: keccak256( - abi.encodePacked(address(positionManager), lowerTick, upperTick) - ) - }); - - emit UniswapV3PositionMinted(tokenId, lowerTick, upperTick); - emit UniswapV3StrategyLib.UniswapV3LiquidityAdded( - tokenId, - amount0, - amount1, - liquidity - ); - } - - /*************************************** - ERC721 management - ****************************************/ - - /// Callback function for whenever a NFT is transferred to this contract - // solhint-disable-next-line max-line-length - /// Ref: https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#IERC721Receiver-onERC721Received-address-address-uint256-bytes- - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) external returns (bytes4) { - // TODO: Should we reject unwanted NFTs being transfered to the strategy? - // Could use `INonfungiblePositionManager.positions(tokenId)` to see if the token0 and token1 are matching - return this.onERC721Received.selector; - } - - /*************************************** - Inherited functions - ****************************************/ - - /// @inheritdoc InitializableAbstractStrategy - function safeApproveAllTokens() - external - override - onlyGovernor - nonReentrant - { - IERC20(token0).safeApprove(vaultAddress, type(uint256).max); - IERC20(token1).safeApprove(vaultAddress, type(uint256).max); - IERC20(token0).safeApprove(address(positionManager), type(uint256).max); - IERC20(token1).safeApprove(address(positionManager), type(uint256).max); - } - - /** - * Removes all allowance of both the tokens from NonfungiblePositionManager - */ - function resetAllowanceOfTokens() external onlyGovernor nonReentrant { - IERC20(token0).safeApprove(address(positionManager), 0); - IERC20(token1).safeApprove(address(positionManager), 0); - } - - /// @inheritdoc InitializableAbstractStrategy - function _abstractSetPToken(address _asset, address) internal override { - IERC20(_asset).safeApprove(vaultAddress, type(uint256).max); - IERC20(_asset).safeApprove(address(positionManager), type(uint256).max); - } - - /// @inheritdoc InitializableAbstractStrategy - function supportsAsset(address _asset) - external - view - override - returns (bool) - { - return _asset == token0 || _asset == token1; - } - - /*************************************** - Hidden functions - ****************************************/ - - function setPTokenAddress(address, address) external override onlyGovernor { - // The pool tokens can never change. - revert("Unsupported method"); - } - - function removePToken(uint256) external override onlyGovernor { - // The pool tokens can never change. - revert("Unsupported method"); - } - - /// @inheritdoc InitializableAbstractStrategy - function collectRewardTokens() - external - override - onlyHarvester - nonReentrant - { - // Do nothing - } -} diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Library.sol b/contracts/contracts/strategies/uniswap/UniswapV3Library.sol new file mode 100644 index 0000000000..f7d8e039c0 --- /dev/null +++ b/contracts/contracts/strategies/uniswap/UniswapV3Library.sol @@ -0,0 +1,52 @@ +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; +import { IVault } from "../../interfaces/IVault.sol"; + +library UniswapV3Library { + function depositAll( + address token0, + address token1, + address vaultAddress, + uint256 minDepositThreshold0, + uint256 minDepositThreshold1 + ) external { + IUniswapV3Strategy strat = IUniswapV3Strategy(msg.sender); + + uint256 token0Bal = IERC20(token0).balanceOf(address(this)); + uint256 token1Bal = IERC20(token1).balanceOf(address(this)); + IVault vault = IVault(vaultAddress); + + if ( + token0Bal > 0 && (minDepositThreshold0 == 0 || token0Bal >= minDepositThreshold0) + ) { + vault.depositToUniswapV3Reserve(token0, token0Bal); + } + if ( + token1Bal > 0 && (minDepositThreshold1 == 0 || token1Bal >= minDepositThreshold1) + ) { + vault.depositToUniswapV3Reserve(token1, token1Bal); + } + // Not emitting Deposit events since the Reserve strategies would do so + } + + // function closePosition(uint256 tokenId, uint256 minAmount0, uint256 minAmount1) external { + // (bool success, bytes memory data) = address(this).delegatecall( + // abi.encodeWithSignature("closePosition(uint256,uint256,uint256)", activeTokenId, 0, 0) + // ); + + // require(success, "Failed to close active position"); + // } + + // function withdrawAssetFromActivePosition( + // address asset, + // uint256 amount + // ) external { + // (bool success, bytes memory data) = address(this).delegatecall( + // abi.encodeWithSignature("withdrawAssetFromActivePosition(asset,uint256)", asset, amount) + // ); + + // require(success, "Failed to liquidate active position"); + // } +} \ No newline at end of file diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol new file mode 100644 index 0000000000..d1b3f32eac --- /dev/null +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -0,0 +1,781 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import { UniswapV3StrategyStorage } from "./UniswapV3StrategyStorage.sol"; +import { UniswapV3Library } from "./UniswapV3Library.sol"; + +import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; +import { IVault } from "../../interfaces/IVault.sol"; +import { IStrategy } from "../../interfaces/IStrategy.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { + using SafeERC20 for IERC20; + + /*************************************** + Deposit/Withdraw + ****************************************/ + function _depositAll() internal { + UniswapV3Library.depositAll( + token0, + token1, + vaultAddress, + poolTokens[token0].minDepositThreshold, + poolTokens[token1].minDepositThreshold + ); + } + + // TODO: Intentionally left out non-reentrant modifier since otherwise Vault would throw + function withdrawAssetFromActivePosition(address _asset, uint256 amount) external onlyVault { + Position memory position = tokenIdToPosition[activeTokenId]; + require(position.exists && position.liquidity > 0, "Liquidity error"); + + // Figure out liquidity to burn + ( + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) = _calculateLiquidityToWithdraw( + position, + _asset, + amount + ); + + // Liquidiate active position + _decreasePositionLiquidity( + position.tokenId, + liquidity, + minAmount0, + minAmount1 + ); + } + + /** + * @notice Calculates the amount liquidity that needs to be removed + * to Withdraw specified amount of the given asset. + * + * @param position Position object + * @param _asset Token needed + * @param amount Minimum amount to liquidate + * + * @return liquidity Liquidity to burn + * @return minAmount0 Minimum amount0 to expect + * @return minAmount1 Minimum amount1 to expect + */ + function _calculateLiquidityToWithdraw( + Position memory position, + address _asset, + uint256 amount + ) + internal + view + returns ( + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) + { + (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); + + // Total amount in Liquidity pools + (uint256 totalAmount0, uint256 totalAmount1) = helper + .getAmountsForLiquidity( + sqrtRatioX96, + position.sqrtRatioAX96, + position.sqrtRatioBX96, + position.liquidity + ); + + if (_asset == token0) { + minAmount0 = amount; + minAmount1 = totalAmount1 / (totalAmount0 / amount); + liquidity = helper.getLiquidityForAmounts( + sqrtRatioX96, + position.sqrtRatioAX96, + position.sqrtRatioBX96, + amount, + minAmount1 + ); + } else if (_asset == token1) { + minAmount0 = totalAmount0 / (totalAmount1 / amount); + minAmount1 = amount; + liquidity = helper.getLiquidityForAmounts( + sqrtRatioX96, + position.sqrtRatioAX96, + position.sqrtRatioBX96, + minAmount0, + amount + ); + } + } + + /*************************************** + Rebalance + ****************************************/ + /** + * @notice Closes active LP position if any and then provides liquidity to the requested position. + * Mints new position, if it doesn't exist already. + * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them + * @param desiredAmount0 Amount of token0 to use to provide liquidity + * @param desiredAmount1 Amount of token1 to use to provide liquidity + * @param minAmount0 Min amount of token0 to deposit/expect + * @param minAmount1 Min amount of token1 to deposit/expect + * @param minRedeemAmount0 Min amount of token0 received from closing active position + * @param minRedeemAmount1 Min amount of token1 received from closing active position + * @param lowerTick Desired lower tick index + * @param upperTick Desired upper tick index + */ + function rebalance( + uint256 desiredAmount0, + uint256 desiredAmount1, + uint256 minAmount0, + uint256 minAmount1, + uint256 minRedeemAmount0, + uint256 minRedeemAmount1, + int24 lowerTick, + int24 upperTick + ) external onlyGovernorOrStrategistOrOperator nonReentrant { + require(lowerTick < upperTick, "Invalid tick range"); + + int48 tickKey = _getTickPositionKey(lowerTick, upperTick); + uint256 tokenId = ticksToTokenId[tickKey]; + + if (activeTokenId > 0) { + // Close any active position + _closePosition( + activeTokenId, + minRedeemAmount0, + minRedeemAmount1 + ); + } + + // Withdraw enough funds from Reserve strategies + _ensureAssetBalances(desiredAmount0, desiredAmount1); + + // Provide liquidity + if (tokenId > 0) { + // Add liquidity to the position token + _increasePositionLiquidity(tokenId, desiredAmount0, desiredAmount1, minAmount0, minAmount1); + } else { + // Mint new position + (tokenId, , , ) = _mintPosition( + desiredAmount0, + desiredAmount1, + minAmount0, + minAmount1, + lowerTick, + upperTick + ); + } + + // Mark it as active position + activeTokenId = tokenId; + + // Move any leftovers to Reserve + _depositAll(); + } + + struct SwapAndRebalanceParams { + uint256 desiredAmount0; + uint256 desiredAmount1; + uint256 minAmount0; + uint256 minAmount1; + uint256 minRedeemAmount0; + uint256 minRedeemAmount1; + int24 lowerTick; + int24 upperTick; + uint256 swapAmountIn; + uint256 swapMinAmountOut; + uint160 sqrtPriceLimitX96; + bool swapZeroForOne; + } + function swapAndRebalance(SwapAndRebalanceParams calldata params) + external + onlyGovernorOrStrategistOrOperator + nonReentrant + { + require(params.lowerTick < params.upperTick, "Invalid tick range"); + + uint256 tokenId = ticksToTokenId[ + _getTickPositionKey(params.lowerTick, params.upperTick) + ]; + + if (activeTokenId > 0) { + // Close any active position + _closePosition( + activeTokenId, + params.minRedeemAmount0, + params.minRedeemAmount1 + ); + } + + // Withdraw enough funds from Reserve strategies and swap to desired amounts + _ensureAssetsBySwapping( + params.desiredAmount0, + params.desiredAmount1, + params.swapAmountIn, + params.swapMinAmountOut, + params.sqrtPriceLimitX96, + params.swapZeroForOne + ); + + // Provide liquidity + if (tokenId > 0) { + // Add liquidity to the position token + _increasePositionLiquidity(tokenId, params.desiredAmount0, params.desiredAmount1, params.minAmount0, params.minAmount1); + } else { + // Mint new position + (tokenId, , , ) = _mintPosition( + params.desiredAmount0, + params.desiredAmount1, + params.minAmount0, + params.minAmount1, + params.lowerTick, + params.upperTick + ); + } + + // Mark it as active position + activeTokenId = tokenId; + + // Move any leftovers to Reserve + _depositAll(); + } + + + /*************************************** + Pool Liquidity Management + ****************************************/ + /** + * @notice Returns a unique ID based on lowerTick and upperTick + * @dev Basically concats the lower tick and upper tick values. Shifts the value + * of lowerTick by 24 bits and then adds the upperTick value to avoid overlaps. + * So, the result is smaller in size (int48 rather than bytes32 when using keccak256) + * @param lowerTick Lower tick index + * @param upperTick Upper tick index + * @return key A unique identifier to be used with ticksToTokenId + */ + function _getTickPositionKey(int24 lowerTick, int24 upperTick) + internal + returns (int48 key) + { + if (lowerTick > upperTick) + (lowerTick, upperTick) = (upperTick, lowerTick); + key = int48(lowerTick) * 2**24; // Shift by 24 bits + key = key + int24(upperTick); + } + + + /** + * @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 + * @param lowerTick Lower tick index + * @param upperTick Upper tick index + * + * @return tokenId ERC721 token ID of the position minted + * @return liquidity Amount of liquidity added to the pool + * @return amount0 Amount of token0 added to the position + * @return amount1 Amount of token1 added to the position + */ + function _mintPosition( + uint256 desiredAmount0, + uint256 desiredAmount1, + uint256 minAmount0, + uint256 minAmount1, + int24 lowerTick, + int24 upperTick + ) + internal + returns ( + uint256 tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { + int48 tickKey = _getTickPositionKey(lowerTick, upperTick); + require(ticksToTokenId[tickKey] == 0, "Duplicate position mint"); + + INonfungiblePositionManager.MintParams + memory params = INonfungiblePositionManager.MintParams({ + token0: token0, + token1: token1, + fee: poolFee, + tickLower: lowerTick, + tickUpper: upperTick, + amount0Desired: desiredAmount0, + amount1Desired: desiredAmount1, + amount0Min: minAmount0, + amount1Min: minAmount1, + recipient: address(this), + deadline: block.timestamp + }); + + (tokenId, liquidity, amount0, amount1) = positionManager.mint(params); + + ticksToTokenId[tickKey] = tokenId; + tokenIdToPosition[tokenId] = Position({ + exists: true, + tokenId: tokenId, + liquidity: liquidity, + lowerTick: lowerTick, + upperTick: upperTick, + sqrtRatioAX96: helper.getSqrtRatioAtTick(lowerTick), + sqrtRatioBX96: helper.getSqrtRatioAtTick(upperTick) + }); + + emit UniswapV3PositionMinted(tokenId, lowerTick, upperTick); + emit UniswapV3LiquidityAdded( + tokenId, + amount0, + amount1, + liquidity + ); + } + + /** + * @notice Increases liquidity of the active position. + * @dev Will pull funds needed from reserve strategies + * @param tokenId Position NFT's tokenId + * @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 _increasePositionLiquidity( + uint256 tokenId, + uint256 desiredAmount0, + uint256 desiredAmount1, + uint256 minAmount0, + uint256 minAmount1 + ) + internal + returns ( + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { + Position storage position = tokenIdToPosition[tokenId]; + require(position.exists, "No active position"); + + // Withdraw enough funds from Reserve strategies + _ensureAssetBalances(desiredAmount0, desiredAmount1); + + INonfungiblePositionManager.IncreaseLiquidityParams + memory params = INonfungiblePositionManager + .IncreaseLiquidityParams({ + tokenId: position.tokenId, + amount0Desired: desiredAmount0, + amount1Desired: desiredAmount1, + amount0Min: minAmount0, + amount1Min: minAmount1, + deadline: block.timestamp + }); + + (liquidity, amount0, amount1) = INonfungiblePositionManager( + positionManager + ).increaseLiquidity(params); + + position.liquidity += liquidity; + + emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); + } + + function increaseActivePositionLiquidity( + uint256 desiredAmount0, + uint256 desiredAmount1, + uint256 minAmount0, + uint256 minAmount1 + ) external onlyGovernorOrStrategistOrOperator nonReentrant returns (uint256 amount0, uint256 amount1) { + _increasePositionLiquidity(activeTokenId, desiredAmount0, desiredAmount1, minAmount0, minAmount1); + } + + /** + * @notice Removes liquidity of the position in the pool + * + * @dev Scope intentionally set to public so that the base strategy can delegatecall this function. + * Setting it to external would restrict other functions in this contract from using it + * + * @param tokenId Position NFT's tokenId + * @param liquidity Amount of liquidity to remove form the position + * @param minAmount0 Min amount of token0 to withdraw + * @param minAmount1 Min amount of token1 to withdraw + * + * @return amount0 Amount of token0 received after liquidation + * @return amount1 Amount of token1 received after liquidation + */ + function _decreasePositionLiquidity( + uint256 tokenId, + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) internal + returns (uint256 amount0, uint256 amount1) + { + Position storage position = tokenIdToPosition[ + tokenId + ]; + require(position.exists, "Unknown position"); + + (uint160 sqrtRatioX96, , , , , , ) = pool + .slot0(); + (uint256 exactAmount0, uint256 exactAmount1) = helper.getAmountsForLiquidity( + sqrtRatioX96, + position.sqrtRatioAX96, + position.sqrtRatioBX96, + liquidity + ); + + INonfungiblePositionManager.DecreaseLiquidityParams + memory params = INonfungiblePositionManager + .DecreaseLiquidityParams({ + tokenId: position.tokenId, + liquidity: liquidity, + amount0Min: minAmount0, + amount1Min: minAmount1, + deadline: block.timestamp + }); + + (amount0, amount1) = positionManager + .decreaseLiquidity(params); + + position.liquidity -= liquidity; + + emit UniswapV3LiquidityRemoved(position.tokenId, amount0, amount1, liquidity); + } + + function decreaseActivePositionLiquidity( + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) external onlyGovernorOrStrategistOrOperator nonReentrant returns (uint256 amount0, uint256 amount1) { + _decreasePositionLiquidity(activeTokenId, liquidity, minAmount0, minAmount1); + } + + + /** + * @notice Closes the position denoted by the tokenId and and collects all fees + * @param tokenId Position NFT's tokenId + * @param minAmount0 Min amount of token0 to receive back + * @param minAmount1 Min amount of token1 to receive back + * @return amount0 Amount of token0 received after removing liquidity + * @return amount1 Amount of token1 received after removing liquidity + */ + function _closePosition( + uint256 tokenId, + uint256 minAmount0, + uint256 minAmount1 + ) internal returns (uint256 amount0, uint256 amount1) { + Position memory position = tokenIdToPosition[ + tokenId + ]; + require(position.exists, "Invalid position"); + + if (position.liquidity == 0) { + return (0, 0); + } + + // Remove all liquidity + (amount0, amount1) = _decreasePositionLiquidity( + tokenId, + position.liquidity, + minAmount0, + minAmount1 + ); + + if (position.tokenId == activeTokenId) { + activeTokenId = 0; + } + + emit UniswapV3PositionClosed(position.tokenId, amount0, amount1); + + // Collect all fees for position + (uint256 amount0Fee, uint256 amount1Fee) = _collectFeesForToken( + position.tokenId + ); + + amount0 = amount0 + amount0Fee; + amount1 = amount1 + amount1Fee; + } + + /** + * @notice Closes the position denoted by the tokenId and and collects all fees + * @param tokenId Token ID of the position to collect fees of. + * @param minAmount0 Min amount of token0 to receive back + * @param minAmount1 Min amount of token1 to receive back + * @return amount0 Amount of token0 received after removing liquidity + * @return amount1 Amount of token1 received after removing liquidity + */ + function closePosition( + uint256 tokenId, + uint256 minAmount0, + uint256 minAmount1 + ) external onlyGovernorOrStrategistOrOperator nonReentrant returns (uint256 amount0, uint256 amount1) { + return _closePosition(tokenId, minAmount0, minAmount1); + } + + /** + * @notice Same as closePosition() but only callable by Vault and doesn't have non-reentrant + * @param tokenId Token ID of the position to collect fees of. + * @param minAmount0 Min amount of token0 to receive back + * @param minAmount1 Min amount of token1 to receive back + * @return amount0 Amount of token0 received after removing liquidity + * @return amount1 Amount of token1 received after removing liquidity + */ + function closePositionOnlyVault( + uint256 tokenId, + uint256 minAmount0, + uint256 minAmount1 + ) external onlyVault returns (uint256 amount0, uint256 amount1) { + return _closePosition(tokenId, minAmount0, minAmount1); + } + + /*************************************** + Balances and Fees + ****************************************/ + /** + * @dev Checks if there's enough balance left in the contract to provide liquidity. + * If not, tries to pull it from reserve strategies + * @param desiredAmount0 Minimum amount of token0 needed + * @param desiredAmount1 Minimum amount of token1 needed + */ + function _ensureAssetBalances( + uint256 desiredAmount0, + uint256 desiredAmount1 + ) internal { + IVault vault = IVault(vaultAddress); + + // Withdraw enough funds from Reserve strategies + uint256 token0Balance = IERC20(token0).balanceOf(address(this)); + if (token0Balance < desiredAmount0) { + vault.withdrawFromUniswapV3Reserve( + token0, + desiredAmount0 - token0Balance + ); + } + + uint256 token1Balance = IERC20(token1).balanceOf(address(this)); + if (token1Balance < desiredAmount1) { + vault.withdrawFromUniswapV3Reserve( + token1, + desiredAmount1 - token1Balance + ); + } + + // TODO: Check value of assets moved here + } + + function _checkSwapLimits( + uint160 sqrtPriceLimitX96, + bool swapZeroForOne + ) internal { + require(!swapsPaused, "Swaps are paused"); + + (uint160 currentPriceX96, , , , , , ) = pool.slot0(); + + if (minSwapPriceX96 > 0 || maxSwapPriceX96 > 0) { + require( + minSwapPriceX96 <= currentPriceX96 && currentPriceX96 <= maxSwapPriceX96, + "Price out of bounds" + ); + + require( + swapZeroForOne ? (sqrtPriceLimitX96 >= minSwapPriceX96) : (sqrtPriceLimitX96 <= maxSwapPriceX96), + "Slippage out of bounds" + ); + } + } + + function _ensureAssetsBySwapping( + uint256 desiredAmount0, + uint256 desiredAmount1, + uint256 swapAmountIn, + uint256 swapMinAmountOut, + uint160 sqrtPriceLimitX96, + bool swapZeroForOne + ) internal { + _checkSwapLimits(sqrtPriceLimitX96, swapZeroForOne); + + IERC20 t0Contract = IERC20(token0); + IERC20 t1Contract = IERC20(token1); + + uint256 token0Balance = t0Contract.balanceOf(address(this)); + uint256 token1Balance = t1Contract.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 = IStrategy(poolTokens[token1].reserveStrategy) + .checkBalance(token1); + + // Only swap when asset isn't available in reserve as well + require( + token1Needed > 0 && 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); + + // Approve for swaps + t0Contract.safeApprove(address(swapRouter), swapAmountIn); + } else { + // Amount available in reserve strategies + uint256 t0ReserveBal = IStrategy(poolTokens[token0].reserveStrategy) + .checkBalance(token0); + + // Only swap when asset isn't available in reserve as well + require( + token0Needed > 0 && 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 + // Subtract token1 that we will get from swapping + token0Needed = (swapMinAmountOut >= token0Needed) ? 0 : (token0Needed - swapMinAmountOut); + + // Approve for swaps + t1Contract.safeApprove(address(swapRouter), swapAmountIn); + } + + // 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 + ); + + // TODO: Check value of assets moved here + } + + function collectFees() external onlyGovernorOrStrategistOrOperator returns (uint256 amount0, uint256 amount1) { + return _collectFeesForToken(activeTokenId); + } + + /** + * @notice Collects the fees generated by the position on V3 pool + * @param tokenId Token ID of the position to collect fees of. + * @return amount0 Amount of token0 collected as fee + * @return amount1 Amount of token1 collected as fee + */ + function _collectFeesForToken(uint256 tokenId) + internal + returns (uint256 amount0, uint256 amount1) + { + require(tokenIdToPosition[tokenId].exists, "Invalid position"); + INonfungiblePositionManager.CollectParams + memory params = INonfungiblePositionManager.CollectParams({ + tokenId: tokenId, + recipient: address(this), + amount0Max: type(uint128).max, + amount1Max: type(uint128).max + }); + + (amount0, amount1) = positionManager.collect(params); + + emit UniswapV3FeeCollected(tokenId, amount0, amount1); + } + + /*************************************** + Hidden functions + ****************************************/ + function _abstractSetPToken(address _asset, address _pToken) internal override { + revert("NO_IMPL"); + } + + function safeApproveAllTokens() external override virtual { + revert("NO_IMPL"); + } + function deposit(address _asset, uint256 _amount) external override virtual { + revert("NO_IMPL"); + } + + function depositAll() external override virtual { + revert("NO_IMPL"); + } + + function withdraw( + address _recipient, + address _asset, + uint256 amount + ) external override virtual { + revert("NO_IMPL"); + } + + function withdrawAll() external override virtual { + revert("NO_IMPL"); + } + + function checkBalance(address _asset) external view override + returns (uint256 balance) + { + revert("NO_IMPL"); + } + + function supportsAsset(address _asset) external view override returns (bool) { + revert("NO_IMPL"); + } + + function setPTokenAddress(address, address) external override onlyGovernor { + revert("NO_IMPL"); + } + + function removePToken(uint256) external override onlyGovernor { + revert("NO_IMPL"); + } + + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + revert("NO_IMPL"); + } + +} diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol new file mode 100644 index 0000000000..4fb9e55207 --- /dev/null +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -0,0 +1,476 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { UniswapV3StrategyStorage } from "./UniswapV3StrategyStorage.sol"; +import { UniswapV3Library } from "./UniswapV3Library.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { IStrategy } from "../../interfaces/IStrategy.sol"; +import { IVault } from "../../interfaces/IVault.sol"; + +import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; +import { IUniswapV3Helper } from "../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; +import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; + +contract UniswapV3Strategy is UniswapV3StrategyStorage { + using SafeERC20 for IERC20; + + /** + * @dev Initialize the contract + * @param _vaultAddress OUSD Vault + * @param _poolAddress Uniswap V3 Pool + * @param _nonfungiblePositionManager Uniswap V3's Position Manager + * @param _helper Deployed UniswapV3Helper contract + * @param _swapRouter Uniswap SwapRouter contract + * @param _operator Operator address + */ + function initialize( + address _vaultAddress, + address _poolAddress, + address _nonfungiblePositionManager, + address _helper, + address _swapRouter, + address _operator + ) external onlyGovernor initializer { + // TODO: Comment on why this is necessary and why it should always be the proxy address + _self = IUniswapV3Strategy(address(this)); + + pool = IUniswapV3Pool(_poolAddress); + helper = IUniswapV3Helper(_helper); + swapRouter = ISwapRouter(_swapRouter); + positionManager = INonfungiblePositionManager( + _nonfungiblePositionManager + ); + + token0 = pool.token0(); + token1 = pool.token1(); + poolFee = pool.fee(); + + address[] memory _assets = new address[](2); + _assets[0] = token0; + _assets[1] = token1; + + super._initialize( + _poolAddress, + _vaultAddress, + new address[](0), // No Reward tokens + _assets, // Asset addresses + _assets // Platform token addresses + ); + + poolTokens[token0] = PoolToken({ + isSupported: true, + reserveStrategy: address(0), // Set using `setReserveStrategy()` + minDepositThreshold: 0 + }); + + poolTokens[token1] = PoolToken({ + isSupported: true, + reserveStrategy: address(0), // Set using `setReserveStrategy() + minDepositThreshold: 0 + }); + + _setOperator(_operator); + } + + /*************************************** + Admin Utils + ****************************************/ + + /** + * @notice Change the address of the operator + * @dev Can only be called by the Governor or Strategist + * @param _operator The new value to be set + */ + function setOperator(address _operator) external onlyGovernorOrStrategist { + _setOperator(_operator); + } + + function _setOperator(address _operator) internal { + operatorAddr = _operator; + emit OperatorChanged(_operator); + } + + /** + * @notice Change the reserve strategy of the supported asset + * @dev Will throw if the strategies don't support the assets or if + * strategy is unsupported by the vault + * @param _asset Asset to set the reserve strategy for + * @param _reserveStrategy The new reserve strategy for token + */ + function setReserveStrategy(address _asset, address _reserveStrategy) + external + onlyGovernorOrStrategist + nonReentrant + { + require( + IVault(vaultAddress).isStrategySupported(_reserveStrategy), + "Unsupported strategy" + ); + + require( + IStrategy(_reserveStrategy).supportsAsset(_asset), + "Invalid strategy for asset" + ); + + poolTokens[_asset].reserveStrategy = _reserveStrategy; + + emit ReserveStrategyChanged(_asset, _reserveStrategy); + } + + /** + * @notice Get reserve strategy of the given asset + * @param _asset Address of the asset + * @return reserveStrategyAddr Reserve strategy address + */ + function reserveStrategy(address _asset) + external + view + onlyPoolTokens(_asset) + returns (address reserveStrategyAddr) + { + reserveStrategyAddr = poolTokens[_asset].reserveStrategy; + } + + /** + * @notice Change the minimum deposit threshold for the supported asset + * @param _asset Asset to set the threshold + * @param _minThreshold The new deposit threshold value + */ + function setMinDepositThreshold(address _asset, uint256 _minThreshold) + external + onlyGovernorOrStrategist + onlyPoolTokens(_asset) + { + PoolToken storage token = poolTokens[_asset]; + token.minDepositThreshold = _minThreshold; + emit MinDepositThresholdChanged(_asset, _minThreshold); + } + + function setSwapsPaused(bool _paused) external onlyGovernorOrStrategist { + swapsPaused = _paused; + emit SwapsPauseStatusChanged(_paused); + } + + function setMaxSwapSlippage(uint24 _maxSlippage) + external + onlyGovernorOrStrategist + { + maxSwapSlippage = _maxSlippage; + emit MaxSwapSlippageChanged(_maxSlippage); + } + + /** + * @notice Change the swap price threshold + * @param minTick Minimum price tick index + * @param maxTick Maximum price tick index + */ + function setSwapPriceThreshold(int24 minTick, int24 maxTick) external onlyGovernorOrStrategist { + require((minTick < maxTick) || (minTick == 0 && maxTick == 0), "Invalid threshold"); + minSwapPriceX96 = helper.getSqrtRatioAtTick(minTick); + maxSwapPriceX96 = helper.getSqrtRatioAtTick(maxTick); + emit SwapPriceThresholdChanged(minTick, minSwapPriceX96, maxTick, maxSwapPriceX96); + } + + /*************************************** + Deposit/Withdraw + ****************************************/ + + /// @inheritdoc InitializableAbstractStrategy + function deposit(address _asset, uint256 _amount) + external + override + onlyVault + onlyPoolTokens(_asset) + nonReentrant + { + if (_amount > poolTokens[_asset].minDepositThreshold) { + IVault(vaultAddress).depositToUniswapV3Reserve(_asset, _amount); + // Not emitting Deposit event since the Reserve strategy would do so + } + } + + /// @inheritdoc InitializableAbstractStrategy + function depositAll() external override onlyVault nonReentrant { + _depositAll(); + } + + /** + * @notice Deposits all undeployed balances of the contract to the reserve strategies + */ + function _depositAll() internal { + UniswapV3Library.depositAll(token0, token1, vaultAddress, poolTokens[token0].minDepositThreshold, poolTokens[token1].minDepositThreshold); + } + + /** + * @inheritdoc InitializableAbstractStrategy + */ + function withdraw( + address recipient, + address _asset, + uint256 amount + ) external override onlyVault onlyPoolTokens(_asset) nonReentrant { + IERC20 asset = IERC20(_asset); + uint256 selfBalance = asset.balanceOf(address(this)); + + if (selfBalance < amount) { + (bool success, bytes memory data) = address(this).delegatecall( + abi.encodeWithSignature("withdrawAssetFromActivePosition(asset,uint256)", _asset, amount - selfBalance) + ); + + require(success, "Failed to liquidate active position"); + } + + // Transfer requested amount + asset.safeTransfer(recipient, amount); + emit Withdrawal(_asset, _asset, amount); + } + + /** + * @notice Closes active LP position, if any, and transfer all token balance to Vault + * @inheritdoc InitializableAbstractStrategy + */ + function withdrawAll() external override onlyVault nonReentrant { + if (activeTokenId > 0) { + // TODO: This method is only callable from Vault directly + // and by Governor or Strategist indirectly. + // Changing the Vault code to pass a minAmount0 and minAmount1 will + // make things complex. We could perhaps make sure that there're no + // active position when withdrawingAll rather than passing zero values? + + (bool success, bytes memory data) = address(this).delegatecall( + abi.encodeWithSignature("closePositionOnlyVault(uint256,uint256,uint256)", activeTokenId, 0, 0) + ); + + require(success, "Failed to close active position"); + } + + IERC20 token0Contract = IERC20(token0); + IERC20 token1Contract = IERC20(token1); + + uint256 token0Balance = token0Contract.balanceOf(address(this)); + if (token0Balance > 0) { + token0Contract.safeTransfer(vaultAddress, token0Balance); + emit Withdrawal(token0, token0, token0Balance); + } + + uint256 token1Balance = token1Contract.balanceOf(address(this)); + if (token1Balance > 0) { + token1Contract.safeTransfer(vaultAddress, token1Balance); + emit Withdrawal(token1, token1, token1Balance); + } + } + + /*************************************** + Balances and Fees + ****************************************/ + /** + * @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) + { + if (activeTokenId > 0) { + (amount0, amount1) = helper.positionFees( + positionManager, + address(pool), + activeTokenId + ); + } + } + + /** + * @dev Only checks the active LP position. + * Doesn't return the balance held in the reserve strategies. + * @inheritdoc InitializableAbstractStrategy + */ + function checkBalance(address _asset) + external + view + override + onlyPoolTokens(_asset) + returns (uint256 balance) + { + balance = IERC20(_asset).balanceOf(address(this)); + + (uint160 sqrtRatioX96, , , , , , ) = pool + .slot0(); + + if (activeTokenId > 0) { + require( + tokenIdToPosition[activeTokenId].exists, + "Invalid token" + ); + + (uint256 amount0, uint256 amount1) = helper.positionValue( + positionManager, + address(pool), + activeTokenId, + sqrtRatioX96 + ); + + if (_asset == token0) { + balance += amount0; + } else if (_asset == token1) { + balance += amount1; + } + } + } + + /*************************************** + ERC721 management + ****************************************/ + + /// Callback function for whenever a NFT is transferred to this contract + // solhint-disable-next-line max-line-length + /// Ref: https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#IERC721Receiver-onERC721Received-address-address-uint256-bytes- + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external returns (bytes4) { + // TODO: Should we reject unwanted NFTs being transfered to the strategy? + // Could use `INonfungiblePositionManager.positions(tokenId)` to see if the token0 and token1 are matching + return this.onERC721Received.selector; + } + + /*************************************** + Inherited functions + ****************************************/ + + /// @inheritdoc InitializableAbstractStrategy + function safeApproveAllTokens() + external + override + onlyGovernor + nonReentrant + { + IERC20(token0).safeApprove(vaultAddress, type(uint256).max); + IERC20(token1).safeApprove(vaultAddress, type(uint256).max); + // TODO: Should approval be done only during minting/increasing liquidity? + IERC20(token0).safeApprove(address(positionManager), type(uint256).max); + IERC20(token1).safeApprove(address(positionManager), type(uint256).max); + } + + /** + * Removes all allowance of both the tokens from NonfungiblePositionManager + */ + function resetAllowanceOfTokens() external onlyGovernor nonReentrant { + IERC20(token0).safeApprove(address(positionManager), 0); + IERC20(token1).safeApprove(address(positionManager), 0); + } + + /// @inheritdoc InitializableAbstractStrategy + function _abstractSetPToken(address _asset, address) internal override { + IERC20(_asset).safeApprove(vaultAddress, type(uint256).max); + // TODO: Should approval be done only during minting/increasing liquidity? + IERC20(_asset).safeApprove(address(positionManager), type(uint256).max); + } + + /// @inheritdoc InitializableAbstractStrategy + function supportsAsset(address _asset) + external + view + override + returns (bool) + { + return _asset == token0 || _asset == token1; + } + + /*************************************** + Hidden functions + ****************************************/ + + function setPTokenAddress(address, address) external override onlyGovernor { + // The pool tokens can never change. + revert("Unsupported method"); + } + + function removePToken(uint256) external override onlyGovernor { + // The pool tokens can never change. + revert("Unsupported method"); + } + + /// @inheritdoc InitializableAbstractStrategy + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + // Do nothing + } + + /*************************************** + Proxy to liquidity management + ****************************************/ + /** + * @dev Sets the implementation for the liquidity manager + * @param newImpl address of the implementation + */ + function setLiquidityManagerImpl(address newImpl) external onlyGovernor { + _setLiquidityManagerImpl(newImpl); + } + + function _setLiquidityManagerImpl(address newImpl) internal { + require( + Address.isContract(newImpl), + "new implementation is not a contract" + ); + bytes32 position = liquidityManagerImplPosition; + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(position, newImpl) + } + } + + /** + * @dev Falldown to the liquidity manager implementation + * @notice This is a catch all for all functions not declared here + */ + // solhint-disable-next-line no-complex-fallback + fallback() external payable { + bytes32 slot = liquidityManagerImplPosition; + // solhint-disable-next-line no-inline-assembly + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall( + gas(), + sload(slot), + 0, + calldatasize(), + 0, + 0 + ) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } +} diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol new file mode 100644 index 0000000000..914feb439e --- /dev/null +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { IStrategy } from "../../interfaces/IStrategy.sol"; +import { IVault } from "../../interfaces/IVault.sol"; + +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; +import { IUniswapV3Helper } from "../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; +import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; +import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; + +abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { + event OperatorChanged(address _address); + event ReserveStrategyChanged(address asset, address reserveStrategy); + event MinDepositThresholdChanged( + address asset, + uint256 minDepositThreshold + ); + event SwapsPauseStatusChanged(bool paused); + event SwapPriceThresholdChanged(int24 minTick, uint160 minSwapPriceX96, int24 maxTick, uint160 maxSwapPriceX96); + event MaxSwapSlippageChanged(uint24 maxSlippage); + event AssetSwappedForRebalancing( + address indexed tokenIn, + address indexed tokenOut, + uint256 amountIn, + uint256 amountOut + ); + 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 + ); + event UniswapV3PositionClosed( + uint256 indexed tokenId, + uint256 amount0Received, + uint256 amount1Received + ); + event UniswapV3FeeCollected( + uint256 indexed tokenId, + uint256 amount0, + uint256 amount1 + ); + + // Represents both tokens supported by the strategy + struct PoolToken { + // True if asset is either token0 or token1 + bool isSupported; + // When the funds are not deployed in Uniswap V3 Pool, they will + // be deposited to these reserve strategies + address reserveStrategy; + // Deposits to reserve strategy when contract balance exceeds this amount + uint256 minDepositThreshold; + } + + // Represents a position minted by UniswapV3Strategy contract + struct Position { + uint256 tokenId; // ERC721 token Id of the minted position + uint128 liquidity; // Amount of liquidity deployed + int24 lowerTick; // Lower tick index + int24 upperTick; // Upper tick index + bool exists; // True, if position is minted + + // 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; + } + + // Set to the proxy address when initialized + IUniswapV3Strategy public _self; + + // The address that can manage the positions on Uniswap V3 + address public operatorAddr; + address public token0; // Token0 of Uniswap V3 Pool + address public token1; // Token1 of Uniswap V3 Pool + + uint24 public poolFee; // Uniswap V3 Pool Fee + uint24 public maxSwapSlippage = 100; // 1%; Reverts if swap slippage is higher than this + bool public swapsPaused = false; // True if Swaps are paused + + uint256 public maxTVL; // In USD, 18 decimals + + uint160 public minSwapPriceX96; + uint160 public maxSwapPriceX96; + + uint256 public activeTokenId; + + // Uniswap V3's PositionManager + IUniswapV3Pool public pool; + + // Uniswap V3's PositionManager + INonfungiblePositionManager public positionManager; + + // A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch + IUniswapV3Helper internal helper; + + ISwapRouter internal swapRouter; + + mapping(address => PoolToken) public poolTokens; + + // A lookup table to find token IDs of position using f(lowerTick, upperTick) + mapping(int48 => uint256) internal ticksToTokenId; + + // Maps tokenIDs to their Position object + mapping(uint256 => Position) public tokenIdToPosition; + + + // keccak256("OUSD.UniswapV3Strategy.LiquidityManager.impl") + bytes32 constant liquidityManagerImplPosition = + 0xec676d52175f7cbb4e4ea392c6b70f8946575021aad20479602b98adc56ad62d; + + // Future-proofing + uint256[100] private __gap; + + /*************************************** + Modifiers + ****************************************/ + + /** + * @dev Ensures that the caller is Governor or Strategist. + */ + modifier onlyGovernorOrStrategist() { + require( + msg.sender == IVault(vaultAddress).strategistAddr() || + msg.sender == governor(), + "Caller is not the Strategist or Governor" + ); + _; + } + + /** + * @dev Ensures that the caller is Governor, Strategist or Operator. + */ + modifier onlyGovernorOrStrategistOrOperator() { + require( + msg.sender == operatorAddr || + msg.sender == IVault(vaultAddress).strategistAddr() || + msg.sender == governor(), + "Caller is not the Operator, Strategist or Governor" + ); + _; + } + + /** + * @dev Ensures that the caller is Governor, Strategist or Operator. + */ + modifier onlyGovernorOrStrategistOrOperatorOrVault() { + require( + msg.sender == operatorAddr || + msg.sender == IVault(vaultAddress).strategistAddr() || + msg.sender == governor(), + "Caller is not the Operator, Strategist, Governor or Vault" + ); + _; + } + + /** + * @dev Ensures that the asset address is either token0 or token1. + */ + modifier onlyPoolTokens(address addr) { + require(addr == token0 || addr == token1, "Unsupported asset"); + _; + } + + /** + * @dev Ensures that the caller is the proxy. + */ + modifier internalProxiedFunction() { + require(msg.sender == address(_self), "Not self"); + _; + } +} diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index d86d30dcc1..d42accaaf9 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -555,12 +555,12 @@ contract VaultAdmin is VaultStorage { * @param asset The asset to deposit * @param amount Amount of tokens to deposit */ - function depositForUniswapV3(address asset, uint256 amount) + function depositToUniswapV3Reserve(address asset, uint256 amount) external onlyUniswapV3Strategies nonReentrant { - _depositForUniswapV3(msg.sender, asset, amount); + _depositToUniswapV3Reserve(msg.sender, asset, amount); } /** @@ -569,7 +569,7 @@ contract VaultAdmin is VaultStorage { * @param asset The asset to deposit * @param amount Amount of tokens to deposit */ - function _depositForUniswapV3( + function _depositToUniswapV3Reserve( address v3Strategy, address asset, uint256 amount @@ -578,54 +578,20 @@ contract VaultAdmin is VaultStorage { address reserveStrategy = IUniswapV3Strategy(v3Strategy) .reserveStrategy(asset); require( - reserveStrategy != address(0), - "Invalid Reserve Strategy address" + strategies[reserveStrategy].isSupported, + "Unknown reserve strategy" ); IERC20(asset).safeTransferFrom(v3Strategy, reserveStrategy, amount); IStrategy(reserveStrategy).deposit(asset, amount); } - /** - * @notice Moves tokens from reserve strategy to the recipient - * @dev Only callable by whitelisted Uniswap V3 strategies - * @param recipient Receiver of the funds - * @param asset The asset to move - * @param amount Amount of tokens to move - */ - function withdrawForUniswapV3( - address recipient, - address asset, - uint256 amount - ) external onlyUniswapV3Strategies nonReentrant { - _withdrawForUniswapV3(msg.sender, recipient, asset, amount); - } - - /** - * @notice Moves tokens from reserve strategy to the recipient - * @param v3Strategy Uniswap V3 Strategy that's requesting the withdraw - * @param recipient Receiver of the funds - * @param asset The asset to move - * @param amount Amount of tokens to move - */ - function _withdrawForUniswapV3( - address v3Strategy, - address recipient, - address asset, - uint256 amount - ) internal { - require(strategies[v3Strategy].isSupported, "Strategy not approved"); - address reserveStrategy = IUniswapV3Strategy(v3Strategy) - .reserveStrategy(asset); - IStrategy(reserveStrategy).withdraw(recipient, asset, amount); - } - /** * @notice Moves tokens from reserve strategy to the Uniswap V3 Strategy * @dev Only callable by whitelisted Uniswap V3 strategies * @param asset Address of the token * @param amount Amount of token1 required */ - function withdrawAssetForUniswapV3(address asset, uint256 amount) + function withdrawFromUniswapV3Reserve(address asset, uint256 amount) external onlyUniswapV3Strategies nonReentrant diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 38b568236e..88ba2d7261 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -1076,7 +1076,7 @@ const main = async () => { await deployConvexStrategy(); await deployConvexOUSDMetaStrategy(); await deployConvexLUSDMetaStrategy(); - await deployUniswapV3Strategy(); + // await deployUniswapV3Strategy(); const harvesterProxy = await deployHarvester(); await configureVault(harvesterProxy); await configureStrategies(harvesterProxy); diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index b08aeb27fd..f77438a990 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -29,8 +29,8 @@ module.exports = deploymentWithGovernanceProposal( // 0. Deploy UniswapV3Helper and UniswapV3StrategyLib const dUniswapV3Helper = await deployWithConfirmation("UniswapV3Helper"); - const dUniswapV3StrategyLib = await deployWithConfirmation( - "UniswapV3StrategyLib" + const dUniV3Lib = await deployWithConfirmation( + "UniswapV3Library" ); // 0. Upgrade VaultAdmin @@ -49,15 +49,23 @@ module.exports = deploymentWithGovernanceProposal( // 2. Deploy new implementation const dUniV3_USDC_USDT_StrategyImpl = await deployWithConfirmation( - "GeneralizedUniswapV3Strategy", - [], + "UniswapV3Strategy", + undefined, undefined, { - UniswapV3StrategyLib: dUniswapV3StrategyLib.address, + UniswapV3Library: dUniV3Lib.address + } + ); + const dUniV3PoolLiquidityManager = await deployWithConfirmation( + "UniswapV3LiquidityManager", + undefined, + undefined, + { + UniswapV3Library: dUniV3Lib.address } ); const cUniV3_USDC_USDT_Strategy = await ethers.getContractAt( - "GeneralizedUniswapV3Strategy", + "UniswapV3Strategy", dUniV3_USDC_USDT_Proxy.address ); @@ -85,22 +93,27 @@ module.exports = deploymentWithGovernanceProposal( // 4. Init and configure new Uniswap V3 strategy const initFunction = - "initialize(address,address,address,address,address,address,address,address)"; + "initialize(address,address,address,address,address,address)"; await withConfirmation( cUniV3_USDC_USDT_Strategy.connect(sDeployer)[initFunction]( cVaultProxy.address, // Vault assetAddresses.UniV3_USDC_USDT_Pool, // Pool address assetAddresses.UniV3PositionManager, // NonfungiblePositionManager - cMorphoCompProxy.address, // Reserve strategy for USDC - cMorphoCompProxy.address, // Reserve strategy for USDT - operatorAddr, dUniswapV3Helper.address, assetAddresses.UniV3SwapRouter, + operatorAddr, await getTxOpts() ) ); - // 5. Transfer governance + // 5. Set LiquidityManager Implementation + await withConfirmation( + cUniV3_USDC_USDT_Strategy + .connect(sDeployer) + .setLiquidityManagerImpl(dUniV3PoolLiquidityManager.address) + ) + + // 6. Transfer governance await withConfirmation( cUniV3_USDC_USDT_Strategy .connect(sDeployer) @@ -152,6 +165,18 @@ module.exports = deploymentWithGovernanceProposal( signature: "setHarvesterAddress(address)", args: [cHarvesterProxy.address], }, + // 4. Set Reserve Strategy for USDC + { + contract: cUniV3_USDC_USDT_Strategy, + signature: "setReserveStrategy(address,address)", + args: [assetAddresses.USDC, cMorphoCompProxy.address], + }, + // 4. Set Reserve Strategy for USDT + { + contract: cUniV3_USDC_USDT_Strategy, + signature: "setReserveStrategy(address,address)", + args: [assetAddresses.USDT, cMorphoCompProxy.address], + }, ], }; } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index d4a446de94..22495d77fc 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -112,19 +112,27 @@ async function defaultFixture() { const buyback = await ethers.getContract("Buyback"); - const UniV3Lib = await ethers.getContract("UniswapV3StrategyLib"); + const UniV3Lib = await ethers.getContract("UniswapV3Library"); const UniV3_USDC_USDT_Proxy = await ethers.getContract( "UniV3_USDC_USDT_Proxy" ); - const UniswapV3StrategyFactory = await ethers.getContractFactory( - "GeneralizedUniswapV3Strategy", - { libraries: { UniswapV3StrategyLib: UniV3Lib.address } } - ); const UniV3_USDC_USDT_Strategy = await ethers.getContractAt( - [ - ...UniV3Lib.interface.format("full").filter((e) => e.startsWith("event")), - ...UniswapV3StrategyFactory.interface.format("full"), - ], + Array.from(new Set([ + ...( + await ethers.getContractFactory("UniswapV3Strategy", { + libraries: { + UniswapV3Library: UniV3Lib.address + } + }) + ).interface.format("full"), + ...( + await ethers.getContractFactory("UniswapV3LiquidityManager", { + libraries: { + UniswapV3Library: UniV3Lib.address + } + }) + ).interface.format("full"), + ])), UniV3_USDC_USDT_Proxy.address ); const UniV3Helper = await ethers.getContract("UniswapV3Helper"); diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 70f9f4cd0a..a2dcfe486d 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -1,5 +1,5 @@ const { expect } = require("chai"); -const { uniswapV3FixturSetup } = require("../_fixture"); +const { uniswapV3FixturSetup, impersonateAndFundContract } = require("../_fixture"); const { forkOnlyDescribe, units, @@ -102,19 +102,63 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { BigNumber.from(usdtAmount).mul(10 ** 6) ); - console.log("Rebalance in process..."); - const tx = await strategy .connect(operator) .rebalance( - [maxUSDC, maxUSDT], - [maxUSDC.mul(9900).div(10000), maxUSDT.mul(9900).div(10000)], - [0, 0], + maxUSDC, maxUSDT, + maxUSDC.mul(9900).div(10000), maxUSDT.mul(9900).div(10000), + 0, 0, lowerTick, upperTick ); - console.log("Rebalance done"); + const { events } = await tx.wait(); + + const [tokenId, amount0Minted, amount1Minted, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + + return { + tokenId, + amount0Minted, + amount1Minted, + liquidityMinted, + tx, + }; + }; + + const mintLiquidityBySwapping = async ( + lowerTick, + upperTick, + usdcAmount, + usdtAmount, + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne + ) => { + const [maxUSDC, maxUSDT] = await findMaxDepositableAmount( + lowerTick, + upperTick, + BigNumber.from(usdcAmount).mul(10 ** 6), + BigNumber.from(usdtAmount).mul(10 ** 6) + ); + + const tx = await strategy + .connect(operator) + .swapAndRebalance({ + desiredAmount0: maxUSDC, + desiredAmount1: maxUSDT, + minAmount0: maxUSDC.mul(9900).div(10000), + minAmount1: maxUSDT.mul(9900).div(10000), + minRedeemAmount0: 0, + minRedeemAmount1: 0, + lowerTick, + upperTick, + swapAmountIn: BigNumber.from(swapAmountIn).mul(10 ** 6), + swapMinAmountOut: BigNumber.from(swapMinAmountOut).mul(10 ** 6), + sqrtPriceLimitX96, + swapZeroForOne + }); const { events } = await tx.wait(); @@ -179,9 +223,161 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(storedPosition.lowerTick).to.equal(lowerTick); expect(storedPosition.upperTick).to.equal(upperTick); expect(storedPosition.liquidity).to.equal(liquidityMinted); - expect(await strategy.currentPositionTokenId()).to.equal(tokenId); + expect(await strategy.activeTokenId()).to.equal(tokenId); }); + it("Should swap USDC for USDT and mint position", async () => { + // Move all USDT out of reserve + await reserveStrategy + .connect(await impersonateAndFundContract(vault.address)) + .withdraw( + vault.address, + usdt.address, + await reserveStrategy.checkBalance(usdt.address) + ) + + const usdcBalBefore = await strategy.checkBalance(usdc.address); + const usdtBalBefore = await strategy.checkBalance(usdt.address); + + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick - 1000; + const upperTick = activeTick + 1000; + + const swapAmountIn = "101000" + const swapAmountOut = "100000" + const sqrtPriceLimitX96 = v3Helper.getSqrtRatioAtTick(activeTick - 50) + const swapZeroForOne = true + + const { tokenId, amount0Minted, amount1Minted, liquidityMinted, tx } = + await mintLiquidityBySwapping( + lowerTick, + upperTick, + "100000", + "100000", + swapAmountIn, + swapAmountOut, + sqrtPriceLimitX96, + swapZeroForOne + ); + + // Check events + await expect(tx).to.have.emittedEvent("AssetSwappedForRebalancing"); + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + await expect(tx).to.have.emittedEvent("UniswapV3LiquidityAdded"); + + // Check minted position data + const nfp = await positionManager.positions(tokenId); + expect(nfp.token0).to.equal(usdc.address, "Invalid token0 address"); + expect(nfp.token1).to.equal(usdt.address, "Invalid token1 address"); + expect(nfp.tickLower).to.equal(lowerTick, "Invalid lower tick"); + expect(nfp.tickUpper).to.equal(upperTick, "Invalid upper tick"); + + // Check Strategy balance + const usdcBalAfter = await strategy.checkBalance(usdc.address); + const usdtBalAfter = await strategy.checkBalance(usdt.address); + expect(usdcBalAfter).gte( + usdcBalBefore, + "Expected USDC balance to have increased" + ); + expect(usdtBalAfter).gte( + usdtBalBefore, + "Expected USDT balance to have increased" + ); + expect(usdcBalAfter).to.approxEqual( + usdcBalBefore.add(amount0Minted), + "Deposited USDC mismatch" + ); + expect(usdtBalAfter).to.approxEqual( + usdtBalBefore.add(amount1Minted), + "Deposited USDT mismatch" + ); + + // Check data on strategy + const storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.exists).to.be.true; + expect(storedPosition.tokenId).to.equal(tokenId); + expect(storedPosition.lowerTick).to.equal(lowerTick); + expect(storedPosition.upperTick).to.equal(upperTick); + expect(storedPosition.liquidity).to.equal(liquidityMinted); + expect(await strategy.activeTokenId()).to.equal(tokenId); + }) + + it("Should swap USDT for USDC and mint position", async () => { + // Move all USDC out of reserve + await reserveStrategy + .connect(await impersonateAndFundContract(vault.address)) + .withdraw( + vault.address, + usdc.address, + await reserveStrategy.checkBalance(usdc.address) + ) + + const usdcBalBefore = await strategy.checkBalance(usdc.address); + const usdtBalBefore = await strategy.checkBalance(usdt.address); + + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick - 1000; + const upperTick = activeTick + 1000; + + const swapAmountIn = "101000" + const swapAmountOut = "100000" + const sqrtPriceLimitX96 = v3Helper.getSqrtRatioAtTick(activeTick + 50) + const swapZeroForOne = false + + const { tokenId, amount0Minted, amount1Minted, liquidityMinted, tx } = + await mintLiquidityBySwapping( + lowerTick, + upperTick, + "100000", + "100000", + swapAmountIn, + swapAmountOut, + sqrtPriceLimitX96, + swapZeroForOne + ); + + // Check events + await expect(tx).to.have.emittedEvent("AssetSwappedForRebalancing"); + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + await expect(tx).to.have.emittedEvent("UniswapV3LiquidityAdded"); + + // Check minted position data + const nfp = await positionManager.positions(tokenId); + expect(nfp.token0).to.equal(usdc.address, "Invalid token0 address"); + expect(nfp.token1).to.equal(usdt.address, "Invalid token1 address"); + expect(nfp.tickLower).to.equal(lowerTick, "Invalid lower tick"); + expect(nfp.tickUpper).to.equal(upperTick, "Invalid upper tick"); + + // Check Strategy balance + const usdcBalAfter = await strategy.checkBalance(usdc.address); + const usdtBalAfter = await strategy.checkBalance(usdt.address); + expect(usdcBalAfter).gte( + usdcBalBefore, + "Expected USDC balance to have increased" + ); + expect(usdtBalAfter).gte( + usdtBalBefore, + "Expected USDT balance to have increased" + ); + expect(usdcBalAfter).to.approxEqual( + usdcBalBefore.add(amount0Minted), + "Deposited USDC mismatch" + ); + expect(usdtBalAfter).to.approxEqual( + usdtBalBefore.add(amount1Minted), + "Deposited USDT mismatch" + ); + + // Check data on strategy + const storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.exists).to.be.true; + expect(storedPosition.tokenId).to.equal(tokenId); + expect(storedPosition.lowerTick).to.equal(lowerTick); + expect(storedPosition.upperTick).to.equal(upperTick); + expect(storedPosition.liquidity).to.equal(liquidityMinted); + expect(await strategy.activeTokenId()).to.equal(tokenId); + }) + it("Should increase liquidity of existing position", async () => { const usdcBalBefore = await strategy.checkBalance(usdc.address); const usdtBalBefore = await strategy.checkBalance(usdt.address); @@ -203,12 +399,12 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); const storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.exists).to.be.true; - expect(await strategy.currentPositionTokenId()).to.equal(tokenId); + expect(await strategy.activeTokenId()).to.equal(tokenId); // Rebalance again to increase liquidity const tx2 = await strategy .connect(operator) - .increaseLiquidityForActivePosition(amountUnits, amountUnits); + .increaseActivePositionLiquidity(amountUnits, amountUnits, 0, 0); await expect(tx2).to.have.emittedEvent("UniswapV3LiquidityAdded"); // Check balance on strategy @@ -243,13 +439,13 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); const storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.exists).to.be.true; - expect(await strategy.currentPositionTokenId()).to.equal(tokenId); + expect(await strategy.activeTokenId()).to.equal(tokenId); // Remove liquidity - const tx2 = await strategy.connect(operator).closePosition(tokenId); + const tx2 = await strategy.connect(operator).closePosition(tokenId, 0, 0); await expect(tx2).to.have.emittedEvent("UniswapV3LiquidityRemoved"); - expect(await strategy.currentPositionTokenId()).to.equal( + expect(await strategy.activeTokenId()).to.equal( BigNumber.from(0), "Should have no active position" ); @@ -257,14 +453,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Check balance on strategy const usdcBalAfter = await strategy.checkBalance(usdc.address); const usdtBalAfter = await strategy.checkBalance(usdt.address); - expect(usdcBalAfter).to.equal( - BigNumber.from(0), - "Expected to have liquidated all USDC" - ); - expect(usdtBalAfter).to.equal( - BigNumber.from(0), - "Expected to have liquidated all USDT" - ); + expect(strategy).to.have.an.approxBalanceOf(usdcBalAfter, usdc) + expect(strategy).to.have.an.approxBalanceOf(usdtBalAfter, usdt) }); async function _swap(user, amount, zeroForOne) { @@ -287,7 +477,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { ]); } - it("Should collect rewards", async () => { + it("Should collect fees", async () => { const [, activeTick] = await pool.slot0(); const lowerTick = activeTick - 12; const upperTick = activeTick + 49; @@ -303,7 +493,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); const storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.exists).to.be.true; - expect(await strategy.currentPositionTokenId()).to.equal(tokenId); + expect(await strategy.activeTokenId()).to.equal(tokenId); // Do some big swaps await _swap(matt, "1000000", true); @@ -312,14 +502,15 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { await _swap(daniel, "1000000", false); await _swap(domen, "1000000", true); - // Check reward amounts + // Check fee amounts let [fee0, fee1] = await strategy.getPendingFees(); expect(fee0).to.be.gt(0); expect(fee1).to.be.gt(0); - // Harvest rewards - await harvester.connect(timelock)["harvest(address)"](strategy.address); - [fee0, fee1] = await strategy.getPendingFees(); + // Collect fees + await strategy.connect(operator).collectFees() + + ;[fee0, fee1] = await strategy.getPendingFees(); expect(fee0).to.equal(0); expect(fee1).to.equal(0); }); From bbe2fb5aad22aa69bf8476b2131fac87c2311481 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 16 Mar 2023 23:39:49 +0530 Subject: [PATCH 19/83] prettify --- .../interfaces/IUniswapV3Strategy.sol | 2 + contracts/contracts/interfaces/IVault.sol | 3 +- .../strategies/uniswap/UniswapV3Library.sol | 8 +- .../uniswap/UniswapV3LiquidityManager.sol | 181 +++++++++++------- .../strategies/uniswap/UniswapV3Strategy.sol | 46 +++-- .../uniswap/UniswapV3StrategyStorage.sol | 11 +- .../deploy/049_uniswap_usdc_usdt_strategy.js | 10 +- contracts/test/_fixture.js | 34 ++-- .../test/strategies/uniswap-v3.fork-test.js | 93 ++++----- 9 files changed, 235 insertions(+), 153 deletions(-) diff --git a/contracts/contracts/interfaces/IUniswapV3Strategy.sol b/contracts/contracts/interfaces/IUniswapV3Strategy.sol index ab7bf50f75..2bc1eaec40 100644 --- a/contracts/contracts/interfaces/IUniswapV3Strategy.sol +++ b/contracts/contracts/interfaces/IUniswapV3Strategy.sol @@ -5,6 +5,8 @@ import { IStrategy } from "./IStrategy.sol"; interface IUniswapV3Strategy is IStrategy { function token0() external view returns (address); + function token1() external view returns (address); + function reserveStrategy(address token) external view returns (address); } diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index 88a31e3af7..dca14e73b3 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -177,5 +177,6 @@ interface IVault { function depositToUniswapV3Reserve(address asset, uint256 amount) external; - function withdrawFromUniswapV3Reserve(address asset, uint256 amount) external; + function withdrawFromUniswapV3Reserve(address asset, uint256 amount) + external; } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Library.sol b/contracts/contracts/strategies/uniswap/UniswapV3Library.sol index f7d8e039c0..274ebb9cee 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Library.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Library.sol @@ -19,12 +19,14 @@ library UniswapV3Library { IVault vault = IVault(vaultAddress); if ( - token0Bal > 0 && (minDepositThreshold0 == 0 || token0Bal >= minDepositThreshold0) + token0Bal > 0 && + (minDepositThreshold0 == 0 || token0Bal >= minDepositThreshold0) ) { vault.depositToUniswapV3Reserve(token0, token0Bal); } if ( - token1Bal > 0 && (minDepositThreshold1 == 0 || token1Bal >= minDepositThreshold1) + token1Bal > 0 && + (minDepositThreshold1 == 0 || token1Bal >= minDepositThreshold1) ) { vault.depositToUniswapV3Reserve(token1, token1Bal); } @@ -49,4 +51,4 @@ library UniswapV3Library { // require(success, "Failed to liquidate active position"); // } -} \ No newline at end of file +} diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index d1b3f32eac..9526494f56 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -20,16 +20,19 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ****************************************/ function _depositAll() internal { UniswapV3Library.depositAll( - token0, - token1, - vaultAddress, - poolTokens[token0].minDepositThreshold, + token0, + token1, + vaultAddress, + poolTokens[token0].minDepositThreshold, poolTokens[token1].minDepositThreshold ); } // TODO: Intentionally left out non-reentrant modifier since otherwise Vault would throw - function withdrawAssetFromActivePosition(address _asset, uint256 amount) external onlyVault { + function withdrawAssetFromActivePosition(address _asset, uint256 amount) + external + onlyVault + { Position memory position = tokenIdToPosition[activeTokenId]; require(position.exists && position.liquidity > 0, "Liquidity error"); @@ -38,11 +41,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint128 liquidity, uint256 minAmount0, uint256 minAmount1 - ) = _calculateLiquidityToWithdraw( - position, - _asset, - amount - ); + ) = _calculateLiquidityToWithdraw(position, _asset, amount); // Liquidiate active position _decreasePositionLiquidity( @@ -145,11 +144,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { if (activeTokenId > 0) { // Close any active position - _closePosition( - activeTokenId, - minRedeemAmount0, - minRedeemAmount1 - ); + _closePosition(activeTokenId, minRedeemAmount0, minRedeemAmount1); } // Withdraw enough funds from Reserve strategies @@ -158,7 +153,13 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // Provide liquidity if (tokenId > 0) { // Add liquidity to the position token - _increasePositionLiquidity(tokenId, desiredAmount0, desiredAmount1, minAmount0, minAmount1); + _increasePositionLiquidity( + tokenId, + desiredAmount0, + desiredAmount1, + minAmount0, + minAmount1 + ); } else { // Mint new position (tokenId, , , ) = _mintPosition( @@ -192,6 +193,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint160 sqrtPriceLimitX96; bool swapZeroForOne; } + function swapAndRebalance(SwapAndRebalanceParams calldata params) external onlyGovernorOrStrategistOrOperator @@ -225,7 +227,13 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // Provide liquidity if (tokenId > 0) { // Add liquidity to the position token - _increasePositionLiquidity(tokenId, params.desiredAmount0, params.desiredAmount1, params.minAmount0, params.minAmount1); + _increasePositionLiquidity( + tokenId, + params.desiredAmount0, + params.desiredAmount1, + params.minAmount0, + params.minAmount1 + ); } else { // Mint new position (tokenId, , , ) = _mintPosition( @@ -245,7 +253,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { _depositAll(); } - /*************************************** Pool Liquidity Management ****************************************/ @@ -268,7 +275,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { key = key + int24(upperTick); } - /** * @notice Mints a new position on the pool and provides liquidity to it * @@ -332,12 +338,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { }); emit UniswapV3PositionMinted(tokenId, lowerTick, upperTick); - emit UniswapV3LiquidityAdded( - tokenId, - amount0, - amount1, - liquidity - ); + emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); } /** @@ -355,8 +356,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 desiredAmount1, uint256 minAmount0, uint256 minAmount1 - ) - internal + ) + internal returns ( uint128 liquidity, uint256 amount0, @@ -394,8 +395,19 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 desiredAmount1, uint256 minAmount0, uint256 minAmount1 - ) external onlyGovernorOrStrategistOrOperator nonReentrant returns (uint256 amount0, uint256 amount1) { - _increasePositionLiquidity(activeTokenId, desiredAmount0, desiredAmount1, minAmount0, minAmount1); + ) + external + onlyGovernorOrStrategistOrOperator + nonReentrant + returns (uint256 amount0, uint256 amount1) + { + _increasePositionLiquidity( + activeTokenId, + desiredAmount0, + desiredAmount1, + minAmount0, + minAmount1 + ); } /** @@ -417,17 +429,13 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint128 liquidity, uint256 minAmount0, uint256 minAmount1 - ) internal - returns (uint256 amount0, uint256 amount1) - { - Position storage position = tokenIdToPosition[ - tokenId - ]; + ) internal returns (uint256 amount0, uint256 amount1) { + Position storage position = tokenIdToPosition[tokenId]; require(position.exists, "Unknown position"); - (uint160 sqrtRatioX96, , , , , , ) = pool - .slot0(); - (uint256 exactAmount0, uint256 exactAmount1) = helper.getAmountsForLiquidity( + (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); + (uint256 exactAmount0, uint256 exactAmount1) = helper + .getAmountsForLiquidity( sqrtRatioX96, position.sqrtRatioAX96, position.sqrtRatioBX96, @@ -444,23 +452,36 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { deadline: block.timestamp }); - (amount0, amount1) = positionManager - .decreaseLiquidity(params); + (amount0, amount1) = positionManager.decreaseLiquidity(params); position.liquidity -= liquidity; - emit UniswapV3LiquidityRemoved(position.tokenId, amount0, amount1, liquidity); + emit UniswapV3LiquidityRemoved( + position.tokenId, + amount0, + amount1, + liquidity + ); } function decreaseActivePositionLiquidity( uint128 liquidity, uint256 minAmount0, uint256 minAmount1 - ) external onlyGovernorOrStrategistOrOperator nonReentrant returns (uint256 amount0, uint256 amount1) { - _decreasePositionLiquidity(activeTokenId, liquidity, minAmount0, minAmount1); + ) + external + onlyGovernorOrStrategistOrOperator + nonReentrant + returns (uint256 amount0, uint256 amount1) + { + _decreasePositionLiquidity( + activeTokenId, + liquidity, + minAmount0, + minAmount1 + ); } - /** * @notice Closes the position denoted by the tokenId and and collects all fees * @param tokenId Position NFT's tokenId @@ -474,9 +495,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 minAmount0, uint256 minAmount1 ) internal returns (uint256 amount0, uint256 amount1) { - Position memory position = tokenIdToPosition[ - tokenId - ]; + Position memory position = tokenIdToPosition[tokenId]; require(position.exists, "Invalid position"); if (position.liquidity == 0) { @@ -518,7 +537,12 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 tokenId, uint256 minAmount0, uint256 minAmount1 - ) external onlyGovernorOrStrategistOrOperator nonReentrant returns (uint256 amount0, uint256 amount1) { + ) + external + onlyGovernorOrStrategistOrOperator + nonReentrant + returns (uint256 amount0, uint256 amount1) + { return _closePosition(tokenId, minAmount0, minAmount1); } @@ -573,22 +597,24 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // TODO: Check value of assets moved here } - function _checkSwapLimits( - uint160 sqrtPriceLimitX96, - bool swapZeroForOne - ) internal { + function _checkSwapLimits(uint160 sqrtPriceLimitX96, bool swapZeroForOne) + internal + { require(!swapsPaused, "Swaps are paused"); (uint160 currentPriceX96, , , , , , ) = pool.slot0(); if (minSwapPriceX96 > 0 || maxSwapPriceX96 > 0) { require( - minSwapPriceX96 <= currentPriceX96 && currentPriceX96 <= maxSwapPriceX96, + minSwapPriceX96 <= currentPriceX96 && + currentPriceX96 <= maxSwapPriceX96, "Price out of bounds" ); require( - swapZeroForOne ? (sqrtPriceLimitX96 >= minSwapPriceX96) : (sqrtPriceLimitX96 <= maxSwapPriceX96), + swapZeroForOne + ? (sqrtPriceLimitX96 >= minSwapPriceX96) + : (sqrtPriceLimitX96 <= maxSwapPriceX96), "Slippage out of bounds" ); } @@ -630,7 +656,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // Additional amount of token0 required for swapping token0Needed += swapAmountIn; // Subtract token1 that we will get from swapping - token1Needed = (swapMinAmountOut >= token1Needed) ? 0 : (token1Needed - swapMinAmountOut); + token1Needed = (swapMinAmountOut >= token1Needed) + ? 0 + : (token1Needed - swapMinAmountOut); // Approve for swaps t0Contract.safeApprove(address(swapRouter), swapAmountIn); @@ -648,7 +676,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { token1Needed += swapAmountIn; // Subtract token0 that we will get from swapping // Subtract token1 that we will get from swapping - token0Needed = (swapMinAmountOut >= token0Needed) ? 0 : (token0Needed - swapMinAmountOut); + token0Needed = (swapMinAmountOut >= token0Needed) + ? 0 + : (token0Needed - swapMinAmountOut); // Approve for swaps t1Contract.safeApprove(address(swapRouter), swapAmountIn); @@ -693,7 +723,11 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // TODO: Check value of assets moved here } - function collectFees() external onlyGovernorOrStrategistOrOperator returns (uint256 amount0, uint256 amount1) { + function collectFees() + external + onlyGovernorOrStrategistOrOperator + returns (uint256 amount0, uint256 amount1) + { return _collectFeesForToken(activeTokenId); } @@ -724,18 +758,26 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /*************************************** Hidden functions ****************************************/ - function _abstractSetPToken(address _asset, address _pToken) internal override { + function _abstractSetPToken(address _asset, address _pToken) + internal + override + { revert("NO_IMPL"); } - function safeApproveAllTokens() external override virtual { + function safeApproveAllTokens() external virtual override { revert("NO_IMPL"); } - function deposit(address _asset, uint256 _amount) external override virtual { + + function deposit(address _asset, uint256 _amount) + external + virtual + override + { revert("NO_IMPL"); } - function depositAll() external override virtual { + function depositAll() external virtual override { revert("NO_IMPL"); } @@ -743,21 +785,29 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { address _recipient, address _asset, uint256 amount - ) external override virtual { + ) external virtual override { revert("NO_IMPL"); } - function withdrawAll() external override virtual { + function withdrawAll() external virtual override { revert("NO_IMPL"); } - function checkBalance(address _asset) external view override + function checkBalance(address _asset) + external + view + override returns (uint256 balance) { revert("NO_IMPL"); } - function supportsAsset(address _asset) external view override returns (bool) { + function supportsAsset(address _asset) + external + view + override + returns (bool) + { revert("NO_IMPL"); } @@ -777,5 +827,4 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { { revert("NO_IMPL"); } - } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 4fb9e55207..3d9fefe1d1 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -171,11 +171,22 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @param minTick Minimum price tick index * @param maxTick Maximum price tick index */ - function setSwapPriceThreshold(int24 minTick, int24 maxTick) external onlyGovernorOrStrategist { - require((minTick < maxTick) || (minTick == 0 && maxTick == 0), "Invalid threshold"); + function setSwapPriceThreshold(int24 minTick, int24 maxTick) + external + onlyGovernorOrStrategist + { + require( + (minTick < maxTick) || (minTick == 0 && maxTick == 0), + "Invalid threshold" + ); minSwapPriceX96 = helper.getSqrtRatioAtTick(minTick); maxSwapPriceX96 = helper.getSqrtRatioAtTick(maxTick); - emit SwapPriceThresholdChanged(minTick, minSwapPriceX96, maxTick, maxSwapPriceX96); + emit SwapPriceThresholdChanged( + minTick, + minSwapPriceX96, + maxTick, + maxSwapPriceX96 + ); } /*************************************** @@ -205,7 +216,13 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @notice Deposits all undeployed balances of the contract to the reserve strategies */ function _depositAll() internal { - UniswapV3Library.depositAll(token0, token1, vaultAddress, poolTokens[token0].minDepositThreshold, poolTokens[token1].minDepositThreshold); + UniswapV3Library.depositAll( + token0, + token1, + vaultAddress, + poolTokens[token0].minDepositThreshold, + poolTokens[token1].minDepositThreshold + ); } /** @@ -221,7 +238,11 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { if (selfBalance < amount) { (bool success, bytes memory data) = address(this).delegatecall( - abi.encodeWithSignature("withdrawAssetFromActivePosition(asset,uint256)", _asset, amount - selfBalance) + abi.encodeWithSignature( + "withdrawAssetFromActivePosition(asset,uint256)", + _asset, + amount - selfBalance + ) ); require(success, "Failed to liquidate active position"); @@ -245,7 +266,12 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { // active position when withdrawingAll rather than passing zero values? (bool success, bytes memory data) = address(this).delegatecall( - abi.encodeWithSignature("closePositionOnlyVault(uint256,uint256,uint256)", activeTokenId, 0, 0) + abi.encodeWithSignature( + "closePositionOnlyVault(uint256,uint256,uint256)", + activeTokenId, + 0, + 0 + ) ); require(success, "Failed to close active position"); @@ -303,14 +329,10 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { { balance = IERC20(_asset).balanceOf(address(this)); - (uint160 sqrtRatioX96, , , , , , ) = pool - .slot0(); + (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); if (activeTokenId > 0) { - require( - tokenIdToPosition[activeTokenId].exists, - "Invalid token" - ); + require(tokenIdToPosition[activeTokenId].exists, "Invalid token"); (uint256 amount0, uint256 amount1) = helper.positionValue( positionManager, diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 914feb439e..ef0132d0b1 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -19,7 +19,12 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint256 minDepositThreshold ); event SwapsPauseStatusChanged(bool paused); - event SwapPriceThresholdChanged(int24 minTick, uint160 minSwapPriceX96, int24 maxTick, uint160 maxSwapPriceX96); + event SwapPriceThresholdChanged( + int24 minTick, + uint160 minSwapPriceX96, + int24 maxTick, + uint160 maxSwapPriceX96 + ); event MaxSwapSlippageChanged(uint24 maxSlippage); event AssetSwappedForRebalancing( address indexed tokenIn, @@ -58,7 +63,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { // Represents both tokens supported by the strategy struct PoolToken { // True if asset is either token0 or token1 - bool isSupported; + bool isSupported; // When the funds are not deployed in Uniswap V3 Pool, they will // be deposited to these reserve strategies address reserveStrategy; @@ -73,7 +78,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { int24 lowerTick; // Lower tick index int24 upperTick; // Upper tick index bool exists; // True, if position is minted - // 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? @@ -119,7 +123,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { // Maps tokenIDs to their Position object mapping(uint256 => Position) public tokenIdToPosition; - // keccak256("OUSD.UniswapV3Strategy.LiquidityManager.impl") bytes32 constant liquidityManagerImplPosition = 0xec676d52175f7cbb4e4ea392c6b70f8946575021aad20479602b98adc56ad62d; diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index f77438a990..45660b390b 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -29,9 +29,7 @@ module.exports = deploymentWithGovernanceProposal( // 0. Deploy UniswapV3Helper and UniswapV3StrategyLib const dUniswapV3Helper = await deployWithConfirmation("UniswapV3Helper"); - const dUniV3Lib = await deployWithConfirmation( - "UniswapV3Library" - ); + const dUniV3Lib = await deployWithConfirmation("UniswapV3Library"); // 0. Upgrade VaultAdmin const dVaultAdmin = await deployWithConfirmation("VaultAdmin"); @@ -53,7 +51,7 @@ module.exports = deploymentWithGovernanceProposal( undefined, undefined, { - UniswapV3Library: dUniV3Lib.address + UniswapV3Library: dUniV3Lib.address, } ); const dUniV3PoolLiquidityManager = await deployWithConfirmation( @@ -61,7 +59,7 @@ module.exports = deploymentWithGovernanceProposal( undefined, undefined, { - UniswapV3Library: dUniV3Lib.address + UniswapV3Library: dUniV3Lib.address, } ); const cUniV3_USDC_USDT_Strategy = await ethers.getContractAt( @@ -111,7 +109,7 @@ module.exports = deploymentWithGovernanceProposal( cUniV3_USDC_USDT_Strategy .connect(sDeployer) .setLiquidityManagerImpl(dUniV3PoolLiquidityManager.address) - ) + ); // 6. Transfer governance await withConfirmation( diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 22495d77fc..6d1c100f58 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -117,22 +117,24 @@ async function defaultFixture() { "UniV3_USDC_USDT_Proxy" ); const UniV3_USDC_USDT_Strategy = await ethers.getContractAt( - Array.from(new Set([ - ...( - await ethers.getContractFactory("UniswapV3Strategy", { - libraries: { - UniswapV3Library: UniV3Lib.address - } - }) - ).interface.format("full"), - ...( - await ethers.getContractFactory("UniswapV3LiquidityManager", { - libraries: { - UniswapV3Library: UniV3Lib.address - } - }) - ).interface.format("full"), - ])), + Array.from( + new Set([ + ...( + await ethers.getContractFactory("UniswapV3Strategy", { + libraries: { + UniswapV3Library: UniV3Lib.address, + }, + }) + ).interface.format("full"), + ...( + await ethers.getContractFactory("UniswapV3LiquidityManager", { + libraries: { + UniswapV3Library: UniV3Lib.address, + }, + }) + ).interface.format("full"), + ]) + ), UniV3_USDC_USDT_Proxy.address ); const UniV3Helper = await ethers.getContract("UniswapV3Helper"); diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index a2dcfe486d..1c583e528d 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -1,5 +1,8 @@ const { expect } = require("chai"); -const { uniswapV3FixturSetup, impersonateAndFundContract } = require("../_fixture"); +const { + uniswapV3FixturSetup, + impersonateAndFundContract, +} = require("../_fixture"); const { forkOnlyDescribe, units, @@ -105,9 +108,12 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const tx = await strategy .connect(operator) .rebalance( - maxUSDC, maxUSDT, - maxUSDC.mul(9900).div(10000), maxUSDT.mul(9900).div(10000), - 0, 0, + maxUSDC, + maxUSDT, + maxUSDC.mul(9900).div(10000), + maxUSDT.mul(9900).div(10000), + 0, + 0, lowerTick, upperTick ); @@ -143,22 +149,20 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { BigNumber.from(usdtAmount).mul(10 ** 6) ); - const tx = await strategy - .connect(operator) - .swapAndRebalance({ - desiredAmount0: maxUSDC, - desiredAmount1: maxUSDT, - minAmount0: maxUSDC.mul(9900).div(10000), - minAmount1: maxUSDT.mul(9900).div(10000), - minRedeemAmount0: 0, - minRedeemAmount1: 0, - lowerTick, - upperTick, - swapAmountIn: BigNumber.from(swapAmountIn).mul(10 ** 6), - swapMinAmountOut: BigNumber.from(swapMinAmountOut).mul(10 ** 6), - sqrtPriceLimitX96, - swapZeroForOne - }); + const tx = await strategy.connect(operator).swapAndRebalance({ + desiredAmount0: maxUSDC, + desiredAmount1: maxUSDT, + minAmount0: maxUSDC.mul(9900).div(10000), + minAmount1: maxUSDT.mul(9900).div(10000), + minRedeemAmount0: 0, + minRedeemAmount1: 0, + lowerTick, + upperTick, + swapAmountIn: BigNumber.from(swapAmountIn).mul(10 ** 6), + swapMinAmountOut: BigNumber.from(swapMinAmountOut).mul(10 ** 6), + sqrtPriceLimitX96, + swapZeroForOne, + }); const { events } = await tx.wait(); @@ -234,8 +238,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { vault.address, usdt.address, await reserveStrategy.checkBalance(usdt.address) - ) - + ); + const usdcBalBefore = await strategy.checkBalance(usdc.address); const usdtBalBefore = await strategy.checkBalance(usdt.address); @@ -243,16 +247,16 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const lowerTick = activeTick - 1000; const upperTick = activeTick + 1000; - const swapAmountIn = "101000" - const swapAmountOut = "100000" - const sqrtPriceLimitX96 = v3Helper.getSqrtRatioAtTick(activeTick - 50) - const swapZeroForOne = true + const swapAmountIn = "101000"; + const swapAmountOut = "100000"; + const sqrtPriceLimitX96 = v3Helper.getSqrtRatioAtTick(activeTick - 50); + const swapZeroForOne = true; const { tokenId, amount0Minted, amount1Minted, liquidityMinted, tx } = await mintLiquidityBySwapping( - lowerTick, - upperTick, - "100000", + lowerTick, + upperTick, + "100000", "100000", swapAmountIn, swapAmountOut, @@ -300,7 +304,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(storedPosition.upperTick).to.equal(upperTick); expect(storedPosition.liquidity).to.equal(liquidityMinted); expect(await strategy.activeTokenId()).to.equal(tokenId); - }) + }); it("Should swap USDT for USDC and mint position", async () => { // Move all USDC out of reserve @@ -310,8 +314,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { vault.address, usdc.address, await reserveStrategy.checkBalance(usdc.address) - ) - + ); + const usdcBalBefore = await strategy.checkBalance(usdc.address); const usdtBalBefore = await strategy.checkBalance(usdt.address); @@ -319,16 +323,16 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const lowerTick = activeTick - 1000; const upperTick = activeTick + 1000; - const swapAmountIn = "101000" - const swapAmountOut = "100000" - const sqrtPriceLimitX96 = v3Helper.getSqrtRatioAtTick(activeTick + 50) - const swapZeroForOne = false + const swapAmountIn = "101000"; + const swapAmountOut = "100000"; + const sqrtPriceLimitX96 = v3Helper.getSqrtRatioAtTick(activeTick + 50); + const swapZeroForOne = false; const { tokenId, amount0Minted, amount1Minted, liquidityMinted, tx } = await mintLiquidityBySwapping( - lowerTick, - upperTick, - "100000", + lowerTick, + upperTick, + "100000", "100000", swapAmountIn, swapAmountOut, @@ -376,7 +380,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(storedPosition.upperTick).to.equal(upperTick); expect(storedPosition.liquidity).to.equal(liquidityMinted); expect(await strategy.activeTokenId()).to.equal(tokenId); - }) + }); it("Should increase liquidity of existing position", async () => { const usdcBalBefore = await strategy.checkBalance(usdc.address); @@ -453,8 +457,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Check balance on strategy const usdcBalAfter = await strategy.checkBalance(usdc.address); const usdtBalAfter = await strategy.checkBalance(usdt.address); - expect(strategy).to.have.an.approxBalanceOf(usdcBalAfter, usdc) - expect(strategy).to.have.an.approxBalanceOf(usdtBalAfter, usdt) + expect(strategy).to.have.an.approxBalanceOf(usdcBalAfter, usdc); + expect(strategy).to.have.an.approxBalanceOf(usdtBalAfter, usdt); }); async function _swap(user, amount, zeroForOne) { @@ -508,9 +512,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(fee1).to.be.gt(0); // Collect fees - await strategy.connect(operator).collectFees() - - ;[fee0, fee1] = await strategy.getPendingFees(); + await strategy.connect(operator).collectFees(); + [fee0, fee1] = await strategy.getPendingFees(); expect(fee0).to.equal(0); expect(fee1).to.equal(0); }); From ce15a7a9e3928298e6031feed3f26eaf47de3441 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 17 Mar 2023 13:55:53 +0530 Subject: [PATCH 20/83] checkpoint --- .../uniswap/UniswapV3LiquidityManager.sol | 52 +++++++-------- .../strategies/uniswap/UniswapV3Strategy.sol | 64 ++++++++++++++++--- .../uniswap/UniswapV3StrategyStorage.sol | 21 +++++- contracts/contracts/utils/UniswapV3Helper.sol | 2 +- 4 files changed, 103 insertions(+), 36 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 9526494f56..598dda1308 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -114,6 +114,12 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /*************************************** Rebalance ****************************************/ + + modifier rebalanceNotPaused() { + require(!rebalancePaused, "Rebalance paused"); + _; + } + /** * @notice Closes active LP position if any and then provides liquidity to the requested position. * Mints new position, if it doesn't exist already. @@ -136,7 +142,12 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 minRedeemAmount1, int24 lowerTick, int24 upperTick - ) external onlyGovernorOrStrategistOrOperator nonReentrant { + ) + external + onlyGovernorOrStrategistOrOperator + nonReentrant + rebalanceNotPaused + { require(lowerTick < upperTick, "Invalid tick range"); int48 tickKey = _getTickPositionKey(lowerTick, upperTick); @@ -198,6 +209,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { external onlyGovernorOrStrategistOrOperator nonReentrant + rebalanceNotPaused { require(params.lowerTick < params.upperTick, "Invalid tick range"); @@ -599,25 +611,24 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { function _checkSwapLimits(uint160 sqrtPriceLimitX96, bool swapZeroForOne) internal + view { require(!swapsPaused, "Swaps are paused"); (uint160 currentPriceX96, , , , , , ) = pool.slot0(); - if (minSwapPriceX96 > 0 || maxSwapPriceX96 > 0) { - require( - minSwapPriceX96 <= currentPriceX96 && - currentPriceX96 <= maxSwapPriceX96, - "Price out of bounds" - ); + require( + minSwapPriceX96 <= currentPriceX96 && + currentPriceX96 <= maxSwapPriceX96, + "Price out of bounds" + ); - require( - swapZeroForOne - ? (sqrtPriceLimitX96 >= minSwapPriceX96) - : (sqrtPriceLimitX96 <= maxSwapPriceX96), - "Slippage out of bounds" - ); - } + require( + swapZeroForOne + ? (sqrtPriceLimitX96 >= minSwapPriceX96) + : (sqrtPriceLimitX96 <= maxSwapPriceX96), + "Slippage out of bounds" + ); } function _ensureAssetsBySwapping( @@ -630,11 +641,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ) internal { _checkSwapLimits(sqrtPriceLimitX96, swapZeroForOne); - IERC20 t0Contract = IERC20(token0); - IERC20 t1Contract = IERC20(token1); - - uint256 token0Balance = t0Contract.balanceOf(address(this)); - uint256 token1Balance = t1Contract.balanceOf(address(this)); + uint256 token0Balance = IERC20(token0).balanceOf(address(this)); + uint256 token1Balance = IERC20(token1).balanceOf(address(this)); uint256 token0Needed = desiredAmount0 > token0Balance ? desiredAmount0 - token0Balance @@ -659,9 +667,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { token1Needed = (swapMinAmountOut >= token1Needed) ? 0 : (token1Needed - swapMinAmountOut); - - // Approve for swaps - t0Contract.safeApprove(address(swapRouter), swapAmountIn); } else { // Amount available in reserve strategies uint256 t0ReserveBal = IStrategy(poolTokens[token0].reserveStrategy) @@ -679,9 +684,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { token0Needed = (swapMinAmountOut >= token0Needed) ? 0 : (token0Needed - swapMinAmountOut); - - // Approve for swaps - t1Contract.safeApprove(address(swapRouter), swapAmountIn); } // Fund strategy from reserve strategies diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 3d9fefe1d1..845a39b662 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -153,6 +153,14 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { emit MinDepositThresholdChanged(_asset, _minThreshold); } + function setRebalancePaused(bool _paused) + external + onlyGovernorOrStrategist + { + rebalancePaused = _paused; + emit RebalancePauseStatusChanged(_paused); + } + function setSwapsPaused(bool _paused) external onlyGovernorOrStrategist { swapsPaused = _paused; emit SwapsPauseStatusChanged(_paused); @@ -175,10 +183,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { external onlyGovernorOrStrategist { - require( - (minTick < maxTick) || (minTick == 0 && maxTick == 0), - "Invalid threshold" - ); + require((minTick < maxTick), "Invalid threshold"); minSwapPriceX96 = helper.getSqrtRatioAtTick(minTick); maxSwapPriceX96 = helper.getSqrtRatioAtTick(maxTick); emit SwapPriceThresholdChanged( @@ -189,6 +194,26 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { ); } + /** + * @notice Change the token price limit + * @param minTick Minimum price tick index + * @param maxTick Maximum price tick index + */ + function setTokenPriceLimit(int24 minTick, int24 maxTick) + external + onlyGovernorOrStrategist + { + require((minTick < maxTick), "Invalid threshold"); + minPriceLimitX96 = helper.getSqrtRatioAtTick(minTick); + maxPriceLimitX96 = helper.getSqrtRatioAtTick(maxTick); + emit TokenPriceLimitChanged( + minTick, + minPriceLimitX96, + maxTick, + maxPriceLimitX96 + ); + } + /*************************************** Deposit/Withdraw ****************************************/ @@ -329,11 +354,10 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { { balance = IERC20(_asset).balanceOf(address(this)); - (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); - if (activeTokenId > 0) { require(tokenIdToPosition[activeTokenId].exists, "Invalid token"); + (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); (uint256 amount0, uint256 amount1) = helper.positionValue( positionManager, address(pool), @@ -349,6 +373,27 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { } } + function checkBalanceOfAllAssets() + external + view + returns (uint256 amount0, uint256 amount1) + { + if (activeTokenId > 0) { + require(tokenIdToPosition[activeTokenId].exists, "Invalid token"); + + (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); + (amount0, amount1) = helper.positionValue( + positionManager, + address(pool), + activeTokenId, + sqrtRatioX96 + ); + } + + amount0 += IERC20(token0).balanceOf(address(this)); + amount1 += IERC20(token1).balanceOf(address(this)); + } + /*************************************** ERC721 management ****************************************/ @@ -380,9 +425,10 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { { IERC20(token0).safeApprove(vaultAddress, type(uint256).max); IERC20(token1).safeApprove(vaultAddress, type(uint256).max); - // TODO: Should approval be done only during minting/increasing liquidity? IERC20(token0).safeApprove(address(positionManager), type(uint256).max); IERC20(token1).safeApprove(address(positionManager), type(uint256).max); + IERC20(token0).safeApprove(address(swapRouter), type(uint256).max); + IERC20(token1).safeApprove(address(swapRouter), type(uint256).max); } /** @@ -391,13 +437,15 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { function resetAllowanceOfTokens() external onlyGovernor nonReentrant { IERC20(token0).safeApprove(address(positionManager), 0); IERC20(token1).safeApprove(address(positionManager), 0); + IERC20(token0).safeApprove(address(swapRouter), 0); + IERC20(token1).safeApprove(address(swapRouter), 0); } /// @inheritdoc InitializableAbstractStrategy function _abstractSetPToken(address _asset, address) internal override { IERC20(_asset).safeApprove(vaultAddress, type(uint256).max); - // TODO: Should approval be done only during minting/increasing liquidity? IERC20(_asset).safeApprove(address(positionManager), type(uint256).max); + IERC20(_asset).safeApprove(address(swapRouter), type(uint256).max); } /// @inheritdoc InitializableAbstractStrategy diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index ef0132d0b1..920a3e2a7f 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -18,6 +18,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { address asset, uint256 minDepositThreshold ); + event RebalancePauseStatusChanged(bool paused); event SwapsPauseStatusChanged(bool paused); event SwapPriceThresholdChanged( int24 minTick, @@ -26,6 +27,12 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint160 maxSwapPriceX96 ); event MaxSwapSlippageChanged(uint24 maxSlippage); + event TokenPriceLimitChanged( + int24 minTick, + uint160 minPriceLimitX96, + int24 maxTick, + uint160 maxPriceLimitX96 + ); event AssetSwappedForRebalancing( address indexed tokenIn, address indexed tokenOut, @@ -96,15 +103,23 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint24 public poolFee; // Uniswap V3 Pool Fee uint24 public maxSwapSlippage = 100; // 1%; Reverts if swap slippage is higher than this bool public swapsPaused = false; // True if Swaps are paused + bool public rebalancePaused = false; // True if Swaps are paused uint256 public maxTVL; // In USD, 18 decimals + // An upper and lower bound of swap price limits uint160 public minSwapPriceX96; uint160 public maxSwapPriceX96; + // Uses these params when checking the values of the tokens + // moved in and out of the reserve strategies + uint160 public minPriceLimitX96; + uint160 public maxPriceLimitX96; + + // Token ID of active Position on the pool. zero, if there are no active LP position uint256 public activeTokenId; - // Uniswap V3's PositionManager + // Uniswap V3's Pool IUniswapV3Pool public pool; // Uniswap V3's PositionManager @@ -113,8 +128,10 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { // A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch IUniswapV3Helper internal helper; + // Uniswap Swap Router ISwapRouter internal swapRouter; + // Contains data about both tokens mapping(address => PoolToken) public poolTokens; // A lookup table to find token IDs of position using f(lowerTick, upperTick) @@ -183,7 +200,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { /** * @dev Ensures that the caller is the proxy. */ - modifier internalProxiedFunction() { + modifier onlySelf() { require(msg.sender == address(_self), "Not self"); _; } diff --git a/contracts/contracts/utils/UniswapV3Helper.sol b/contracts/contracts/utils/UniswapV3Helper.sol index 322b2f3d32..e6d9471ded 100644 --- a/contracts/contracts/utils/UniswapV3Helper.sol +++ b/contracts/contracts/utils/UniswapV3Helper.sol @@ -10,7 +10,7 @@ import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; import { INonfungiblePositionManager } from "../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; /** - * @dev Uniswap V3 Contracts use Solidity v0.7.6 and OUSD contracts are on 0.8.6. + * @dev Uniswap V3 Contracts use Solidity v0.7.6 and OUSD contracts are on 0.8.7. * So, the libraries cannot be directly imported into OUSD contracts. * This contract (on v0.7.6) just proxies the calls to the Uniswap Libraries. */ From 87324d0c9ad5aa2046e463b8abcad2ed22d97553 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 17 Mar 2023 11:48:40 +0100 Subject: [PATCH 21/83] possible contract moves to circumvent delegate calls (#1263) * possible contract moves to circumvent delegate calls * add comment * fix issues * fixture fixes --- .../uniswap/UniswapV3LiquidityManager.sol | 92 +++++++++++-------- .../strategies/uniswap/UniswapV3Strategy.sol | 64 +------------ .../uniswap/UniswapV3StrategyStorage.sol | 29 ++++++ .../deploy/049_uniswap_usdc_usdt_strategy.js | 6 -- contracts/test/_fixture.js | 13 +-- 5 files changed, 88 insertions(+), 116 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 598dda1308..8ff429e55d 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; import { UniswapV3StrategyStorage } from "./UniswapV3StrategyStorage.sol"; -import { UniswapV3Library } from "./UniswapV3Library.sol"; import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; import { IVault } from "../../interfaces/IVault.sol"; @@ -15,23 +14,16 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { using SafeERC20 for IERC20; - /*************************************** - Deposit/Withdraw - ****************************************/ - function _depositAll() internal { - UniswapV3Library.depositAll( - token0, - token1, - vaultAddress, - poolTokens[token0].minDepositThreshold, - poolTokens[token1].minDepositThreshold - ); - } - - // TODO: Intentionally left out non-reentrant modifier since otherwise Vault would throw function withdrawAssetFromActivePosition(address _asset, uint256 amount) external onlyVault + nonReentrant + { + _withdrawAssetFromActivePosition(_asset, amount); + } + + function _withdrawAssetFromActivePosition(address _asset, uint256 amount) + internal { Position memory position = tokenIdToPosition[activeTokenId]; require(position.exists && position.liquidity > 0, "Liquidity error"); @@ -187,7 +179,13 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { activeTokenId = tokenId; // Move any leftovers to Reserve - _depositAll(); + _depositAll( + token0, + token1, + vaultAddress, + poolTokens[token0].minDepositThreshold, + poolTokens[token1].minDepositThreshold + ); } struct SwapAndRebalanceParams { @@ -262,7 +260,13 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { activeTokenId = tokenId; // Move any leftovers to Reserve - _depositAll(); + _depositAll( + token0, + token1, + vaultAddress, + poolTokens[token0].minDepositThreshold, + poolTokens[token1].minDepositThreshold + ); } /*************************************** @@ -550,7 +554,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 minAmount0, uint256 minAmount1 ) - external + public onlyGovernorOrStrategistOrOperator nonReentrant returns (uint256 amount0, uint256 amount1) @@ -558,22 +562,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { return _closePosition(tokenId, minAmount0, minAmount1); } - /** - * @notice Same as closePosition() but only callable by Vault and doesn't have non-reentrant - * @param tokenId Token ID of the position to collect fees of. - * @param minAmount0 Min amount of token0 to receive back - * @param minAmount1 Min amount of token1 to receive back - * @return amount0 Amount of token0 received after removing liquidity - * @return amount1 Amount of token1 received after removing liquidity - */ - function closePositionOnlyVault( - uint256 tokenId, - uint256 minAmount0, - uint256 minAmount1 - ) external onlyVault returns (uint256 amount0, uint256 amount1) { - return _closePosition(tokenId, minAmount0, minAmount1); - } - /*************************************** Balances and Fees ****************************************/ @@ -784,15 +772,41 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { } function withdraw( - address _recipient, + address recipient, address _asset, uint256 amount - ) external virtual override { - revert("NO_IMPL"); + ) external override onlyVault onlyPoolTokens(_asset) nonReentrant { + IERC20 asset = IERC20(_asset); + uint256 selfBalance = asset.balanceOf(address(this)); + + if (selfBalance < amount) { + _withdrawAssetFromActivePosition(_asset, amount - selfBalance); + } + + // Transfer requested amount + asset.safeTransfer(recipient, amount); + emit Withdrawal(_asset, _asset, amount); } - function withdrawAll() external virtual override { - revert("NO_IMPL"); + /** + * @notice Closes active LP position, if any, and transfer all token balance to Vault + */ + function withdrawAll() external override onlyVault nonReentrant { + if (activeTokenId > 0) { + _closePosition(activeTokenId, 0, 0); + } + + // saves 100B of contract size to loop through these 2 tokens + address[2] memory tokens = [token0, token1]; + for(uint256 i = 0; i < 2; i++) { + IERC20 tokenContract = IERC20(tokens[i]); + uint256 tokenBalance = tokenContract.balanceOf(address(this)); + + if (tokenBalance > 0) { + tokenContract.safeTransfer(vaultAddress, tokenBalance); + emit Withdrawal(tokens[i], tokens[i], tokenBalance); + } + } } function checkBalance(address _asset) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 845a39b662..d4dcdfe0b8 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -234,14 +234,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { /// @inheritdoc InitializableAbstractStrategy function depositAll() external override onlyVault nonReentrant { - _depositAll(); - } - - /** - * @notice Deposits all undeployed balances of the contract to the reserve strategies - */ - function _depositAll() internal { - UniswapV3Library.depositAll( + _depositAll( token0, token1, vaultAddress, @@ -258,64 +251,15 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { address _asset, uint256 amount ) external override onlyVault onlyPoolTokens(_asset) nonReentrant { - IERC20 asset = IERC20(_asset); - uint256 selfBalance = asset.balanceOf(address(this)); - - if (selfBalance < amount) { - (bool success, bytes memory data) = address(this).delegatecall( - abi.encodeWithSignature( - "withdrawAssetFromActivePosition(asset,uint256)", - _asset, - amount - selfBalance - ) - ); - - require(success, "Failed to liquidate active position"); - } - - // Transfer requested amount - asset.safeTransfer(recipient, amount); - emit Withdrawal(_asset, _asset, amount); + revert("NO_IMPL"); } /** * @notice Closes active LP position, if any, and transfer all token balance to Vault * @inheritdoc InitializableAbstractStrategy */ - function withdrawAll() external override onlyVault nonReentrant { - if (activeTokenId > 0) { - // TODO: This method is only callable from Vault directly - // and by Governor or Strategist indirectly. - // Changing the Vault code to pass a minAmount0 and minAmount1 will - // make things complex. We could perhaps make sure that there're no - // active position when withdrawingAll rather than passing zero values? - - (bool success, bytes memory data) = address(this).delegatecall( - abi.encodeWithSignature( - "closePositionOnlyVault(uint256,uint256,uint256)", - activeTokenId, - 0, - 0 - ) - ); - - require(success, "Failed to close active position"); - } - - IERC20 token0Contract = IERC20(token0); - IERC20 token1Contract = IERC20(token1); - - uint256 token0Balance = token0Contract.balanceOf(address(this)); - if (token0Balance > 0) { - token0Contract.safeTransfer(vaultAddress, token0Balance); - emit Withdrawal(token0, token0, token0Balance); - } - - uint256 token1Balance = token1Contract.balanceOf(address(this)); - if (token1Balance > 0) { - token1Contract.safeTransfer(vaultAddress, token1Balance); - emit Withdrawal(token1, token1, token1Balance); - } + function withdrawAll() external virtual override { + revert("NO_IMPL"); } /*************************************** diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 920a3e2a7f..0bf73e44ca 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -5,6 +5,7 @@ import { InitializableAbstractStrategy } from "../../utils/InitializableAbstract import { IStrategy } from "../../interfaces/IStrategy.sol"; import { IVault } from "../../interfaces/IVault.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; import { IUniswapV3Helper } from "../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; @@ -204,4 +205,32 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { require(msg.sender == address(_self), "Not self"); _; } + + function _depositAll( + address token0, + address token1, + address vaultAddress, + uint256 minDepositThreshold0, + uint256 minDepositThreshold1 + ) internal { + IUniswapV3Strategy strat = IUniswapV3Strategy(msg.sender); + + uint256 token0Bal = IERC20(token0).balanceOf(address(this)); + uint256 token1Bal = IERC20(token1).balanceOf(address(this)); + IVault vault = IVault(vaultAddress); + + if ( + token0Bal > 0 && + (minDepositThreshold0 == 0 || token0Bal >= minDepositThreshold0) + ) { + vault.depositToUniswapV3Reserve(token0, token0Bal); + } + if ( + token1Bal > 0 && + (minDepositThreshold1 == 0 || token1Bal >= minDepositThreshold1) + ) { + vault.depositToUniswapV3Reserve(token1, token1Bal); + } + // Not emitting Deposit events since the Reserve strategies would do so + } } diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index 45660b390b..fa22a77e90 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -50,17 +50,11 @@ module.exports = deploymentWithGovernanceProposal( "UniswapV3Strategy", undefined, undefined, - { - UniswapV3Library: dUniV3Lib.address, - } ); const dUniV3PoolLiquidityManager = await deployWithConfirmation( "UniswapV3LiquidityManager", undefined, undefined, - { - UniswapV3Library: dUniV3Lib.address, - } ); const cUniV3_USDC_USDT_Strategy = await ethers.getContractAt( "UniswapV3Strategy", diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 6d1c100f58..a2143b4177 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -112,7 +112,6 @@ async function defaultFixture() { const buyback = await ethers.getContract("Buyback"); - const UniV3Lib = await ethers.getContract("UniswapV3Library"); const UniV3_USDC_USDT_Proxy = await ethers.getContract( "UniV3_USDC_USDT_Proxy" ); @@ -120,18 +119,10 @@ async function defaultFixture() { Array.from( new Set([ ...( - await ethers.getContractFactory("UniswapV3Strategy", { - libraries: { - UniswapV3Library: UniV3Lib.address, - }, - }) + await ethers.getContractFactory("UniswapV3Strategy") ).interface.format("full"), ...( - await ethers.getContractFactory("UniswapV3LiquidityManager", { - libraries: { - UniswapV3Library: UniV3Lib.address, - }, - }) + await ethers.getContractFactory("UniswapV3LiquidityManager") ).interface.format("full"), ]) ), From eed621235c6b430d1f7f8eb97e0db2dda683d368 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 17 Mar 2023 22:55:24 +0530 Subject: [PATCH 22/83] cleanup --- .../strategies/uniswap/UniswapV3Library.sol | 54 ---- .../uniswap/UniswapV3LiquidityManager.sol | 20 +- .../strategies/uniswap/UniswapV3Strategy.sol | 26 +- .../uniswap/UniswapV3StrategyStorage.sol | 13 +- .../contracts/utils/UniswapV3StrategyLib.sol | 239 ------------------ contracts/deploy/001_core.js | 64 ++--- .../deploy/049_uniswap_usdc_usdt_strategy.js | 15 +- contracts/test/_fixture.js | 19 +- 8 files changed, 78 insertions(+), 372 deletions(-) delete mode 100644 contracts/contracts/strategies/uniswap/UniswapV3Library.sol delete mode 100644 contracts/contracts/utils/UniswapV3StrategyLib.sol diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Library.sol b/contracts/contracts/strategies/uniswap/UniswapV3Library.sol deleted file mode 100644 index 274ebb9cee..0000000000 --- a/contracts/contracts/strategies/uniswap/UniswapV3Library.sol +++ /dev/null @@ -1,54 +0,0 @@ -pragma solidity ^0.8.0; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; -import { IVault } from "../../interfaces/IVault.sol"; - -library UniswapV3Library { - function depositAll( - address token0, - address token1, - address vaultAddress, - uint256 minDepositThreshold0, - uint256 minDepositThreshold1 - ) external { - IUniswapV3Strategy strat = IUniswapV3Strategy(msg.sender); - - uint256 token0Bal = IERC20(token0).balanceOf(address(this)); - uint256 token1Bal = IERC20(token1).balanceOf(address(this)); - IVault vault = IVault(vaultAddress); - - if ( - token0Bal > 0 && - (minDepositThreshold0 == 0 || token0Bal >= minDepositThreshold0) - ) { - vault.depositToUniswapV3Reserve(token0, token0Bal); - } - if ( - token1Bal > 0 && - (minDepositThreshold1 == 0 || token1Bal >= minDepositThreshold1) - ) { - vault.depositToUniswapV3Reserve(token1, token1Bal); - } - // Not emitting Deposit events since the Reserve strategies would do so - } - - // function closePosition(uint256 tokenId, uint256 minAmount0, uint256 minAmount1) external { - // (bool success, bytes memory data) = address(this).delegatecall( - // abi.encodeWithSignature("closePosition(uint256,uint256,uint256)", activeTokenId, 0, 0) - // ); - - // require(success, "Failed to close active position"); - // } - - // function withdrawAssetFromActivePosition( - // address asset, - // uint256 amount - // ) external { - // (bool success, bytes memory data) = address(this).delegatecall( - // abi.encodeWithSignature("withdrawAssetFromActivePosition(asset,uint256)", asset, amount) - // ); - - // require(success, "Failed to liquidate active position"); - // } -} diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 8ff429e55d..b465619482 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -179,13 +179,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { activeTokenId = tokenId; // Move any leftovers to Reserve - _depositAll( - token0, - token1, - vaultAddress, - poolTokens[token0].minDepositThreshold, - poolTokens[token1].minDepositThreshold - ); + _depositAll(); } struct SwapAndRebalanceParams { @@ -260,13 +254,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { activeTokenId = tokenId; // Move any leftovers to Reserve - _depositAll( - token0, - token1, - vaultAddress, - poolTokens[token0].minDepositThreshold, - poolTokens[token1].minDepositThreshold - ); + _depositAll(); } /*************************************** @@ -798,8 +786,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // saves 100B of contract size to loop through these 2 tokens address[2] memory tokens = [token0, token1]; - for(uint256 i = 0; i < 2; i++) { - IERC20 tokenContract = IERC20(tokens[i]); + for (uint256 i = 0; i < 2; i++) { + IERC20 tokenContract = IERC20(tokens[i]); uint256 tokenBalance = tokenContract.balanceOf(address(this)); if (tokenBalance > 0) { diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index d4dcdfe0b8..0cd6f81eed 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { UniswapV3StrategyStorage } from "./UniswapV3StrategyStorage.sol"; -import { UniswapV3Library } from "./UniswapV3Library.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -107,6 +106,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { function setReserveStrategy(address _asset, address _reserveStrategy) external onlyGovernorOrStrategist + onlyPoolTokens(_asset) nonReentrant { require( @@ -153,6 +153,10 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { emit MinDepositThresholdChanged(_asset, _minThreshold); } + /** + * @notice Toggle rebalance methods + * @param _paused True if rebalance has to be paused + */ function setRebalancePaused(bool _paused) external onlyGovernorOrStrategist @@ -161,19 +165,15 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { emit RebalancePauseStatusChanged(_paused); } + /** + * @notice Toggle swapAndRebalance method + * @param _paused True if swaps have to be paused + */ function setSwapsPaused(bool _paused) external onlyGovernorOrStrategist { swapsPaused = _paused; emit SwapsPauseStatusChanged(_paused); } - function setMaxSwapSlippage(uint24 _maxSlippage) - external - onlyGovernorOrStrategist - { - maxSwapSlippage = _maxSlippage; - emit MaxSwapSlippageChanged(_maxSlippage); - } - /** * @notice Change the swap price threshold * @param minTick Minimum price tick index @@ -234,13 +234,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { /// @inheritdoc InitializableAbstractStrategy function depositAll() external override onlyVault nonReentrant { - _depositAll( - token0, - token1, - vaultAddress, - poolTokens[token0].minDepositThreshold, - poolTokens[token1].minDepositThreshold - ); + _depositAll(); } /** diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 0bf73e44ca..aee4e72062 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -27,7 +27,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { int24 maxTick, uint160 maxSwapPriceX96 ); - event MaxSwapSlippageChanged(uint24 maxSlippage); event TokenPriceLimitChanged( int24 minTick, uint160 minPriceLimitX96, @@ -102,7 +101,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { address public token1; // Token1 of Uniswap V3 Pool uint24 public poolFee; // Uniswap V3 Pool Fee - uint24 public maxSwapSlippage = 100; // 1%; Reverts if swap slippage is higher than this bool public swapsPaused = false; // True if Swaps are paused bool public rebalancePaused = false; // True if Swaps are paused @@ -206,19 +204,16 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { _; } - function _depositAll( - address token0, - address token1, - address vaultAddress, - uint256 minDepositThreshold0, - uint256 minDepositThreshold1 - ) internal { + function _depositAll() internal { IUniswapV3Strategy strat = IUniswapV3Strategy(msg.sender); uint256 token0Bal = IERC20(token0).balanceOf(address(this)); uint256 token1Bal = IERC20(token1).balanceOf(address(this)); IVault vault = IVault(vaultAddress); + uint256 minDepositThreshold0 = poolTokens[token0].minDepositThreshold; + uint256 minDepositThreshold1 = poolTokens[token1].minDepositThreshold; + if ( token0Bal > 0 && (minDepositThreshold0 == 0 || token0Bal >= minDepositThreshold0) diff --git a/contracts/contracts/utils/UniswapV3StrategyLib.sol b/contracts/contracts/utils/UniswapV3StrategyLib.sol deleted file mode 100644 index af08a1204c..0000000000 --- a/contracts/contracts/utils/UniswapV3StrategyLib.sol +++ /dev/null @@ -1,239 +0,0 @@ -// SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.8.0; - -import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; -import "../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; -import "../interfaces/uniswap/v3/IUniswapV3Helper.sol"; - -library UniswapV3StrategyLib { - // Represents a position minted by UniswapV3Strategy contract - struct Position { - bytes32 positionKey; // Required to read collectible fees from the V3 Pool - uint256 tokenId; // ERC721 token Id of the minted position - uint128 liquidity; // Amount of liquidity deployed - int24 lowerTick; // Lower tick index - int24 upperTick; // Upper tick index - bool exists; // True, if position is minted - // 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; - } - - event UniswapV3LiquidityAdded( - uint256 indexed tokenId, - uint256 amount0Sent, - uint256 amount1Sent, - uint128 liquidityMinted - ); - event UniswapV3LiquidityRemoved( - uint256 indexed tokenId, - uint256 amount0Received, - uint256 amount1Received, - uint128 liquidityBurned - ); - event UniswapV3PositionClosed( - uint256 indexed tokenId, - uint256 amount0Received, - uint256 amount1Received - ); - event UniswapV3FeeCollected( - uint256 indexed tokenId, - uint256 amount0, - uint256 amount1 - ); - - /** - * @notice Increases liquidity of the position in the pool - * @param positionManager Uniswap V3 Position manager - * @param p Position object - * @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 - * @return liquidity Amount of liquidity added to the pool - * @return amount0 Amount of token0 added to the position - * @return amount1 Amount of token1 added to the position - */ - function increaseLiquidityForPosition( - address positionManager, - UniswapV3StrategyLib.Position storage p, - uint256 desiredAmount0, - uint256 desiredAmount1, - uint256 minAmount0, - uint256 minAmount1 - ) - external - returns ( - uint128 liquidity, - uint256 amount0, - uint256 amount1 - ) - { - require(p.exists, "Unknown position"); - - INonfungiblePositionManager.IncreaseLiquidityParams - memory params = INonfungiblePositionManager - .IncreaseLiquidityParams({ - tokenId: p.tokenId, - amount0Desired: desiredAmount0, - amount1Desired: desiredAmount1, - amount0Min: minAmount0, - amount1Min: minAmount1, - deadline: block.timestamp - }); - - (liquidity, amount0, amount1) = INonfungiblePositionManager( - positionManager - ).increaseLiquidity(params); - - p.liquidity += liquidity; - - emit UniswapV3LiquidityAdded(p.tokenId, amount0, amount1, liquidity); - } - - /** - * @notice Removes liquidity of the position in the pool - * - * @param poolAddress Uniswap V3 pool address - * @param positionManager Uniswap V3 Position manager - * @param v3Helper Uniswap V3 helper contract - * @param p Position object reference - * @param liquidity Amount of liquidity to remove form the position - * @param minAmount0 Min amount of token0 to withdraw - * @param minAmount1 Min amount of token1 to withdraw - * - * @return amount0 Amount of token0 received after liquidation - * @return amount1 Amount of token1 received after liquidation - */ - function decreaseLiquidityForPosition( - address poolAddress, - address positionManager, - address v3Helper, - Position storage p, - uint128 liquidity, - uint256 minAmount0, - uint256 minAmount1 - ) external returns (uint256 amount0, uint256 amount1) { - require(p.exists, "Unknown position"); - - (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(poolAddress) - .slot0(); - (uint256 exactAmount0, uint256 exactAmount1) = IUniswapV3Helper( - v3Helper - ).getAmountsForLiquidity( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - liquidity - ); - - INonfungiblePositionManager.DecreaseLiquidityParams - memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: p.tokenId, - liquidity: liquidity, - amount0Min: minAmount0, - amount1Min: minAmount1, - deadline: block.timestamp - }); - - (amount0, amount1) = INonfungiblePositionManager(positionManager) - .decreaseLiquidity(params); - - p.liquidity -= liquidity; - - emit UniswapV3LiquidityRemoved(p.tokenId, amount0, amount1, liquidity); - } - - /** - * @notice Collects the fees generated by the position on V3 pool - * @param positionManager Uniswap V3 Position manager - * @param tokenId Token ID of the position to collect fees of. - * @return amount0 Amount of token0 collected as fee - * @return amount1 Amount of token1 collected as fee - */ - function collectFeesForToken(address positionManager, uint256 tokenId) - external - returns (uint256 amount0, uint256 amount1) - { - INonfungiblePositionManager.CollectParams - memory params = INonfungiblePositionManager.CollectParams({ - tokenId: tokenId, - recipient: address(this), - amount0Max: type(uint128).max, - amount1Max: type(uint128).max - }); - - (amount0, amount1) = INonfungiblePositionManager(positionManager) - .collect(params); - - emit UniswapV3FeeCollected(tokenId, amount0, amount1); - } - - /** - * @notice Calculates the amount liquidity that needs to be removed - * to Withdraw specified amount of the given asset. - * - * @param poolAddress Uniswap V3 pool address - * @param v3Helper Uniswap V3 helper contract - * @param p Position object - * @param _asset Token needed - * @param amount Minimum amount to liquidate - * - * @return liquidity Liquidity to burn - * @return minAmount0 Minimum amount0 to expect - * @return minAmount1 Minimum amount1 to expect - */ - function calculateLiquidityToWithdraw( - address poolAddress, - address v3Helper, - UniswapV3StrategyLib.Position memory p, - address _asset, - uint256 amount - ) - external - view - returns ( - uint128 liquidity, - uint256 minAmount0, - uint256 minAmount1 - ) - { - IUniswapV3Helper uniswapV3Helper = IUniswapV3Helper(v3Helper); - IUniswapV3Pool pool = IUniswapV3Pool(poolAddress); - (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); - - // Total amount in Liquidity pools - (uint256 totalAmount0, uint256 totalAmount1) = uniswapV3Helper - .getAmountsForLiquidity( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - p.liquidity - ); - - if (_asset == pool.token0()) { - minAmount0 = amount; - minAmount1 = totalAmount1 / (totalAmount0 / amount); - liquidity = uniswapV3Helper.getLiquidityForAmounts( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - amount, - minAmount1 - ); - } else if (_asset == pool.token1()) { - minAmount0 = totalAmount0 / (totalAmount1 / amount); - minAmount1 = amount; - liquidity = uniswapV3Helper.getLiquidityForAmounts( - sqrtRatioX96, - p.sqrtRatioAX96, - p.sqrtRatioBX96, - minAmount0, - amount - ); - } - } -} diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 88ba2d7261..52b6a301dc 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -638,7 +638,7 @@ const configureStrategies = async (harvesterProxy) => { const uniV3UsdcUsdtProxy = await ethers.getContract("UniV3_USDC_USDT_Proxy"); const uniV3UsdcUsdt = await ethers.getContractAt( - "GeneralizedUniswapV3Strategy", + "UniswapV3Strategy", uniV3UsdcUsdtProxy.address ); await withConfirmation( @@ -992,76 +992,80 @@ const deployUniswapV3Strategy = async () => { [mockUSDC.address, mockUSDT.address], ]); + await deployWithConfirmation( + "MockStrategy2", + [vault.address, [mockUSDC.address, mockUSDT.address]], + "MockStrategy" + ); + await deployWithConfirmation( "MockStrategyDAI", [vault.address, [mockDAI.address]], "MockStrategy" ); - const mockStrat = await ethers.getContract("MockStrategy"); + // const mockStrat = await ethers.getContract("MockStrategy"); - const UniswapV3StrategyLib = await deployWithConfirmation( - "UniswapV3StrategyLib" + const dUniswapV3Strategy = await deployWithConfirmation("UniswapV3Strategy"); + const dUniswapV3LiquidityManager = await deployWithConfirmation( + "UniswapV3LiquidityManager" ); - const uniV3UsdcUsdtImpl = await deployWithConfirmation( - "UniV3_USDC_USDT_Strategy", - [], - "GeneralizedUniswapV3Strategy", - { - UniswapV3StrategyLib: UniswapV3StrategyLib.address, - } - ); await deployWithConfirmation("UniV3_USDC_USDT_Proxy"); - const uniV3UsdcUsdtProxy = await ethers.getContract("UniV3_USDC_USDT_Proxy"); + const uniV3Proxy = await ethers.getContract("UniV3_USDC_USDT_Proxy"); await withConfirmation( - uniV3UsdcUsdtProxy["initialize(address,address,bytes)"]( - uniV3UsdcUsdtImpl.address, + uniV3Proxy["initialize(address,address,bytes)"]( + dUniswapV3Strategy.address, deployerAddr, [] ) ); log("Initialized UniV3_USDC_USDT_Proxy"); - const uniV3UsdcUsdtStrat = await ethers.getContractAt( - "GeneralizedUniswapV3Strategy", - uniV3UsdcUsdtProxy.address + const uniV3Strat = await ethers.getContractAt( + "UniswapV3Strategy", + uniV3Proxy.address ); await withConfirmation( - uniV3UsdcUsdtStrat + uniV3Strat .connect(sDeployer) - ["initialize(address,address,address,address,address,address,address)"]( + ["initialize(address,address,address,address,address,address)"]( vault.address, pool.address, manager.address, - mockStrat.address, - mockStrat.address, - operatorAddr, v3Helper.address, - mockRouter.address + mockRouter.address, + operatorAddr ) ); - log("Initialized UniV3_USDC_USDT_Strategy"); + log("Initialized UniswapV3Strategy"); + + await withConfirmation( + uniV3Strat + .connect(sDeployer) + .setLiquidityManagerImpl(dUniswapV3LiquidityManager.address) + ); + log("Initialized UniswapV3LiquidityManager"); await withConfirmation( - uniV3UsdcUsdtStrat.connect(sDeployer).transferGovernance(governorAddr) + uniV3Strat.connect(sDeployer).transferGovernance(governorAddr) ); - log(`UniV3_USDC_USDT_Strategy transferGovernance(${governorAddr}) called`); + log(`UniswapV3Strategy transferGovernance(${governorAddr}) called`); // On Mainnet the governance transfer gets executed separately, via the // multi-sig wallet. On other networks, this migration script can claim // governance by the governor. if (!isMainnet) { await withConfirmation( - uniV3UsdcUsdtStrat + uniV3Strat .connect(sGovernor) // Claim governance with governor .claimGovernance() ); log("Claimed governance for UniV3_USDC_USDT_Strategy"); } - return uniV3UsdcUsdtStrat; + return uniV3Strat; }; const main = async () => { @@ -1076,7 +1080,7 @@ const main = async () => { await deployConvexStrategy(); await deployConvexOUSDMetaStrategy(); await deployConvexLUSDMetaStrategy(); - // await deployUniswapV3Strategy(); + await deployUniswapV3Strategy(); const harvesterProxy = await deployHarvester(); await configureVault(harvesterProxy); await configureStrategies(harvesterProxy); diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index fa22a77e90..b8ed3ce3b1 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -29,7 +29,6 @@ module.exports = deploymentWithGovernanceProposal( // 0. Deploy UniswapV3Helper and UniswapV3StrategyLib const dUniswapV3Helper = await deployWithConfirmation("UniswapV3Helper"); - const dUniV3Lib = await deployWithConfirmation("UniswapV3Library"); // 0. Upgrade VaultAdmin const dVaultAdmin = await deployWithConfirmation("VaultAdmin"); @@ -47,14 +46,10 @@ module.exports = deploymentWithGovernanceProposal( // 2. Deploy new implementation const dUniV3_USDC_USDT_StrategyImpl = await deployWithConfirmation( - "UniswapV3Strategy", - undefined, - undefined, + "UniswapV3Strategy" ); const dUniV3PoolLiquidityManager = await deployWithConfirmation( - "UniswapV3LiquidityManager", - undefined, - undefined, + "UniswapV3LiquidityManager" ); const cUniV3_USDC_USDT_Strategy = await ethers.getContractAt( "UniswapV3Strategy", @@ -169,6 +164,12 @@ module.exports = deploymentWithGovernanceProposal( signature: "setReserveStrategy(address,address)", args: [assetAddresses.USDT, cMorphoCompProxy.address], }, + // 4. Set Reserve Strategy for USDT + { + contract: cUniV3_USDC_USDT_Strategy, + signature: "setSwapPriceThreshold(int24,int24)", + args: [-1000, 1000], + }, ], }; } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index a2143b4177..416426dbab 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -30,7 +30,7 @@ const threepoolSwapAbi = require("./abi/threepoolSwap.json"); async function defaultFixture() { await deployments.fixture(isFork ? undefined : ["unit_tests"], { - keepExistingDeployments: true, // Boolean(isForkWithLocalNode), + keepExistingDeployments: true, }); const { governorAddr, timelockAddr, operatorAddr } = await getNamedAccounts(); @@ -182,6 +182,7 @@ async function defaultFixture() { UniV3_USDC_USDT_Pool, UniV3SwapRouter, mockStrategy, + mockStrategy2, mockStrategyDAI; if (isFork) { @@ -339,6 +340,7 @@ async function defaultFixture() { ); UniV3_USDC_USDT_Pool = await ethers.getContract("MockUniswapV3Pool"); mockStrategy = await ethers.getContract("MockStrategy"); + mockStrategy2 = await ethers.getContract("MockStrategy2"); mockStrategyDAI = await ethers.getContract("MockStrategyDAI"); } if (!isFork) { @@ -482,6 +484,7 @@ async function defaultFixture() { UniV3SwapRouter, mockStrategy, mockStrategyDAI, + mockStrategy2, }; } @@ -1242,12 +1245,14 @@ function uniswapV3FixturSetup() { dai, UniV3_USDC_USDT_Strategy, mockStrategy, + mockStrategy2, mockStrategyDAI, } = fixture; if (!isFork) { // Approve mockStrategy await _approveStrategy(fixture, mockStrategy); + await _approveStrategy(fixture, mockStrategy2); await _approveStrategy(fixture, mockStrategyDAI); // Approve Uniswap V3 Strategy @@ -1258,9 +1263,21 @@ function uniswapV3FixturSetup() { await _setDefaultStrategy(fixture, usdc, UniV3_USDC_USDT_Strategy); await _setDefaultStrategy(fixture, usdt, UniV3_USDC_USDT_Strategy); + // await UniV3_USDC_USDT_Strategy.setSwapPriceThreshold(-1000, 1000); + if (!isFork) { // And a different one for DAI await _setDefaultStrategy(fixture, dai, mockStrategyDAI); + + // Set reserve strategy + await UniV3_USDC_USDT_Strategy.setReserveStrategy( + usdc.address, + mockStrategy.address + ); + await UniV3_USDC_USDT_Strategy.setReserveStrategy( + usdt.address, + mockStrategy.address + ); } return fixture; From 47dc4b503d813100c726aa379f44fe561b557bab Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 17 Mar 2023 22:55:41 +0530 Subject: [PATCH 23/83] some basic unit tests --- contracts/test/strategies/uniswap-v3.js | 180 +++++++++++++++++++++++- 1 file changed, 174 insertions(+), 6 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 5f80da3134..4f700c6c55 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -7,12 +7,19 @@ const uniswapV3Fixture = uniswapV3FixturSetup(); describe("Uniswap V3 Strategy", function () { let fixture; let vault, harvester, ousd, usdc, usdt, dai; - let reserveStrategy, strategy, mockPool, mockPositionManager; + let reserveStrategy, + strategy, + mockPool, + mockPositionManager, + mockStrategy2, + mockStrategyDAI; let governor, strategist, operator, josh, matt, daniel, domen, franck; beforeEach(async () => { fixture = await uniswapV3Fixture(); reserveStrategy = fixture.mockStrategy; + mockStrategy2 = fixture.mockStrategy2; + mockStrategyDAI = fixture.mockStrategyDAI; strategy = fixture.UniV3_USDC_USDT_Strategy; mockPool = fixture.UniV3_USDC_USDT_Pool; mockPositionManager = fixture.UniV3PositionManager; @@ -95,12 +102,173 @@ describe("Uniswap V3 Strategy", function () { await vault.connect(matt).redeem(ousdUnits("30000"), 0); await expectApproxSupply(ousd, ousdUnits("200")); }); + + it.skip("Should withdraw from active position"); }); - describe("Rewards", function () { - it("Should show correct amount of fees", async () => {}); + + describe("Balance & Fees", () => { + describe("getPendingFees()", () => {}); + describe("checkBalance()", () => {}); + describe("checkBalanceOfAllAssets()", () => {}); }); - describe("Rebalance", function () { - it("Should provide liquidity on given tick", async () => {}); - it("Should close existing position", async () => {}); + + describe("Admin functions", () => { + describe("setOperator()", () => { + it("Governor can set the operator", async () => { + const addr1 = "0x0000000000000000000000000000000000011111"; + await strategy.connect(governor).setOperator(addr1); + expect(await strategy.operatorAddr()).to.equal(addr1); + }); + it("Strategist can set the operator", async () => { + const addr1 = "0x0000000000000000000000000000000000011111"; + await strategy.connect(strategist).setOperator(addr1); + expect(await strategy.operatorAddr()).to.equal(addr1); + }); + it("Should not change operator if not governor/strategist", async () => { + const addr1 = "0x0000000000000000000000000000000000011111"; + await expect( + strategy.connect(operator).setOperator(addr1) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + }); + }); + + describe("setReserveStrategy()", () => { + describe("Validations", () => { + it("Can set a valid strategy as reserve", async () => { + await strategy + .connect(governor) + .setReserveStrategy(usdc.address, mockStrategy2.address); + expect(await strategy.reserveStrategy(usdc.address)).to.equal( + mockStrategy2.address + ); + }); + it("Cannot set an unsupported strategy as reserve", async () => { + await expect( + strategy + .connect(governor) + .setReserveStrategy(usdc.address, mockStrategyDAI.address) + ).to.be.revertedWith("Invalid strategy for asset"); + }); + it("Cannot set reserve strategy for unsupported assets", async () => { + await expect( + strategy + .connect(governor) + .setReserveStrategy(dai.address, mockStrategyDAI.address) + ).to.be.revertedWith("Unsupported asset"); + }); + it("Cannot set an unapproved strategy as reserve", async () => { + await vault.connect(governor).removeStrategy(mockStrategy2.address); + + await expect( + strategy + .connect(governor) + .setReserveStrategy(usdc.address, mockStrategy2.address) + ).to.be.revertedWith("Unsupported strategy"); + }); + }); + describe("Permissions", () => { + it("Governor can change reserve strategy", async () => { + await strategy + .connect(governor) + .setReserveStrategy(usdc.address, mockStrategy2.address); + expect(await strategy.reserveStrategy(usdc.address)).to.equal( + mockStrategy2.address + ); + }); + it("Strategist can change reserve strategy", async () => { + await strategy + .connect(strategist) + .setReserveStrategy(usdc.address, mockStrategy2.address); + expect(await strategy.reserveStrategy(usdc.address)).to.equal( + mockStrategy2.address + ); + }); + it("Anyone else cannot change reserve strategy", async () => { + await expect( + strategy + .connect(operator) + .setReserveStrategy(usdc.address, mockStrategy2.address) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + }); + }); + }); + + describe("setMinDepositThreshold()", () => { + describe("Permissions", () => { + it("Governer & Strategist can set the threshold", async () => { + await strategy + .connect(governor) + .setMinDepositThreshold(usdc.address, "1000"); + await strategy + .connect(strategist) + .setMinDepositThreshold(usdc.address, "2000"); + }); + it("Nobody else can change the threshold", async () => { + await expect( + strategy + .connect(operator) + .setMinDepositThreshold(usdc.address, "2000") + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + }); + }); + describe("Validations", () => { + it("Cannot call with invalid assets", async () => { + await expect( + strategy + .connect(operator) + .setMinDepositThreshold(dai.address, "2000") + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + }); + }); + }); + + describe("setRebalancePaused()", () => { + it("Governer & Strategist can pause rebalance", async () => { + await strategy.connect(governor).setRebalancePaused(true); + expect(await strategy.rebalancePaused()).to.be.true; + await strategy.connect(strategist).setRebalancePaused(false); + expect(await strategy.rebalancePaused()).to.be.false; + }); + it("Nobody else can pause rebalance", async () => { + await expect( + strategy.connect(operator).setRebalancePaused(false) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + }); + }); + + describe("setSwapsPaused()", () => { + it("Governer & Strategist can pause swaps", async () => { + await strategy.connect(governor).setSwapsPaused(true); + expect(await strategy.swapsPaused()).to.be.true; + await strategy.connect(strategist).setSwapsPaused(false); + expect(await strategy.swapsPaused()).to.be.false; + }); + it("Nobody else can pause swaps", async () => { + await expect( + strategy.connect(operator).setSwapsPaused(false) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + }); + }); + + describe.skip("setSwapPriceThreshold()", () => {}); + + describe.skip("setTokenPriceLimit()", () => {}); }); + + describe("LiquidityManager", function () { + it.skip("Should mint new position"); + it.skip("Should increase liquidity for active position"); + it.skip("Should close active position"); + it.skip("Should rebalance"); + it.skip("Should swap USDC for USDT and then rebalance"); + it.skip("Should swap USDT for USDC and then rebalance"); + }); + + // describe("Rewards", function () { + // it("Should show correct amount of fees", async () => {}); + // }); + // describe("Rebalance", function () { + // it("Should provide liquidity on given tick", async () => {}); + // it("Should close existing position", async () => {}); + // }); }); From 875f0de31b7f7ab3f13c1aeaaed96822e2150a88 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 18 Mar 2023 06:39:36 +0100 Subject: [PATCH 24/83] Sparrow dom/uniswap v3changes (#1264) * add limits within which the strategy is able to rebalance * let only governor set these threshold * add maxTVL check * tests for max tvl check * correct the tests * remove .only --- .../uniswap/UniswapV3LiquidityManager.sol | 53 +++++++++-------- .../strategies/uniswap/UniswapV3Strategy.sol | 43 +++++++------- .../uniswap/UniswapV3StrategyStorage.sol | 33 +++-------- .../deploy/049_uniswap_usdc_usdt_strategy.js | 14 ++--- contracts/test/_fixture.js | 19 ++++++- .../test/strategies/uniswap-v3.fork-test.js | 57 ++++++++++++++++++- 6 files changed, 138 insertions(+), 81 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index b465619482..1f7304a5f6 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import { UniswapV3StrategyStorage } from "./UniswapV3StrategyStorage.sol"; +import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; import { IVault } from "../../interfaces/IVault.sol"; import { IStrategy } from "../../interfaces/IStrategy.sol"; @@ -11,9 +12,11 @@ import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRou import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { using SafeERC20 for IERC20; - + function withdrawAssetFromActivePosition(address _asset, uint256 amount) external onlyVault @@ -112,6 +115,26 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { _; } + modifier withinRebalacingLimits(int24 lowerTick, int24 upperTick) { + require( + minRebalanceTick <= lowerTick && + maxRebalanceTick >= upperTick, + "Rebalance position out of bounds" + ); + _; + } + + modifier ensureTVL() { + _; + uint256 balance = InitializableAbstractStrategy(this).checkBalance(token0) + + InitializableAbstractStrategy(this).checkBalance(token1); + + require( + balance <= maxTVL, + "MaxTVL threshold has been reached" + ); + } + /** * @notice Closes active LP position if any and then provides liquidity to the requested position. * Mints new position, if it doesn't exist already. @@ -139,6 +162,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { onlyGovernorOrStrategistOrOperator nonReentrant rebalanceNotPaused + withinRebalacingLimits(lowerTick, upperTick) + ensureTVL { require(lowerTick < upperTick, "Invalid tick range"); @@ -202,6 +227,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { onlyGovernorOrStrategistOrOperator nonReentrant rebalanceNotPaused + withinRebalacingLimits(params.lowerTick, params.upperTick) + ensureTVL { require(params.lowerTick < params.upperTick, "Invalid tick range"); @@ -585,28 +612,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // TODO: Check value of assets moved here } - function _checkSwapLimits(uint160 sqrtPriceLimitX96, bool swapZeroForOne) - internal - view - { - require(!swapsPaused, "Swaps are paused"); - - (uint160 currentPriceX96, , , , , , ) = pool.slot0(); - - require( - minSwapPriceX96 <= currentPriceX96 && - currentPriceX96 <= maxSwapPriceX96, - "Price out of bounds" - ); - - require( - swapZeroForOne - ? (sqrtPriceLimitX96 >= minSwapPriceX96) - : (sqrtPriceLimitX96 <= maxSwapPriceX96), - "Slippage out of bounds" - ); - } - function _ensureAssetsBySwapping( uint256 desiredAmount0, uint256 desiredAmount1, @@ -615,7 +620,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint160 sqrtPriceLimitX96, bool swapZeroForOne ) internal { - _checkSwapLimits(sqrtPriceLimitX96, swapZeroForOne); + require(!swapsPaused, "Swaps are paused"); uint256 token0Balance = IERC20(token0).balanceOf(address(this)); uint256 token1Balance = IERC20(token1).balanceOf(address(this)); diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 0cd6f81eed..da19be6f0a 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -16,9 +16,12 @@ import { IUniswapV3Helper } from "../../interfaces/uniswap/v3/IUniswapV3Helper.s import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import { StableMath } from "../../utils/StableMath.sol"; +import { Helpers } from "../../utils/Helpers.sol"; contract UniswapV3Strategy is UniswapV3StrategyStorage { using SafeERC20 for IERC20; + using StableMath for uint256; /** * @dev Initialize the contract @@ -175,42 +178,34 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { } /** - * @notice Change the swap price threshold - * @param minTick Minimum price tick index - * @param maxTick Maximum price tick index + * @notice Change the maxTVL amount threshold + * @param _maxTVL Maximum amount the strategy can have deployed in the Uniswap pool */ - function setSwapPriceThreshold(int24 minTick, int24 maxTick) + function setMaxTVL(uint256 _maxTVL) external onlyGovernorOrStrategist { - require((minTick < maxTick), "Invalid threshold"); - minSwapPriceX96 = helper.getSqrtRatioAtTick(minTick); - maxSwapPriceX96 = helper.getSqrtRatioAtTick(maxTick); - emit SwapPriceThresholdChanged( - minTick, - minSwapPriceX96, - maxTick, - maxSwapPriceX96 - ); + maxTVL = _maxTVL; + emit MaxTVLChanged(_maxTVL); } /** - * @notice Change the token price limit + * @notice Change the rebalance price threshold * @param minTick Minimum price tick index * @param maxTick Maximum price tick index */ - function setTokenPriceLimit(int24 minTick, int24 maxTick) + function setRebalancePriceThreshold(int24 minTick, int24 maxTick) external onlyGovernorOrStrategist { require((minTick < maxTick), "Invalid threshold"); - minPriceLimitX96 = helper.getSqrtRatioAtTick(minTick); - maxPriceLimitX96 = helper.getSqrtRatioAtTick(maxTick); - emit TokenPriceLimitChanged( + minRebalanceTick = minTick; + maxRebalanceTick = maxTick; + emit RebalancePriceThresholdChanged( minTick, - minPriceLimitX96, + helper.getSqrtRatioAtTick(minTick), maxTick, - maxPriceLimitX96 + helper.getSqrtRatioAtTick(maxTick) ); } @@ -309,6 +304,9 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { balance += amount1; } } + + uint256 assetDecimals = Helpers.getDecimals(_asset); + balance = balance.scaleBy(18, assetDecimals); } function checkBalanceOfAllAssets() @@ -330,6 +328,11 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { amount0 += IERC20(token0).balanceOf(address(this)); amount1 += IERC20(token1).balanceOf(address(this)); + + uint256 asset0Decimals = Helpers.getDecimals(token0); + uint256 asset1Decimals = Helpers.getDecimals(token1); + amount0 = amount0.scaleBy(18, asset0Decimals); + amount1 = amount1.scaleBy(18, asset1Decimals); } /*************************************** diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index aee4e72062..ce0ca700e7 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -21,18 +21,14 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { ); event RebalancePauseStatusChanged(bool paused); event SwapsPauseStatusChanged(bool paused); - event SwapPriceThresholdChanged( + event RebalancePriceThresholdChanged( int24 minTick, - uint160 minSwapPriceX96, + uint160 minRebalancePriceX96, int24 maxTick, - uint160 maxSwapPriceX96 - ); - event TokenPriceLimitChanged( - int24 minTick, - uint160 minPriceLimitX96, - int24 maxTick, - uint160 maxPriceLimitX96 + uint160 maxRebalancePriceX96 ); + event MaxSwapSlippageChanged(uint24 maxSlippage); + event MaxTVLChanged(uint256 amountIn); event AssetSwappedForRebalancing( address indexed tokenIn, address indexed tokenOut, @@ -106,14 +102,9 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint256 public maxTVL; // In USD, 18 decimals - // An upper and lower bound of swap price limits - uint160 public minSwapPriceX96; - uint160 public maxSwapPriceX96; - - // Uses these params when checking the values of the tokens - // moved in and out of the reserve strategies - uint160 public minPriceLimitX96; - uint160 public maxPriceLimitX96; + // An upper and lower bound of rebalancing price limits + int24 public minRebalanceTick; + int24 public maxRebalanceTick; // Token ID of active Position on the pool. zero, if there are no active LP position uint256 public activeTokenId; @@ -196,14 +187,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { _; } - /** - * @dev Ensures that the caller is the proxy. - */ - modifier onlySelf() { - require(msg.sender == address(_self), "Not self"); - _; - } - function _depositAll() internal { IUniswapV3Strategy strat = IUniswapV3Strategy(msg.sender); diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index b8ed3ce3b1..9faf427deb 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -152,24 +152,24 @@ module.exports = deploymentWithGovernanceProposal( signature: "setHarvesterAddress(address)", args: [cHarvesterProxy.address], }, - // 4. Set Reserve Strategy for USDC + // 5. Set Reserve Strategy for USDC { contract: cUniV3_USDC_USDT_Strategy, signature: "setReserveStrategy(address,address)", args: [assetAddresses.USDC, cMorphoCompProxy.address], }, - // 4. Set Reserve Strategy for USDT + // 6. Set Reserve Strategy for USDT { contract: cUniV3_USDC_USDT_Strategy, signature: "setReserveStrategy(address,address)", args: [assetAddresses.USDT, cMorphoCompProxy.address], }, // 4. Set Reserve Strategy for USDT - { - contract: cUniV3_USDC_USDT_Strategy, - signature: "setSwapPriceThreshold(int24,int24)", - args: [-1000, 1000], - }, + // { + // contract: cUniV3_USDC_USDT_Strategy, + // signature: "setSwapPriceThreshold(int24,int24)", + // args: [-1000, 1000], + // }, ], }; } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 416426dbab..881f10b8fa 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1235,7 +1235,7 @@ async function rebornFixture() { return fixture; } -function uniswapV3FixturSetup() { +function uniswapV3FixtureSetup() { return deployments.createFixture(async () => { const fixture = await defaultFixture(); @@ -1280,6 +1280,21 @@ function uniswapV3FixturSetup() { ); } + const { governorAddr, timelockAddr } = await getNamedAccounts(); + const sGovernor = await ethers.provider.getSigner( + isFork ? timelockAddr : governorAddr + ); + + UniV3_USDC_USDT_Strategy + .connect(sGovernor) + // 2 million + .setMaxTVL(utils.parseUnits("2", 24)) + + UniV3_USDC_USDT_Strategy + .connect(sGovernor) + .setRebalancePriceThreshold(-100, 100) + + return fixture; }); } @@ -1331,7 +1346,7 @@ module.exports = { aaveVaultFixture, hackedVaultFixture, rebornFixture, - uniswapV3FixturSetup, + uniswapV3FixtureSetup, withImpersonatedAccount, impersonateAndFundContract, impersonateAccount, diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 1c583e528d..811540a4df 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -1,6 +1,6 @@ const { expect } = require("chai"); const { - uniswapV3FixturSetup, + uniswapV3FixtureSetup, impersonateAndFundContract, } = require("../_fixture"); const { @@ -10,12 +10,14 @@ const { usdcUnitsFormat, usdtUnitsFormat, daiUnits, + isFork, daiUnitsFormat, getBlockTimestamp, } = require("../helpers"); -const { BigNumber } = require("ethers"); +const { BigNumber, utils } = require("ethers"); +const { ethers } = hre; -const uniswapV3Fixture = uniswapV3FixturSetup(); +const uniswapV3Fixture = uniswapV3FixtureSetup(); forkOnlyDescribe("Uniswap V3 Strategy", function () { this.timeout(0); @@ -59,6 +61,31 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { franck = fixture.franck; }); + async function setRebalancePriceThreshold(lowerTick, upperTick) { + const { vault } = fixture; + const { governorAddr, timelockAddr } = await getNamedAccounts(); + const sGovernor = await ethers.provider.getSigner( + isFork ? timelockAddr : governorAddr + ); + + await strategy + .connect(sGovernor) + .setRebalancePriceThreshold(lowerTick, upperTick); + } + + // maxTvl is denominated in 18 decimals already + async function setMaxTVL(maxTvl) { + const { vault } = fixture; + const { governorAddr, timelockAddr } = await getNamedAccounts(); + const sGovernor = await ethers.provider.getSigner( + isFork ? timelockAddr : governorAddr + ); + + await strategy + .connect(sGovernor) + .setMaxTVL(utils.parseUnits(maxTvl, 18)); + } + describe("Uniswap V3 LP positions", function () { // NOTE: These tests all work on the assumption that the strategy // has no active position, which might not be true after deployment. @@ -230,6 +257,30 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(await strategy.activeTokenId()).to.equal(tokenId); }); + it("Should not mint if the position is out of hard boundary tick range", async () => { + await setRebalancePriceThreshold(-5, 5); + + const lowerTick = -10; + const upperTick = 10; + + await expect( + mintLiquidity(lowerTick, upperTick, "100000", "100000") + ).to.be.revertedWith("Rebalance position out of bounds"); + }); + + it("Should not mint if the position surpasses the maxTVL amount", async () => { + // set max TVL of 100 units (denominated in 18 decimals) + await setMaxTVL("100"); + + const lowerTick = -10; + const upperTick = 10; + + await expect( + mintLiquidity(lowerTick, upperTick, "100000", "100000") + ).to.be.revertedWith("MaxTVL threshold has been reached"); + + }); + it("Should swap USDC for USDT and mint position", async () => { // Move all USDT out of reserve await reserveStrategy From ef15c7163dd04f16735b8b4e2b680e1ecd3e5025 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 18 Mar 2023 12:28:56 +0530 Subject: [PATCH 25/83] Make some changes --- .../uniswap/UniswapV3LiquidityManager.sol | 174 ++++++++++-------- .../strategies/uniswap/UniswapV3Strategy.sol | 133 ++++++++----- .../uniswap/UniswapV3StrategyStorage.sol | 26 ++- contracts/test/_fixture.js | 15 +- .../test/strategies/uniswap-v3.fork-test.js | 5 +- 5 files changed, 218 insertions(+), 135 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 1f7304a5f6..88ff3f3b06 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -11,48 +11,24 @@ import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3 import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Helpers } from "../../utils/Helpers.sol"; +import { StableMath } from "../../utils/StableMath.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { using SafeERC20 for IERC20; - - function withdrawAssetFromActivePosition(address _asset, uint256 amount) - external - onlyVault - nonReentrant - { - _withdrawAssetFromActivePosition(_asset, amount); - } - - function _withdrawAssetFromActivePosition(address _asset, uint256 amount) - internal - { - Position memory position = tokenIdToPosition[activeTokenId]; - require(position.exists && position.liquidity > 0, "Liquidity error"); - - // Figure out liquidity to burn - ( - uint128 liquidity, - uint256 minAmount0, - uint256 minAmount1 - ) = _calculateLiquidityToWithdraw(position, _asset, amount); - - // Liquidiate active position - _decreasePositionLiquidity( - position.tokenId, - liquidity, - minAmount0, - minAmount1 - ); - } + using StableMath for uint256; + /*************************************** + Withdraw + ****************************************/ /** * @notice Calculates the amount liquidity that needs to be removed * to Withdraw specified amount of the given asset. * * @param position Position object - * @param _asset Token needed + * @param asset Token needed * @param amount Minimum amount to liquidate * * @return liquidity Liquidity to burn @@ -61,7 +37,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { */ function _calculateLiquidityToWithdraw( Position memory position, - address _asset, + address asset, uint256 amount ) internal @@ -83,7 +59,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { position.liquidity ); - if (_asset == token0) { + if (asset == token0) { minAmount0 = amount; minAmount1 = totalAmount1 / (totalAmount0 / amount); liquidity = helper.getLiquidityForAmounts( @@ -93,7 +69,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { amount, minAmount1 ); - } else if (_asset == token1) { + } else if (asset == token1) { minAmount0 = totalAmount0 / (totalAmount1 / amount); minAmount1 = amount; liquidity = helper.getLiquidityForAmounts( @@ -106,6 +82,38 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { } } + /** + * @notice Liquidiates active position to remove required amount of give asset + * @dev Doesn't have non-Reentrant modifier since it's supposed to be delegatecalled + * only from `UniswapV3Strategy.withdraw` which already has a nonReentrant check + * and the storage is shared between these two contract. + * + * @param asset Asset address + * @param amount Min amount of token to receive + */ + function withdrawAssetFromActivePositionOnlyVault( + address asset, + uint256 amount + ) external onlyVault { + Position memory position = tokenIdToPosition[activeTokenId]; + require(position.exists && position.liquidity > 0, "Liquidity error"); + + // Figure out liquidity to burn + ( + uint128 liquidity, + uint256 minAmount0, + uint256 minAmount1 + ) = _calculateLiquidityToWithdraw(position, asset, amount); + + // Liquidiate active position + _decreasePositionLiquidity( + position.tokenId, + liquidity, + minAmount0, + minAmount1 + ); + } + /*************************************** Rebalance ****************************************/ @@ -115,24 +123,53 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { _; } + modifier swapsNotPaused() { + require(!swapsPaused, "Swaps are paused"); + _; + } + modifier withinRebalacingLimits(int24 lowerTick, int24 upperTick) { + require(rebalancePaused, "Rebalances are paused"); require( - minRebalanceTick <= lowerTick && - maxRebalanceTick >= upperTick, + minRebalanceTick <= lowerTick && maxRebalanceTick >= upperTick, "Rebalance position out of bounds" ); _; } modifier ensureTVL() { + // Vaule change should either be between 0 and 0 (for rebalance) + // and 0 and slippage (for swapAndRebalance) + _; - uint256 balance = InitializableAbstractStrategy(this).checkBalance(token0) + - InitializableAbstractStrategy(this).checkBalance(token1); + uint256 balance = _self.checkBalance(token0).scaleBy( + 18, + Helpers.getDecimals(token0) + ) + _self.checkBalance(token1).scaleBy(18, Helpers.getDecimals(token1)); + + require(balance <= maxTVL, "MaxTVL threshold has been reached"); + } + + modifier withinSwapPriceLimits( + uint160 sqrtPriceLimitX96, + bool swapZeroForOne + ) { + (uint160 currentPriceX96, , , , , , ) = pool.slot0(); + + require( + minSwapPriceX96 <= currentPriceX96 && + currentPriceX96 <= maxSwapPriceX96, + "Price out of bounds" + ); require( - balance <= maxTVL, - "MaxTVL threshold has been reached" + swapZeroForOne + ? (sqrtPriceLimitX96 >= minSwapPriceX96) + : (sqrtPriceLimitX96 <= maxSwapPriceX96), + "Slippage out of bounds" ); + + _; } /** @@ -228,6 +265,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { nonReentrant rebalanceNotPaused withinRebalacingLimits(params.lowerTick, params.upperTick) + swapsNotPaused + withinSwapPriceLimits(params.sqrtPriceLimitX96, params.swapZeroForOne) ensureTVL { require(params.lowerTick < params.upperTick, "Invalid tick range"); @@ -569,7 +608,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 minAmount0, uint256 minAmount1 ) - public + external onlyGovernorOrStrategistOrOperator nonReentrant returns (uint256 amount0, uint256 amount1) @@ -577,6 +616,23 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { return _closePosition(tokenId, minAmount0, minAmount1); } + /** + * @notice Same as closePosition but only callable by Vault + * @dev Doesn't have non-Reentrant modifier since it's supposed to be delegatecalled + * only from `UniswapV3Strategy.withdrawAll` which already has a nonReentrant check + * and the storage is shared between these two contract. + */ + function closeActivePositionOnlyVault() + external + onlyVault + returns (uint256 amount0, uint256 amount1) + { + // Since this is called by the Vault, we cannot pass min redeem amounts + // without complicating the code of the Vault. So, passing 0 instead. + // A better way + return _closePosition(activeTokenId, 0, 0); + } + /*************************************** Balances and Fees ****************************************/ @@ -764,42 +820,16 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { revert("NO_IMPL"); } + function withdrawAll() external virtual override { + revert("NO_IMPL"); + } + function withdraw( address recipient, address _asset, uint256 amount ) external override onlyVault onlyPoolTokens(_asset) nonReentrant { - IERC20 asset = IERC20(_asset); - uint256 selfBalance = asset.balanceOf(address(this)); - - if (selfBalance < amount) { - _withdrawAssetFromActivePosition(_asset, amount - selfBalance); - } - - // Transfer requested amount - asset.safeTransfer(recipient, amount); - emit Withdrawal(_asset, _asset, amount); - } - - /** - * @notice Closes active LP position, if any, and transfer all token balance to Vault - */ - function withdrawAll() external override onlyVault nonReentrant { - if (activeTokenId > 0) { - _closePosition(activeTokenId, 0, 0); - } - - // saves 100B of contract size to loop through these 2 tokens - address[2] memory tokens = [token0, token1]; - for (uint256 i = 0; i < 2; i++) { - IERC20 tokenContract = IERC20(tokens[i]); - uint256 tokenBalance = tokenContract.balanceOf(address(this)); - - if (tokenBalance > 0) { - tokenContract.safeTransfer(vaultAddress, tokenBalance); - emit Withdrawal(tokens[i], tokens[i], tokenBalance); - } - } + revert("NO_IMPL"); } function checkBalance(address _asset) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index da19be6f0a..de575f6a18 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -17,7 +17,6 @@ import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3 import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import { StableMath } from "../../utils/StableMath.sol"; -import { Helpers } from "../../utils/Helpers.sol"; contract UniswapV3Strategy is UniswapV3StrategyStorage { using SafeERC20 for IERC20; @@ -181,10 +180,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @notice Change the maxTVL amount threshold * @param _maxTVL Maximum amount the strategy can have deployed in the Uniswap pool */ - function setMaxTVL(uint256 _maxTVL) - external - onlyGovernorOrStrategist - { + function setMaxTVL(uint256 _maxTVL) external onlyGovernorOrStrategist { maxTVL = _maxTVL; emit MaxTVLChanged(_maxTVL); } @@ -198,14 +194,29 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { external onlyGovernorOrStrategist { - require((minTick < maxTick), "Invalid threshold"); + require(minTick < maxTick, "Invalid threshold"); minRebalanceTick = minTick; maxRebalanceTick = maxTick; - emit RebalancePriceThresholdChanged( + emit RebalancePriceThresholdChanged(minTick, maxTick); + } + + /** + * @notice Change the swap price threshold + * @param minTick Minimum price tick index + * @param maxTick Maximum price tick index + */ + function setSwapPriceThreshold(int24 minTick, int24 maxTick) + external + onlyGovernorOrStrategist + { + require(minTick < maxTick, "Invalid threshold"); + minSwapPriceX96 = helper.getSqrtRatioAtTick(minTick); + maxSwapPriceX96 = helper.getSqrtRatioAtTick(maxTick); + emit SwapPriceThresholdChanged( minTick, - helper.getSqrtRatioAtTick(minTick), + minSwapPriceX96, maxTick, - helper.getSqrtRatioAtTick(maxTick) + maxSwapPriceX96 ); } @@ -232,23 +243,58 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { _depositAll(); } - /** - * @inheritdoc InitializableAbstractStrategy - */ + /// @inheritdoc InitializableAbstractStrategy function withdraw( address recipient, address _asset, uint256 amount ) external override onlyVault onlyPoolTokens(_asset) nonReentrant { - revert("NO_IMPL"); + IERC20 asset = IERC20(_asset); + uint256 selfBalance = asset.balanceOf(address(this)); + + if (selfBalance < amount) { + require(activeTokenId > 0, "Liquidity error"); + + // Delegatecall to `UniswapV3LiquidityManager` to remove liquidity from + // active LP position + (bool success, bytes memory data) = address(_self).delegatecall( + abi.encodeWithSignature( + "withdrawAssetFromActivePositionOnlyVault(address,uint256)", + _asset, + amount - selfBalance + ) + ); + require(success, "DelegateCall to close position failed"); + } + + // Transfer requested amount + asset.safeTransfer(recipient, amount); + emit Withdrawal(_asset, _asset, amount); } /** * @notice Closes active LP position, if any, and transfer all token balance to Vault * @inheritdoc InitializableAbstractStrategy */ - function withdrawAll() external virtual override { - revert("NO_IMPL"); + function withdrawAll() external override onlyVault nonReentrant { + if (activeTokenId > 0) { + (bool success, bytes memory data) = address(_self).delegatecall( + abi.encodeWithSignature("closeActivePositionOnlyVault()") + ); + require(success, "DelegateCall to close position failed"); + } + + // saves 100B of contract size to loop through these 2 tokens + address[2] memory tokens = [token0, token1]; + for (uint256 i = 0; i < 2; i++) { + IERC20 tokenContract = IERC20(tokens[i]); + uint256 tokenBalance = tokenContract.balanceOf(address(this)); + + if (tokenBalance > 0) { + tokenContract.safeTransfer(vaultAddress, tokenBalance); + emit Withdrawal(tokens[i], tokens[i], tokenBalance); + } + } } /*************************************** @@ -304,36 +350,37 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { balance += amount1; } } - - uint256 assetDecimals = Helpers.getDecimals(_asset); - balance = balance.scaleBy(18, assetDecimals); } - function checkBalanceOfAllAssets() - external - view - returns (uint256 amount0, uint256 amount1) - { - if (activeTokenId > 0) { - require(tokenIdToPosition[activeTokenId].exists, "Invalid token"); - - (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); - (amount0, amount1) = helper.positionValue( - positionManager, - address(pool), - activeTokenId, - sqrtRatioX96 - ); - } - - amount0 += IERC20(token0).balanceOf(address(this)); - amount1 += IERC20(token1).balanceOf(address(this)); - - uint256 asset0Decimals = Helpers.getDecimals(token0); - uint256 asset1Decimals = Helpers.getDecimals(token1); - amount0 = amount0.scaleBy(18, asset0Decimals); - amount1 = amount1.scaleBy(18, asset1Decimals); - } + // /** + // * @dev Only checks the active LP position. + // * Doesn't return the balance held in the reserve strategies. + // */ + // function checkBalanceOfAllAssets() + // external + // view + // returns (uint256 amount0, uint256 amount1) + // { + // if (activeTokenId > 0) { + // require(tokenIdToPosition[activeTokenId].exists, "Invalid token"); + + // (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); + // (amount0, amount1) = helper.positionValue( + // positionManager, + // address(pool), + // activeTokenId, + // sqrtRatioX96 + // ); + // } + + // amount0 += IERC20(token0).balanceOf(address(this)); + // amount1 += IERC20(token1).balanceOf(address(this)); + + // // uint256 asset0Decimals = Helpers.getDecimals(token0); + // // uint256 asset1Decimals = Helpers.getDecimals(token1); + // // amount0 = amount0.scaleBy(18, asset0Decimals); + // // amount1 = amount1.scaleBy(18, asset1Decimals); + // } /*************************************** ERC721 management diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index ce0ca700e7..0c735ac9e5 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -21,13 +21,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { ); event RebalancePauseStatusChanged(bool paused); event SwapsPauseStatusChanged(bool paused); - event RebalancePriceThresholdChanged( - int24 minTick, - uint160 minRebalancePriceX96, - int24 maxTick, - uint160 maxRebalancePriceX96 - ); - event MaxSwapSlippageChanged(uint24 maxSlippage); + event RebalancePriceThresholdChanged(int24 minTick, int24 maxTick); event MaxTVLChanged(uint256 amountIn); event AssetSwappedForRebalancing( address indexed tokenIn, @@ -62,6 +56,12 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint256 amount0, uint256 amount1 ); + event SwapPriceThresholdChanged( + int24 minTick, + uint160 minSwapPriceX96, + int24 maxTick, + uint160 maxSwapPriceX96 + ); // Represents both tokens supported by the strategy struct PoolToken { @@ -102,10 +102,14 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint256 public maxTVL; // In USD, 18 decimals - // An upper and lower bound of rebalancing price limits + // An upper and lower bound of rebalancing price limits int24 public minRebalanceTick; int24 public maxRebalanceTick; + // An upper and lower bound of swap price limits + uint160 public minSwapPriceX96; + uint160 public maxSwapPriceX96; + // Token ID of active Position on the pool. zero, if there are no active LP position uint256 public activeTokenId; @@ -187,6 +191,12 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { _; } + /*************************************** + Commom functions + ****************************************/ + /** + * Deposits back strategy token balances back to the reserve strategies + */ function _depositAll() internal { IUniswapV3Strategy strat = IUniswapV3Strategy(msg.sender); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 881f10b8fa..eec52414bf 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1283,17 +1283,16 @@ function uniswapV3FixtureSetup() { const { governorAddr, timelockAddr } = await getNamedAccounts(); const sGovernor = await ethers.provider.getSigner( isFork ? timelockAddr : governorAddr - ); + ); - UniV3_USDC_USDT_Strategy - .connect(sGovernor) + UniV3_USDC_USDT_Strategy.connect(sGovernor) // 2 million - .setMaxTVL(utils.parseUnits("2", 24)) - - UniV3_USDC_USDT_Strategy - .connect(sGovernor) - .setRebalancePriceThreshold(-100, 100) + .setMaxTVL(utils.parseUnits("2", 24)); + UniV3_USDC_USDT_Strategy.connect(sGovernor).setRebalancePriceThreshold( + -100, + 100 + ); return fixture; }); diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 811540a4df..941fb89a57 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -81,9 +81,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { isFork ? timelockAddr : governorAddr ); - await strategy - .connect(sGovernor) - .setMaxTVL(utils.parseUnits(maxTvl, 18)); + await strategy.connect(sGovernor).setMaxTVL(utils.parseUnits(maxTvl, 18)); } describe("Uniswap V3 LP positions", function () { @@ -278,7 +276,6 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { await expect( mintLiquidity(lowerTick, upperTick, "100000", "100000") ).to.be.revertedWith("MaxTVL threshold has been reached"); - }); it("Should swap USDC for USDT and mint position", async () => { From 53cd3c76b9cf29bc418e64cdd1a9b9142ed444ec Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 18 Mar 2023 12:40:02 +0530 Subject: [PATCH 26/83] merge modifiers --- .../uniswap/UniswapV3LiquidityManager.sol | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 88ff3f3b06..c37cc60930 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -117,18 +117,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /*************************************** Rebalance ****************************************/ - - modifier rebalanceNotPaused() { - require(!rebalancePaused, "Rebalance paused"); - _; - } - - modifier swapsNotPaused() { - require(!swapsPaused, "Swaps are paused"); - _; - } - - modifier withinRebalacingLimits(int24 lowerTick, int24 upperTick) { + modifier rebalanceNotPausedAndWithinLimits(int24 lowerTick, int24 upperTick) { require(rebalancePaused, "Rebalances are paused"); require( minRebalanceTick <= lowerTick && maxRebalanceTick >= upperTick, @@ -150,10 +139,12 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { require(balance <= maxTVL, "MaxTVL threshold has been reached"); } - modifier withinSwapPriceLimits( + modifier swapsNotPausedAndWithinLimits( uint160 sqrtPriceLimitX96, bool swapZeroForOne ) { + require(!swapsPaused, "Swaps are paused"); + (uint160 currentPriceX96, , , , , , ) = pool.slot0(); require( @@ -198,8 +189,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { external onlyGovernorOrStrategistOrOperator nonReentrant - rebalanceNotPaused - withinRebalacingLimits(lowerTick, upperTick) + rebalanceNotPausedAndWithinLimits(lowerTick, upperTick) ensureTVL { require(lowerTick < upperTick, "Invalid tick range"); @@ -263,10 +253,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { external onlyGovernorOrStrategistOrOperator nonReentrant - rebalanceNotPaused - withinRebalacingLimits(params.lowerTick, params.upperTick) - swapsNotPaused - withinSwapPriceLimits(params.sqrtPriceLimitX96, params.swapZeroForOne) + rebalanceNotPausedAndWithinLimits(params.lowerTick, params.upperTick) + swapsNotPausedAndWithinLimits(params.sqrtPriceLimitX96, params.swapZeroForOne) ensureTVL { require(params.lowerTick < params.upperTick, "Invalid tick range"); From 7bead4387be89cfd2c5e69478f0f8b8a033b59a4 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 18 Mar 2023 12:59:12 +0530 Subject: [PATCH 27/83] Cleanup usage of PoolToken struct to save gas/contract size --- .../interfaces/IVaultValueChecker.sol | 13 ++++ .../uniswap/UniswapV3LiquidityManager.sol | 16 ++-- .../strategies/uniswap/UniswapV3Strategy.sol | 75 +++++++------------ .../uniswap/UniswapV3StrategyStorage.sol | 43 +++-------- 4 files changed, 63 insertions(+), 84 deletions(-) create mode 100644 contracts/contracts/interfaces/IVaultValueChecker.sol diff --git a/contracts/contracts/interfaces/IVaultValueChecker.sol b/contracts/contracts/interfaces/IVaultValueChecker.sol new file mode 100644 index 0000000000..8174da9e21 --- /dev/null +++ b/contracts/contracts/interfaces/IVaultValueChecker.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +interface IVaultValueChecker { + function takeSnapshot() external; + + function checkDelta( + int256 lowValueDelta, + int256 highValueDelta, + int256 lowSupplyDelta, + int256 highSupplyDelta + ) external; +} diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index c37cc60930..77347fbafa 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -117,7 +117,10 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /*************************************** Rebalance ****************************************/ - modifier rebalanceNotPausedAndWithinLimits(int24 lowerTick, int24 upperTick) { + modifier rebalanceNotPausedAndWithinLimits( + int24 lowerTick, + int24 upperTick + ) { require(rebalancePaused, "Rebalances are paused"); require( minRebalanceTick <= lowerTick && maxRebalanceTick >= upperTick, @@ -254,7 +257,10 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { onlyGovernorOrStrategistOrOperator nonReentrant rebalanceNotPausedAndWithinLimits(params.lowerTick, params.upperTick) - swapsNotPausedAndWithinLimits(params.sqrtPriceLimitX96, params.swapZeroForOne) + swapsNotPausedAndWithinLimits( + params.sqrtPriceLimitX96, + params.swapZeroForOne + ) ensureTVL { require(params.lowerTick < params.upperTick, "Invalid tick range"); @@ -678,8 +684,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { if (swapZeroForOne) { // Amount available in reserve strategies - uint256 t1ReserveBal = IStrategy(poolTokens[token1].reserveStrategy) - .checkBalance(token1); + uint256 t1ReserveBal = reserveStrategy1.checkBalance(token1); // Only swap when asset isn't available in reserve as well require( @@ -694,8 +699,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { : (token1Needed - swapMinAmountOut); } else { // Amount available in reserve strategies - uint256 t0ReserveBal = IStrategy(poolTokens[token0].reserveStrategy) - .checkBalance(token0); + uint256 t0ReserveBal = reserveStrategy0.checkBalance(token0); // Only swap when asset isn't available in reserve as well require( diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index de575f6a18..5bee3b7a60 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -17,6 +17,7 @@ import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3 import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import { StableMath } from "../../utils/StableMath.sol"; +import { IVaultValueChecker } from "../../interfaces/IVaultValueChecker.sol"; contract UniswapV3Strategy is UniswapV3StrategyStorage { using SafeERC20 for IERC20; @@ -29,6 +30,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @param _nonfungiblePositionManager Uniswap V3's Position Manager * @param _helper Deployed UniswapV3Helper contract * @param _swapRouter Uniswap SwapRouter contract + * @param _vaultValueChecker VaultValueChecker * @param _operator Operator address */ function initialize( @@ -37,6 +39,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { address _nonfungiblePositionManager, address _helper, address _swapRouter, + address _vaultValueChecker, address _operator ) external onlyGovernor initializer { // TODO: Comment on why this is necessary and why it should always be the proxy address @@ -49,6 +52,8 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { _nonfungiblePositionManager ); + vaultValueChecker = IVaultValueChecker(_vaultValueChecker); + token0 = pool.token0(); token1 = pool.token1(); poolFee = pool.fee(); @@ -65,18 +70,6 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { _assets // Platform token addresses ); - poolTokens[token0] = PoolToken({ - isSupported: true, - reserveStrategy: address(0), // Set using `setReserveStrategy()` - minDepositThreshold: 0 - }); - - poolTokens[token1] = PoolToken({ - isSupported: true, - reserveStrategy: address(0), // Set using `setReserveStrategy() - minDepositThreshold: 0 - }); - _setOperator(_operator); } @@ -121,7 +114,11 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { "Invalid strategy for asset" ); - poolTokens[_asset].reserveStrategy = _reserveStrategy; + if (_asset == token0) { + reserveStrategy0 = IStrategy(_reserveStrategy); + } else if (_asset == token1) { + reserveStrategy1 = IStrategy(_reserveStrategy); + } emit ReserveStrategyChanged(_asset, _reserveStrategy); } @@ -137,7 +134,11 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { onlyPoolTokens(_asset) returns (address reserveStrategyAddr) { - reserveStrategyAddr = poolTokens[_asset].reserveStrategy; + if (_asset == token0) { + reserveStrategyAddr = address(reserveStrategy0); + } else if (_asset == token1) { + reserveStrategyAddr = address(reserveStrategy1); + } } /** @@ -150,8 +151,11 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { onlyGovernorOrStrategist onlyPoolTokens(_asset) { - PoolToken storage token = poolTokens[_asset]; - token.minDepositThreshold = _minThreshold; + if (_asset == token0) { + minDepositThreshold0 = _minThreshold; + } else if (_asset == token1) { + minDepositThreshold1 = _minThreshold; + } emit MinDepositThresholdChanged(_asset, _minThreshold); } @@ -232,7 +236,14 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { onlyPoolTokens(_asset) nonReentrant { - if (_amount > poolTokens[_asset].minDepositThreshold) { + if ( + _amount > 0 && + ( + _asset == token0 + ? (_amount > minDepositThreshold0) + : (_amount > minDepositThreshold1) + ) + ) { IVault(vaultAddress).depositToUniswapV3Reserve(_asset, _amount); // Not emitting Deposit event since the Reserve strategy would do so } @@ -352,36 +363,6 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { } } - // /** - // * @dev Only checks the active LP position. - // * Doesn't return the balance held in the reserve strategies. - // */ - // function checkBalanceOfAllAssets() - // external - // view - // returns (uint256 amount0, uint256 amount1) - // { - // if (activeTokenId > 0) { - // require(tokenIdToPosition[activeTokenId].exists, "Invalid token"); - - // (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); - // (amount0, amount1) = helper.positionValue( - // positionManager, - // address(pool), - // activeTokenId, - // sqrtRatioX96 - // ); - // } - - // amount0 += IERC20(token0).balanceOf(address(this)); - // amount1 += IERC20(token1).balanceOf(address(this)); - - // // uint256 asset0Decimals = Helpers.getDecimals(token0); - // // uint256 asset1Decimals = Helpers.getDecimals(token1); - // // amount0 = amount0.scaleBy(18, asset0Decimals); - // // amount1 = amount1.scaleBy(18, asset1Decimals); - // } - /*************************************** ERC721 management ****************************************/ diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 0c735ac9e5..e14a17d487 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -10,6 +10,7 @@ import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3 import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; import { IUniswapV3Helper } from "../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; +import { IVaultValueChecker } from "../../interfaces/IVaultValueChecker.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { @@ -63,17 +64,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint160 maxSwapPriceX96 ); - // Represents both tokens supported by the strategy - struct PoolToken { - // True if asset is either token0 or token1 - bool isSupported; - // When the funds are not deployed in Uniswap V3 Pool, they will - // be deposited to these reserve strategies - address reserveStrategy; - // Deposits to reserve strategy when contract balance exceeds this amount - uint256 minDepositThreshold; - } - // Represents a position minted by UniswapV3Strategy contract struct Position { uint256 tokenId; // ERC721 token Id of the minted position @@ -96,6 +86,15 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { address public token0; // Token0 of Uniswap V3 Pool address public token1; // Token1 of Uniswap V3 Pool + // When the funds are not deployed in Uniswap V3 Pool, they will + // be deposited to these reserve strategies + IStrategy public reserveStrategy0; // Reserve strategy for token0 + IStrategy public reserveStrategy1; // Reserve strategy for token1 + + // Deposits to reserve strategy when contract balance exceeds this amount + uint256 public minDepositThreshold0; + uint256 public minDepositThreshold1; + uint24 public poolFee; // Uniswap V3 Pool Fee bool public swapsPaused = false; // True if Swaps are paused bool public rebalancePaused = false; // True if Swaps are paused @@ -125,8 +124,8 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { // Uniswap Swap Router ISwapRouter internal swapRouter; - // Contains data about both tokens - mapping(address => PoolToken) public poolTokens; + // VaultValueChecker + IVaultValueChecker public vaultValueChecker; // A lookup table to find token IDs of position using f(lowerTick, upperTick) mapping(int48 => uint256) internal ticksToTokenId; @@ -170,19 +169,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { _; } - /** - * @dev Ensures that the caller is Governor, Strategist or Operator. - */ - modifier onlyGovernorOrStrategistOrOperatorOrVault() { - require( - msg.sender == operatorAddr || - msg.sender == IVault(vaultAddress).strategistAddr() || - msg.sender == governor(), - "Caller is not the Operator, Strategist, Governor or Vault" - ); - _; - } - /** * @dev Ensures that the asset address is either token0 or token1. */ @@ -198,15 +184,10 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { * Deposits back strategy token balances back to the reserve strategies */ function _depositAll() internal { - IUniswapV3Strategy strat = IUniswapV3Strategy(msg.sender); - uint256 token0Bal = IERC20(token0).balanceOf(address(this)); uint256 token1Bal = IERC20(token1).balanceOf(address(this)); IVault vault = IVault(vaultAddress); - uint256 minDepositThreshold0 = poolTokens[token0].minDepositThreshold; - uint256 minDepositThreshold1 = poolTokens[token1].minDepositThreshold; - if ( token0Bal > 0 && (minDepositThreshold0 == 0 || token0Bal >= minDepositThreshold0) From 197d5ee07635d740bcf42feb8936e2d17b5b9002 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 18 Mar 2023 08:48:54 +0100 Subject: [PATCH 28/83] save 0.2KB by changing the modifier to a function --- .../uniswap/UniswapV3LiquidityManager.sol | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 77347fbafa..d213521815 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -117,18 +117,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /*************************************** Rebalance ****************************************/ - modifier rebalanceNotPausedAndWithinLimits( - int24 lowerTick, - int24 upperTick - ) { - require(rebalancePaused, "Rebalances are paused"); - require( - minRebalanceTick <= lowerTick && maxRebalanceTick >= upperTick, - "Rebalance position out of bounds" - ); - _; - } - modifier ensureTVL() { // Vaule change should either be between 0 and 0 (for rebalance) // and 0 and slippage (for swapAndRebalance) @@ -142,10 +130,10 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { require(balance <= maxTVL, "MaxTVL threshold has been reached"); } - modifier swapsNotPausedAndWithinLimits( + function swapsNotPausedAndWithinLimits( uint160 sqrtPriceLimitX96, bool swapZeroForOne - ) { + ) internal { require(!swapsPaused, "Swaps are paused"); (uint160 currentPriceX96, , , , , , ) = pool.slot0(); @@ -162,8 +150,14 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { : (sqrtPriceLimitX96 <= maxSwapPriceX96), "Slippage out of bounds" ); + } - _; + function rebalanceNotPausedAndWithinLimits(int24 lowerTick, int24 upperTick) internal { + require(rebalancePaused, "Rebalances are paused"); + require( + minRebalanceTick <= lowerTick && maxRebalanceTick >= upperTick, + "Rebalance position out of bounds" + ); } /** @@ -192,9 +186,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { external onlyGovernorOrStrategistOrOperator nonReentrant - rebalanceNotPausedAndWithinLimits(lowerTick, upperTick) ensureTVL { + rebalanceNotPausedAndWithinLimits(lowerTick, upperTick); require(lowerTick < upperTick, "Invalid tick range"); int48 tickKey = _getTickPositionKey(lowerTick, upperTick); @@ -256,13 +250,10 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { external onlyGovernorOrStrategistOrOperator nonReentrant - rebalanceNotPausedAndWithinLimits(params.lowerTick, params.upperTick) - swapsNotPausedAndWithinLimits( - params.sqrtPriceLimitX96, - params.swapZeroForOne - ) ensureTVL { + swapsNotPausedAndWithinLimits(params.sqrtPriceLimitX96, params.swapZeroForOne); + rebalanceNotPausedAndWithinLimits(params.lowerTick, params.upperTick); require(params.lowerTick < params.upperTick, "Invalid tick range"); uint256 tokenId = ticksToTokenId[ From 5cbd66b481418b056f599bf03fb70b87b48aa694 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 18 Mar 2023 16:15:25 +0530 Subject: [PATCH 29/83] Add net value loss threshold --- .../uniswap/v3/IUniswapV3Helper.sol | 8 +- .../uniswap/UniswapV3LiquidityManager.sol | 215 ++++++++++++------ .../strategies/uniswap/UniswapV3Strategy.sol | 57 +++-- .../uniswap/UniswapV3StrategyStorage.sol | 65 +++++- contracts/contracts/utils/UniswapV3Helper.sol | 10 +- contracts/test/_fixture.js | 7 +- .../test/strategies/uniswap-v3.fork-test.js | 36 +-- contracts/test/strategies/uniswap-v3.js | 19 +- 8 files changed, 278 insertions(+), 139 deletions(-) diff --git a/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol b/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol index 7cdf3abe07..38a4108992 100644 --- a/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol +++ b/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol @@ -30,10 +30,16 @@ interface IUniswapV3Helper { uint256 tokenId ) external view returns (uint256 amount0, uint256 amount1); - function positionValue( + function positionTotal( INonfungiblePositionManager positionManager, address poolAddress, uint256 tokenId, uint160 sqrtRatioX96 ) external view returns (uint256 amount0, uint256 amount1); + + function positionPrincipal( + INonfungiblePositionManager positionManager, + uint256 tokenId, + uint160 sqrtRatioX96 + ) external view returns (uint256 amount0, uint256 amount1); } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index d213521815..3048d81d88 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -20,6 +20,36 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { using SafeERC20 for IERC20; using StableMath for uint256; + /** + * @notice Calculates the net value of the position exlcuding fees + * @param tokenId tokenID of the Position NFT + * @return posValue Value of position (in 18 decimals) + */ + function getPositionValue(uint256 tokenId) + internal + view + returns (uint256 posValue) + { + (uint256 amount0, uint256 amount1) = getPositionPrincipal(tokenId); + + posValue = _getValueOfTokens(amount0, amount1); + } + + /** + * @notice Calculates the net value of the token amounts (assumes it's pegged to $1) + * @param amount0 Amount of token0 + * @param amount1 Amount of token1 + * @return value Net value (in 18 decimals) + */ + function _getValueOfTokens(uint256 amount0, uint256 amount1) + internal + view + returns (uint256 value) + { + value += amount0.scaleBy(18, Helpers.getDecimals(token0)); + value += amount1.scaleBy(18, Helpers.getDecimals(token1)); + } + /*************************************** Withdraw ****************************************/ @@ -117,17 +147,11 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /*************************************** Rebalance ****************************************/ - modifier ensureTVL() { - // Vaule change should either be between 0 and 0 (for rebalance) - // and 0 and slippage (for swapAndRebalance) - - _; - uint256 balance = _self.checkBalance(token0).scaleBy( - 18, - Helpers.getDecimals(token0) - ) + _self.checkBalance(token1).scaleBy(18, Helpers.getDecimals(token1)); - - require(balance <= maxTVL, "MaxTVL threshold has been reached"); + function ensureTVL() internal { + require( + getPositionValue(activeTokenId) <= maxTVL, + "MaxTVL threshold has been reached" + ); } function swapsNotPausedAndWithinLimits( @@ -152,7 +176,13 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ); } - function rebalanceNotPausedAndWithinLimits(int24 lowerTick, int24 upperTick) internal { + function rebalanceNotPaused() internal { + require(rebalancePaused, "Rebalances are paused"); + } + + function rebalanceNotPausedAndWithinLimits(int24 lowerTick, int24 upperTick) + internal + { require(rebalancePaused, "Rebalances are paused"); require( minRebalanceTick <= lowerTick && maxRebalanceTick >= upperTick, @@ -160,6 +190,36 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ); } + function updatePositionNetVal(uint256 tokenId) + internal + returns (uint256 valueLost) + { + if (tokenId == 0) { + return 0; + } + + uint256 currentVal = getPositionValue(tokenId); + uint256 lastVal = tokenIdToPosition[tokenId].netValue; + + if (currentVal < lastVal) { + valueLost = lastVal - currentVal; + + // TODO: Should these be also updated when the value rises? + netLostValue += valueLost; + tokenIdToPosition[tokenId].netValue = currentVal; + + emit PositionLostValue(tokenId, lastVal, currentVal, valueLost); + } + } + + function ensureNetLossThreshold(uint256 tokenId) internal { + updatePositionNetVal(tokenId); + require( + netLostValue < maxPositionValueLossThreshold, + "Over max value loss threshold" + ); + } + /** * @notice Closes active LP position if any and then provides liquidity to the requested position. * Mints new position, if it doesn't exist already. @@ -182,14 +242,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 minRedeemAmount1, int24 lowerTick, int24 upperTick - ) - external - onlyGovernorOrStrategistOrOperator - nonReentrant - ensureTVL - { - rebalanceNotPausedAndWithinLimits(lowerTick, upperTick); + ) external onlyGovernorOrStrategistOrOperator nonReentrant { require(lowerTick < upperTick, "Invalid tick range"); + rebalanceNotPausedAndWithinLimits(lowerTick, upperTick); int48 tickKey = _getTickPositionKey(lowerTick, upperTick); uint256 tokenId = ticksToTokenId[tickKey]; @@ -229,6 +284,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // Move any leftovers to Reserve _depositAll(); + + // Final position value/sanity check + ensureTVL(); } struct SwapAndRebalanceParams { @@ -250,11 +308,13 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { external onlyGovernorOrStrategistOrOperator nonReentrant - ensureTVL { - swapsNotPausedAndWithinLimits(params.sqrtPriceLimitX96, params.swapZeroForOne); - rebalanceNotPausedAndWithinLimits(params.lowerTick, params.upperTick); require(params.lowerTick < params.upperTick, "Invalid tick range"); + swapsNotPausedAndWithinLimits( + params.sqrtPriceLimitX96, + params.swapZeroForOne + ); + rebalanceNotPausedAndWithinLimits(params.lowerTick, params.upperTick); uint256 tokenId = ticksToTokenId[ _getTickPositionKey(params.lowerTick, params.upperTick) @@ -306,6 +366,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // Move any leftovers to Reserve _depositAll(); + + // Final position value/sanity check + ensureTVL(); } /*************************************** @@ -364,6 +427,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { int48 tickKey = _getTickPositionKey(lowerTick, upperTick); require(ticksToTokenId[tickKey] == 0, "Duplicate position mint"); + // Make sure liquidity management is disabled when value lost threshold is breached + ensureNetLossThreshold(0); + INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ token0: token0, @@ -389,7 +455,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { lowerTick: lowerTick, upperTick: upperTick, sqrtRatioAX96: helper.getSqrtRatioAtTick(lowerTick), - sqrtRatioBX96: helper.getSqrtRatioAtTick(upperTick) + sqrtRatioBX96: helper.getSqrtRatioAtTick(upperTick), + netValue: _getValueOfTokens(amount0, amount1) }); emit UniswapV3PositionMinted(tokenId, lowerTick, upperTick); @@ -422,6 +489,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { Position storage position = tokenIdToPosition[tokenId]; require(position.exists, "No active position"); + // Make sure liquidity management is disabled when value lost threshold is breached + ensureNetLossThreshold(tokenId); + // Withdraw enough funds from Reserve strategies _ensureAssetBalances(desiredAmount0, desiredAmount1); @@ -441,6 +511,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ).increaseLiquidity(params); position.liquidity += liquidity; + // Update last known value + position.netValue = getPositionValue(tokenId); emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); } @@ -456,6 +528,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { nonReentrant returns (uint256 amount0, uint256 amount1) { + rebalanceNotPaused(); + _increasePositionLiquidity( activeTokenId, desiredAmount0, @@ -463,6 +537,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { minAmount0, minAmount1 ); + + // Final position value/sanity check + ensureTVL(); } /** @@ -488,14 +565,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { Position storage position = tokenIdToPosition[tokenId]; require(position.exists, "Unknown position"); - (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); - (uint256 exactAmount0, uint256 exactAmount1) = helper - .getAmountsForLiquidity( - sqrtRatioX96, - position.sqrtRatioAX96, - position.sqrtRatioBX96, - liquidity - ); + // Make sure liquidity management is disabled when value lost threshold is breached + ensureNetLossThreshold(tokenId); INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager @@ -510,6 +581,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { (amount0, amount1) = positionManager.decreaseLiquidity(params); position.liquidity -= liquidity; + // Update last known value + position.netValue = getPositionValue(tokenId); emit UniswapV3LiquidityRemoved( position.tokenId, @@ -529,12 +602,17 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { nonReentrant returns (uint256 amount0, uint256 amount1) { - _decreasePositionLiquidity( - activeTokenId, - liquidity, - minAmount0, - minAmount1 - ); + rebalanceNotPaused(); + + return + _decreasePositionLiquidity( + activeTokenId, + liquidity, + minAmount0, + minAmount1 + ); + + // Intentionally skipping TVL check since removing liquidity won't cause it to fail } /** @@ -599,6 +677,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { returns (uint256 amount0, uint256 amount1) { return _closePosition(tokenId, minAmount0, minAmount1); + + // Intentionally skipping TVL check since removing liquidity won't cause it to fail } /** @@ -616,6 +696,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // without complicating the code of the Vault. So, passing 0 instead. // A better way return _closePosition(activeTokenId, 0, 0); + + // Intentionally skipping TVL check since removing liquidity won't cause it to fail } /*************************************** @@ -774,79 +856,66 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { (amount0, amount1) = positionManager.collect(params); + if (netLostValue > 0) { + // Reset loss counter to include value of fee collected + uint256 feeValue = _getValueOfTokens(amount0, amount1); + netLostValue = (feeValue >= netLostValue) + ? 0 + : (netLostValue - feeValue); + } + emit UniswapV3FeeCollected(tokenId, amount0, amount1); } /*************************************** Hidden functions ****************************************/ - function _abstractSetPToken(address _asset, address _pToken) - internal - override - { + // solhint-disable-next-line + function _abstractSetPToken(address, address) internal override { revert("NO_IMPL"); } - function safeApproveAllTokens() external virtual override { + function safeApproveAllTokens() external override { revert("NO_IMPL"); } - function deposit(address _asset, uint256 _amount) - external - virtual - override - { + function deposit(address, uint256) external override { revert("NO_IMPL"); } - function depositAll() external virtual override { + function depositAll() external override { revert("NO_IMPL"); } - function withdrawAll() external virtual override { + function withdrawAll() external override { revert("NO_IMPL"); } function withdraw( - address recipient, - address _asset, - uint256 amount - ) external override onlyVault onlyPoolTokens(_asset) nonReentrant { + address, + address, + uint256 + ) external override { revert("NO_IMPL"); } - function checkBalance(address _asset) - external - view - override - returns (uint256 balance) - { + function checkBalance(address) external view override returns (uint256) { revert("NO_IMPL"); } - function supportsAsset(address _asset) - external - view - override - returns (bool) - { + function supportsAsset(address) external view override returns (bool) { revert("NO_IMPL"); } - function setPTokenAddress(address, address) external override onlyGovernor { + function setPTokenAddress(address, address) external override { revert("NO_IMPL"); } - function removePToken(uint256) external override onlyGovernor { + function removePToken(uint256) external override { revert("NO_IMPL"); } - function collectRewardTokens() - external - override - onlyHarvester - nonReentrant - { + function collectRewardTokens() external override { revert("NO_IMPL"); } } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 5bee3b7a60..dc144e3d85 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -189,6 +189,27 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { emit MaxTVLChanged(_maxTVL); } + /** + * @notice Maximum value of loss the LP positions can incur before strategy shuts off rebalances + * @param _maxLossThreshold Maximum amount in 18 decimals + */ + function setMaxPositionValueLossThreshold(uint256 _maxLossThreshold) + external + onlyGovernorOrStrategist + { + maxPositionValueLossThreshold = _maxLossThreshold; + emit MaxValueLossThresholdChanged(_maxLossThreshold); + } + + /** + * @notice Reset loss counter + * @dev Only governor can call it + */ + function resetLostValue() external onlyGovernor { + emit MaxValueLossThresholdChanged(netLostValue); + netLostValue = 0; + } + /** * @notice Change the rebalance price threshold * @param minTick Minimum price tick index @@ -266,9 +287,10 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { if (selfBalance < amount) { require(activeTokenId > 0, "Liquidity error"); - // Delegatecall to `UniswapV3LiquidityManager` to remove liquidity from - // active LP position - (bool success, bytes memory data) = address(_self).delegatecall( + // Delegatecall to `UniswapV3LiquidityManager` to remove + // liquidity from active LP position + // solhint-disable-next-line + (bool success, bytes memory _) = address(_self).delegatecall( abi.encodeWithSignature( "withdrawAssetFromActivePositionOnlyVault(address,uint256)", _asset, @@ -289,7 +311,10 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { */ function withdrawAll() external override onlyVault nonReentrant { if (activeTokenId > 0) { - (bool success, bytes memory data) = address(_self).delegatecall( + // Delegatecall to `UniswapV3LiquidityManager` to remove + // liquidity from active LP position + // solhint-disable-next-line + (bool success, bytes memory _) = address(_self).delegatecall( abi.encodeWithSignature("closeActivePositionOnlyVault()") ); require(success, "DelegateCall to close position failed"); @@ -322,6 +347,8 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { returns (uint256 amount0, uint256 amount1) { if (activeTokenId > 0) { + require(tokenIdToPosition[activeTokenId].exists, "Invalid token"); + (amount0, amount1) = helper.positionFees( positionManager, address(pool), @@ -331,7 +358,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { } /** - * @dev Only checks the active LP position. + * @dev Only checks the active LP position and undeployed/undeposited balance held by the contract. * Doesn't return the balance held in the reserve strategies. * @inheritdoc InitializableAbstractStrategy */ @@ -346,13 +373,8 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { if (activeTokenId > 0) { require(tokenIdToPosition[activeTokenId].exists, "Invalid token"); - - (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); - (uint256 amount0, uint256 amount1) = helper.positionValue( - positionManager, - address(pool), - activeTokenId, - sqrtRatioX96 + (uint256 amount0, uint256 amount1) = getPositionBalance( + activeTokenId ); if (_asset == token0) { @@ -431,23 +453,18 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { Hidden functions ****************************************/ - function setPTokenAddress(address, address) external override onlyGovernor { + function setPTokenAddress(address, address) external override { // The pool tokens can never change. revert("Unsupported method"); } - function removePToken(uint256) external override onlyGovernor { + function removePToken(uint256) external override { // The pool tokens can never change. revert("Unsupported method"); } /// @inheritdoc InitializableAbstractStrategy - function collectRewardTokens() - external - override - onlyHarvester - nonReentrant - { + function collectRewardTokens() external override { // Do nothing } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index e14a17d487..602fd645ea 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -23,7 +23,9 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { event RebalancePauseStatusChanged(bool paused); event SwapsPauseStatusChanged(bool paused); event RebalancePriceThresholdChanged(int24 minTick, int24 maxTick); - event MaxTVLChanged(uint256 amountIn); + event MaxTVLChanged(uint256 maxTVL); + event MaxValueLossThresholdChanged(uint256 amount); + event NetLossValueReset(uint256 lastValue); event AssetSwappedForRebalancing( address indexed tokenIn, address indexed tokenOut, @@ -63,6 +65,12 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { int24 maxTick, uint160 maxSwapPriceX96 ); + event PositionLostValue( + uint256 indexed tokenId, + uint256 initialValue, + uint256 currentValue, + uint256 netValueLost + ); // Represents a position minted by UniswapV3Strategy contract struct Position { @@ -76,6 +84,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { // compute it every time? uint160 sqrtRatioAX96; uint160 sqrtRatioBX96; + uint256 netValue; // Last recorded net value of the position } // Set to the proxy address when initialized @@ -112,6 +121,15 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { // Token ID of active Position on the pool. zero, if there are no active LP position uint256 public activeTokenId; + // Value of the active position since mint (or last liquidity change) + uint256 public activePositionMintValue = 0; + + // Sum of loss in value of tokens deployed to the pool + uint256 public netLostValue = 0; + + // Max value loss threshold after which rebalances aren't allowed + uint256 public maxPositionValueLossThreshold; + // Uniswap V3's Pool IUniswapV3Pool public pool; @@ -178,10 +196,10 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { } /*************************************** - Commom functions + Shared functions ****************************************/ /** - * Deposits back strategy token balances back to the reserve strategies + * @notice Deposits back strategy token balances back to the reserve strategies */ function _depositAll() internal { uint256 token0Bal = IERC20(token0).balanceOf(address(this)); @@ -202,4 +220,45 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { } // Not emitting Deposit events since the Reserve strategies would do so } + + /** + * @notice Returns the balance of both tokens in a given position (including fees) + * @param tokenId tokenID of the Position NFT + * @return amount0 Amount of token0 in position + * @return amount1 Amount of token1 in position + */ + function getPositionBalance(uint256 tokenId) + internal + view + returns (uint256 amount0, uint256 amount1) + { + require(tokenIdToPosition[tokenId].exists, "Invalid position"); + (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); + (amount0, amount1) = helper.positionTotal( + positionManager, + address(pool), + tokenId, + sqrtRatioX96 + ); + } + + /** + * @notice Returns the balance of both tokens in a given position (without fees) + * @param tokenId tokenID of the Position NFT + * @return amount0 Amount of token0 in position + * @return amount1 Amount of token1 in position + */ + function getPositionPrincipal(uint256 tokenId) + internal + view + returns (uint256 amount0, uint256 amount1) + { + require(tokenIdToPosition[tokenId].exists, "Invalid position"); + (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); + (amount0, amount1) = helper.positionPrincipal( + positionManager, + tokenId, + sqrtRatioX96 + ); + } } diff --git a/contracts/contracts/utils/UniswapV3Helper.sol b/contracts/contracts/utils/UniswapV3Helper.sol index e6d9471ded..fde134402a 100644 --- a/contracts/contracts/utils/UniswapV3Helper.sol +++ b/contracts/contracts/utils/UniswapV3Helper.sol @@ -63,7 +63,7 @@ contract UniswapV3Helper { return PositionValue.fees(positionManager, poolAddress, tokenId); } - function positionValue( + function positionTotal( INonfungiblePositionManager positionManager, address poolAddress, uint256 tokenId, @@ -77,6 +77,14 @@ contract UniswapV3Helper { sqrtRatioX96 ); } + + function positionPrincipal( + INonfungiblePositionManager positionManager, + uint256 tokenId, + uint160 sqrtRatioX96 + ) external view returns (uint256 amount0, uint256 amount1) { + return PositionValue.principal(positionManager, tokenId, sqrtRatioX96); + } } /// @dev Couldn't import this library directly either because of issues with OpenZeppelin versioning diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index eec52414bf..cf43cacda3 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -4,12 +4,7 @@ const { ethers } = hre; const addresses = require("../utils/addresses"); const { fundAccounts } = require("../utils/funding"); -const { - getAssetAddresses, - daiUnits, - isFork, - isForkWithLocalNode, -} = require("./helpers"); +const { getAssetAddresses, daiUnits, isFork } = require("./helpers"); const { utils } = require("ethers"); const { airDropPayouts } = require("../scripts/staking/airDrop.js"); diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 941fb89a57..33064d0f6c 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -10,12 +10,10 @@ const { usdcUnitsFormat, usdtUnitsFormat, daiUnits, - isFork, daiUnitsFormat, getBlockTimestamp, } = require("../helpers"); const { BigNumber, utils } = require("ethers"); -const { ethers } = hre; const uniswapV3Fixture = uniswapV3FixtureSetup(); @@ -23,17 +21,13 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { this.timeout(0); let fixture; - let vault, harvester, ousd, usdc, usdt, dai; + let vault, ousd, usdc, usdt, dai; let reserveStrategy, strategy, pool, positionManager, v3Helper, swapRouter; - let timelock, - // governor, - // strategist, - operator, - josh, - matt, - daniel, - domen, - franck; + let timelock; + // governor, + // strategist, + // harvester + let operator, josh, matt, daniel, domen, franck; beforeEach(async () => { fixture = await uniswapV3Fixture(); @@ -49,7 +43,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { usdt = fixture.usdt; dai = fixture.dai; vault = fixture.vault; - harvester = fixture.harvester; + // harvester = fixture.harvester; // governor = fixture.governor; // strategist = fixture.strategist; operator = fixture.operator; @@ -62,26 +56,14 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { }); async function setRebalancePriceThreshold(lowerTick, upperTick) { - const { vault } = fixture; - const { governorAddr, timelockAddr } = await getNamedAccounts(); - const sGovernor = await ethers.provider.getSigner( - isFork ? timelockAddr : governorAddr - ); - await strategy - .connect(sGovernor) + .connect(timelock) .setRebalancePriceThreshold(lowerTick, upperTick); } // maxTvl is denominated in 18 decimals already async function setMaxTVL(maxTvl) { - const { vault } = fixture; - const { governorAddr, timelockAddr } = await getNamedAccounts(); - const sGovernor = await ethers.provider.getSigner( - isFork ? timelockAddr : governorAddr - ); - - await strategy.connect(sGovernor).setMaxTVL(utils.parseUnits(maxTvl, 18)); + await strategy.connect(timelock).setMaxTVL(utils.parseUnits(maxTvl, 18)); } describe("Uniswap V3 LP positions", function () { diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 4f700c6c55..b2c677c3a9 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -6,11 +6,11 @@ const uniswapV3Fixture = uniswapV3FixturSetup(); describe("Uniswap V3 Strategy", function () { let fixture; - let vault, harvester, ousd, usdc, usdt, dai; + let vault, ousd, usdc, usdt, dai; let reserveStrategy, strategy, - mockPool, - mockPositionManager, + // mockPool, + // mockPositionManager, mockStrategy2, mockStrategyDAI; let governor, strategist, operator, josh, matt, daniel, domen, franck; @@ -21,14 +21,14 @@ describe("Uniswap V3 Strategy", function () { mockStrategy2 = fixture.mockStrategy2; mockStrategyDAI = fixture.mockStrategyDAI; strategy = fixture.UniV3_USDC_USDT_Strategy; - mockPool = fixture.UniV3_USDC_USDT_Pool; - mockPositionManager = fixture.UniV3PositionManager; + // mockPool = fixture.UniV3_USDC_USDT_Pool; + // mockPositionManager = fixture.UniV3PositionManager; ousd = fixture.ousd; usdc = fixture.usdc; usdt = fixture.usdt; dai = fixture.dai; vault = fixture.vault; - harvester = fixture.harvester; + // harvester = fixture.harvester; governor = fixture.governor; strategist = fixture.strategist; operator = fixture.operator; @@ -129,6 +129,9 @@ describe("Uniswap V3 Strategy", function () { await expect( strategy.connect(operator).setOperator(addr1) ).to.be.revertedWith("Caller is not the Strategist or Governor"); + await expect( + strategy.connect(josh).setOperator(addr1) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); }); }); @@ -215,9 +218,9 @@ describe("Uniswap V3 Strategy", function () { it("Cannot call with invalid assets", async () => { await expect( strategy - .connect(operator) + .connect(governor) .setMinDepositThreshold(dai.address, "2000") - ).to.be.revertedWith("Caller is not the Strategist or Governor"); + ).to.be.revertedWith("Unsupported asset"); }); }); }); From 7f11995d28f4935d730a257fb6e35ceba239d988 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 18 Mar 2023 16:22:55 +0530 Subject: [PATCH 30/83] Remove VaultValueChecker --- .../strategies/uniswap/UniswapV3Strategy.sol | 13 ++++--------- .../strategies/uniswap/UniswapV3StrategyStorage.sol | 4 ---- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index dc144e3d85..cd141d6674 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -17,7 +17,6 @@ import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3 import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import { StableMath } from "../../utils/StableMath.sol"; -import { IVaultValueChecker } from "../../interfaces/IVaultValueChecker.sol"; contract UniswapV3Strategy is UniswapV3StrategyStorage { using SafeERC20 for IERC20; @@ -30,7 +29,6 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @param _nonfungiblePositionManager Uniswap V3's Position Manager * @param _helper Deployed UniswapV3Helper contract * @param _swapRouter Uniswap SwapRouter contract - * @param _vaultValueChecker VaultValueChecker * @param _operator Operator address */ function initialize( @@ -39,7 +37,6 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { address _nonfungiblePositionManager, address _helper, address _swapRouter, - address _vaultValueChecker, address _operator ) external onlyGovernor initializer { // TODO: Comment on why this is necessary and why it should always be the proxy address @@ -52,8 +49,6 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { _nonfungiblePositionManager ); - vaultValueChecker = IVaultValueChecker(_vaultValueChecker); - token0 = pool.token0(); token1 = pool.token1(); poolFee = pool.fee(); @@ -289,8 +284,8 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { // Delegatecall to `UniswapV3LiquidityManager` to remove // liquidity from active LP position - // solhint-disable-next-line - (bool success, bytes memory _) = address(_self).delegatecall( + // solhint-disable-next-line no-unused-vars + (bool success, bytes memory data) = address(_self).delegatecall( abi.encodeWithSignature( "withdrawAssetFromActivePositionOnlyVault(address,uint256)", _asset, @@ -313,8 +308,8 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { if (activeTokenId > 0) { // Delegatecall to `UniswapV3LiquidityManager` to remove // liquidity from active LP position - // solhint-disable-next-line - (bool success, bytes memory _) = address(_self).delegatecall( + // solhint-disable-next-line no-unused-vars + (bool success, bytes memory data) = address(_self).delegatecall( abi.encodeWithSignature("closeActivePositionOnlyVault()") ); require(success, "DelegateCall to close position failed"); diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 602fd645ea..c31e26d846 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -10,7 +10,6 @@ import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3 import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; import { IUniswapV3Helper } from "../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; -import { IVaultValueChecker } from "../../interfaces/IVaultValueChecker.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { @@ -142,9 +141,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { // Uniswap Swap Router ISwapRouter internal swapRouter; - // VaultValueChecker - IVaultValueChecker public vaultValueChecker; - // A lookup table to find token IDs of position using f(lowerTick, upperTick) mapping(int48 => uint256) internal ticksToTokenId; From 331f33c80b409c9ab10cdf3a1c4675aeee619338 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 18 Mar 2023 19:36:35 +0530 Subject: [PATCH 31/83] Fork test for net value loss --- .../uniswap/UniswapV3LiquidityManager.sol | 9 +- .../strategies/uniswap/UniswapV3Strategy.sol | 23 +- .../uniswap/UniswapV3StrategyStorage.sol | 23 +- .../deploy/049_uniswap_usdc_usdt_strategy.js | 37 +- contracts/test/_fixture.js | 26 +- .../test/strategies/uniswap-v3.fork-test.js | 397 ++++++++++++------ 6 files changed, 347 insertions(+), 168 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 3048d81d88..2a07ab5743 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -177,13 +177,13 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { } function rebalanceNotPaused() internal { - require(rebalancePaused, "Rebalances are paused"); + require(!rebalancePaused, "Rebalances are paused"); } function rebalanceNotPausedAndWithinLimits(int24 lowerTick, int24 upperTick) internal { - require(rebalancePaused, "Rebalances are paused"); + require(!rebalancePaused, "Rebalances are paused"); require( minRebalanceTick <= lowerTick && maxRebalanceTick >= upperTick, "Rebalance position out of bounds" @@ -731,8 +731,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { desiredAmount1 - token1Balance ); } - - // TODO: Check value of assets moved here } function _ensureAssetsBySwapping( @@ -823,8 +821,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { swapAmountIn, amountReceived ); - - // TODO: Check value of assets moved here } function collectFees() @@ -870,7 +866,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /*************************************** Hidden functions ****************************************/ - // solhint-disable-next-line function _abstractSetPToken(address, address) internal override { revert("NO_IMPL"); } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index cd141d6674..9d804d9512 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -96,9 +96,10 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { function setReserveStrategy(address _asset, address _reserveStrategy) external onlyGovernorOrStrategist - onlyPoolTokens(_asset) nonReentrant { + onlyPoolTokens(_asset); + require( IVault(vaultAddress).isStrategySupported(_reserveStrategy), "Unsupported strategy" @@ -126,7 +127,6 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { function reserveStrategy(address _asset) external view - onlyPoolTokens(_asset) returns (address reserveStrategyAddr) { if (_asset == token0) { @@ -144,8 +144,9 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { function setMinDepositThreshold(address _asset, uint256 _minThreshold) external onlyGovernorOrStrategist - onlyPoolTokens(_asset) { + onlyPoolTokens(_asset); + if (_asset == token0) { minDepositThreshold0 = _minThreshold; } else if (_asset == token1) { @@ -249,9 +250,10 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { external override onlyVault - onlyPoolTokens(_asset) nonReentrant { + onlyPoolTokens(_asset); + if ( _amount > 0 && ( @@ -275,7 +277,9 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { address recipient, address _asset, uint256 amount - ) external override onlyVault onlyPoolTokens(_asset) nonReentrant { + ) external override onlyVault nonReentrant { + onlyPoolTokens(_asset); + IERC20 asset = IERC20(_asset); uint256 selfBalance = asset.balanceOf(address(this)); @@ -361,9 +365,9 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { external view override - onlyPoolTokens(_asset) returns (uint256 balance) { + onlyPoolTokens(_asset); balance = IERC20(_asset).balanceOf(address(this)); if (activeTokenId > 0) { @@ -380,6 +384,13 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { } } + /** + * @dev Ensures that the asset address is either token0 or token1. + */ + function onlyPoolTokens(address addr) internal view { + require(addr == token0 || addr == token1, "Unsupported asset"); + } + /*************************************** ERC721 management ****************************************/ diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index c31e26d846..05e6803963 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -99,15 +99,15 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { IStrategy public reserveStrategy0; // Reserve strategy for token0 IStrategy public reserveStrategy1; // Reserve strategy for token1 - // Deposits to reserve strategy when contract balance exceeds this amount - uint256 public minDepositThreshold0; - uint256 public minDepositThreshold1; - uint24 public poolFee; // Uniswap V3 Pool Fee bool public swapsPaused = false; // True if Swaps are paused bool public rebalancePaused = false; // True if Swaps are paused - uint256 public maxTVL; // In USD, 18 decimals + uint256 public maxTVL = 1000000 ether; // In USD, 18 decimals, defaults to 1M + + // Deposits to reserve strategy when contract balance exceeds this amount + uint256 public minDepositThreshold0; + uint256 public minDepositThreshold1; // An upper and lower bound of rebalancing price limits int24 public minRebalanceTick; @@ -120,14 +120,11 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { // Token ID of active Position on the pool. zero, if there are no active LP position uint256 public activeTokenId; - // Value of the active position since mint (or last liquidity change) - uint256 public activePositionMintValue = 0; - // Sum of loss in value of tokens deployed to the pool uint256 public netLostValue = 0; // Max value loss threshold after which rebalances aren't allowed - uint256 public maxPositionValueLossThreshold; + uint256 public maxPositionValueLossThreshold = 50000 ether; // default to 50k // Uniswap V3's Pool IUniswapV3Pool public pool; @@ -183,14 +180,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { _; } - /** - * @dev Ensures that the asset address is either token0 or token1. - */ - modifier onlyPoolTokens(address addr) { - require(addr == token0 || addr == token1, "Unsupported asset"); - _; - } - /*************************************** Shared functions ****************************************/ diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index 9faf427deb..1c03d47e04 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -1,4 +1,5 @@ const { deploymentWithGovernanceProposal } = require("../utils/deploy"); +const { utils } = require("ethers"); module.exports = deploymentWithGovernanceProposal( { @@ -164,12 +165,42 @@ module.exports = deploymentWithGovernanceProposal( signature: "setReserveStrategy(address,address)", args: [assetAddresses.USDT, cMorphoCompProxy.address], }, - // 4. Set Reserve Strategy for USDT + // 7. Set Minimum Deposit threshold for USDC + { + contract: cUniV3_USDC_USDT_Strategy, + signature: "setMinDepositThreshold(address,uint256)", + args: [assetAddresses.USDC, utils.parseUnits("30000", 6)], // 30k + }, + // 8. Set Minimum Deposit threshold for USDT + { + contract: cUniV3_USDC_USDT_Strategy, + signature: "setMinDepositThreshold(address,uint256)", + args: [assetAddresses.USDT, utils.parseUnits("30000", 6)], // 30k + }, + // // 9. Set Max TVL // { // contract: cUniV3_USDC_USDT_Strategy, - // signature: "setSwapPriceThreshold(int24,int24)", - // args: [-1000, 1000], + // signature: "setMaxTVL(uint256)", + // args: [utils.parseEther("1000000", 18)], // 1M // }, + // // 10. Set Max Loss threshold + // { + // contract: cUniV3_USDC_USDT_Strategy, + // signature: "setMaxPositionValueLossThreshold(uint256)", + // args: [utils.parseEther("50000", 18)], // 50k + // }, + // 11. Set Rebalance Price Threshold + { + contract: cUniV3_USDC_USDT_Strategy, + signature: "setRebalancePriceThreshold(int24,int24)", + args: [-1000, 1000], + }, + // 12. Set Swap price threshold + { + contract: cUniV3_USDC_USDT_Strategy, + signature: "setSwapPriceThreshold(int24,int24)", + args: [-1000, 1000], + }, ], }; } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index cf43cacda3..5e8e4908e7 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1279,15 +1279,23 @@ function uniswapV3FixtureSetup() { const sGovernor = await ethers.provider.getSigner( isFork ? timelockAddr : governorAddr ); - - UniV3_USDC_USDT_Strategy.connect(sGovernor) - // 2 million - .setMaxTVL(utils.parseUnits("2", 24)); - - UniV3_USDC_USDT_Strategy.connect(sGovernor).setRebalancePriceThreshold( - -100, - 100 - ); + + if (!isFork) { + UniV3_USDC_USDT_Strategy.connect(sGovernor) + // 2 million + .setMaxTVL(utils.parseUnits("2", 24)); + UniV3_USDC_USDT_Strategy.connect(sGovernor).setRebalancePriceThreshold( + -100, + 100 + ); + } else { + const [, activeTick] = await fixture.UniV3_USDC_USDT_Pool.slot0(); + await UniV3_USDC_USDT_Strategy.connect(sGovernor).setRebalancePriceThreshold(activeTick - 10000, activeTick + 10000); + await UniV3_USDC_USDT_Strategy.connect(sGovernor).setSwapPriceThreshold(activeTick - 10000, activeTick + 10000); + await UniV3_USDC_USDT_Strategy.connect(sGovernor).setMaxPositionValueLossThreshold(utils.parseUnits('50000', 18)); + UniV3_USDC_USDT_Strategy.connect(sGovernor) + .setMaxTVL(utils.parseUnits('2000000', 18)); // 2M + } return fixture; }); diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 33064d0f6c..2dca102f1b 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -61,11 +61,31 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { .setRebalancePriceThreshold(lowerTick, upperTick); } - // maxTvl is denominated in 18 decimals already + async function setSwapPriceThreshold(lowerTick, upperTick) { + await strategy + .connect(timelock) + .setSwapPriceThreshold(lowerTick, upperTick); + } + async function setMaxTVL(maxTvl) { await strategy.connect(timelock).setMaxTVL(utils.parseUnits(maxTvl, 18)); } + async function setMinDepositThreshold(asset, minThreshold) { + await strategy + .connect(timelock) + .setMaxTVL( + asset.address, + utils.parseUnits(minThreshold, await asset.decimals()) + ); + } + + async function setMaxPositionValueLossThreshold(maxLossThreshold) { + await strategy + .connect(timelock) + .setMaxPositionValueLossThreshold(utils.parseUnits(maxLossThreshold, 18)); + } + describe("Uniswap V3 LP positions", function () { // NOTE: These tests all work on the assumption that the strategy // has no active position, which might not be true after deployment. @@ -139,6 +159,43 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { }; }; + const increaseLiquidity = async ( + tokenId, + usdcAmount, + usdtAmount, + returnAsPromise + ) => { + const storedPosition = await strategy.tokenIdToPosition(tokenId); + const [maxUSDC, maxUSDT] = await findMaxDepositableAmount( + storedPosition.lowerTick, + storedPosition.upperTick, + BigNumber.from(usdcAmount).mul(10 ** 6), + BigNumber.from(usdtAmount).mul(10 ** 6) + ); + + const tx = await strategy + .connect(operator) + .increaseActivePositionLiquidity( + maxUSDC, + maxUSDT, + maxUSDC.mul(9900).div(10000), + maxUSDT.mul(9900).div(10000) + ); + + const { events } = await tx.wait(); + + const [, amount0Added, amount1Added, liquidityAdded] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + + return { + tokenId, + amount0Added, + amount1Added, + liquidityAdded, + tx, + }; + }; + const mintLiquidityBySwapping = async ( lowerTick, upperTick, @@ -234,6 +291,9 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(storedPosition.lowerTick).to.equal(lowerTick); expect(storedPosition.upperTick).to.equal(upperTick); expect(storedPosition.liquidity).to.equal(liquidityMinted); + expect(storedPosition.netValue).to.equal( + amount0Minted.add(amount1Minted).mul(BigNumber.from(1e12)) + ); expect(await strategy.activeTokenId()).to.equal(tokenId); }); @@ -317,14 +377,16 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { usdtBalBefore, "Expected USDT balance to have increased" ); - expect(usdcBalAfter).to.approxEqual( - usdcBalBefore.add(amount0Minted), - "Deposited USDC mismatch" - ); - expect(usdtBalAfter).to.approxEqual( - usdtBalBefore.add(amount1Minted), - "Deposited USDT mismatch" - ); + // expect(usdcBalAfter).to.approxEqualTolerance( + // usdcBalBefore.add(amount0Minted), + // 1, + // "Deposited USDC mismatch" + // ); + // expect(usdtBalAfter).to.approxEqualTolerance( + // usdtBalBefore.add(amount1Minted), + // 1, + // "Deposited USDT mismatch" + // ); // Check data on strategy const storedPosition = await strategy.tokenIdToPosition(tokenId); @@ -333,6 +395,9 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(storedPosition.lowerTick).to.equal(lowerTick); expect(storedPosition.upperTick).to.equal(upperTick); expect(storedPosition.liquidity).to.equal(liquidityMinted); + expect(storedPosition.netValue).to.equal( + amount0Minted.add(amount1Minted).mul(BigNumber.from(1e12)) + ); expect(await strategy.activeTokenId()).to.equal(tokenId); }); @@ -385,6 +450,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Check Strategy balance const usdcBalAfter = await strategy.checkBalance(usdc.address); const usdtBalAfter = await strategy.checkBalance(usdt.address); + expect(usdcBalAfter).gte( usdcBalBefore, "Expected USDC balance to have increased" @@ -393,14 +459,14 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { usdtBalBefore, "Expected USDT balance to have increased" ); - expect(usdcBalAfter).to.approxEqual( - usdcBalBefore.add(amount0Minted), - "Deposited USDC mismatch" - ); - expect(usdtBalAfter).to.approxEqual( - usdtBalBefore.add(amount1Minted), - "Deposited USDT mismatch" - ); + // expect(usdcBalAfter).to.approxEqual( + // usdcBalBefore.add(amount0Minted), + // "Deposited USDC mismatch" + // ); + // expect(usdtBalAfter).to.approxEqual( + // usdtBalBefore.add(amount1Minted), + // "Deposited USDT mismatch" + // ); // Check data on strategy const storedPosition = await strategy.tokenIdToPosition(tokenId); @@ -409,6 +475,9 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(storedPosition.lowerTick).to.equal(lowerTick); expect(storedPosition.upperTick).to.equal(upperTick); expect(storedPosition.liquidity).to.equal(liquidityMinted); + expect(storedPosition.netValue).to.equal( + amount0Minted.add(amount1Minted).mul(BigNumber.from(1e12)) + ); expect(await strategy.activeTokenId()).to.equal(tokenId); }); @@ -424,22 +493,31 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const amountUnits = BigNumber.from(amount).mul(10 ** 6); // Mint position - const { tokenId, tx } = await mintLiquidity( + const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = await mintLiquidity( lowerTick, upperTick, amount, amount ); await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); - const storedPosition = await strategy.tokenIdToPosition(tokenId); + let storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.exists).to.be.true; + expect(storedPosition.liquidity).to.equal(liquidityMinted); + expect(storedPosition.netValue).to.equal( + amount0Minted.add(amount1Minted).mul(BigNumber.from(1e12)) + ); expect(await strategy.activeTokenId()).to.equal(tokenId); // Rebalance again to increase liquidity - const tx2 = await strategy - .connect(operator) - .increaseActivePositionLiquidity(amountUnits, amountUnits, 0, 0); - await expect(tx2).to.have.emittedEvent("UniswapV3LiquidityAdded"); + const { tx: increaseTx, amount0Added, amount1Added, liquidityAdded } = await increaseLiquidity(tokenId, amount, amount); + await expect(increaseTx).to.have.emittedEvent("UniswapV3LiquidityAdded"); + + // Check storage + storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.liquidity).to.approxEqual(liquidityMinted.add(liquidityAdded)); + expect(storedPosition.netValue).to.approxEqual( + amount0Minted.add(amount1Minted).add(amount0Added).add(amount1Added).mul(BigNumber.from(1e12)) + ); // Check balance on strategy const usdcBalAfter = await strategy.checkBalance(usdc.address); @@ -471,13 +549,18 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { amount ); await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); - const storedPosition = await strategy.tokenIdToPosition(tokenId); + let storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.exists).to.be.true; expect(await strategy.activeTokenId()).to.equal(tokenId); // Remove liquidity const tx2 = await strategy.connect(operator).closePosition(tokenId, 0, 0); await expect(tx2).to.have.emittedEvent("UniswapV3LiquidityRemoved"); + + // Check storage + storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.liquidity).to.be.equal(0); + expect(storedPosition.netValue).to.be.equal(0); expect(await strategy.activeTokenId()).to.equal( BigNumber.from(0), @@ -499,7 +582,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const swapAmount = BigNumber.from(amount).mul(10 ** 6); usdc.connect(user).approve(swapRouter.address, swapAmount.mul(10)); usdt.connect(user).approve(swapRouter.address, swapAmount.mul(10)); - await swapRouter.connect(user).exactInputSingle([ + const tx = await swapRouter.connect(user).exactInputSingle([ zeroForOne ? usdc.address : usdt.address, // tokenIn zeroForOne ? usdt.address : usdc.address, // tokenOut 100, // fee @@ -547,116 +630,178 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(fee0).to.equal(0); expect(fee1).to.equal(0); }); - }); - describe("Mint", function () { - const mintTest = async (user, amount, asset) => { - const ousdAmount = ousdUnits(amount); - const tokenAmount = await units(amount, asset); + it("Should not mint if net loss threshold is breached", async () => { + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick; + const upperTick = activeTick + 1; - const currentSupply = await ousd.totalSupply(); - const ousdBalance = await ousd.balanceOf(user.address); - const tokenBalance = await asset.balanceOf(user.address); - const reserveTokenBalance = await reserveStrategy.checkBalance( - asset.address + // Mint position + const amount = "100000"; + const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = await mintLiquidity( + lowerTick, + upperTick, + amount, + amount ); + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + let storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.exists).to.be.true; + expect(await strategy.activeTokenId()).to.equal(tokenId); + expect(await strategy.netLostValue()).to.equal(0) - // await asset.connect(user).approve(vault.address, tokenAmount) - await vault.connect(user).mint(asset.address, tokenAmount, 0); + // Do some big swaps to move active tick + await _swap(matt, "1000000", false); + await _swap(josh, "1000000", false); + await _swap(franck, "1000000", false); + await _swap(daniel, "1000000", false); + await _swap(domen, "1000000", false); - await expect(ousd).to.have.an.approxTotalSupplyOf( - currentSupply.add(ousdAmount), - "Total supply mismatch" - ); - if (asset == dai) { - // DAI is unsupported and should not be deposited in reserve strategy - await expect(reserveStrategy).to.have.an.assetBalanceOf( - reserveTokenBalance, - asset, - "Expected reserve strategy to not support DAI" + // Set threshold to a low value to see if it throws + await setMaxPositionValueLossThreshold("0.01"); + await expect( + strategy + .connect(operator) + .increaseActivePositionLiquidity( + "1000000", + "1000000", + "0", + "0" + ) + ).to.be.revertedWith("Over max value loss threshold") + + // Set the threshold higher and make sure the net loss event is emitted + // and state updated properly + await setMaxPositionValueLossThreshold("1000000000000000"); + const { amount0Added, amount1Added, liquidityAdded } = await increaseLiquidity(tokenId, "1", "1") + storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.liquidity).approxEqual( + liquidityMinted.add(liquidityAdded) + ) + const netLost = await strategy.netLostValue() + expect(netLost).to.be.gte(0, "Expected lost value to have been updated") + expect(storedPosition.netValue).to.be.lte( + amount0Minted + .add(amount1Minted) + .add(amount0Added) + .add(amount1Added) + .mul(1e12) + .sub(netLost) + ) + }) + }); + + describe("Sanity checks", () => { + describe("Mint", function () { + const mintTest = async (user, amount, asset) => { + const ousdAmount = ousdUnits(amount); + const tokenAmount = await units(amount, asset); + + const currentSupply = await ousd.totalSupply(); + const ousdBalance = await ousd.balanceOf(user.address); + const tokenBalance = await asset.balanceOf(user.address); + const reserveTokenBalance = await reserveStrategy.checkBalance( + asset.address ); - } else { - await expect(reserveStrategy).to.have.an.assetBalanceOf( - reserveTokenBalance.add(tokenAmount), + + // await asset.connect(user).approve(vault.address, tokenAmount) + await vault.connect(user).mint(asset.address, tokenAmount, 0); + + await expect(ousd).to.have.an.approxTotalSupplyOf( + currentSupply.add(ousdAmount), + "Total supply mismatch" + ); + if (asset == dai) { + // DAI is unsupported and should not be deposited in reserve strategy + await expect(reserveStrategy).to.have.an.assetBalanceOf( + reserveTokenBalance, + asset, + "Expected reserve strategy to not support DAI" + ); + } else { + await expect(reserveStrategy).to.have.an.assetBalanceOf( + reserveTokenBalance.add(tokenAmount), + asset, + "Expected reserve strategy to have received the other token" + ); + } + + await expect(user).to.have.an.approxBalanceWithToleranceOf( + ousdBalance.add(ousdAmount), + ousd, + 1, + "Should've minted equivalent OUSD" + ); + await expect(user).to.have.an.approxBalanceWithToleranceOf( + tokenBalance.sub(tokenAmount), asset, - "Expected reserve strategy to have received the other token" + 1, + "Should've deposoited equivaluent other token" ); - } - - await expect(user).to.have.an.approxBalanceWithToleranceOf( - ousdBalance.add(ousdAmount), - ousd, - 1, - "Should've minted equivalent OUSD" - ); - await expect(user).to.have.an.approxBalanceWithToleranceOf( - tokenBalance.sub(tokenAmount), - asset, - 1, - "Should've deposoited equivaluent other token" - ); - }; - - it("with USDC", async () => { - await mintTest(daniel, "30000", usdc); - }); - it("with USDT", async () => { - await mintTest(domen, "30000", usdt); - }); - it("with DAI", async () => { - await mintTest(franck, "30000", dai); + }; + + it("with USDC", async () => { + await mintTest(daniel, "30000", usdc); + }); + it("with USDT", async () => { + await mintTest(domen, "30000", usdt); + }); + it("with DAI", async () => { + await mintTest(franck, "30000", dai); + }); }); - }); - - describe("Redeem", function () { - const redeemTest = async (user, amount) => { - const ousdAmount = ousdUnits(amount); - - let ousdBalance = await ousd.balanceOf(user.address); - if (ousdBalance.lt(ousdAmount)) { - // Mint some OUSD - await vault.connect(user).mint(dai.address, daiUnits(amount), 0); - ousdBalance = await ousd.balanceOf(user.address); - } - - const currentSupply = await ousd.totalSupply(); - const usdcBalance = await usdc.balanceOf(user.address); - const usdtBalance = await usdt.balanceOf(user.address); - const daiBalance = await dai.balanceOf(user.address); - - await vault.connect(user).redeem(ousdAmount, 0); - - await expect(ousd).to.have.an.approxTotalSupplyOf( - currentSupply.sub(ousdAmount), - "Total supply mismatch" - ); - await expect(user).to.have.an.approxBalanceWithToleranceOf( - ousdBalance.sub(ousdAmount), - ousd, - 1, - "Should've burned equivalent OUSD" - ); - - const balanceDiff = - parseFloat( - usdcUnitsFormat((await usdc.balanceOf(user.address)) - usdcBalance) - ) + - parseFloat( - usdtUnitsFormat((await usdt.balanceOf(user.address)) - usdtBalance) - ) + - parseFloat( - daiUnitsFormat((await dai.balanceOf(user.address)) - daiBalance) + + describe("Redeem", function () { + const redeemTest = async (user, amount) => { + const ousdAmount = ousdUnits(amount); + + let ousdBalance = await ousd.balanceOf(user.address); + if (ousdBalance.lt(ousdAmount)) { + // Mint some OUSD + await vault.connect(user).mint(dai.address, daiUnits(amount), 0); + ousdBalance = await ousd.balanceOf(user.address); + } + + const currentSupply = await ousd.totalSupply(); + const usdcBalance = await usdc.balanceOf(user.address); + const usdtBalance = await usdt.balanceOf(user.address); + const daiBalance = await dai.balanceOf(user.address); + + await vault.connect(user).redeem(ousdAmount, 0); + + await expect(ousd).to.have.an.approxTotalSupplyOf( + currentSupply.sub(ousdAmount), + "Total supply mismatch" ); - - await expect(balanceDiff).to.approxEqualTolerance( - amount, - 1, - "Should've redeemed equivaluent other token" - ); - }; - - it("Should withdraw from reserve strategy", async () => { - redeemTest(josh, "10000"); + await expect(user).to.have.an.approxBalanceWithToleranceOf( + ousdBalance.sub(ousdAmount), + ousd, + 1, + "Should've burned equivalent OUSD" + ); + + const balanceDiff = + parseFloat( + usdcUnitsFormat((await usdc.balanceOf(user.address)) - usdcBalance) + ) + + parseFloat( + usdtUnitsFormat((await usdt.balanceOf(user.address)) - usdtBalance) + ) + + parseFloat( + daiUnitsFormat((await dai.balanceOf(user.address)) - daiBalance) + ); + + await expect(balanceDiff).to.approxEqualTolerance( + amount, + 1, + "Should've redeemed equivaluent other token" + ); + }; + + it("Should withdraw from reserve strategy", async () => { + redeemTest(josh, "10000"); + }); }); - }); + }) + }); From 2c9c91edbeef56f497762117f73ad0e804e4b51e Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 18 Mar 2023 19:39:43 +0530 Subject: [PATCH 32/83] Check event --- contracts/test/_fixture.js | 20 ++-- .../test/strategies/uniswap-v3.fork-test.js | 101 +++++++++--------- 2 files changed, 67 insertions(+), 54 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 5e8e4908e7..a6cacd8543 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1279,7 +1279,7 @@ function uniswapV3FixtureSetup() { const sGovernor = await ethers.provider.getSigner( isFork ? timelockAddr : governorAddr ); - + if (!isFork) { UniV3_USDC_USDT_Strategy.connect(sGovernor) // 2 million @@ -1290,11 +1290,19 @@ function uniswapV3FixtureSetup() { ); } else { const [, activeTick] = await fixture.UniV3_USDC_USDT_Pool.slot0(); - await UniV3_USDC_USDT_Strategy.connect(sGovernor).setRebalancePriceThreshold(activeTick - 10000, activeTick + 10000); - await UniV3_USDC_USDT_Strategy.connect(sGovernor).setSwapPriceThreshold(activeTick - 10000, activeTick + 10000); - await UniV3_USDC_USDT_Strategy.connect(sGovernor).setMaxPositionValueLossThreshold(utils.parseUnits('50000', 18)); - UniV3_USDC_USDT_Strategy.connect(sGovernor) - .setMaxTVL(utils.parseUnits('2000000', 18)); // 2M + await UniV3_USDC_USDT_Strategy.connect( + sGovernor + ).setRebalancePriceThreshold(activeTick - 10000, activeTick + 10000); + await UniV3_USDC_USDT_Strategy.connect(sGovernor).setSwapPriceThreshold( + activeTick - 10000, + activeTick + 10000 + ); + await UniV3_USDC_USDT_Strategy.connect( + sGovernor + ).setMaxPositionValueLossThreshold(utils.parseUnits("50000", 18)); + UniV3_USDC_USDT_Strategy.connect(sGovernor).setMaxTVL( + utils.parseUnits("2000000", 18) + ); // 2M } return fixture; diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 2dca102f1b..92a896c0b4 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -184,8 +184,9 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const { events } = await tx.wait(); - const [, amount0Added, amount1Added, liquidityAdded] = - events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + const [, amount0Added, amount1Added, liquidityAdded] = events.find( + (e) => e.event == "UniswapV3LiquidityAdded" + ).args; return { tokenId, @@ -450,7 +451,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Check Strategy balance const usdcBalAfter = await strategy.checkBalance(usdc.address); const usdtBalAfter = await strategy.checkBalance(usdt.address); - + expect(usdcBalAfter).gte( usdcBalBefore, "Expected USDC balance to have increased" @@ -493,12 +494,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const amountUnits = BigNumber.from(amount).mul(10 ** 6); // Mint position - const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = await mintLiquidity( - lowerTick, - upperTick, - amount, - amount - ); + const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = + await mintLiquidity(lowerTick, upperTick, amount, amount); await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); let storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.exists).to.be.true; @@ -509,14 +506,25 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(await strategy.activeTokenId()).to.equal(tokenId); // Rebalance again to increase liquidity - const { tx: increaseTx, amount0Added, amount1Added, liquidityAdded } = await increaseLiquidity(tokenId, amount, amount); + const { + tx: increaseTx, + amount0Added, + amount1Added, + liquidityAdded, + } = await increaseLiquidity(tokenId, amount, amount); await expect(increaseTx).to.have.emittedEvent("UniswapV3LiquidityAdded"); // Check storage storedPosition = await strategy.tokenIdToPosition(tokenId); - expect(storedPosition.liquidity).to.approxEqual(liquidityMinted.add(liquidityAdded)); + expect(storedPosition.liquidity).to.approxEqual( + liquidityMinted.add(liquidityAdded) + ); expect(storedPosition.netValue).to.approxEqual( - amount0Minted.add(amount1Minted).add(amount0Added).add(amount1Added).mul(BigNumber.from(1e12)) + amount0Minted + .add(amount1Minted) + .add(amount0Added) + .add(amount1Added) + .mul(BigNumber.from(1e12)) ); // Check balance on strategy @@ -556,7 +564,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Remove liquidity const tx2 = await strategy.connect(operator).closePosition(tokenId, 0, 0); await expect(tx2).to.have.emittedEvent("UniswapV3LiquidityRemoved"); - + // Check storage storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.liquidity).to.be.equal(0); @@ -638,17 +646,13 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Mint position const amount = "100000"; - const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = await mintLiquidity( - lowerTick, - upperTick, - amount, - amount - ); + const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = + await mintLiquidity(lowerTick, upperTick, amount, amount); await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); let storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.exists).to.be.true; expect(await strategy.activeTokenId()).to.equal(tokenId); - expect(await strategy.netLostValue()).to.equal(0) + expect(await strategy.netLostValue()).to.equal(0); // Do some big swaps to move active tick await _swap(matt, "1000000", false); @@ -662,24 +666,26 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { await expect( strategy .connect(operator) - .increaseActivePositionLiquidity( - "1000000", - "1000000", - "0", - "0" - ) - ).to.be.revertedWith("Over max value loss threshold") + .increaseActivePositionLiquidity("1000000", "1000000", "0", "0") + ).to.be.revertedWith("Over max value loss threshold"); // Set the threshold higher and make sure the net loss event is emitted // and state updated properly await setMaxPositionValueLossThreshold("1000000000000000"); - const { amount0Added, amount1Added, liquidityAdded } = await increaseLiquidity(tokenId, "1", "1") + const { + tx: increaseTx, + amount0Added, + amount1Added, + liquidityAdded, + } = await increaseLiquidity(tokenId, "1", "1"); + await expect(increaseTx).to.have.emittedEvent("PositionLostValue"); + storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.liquidity).approxEqual( liquidityMinted.add(liquidityAdded) - ) - const netLost = await strategy.netLostValue() - expect(netLost).to.be.gte(0, "Expected lost value to have been updated") + ); + const netLost = await strategy.netLostValue(); + expect(netLost).to.be.gte(0, "Expected lost value to have been updated"); expect(storedPosition.netValue).to.be.lte( amount0Minted .add(amount1Minted) @@ -687,8 +693,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { .add(amount1Added) .mul(1e12) .sub(netLost) - ) - }) + ); + }); }); describe("Sanity checks", () => { @@ -696,17 +702,17 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const mintTest = async (user, amount, asset) => { const ousdAmount = ousdUnits(amount); const tokenAmount = await units(amount, asset); - + const currentSupply = await ousd.totalSupply(); const ousdBalance = await ousd.balanceOf(user.address); const tokenBalance = await asset.balanceOf(user.address); const reserveTokenBalance = await reserveStrategy.checkBalance( asset.address ); - + // await asset.connect(user).approve(vault.address, tokenAmount) await vault.connect(user).mint(asset.address, tokenAmount, 0); - + await expect(ousd).to.have.an.approxTotalSupplyOf( currentSupply.add(ousdAmount), "Total supply mismatch" @@ -725,7 +731,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { "Expected reserve strategy to have received the other token" ); } - + await expect(user).to.have.an.approxBalanceWithToleranceOf( ousdBalance.add(ousdAmount), ousd, @@ -739,7 +745,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { "Should've deposoited equivaluent other token" ); }; - + it("with USDC", async () => { await mintTest(daniel, "30000", usdc); }); @@ -750,25 +756,25 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { await mintTest(franck, "30000", dai); }); }); - + describe("Redeem", function () { const redeemTest = async (user, amount) => { const ousdAmount = ousdUnits(amount); - + let ousdBalance = await ousd.balanceOf(user.address); if (ousdBalance.lt(ousdAmount)) { // Mint some OUSD await vault.connect(user).mint(dai.address, daiUnits(amount), 0); ousdBalance = await ousd.balanceOf(user.address); } - + const currentSupply = await ousd.totalSupply(); const usdcBalance = await usdc.balanceOf(user.address); const usdtBalance = await usdt.balanceOf(user.address); const daiBalance = await dai.balanceOf(user.address); - + await vault.connect(user).redeem(ousdAmount, 0); - + await expect(ousd).to.have.an.approxTotalSupplyOf( currentSupply.sub(ousdAmount), "Total supply mismatch" @@ -779,7 +785,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { 1, "Should've burned equivalent OUSD" ); - + const balanceDiff = parseFloat( usdcUnitsFormat((await usdc.balanceOf(user.address)) - usdcBalance) @@ -790,18 +796,17 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { parseFloat( daiUnitsFormat((await dai.balanceOf(user.address)) - daiBalance) ); - + await expect(balanceDiff).to.approxEqualTolerance( amount, 1, "Should've redeemed equivaluent other token" ); }; - + it("Should withdraw from reserve strategy", async () => { redeemTest(josh, "10000"); }); }); - }) - + }); }); From dedcbac463945649301c80a751af88a6732f0816 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 18 Mar 2023 19:41:14 +0530 Subject: [PATCH 33/83] Cleanup --- .../test/strategies/uniswap-v3.fork-test.js | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 92a896c0b4..a1e8955e0b 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -61,25 +61,10 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { .setRebalancePriceThreshold(lowerTick, upperTick); } - async function setSwapPriceThreshold(lowerTick, upperTick) { - await strategy - .connect(timelock) - .setSwapPriceThreshold(lowerTick, upperTick); - } - async function setMaxTVL(maxTvl) { await strategy.connect(timelock).setMaxTVL(utils.parseUnits(maxTvl, 18)); } - async function setMinDepositThreshold(asset, minThreshold) { - await strategy - .connect(timelock) - .setMaxTVL( - asset.address, - utils.parseUnits(minThreshold, await asset.decimals()) - ); - } - async function setMaxPositionValueLossThreshold(maxLossThreshold) { await strategy .connect(timelock) @@ -159,12 +144,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { }; }; - const increaseLiquidity = async ( - tokenId, - usdcAmount, - usdtAmount, - returnAsPromise - ) => { + const increaseLiquidity = async (tokenId, usdcAmount, usdtAmount) => { const storedPosition = await strategy.tokenIdToPosition(tokenId); const [maxUSDC, maxUSDT] = await findMaxDepositableAmount( storedPosition.lowerTick, @@ -590,7 +570,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const swapAmount = BigNumber.from(amount).mul(10 ** 6); usdc.connect(user).approve(swapRouter.address, swapAmount.mul(10)); usdt.connect(user).approve(swapRouter.address, swapAmount.mul(10)); - const tx = await swapRouter.connect(user).exactInputSingle([ + await swapRouter.connect(user).exactInputSingle([ zeroForOne ? usdc.address : usdt.address, // tokenIn zeroForOne ? usdt.address : usdc.address, // tokenOut 100, // fee From 81a02ad8631e0ec0dc925c477e3e3cbec557f4c0 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 18 Mar 2023 15:35:40 +0100 Subject: [PATCH 34/83] allow for net lost value to reduce on favourable rebalances --- .../uniswap/UniswapV3LiquidityManager.sol | 16 +++++++--------- .../uniswap/UniswapV3StrategyStorage.sol | 4 ++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 2a07ab5743..e18181ad13 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -192,24 +192,22 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { function updatePositionNetVal(uint256 tokenId) internal - returns (uint256 valueLost) { if (tokenId == 0) { - return 0; + return; } uint256 currentVal = getPositionValue(tokenId); uint256 lastVal = tokenIdToPosition[tokenId].netValue; - if (currentVal < lastVal) { - valueLost = lastVal - currentVal; + int256 valueChange = int256(currentVal) - int256(lastVal); - // TODO: Should these be also updated when the value rises? - netLostValue += valueLost; - tokenIdToPosition[tokenId].netValue = currentVal; + netLostValue = int256(netLostValue) - valueChange < 0 ? + 0 : + uint256(int256(netLostValue) - valueChange); - emit PositionLostValue(tokenId, lastVal, currentVal, valueLost); - } + tokenIdToPosition[tokenId].netValue = currentVal; + emit PositionValueChanged(tokenId, lastVal, currentVal, valueChange); } function ensureNetLossThreshold(uint256 tokenId) internal { diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 05e6803963..500d24313c 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -64,11 +64,11 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { int24 maxTick, uint160 maxSwapPriceX96 ); - event PositionLostValue( + event PositionValueChanged( uint256 indexed tokenId, uint256 initialValue, uint256 currentValue, - uint256 netValueLost + int256 netValueChange ); // Represents a position minted by UniswapV3Strategy contract From 95d20b6c64be0c2c91608a795ad6a832c26191ff Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 18 Mar 2023 21:09:12 +0530 Subject: [PATCH 35/83] prettify --- .../strategies/uniswap/UniswapV3LiquidityManager.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index e18181ad13..1c0e66ef07 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -190,9 +190,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ); } - function updatePositionNetVal(uint256 tokenId) - internal - { + function updatePositionNetVal(uint256 tokenId) internal { if (tokenId == 0) { return; } @@ -202,9 +200,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { int256 valueChange = int256(currentVal) - int256(lastVal); - netLostValue = int256(netLostValue) - valueChange < 0 ? - 0 : - uint256(int256(netLostValue) - valueChange); + netLostValue = (int256(netLostValue) - valueChange < 0) + ? 0 + : uint256(int256(netLostValue) - valueChange); tokenIdToPosition[tokenId].netValue = currentVal; emit PositionValueChanged(tokenId, lastVal, currentVal, valueChange); From 0e39f7e2d6826e5334c9e7b558708f19e50aabe0 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 02:10:33 +0530 Subject: [PATCH 36/83] Add more unit tests --- .../v3/MockNonfungiblePositionManager.sol | 8 +- .../mocks/uniswap/v3/MockUniswapV3Pool.sol | 17 + .../uniswap/UniswapV3LiquidityManager.sol | 5 +- contracts/hardhat.config.js | 15 + contracts/test/_fixture.js | 18 +- .../test/strategies/uniswap-v3.fork-test.js | 2 +- contracts/test/strategies/uniswap-v3.js | 514 +++++++++++++++++- 7 files changed, 540 insertions(+), 39 deletions(-) diff --git a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol index 582463b4c3..0912347113 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol @@ -101,10 +101,10 @@ contract MockNonfungiblePositionManager { address(0), address(0), address(0), - 0, - 0, - 0, - 0, + p.fee, + p.tickLower, + p.tickUpper, + p.liquidity, 0, 0, p.token0Owed, diff --git a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol index b959b693b1..9d11527ad0 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol @@ -50,6 +50,23 @@ contract MockUniswapV3Pool { mockSqrtPriceX96 = sqrtPriceX96; mockTick = tick; } + + function ticks(int24 tick) + public + view + returns ( + uint128 liquidityGross, + int128 liquidityNet, + uint256 feeGrowthOutside0X128, + uint256 feeGrowthOutside1X128, + int56 tickCumulativeOutside, + uint160 secondsPerLiquidityOutsideX128, + uint32 secondsOutside, + bool initialized + ) + { + // + } } interface IMockUniswapV3Pool { diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 1c0e66ef07..357d474a45 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -561,8 +561,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { Position storage position = tokenIdToPosition[tokenId]; require(position.exists, "Unknown position"); - // Make sure liquidity management is disabled when value lost threshold is breached - ensureNetLossThreshold(tokenId); + // Update net value loss (to capture the state value before updating it). + // Also allows to close/decrease liquidity even if beyond the net loss threshold. + updatePositionNetVal(tokenId); INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index c9dd10c8ab..0b441bcc27 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -259,6 +259,21 @@ module.exports = { initialBaseFeePerGas: 0, gas: 7000000, gasPrice: 1000, + + ...(process.env.FORKED_LOCAL_TEST + ? { + timeout: 0, + forking: { + enabled: true, + url: `${ + process.env.LOCAL_PROVIDER_URL || process.env.PROVIDER_URL + }`, + blockNumber: + Number(process.env.FORK_BLOCK_NUMBER) || undefined, + timeout: 0, + }, + } + : {}), }), }, localhost: { diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index a6cacd8543..dbbcd4edbc 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -24,9 +24,17 @@ const threepoolLPAbi = require("./abi/threepoolLP.json"); const threepoolSwapAbi = require("./abi/threepoolSwap.json"); async function defaultFixture() { - await deployments.fixture(isFork ? undefined : ["unit_tests"], { - keepExistingDeployments: true, - }); + // TODO: reset this tag later + await deployments.fixture( + isFork + ? undefined + : process.env.FORKED_LOCAL_TEST + ? ["none"] + : ["unit_tests"], + { + keepExistingDeployments: true, + } + ); const { governorAddr, timelockAddr, operatorAddr } = await getNamedAccounts(); @@ -1285,8 +1293,8 @@ function uniswapV3FixtureSetup() { // 2 million .setMaxTVL(utils.parseUnits("2", 24)); UniV3_USDC_USDT_Strategy.connect(sGovernor).setRebalancePriceThreshold( - -100, - 100 + -10000, + 10000 ); } else { const [, activeTick] = await fixture.UniV3_USDC_USDT_Pool.slot0(); diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index a1e8955e0b..5d0bd3fc2d 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -658,7 +658,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { amount1Added, liquidityAdded, } = await increaseLiquidity(tokenId, "1", "1"); - await expect(increaseTx).to.have.emittedEvent("PositionLostValue"); + await expect(increaseTx).to.have.emittedEvent("PositionValueChanged"); storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.liquidity).approxEqual( diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index b2c677c3a9..bdebe17bf0 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -1,28 +1,74 @@ const { expect } = require("chai"); -const { uniswapV3FixturSetup } = require("../_fixture"); -const { units, ousdUnits, expectApproxSupply } = require("../helpers"); +const { uniswapV3FixtureSetup } = require("../_fixture"); +const { + units, + ousdUnits, + expectApproxSupply, + usdcUnits, + usdtUnits, +} = require("../helpers"); +const { deployments } = require("hardhat"); -const uniswapV3Fixture = uniswapV3FixturSetup(); +const uniswapV3Fixture = uniswapV3FixtureSetup(); + +const liquidityManagerFixture = deployments.createFixture(async () => { + const fixture = await uniswapV3Fixture(); + const { franck, daniel, domen, vault, usdc, usdt, dai } = fixture; + + // Mint some liquidity + for (const user of [franck, daniel, domen]) { + for (const asset of [usdc, usdt, dai]) { + const amount = "1000000"; + await asset.connect(user).mint(await units(amount, asset)); + await asset + .connect(user) + .approve(vault.address, await units(amount, asset)); + await vault + .connect(user) + .mint(asset.address, await units(amount, asset), 0); + } + } + + // Configure mockPool + const { UniV3_USDC_USDT_Pool: mockPool, governor } = fixture; + await mockPool.connect(governor).setTick(-2); + + await fixture.UniV3_USDC_USDT_Strategy.connect( + governor + ).setMaxPositionValueLossThreshold(ousdUnits("50000", 18)); +}); describe("Uniswap V3 Strategy", function () { let fixture; let vault, ousd, usdc, usdt, dai; let reserveStrategy, strategy, - // mockPool, - // mockPositionManager, + helper, + mockPool, + mockPositionManager, mockStrategy2, mockStrategyDAI; let governor, strategist, operator, josh, matt, daniel, domen, franck; + const mint = async (user, amount, asset) => { + await asset.connect(user).mint(await units(amount, asset)); + await asset + .connect(user) + .approve(vault.address, await units(amount, asset)); + await vault + .connect(user) + .mint(asset.address, await units(amount, asset), 0); + }; + beforeEach(async () => { fixture = await uniswapV3Fixture(); reserveStrategy = fixture.mockStrategy; mockStrategy2 = fixture.mockStrategy2; mockStrategyDAI = fixture.mockStrategyDAI; strategy = fixture.UniV3_USDC_USDT_Strategy; - // mockPool = fixture.UniV3_USDC_USDT_Pool; - // mockPositionManager = fixture.UniV3PositionManager; + helper = fixture.UniV3Helper; + mockPool = fixture.UniV3_USDC_USDT_Pool; + mockPositionManager = fixture.UniV3PositionManager; ousd = fixture.ousd; usdc = fixture.usdc; usdt = fixture.usdt; @@ -39,12 +85,6 @@ describe("Uniswap V3 Strategy", function () { franck = fixture.franck; }); - const mint = async (user, amount, asset) => { - await asset.connect(user).mint(units(amount, asset)); - await asset.connect(user).approve(vault.address, units(amount, asset)); - await vault.connect(user).mint(asset.address, units(amount, asset), 0); - }; - for (const assetSymbol of ["USDC", "USDT"]) { describe(`Mint w/ ${assetSymbol}`, function () { let asset; @@ -259,19 +299,439 @@ describe("Uniswap V3 Strategy", function () { }); describe("LiquidityManager", function () { - it.skip("Should mint new position"); - it.skip("Should increase liquidity for active position"); - it.skip("Should close active position"); - it.skip("Should rebalance"); - it.skip("Should swap USDC for USDT and then rebalance"); - it.skip("Should swap USDT for USDC and then rebalance"); - }); + beforeEach(async () => { + fixture = await liquidityManagerFixture(); + }); + + const mintLiquidity = async ({ + amount0, + amount1, + minAmount0, + minAmount1, + minRedeemAmount0, + minRedeemAmount1, + lowerTick, + upperTick, + existingPosition, + }) => { + const tx = await strategy + .connect(operator) + .rebalance( + usdcUnits(amount0), + usdtUnits(amount1), + usdcUnits(minAmount0 || "0"), + usdtUnits(minAmount1 || "0"), + usdcUnits(minRedeemAmount0 || "0"), + usdtUnits(minRedeemAmount1 || "0"), + lowerTick, + upperTick + ); + + if (!existingPosition) { + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + } + + await expect(tx).to.have.emittedEvent("UniswapV3LiquidityAdded"); + + return await tx.wait(); + }; + + describe("Rebalance/Mint", () => { + it("Should mint a new position (with reserve funds)", async () => { + const { events } = await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-1000", + upperTick: "1000", + }); + + const [tokenId, amount0Deposited, amount1Deposited, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + + // Check storage + const position = await strategy.tokenIdToPosition(tokenId); + expect(position.exists).to.be.true; + expect(position.liquidity).to.equal(liquidityMinted); + expect(position.netValue).to.equal( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + }); + + it("Should mint a new position below active tick", async () => { + const { events } = await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-1000", + upperTick: "-500", + }); + + const [tokenId, amount0Deposited, amount1Deposited, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + + // Check storage + const position = await strategy.tokenIdToPosition(tokenId); + expect(position.exists).to.be.true; + expect(position.liquidity).to.equal(liquidityMinted); + expect(position.netValue).to.equal( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + }); + + it("Should mint a new position above active tick", async () => { + const { events } = await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "1000", + upperTick: "1500", + }); + + const [tokenId, amount0Deposited, amount1Deposited, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + + // Check storage + const position = await strategy.tokenIdToPosition(tokenId); + expect(position.exists).to.be.true; + expect(position.liquidity).to.equal(liquidityMinted); + expect(position.netValue).to.equal( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + }); + + it("Should mint a new position (without reserve funds)", async () => { + // Transfer some tokens to strategy + const amount = await units("1000000", usdc); + await usdc.connect(josh).mint(amount); + await usdt.connect(josh).mint(amount); + await usdc.connect(josh).transfer(strategy.address, amount); + await usdt.connect(josh).transfer(strategy.address, amount); + + // Mint + const { events } = await mintLiquidity({ + amount0: "1000000", + amount1: "1000000", + lowerTick: "-100", + upperTick: "100", + }); + + const [tokenId, amount0Deposited, amount1Deposited, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + + // Check storage + const position = await strategy.tokenIdToPosition(tokenId); + expect(position.exists).to.be.true; + expect(position.liquidity).to.equal(liquidityMinted); + expect(position.netValue).to.equal( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + + // Verify balance + const amount0Bal = await usdc.balanceOf(strategy.address); + const amount1Bal = await usdt.balanceOf(strategy.address); + expect(amount0Bal).to.approxEqual(amount.sub(amount0Deposited)); + expect(amount1Bal).to.approxEqual(amount.sub(amount1Deposited)); + }); + + it("Should allow Governor/Strategist/Operator to rebalance", async () => { + for (const user of [governor, strategist, operator]) { + await strategy + .connect(user) + .rebalance("1", "1", "0", "0", "0", "0", "-1", "1"); + } + }); + it("Should revert if caller isn't Governor/Strategist/Operator", async () => { + expect( + strategy + .connect(matt) + .rebalance("1", "1", "0", "0", "0", "0", "-1", "1") + ).to.be.revertedWith( + "Caller is not the Operator, Strategist or Governor" + ); + }); + + it("Should revert if rebalance is paused", async () => { + await strategy.connect(governor).setRebalancePaused(true); + expect( + strategy + .connect(operator) + .rebalance("1", "1", "0", "0", "0", "0", "-1", "1") + ).to.be.revertedWith("Rebalances are paused"); + }); + + it("Should revert if out of rebalance limits", async () => { + await strategy.connect(governor).setRebalancePriceThreshold(-100, 200); + expect( + strategy + .connect(operator) + .rebalance("1", "1", "0", "0", "0", "0", "-200", "1") + ).to.be.revertedWith("Rebalance position out of bounds"); + }); + + it("Should revert if TVL check fails", async () => { + await strategy.connect(governor).setMaxTVL(await ousdUnits("100000")); + + expect( + strategy + .connect(operator) + .rebalance( + await units("100000", 18), + await units("100000", 18), + "0", + "0", + "0", + "0", + "-200", + "1" + ) + ).to.be.revertedWith("MaxTVL threshold has been reached"); + }); + + it("Should revert if reserve funds aren't available", async () => { + const reserve0 = await reserveStrategy.checkBalance(usdt.address); + const reserve1 = await reserveStrategy.checkBalance(usdc.address); + + expect( + strategy + .connect(operator) + .rebalance( + reserve0.mul(200), + reserve1.mul(321), + "0", + "0", + "0", + "0", + "-200", + "1" + ) + ).to.be.reverted; + }); + + it("Should revert if tick range is invalid", async () => { + expect( + strategy + .connect(operator) + .rebalance( + await units("100000", 18), + await units("100000", 18), + "0", + "0", + "0", + "0", + "200", + "100" + ) + ).to.be.revertedWith("Invalid tick range"); + }); + }); + + describe("IncreaseLiquidity", () => { + it("Should increase liquidity w/ mint (if position already exists)", async () => { + const { events } = await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-500", + upperTick: "500", + }); + + const [tokenId, amount0Deposited, amount1Deposited, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; - // describe("Rewards", function () { - // it("Should show correct amount of fees", async () => {}); - // }); - // describe("Rebalance", function () { - // it("Should provide liquidity on given tick", async () => {}); - // it("Should close existing position", async () => {}); - // }); + // Check storage + expect(await strategy.activeTokenId()).to.equal(tokenId); + const position = await strategy.tokenIdToPosition(tokenId); + expect(position.exists).to.be.true; + expect(position.liquidity).to.equal(liquidityMinted); + expect(position.netValue).to.equal( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + + // Call mintLiquidity again + await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-500", + upperTick: "500", + existingPosition: true, + }); + + // Check storage and ensure it has increased liquidity + expect(await strategy.activeTokenId()).to.equal(tokenId); + const newPosition = await strategy.tokenIdToPosition(tokenId); + expect(newPosition.liquidity).to.be.gt(liquidityMinted); + expect(newPosition.netValue).to.be.gt( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + }); + + it("Should increase liquidity of active position", async () => { + const { events } = await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-500", + upperTick: "500", + }); + + const [tokenId, amount0Deposited, amount1Deposited, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + + // Check storage + expect(await strategy.activeTokenId()).to.equal(tokenId); + const position = await strategy.tokenIdToPosition(tokenId); + expect(position.exists).to.be.true; + expect(position.liquidity).to.equal(liquidityMinted); + expect(position.netValue).to.equal( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + + // Call increaseActivePositionLiquidity + await strategy + .connect(governor) + .increaseActivePositionLiquidity( + usdcUnits("100000"), + usdtUnits("100000"), + "0", + "0" + ); + + // Check storage and ensure it has increased liquidity + const newPosition = await strategy.tokenIdToPosition(tokenId); + expect(newPosition.liquidity).to.be.gt(liquidityMinted); + expect(newPosition.netValue).to.be.gt( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + }); + + it("Should revert if caller isn't Governor/Strategist/Operator", async () => { + await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-500", + upperTick: "500", + }); + + for (const user of [governor, strategist, operator]) { + await strategy + .connect(user) + .increaseActivePositionLiquidity("1", "1", "0", "0"); + } + + expect( + strategy + .connect(matt) + .increaseActivePositionLiquidity("1", "1", "0", "0") + ).to.be.revertedWith( + "Caller is not the Operator, Strategist or Governor" + ); + }); + + it("Should revert if no active position", async () => { + expect( + strategy + .connect(operator) + .increaseActivePositionLiquidity("1", "1", "0", "0") + ).to.be.revertedWith("No active position"); + }); + + it("Should revert if rebalance is paused", async () => { + await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-500", + upperTick: "500", + }); + + await strategy.connect(governor).setRebalancePaused(true); + expect( + strategy + .connect(operator) + .increaseActivePositionLiquidity("1", "1", "0", "0") + ).to.be.revertedWith("Rebalances are paused"); + }); + + it("Should revert if reserve funds aren't available", async () => { + await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-500", + upperTick: "500", + }); + + const reserve0 = await reserveStrategy.checkBalance(usdt.address); + const reserve1 = await reserveStrategy.checkBalance(usdc.address); + + expect( + strategy + .connect(operator) + .increaseActivePositionLiquidity( + reserve0.mul(200), + reserve1.mul(321), + "0", + "0" + ) + ).to.be.reverted; + }); + + it("Should revert if TVL check fails", async () => { + await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-500", + upperTick: "500", + }); + + await strategy.connect(governor).setMaxTVL(ousdUnits("100000")); + + expect( + strategy + .connect(operator) + .increaseActivePositionLiquidity("1", "1", "0", "0") + ).to.be.revertedWith("MaxTVL threshold has been reached"); + }); + }); + + describe("DecreaseLiquidity/ClosePosition", () => { + it("Should close active position during a mint", async () => {}); + + it("Should close active position", async () => {}); + + it("Should liquidate active position during withdraw", async () => {}); + + it("Should liquidate active position during withdrawAll", async () => {}); + + it("Should revert if caller isn't Governor/Strategist/Operator", async () => {}); + }); + + describe("Swap And Rebalance", () => { + it("Should swap token0 for token1 during rebalance", async () => {}); + + it("Should swap token1 for token0 during rebalance", async () => {}); + + it("Should revert if caller isn't Governor/Strategist/Operator", async () => {}); + + it("Should revert if TVL check fails", async () => {}); + + it("Should revert if swap is paused"); + + it("Should revert if rebalance is paused"); + + it("Should revert if swapping is unnecessary", async () => {}); + + it("Should revert if beyond swap limits", async () => {}); + }); + + describe("Fees", () => { + it("Should accrue and collect fees", async () => {}); + }); + + describe("Net Value Lost Threshold", () => { + it("Should update threshold when value changes", async () => {}); + + it("Should update threshold when collecting fees", async () => {}); + + it("Should allow close/withdraw beyond threshold", async () => {}); + + it("Should revert if beyond threshold (during mint)", async () => {}); + + it("Should revert if beyond threshold (when increasing liquidity)", async () => {}); + }); + }); }); From 6e3128b608d01ce95b9c6fbfb67ba7a54fece103 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 10:51:08 +0530 Subject: [PATCH 37/83] Fix increase liquidity tests --- .../uniswap/UniswapV3LiquidityManager.sol | 20 +++++++++++-------- contracts/test/strategies/uniswap-v3.js | 2 ++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 357d474a45..af43533f94 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -218,7 +218,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /** * @notice Closes active LP position if any and then provides liquidity to the requested position. - * Mints new position, if it doesn't exist already. + * Mints new position, if it doesn't exist already. If active position is on the same tick + * range, then just increases the liquidity by the desiredAmounts * @dev Will pull funds needed from reserve strategies and then will deposit back all dust to them * @param desiredAmount0 Amount of token0 to use to provide liquidity * @param desiredAmount1 Amount of token1 to use to provide liquidity @@ -245,8 +246,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { int48 tickKey = _getTickPositionKey(lowerTick, upperTick); uint256 tokenId = ticksToTokenId[tickKey]; - if (activeTokenId > 0) { - // Close any active position + if (activeTokenId > 0 && activeTokenId != tokenId) { + // Close any active position (if it's not the same) _closePosition(activeTokenId, minRedeemAmount0, minRedeemAmount1); } @@ -316,8 +317,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { _getTickPositionKey(params.lowerTick, params.upperTick) ]; - if (activeTokenId > 0) { - // Close any active position + if (activeTokenId > 0 && activeTokenId != tokenId) { + // Close any active position (if it's not the same) _closePosition( activeTokenId, params.minRedeemAmount0, @@ -488,9 +489,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // Make sure liquidity management is disabled when value lost threshold is breached ensureNetLossThreshold(tokenId); - // Withdraw enough funds from Reserve strategies - _ensureAssetBalances(desiredAmount0, desiredAmount1); - INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager .IncreaseLiquidityParams({ @@ -526,6 +524,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { { rebalanceNotPaused(); + // Withdraw enough funds from Reserve strategies + _ensureAssetBalances(desiredAmount0, desiredAmount1); + _increasePositionLiquidity( activeTokenId, desiredAmount0, @@ -534,6 +535,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { minAmount1 ); + // Deposit + _depositAll(); + // Final position value/sanity check ensureTVL(); } diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index bdebe17bf0..67b9d38e8b 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -36,6 +36,8 @@ const liquidityManagerFixture = deployments.createFixture(async () => { await fixture.UniV3_USDC_USDT_Strategy.connect( governor ).setMaxPositionValueLossThreshold(ousdUnits("50000", 18)); + + return fixture; }); describe("Uniswap V3 Strategy", function () { From 8de11f30ae7c8646a9739e59e349e1748a17439a Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 11:57:12 +0530 Subject: [PATCH 38/83] Cleanup fixtures and add unit tests for Fees --- .../mocks/uniswap/v3/MockUniswapV3Pool.sol | 8 + contracts/test/helpers.js | 4 +- contracts/test/strategies/uniswap-v3.js | 207 +++++++++++++----- 3 files changed, 164 insertions(+), 55 deletions(-) diff --git a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol index 9d11527ad0..295990df33 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol @@ -67,6 +67,14 @@ contract MockUniswapV3Pool { { // } + + function feeGrowthGlobal0X128() public view returns (uint256) { + return 0; + } + + function feeGrowthGlobal1X128() public view returns (uint256) { + return 0; + } } interface IMockUniswapV3Pool { diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index b4734a6a88..30378cb88c 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -117,7 +117,9 @@ chai.Assertion.addMethod("emittedEvent", async function (eventName, args) { chai.expect(log).to.not.be.undefined; if (Array.isArray(args)) { - chai.expect(log.args).to.equal(args.length, "Invalid event arg count"); + chai + .expect(log.args.length) + .to.equal(args.length, "Invalid event arg count"); for (let i = 0; i < args.length; i++) { chai.expect(log.args[i]).to.equal(args[i]); } diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 67b9d38e8b..34a850ae76 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -40,6 +40,25 @@ const liquidityManagerFixture = deployments.createFixture(async () => { return fixture; }); +const activePositionFixture = deployments.createFixture(async () => { + const fixture = await liquidityManagerFixture(); + + // Mint a position + const { operator, UniV3_USDC_USDT_Strategy } = fixture; + await UniV3_USDC_USDT_Strategy.connect(operator).rebalance( + usdcUnits("100000"), + usdcUnits("100000"), + "0", + "0", + "0", + "0", + "-100", + "100" + ); + + return fixture; +}); + describe("Uniswap V3 Strategy", function () { let fixture; let vault, ousd, usdc, usdt, dai; @@ -62,35 +81,36 @@ describe("Uniswap V3 Strategy", function () { .mint(asset.address, await units(amount, asset), 0); }; - beforeEach(async () => { - fixture = await uniswapV3Fixture(); - reserveStrategy = fixture.mockStrategy; - mockStrategy2 = fixture.mockStrategy2; - mockStrategyDAI = fixture.mockStrategyDAI; - strategy = fixture.UniV3_USDC_USDT_Strategy; - helper = fixture.UniV3Helper; - mockPool = fixture.UniV3_USDC_USDT_Pool; - mockPositionManager = fixture.UniV3PositionManager; - ousd = fixture.ousd; - usdc = fixture.usdc; - usdt = fixture.usdt; - dai = fixture.dai; - vault = fixture.vault; - // harvester = fixture.harvester; - governor = fixture.governor; - strategist = fixture.strategist; - operator = fixture.operator; - josh = fixture.josh; - matt = fixture.matt; - daniel = fixture.daniel; - domen = fixture.domen; - franck = fixture.franck; - }); + function _destructureFixture(_fixture) { + fixture = _fixture; + reserveStrategy = _fixture.mockStrategy; + mockStrategy2 = _fixture.mockStrategy2; + mockStrategyDAI = _fixture.mockStrategyDAI; + strategy = _fixture.UniV3_USDC_USDT_Strategy; + helper = _fixture.UniV3Helper; + mockPool = _fixture.UniV3_USDC_USDT_Pool; + mockPositionManager = _fixture.UniV3PositionManager; + ousd = _fixture.ousd; + usdc = _fixture.usdc; + usdt = _fixture.usdt; + dai = _fixture.dai; + vault = _fixture.vault; + // harvester = _fixture.harvester; + governor = _fixture.governor; + strategist = _fixture.strategist; + operator = _fixture.operator; + josh = _fixture.josh; + matt = _fixture.matt; + daniel = _fixture.daniel; + domen = _fixture.domen; + franck = _fixture.franck; + } for (const assetSymbol of ["USDC", "USDT"]) { describe(`Mint w/ ${assetSymbol}`, function () { let asset; - beforeEach(() => { + beforeEach(async () => { + _destructureFixture(await uniswapV3Fixture()); asset = assetSymbol == "USDT" ? usdt : usdc; }); @@ -119,6 +139,10 @@ describe("Uniswap V3 Strategy", function () { } describe("Redeem", function () { + beforeEach(async () => { + _destructureFixture(await uniswapV3Fixture()); + }); + it("Should withdraw from vault balance", async () => { // Vault has 200 DAI from fixtures await expectApproxSupply(ousd, ousdUnits("200")); @@ -148,13 +172,11 @@ describe("Uniswap V3 Strategy", function () { it.skip("Should withdraw from active position"); }); - describe("Balance & Fees", () => { - describe("getPendingFees()", () => {}); - describe("checkBalance()", () => {}); - describe("checkBalanceOfAllAssets()", () => {}); - }); - describe("Admin functions", () => { + beforeEach(async () => { + _destructureFixture(await uniswapV3Fixture()); + }); + describe("setOperator()", () => { it("Governor can set the operator", async () => { const addr1 = "0x0000000000000000000000000000000000011111"; @@ -178,6 +200,10 @@ describe("Uniswap V3 Strategy", function () { }); describe("setReserveStrategy()", () => { + beforeEach(async () => { + _destructureFixture(await uniswapV3Fixture()); + }); + describe("Validations", () => { it("Can set a valid strategy as reserve", async () => { await strategy @@ -239,6 +265,10 @@ describe("Uniswap V3 Strategy", function () { }); describe("setMinDepositThreshold()", () => { + beforeEach(async () => { + _destructureFixture(await uniswapV3Fixture()); + }); + describe("Permissions", () => { it("Governer & Strategist can set the threshold", async () => { await strategy @@ -268,6 +298,10 @@ describe("Uniswap V3 Strategy", function () { }); describe("setRebalancePaused()", () => { + beforeEach(async () => { + _destructureFixture(await uniswapV3Fixture()); + }); + it("Governer & Strategist can pause rebalance", async () => { await strategy.connect(governor).setRebalancePaused(true); expect(await strategy.rebalancePaused()).to.be.true; @@ -282,6 +316,10 @@ describe("Uniswap V3 Strategy", function () { }); describe("setSwapsPaused()", () => { + beforeEach(async () => { + _destructureFixture(await uniswapV3Fixture()); + }); + it("Governer & Strategist can pause swaps", async () => { await strategy.connect(governor).setSwapsPaused(true); expect(await strategy.swapsPaused()).to.be.true; @@ -301,10 +339,6 @@ describe("Uniswap V3 Strategy", function () { }); describe("LiquidityManager", function () { - beforeEach(async () => { - fixture = await liquidityManagerFixture(); - }); - const mintLiquidity = async ({ amount0, amount1, @@ -339,6 +373,10 @@ describe("Uniswap V3 Strategy", function () { }; describe("Rebalance/Mint", () => { + beforeEach(async () => { + _destructureFixture(await liquidityManagerFixture()); + }); + it("Should mint a new position (with reserve funds)", async () => { const { events } = await mintLiquidity({ amount0: "100000", @@ -407,10 +445,13 @@ describe("Uniswap V3 Strategy", function () { await usdc.connect(josh).transfer(strategy.address, amount); await usdt.connect(josh).transfer(strategy.address, amount); + const reserve0Bal = await reserveStrategy.checkBalance(usdc.address); + const reserve1Bal = await reserveStrategy.checkBalance(usdt.address); + // Mint const { events } = await mintLiquidity({ - amount0: "1000000", - amount1: "1000000", + amount0: "100000", + amount1: "100000", lowerTick: "-100", upperTick: "100", }); @@ -426,11 +467,15 @@ describe("Uniswap V3 Strategy", function () { amount0Deposited.add(amount1Deposited).mul(1e12) ); - // Verify balance - const amount0Bal = await usdc.balanceOf(strategy.address); - const amount1Bal = await usdt.balanceOf(strategy.address); - expect(amount0Bal).to.approxEqual(amount.sub(amount0Deposited)); - expect(amount1Bal).to.approxEqual(amount.sub(amount1Deposited)); + // Should have deposited everything to reserve after mint + expect(await usdc.balanceOf(strategy.address)).to.equal(0); + expect(await reserveStrategy.checkBalance(usdc.address)).to.approxEqual( + reserve0Bal.add(amount).sub(amount0Deposited) + ); + expect(await usdt.balanceOf(strategy.address)).to.equal(0); + expect(await reserveStrategy.checkBalance(usdt.address)).to.approxEqual( + reserve1Bal.add(amount).sub(amount1Deposited) + ); }); it("Should allow Governor/Strategist/Operator to rebalance", async () => { @@ -469,14 +514,14 @@ describe("Uniswap V3 Strategy", function () { }); it("Should revert if TVL check fails", async () => { - await strategy.connect(governor).setMaxTVL(await ousdUnits("100000")); + await strategy.connect(governor).setMaxTVL(ousdUnits("100000")); expect( strategy .connect(operator) .rebalance( - await units("100000", 18), - await units("100000", 18), + usdcUnits("100000"), + usdtUnits("100000"), "0", "0", "0", @@ -512,8 +557,8 @@ describe("Uniswap V3 Strategy", function () { strategy .connect(operator) .rebalance( - await units("100000", 18), - await units("100000", 18), + usdcUnits("100000"), + usdtUnits("100000"), "0", "0", "0", @@ -526,6 +571,10 @@ describe("Uniswap V3 Strategy", function () { }); describe("IncreaseLiquidity", () => { + beforeEach(async () => { + _destructureFixture(await activePositionFixture()); + }); + it("Should increase liquidity w/ mint (if position already exists)", async () => { const { events } = await mintLiquidity({ amount0: "100000", @@ -611,9 +660,11 @@ describe("Uniswap V3 Strategy", function () { }); for (const user of [governor, strategist, operator]) { - await strategy - .connect(user) - .increaseActivePositionLiquidity("1", "1", "0", "0"); + await expect( + strategy + .connect(user) + .increaseActivePositionLiquidity("1", "1", "0", "0") + ).to.not.be.reverted; } expect( @@ -690,7 +741,11 @@ describe("Uniswap V3 Strategy", function () { }); }); - describe("DecreaseLiquidity/ClosePosition", () => { + describe.skip("DecreaseLiquidity/ClosePosition", () => { + beforeEach(async () => { + _destructureFixture(await activePositionFixture()); + }); + it("Should close active position during a mint", async () => {}); it("Should close active position", async () => {}); @@ -702,7 +757,10 @@ describe("Uniswap V3 Strategy", function () { it("Should revert if caller isn't Governor/Strategist/Operator", async () => {}); }); - describe("Swap And Rebalance", () => { + describe.skip("Swap And Rebalance", () => { + beforeEach(async () => { + _destructureFixture(await liquidityManagerFixture()); + }); it("Should swap token0 for token1 during rebalance", async () => {}); it("Should swap token1 for token0 during rebalance", async () => {}); @@ -721,10 +779,51 @@ describe("Uniswap V3 Strategy", function () { }); describe("Fees", () => { - it("Should accrue and collect fees", async () => {}); + beforeEach(async () => { + _destructureFixture(await activePositionFixture()); + }); + + it("Should accrue and collect fees", async () => { + // No fee accrued yet + let fees = await strategy.getPendingFees(); + expect(fees[0]).to.equal(0, "No fee after mint"); + expect(fees[1]).to.equal(0, "No fee after mint"); + + const expectedFee0 = usdcUnits("1234"); + const expectedFee1 = usdtUnits("5678"); + const tokenId = await strategy.activeTokenId(); + await mockPositionManager.setTokensOwed( + tokenId.toString(), + expectedFee0.toString(), + expectedFee1.toString() + ); + + fees = await strategy.getPendingFees(); + expect(fees[0]).to.equal(expectedFee0, "Fee0 mismatch"); + expect(fees[1]).to.equal(expectedFee1, "Fee1 mismatch"); + + const tx = await strategy.connect(operator).collectFees(); + expect(tx).to.have.emittedEvent("UniswapV3FeeCollected", [ + tokenId, + expectedFee0, + expectedFee1, + ]); + }); + + it("Should allow Governor/Strategist/Operator to collect fees", async () => { + for (const user of [governor, strategist, operator]) { + expect(strategy.connect(user).collectFees()).to.not.be.reverted; + } + }); + + it("Should revert if caller isn't Governor/Strategist/Operator", async () => { + await expect(strategy.connect(matt).collectFees()).to.be.revertedWith( + "Caller is not the Operator, Strategist or Governor" + ); + }); }); - describe("Net Value Lost Threshold", () => { + describe.skip("Net Value Lost Threshold", () => { it("Should update threshold when value changes", async () => {}); it("Should update threshold when collecting fees", async () => {}); From b5f98fe69e6d0ab3906e914fabfab30d730a4343 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 13:28:54 +0530 Subject: [PATCH 39/83] Add some tests for net lost value --- .../uniswap/UniswapV3LiquidityManager.sol | 69 ++++++-- .../uniswap/UniswapV3StrategyStorage.sol | 3 +- contracts/test/strategies/uniswap-v3.js | 157 ++++++++++++++++-- 3 files changed, 203 insertions(+), 26 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index af43533f94..f217019bbc 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -190,6 +190,39 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ); } + /** + * @notice Update the netLostValue state. + * + * If the value of position increase multiple times without a drop in value, + * the counter would still be at zero. We don't keep track of any value gained + * until the counter is > 0, as the only purpose of this state variable is + * to shut off rebalancing if the LP positions are losing capital across rebalances. + */ + function _setNetLostValue(uint256 delta, bool gained) internal { + if (delta == 0) { + // No change + return; + } + + if (gained) { + if (netLostValue == 0) { + // No change + return; + } else if (delta >= netLostValue) { + // Reset lost value + netLostValue = 0; + } else { + // Deduct gained amount from netLostValue + netLostValue -= delta; + } + } else { + // Add lost value + netLostValue += delta; + } + + emit NetLostValueChanged(netLostValue); + } + function updatePositionNetVal(uint256 tokenId) internal { if (tokenId == 0) { return; @@ -198,14 +231,28 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 currentVal = getPositionValue(tokenId); uint256 lastVal = tokenIdToPosition[tokenId].netValue; - int256 valueChange = int256(currentVal) - int256(lastVal); + if (currentVal == lastVal) { + // No change in value + return; + } + + if (currentVal > lastVal) { + _setNetLostValue(currentVal - lastVal, true); + } else { + _setNetLostValue(lastVal - currentVal, false); + } - netLostValue = (int256(netLostValue) - valueChange < 0) - ? 0 - : uint256(int256(netLostValue) - valueChange); + // NOTE: Intentionally skipped passing the `int256 delta` to `_setNetLostValue`, + // Wanna be safe about uint() to int() conversions. + emit PositionValueChanged( + tokenId, + lastVal, + currentVal, + int256(currentVal) - int256(lastVal) + ); + // Update state tokenIdToPosition[tokenId].netValue = currentVal; - emit PositionValueChanged(tokenId, lastVal, currentVal, valueChange); } function ensureNetLossThreshold(uint256 tokenId) internal { @@ -833,7 +880,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { } /** - * @notice Collects the fees generated by the position on V3 pool + * @notice Collects the fees generated by the position on V3 pool. + * Also adjusts netLostValue based on fee collected. * @param tokenId Token ID of the position to collect fees of. * @return amount0 Amount of token0 collected as fee * @return amount1 Amount of token1 collected as fee @@ -853,13 +901,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { (amount0, amount1) = positionManager.collect(params); - if (netLostValue > 0) { - // Reset loss counter to include value of fee collected - uint256 feeValue = _getValueOfTokens(amount0, amount1); - netLostValue = (feeValue >= netLostValue) - ? 0 - : (netLostValue - feeValue); - } + // Reset loss counter to include value of fee collected + _setNetLostValue(_getValueOfTokens(amount0, amount1), true); emit UniswapV3FeeCollected(tokenId, amount0, amount1); } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 500d24313c..26fec5e4a3 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -68,8 +68,9 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint256 indexed tokenId, uint256 initialValue, uint256 currentValue, - int256 netValueChange + int256 delta ); + event NetLostValueChanged(uint256 currentNetLostValue); // Represents a position minted by UniswapV3Strategy contract struct Position { diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 34a850ae76..34e7cc509e 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -8,6 +8,7 @@ const { usdtUnits, } = require("../helpers"); const { deployments } = require("hardhat"); +const { utils, BigNumber } = require("ethers") const uniswapV3Fixture = uniswapV3FixtureSetup(); @@ -741,20 +742,37 @@ describe("Uniswap V3 Strategy", function () { }); }); - describe.skip("DecreaseLiquidity/ClosePosition", () => { + describe("DecreaseLiquidity/ClosePosition", () => { beforeEach(async () => { _destructureFixture(await activePositionFixture()); }); - it("Should close active position during a mint", async () => {}); + it("Should close active position during a mint", async () => { + const tokenId = await strategy.activeTokenId(); - it("Should close active position", async () => {}); + // Mint position in a different tick range + await mintLiquidity({ + amount0: "100000", + amount1: "100000", + lowerTick: "-50", + upperTick: "5000", + }); - it("Should liquidate active position during withdraw", async () => {}); + expect(await strategy.activeTokenId()).to.not.equal(tokenId); + const lastPos = await strategy.tokenIdToPosition(tokenId); + expect(lastPos.liquidity).to.equal( + 0, + "Should've removed all liquidity from closed position" + ); + }); - it("Should liquidate active position during withdrawAll", async () => {}); + it.skip("Should close active position", async () => {}); - it("Should revert if caller isn't Governor/Strategist/Operator", async () => {}); + it.skip("Should liquidate active position during withdraw", async () => {}); + + it.skip("Should liquidate active position during withdrawAll", async () => {}); + + it.skip("Should revert if caller isn't Governor/Strategist/Operator", async () => {}); }); describe.skip("Swap And Rebalance", () => { @@ -823,16 +841,131 @@ describe("Uniswap V3 Strategy", function () { }); }); - describe.skip("Net Value Lost Threshold", () => { - it("Should update threshold when value changes", async () => {}); + describe.only("Net Value Lost Threshold", () => { + beforeEach(async () => { + _destructureFixture(await activePositionFixture()); + }); + + const _setNetLossVal = async (val) => { + const netLostValueStorageSlot = BigNumber.from(169).toHexString(); + const expectedVal = BigNumber.from(val) + + const byte32Val = expectedVal.toHexString(val).replace( + "0x", + "0x" + (new Array(64 - (expectedVal.toHexString().length - 2)).fill("0").join("")) + ) + await hre.network.provider.send("hardhat_setStorageAt", [ + strategy.address, + netLostValueStorageSlot, + // Set an higher lost value manually + byte32Val + ]) + expect(await strategy.netLostValue()).to.equal( + expectedVal, + "Storage slot changed?" + ) + } + + it.skip("Should update lost value of position during mint/increase", async () => { + + // for (let i = 150; i <= 200; i++) { + // const val = await hre.network.provider.send("eth_getStorageAt", [ + // strategy.address, + // BigNumber.from(i).toHexString(), + // "latest" + // ]) + + // console.log(i, val) + // } + }); + + it("Should update lost value when collecting fees", async () => { + await _setNetLossVal(ousdUnits("1000")) // $1000 + const tokenId = await strategy.activeTokenId(); + + await mockPositionManager.setTokensOwed( + tokenId, + usdcUnits("330"), // $330 + usdtUnits("120"), // $120 + ) + await strategy.collectFees() + + expect(await strategy.netLostValue()).to.equal( + BigNumber.from(ousdUnits("550")) + ) + }); + + it("Should reset lost value when collecting huge fees", async () => { + await _setNetLossVal(ousdUnits("1000")) // $1000 + const tokenId = await strategy.activeTokenId(); + + await mockPositionManager.setTokensOwed( + tokenId, + usdcUnits("4000"), // $4000 + usdtUnits("5000"), // $5000 + ) + await strategy.collectFees() + + expect(await strategy.netLostValue()).to.equal( + BigNumber.from("0") + ) + }); + + it("Should allow close/withdraw beyond threshold", async () => { + await _setNetLossVal("999999999999999999999999999999999999999999999") + + const tokenId = await strategy.activeTokenId(); + + await expect( + strategy + .connect(operator) + .closePosition( + tokenId, + "0", + "0" + ) + ).to.not.be.reverted + + expect(await strategy.activeTokenId()).to.not.equal(tokenId) + }); - it("Should update threshold when collecting fees", async () => {}); + it("Should revert if beyond threshold (during mint)", async () => { + await _setNetLossVal("999999999999999999999999999999999999999999999") - it("Should allow close/withdraw beyond threshold", async () => {}); + await expect( + strategy + .connect(operator) + .rebalance( + "1", + "1", + "0", + "0", + "0", + "0", + "-120", + "1234" + ) + ).to.be.revertedWith( + "Over max value loss threshold" + ) + }); - it("Should revert if beyond threshold (during mint)", async () => {}); + it("Should revert if beyond threshold (when increasing liquidity)", async () => { + await _setNetLossVal("999999999999999999999999999999999999999999999") - it("Should revert if beyond threshold (when increasing liquidity)", async () => {}); + await expect( + strategy + .connect(operator) + .increaseActivePositionLiquidity( + "1", + "1", + "0", + "0" + ) + ).to.be.revertedWith( + "Over max value loss threshold" + ) + }); }); }); }); From 2928ff541d1acbeba320aa85571b7772a8b90cfe Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 15:53:06 +0530 Subject: [PATCH 40/83] Add more tests --- .../v3/MockNonfungiblePositionManager.sol | 2 + .../uniswap/UniswapV3LiquidityManager.sol | 16 +- contracts/test/_fixture.js | 2 + contracts/test/strategies/uniswap-v3.js | 295 +++++++++++++----- 4 files changed, 233 insertions(+), 82 deletions(-) diff --git a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol index 0912347113..b11148b820 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol @@ -7,6 +7,8 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { IUniswapV3Helper } from "../../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; import { IMockUniswapV3Pool } from "./MockUniswapV3Pool.sol"; +import "hardhat/console.sol"; + contract MockNonfungiblePositionManager { using SafeERC20 for IERC20; diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index f217019bbc..3d9010934f 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -652,13 +652,15 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { { rebalanceNotPaused(); - return - _decreasePositionLiquidity( - activeTokenId, - liquidity, - minAmount0, - minAmount1 - ); + (amount0, amount1) = _decreasePositionLiquidity( + activeTokenId, + liquidity, + minAmount0, + minAmount1 + ); + + // Deposit + _depositAll(); // Intentionally skipping TVL check since removing liquidity won't cause it to fail } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index dbbcd4edbc..e27fe21b95 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -900,6 +900,7 @@ async function convexGeneralizedMetaForkedFixture( } async function impersonateAccount(address) { + address = address?.address || address; // Support passing contracts as well await hre.network.provider.request({ method: "hardhat_impersonateAccount", params: [address], @@ -907,6 +908,7 @@ async function impersonateAccount(address) { } async function impersonateAndFundContract(address) { + address = address?.address || address; // Support passing contracts as well await impersonateAccount(address); await hre.network.provider.send("hardhat_setBalance", [ diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 34e7cc509e..0f0d4b6b9a 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -1,5 +1,8 @@ const { expect } = require("chai"); -const { uniswapV3FixtureSetup } = require("../_fixture"); +const { + uniswapV3FixtureSetup, + impersonateAndFundContract, +} = require("../_fixture"); const { units, ousdUnits, @@ -8,7 +11,7 @@ const { usdtUnits, } = require("../helpers"); const { deployments } = require("hardhat"); -const { utils, BigNumber } = require("ethers") +const { BigNumber } = require("ethers"); const uniswapV3Fixture = uniswapV3FixtureSetup(); @@ -61,7 +64,7 @@ const activePositionFixture = deployments.createFixture(async () => { }); describe("Uniswap V3 Strategy", function () { - let fixture; + // let fixture; let vault, ousd, usdc, usdt, dai; let reserveStrategy, strategy, @@ -742,7 +745,7 @@ describe("Uniswap V3 Strategy", function () { }); }); - describe("DecreaseLiquidity/ClosePosition", () => { + describe.only("DecreaseLiquidity/ClosePosition", () => { beforeEach(async () => { _destructureFixture(await activePositionFixture()); }); @@ -766,13 +769,117 @@ describe("Uniswap V3 Strategy", function () { ); }); - it.skip("Should close active position", async () => {}); + it("Should close position", async () => { + const tokenId = await strategy.activeTokenId(); + + await strategy.connect(operator).closePosition(tokenId, 0, 0); + expect(await strategy.activeTokenId()).to.not.equal(tokenId); + const storagePos = await strategy.tokenIdToPosition(tokenId); + expect(storagePos.liquidity).to.equal( + 0, + "Should've removed all liquidity from closed position" + ); + }); + + it("Should decrease liquidity of active position", async () => { + const tokenId = await strategy.activeTokenId(); + const lastPos = await strategy.tokenIdToPosition(tokenId); + + await strategy + .connect(operator) + .decreaseActivePositionLiquidity(lastPos.liquidity.div(4), "0", "0"); + + const newPos = await strategy.tokenIdToPosition(tokenId); + + expect(newPos.liquidity).to.be.lt(lastPos.liquidity); + expect(newPos.netValue).to.be.lt(lastPos.netValue); + }); + + it("Should liquidate active position during withdraw", async () => { + const tokenId = await strategy.activeTokenId(); + const lastPos = await strategy.tokenIdToPosition(tokenId); + + await strategy + .connect(await impersonateAndFundContract(vault.address)) + .withdrawAssetFromActivePositionOnlyVault( + usdt.address, + usdtUnits("10000") + ); + + const newPos = await strategy.tokenIdToPosition(tokenId); - it.skip("Should liquidate active position during withdraw", async () => {}); + expect(newPos.liquidity).to.be.lt(lastPos.liquidity); + expect(newPos.netValue).to.be.lt( + lastPos.netValue.sub(ousdUnits("10000")) + ); + }); - it.skip("Should liquidate active position during withdrawAll", async () => {}); + it("Should liquidate active position during withdrawAll", async () => { + const tokenId = await strategy.activeTokenId(); + + await strategy + .connect(await impersonateAndFundContract(vault.address)) + .withdrawAll(); - it.skip("Should revert if caller isn't Governor/Strategist/Operator", async () => {}); + const pos = await strategy.tokenIdToPosition(tokenId); + + expect(pos.liquidity).to.equal(0); + expect(await strategy.activeTokenId()).to.equal(0); + expect(pos.netValue).to.equal(0); + }); + + it("Only vault can do withdraw/withdrawAll", async () => { + const impersonatedVaultSigner = await impersonateAndFundContract( + vault.address + ); + + await expect( + strategy + .connect(impersonatedVaultSigner) + .withdrawAssetFromActivePositionOnlyVault( + usdt.address, + usdtUnits("10000") + ) + ).to.not.be.reverted; + + await expect(strategy.connect(impersonatedVaultSigner).withdrawAll()).to + .not.be.reverted; + + for (const user of [governor, strategist, operator, daniel]) { + await expect( + strategy + .connect(user) + .withdrawAssetFromActivePositionOnlyVault( + usdt.address, + usdtUnits("10000") + ) + ).to.be.revertedWith("Caller is not the Vault"); + + await expect(strategy.connect(user).withdrawAll()).to.be.revertedWith( + "Caller is not the Vault" + ); + } + }); + + it("Should let Governor/Strategist/Operator to decrease liquidity of positions", async () => { + for (const user of [governor, strategist, operator]) { + await expect( + strategy + .connect(user) + .decreaseActivePositionLiquidity("1000", "0", "0") + ).to.not.be.reverted; + } + }); + + it("Should revert if caller isn't Governor/Strategist/Operator", async () => { + await expect( + strategy + .connect(matt) + .decreaseActivePositionLiquidity("1000", "0", "0") + ).to.be.revertedWith( + "Caller is not the Operator, Strategist or Governor" + ); + }); }); describe.skip("Swap And Rebalance", () => { @@ -841,130 +948,168 @@ describe("Uniswap V3 Strategy", function () { }); }); - describe.only("Net Value Lost Threshold", () => { + describe("Net Value Lost Threshold", () => { beforeEach(async () => { _destructureFixture(await activePositionFixture()); }); const _setNetLossVal = async (val) => { const netLostValueStorageSlot = BigNumber.from(169).toHexString(); - const expectedVal = BigNumber.from(val) - - const byte32Val = expectedVal.toHexString(val).replace( - "0x", - "0x" + (new Array(64 - (expectedVal.toHexString().length - 2)).fill("0").join("")) - ) + const expectedVal = BigNumber.from(val); + + const byte32Val = expectedVal + .toHexString(val) + .replace( + "0x", + "0x" + + new Array(64 - (expectedVal.toHexString().length - 2)) + .fill("0") + .join("") + ); await hre.network.provider.send("hardhat_setStorageAt", [ strategy.address, netLostValueStorageSlot, // Set an higher lost value manually - byte32Val - ]) + byte32Val, + ]); expect(await strategy.netLostValue()).to.equal( expectedVal, "Storage slot changed?" - ) - } + ); + }; + + it("Should update gained value of position when rebalancing", async () => { + _setNetLossVal(ousdUnits("1000")); // $1000 + const tokenId = await strategy.activeTokenId(); + + const initialValue = (await strategy.checkBalance(usdc.address)) + .add(await strategy.checkBalance(usdt.address)) + .mul(1e12); + + // Increase value of that position (Hackish way) + for (const asset of [usdc, usdt]) { + const amount = "1000000"; + await asset.connect(domen).mint(await units(amount, asset)); + await asset + .connect(domen) + .approve(mockPositionManager.address, await units(amount, asset)); + } + await mockPositionManager.connect(domen).increaseLiquidity({ + tokenId, + amount0Desired: usdcUnits("30000"), + amount1Desired: usdtUnits("30000"), + amount0Min: 0, + amount1Min: 0, + deadline: 10000000, + }); - it.skip("Should update lost value of position during mint/increase", async () => { + const currentValue = (await strategy.checkBalance(usdc.address)) + .add(await strategy.checkBalance(usdt.address)) + .mul(1e12); - // for (let i = 150; i <= 200; i++) { - // const val = await hre.network.provider.send("eth_getStorageAt", [ - // strategy.address, - // BigNumber.from(i).toHexString(), - // "latest" - // ]) + expect(currentValue).to.be.gt(initialValue); - // console.log(i, val) - // } + // Increase liquidity to trigger net loss update + await mintLiquidity({ + amount0: "1", + amount1: "1", + lowerTick: "-5", + upperTick: "-2", + }); + + expect(await strategy.netLostValue()).to.equal(0); + }); + + it("Should update lost value of position when rebalancing", async () => { + const tokenId = await strategy.activeTokenId(); + + const initialValue = (await strategy.checkBalance(usdc.address)) + .add(await strategy.checkBalance(usdt.address)) + .mul(1e12); + + // Reduce value of that position (Hackish way) + const liquidity = (await mockPositionManager.positions(tokenId)) + .liquidity; + await mockPositionManager.connect(operator).decreaseLiquidity({ + tokenId, + liquidity: liquidity.div(4), + amount0Min: 0, + amount1Min: 0, + deadline: 10000000, + }); + + const currentValue = (await strategy.checkBalance(usdc.address)) + .add(await strategy.checkBalance(usdt.address)) + .mul(1e12); + + const valueLost = initialValue.sub(currentValue); + + // Increase liquidity to trigger net loss update + await strategy.increaseActivePositionLiquidity("1", "1", "0", "0"); + + expect(await strategy.netLostValue()).to.equal(valueLost); }); it("Should update lost value when collecting fees", async () => { - await _setNetLossVal(ousdUnits("1000")) // $1000 + await _setNetLossVal(ousdUnits("1000")); // $1000 const tokenId = await strategy.activeTokenId(); await mockPositionManager.setTokensOwed( tokenId, usdcUnits("330"), // $330 - usdtUnits("120"), // $120 - ) - await strategy.collectFees() + usdtUnits("120") // $120 + ); + await strategy.collectFees(); expect(await strategy.netLostValue()).to.equal( BigNumber.from(ousdUnits("550")) - ) + ); }); it("Should reset lost value when collecting huge fees", async () => { - await _setNetLossVal(ousdUnits("1000")) // $1000 + await _setNetLossVal(ousdUnits("1000")); // $1000 const tokenId = await strategy.activeTokenId(); await mockPositionManager.setTokensOwed( tokenId, usdcUnits("4000"), // $4000 - usdtUnits("5000"), // $5000 - ) - await strategy.collectFees() + usdtUnits("5000") // $5000 + ); + await strategy.collectFees(); - expect(await strategy.netLostValue()).to.equal( - BigNumber.from("0") - ) + expect(await strategy.netLostValue()).to.equal(BigNumber.from("0")); }); it("Should allow close/withdraw beyond threshold", async () => { - await _setNetLossVal("999999999999999999999999999999999999999999999") + await _setNetLossVal("999999999999999999999999999999999999999999999"); const tokenId = await strategy.activeTokenId(); await expect( - strategy - .connect(operator) - .closePosition( - tokenId, - "0", - "0" - ) - ).to.not.be.reverted - - expect(await strategy.activeTokenId()).to.not.equal(tokenId) + strategy.connect(operator).closePosition(tokenId, "0", "0") + ).to.not.be.reverted; + + expect(await strategy.activeTokenId()).to.not.equal(tokenId); }); it("Should revert if beyond threshold (during mint)", async () => { - await _setNetLossVal("999999999999999999999999999999999999999999999") + await _setNetLossVal("999999999999999999999999999999999999999999999"); await expect( strategy .connect(operator) - .rebalance( - "1", - "1", - "0", - "0", - "0", - "0", - "-120", - "1234" - ) - ).to.be.revertedWith( - "Over max value loss threshold" - ) + .rebalance("1", "1", "0", "0", "0", "0", "-120", "1234") + ).to.be.revertedWith("Over max value loss threshold"); }); it("Should revert if beyond threshold (when increasing liquidity)", async () => { - await _setNetLossVal("999999999999999999999999999999999999999999999") + await _setNetLossVal("999999999999999999999999999999999999999999999"); await expect( strategy .connect(operator) - .increaseActivePositionLiquidity( - "1", - "1", - "0", - "0" - ) - ).to.be.revertedWith( - "Over max value loss threshold" - ) + .increaseActivePositionLiquidity("1", "1", "0", "0") + ).to.be.revertedWith("Over max value loss threshold"); }); }); }); From eef6d49dc2ea86aebab189a8d51da16a3d7652d3 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 16:52:52 +0530 Subject: [PATCH 41/83] Even more tests --- contracts/test/_fixture.js | 2 + contracts/test/helpers.js | 6 +- contracts/test/strategies/uniswap-v3.js | 382 ++++++++++++++++++++++-- 3 files changed, 365 insertions(+), 25 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index e27fe21b95..357dc25763 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -345,6 +345,8 @@ async function defaultFixture() { mockStrategy = await ethers.getContract("MockStrategy"); mockStrategy2 = await ethers.getContract("MockStrategy2"); mockStrategyDAI = await ethers.getContract("MockStrategyDAI"); + + UniV3SwapRouter = await ethers.getContract("MockUniswapRouter"); } if (!isFork) { const assetAddresses = await getAssetAddresses(deployments); diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index 30378cb88c..e4d2d04ff5 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -121,7 +121,11 @@ chai.Assertion.addMethod("emittedEvent", async function (eventName, args) { .expect(log.args.length) .to.equal(args.length, "Invalid event arg count"); for (let i = 0; i < args.length; i++) { - chai.expect(log.args[i]).to.equal(args[i]); + if (typeof args[i] == "function") { + args[i](log.args[i]); + } else { + chai.expect(log.args[i]).to.equal(args[i]); + } } } }); diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 0f0d4b6b9a..b4293ae505 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -17,19 +17,26 @@ const uniswapV3Fixture = uniswapV3FixtureSetup(); const liquidityManagerFixture = deployments.createFixture(async () => { const fixture = await uniswapV3Fixture(); - const { franck, daniel, domen, vault, usdc, usdt, dai } = fixture; + const { franck, daniel, domen, vault, usdc, usdt, dai, UniV3SwapRouter } = + fixture; // Mint some liquidity - for (const user of [franck, daniel, domen]) { + for (const user of [franck, daniel, domen, UniV3SwapRouter]) { + const isRouter = user.address == UniV3SwapRouter.address; + const account = isRouter + ? await impersonateAndFundContract(UniV3SwapRouter.address) + : user; for (const asset of [usdc, usdt, dai]) { - const amount = "1000000"; - await asset.connect(user).mint(await units(amount, asset)); - await asset - .connect(user) - .approve(vault.address, await units(amount, asset)); - await vault - .connect(user) - .mint(asset.address, await units(amount, asset), 0); + const amount = isRouter ? "10000000000" : "1000000"; + await asset.connect(account).mint(await units(amount, asset)); + if (!isRouter) { + await asset + .connect(user) + .approve(vault.address, await units(amount, asset)); + await vault + .connect(user) + .mint(asset.address, await units(amount, asset), 0); + } } } @@ -41,6 +48,10 @@ const liquidityManagerFixture = deployments.createFixture(async () => { governor ).setMaxPositionValueLossThreshold(ousdUnits("50000", 18)); + await fixture.UniV3_USDC_USDT_Strategy.connect( + governor + ).setSwapPriceThreshold(-100, 100); + return fixture; }); @@ -86,7 +97,7 @@ describe("Uniswap V3 Strategy", function () { }; function _destructureFixture(_fixture) { - fixture = _fixture; + // fixture = _fixture; reserveStrategy = _fixture.mockStrategy; mockStrategy2 = _fixture.mockStrategy2; mockStrategyDAI = _fixture.mockStrategyDAI; @@ -94,12 +105,12 @@ describe("Uniswap V3 Strategy", function () { helper = _fixture.UniV3Helper; mockPool = _fixture.UniV3_USDC_USDT_Pool; mockPositionManager = _fixture.UniV3PositionManager; + // swapRotuer = _fixture.UniV3SwapRouter; ousd = _fixture.ousd; usdc = _fixture.usdc; usdt = _fixture.usdt; dai = _fixture.dai; vault = _fixture.vault; - // harvester = _fixture.harvester; governor = _fixture.governor; strategist = _fixture.strategist; operator = _fixture.operator; @@ -172,8 +183,6 @@ describe("Uniswap V3 Strategy", function () { await vault.connect(matt).redeem(ousdUnits("30000"), 0); await expectApproxSupply(ousd, ousdUnits("200")); }); - - it.skip("Should withdraw from active position"); }); describe("Admin functions", () => { @@ -376,6 +385,76 @@ describe("Uniswap V3 Strategy", function () { return await tx.wait(); }; + const mintLiquidityBySwapping = async ({ + lowerTick, + upperTick, + amount0, + amount1, + minAmount0, + minAmount1, + minRedeemAmount0, + minRedeemAmount1, + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne, + existingPosition, + + signer, + expectRevert, + }) => { + const params = { + desiredAmount0: usdcUnits(amount0 || "10000"), + desiredAmount1: usdtUnits(amount1 || "10000"), + minAmount0: usdcUnits(minAmount0 || "0"), + minAmount1: usdtUnits(minAmount1 || "0"), + minRedeemAmount0: usdcUnits(minRedeemAmount0 || "0"), + minRedeemAmount1: usdtUnits(minRedeemAmount1 || "0"), + lowerTick, + upperTick, + swapAmountIn: BigNumber.from(swapAmountIn).mul(10 ** 6), + swapMinAmountOut: BigNumber.from(swapMinAmountOut).mul(10 ** 6), + sqrtPriceLimitX96, + swapZeroForOne, + }; + + if (expectRevert) { + if (typeof expectRevert === "string") { + await expect( + strategy.connect(signer || operator).swapAndRebalance(params) + ).to.be.revertedWith(expectRevert); + } else { + await expect( + strategy.connect(signer || operator).swapAndRebalance(params) + ).to.be.reverted; + } + + return; + } + + const tx = await strategy + .connect(signer || operator) + .swapAndRebalance(params); + + if (!existingPosition) { + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + } + + await expect(tx).to.have.emittedEvent("UniswapV3LiquidityAdded"); + await expect(tx).to.have.emittedEvent("AssetSwappedForRebalancing", [ + swapZeroForOne ? usdc.address : usdt.address, + swapZeroForOne ? usdt.address : usdc.address, + usdcUnits(swapAmountIn), + (amountReceived) => + expect(amountReceived).to.be.gte( + usdcUnits(swapMinAmountOut), + "Swap amountOut mismatch" + ), + ]); + + return await tx.wait(); + }; + describe("Rebalance/Mint", () => { beforeEach(async () => { _destructureFixture(await liquidityManagerFixture()); @@ -745,7 +824,7 @@ describe("Uniswap V3 Strategy", function () { }); }); - describe.only("DecreaseLiquidity/ClosePosition", () => { + describe("DecreaseLiquidity/ClosePosition", () => { beforeEach(async () => { _destructureFixture(await activePositionFixture()); }); @@ -882,25 +961,280 @@ describe("Uniswap V3 Strategy", function () { }); }); - describe.skip("Swap And Rebalance", () => { + describe("Swap And Rebalance", () => { + let impersonatedVault; beforeEach(async () => { _destructureFixture(await liquidityManagerFixture()); + impersonatedVault = await impersonateAndFundContract(vault.address); }); - it("Should swap token0 for token1 during rebalance", async () => {}); - it("Should swap token1 for token0 during rebalance", async () => {}); + const drainFromReserve = async (asset) => { + const fSign = "withdraw(address,address,uint256)"; + // Move all out of reserve + // prettier-ignore + await reserveStrategy.connect(impersonatedVault)[fSign]( + vault.address, + asset.address, + await reserveStrategy.checkBalance(asset.address) + ); + }; + + it("Should swap token0 for token1 during rebalance", async () => { + // Move all USDT out of reserve + await drainFromReserve(usdt); - it("Should revert if caller isn't Governor/Strategist/Operator", async () => {}); + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(2); + const swapZeroForOne = true; - it("Should revert if TVL check fails", async () => {}); + const { events } = await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "-100", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne, + }); + + const [tokenId, amount0Deposited, amount1Deposited, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; - it("Should revert if swap is paused"); + // Check storage + expect(await strategy.activeTokenId()).to.equal(tokenId); + const position = await strategy.tokenIdToPosition(tokenId); + expect(position.exists).to.be.true; + expect(position.liquidity).to.equal(liquidityMinted); + expect(position.netValue).to.equal( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + }); - it("Should revert if rebalance is paused"); + it("Should swap token1 for token0 during rebalance", async () => { + // Move all USDC out of reserve + await drainFromReserve(usdc); - it("Should revert if swapping is unnecessary", async () => {}); + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(-2); + const swapZeroForOne = false; + + const { events } = await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "-100", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne, + }); + + const [tokenId, amount0Deposited, amount1Deposited, liquidityMinted] = + events.find((e) => e.event == "UniswapV3LiquidityAdded").args; + + // Check storage + expect(await strategy.activeTokenId()).to.equal(tokenId); + const position = await strategy.tokenIdToPosition(tokenId); + expect(position.exists).to.be.true; + expect(position.liquidity).to.equal(liquidityMinted); + expect(position.netValue).to.equal( + amount0Deposited.add(amount1Deposited).mul(1e12) + ); + }); - it("Should revert if beyond swap limits", async () => {}); + it("Should revert if TVL check fails", async () => { + await drainFromReserve(usdt); + + await strategy.connect(governor).setMaxTVL(ousdUnits("1000")); + + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(2); + const swapZeroForOne = true; + + await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "-100", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne, + + expectRevert: "MaxTVL threshold has been reached", + }); + }); + + it("Should revert if swap is paused", async () => { + await strategy.connect(governor).setSwapsPaused(true); + + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(2); + const swapZeroForOne = true; + + await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "-100", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne, + + expectRevert: "Swaps are paused", + }); + }); + + it("Should revert if rebalance is paused", async () => { + await strategy.connect(governor).setRebalancePaused(true); + + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(2); + const swapZeroForOne = true; + + await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "-100", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne, + + expectRevert: "Rebalances are paused", + }); + }); + + it("Should revert if swapping is unnecessary", async () => { + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(2); + + await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "-100", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne: true, + + expectRevert: "Cannot swap when the asset is available in reserve", + }); + + await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "-100", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne: false, + + expectRevert: "Cannot swap when the asset is available in reserve", + }); + }); + + it("Should revert if beyond swap limits", async () => { + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(2); + + await mockPool.setTick(-200000); + + await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "-100", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne: true, + + expectRevert: "Price out of bounds", + }); + }); + + it("Should revert if pool is tilted", async () => { + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(-200000); + + await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "-100", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne: true, + + expectRevert: "Slippage out of bounds", + }); + }); + + it("Should revert if tick range is invalid", async () => { + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(2); + const swapZeroForOne = true; + + await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "1000", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne, + + expectRevert: "Invalid tick range", + }); + }); + + it("Should revert if caller isn't Governor/Strategist/Operator", async () => { + const amount = "100000"; + const swapAmountIn = "100000"; + const swapMinAmountOut = "100000"; + const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(2); + const swapZeroForOne = true; + + await mintLiquidityBySwapping({ + amount0: amount, + amount1: amount, + lowerTick: "1000", + upperTick: "100", + swapAmountIn, + swapMinAmountOut, + sqrtPriceLimitX96, + swapZeroForOne, + + signer: domen, + expectRevert: "Caller is not the Operator, Strategist or Governor", + }); + }); }); describe("Fees", () => { From 5d1066b5093dc2430dbe0fb59c409558d46cdfc2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 17:14:15 +0530 Subject: [PATCH 42/83] Get rid off unwanted changes --- .../GeneralizedUniswapV3Strategy.json | 983 ------------------ brownie/scripts/uniswap_v3.py | 45 - contracts/contracts/proxies/Proxies.sol | 2 +- contracts/contracts/vault/VaultAdmin.sol | 2 +- contracts/contracts/vault/VaultCore.sol | 20 +- contracts/contracts/vault/VaultStorage.sol | 2 +- .../deploy/025_resolution_upgrade_end.js | 1 - contracts/deploy/032_convex_rewards.js | 1 - .../deploy/036_multiple_rewards_strategies.js | 5 - contracts/deploy/042_ogv_buyback.js | 1 - contracts/deploy/046_value_value_checker.js | 1 - .../deploy/049_uniswap_usdc_usdt_strategy.js | 2 +- contracts/tasks/storageSlots.js | 22 +- contracts/utils/deploy.js | 5 +- 14 files changed, 15 insertions(+), 1077 deletions(-) delete mode 100644 brownie/interfaces/GeneralizedUniswapV3Strategy.json delete mode 100644 brownie/scripts/uniswap_v3.py diff --git a/brownie/interfaces/GeneralizedUniswapV3Strategy.json b/brownie/interfaces/GeneralizedUniswapV3Strategy.json deleted file mode 100644 index d27652d480..0000000000 --- a/brownie/interfaces/GeneralizedUniswapV3Strategy.json +++ /dev/null @@ -1,983 +0,0 @@ -[ - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_asset", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "_pToken", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "Deposit", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousGovernor", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newGovernor", - "type": "address" - } - ], - "name": "GovernorshipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_oldHarvesterAddress", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "_newHarvesterAddress", - "type": "address" - } - ], - "name": "HarvesterAddressesUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_address", - "type": "address" - } - ], - "name": "OperatorChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_asset", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "_pToken", - "type": "address" - } - ], - "name": "PTokenAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_asset", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "_pToken", - "type": "address" - } - ], - "name": "PTokenRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousGovernor", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newGovernor", - "type": "address" - } - ], - "name": "PendingGovernorshipTransfer", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "token", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "strategy", - "type": "address" - } - ], - "name": "ReserveStrategyChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address[]", - "name": "_oldAddresses", - "type": "address[]" - }, - { - "indexed": false, - "internalType": "address[]", - "name": "_newAddresses", - "type": "address[]" - } - ], - "name": "RewardTokenAddressesUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "rewardToken", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "RewardTokenCollected", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount0", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount1", - "type": "uint256" - } - ], - "name": "UniswapV3FeeCollected", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount0Sent", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount1Sent", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint128", - "name": "liquidityMinted", - "type": "uint128" - } - ], - "name": "UniswapV3LiquidityAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount0Received", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount1Received", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint128", - "name": "liquidityBurned", - "type": "uint128" - } - ], - "name": "UniswapV3LiquidityRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount0Received", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount1Received", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint128", - "name": "liquidityBurned", - "type": "uint128" - } - ], - "name": "UniswapV3PositionClosed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_asset", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "_pToken", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "Withdrawal", - "type": "event" - }, - { - "inputs": [], - "name": "_deprecated_rewardLiquidationThreshold", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "_deprecated_rewardTokenAddress", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "assetToPToken", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_asset", - "type": "address" - } - ], - "name": "checkBalance", - "outputs": [ - { - "internalType": "uint256", - "name": "balance", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "claimGovernance", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "closeActivePosition", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - } - ], - "name": "closePosition", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "collectRewardTokens", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_asset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "deposit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "depositAll", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "getPendingRewards", - "outputs": [ - { - "internalType": "uint128", - "name": "amount0", - "type": "uint128" - }, - { - "internalType": "uint128", - "name": "amount1", - "type": "uint128" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getRewardTokenAddresses", - "outputs": [ - { - "internalType": "address[]", - "name": "", - "type": "address[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "governor", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "harvesterAddress", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_vaultAddress", - "type": "address" - }, - { - "internalType": "address", - "name": "_poolAddress", - "type": "address" - }, - { - "internalType": "address", - "name": "_nonfungiblePositionManager", - "type": "address" - }, - { - "internalType": "address", - "name": "_token0ReserveStrategy", - "type": "address" - }, - { - "internalType": "address", - "name": "_token1ReserveStrategy", - "type": "address" - }, - { - "internalType": "address", - "name": "_operator", - "type": "address" - }, - { - "internalType": "address", - "name": "_uniswapV3Helper", - "type": "address" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_platformAddress", - "type": "address" - }, - { - "internalType": "address", - "name": "_vaultAddress", - "type": "address" - }, - { - "internalType": "address[]", - "name": "_rewardTokenAddresses", - "type": "address[]" - }, - { - "internalType": "address[]", - "name": "_assets", - "type": "address[]" - }, - { - "internalType": "address[]", - "name": "_pTokens", - "type": "address[]" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "isGovernor", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "positionManager", - "outputs": [ - { - "internalType": "contract INonfungiblePositionManager", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - }, - { - "internalType": "address", - "name": "", - "type": "address" - }, - { - "internalType": "uint256", - "name": "", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "name": "onERC721Received", - "outputs": [ - { - "internalType": "bytes4", - "name": "", - "type": "bytes4" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "operatorAddr", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "platformAddress", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "poolFee", - "outputs": [ - { - "internalType": "uint24", - "name": "", - "type": "uint24" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "maxAmount0", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "maxAmount1", - "type": "uint256" - }, - { - "internalType": "int24", - "name": "lowerTick", - "type": "int24" - }, - { - "internalType": "int24", - "name": "upperTick", - "type": "int24" - } - ], - "name": "rebalance", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_assetIndex", - "type": "uint256" - } - ], - "name": "removePToken", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "reserveStrategy", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "resetAllowanceOfTokens", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "rewardTokenAddresses", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "safeApproveAllTokens", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_harvesterAddress", - "type": "address" - } - ], - "name": "setHarvesterAddress", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_operator", - "type": "address" - } - ], - "name": "setOperator", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_asset", - "type": "address" - }, - { - "internalType": "address", - "name": "_pToken", - "type": "address" - } - ], - "name": "setPTokenAddress", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_token0ReserveStrategy", - "type": "address" - }, - { - "internalType": "address", - "name": "_token1ReserveStrategy", - "type": "address" - } - ], - "name": "setReserveStrategy", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "_rewardTokenAddresses", - "type": "address[]" - } - ], - "name": "setRewardTokenAddresses", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_asset", - "type": "address" - } - ], - "name": "supportsAsset", - "outputs": [ - { - "internalType": "bool", - "name": "", - "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": [ - { - "internalType": "address", - "name": "_newGovernor", - "type": "address" - } - ], - "name": "transferGovernance", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_asset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "transferToken", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "vaultAddress", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "address", - "name": "asset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "withdraw", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "withdrawAll", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } -] \ No newline at end of file diff --git a/brownie/scripts/uniswap_v3.py b/brownie/scripts/uniswap_v3.py deleted file mode 100644 index 20865775a6..0000000000 --- a/brownie/scripts/uniswap_v3.py +++ /dev/null @@ -1,45 +0,0 @@ -from world import * -from brownie import interface, accounts - -user1 = accounts.at("0x0000000000000000000000000000000000000001", force=True) -user2 = accounts.at("0x0000000000000000000000000000000000000002", force=True) - -uni_usdc_usdt_proxy = "0xa863A50233FB5Aa5aFb515e6C3e6FB9c075AA594" -uni_usdc_usdt_strat = interface.GeneralizedUniswapV3Strategy(uni_usdc_usdt_proxy) - -def get_some_balance(user): - USDT_BAGS = '0x5754284f345afc66a98fbb0a0afe71e0f007b949' - USDT_BAGS_2 = '0x5041ed759dd4afc3a72b8192c143f72f4724081a' - USDC_BAGS = '0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf' - USDC_BAGS_2 = '0x0a59649758aa4d66e25f08dd01271e891fe52199' - - usdt.transfer(user.address, int(usdt.balanceOf(USDT_BAGS) / 10), {'from': USDT_BAGS}) - # usdt.transfer(user.address, int(usdt.balanceOf(USDT_BAGS_2) / 10), {'from': USDT_BAGS_2}) - usdc.transfer(user.address, int(usdc.balanceOf(USDC_BAGS) / 10), {'from': USDC_BAGS}) - # usdc.transfer(user.address, int(usdc.balanceOf(USDC_BAGS_2) / 10), {'from': USDC_BAGS_2}) - - usdt.approve(vault_core.address, int(0), {'from': user}) - usdc.approve(vault_core.address, int(0), {'from': user}) - print("Loaded wallets with some funds") - -def set_as_default_strategy(): - vault_admin.setAssetDefaultStrategy(usdt.address, uni_usdc_usdt_proxy, {'from': TIMELOCK}) - vault_admin.setAssetDefaultStrategy(usdc.address, uni_usdc_usdt_proxy, {'from': TIMELOCK}) - print("Uniswap V3 set as default strategy") - -def main(): - brownie.chain.snapshot() - - try: - get_some_balance(user1) - # get_some_balance(user2) - - set_as_default_strategy() - - print("Trying to mint") - # vault_core.mint(usdt.address, 10000 * 1000000, 0, {'from': user1}) - vault_core.mint(usdc.address, 10000 * 1000000, 0, {'from': user1}) - except Exception as e: - print("Exception", e) - - brownie.chain.revert() \ No newline at end of file diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 69f811f743..bc73e7b51c 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -95,7 +95,7 @@ contract MorphoAaveStrategyProxy is InitializeGovernedUpgradeabilityProxy { } /** - * @notice UniV3_USDC_USDT_Proxy delegates calls to a GeneralizedUniswapV3Strategy implementation + * @notice UniV3_USDC_USDT_Proxy delegates calls to a UniswapV3Strategy implementation */ contract UniV3_USDC_USDT_Proxy is InitializeGovernedUpgradeabilityProxy { diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index d42accaaf9..486a03cff5 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -208,7 +208,7 @@ contract VaultAdmin is VaultStorage { /** * @dev Add a strategy to the Vault * @param _addr Address of the strategy to add - * @param isUniswapV3 Set to true, if the strategy is an instance of GeneralizedUniswapV3Strategy + * @param isUniswapV3 Set to true, if the strategy is an instance of UniswapV3Strategy */ function _approveStrategy(address _addr, bool isUniswapV3) internal { require(!strategies[_addr].isSupported, "Strategy already approved"); diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 9675ef3169..dcdc2d5823 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -203,23 +203,11 @@ contract VaultCore is VaultStorage { } else { address strategyAddr = assetDefaultStrategies[assetAddr]; - // `strategies` is initialized in `VaultAdmin` - // slither-disable-next-line uninitialized-state - if (strategies[strategyAddr].isUniswapV3Strategy) { - // In case of Uniswap Strategy, withdraw from - // Reserve strategy directly - strategyAddr = IUniswapV3Strategy(strategyAddr) - .reserveStrategy(assetAddr); - } + require(strategyAddr != address(0), "Liquidity error"); - if (strategyAddr != address(0)) { - // Nothing in Vault, but something in Strategy, send from there - IStrategy strategy = IStrategy(strategyAddr); - strategy.withdraw(msg.sender, assetAddr, outputs[i]); - } else { - // Cant find funds anywhere - revert("Liquidity error"); - } + // Nothing in Vault, but something in Strategy, send from there + IStrategy strategy = IStrategy(strategyAddr); + strategy.withdraw(msg.sender, assetAddr, outputs[i]); } } diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index d473a8ee4b..49d527c5db 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -66,7 +66,7 @@ contract VaultStorage is Initializable, Governable { struct Strategy { bool isSupported; uint256 _deprecated; // Deprecated storage slot - // Set to true if the Strategy is an instance of `GeneralizedUniswapV3Strategy` + // Set to true if the Strategy is an instance of `UniswapV3Strategy` bool isUniswapV3Strategy; } mapping(address => Strategy) internal strategies; diff --git a/contracts/deploy/025_resolution_upgrade_end.js b/contracts/deploy/025_resolution_upgrade_end.js index dbfd1234ea..1b83ccb7ff 100644 --- a/contracts/deploy/025_resolution_upgrade_end.js +++ b/contracts/deploy/025_resolution_upgrade_end.js @@ -7,7 +7,6 @@ module.exports = deploymentWithProposal( "OUSD", undefined, undefined, - undefined, true ); const cOUSDProxy = await ethers.getContract("OUSDProxy"); diff --git a/contracts/deploy/032_convex_rewards.js b/contracts/deploy/032_convex_rewards.js index 935000c808..ba05f63942 100644 --- a/contracts/deploy/032_convex_rewards.js +++ b/contracts/deploy/032_convex_rewards.js @@ -27,7 +27,6 @@ module.exports = deploymentWithProposal( "ConvexStrategy", undefined, undefined, - undefined, true // Disable storage slot checking. We are intentionally renaming a slot. ); const cConvexStrategyProxy = await ethers.getContract( diff --git a/contracts/deploy/036_multiple_rewards_strategies.js b/contracts/deploy/036_multiple_rewards_strategies.js index d7414b557f..c28fd312ca 100644 --- a/contracts/deploy/036_multiple_rewards_strategies.js +++ b/contracts/deploy/036_multiple_rewards_strategies.js @@ -17,14 +17,12 @@ module.exports = deploymentWithProposal( "VaultAdmin", undefined, undefined, - undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); const dVaultCore = await deployWithConfirmation( "VaultCore", undefined, undefined, - undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); @@ -50,21 +48,18 @@ module.exports = deploymentWithProposal( "ConvexStrategy", undefined, undefined, - undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); const dCompoundStrategyImpl = await deployWithConfirmation( "CompoundStrategy", undefined, undefined, - undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); const dAaveStrategyImpl = await deployWithConfirmation( "AaveStrategy", undefined, undefined, - undefined, true // Disable storage slot checking, we are renaming variables in InitializableAbstractStrategy. ); diff --git a/contracts/deploy/042_ogv_buyback.js b/contracts/deploy/042_ogv_buyback.js index 7998f49859..626e8e1d42 100644 --- a/contracts/deploy/042_ogv_buyback.js +++ b/contracts/deploy/042_ogv_buyback.js @@ -41,7 +41,6 @@ module.exports = deploymentWithProposal( assetAddresses.RewardsSource, ], "Buyback", - undefined, true ); const cBuyback = await ethers.getContract("Buyback"); diff --git a/contracts/deploy/046_value_value_checker.js b/contracts/deploy/046_value_value_checker.js index 9e64ef5c54..1e7ee74ac6 100644 --- a/contracts/deploy/046_value_value_checker.js +++ b/contracts/deploy/046_value_value_checker.js @@ -21,7 +21,6 @@ module.exports = deploymentWithProposal( "VaultValueChecker", [cVaultProxy.address, cOUSDProxy.address], undefined, - undefined, true // Incompatibable storage layout ); const vaultValueChecker = await ethers.getContract("VaultValueChecker"); diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index 1c03d47e04..46d98c9ac5 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -28,7 +28,7 @@ module.exports = deploymentWithGovernanceProposal( // Deployer Actions // ---------------- - // 0. Deploy UniswapV3Helper and UniswapV3StrategyLib + // 0. Deploy UniswapV3Helper const dUniswapV3Helper = await deployWithConfirmation("UniswapV3Helper"); // 0. Upgrade VaultAdmin diff --git a/contracts/tasks/storageSlots.js b/contracts/tasks/storageSlots.js index 04a8eca418..d82a76472e 100644 --- a/contracts/tasks/storageSlots.js +++ b/contracts/tasks/storageSlots.js @@ -27,11 +27,9 @@ const getStorageFileLocation = (hre, contractName) => { return `${layoutFolder}${contractName}.json`; }; -const getStorageLayoutForContract = async (hre, contractName, libraries) => { +const getStorageLayoutForContract = async (hre, contractName) => { const validations = await readValidations(hre); - const implFactory = await hre.ethers.getContractFactory(contractName, { - libraries, - }); + const implFactory = await hre.ethers.getContractFactory(contractName); const unlinkedBytecode = getUnlinkedBytecode( validations, implFactory.bytecode @@ -52,12 +50,8 @@ const loadPreviousStorageLayoutForContract = async (hre, contractName) => { return JSON.parse(await promises.readFile(location, "utf8")); }; -const storeStorageLayoutForContract = async (hre, contractName, libraries) => { - const layout = await getStorageLayoutForContract( - hre, - contractName, - libraries - ); +const storeStorageLayoutForContract = async (hre, contractName) => { + const layout = await getStorageLayoutForContract(hre, contractName); const storageLayoutFile = getStorageFileLocation(hre, contractName); // pretty print storage layout for the contract @@ -122,17 +116,13 @@ const showStorageLayout = async (taskArguments, hre) => { visualizeLayoutData(layout); }; -const assertUpgradeIsSafe = async (hre, contractName, libraries) => { +const assertUpgradeIsSafe = async (hre, contractName) => { if (!isContractEligible(contractName)) { console.debug(`Skipping storage slot validation of ${contractName}.`); return true; } - const layout = await getStorageLayoutForContract( - hre, - contractName, - libraries - ); + const layout = await getStorageLayoutForContract(hre, contractName); const oldLayout = await loadPreviousStorageLayoutForContract( hre, diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index dfb49ff4ae..0be336345d 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -50,15 +50,13 @@ const deployWithConfirmation = async ( contractName, args, contract, - libraries, skipUpgradeSafety = false ) => { // check that upgrade doesn't corrupt the storage slots if (!skipUpgradeSafety) { await assertUpgradeIsSafe( hre, - typeof contract == "string" ? contract : contractName, - libraries + typeof contract == "string" ? contract : contractName ); } @@ -72,7 +70,6 @@ const deployWithConfirmation = async ( args, contract, fieldsToCompare: null, - libraries, ...(await getTxOpts()), }) ); From a6142425766ca50dd04324c8126b1a39872278bf Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 17:17:39 +0530 Subject: [PATCH 43/83] Fix vault tests --- contracts/test/vault/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/test/vault/index.js b/contracts/test/vault/index.js index 3581c79d76..1cd2f10eec 100644 --- a/contracts/test/vault/index.js +++ b/contracts/test/vault/index.js @@ -2,6 +2,7 @@ const { defaultFixture } = require("../_fixture"); const chai = require("chai"); const hre = require("hardhat"); const { utils } = require("ethers"); +const { solidity } = require("ethereum-waffle"); const { ousdUnits, @@ -15,6 +16,9 @@ const { isFork, } = require("../helpers"); +// Support BigNumber and all that with ethereum-waffle +chai.use(solidity); + const expect = chai.expect; describe("Vault", function () { From ebbab24605c870c6b72db642cf19ffc392e76e00 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 17:32:46 +0530 Subject: [PATCH 44/83] Fix failing unit tests --- contracts/test/strategies/uniswap-v3.fork-test.js | 3 ++- contracts/test/strategies/uniswap-v3.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 5d0bd3fc2d..907f5ddc1c 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -784,7 +784,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { ); }; - it("Should withdraw from reserve strategy", async () => { + it.skip("Should withdraw from reserve strategy", async () => { + // Withdraw liquidates current position, so this test is no longer valid redeemTest(josh, "10000"); }); }); diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index b4293ae505..5f52f9e272 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -170,7 +170,9 @@ describe("Uniswap V3 Strategy", function () { await expectApproxSupply(ousd, ousdUnits("200")); }); - it("Should withdraw from reserve strategy", async () => { + it.skip("Should withdraw from reserve strategy", async () => { + // Withdraw liquidates current position, so this test is no longer valid + // Vault has 200 DAI from fixtures await expectApproxSupply(ousd, ousdUnits("200")); await expect(vault).has.an.approxBalanceOf("200", dai); From 03cdc0c9fb1804889926b827e22b8916d8a9f9e7 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 17:36:25 +0530 Subject: [PATCH 45/83] Resolve global fixture conflict for fork tests --- contracts/fork-test.sh | 2 +- contracts/test/vault/vault.fork-test.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/fork-test.sh b/contracts/fork-test.sh index f6d3d14868..5cd7275d1c 100755 --- a/contracts/fork-test.sh +++ b/contracts/fork-test.sh @@ -45,7 +45,7 @@ main() if [ -z "$LOCAL_PROVIDER_URL" ]; then cp -r deployments/mainnet deployments/hardhat echo "No running node detected spinning up a fresh one" - params+="--deploy-fixture " + # params+="--deploy-fixture " else if ! command -v jq &> /dev/null then diff --git a/contracts/test/vault/vault.fork-test.js b/contracts/test/vault/vault.fork-test.js index 7e9932ebeb..77f79cce9e 100644 --- a/contracts/test/vault/vault.fork-test.js +++ b/contracts/test/vault/vault.fork-test.js @@ -301,7 +301,7 @@ forkOnlyDescribe("ForkTest: Vault", function () { "0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAaveStrategy // TODO: Hard-code these after deploy //"0x7A192DD9Cc4Ea9bdEdeC9992df74F1DA55e60a19", // LUSD MetaStrategy - "0xa863A50233FB5Aa5aFb515e6C3e6FB9c075AA594", // USDC<>USDT Uniswap V3 Strategy + "0x050c4FcA28725d975c2896682eBD2905D2E58E84", // USDC<>USDT Uniswap V3 Strategy ]; for (const s of strategies) { @@ -328,6 +328,7 @@ forkOnlyDescribe("ForkTest: Vault", function () { "0x9c459eeb3FA179a40329b81C1635525e9A0Ef094", "0x5A4eEe58744D1430876d5cA93cAB5CcB763C037D", // Morpho "0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAave + "0x050c4FcA28725d975c2896682eBD2905D2E58E84", // USDC<>USDT Uniswap V3 Strategy ]).to.include(await vault.assetDefaultStrategies(usdt.address)); }); @@ -340,6 +341,7 @@ forkOnlyDescribe("ForkTest: Vault", function () { "0x9c459eeb3FA179a40329b81C1635525e9A0Ef094", "0x5A4eEe58744D1430876d5cA93cAB5CcB763C037D", // Morpho "0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAave + "0x050c4FcA28725d975c2896682eBD2905D2E58E84", // USDC<>USDT Uniswap V3 Strategy ]).to.include(await vault.assetDefaultStrategies(usdc.address)); }); @@ -352,6 +354,7 @@ forkOnlyDescribe("ForkTest: Vault", function () { "0x9c459eeb3FA179a40329b81C1635525e9A0Ef094", "0x5A4eEe58744D1430876d5cA93cAB5CcB763C037D", // Morpho "0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAave + "0x050c4FcA28725d975c2896682eBD2905D2E58E84", // USDC<>USDT Uniswap V3 Strategy ]).to.include(await vault.assetDefaultStrategies(dai.address)); }); From 60aa462e48980e1db267fd990f854e1204cca4d6 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 18:17:57 +0530 Subject: [PATCH 46/83] Cleanup helper functions --- contracts/test/helpers.js | 8 ++------ contracts/test/strategies/uniswap-v3.js | 1 + contracts/test/vault/index.js | 3 +-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index e4d2d04ff5..2bbba4ee21 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -62,9 +62,7 @@ chai.Assertion.addMethod( const user = this._obj; const address = user.address || user.getAddress(); // supports contracts too const actual = await contract.balanceOf(address); - if (!BigNumber.isBigNumber(expected)) { - expected = parseUnits(expected, await decimalsFor(contract)); - } + expected = parseUnits(expected, await decimalsFor(contract)); chai.expect(actual).to.approxEqual(expected, message); } ); @@ -90,9 +88,7 @@ chai.Assertion.addMethod( const user = this._obj; const address = user.address || user.getAddress(); // supports contracts too const actual = await contract.balanceOf(address); - if (!BigNumber.isBigNumber(expected)) { - expected = parseUnits(expected, await decimalsFor(contract)); - } + expected = parseUnits(expected, await decimalsFor(contract)); chai.expect(actual).to.equal(expected, message); } ); diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 5f52f9e272..b3e8587f4f 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -176,6 +176,7 @@ describe("Uniswap V3 Strategy", function () { // Vault has 200 DAI from fixtures await expectApproxSupply(ousd, ousdUnits("200")); await expect(vault).has.an.approxBalanceOf("200", dai); + // Mint some OUSD with USDT await mint(matt, "30000", usdt); await expectApproxSupply(ousd, ousdUnits("30200")); diff --git a/contracts/test/vault/index.js b/contracts/test/vault/index.js index 1cd2f10eec..c19701d07b 100644 --- a/contracts/test/vault/index.js +++ b/contracts/test/vault/index.js @@ -1,8 +1,8 @@ const { defaultFixture } = require("../_fixture"); const chai = require("chai"); const hre = require("hardhat"); -const { utils } = require("ethers"); const { solidity } = require("ethereum-waffle"); +const { utils } = require("ethers"); const { ousdUnits, @@ -18,7 +18,6 @@ const { // Support BigNumber and all that with ethereum-waffle chai.use(solidity); - const expect = chai.expect; describe("Vault", function () { From 51c050b589ccb57a6e496265972366a763e4a2d2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 18:59:13 +0530 Subject: [PATCH 47/83] Add comments --- .../uniswap/UniswapV3LiquidityManager.sol | 110 +++++++++++++++--- .../strategies/uniswap/UniswapV3Strategy.sol | 29 ++--- .../uniswap/UniswapV3StrategyStorage.sol | 1 + contracts/test/_fixture.js | 1 - .../test/strategies/uniswap-v3.fork-test.js | 4 +- contracts/test/strategies/uniswap-v3.js | 109 ++++++++--------- 6 files changed, 166 insertions(+), 88 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 3d9010934f..cbe8593dcf 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -20,6 +20,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { using SafeERC20 for IERC20; using StableMath for uint256; + /*************************************** + Position Value + ****************************************/ /** * @notice Calculates the net value of the position exlcuding fees * @param tokenId tokenID of the Position NFT @@ -147,6 +150,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /*************************************** Rebalance ****************************************/ + /// Reverts if active position's value is greater than maxTVL function ensureTVL() internal { require( getPositionValue(activeTokenId) <= maxTVL, @@ -154,6 +158,11 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ); } + /** + * @notice Reverts if swaps are paused or if swapping constraints aren't met. + * @param sqrtPriceLimitX96 Desired swap price limit + * @param swapZeroForOne True when swapping token0 for token1 + */ function swapsNotPausedAndWithinLimits( uint160 sqrtPriceLimitX96, bool swapZeroForOne @@ -176,10 +185,16 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ); } + /// Reverts if rebalances are paused function rebalanceNotPaused() internal { require(!rebalancePaused, "Rebalances are paused"); } + /** + * @notice Reverts if rebalances are paused or if rebalance constraints aren't met. + * @param upperTick Upper tick index + * @param lowerTick Lower tick inded + */ function rebalanceNotPausedAndWithinLimits(int24 lowerTick, int24 upperTick) internal { @@ -197,6 +212,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { * the counter would still be at zero. We don't keep track of any value gained * until the counter is > 0, as the only purpose of this state variable is * to shut off rebalancing if the LP positions are losing capital across rebalances. + * + * @param delta The unsigned change in value + * @param gained True, if sign of delta is positive */ function _setNetLostValue(uint256 delta, bool gained) internal { if (delta == 0) { @@ -223,6 +241,11 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { emit NetLostValueChanged(netLostValue); } + /** + * @notice Computes the current value of the given token and updates + * the storage. Also, updates netLostValue state + * @param tokenId Token ID of the position + */ function updatePositionNetVal(uint256 tokenId) internal { if (tokenId == 0) { return; @@ -255,6 +278,11 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { tokenIdToPosition[tokenId].netValue = currentVal; } + /** + * @notice Updates the value of the current position. + * Reverts if netLostValue threshold is breached. + * @param tokenId Token ID of the position + */ function ensureNetLossThreshold(uint256 tokenId) internal { updatePositionNetVal(tokenId); require( @@ -348,6 +376,24 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { bool swapZeroForOne; } + /** + * @notice Closes active LP position if any and then provides liquidity to the requested position. + * Mints new position, if it doesn't exist already. If active position is on the same tick + * range, then just increases the liquidity by the desiredAmounts. Will pull funds needed + * from reserve strategies and then will deposit back all dust to them + * @param params.desiredAmount0 Amount of token0 to use to provide liquidity + * @param params.desiredAmount1 Amount of token1 to use to provide liquidity + * @param params.minAmount0 Min amount of token0 to deposit/expect + * @param params.minAmount1 Min amount of token1 to deposit/expect + * @param params.minRedeemAmount0 Min amount of token0 received from closing active position + * @param params.minRedeemAmount1 Min amount of token1 received from closing active position + * @param params.lowerTick Desired lower tick index + * @param params.upperTick Desired upper tick index + * @param params.swapAmountIn Amount of tokens to swap + * @param params.swapMinAmountOut Minimum amount of other tokens expected + * @param params.sqrtPriceLimitX96 Max price limit for swap + * @param params.swapZeroForOne True if swapping from token0 to token1 + */ function swapAndRebalance(SwapAndRebalanceParams calldata params) external onlyGovernorOrStrategistOrOperator @@ -508,13 +554,17 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { } /** - * @notice Increases liquidity of the active position. - * @dev Will pull funds needed from reserve strategies + * @notice Increases liquidity of the given token. + * * @param tokenId Position NFT's tokenId * @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 + * + * @return liquidity Amount of liquidity added + * @return amount0 Amount of token0 deposited + * @return amount1 Amount of token1 deposited */ function _increasePositionLiquidity( uint256 tokenId, @@ -558,6 +608,18 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); } + /** + * @notice Increases liquidity of the active position token. + * Will pull funds needed from reserve strategies if needed. + * + * @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 + * + * @return amount0 Amount of token0 deposited + * @return amount1 Amount of token1 deposited + */ function increaseActivePositionLiquidity( uint256 desiredAmount0, uint256 desiredAmount1, @@ -592,9 +654,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /** * @notice Removes liquidity of the position in the pool * - * @dev Scope intentionally set to public so that the base strategy can delegatecall this function. - * Setting it to external would restrict other functions in this contract from using it - * * @param tokenId Position NFT's tokenId * @param liquidity Amount of liquidity to remove form the position * @param minAmount0 Min amount of token0 to withdraw @@ -640,6 +699,16 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ); } + /** + * @notice Removes liquidity of the active position in the pool + * + * @param liquidity Amount of liquidity to remove form the position + * @param minAmount0 Min amount of token0 to withdraw + * @param minAmount1 Min amount of token1 to withdraw + * + * @return amount0 Amount of token0 received after liquidation + * @return amount1 Amount of token1 received after liquidation + */ function decreaseActivePositionLiquidity( uint128 liquidity, uint256 minAmount0, @@ -783,6 +852,16 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { } } + /** + * @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, @@ -873,14 +952,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { ); } - function collectFees() - external - onlyGovernorOrStrategistOrOperator - returns (uint256 amount0, uint256 amount1) - { - return _collectFeesForToken(activeTokenId); - } - /** * @notice Collects the fees generated by the position on V3 pool. * Also adjusts netLostValue based on fee collected. @@ -909,6 +980,19 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { emit UniswapV3FeeCollected(tokenId, amount0, amount1); } + /** + * @notice Collects fees from the active LP position + * @return amount0 Amount of token0 collected as fee + * @return amount1 Amount of token1 collected as fee + */ + function collectFees() + external + onlyGovernorOrStrategistOrOperator + returns (uint256 amount0, uint256 amount1) + { + return _collectFeesForToken(activeTokenId); + } + /*************************************** Hidden functions ****************************************/ diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 9d804d9512..bb10e2638f 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -39,7 +39,9 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { address _swapRouter, address _operator ) external onlyGovernor initializer { - // TODO: Comment on why this is necessary and why it should always be the proxy address + // NOTE: _self should always be the address of the proxy. + // This is used to do `delegecall` between the this contract and + // `UniswapV3LiquidityManager` whenever it's required. _self = IUniswapV3Strategy(address(this)); pool = IUniswapV3Pool(_poolAddress); @@ -255,12 +257,9 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { onlyPoolTokens(_asset); if ( - _amount > 0 && - ( - _asset == token0 - ? (_amount > minDepositThreshold0) - : (_amount > minDepositThreshold1) - ) + _asset == token0 + ? (_amount > minDepositThreshold0) + : (_amount > minDepositThreshold1) ) { IVault(vaultAddress).depositToUniswapV3Reserve(_asset, _amount); // Not emitting Deposit event since the Reserve strategy would do so @@ -319,15 +318,13 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { require(success, "DelegateCall to close position failed"); } - // saves 100B of contract size to loop through these 2 tokens - address[2] memory tokens = [token0, token1]; for (uint256 i = 0; i < 2; i++) { - IERC20 tokenContract = IERC20(tokens[i]); + IERC20 tokenContract = IERC20(assetsMapped[i]); uint256 tokenBalance = tokenContract.balanceOf(address(this)); if (tokenBalance > 0) { tokenContract.safeTransfer(vaultAddress, tokenBalance); - emit Withdrawal(tokens[i], tokens[i], tokenBalance); + emit Withdrawal(assetsMapped[i], assetsMapped[i], tokenBalance); } } } @@ -404,8 +401,6 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { uint256, bytes calldata ) external returns (bytes4) { - // TODO: Should we reject unwanted NFTs being transfered to the strategy? - // Could use `INonfungiblePositionManager.positions(tokenId)` to see if the token0 and token1 are matching return this.onERC721Received.selector; } @@ -458,12 +453,13 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { /*************************************** 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"); @@ -482,10 +478,6 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @param newImpl address of the implementation */ function setLiquidityManagerImpl(address newImpl) external onlyGovernor { - _setLiquidityManagerImpl(newImpl); - } - - function _setLiquidityManagerImpl(address newImpl) internal { require( Address.isContract(newImpl), "new implementation is not a contract" @@ -495,6 +487,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { assembly { sstore(position, newImpl) } + emit LiquidityManagerImplementationUpgraded(newImpl); } /** diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 26fec5e4a3..8d5bdd716c 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -14,6 +14,7 @@ import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRou abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { event OperatorChanged(address _address); + event LiquidityManagerImplementationUpgraded(address _newImpl); event ReserveStrategyChanged(address asset, address reserveStrategy); event MinDepositThresholdChanged( address asset, diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 357dc25763..1d5890b98c 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -24,7 +24,6 @@ const threepoolLPAbi = require("./abi/threepoolLP.json"); const threepoolSwapAbi = require("./abi/threepoolSwap.json"); async function defaultFixture() { - // TODO: reset this tag later await deployments.fixture( isFork ? undefined diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 907f5ddc1c..8b7a6475f9 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -15,11 +15,11 @@ const { } = require("../helpers"); const { BigNumber, utils } = require("ethers"); -const uniswapV3Fixture = uniswapV3FixtureSetup(); - forkOnlyDescribe("Uniswap V3 Strategy", function () { this.timeout(0); + const uniswapV3Fixture = uniswapV3FixtureSetup(); + let fixture; let vault, ousd, usdc, usdt, dai; let reserveStrategy, strategy, pool, positionManager, v3Helper, swapRouter; diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index b3e8587f4f..1853bc3460 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -13,68 +13,69 @@ const { const { deployments } = require("hardhat"); const { BigNumber } = require("ethers"); -const uniswapV3Fixture = uniswapV3FixtureSetup(); - -const liquidityManagerFixture = deployments.createFixture(async () => { - const fixture = await uniswapV3Fixture(); - const { franck, daniel, domen, vault, usdc, usdt, dai, UniV3SwapRouter } = - fixture; - - // Mint some liquidity - for (const user of [franck, daniel, domen, UniV3SwapRouter]) { - const isRouter = user.address == UniV3SwapRouter.address; - const account = isRouter - ? await impersonateAndFundContract(UniV3SwapRouter.address) - : user; - for (const asset of [usdc, usdt, dai]) { - const amount = isRouter ? "10000000000" : "1000000"; - await asset.connect(account).mint(await units(amount, asset)); - if (!isRouter) { - await asset - .connect(user) - .approve(vault.address, await units(amount, asset)); - await vault - .connect(user) - .mint(asset.address, await units(amount, asset), 0); +describe("Uniswap V3 Strategy", function () { + // Fixtures + const uniswapV3Fixture = uniswapV3FixtureSetup(); + + const liquidityManagerFixture = deployments.createFixture(async () => { + const fixture = await uniswapV3Fixture(); + const { franck, daniel, domen, vault, usdc, usdt, dai, UniV3SwapRouter } = + fixture; + + // Mint some liquidity + for (const user of [franck, daniel, domen, UniV3SwapRouter]) { + const isRouter = user.address == UniV3SwapRouter.address; + const account = isRouter + ? await impersonateAndFundContract(UniV3SwapRouter.address) + : user; + for (const asset of [usdc, usdt, dai]) { + const amount = isRouter ? "10000000000" : "1000000"; + await asset.connect(account).mint(await units(amount, asset)); + if (!isRouter) { + await asset + .connect(user) + .approve(vault.address, await units(amount, asset)); + await vault + .connect(user) + .mint(asset.address, await units(amount, asset), 0); + } } } - } - // Configure mockPool - const { UniV3_USDC_USDT_Pool: mockPool, governor } = fixture; - await mockPool.connect(governor).setTick(-2); + // Configure mockPool + const { UniV3_USDC_USDT_Pool: mockPool, governor } = fixture; + await mockPool.connect(governor).setTick(-2); - await fixture.UniV3_USDC_USDT_Strategy.connect( - governor - ).setMaxPositionValueLossThreshold(ousdUnits("50000", 18)); + await fixture.UniV3_USDC_USDT_Strategy.connect( + governor + ).setMaxPositionValueLossThreshold(ousdUnits("50000", 18)); - await fixture.UniV3_USDC_USDT_Strategy.connect( - governor - ).setSwapPriceThreshold(-100, 100); + await fixture.UniV3_USDC_USDT_Strategy.connect( + governor + ).setSwapPriceThreshold(-100, 100); - return fixture; -}); + return fixture; + }); -const activePositionFixture = deployments.createFixture(async () => { - const fixture = await liquidityManagerFixture(); - - // Mint a position - const { operator, UniV3_USDC_USDT_Strategy } = fixture; - await UniV3_USDC_USDT_Strategy.connect(operator).rebalance( - usdcUnits("100000"), - usdcUnits("100000"), - "0", - "0", - "0", - "0", - "-100", - "100" - ); - - return fixture; -}); + const activePositionFixture = deployments.createFixture(async () => { + const fixture = await liquidityManagerFixture(); + + // Mint a position + const { operator, UniV3_USDC_USDT_Strategy } = fixture; + await UniV3_USDC_USDT_Strategy.connect(operator).rebalance( + usdcUnits("100000"), + usdcUnits("100000"), + "0", + "0", + "0", + "0", + "-100", + "100" + ); + + return fixture; + }); -describe("Uniswap V3 Strategy", function () { // let fixture; let vault, ousd, usdc, usdt, dai; let reserveStrategy, From 748ceced71e96890253cdc8cfee865ab4f914f7d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 19:13:04 +0530 Subject: [PATCH 48/83] Fix fixtures bleed --- .../contracts/strategies/uniswap/UniswapV3Strategy.sol | 4 ++-- contracts/test/_fixture.js | 7 +++++++ contracts/test/strategies/uniswap-v3.js | 9 +++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index bb10e2638f..797ab40cb9 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -40,8 +40,8 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { address _operator ) external onlyGovernor initializer { // NOTE: _self should always be the address of the proxy. - // This is used to do `delegecall` between the this contract and - // `UniswapV3LiquidityManager` whenever it's required. + // This is used to do `delegecall` between the this contract and + // `UniswapV3LiquidityManager` whenever it's required. _self = IUniswapV3Strategy(address(this)); pool = IUniswapV3Pool(_poolAddress); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 1d5890b98c..0896c9246d 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1241,6 +1241,12 @@ async function rebornFixture() { return fixture; } +function defaultFixtureSetup() { + return deployments.createFixture(async () => { + return await defaultFixture(); + }); +} + function uniswapV3FixtureSetup() { return deployments.createFixture(async () => { const fixture = await defaultFixture(); @@ -1368,6 +1374,7 @@ module.exports = { hackedVaultFixture, rebornFixture, uniswapV3FixtureSetup, + defaultFixtureSetup, withImpersonatedAccount, impersonateAndFundContract, impersonateAccount, diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 1853bc3460..d6979d01f3 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -2,6 +2,7 @@ const { expect } = require("chai"); const { uniswapV3FixtureSetup, impersonateAndFundContract, + defaultFixtureSetup, } = require("../_fixture"); const { units, @@ -14,6 +15,14 @@ const { deployments } = require("hardhat"); const { BigNumber } = require("ethers"); describe("Uniswap V3 Strategy", function () { + after(async () => { + // This is needed to revert fixtures + // The other tests as of now don't use proper fixtures + // Rel: https://github.com/OriginProtocol/origin-dollar/issues/1259 + const f = defaultFixtureSetup(); + await f(); + }); + // Fixtures const uniswapV3Fixture = uniswapV3FixtureSetup(); From 97546b2c283260bbf7680363eaab02eeecc80760 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 19:20:50 +0530 Subject: [PATCH 49/83] Fix bleeding fixtures on fork tests --- .../test/strategies/uniswap-v3.fork-test.js | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 8b7a6475f9..e1beb789c6 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -2,6 +2,7 @@ const { expect } = require("chai"); const { uniswapV3FixtureSetup, impersonateAndFundContract, + defaultFixtureSetup, } = require("../_fixture"); const { forkOnlyDescribe, @@ -16,6 +17,14 @@ const { const { BigNumber, utils } = require("ethers"); forkOnlyDescribe("Uniswap V3 Strategy", function () { + after(async () => { + // This is needed to revert fixtures + // The other tests as of now don't use proper fixtures + // Rel: https://github.com/OriginProtocol/origin-dollar/issues/1259 + const f = defaultFixtureSetup(); + await f(); + }); + this.timeout(0); const uniswapV3Fixture = uniswapV3FixtureSetup(); @@ -223,6 +232,26 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { }; }; + async function _swap(user, amount, zeroForOne) { + const [, activeTick] = await pool.slot0(); + const sqrtPriceLimitX96 = await v3Helper.getSqrtRatioAtTick( + activeTick + (zeroForOne ? -2 : 2) + ); + const swapAmount = BigNumber.from(amount).mul(10 ** 6); + usdc.connect(user).approve(swapRouter.address, swapAmount.mul(10)); + usdt.connect(user).approve(swapRouter.address, swapAmount.mul(10)); + await swapRouter.connect(user).exactInputSingle([ + zeroForOne ? usdc.address : usdt.address, // tokenIn + zeroForOne ? usdt.address : usdc.address, // tokenOut + 100, // fee + user.address, // recipient + (await getBlockTimestamp()) + 5, // deadline + swapAmount, // amountIn + 0, // amountOutMinimum + sqrtPriceLimitX96, + ]); + } + it("Should mint position", async () => { const usdcBalBefore = await strategy.checkBalance(usdc.address); const usdtBalBefore = await strategy.checkBalance(usdt.address); @@ -562,26 +591,6 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(strategy).to.have.an.approxBalanceOf(usdtBalAfter, usdt); }); - async function _swap(user, amount, zeroForOne) { - const [, activeTick] = await pool.slot0(); - const sqrtPriceLimitX96 = await v3Helper.getSqrtRatioAtTick( - activeTick + (zeroForOne ? -2 : 2) - ); - const swapAmount = BigNumber.from(amount).mul(10 ** 6); - usdc.connect(user).approve(swapRouter.address, swapAmount.mul(10)); - usdt.connect(user).approve(swapRouter.address, swapAmount.mul(10)); - await swapRouter.connect(user).exactInputSingle([ - zeroForOne ? usdc.address : usdt.address, // tokenIn - zeroForOne ? usdt.address : usdc.address, // tokenOut - 100, // fee - user.address, // recipient - (await getBlockTimestamp()) + 5, // deadline - swapAmount, // amountIn - 0, // amountOutMinimum - sqrtPriceLimitX96, - ]); - } - it("Should collect fees", async () => { const [, activeTick] = await pool.slot0(); const lowerTick = activeTick - 12; From 1eb7709a1fd0f7e1f1a3c4d9505a110ea1f9f78c Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 19:59:49 +0530 Subject: [PATCH 50/83] More unit tests --- .../uniswap/UniswapV3LiquidityManager.sol | 2 +- .../strategies/uniswap/UniswapV3Strategy.sol | 9 +- .../uniswap/UniswapV3StrategyStorage.sol | 32 +-- .../deploy/049_uniswap_usdc_usdt_strategy.js | 2 +- contracts/test/_fixture.js | 2 +- .../test/strategies/uniswap-v3.fork-test.js | 8 +- contracts/test/strategies/uniswap-v3.js | 197 +++++++++++++++++- 7 files changed, 222 insertions(+), 30 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index cbe8593dcf..c3d3bf55be 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -286,7 +286,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { function ensureNetLossThreshold(uint256 tokenId) internal { updatePositionNetVal(tokenId); require( - netLostValue < maxPositionValueLossThreshold, + netLostValue < maxPositionValueLostThreshold, "Over max value loss threshold" ); } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 797ab40cb9..7ea737bbd9 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -191,12 +191,12 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @notice Maximum value of loss the LP positions can incur before strategy shuts off rebalances * @param _maxLossThreshold Maximum amount in 18 decimals */ - function setMaxPositionValueLossThreshold(uint256 _maxLossThreshold) + function setMaxPositionValueLostThreshold(uint256 _maxLossThreshold) external onlyGovernorOrStrategist { - maxPositionValueLossThreshold = _maxLossThreshold; - emit MaxValueLossThresholdChanged(_maxLossThreshold); + maxPositionValueLostThreshold = _maxLossThreshold; + emit MaxValueLostThresholdChanged(_maxLossThreshold); } /** @@ -204,7 +204,8 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @dev Only governor can call it */ function resetLostValue() external onlyGovernor { - emit MaxValueLossThresholdChanged(netLostValue); + emit NetLossValueReset(msg.sender); + emit NetLostValueChanged(0); netLostValue = 0; } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 8d5bdd716c..eb2e3862d7 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -23,9 +23,22 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { event RebalancePauseStatusChanged(bool paused); event SwapsPauseStatusChanged(bool paused); event RebalancePriceThresholdChanged(int24 minTick, int24 maxTick); + event SwapPriceThresholdChanged( + int24 minTick, + uint160 minSwapPriceX96, + int24 maxTick, + uint160 maxSwapPriceX96 + ); event MaxTVLChanged(uint256 maxTVL); - event MaxValueLossThresholdChanged(uint256 amount); - event NetLossValueReset(uint256 lastValue); + event MaxValueLostThresholdChanged(uint256 amount); + event NetLossValueReset(address indexed _by); + event NetLostValueChanged(uint256 currentNetLostValue); + event PositionValueChanged( + uint256 indexed tokenId, + uint256 initialValue, + uint256 currentValue, + int256 delta + ); event AssetSwappedForRebalancing( address indexed tokenIn, address indexed tokenOut, @@ -59,19 +72,6 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint256 amount0, uint256 amount1 ); - event SwapPriceThresholdChanged( - int24 minTick, - uint160 minSwapPriceX96, - int24 maxTick, - uint160 maxSwapPriceX96 - ); - event PositionValueChanged( - uint256 indexed tokenId, - uint256 initialValue, - uint256 currentValue, - int256 delta - ); - event NetLostValueChanged(uint256 currentNetLostValue); // Represents a position minted by UniswapV3Strategy contract struct Position { @@ -126,7 +126,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint256 public netLostValue = 0; // Max value loss threshold after which rebalances aren't allowed - uint256 public maxPositionValueLossThreshold = 50000 ether; // default to 50k + uint256 public maxPositionValueLostThreshold = 50000 ether; // default to 50k // Uniswap V3's Pool IUniswapV3Pool public pool; diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js index 46d98c9ac5..9f3498acc6 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/049_uniswap_usdc_usdt_strategy.js @@ -186,7 +186,7 @@ module.exports = deploymentWithGovernanceProposal( // // 10. Set Max Loss threshold // { // contract: cUniV3_USDC_USDT_Strategy, - // signature: "setMaxPositionValueLossThreshold(uint256)", + // signature: "setMaxPositionValueLostThreshold(uint256)", // args: [utils.parseEther("50000", 18)], // 50k // }, // 11. Set Rebalance Price Threshold diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 0896c9246d..8d1fa1f4a2 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1316,7 +1316,7 @@ function uniswapV3FixtureSetup() { ); await UniV3_USDC_USDT_Strategy.connect( sGovernor - ).setMaxPositionValueLossThreshold(utils.parseUnits("50000", 18)); + ).setMaxPositionValueLostThreshold(utils.parseUnits("50000", 18)); UniV3_USDC_USDT_Strategy.connect(sGovernor).setMaxTVL( utils.parseUnits("2000000", 18) ); // 2M diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index e1beb789c6..08e8431761 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -74,10 +74,10 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { await strategy.connect(timelock).setMaxTVL(utils.parseUnits(maxTvl, 18)); } - async function setMaxPositionValueLossThreshold(maxLossThreshold) { + async function setMaxPositionValueLostThreshold(maxLossThreshold) { await strategy .connect(timelock) - .setMaxPositionValueLossThreshold(utils.parseUnits(maxLossThreshold, 18)); + .setMaxPositionValueLostThreshold(utils.parseUnits(maxLossThreshold, 18)); } describe("Uniswap V3 LP positions", function () { @@ -651,7 +651,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { await _swap(domen, "1000000", false); // Set threshold to a low value to see if it throws - await setMaxPositionValueLossThreshold("0.01"); + await setMaxPositionValueLostThreshold("0.01"); await expect( strategy .connect(operator) @@ -660,7 +660,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Set the threshold higher and make sure the net loss event is emitted // and state updated properly - await setMaxPositionValueLossThreshold("1000000000000000"); + await setMaxPositionValueLostThreshold("1000000000000000"); const { tx: increaseTx, amount0Added, diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index d6979d01f3..78a1396fcf 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -57,7 +57,7 @@ describe("Uniswap V3 Strategy", function () { await fixture.UniV3_USDC_USDT_Strategy.connect( governor - ).setMaxPositionValueLossThreshold(ousdUnits("50000", 18)); + ).setMaxPositionValueLostThreshold(ousdUnits("50000", 18)); await fixture.UniV3_USDC_USDT_Strategy.connect( governor @@ -359,9 +359,200 @@ describe("Uniswap V3 Strategy", function () { }); }); - describe.skip("setSwapPriceThreshold()", () => {}); + describe("setMaxTVL()", async () => { + it("Should change max TVL", async () => { + const newVal = ousdUnits("12345"); + const tx = await strategy.connect(governor).setMaxTVL(newVal); - describe.skip("setTokenPriceLimit()", () => {}); + await expect(tx).to.have.emittedEvent("MaxTVLChanged", [newVal]); + + expect(await strategy.maxTVL()).to.equal(newVal); + }); + + it("Should let Governor/Strategist set maxTVL", async () => { + for (const user of [governor, strategist]) { + await expect(strategy.connect(user).setMaxTVL(ousdUnits("12232"))).to + .not.be.reverted; + } + }); + + it("Should not let anyone else set maxTVL", async () => { + for (const user of [operator, matt]) { + await expect( + strategy.connect(user).setMaxTVL(ousdUnits("122332")) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + } + }); + }); + + describe("setMaxPositionValueLostThreshold()", () => { + it("Should change max loss threshold", async () => { + const newVal = ousdUnits("12345"); + const tx = await strategy + .connect(governor) + .setMaxPositionValueLostThreshold(newVal); + + await expect(tx).to.have.emittedEvent("MaxValueLostThresholdChanged", [ + newVal, + ]); + + expect(await strategy.maxPositionValueLostThreshold()).to.equal(newVal); + }); + + it("Should let Governor/Strategist set max loss threshold", async () => { + for (const user of [governor, strategist]) { + await expect( + strategy + .connect(user) + .setMaxPositionValueLostThreshold(ousdUnits("12232")) + ).to.not.be.reverted; + } + }); + + it("Should not let anyone else set the threshold", async () => { + for (const user of [operator, daniel]) { + await expect( + strategy + .connect(user) + .setMaxPositionValueLostThreshold(ousdUnits("122332")) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + } + }); + }); + + describe("resetLostValue()", () => { + it("Should let Governor reset the lost value ", async () => { + const tx = await strategy.connect(governor).resetLostValue(); + + await expect(tx).to.have.emittedEvent("NetLossValueReset", [ + governor.address, + ]); + + await expect(tx).to.have.emittedEvent("NetLostValueChanged", [0]); + }); + + it("Should not let anyone else set the threshold", async () => { + for (const user of [strategist, operator, franck]) { + await expect( + strategy.connect(user).resetLostValue() + ).to.be.revertedWith("Caller is not the Governor"); + } + }); + }); + + describe("setRebalancePriceThreshold()", () => { + it("Should change rebalance threshold", async () => { + const minTick = 111; + const maxTick = 222; + + const tx = await strategy + .connect(governor) + .setRebalancePriceThreshold(minTick, maxTick); + + await expect(tx).to.have.emittedEvent( + "RebalancePriceThresholdChanged", + [minTick, maxTick] + ); + + expect(await strategy.minRebalanceTick()).to.equal(minTick); + expect(await strategy.maxRebalanceTick()).to.equal(maxTick); + }); + + it("Should let Governor/Strategist set the threshold", async () => { + for (const user of [governor, strategist]) { + await expect( + strategy.connect(user).setRebalancePriceThreshold(111, 222) + ).to.not.be.reverted; + } + }); + + it("Should revert if tick indices are invalid", async () => { + await expect( + strategy.connect(governor).setRebalancePriceThreshold(222, 111) + ).to.be.revertedWith("Invalid threshold"); + }); + + it("Should not let anyone else set the threshold", async () => { + for (const user of [operator, josh]) { + await expect( + strategy.connect(user).setRebalancePriceThreshold(111, 222) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + } + }); + }); + + describe("setSwapPriceThreshold()", () => { + it("Should change swap threshold", async () => { + const minTick = 111; + const maxTick = 222; + const minTickX96 = await helper.getSqrtRatioAtTick(minTick); + const maxTickX96 = await helper.getSqrtRatioAtTick(maxTick); + + const tx = await strategy + .connect(governor) + .setSwapPriceThreshold(minTick, maxTick); + + await expect(tx).to.have.emittedEvent("SwapPriceThresholdChanged", [ + minTick, + minTickX96, + maxTick, + maxTickX96, + ]); + + expect(await strategy.minSwapPriceX96()).to.equal(minTickX96); + expect(await strategy.maxSwapPriceX96()).to.equal(maxTickX96); + }); + + it("Should let Governor/Strategist set the threshold", async () => { + for (const user of [governor, strategist]) { + await expect(strategy.connect(user).setSwapPriceThreshold(111, 222)) + .to.not.be.reverted; + } + }); + + it("Should revert if tick indices are invalid", async () => { + await expect( + strategy.connect(governor).setSwapPriceThreshold(222, 111) + ).to.be.revertedWith("Invalid threshold"); + }); + + it("Should not let anyone else set the threshold", async () => { + for (const user of [operator, josh]) { + await expect( + strategy.connect(user).setSwapPriceThreshold(111, 222) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + } + }); + }); + + describe("setLiquidityManagerImpl()", () => { + it("Should let Governor upgrade liquidity manager implementation ", async () => { + const tx = await strategy + .connect(governor) + .setLiquidityManagerImpl(mockStrategy2.address); + + await expect(tx).to.have.emittedEvent( + "LiquidityManagerImplementationUpgraded", + [mockStrategy2.address] + ); + }); + + it("Should throw if new implentation address is not a contract", async () => { + await expect( + strategy.connect(governor).setLiquidityManagerImpl(josh.address) + ).to.be.revertedWith("new implementation is not a contract"); + }); + + it("Should not let anyone else set the threshold", async () => { + for (const user of [strategist, operator, domen]) { + await expect( + strategy + .connect(user) + .setLiquidityManagerImpl(mockStrategy2.address) + ).to.be.revertedWith("Caller is not the Governor"); + } + }); + }); }); describe("LiquidityManager", function () { From b4115ee954fc3cd80dc8cec0305c81bafd1df3ea Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 20:07:59 +0530 Subject: [PATCH 51/83] More unit tests --- contracts/test/strategies/uniswap-v3.js | 34 ++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 78a1396fcf..1226303790 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -1441,11 +1441,43 @@ describe("Uniswap V3 Strategy", function () { }); }); - describe("Fees", () => { + describe("Fees & Balances", () => { beforeEach(async () => { _destructureFixture(await activePositionFixture()); }); + it("Should include active position in balances", async () => { + expect(await strategy.checkBalance(usdc.address)).to.approxEqual( + usdcUnits("100000") + ); + expect(await strategy.checkBalance(usdt.address)).to.approxEqual( + usdtUnits("100000") + ); + }); + + it("Should include pending fees in balances", async () => { + const balance0Before = await strategy.checkBalance(usdc.address); + const balance1Before = await strategy.checkBalance(usdt.address); + + // Set some fee + const expectedFee0 = usdcUnits("1234"); + const expectedFee1 = usdtUnits("5678"); + const tokenId = await strategy.activeTokenId(); + await mockPositionManager.setTokensOwed( + tokenId.toString(), + expectedFee0.toString(), + expectedFee1.toString() + ); + + expect(await strategy.checkBalance(usdc.address)).to.equal( + balance0Before.add(expectedFee0) + ); + + expect(await strategy.checkBalance(usdt.address)).to.equal( + balance1Before.add(expectedFee1) + ); + }); + it("Should accrue and collect fees", async () => { // No fee accrued yet let fees = await strategy.getPendingFees(); From a98aecb3b70f2c673d22f4a139a91d63eeafc284 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 20:14:30 +0530 Subject: [PATCH 52/83] fix async reverts --- .../test/strategies/uniswap-v3.fork-test.js | 4 ++-- contracts/test/strategies/uniswap-v3.js | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 08e8431761..0fe28b777c 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -587,8 +587,8 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Check balance on strategy const usdcBalAfter = await strategy.checkBalance(usdc.address); const usdtBalAfter = await strategy.checkBalance(usdt.address); - expect(strategy).to.have.an.approxBalanceOf(usdcBalAfter, usdc); - expect(strategy).to.have.an.approxBalanceOf(usdtBalAfter, usdt); + await expect(strategy).to.have.an.approxBalanceOf(usdcBalAfter, usdc); + await expect(strategy).to.have.an.approxBalanceOf(usdtBalAfter, usdt); }); it("Should collect fees", async () => { diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 1226303790..20f8858a28 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -773,7 +773,7 @@ describe("Uniswap V3 Strategy", function () { } }); it("Should revert if caller isn't Governor/Strategist/Operator", async () => { - expect( + await expect( strategy .connect(matt) .rebalance("1", "1", "0", "0", "0", "0", "-1", "1") @@ -784,7 +784,7 @@ describe("Uniswap V3 Strategy", function () { it("Should revert if rebalance is paused", async () => { await strategy.connect(governor).setRebalancePaused(true); - expect( + await expect( strategy .connect(operator) .rebalance("1", "1", "0", "0", "0", "0", "-1", "1") @@ -793,7 +793,7 @@ describe("Uniswap V3 Strategy", function () { it("Should revert if out of rebalance limits", async () => { await strategy.connect(governor).setRebalancePriceThreshold(-100, 200); - expect( + await expect( strategy .connect(operator) .rebalance("1", "1", "0", "0", "0", "0", "-200", "1") @@ -803,7 +803,7 @@ describe("Uniswap V3 Strategy", function () { it("Should revert if TVL check fails", async () => { await strategy.connect(governor).setMaxTVL(ousdUnits("100000")); - expect( + await expect( strategy .connect(operator) .rebalance( @@ -823,7 +823,7 @@ describe("Uniswap V3 Strategy", function () { const reserve0 = await reserveStrategy.checkBalance(usdt.address); const reserve1 = await reserveStrategy.checkBalance(usdc.address); - expect( + await expect( strategy .connect(operator) .rebalance( @@ -840,7 +840,7 @@ describe("Uniswap V3 Strategy", function () { }); it("Should revert if tick range is invalid", async () => { - expect( + await expect( strategy .connect(operator) .rebalance( @@ -964,7 +964,7 @@ describe("Uniswap V3 Strategy", function () { }); it("Should revert if no active position", async () => { - expect( + await expect( strategy .connect(operator) .increaseActivePositionLiquidity("1", "1", "0", "0") @@ -980,7 +980,7 @@ describe("Uniswap V3 Strategy", function () { }); await strategy.connect(governor).setRebalancePaused(true); - expect( + await expect( strategy .connect(operator) .increaseActivePositionLiquidity("1", "1", "0", "0") @@ -998,7 +998,7 @@ describe("Uniswap V3 Strategy", function () { const reserve0 = await reserveStrategy.checkBalance(usdt.address); const reserve1 = await reserveStrategy.checkBalance(usdc.address); - expect( + await expect( strategy .connect(operator) .increaseActivePositionLiquidity( @@ -1020,7 +1020,7 @@ describe("Uniswap V3 Strategy", function () { await strategy.connect(governor).setMaxTVL(ousdUnits("100000")); - expect( + await expect( strategy .connect(operator) .increaseActivePositionLiquidity("1", "1", "0", "0") @@ -1507,7 +1507,7 @@ describe("Uniswap V3 Strategy", function () { it("Should allow Governor/Strategist/Operator to collect fees", async () => { for (const user of [governor, strategist, operator]) { - expect(strategy.connect(user).collectFees()).to.not.be.reverted; + await expect(strategy.connect(user).collectFees()).to.not.be.reverted; } }); From d9b0a69f17cd77bfe27987b0f75578bcfab0f7b8 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 21:02:52 +0530 Subject: [PATCH 53/83] Add comment about slippage and fix failing unit test --- .../strategies/uniswap/UniswapV3LiquidityManager.sol | 7 +++++++ contracts/test/strategies/uniswap-v3.js | 12 +++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index c3d3bf55be..fc6dcba975 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -138,6 +138,13 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { uint256 minAmount1 ) = _calculateLiquidityToWithdraw(position, asset, amount); + // NOTE: The minAmount is calculated using the current pool price. + // It can be tilted and a large amount OUSD can be redeemed to make the strategy + // liquidate positions on the tilted pool. + // However, we don't plan on making this the default strategy. So, this method + // would never be invoked on prod. + // TODO: Should we still a slippage (just in case it becomes a default strategy in future)? + // Liquidiate active position _decreasePositionLiquidity( position.tokenId, diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 20f8858a28..9edebdff24 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -964,6 +964,9 @@ describe("Uniswap V3 Strategy", function () { }); it("Should revert if no active position", async () => { + await strategy + .connect(operator) + .closePosition(await strategy.activeTokenId(), 0, 0); await expect( strategy .connect(operator) @@ -1447,12 +1450,11 @@ describe("Uniswap V3 Strategy", function () { }); it("Should include active position in balances", async () => { - expect(await strategy.checkBalance(usdc.address)).to.approxEqual( - usdcUnits("100000") - ); - expect(await strategy.checkBalance(usdt.address)).to.approxEqual( - usdtUnits("100000") + const netBalance = (await strategy.checkBalance(usdc.address)).add( + await strategy.checkBalance(usdt.address) ); + + expect(netBalance).to.approxEqualTolerance(usdcUnits("200000"), 3); }); it("Should include pending fees in balances", async () => { From 70aa199ce0725d8a829d5d1adaebf7be3574b094 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 Mar 2023 21:21:20 +0530 Subject: [PATCH 54/83] Fix failing fork test --- contracts/test/_fixture.js | 11 ++++++++++- contracts/test/helpers.js | 8 ++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 8d1fa1f4a2..1a38015b04 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -125,7 +125,16 @@ async function defaultFixture() { ).interface.format("full"), ...( await ethers.getContractFactory("UniswapV3LiquidityManager") - ).interface.format("full"), + ).interface + .format("full") + .filter((x) => { + return !( + x.startsWith("function checkBalance(") || + x.startsWith("function supportsAsset(") || + x.startsWith("function deposit(") || + x.startsWith("function withdraw(") + ); + }), ]) ), UniV3_USDC_USDT_Proxy.address diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index 2bbba4ee21..e4d2d04ff5 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -62,7 +62,9 @@ chai.Assertion.addMethod( const user = this._obj; const address = user.address || user.getAddress(); // supports contracts too const actual = await contract.balanceOf(address); - expected = parseUnits(expected, await decimalsFor(contract)); + if (!BigNumber.isBigNumber(expected)) { + expected = parseUnits(expected, await decimalsFor(contract)); + } chai.expect(actual).to.approxEqual(expected, message); } ); @@ -88,7 +90,9 @@ chai.Assertion.addMethod( const user = this._obj; const address = user.address || user.getAddress(); // supports contracts too const actual = await contract.balanceOf(address); - expected = parseUnits(expected, await decimalsFor(contract)); + if (!BigNumber.isBigNumber(expected)) { + expected = parseUnits(expected, await decimalsFor(contract)); + } chai.expect(actual).to.equal(expected, message); } ); From 2ddb8d0bdbe9e80467308ffe9cce63fe220f03a5 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 20 Mar 2023 11:54:55 +0100 Subject: [PATCH 55/83] add fork test for withdrawing when netLoss barrier is reached --- .../test/strategies/uniswap-v3.fork-test.js | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 0fe28b777c..b7f4b73422 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -245,7 +245,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { zeroForOne ? usdt.address : usdc.address, // tokenOut 100, // fee user.address, // recipient - (await getBlockTimestamp()) + 5, // deadline + (await getBlockTimestamp()) + 150, // deadline swapAmount, // amountIn 0, // amountOutMinimum sqrtPriceLimitX96, @@ -684,6 +684,42 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { .sub(netLost) ); }); + + it("Should be allowed to close position when net loss threshold is breached", async () => { + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick; + const upperTick = activeTick + 1; + + // Mint position + const amount = "100000"; + const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = + await mintLiquidity(lowerTick, upperTick, amount, amount); + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + let storedPosition = await strategy.tokenIdToPosition(tokenId); + expect(storedPosition.exists).to.be.true; + expect(await strategy.activeTokenId()).to.equal(tokenId); + expect(await strategy.netLostValue()).to.equal(0); + + // Do some big swaps to move active tick + await _swap(matt, "1000000", false); + await _swap(josh, "1000000", false); + await _swap(franck, "1000000", false); + await _swap(daniel, "1000000", false); + await _swap(domen, "1000000", false); + + // Set threshold to a low value to see if it throws + await setMaxPositionValueLostThreshold("0.01"); + await expect( + strategy + .connect(operator) + .increaseActivePositionLiquidity("1000000", "1000000", "0", "0") + ).to.be.revertedWith("Over max value loss threshold"); + + // should still be allowed to close the position + strategy + .connect(operator) + .closePosition(tokenId, amount0Minted * 0.98, amount1Minted * 0.98) + }); }); describe("Sanity checks", () => { From df7f8e3bb2637d41aaceb3f919c6ca3d48185628 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 20 Mar 2023 14:10:15 +0100 Subject: [PATCH 56/83] add test --- .../test/strategies/uniswap-v3.fork-test.js | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index b7f4b73422..c3b25f63f2 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -237,15 +237,16 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const sqrtPriceLimitX96 = await v3Helper.getSqrtRatioAtTick( activeTick + (zeroForOne ? -2 : 2) ); - const swapAmount = BigNumber.from(amount).mul(10 ** 6); + const swapAmount = BigNumber.from(amount).mul(10e6); usdc.connect(user).approve(swapRouter.address, swapAmount.mul(10)); usdt.connect(user).approve(swapRouter.address, swapAmount.mul(10)); + console.log("ATTEMPTING TO SWAP", sqrtPriceLimitX96.toString()) await swapRouter.connect(user).exactInputSingle([ zeroForOne ? usdc.address : usdt.address, // tokenIn zeroForOne ? usdt.address : usdc.address, // tokenOut 100, // fee user.address, // recipient - (await getBlockTimestamp()) + 150, // deadline + (await getBlockTimestamp()) + 10, // deadline swapAmount, // amountIn 0, // amountOutMinimum sqrtPriceLimitX96, @@ -692,7 +693,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Mint position const amount = "100000"; - const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = + const { tokenId, tx, amount0Minted, amount1Minted } = await mintLiquidity(lowerTick, upperTick, amount, amount); await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); let storedPosition = await strategy.tokenIdToPosition(tokenId); @@ -718,7 +719,38 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // should still be allowed to close the position strategy .connect(operator) - .closePosition(tokenId, amount0Minted * 0.98, amount1Minted * 0.98) + .closePosition(tokenId, Math.round(amount0Minted * 0.98), Math.round(amount1Minted * 0.98)) + }); + + it("netLostValue will catch possible pool tilts", async () => { + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick; + const upperTick = activeTick + 1; + let drainLoops = 10 + console.log("Starting loop") + while (drainLoops > 0) { + console.log("Beginning of loop") + // Mint position + // Do some big swaps to move active tick + console.log("DEBUG MATT USDT: ", (await usdt.balanceOf(matt.address)).toString()) + console.log("DEBUG MATT USDC: ", (await usdc.balanceOf(matt.address)).toString()) + console.log("DEBUG FRANCK USDT: ", (await usdt.balanceOf(franck.address)).toString()) + console.log("DEBUG FRANCK USDC: ", (await usdc.balanceOf(franck.address)).toString()) + await _swap(matt, "700000", false); + await _swap(franck, "700000", false); + + console.log("pre-swaps done") + const amount = "100000"; + const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = + await mintLiquidity(lowerTick, upperTick, amount, amount); + await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + + console.log("netLostValue", (await strategy.netLostValue()).toString() ) + await _swap(matt, "600000", true); + console.log("worked for matt") + await _swap(franck, "600000", true); + console.log("post-swaps done") + } }); }); From 99c6238d7c8ee90a6c15e4c5e2adcf409603a2b5 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 20 Mar 2023 15:27:34 +0100 Subject: [PATCH 57/83] fix some tests --- .../test/strategies/uniswap-v3.fork-test.js | 92 ++++++++++--------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index c3b25f63f2..afec59c560 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -238,9 +238,10 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { activeTick + (zeroForOne ? -2 : 2) ); const swapAmount = BigNumber.from(amount).mul(10e6); - usdc.connect(user).approve(swapRouter.address, swapAmount.mul(10)); - usdt.connect(user).approve(swapRouter.address, swapAmount.mul(10)); - console.log("ATTEMPTING TO SWAP", sqrtPriceLimitX96.toString()) + usdc.connect(user).approve(swapRouter.address, 0); + usdt.connect(user).approve(swapRouter.address, 0); + usdc.connect(user).approve(swapRouter.address, swapAmount.mul(1e3)); + usdt.connect(user).approve(swapRouter.address, swapAmount.mul(1e3)); await swapRouter.connect(user).exactInputSingle([ zeroForOne ? usdc.address : usdt.address, // tokenIn zeroForOne ? usdt.address : usdc.address, // tokenOut @@ -645,11 +646,11 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(await strategy.netLostValue()).to.equal(0); // Do some big swaps to move active tick - await _swap(matt, "1000000", false); - await _swap(josh, "1000000", false); - await _swap(franck, "1000000", false); - await _swap(daniel, "1000000", false); - await _swap(domen, "1000000", false); + await _swap(matt, "100000", false); + await _swap(josh, "100000", false); + await _swap(franck, "100000", false); + await _swap(daniel, "100000", false); + await _swap(domen, "100000", false); // Set threshold to a low value to see if it throws await setMaxPositionValueLostThreshold("0.01"); @@ -702,11 +703,11 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(await strategy.netLostValue()).to.equal(0); // Do some big swaps to move active tick - await _swap(matt, "1000000", false); - await _swap(josh, "1000000", false); - await _swap(franck, "1000000", false); - await _swap(daniel, "1000000", false); - await _swap(domen, "1000000", false); + await _swap(matt, "100000", false); + await _swap(josh, "100000", false); + await _swap(franck, "100000", false); + await _swap(daniel, "100000", false); + await _swap(domen, "100000", false); // Set threshold to a low value to see if it throws await setMaxPositionValueLostThreshold("0.01"); @@ -719,39 +720,42 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // should still be allowed to close the position strategy .connect(operator) - .closePosition(tokenId, Math.round(amount0Minted * 0.98), Math.round(amount1Minted * 0.98)) + .closePosition(tokenId, Math.round(amount0Minted * 0.92), Math.round(amount1Minted * 0.92)) }); - it("netLostValue will catch possible pool tilts", async () => { - const [, activeTick] = await pool.slot0(); - const lowerTick = activeTick; - const upperTick = activeTick + 1; - let drainLoops = 10 - console.log("Starting loop") - while (drainLoops > 0) { - console.log("Beginning of loop") - // Mint position - // Do some big swaps to move active tick - console.log("DEBUG MATT USDT: ", (await usdt.balanceOf(matt.address)).toString()) - console.log("DEBUG MATT USDC: ", (await usdc.balanceOf(matt.address)).toString()) - console.log("DEBUG FRANCK USDT: ", (await usdt.balanceOf(franck.address)).toString()) - console.log("DEBUG FRANCK USDC: ", (await usdc.balanceOf(franck.address)).toString()) - await _swap(matt, "700000", false); - await _swap(franck, "700000", false); - - console.log("pre-swaps done") - const amount = "100000"; - const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = - await mintLiquidity(lowerTick, upperTick, amount, amount); - await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); - - console.log("netLostValue", (await strategy.netLostValue()).toString() ) - await _swap(matt, "600000", true); - console.log("worked for matt") - await _swap(franck, "600000", true); - console.log("post-swaps done") - } - }); +// it.only("netLostValue will catch possible pool tilts", async () => { +// const [, activeTick] = await pool.slot0(); +// const lowerTick = activeTick; +// const upperTick = activeTick + 1; +// let drainLoops = 10 +// console.log("Starting loop") +// while (drainLoops > 0) { +// console.log("Beginning of loop") +// // Mint position +// // Do some big swaps to move active tick +// console.log("DEBUG daniel USDT: ", (await usdt.balanceOf(daniel.address)).toString()) +// console.log("DEBUG daniel USDC: ", (await usdc.balanceOf(daniel.address)).toString()) +// console.log("DEBUG josh USDT: ", (await usdt.balanceOf(josh.address)).toString()) +// console.log("DEBUG josh USDC: ", (await usdc.balanceOf(josh.address)).toString()) +// await _swap(daniel, "100000", false); +// console.log("WHAT?") +// await _swap(josh, "100000", false); +// +// console.log("pre-swaps done") +// const amount = "10000"; +// const { tx, tokenId } = +// await mintLiquidity(lowerTick, upperTick, amount, amount); +// //await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); +// +// console.log("netLostValue", (await strategy.netLostValue()).toString() ) +// await _swap(daniel, "200000", true); +// await _swap(josh, "200000", true); +// console.log("post-swaps done") +// +// await strategy.connect(operator).closePosition(tokenId, 0, 0); +// console.log("close position done") +// } +// }); }); describe("Sanity checks", () => { From c40a955949751cfa61ba7f1a8885dbf1f2f8fb93 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 20 Mar 2023 17:03:07 +0100 Subject: [PATCH 58/83] intermittent commit --- .../test/strategies/uniswap-v3.fork-test.js | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index afec59c560..a390a2b3d1 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -723,39 +723,39 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { .closePosition(tokenId, Math.round(amount0Minted * 0.92), Math.round(amount1Minted * 0.92)) }); -// it.only("netLostValue will catch possible pool tilts", async () => { -// const [, activeTick] = await pool.slot0(); -// const lowerTick = activeTick; -// const upperTick = activeTick + 1; -// let drainLoops = 10 -// console.log("Starting loop") -// while (drainLoops > 0) { -// console.log("Beginning of loop") -// // Mint position -// // Do some big swaps to move active tick -// console.log("DEBUG daniel USDT: ", (await usdt.balanceOf(daniel.address)).toString()) -// console.log("DEBUG daniel USDC: ", (await usdc.balanceOf(daniel.address)).toString()) -// console.log("DEBUG josh USDT: ", (await usdt.balanceOf(josh.address)).toString()) -// console.log("DEBUG josh USDC: ", (await usdc.balanceOf(josh.address)).toString()) -// await _swap(daniel, "100000", false); -// console.log("WHAT?") -// await _swap(josh, "100000", false); -// -// console.log("pre-swaps done") -// const amount = "10000"; -// const { tx, tokenId } = -// await mintLiquidity(lowerTick, upperTick, amount, amount); -// //await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); -// -// console.log("netLostValue", (await strategy.netLostValue()).toString() ) -// await _swap(daniel, "200000", true); -// await _swap(josh, "200000", true); -// console.log("post-swaps done") -// -// await strategy.connect(operator).closePosition(tokenId, 0, 0); -// console.log("close position done") -// } -// }); + it.only("netLostValue will catch possible pool tilts", async () => { + const [, activeTick] = await pool.slot0(); + const lowerTick = activeTick; + const upperTick = activeTick + 1; + let drainLoops = 10 + console.log("Starting loop") + while (drainLoops > 0) { + console.log("Beginning of loop") + // Mint position + // Do some big swaps to move active tick + console.log("DEBUG daniel USDT: ", (await usdt.balanceOf(daniel.address)).toString()) + console.log("DEBUG daniel USDC: ", (await usdc.balanceOf(daniel.address)).toString()) + console.log("DEBUG josh USDT: ", (await usdt.balanceOf(josh.address)).toString()) + console.log("DEBUG josh USDC: ", (await usdc.balanceOf(josh.address)).toString()) + await _swap(daniel, "100000", false); + console.log("WHAT?") + await _swap(josh, "100000", false); + + console.log("pre-swaps done") + const amount = "10000"; + const { tx, tokenId } = + await mintLiquidity(lowerTick, upperTick, amount, amount); + //await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + + console.log("netLostValue", (await strategy.netLostValue()).toString() ) + await _swap(daniel, "100000", true); + await _swap(josh, "100000", true); + console.log("post-swaps done") + + await strategy.connect(operator).closePosition(tokenId, 0, 0); + console.log("close position done") + } + }); }); describe("Sanity checks", () => { From 4b2e2c6c649412a38c8fee3bce7ed64c2f381117 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 20 Mar 2023 23:50:03 +0100 Subject: [PATCH 59/83] fix test --- .../test/strategies/uniswap-v3.fork-test.js | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index a390a2b3d1..a79d76a556 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -237,7 +237,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const sqrtPriceLimitX96 = await v3Helper.getSqrtRatioAtTick( activeTick + (zeroForOne ? -2 : 2) ); - const swapAmount = BigNumber.from(amount).mul(10e6); + const swapAmount = BigNumber.from(amount).mul(1e6); usdc.connect(user).approve(swapRouter.address, 0); usdt.connect(user).approve(swapRouter.address, 0); usdc.connect(user).approve(swapRouter.address, swapAmount.mul(1e3)); @@ -723,37 +723,29 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { .closePosition(tokenId, Math.round(amount0Minted * 0.92), Math.round(amount1Minted * 0.92)) }); - it.only("netLostValue will catch possible pool tilts", async () => { + it("netLostValue will catch possible pool tilts", async () => { const [, activeTick] = await pool.slot0(); const lowerTick = activeTick; const upperTick = activeTick + 1; let drainLoops = 10 - console.log("Starting loop") while (drainLoops > 0) { - console.log("Beginning of loop") + const amount = "10000"; + await mintLiquidity(lowerTick, upperTick, amount, amount); // Mint position // Do some big swaps to move active tick - console.log("DEBUG daniel USDT: ", (await usdt.balanceOf(daniel.address)).toString()) - console.log("DEBUG daniel USDC: ", (await usdc.balanceOf(daniel.address)).toString()) - console.log("DEBUG josh USDT: ", (await usdt.balanceOf(josh.address)).toString()) - console.log("DEBUG josh USDC: ", (await usdc.balanceOf(josh.address)).toString()) - await _swap(daniel, "100000", false); - console.log("WHAT?") - await _swap(josh, "100000", false); - - console.log("pre-swaps done") - const amount = "10000"; - const { tx, tokenId } = - await mintLiquidity(lowerTick, upperTick, amount, amount); - //await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); + await _swap(daniel, "500000", false); + await _swap(josh, "500000", false); - console.log("netLostValue", (await strategy.netLostValue()).toString() ) - await _swap(daniel, "100000", true); - await _swap(josh, "100000", true); - console.log("post-swaps done") + await mintLiquidity(lowerTick, upperTick, amount, amount); + + expect(await strategy.netLostValue()).gte( + 0, + "Expected netLostValue to be greater than 0" + ); - await strategy.connect(operator).closePosition(tokenId, 0, 0); - console.log("close position done") + await _swap(daniel, "500000", true); + await _swap(josh, "500000", true); + drainLoops--; } }); }); From 52ef747305ca7276d22f59e0146c56348f545d5e Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:00:10 +0400 Subject: [PATCH 60/83] Change deployment number --- contracts/contracts/proxies/Proxies.sol | 2 +- ...tegy.js => 051_uniswap_usdc_usdt_strategy.js} | 2 +- .../test/strategies/uniswap-v3.fork-test.js | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) rename contracts/deploy/{049_uniswap_usdc_usdt_strategy.js => 051_uniswap_usdc_usdt_strategy.js} (99%) diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 6f51b0fe9a..4c28e1b6e9 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -98,7 +98,7 @@ contract MorphoAaveStrategyProxy is InitializeGovernedUpgradeabilityProxy { * @notice UniV3_USDC_USDT_Proxy delegates calls to a UniswapV3Strategy implementation */ contract UniV3_USDC_USDT_Proxy is InitializeGovernedUpgradeabilityProxy { - + } /** diff --git a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js b/contracts/deploy/051_uniswap_usdc_usdt_strategy.js similarity index 99% rename from contracts/deploy/049_uniswap_usdc_usdt_strategy.js rename to contracts/deploy/051_uniswap_usdc_usdt_strategy.js index 9f3498acc6..737963771e 100644 --- a/contracts/deploy/049_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/051_uniswap_usdc_usdt_strategy.js @@ -3,7 +3,7 @@ const { utils } = require("ethers"); module.exports = deploymentWithGovernanceProposal( { - deployName: "049_uniswap_usdc_usdt_strategy", + deployName: "051_uniswap_usdc_usdt_strategy", forceDeploy: false, }, async ({ diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index a79d76a556..9f2e9bcc74 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -694,8 +694,12 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Mint position const amount = "100000"; - const { tokenId, tx, amount0Minted, amount1Minted } = - await mintLiquidity(lowerTick, upperTick, amount, amount); + const { tokenId, tx, amount0Minted, amount1Minted } = await mintLiquidity( + lowerTick, + upperTick, + amount, + amount + ); await expect(tx).to.have.emittedEvent("UniswapV3PositionMinted"); let storedPosition = await strategy.tokenIdToPosition(tokenId); expect(storedPosition.exists).to.be.true; @@ -720,14 +724,18 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // should still be allowed to close the position strategy .connect(operator) - .closePosition(tokenId, Math.round(amount0Minted * 0.92), Math.round(amount1Minted * 0.92)) + .closePosition( + tokenId, + Math.round(amount0Minted * 0.92), + Math.round(amount1Minted * 0.92) + ); }); it("netLostValue will catch possible pool tilts", async () => { const [, activeTick] = await pool.slot0(); const lowerTick = activeTick; const upperTick = activeTick + 1; - let drainLoops = 10 + let drainLoops = 10; while (drainLoops > 0) { const amount = "10000"; await mintLiquidity(lowerTick, upperTick, amount, amount); From 6684429ca4d7032510ea52fb97bac5b63b148106 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 10 Apr 2023 17:55:29 +0400 Subject: [PATCH 61/83] Change reserve to Morpho Aave --- contracts/deploy/051_uniswap_usdc_usdt_strategy.js | 8 ++++---- contracts/test/strategies/uniswap-v3.fork-test.js | 2 +- dapp/abis/Flipper.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/deploy/051_uniswap_usdc_usdt_strategy.js b/contracts/deploy/051_uniswap_usdc_usdt_strategy.js index 737963771e..4196783e7e 100644 --- a/contracts/deploy/051_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/051_uniswap_usdc_usdt_strategy.js @@ -57,8 +57,8 @@ module.exports = deploymentWithGovernanceProposal( dUniV3_USDC_USDT_Proxy.address ); - const cMorphoCompProxy = await ethers.getContract( - "MorphoCompoundStrategyProxy" + const cMorphoAaveProxy = await ethers.getContract( + "MorphoAaveStrategyProxy" ); const cHarvesterProxy = await ethers.getContract("HarvesterProxy"); @@ -157,13 +157,13 @@ module.exports = deploymentWithGovernanceProposal( { contract: cUniV3_USDC_USDT_Strategy, signature: "setReserveStrategy(address,address)", - args: [assetAddresses.USDC, cMorphoCompProxy.address], + args: [assetAddresses.USDC, cMorphoAaveProxy.address], }, // 6. Set Reserve Strategy for USDT { contract: cUniV3_USDC_USDT_Strategy, signature: "setReserveStrategy(address,address)", - args: [assetAddresses.USDT, cMorphoCompProxy.address], + args: [assetAddresses.USDT, cMorphoAaveProxy.address], }, // 7. Set Minimum Deposit threshold for USDC { diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 9f2e9bcc74..9fb035054d 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -40,7 +40,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { beforeEach(async () => { fixture = await uniswapV3Fixture(); - reserveStrategy = fixture.morphoCompoundStrategy; + reserveStrategy = fixture.morphoAaveStrategy; strategy = fixture.UniV3_USDC_USDT_Strategy; pool = fixture.UniV3_USDC_USDT_Pool; positionManager = fixture.UniV3PositionManager; diff --git a/dapp/abis/Flipper.json b/dapp/abis/Flipper.json index 2e1d8b249f..955ce8085f 100644 --- a/dapp/abis/Flipper.json +++ b/dapp/abis/Flipper.json @@ -224,8 +224,8 @@ "type": "function" } ], - "bytecode": "0x6101006040523480156200001257600080fd5b50604051620018ba380380620018ba833981016040819052620000359162000132565b6200004d336000805160206200189a83398151915255565b6000805160206200189a833981519152546040516001600160a01b03909116906000907fc7c0c772add429241571afb3805861fb3cfa2af374534088b76cdb4325a87e9a908290a36001600160a01b038416620000a957600080fd5b6001600160a01b038316620000bd57600080fd5b6001600160a01b038216620000d157600080fd5b6001600160a01b038116620000e557600080fd5b6001600160601b0319606094851b811660805292841b831660a05290831b821660c05290911b1660e0526200018f565b80516001600160a01b03811681146200012d57600080fd5b919050565b600080600080608085870312156200014957600080fd5b620001548562000115565b9350620001646020860162000115565b9250620001746040860162000115565b9150620001846060860162000115565b905092959194509250565b60805160601c60a05160601c60c05160601c60e05160601c61165c6200023e6000396000818161021d015281816107ed0152818161087b0152610d920152600081816108d00152818161095c01528181610b1a0152610c370152600081816102bd015281816104b40152818161070c0152818161079801528181610aad01528181610e3a01526110260152600081816103cb0152818161062b015281816106b701526109d0015261165c6000f3fe608060405234801561001057600080fd5b50600436106100cf5760003560e01c8063bfc11ffd1161008c578063cb93905311610066578063cb93905314610182578063d38bfff414610195578063f3fef3a3146101a8578063f51b0fd4146101bb57600080fd5b8063bfc11ffd14610144578063c6b6816914610157578063c7af33521461016a57600080fd5b80630c340a24146100d457806335aa0b96146100f95780635981c7461461010e5780635d36b19014610121578063853828b6146101295780638a095a0f14610131575b600080fd5b6100dc6101c3565b6040516001600160a01b0390911681526020015b60405180910390f35b61010c610107366004611486565b6101e0565b005b61010c61011c366004611486565b61038a565b61010c6104eb565b61010c610591565b61010c61013f366004611486565b61098a565b61010c610152366004611486565b610ae6565b61010c610165366004611486565b610c03565b610172610d2d565b60405190151581526020016100f0565b61010c610190366004611486565b610d5e565b61010c6101a336600461141f565b610e75565b61010c6101b636600461143a565b610f19565b61010c610fb8565b60006101db6000805160206116078339815191525490565b905090565b69054b40b1f852bda000008111156102135760405162461bcd60e51b815260040161020a90611562565b60405180910390fd5b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000166323b872dd333061025364e8d4a51000866115b4565b6040518463ffffffff1660e01b8152600401610271939291906114d4565b600060405180830381600087803b15801561028b57600080fd5b505af115801561029f573d6000803e3d6000fd5b505060405163a9059cbb60e01b8152336004820152602481018490527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316925063a9059cbb91506044015b602060405180830381600087803b15801561030c57600080fd5b505af1158015610320573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103449190611464565b6103875760405162461bcd60e51b815260206004820152601460248201527313d554d1081d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b50565b69054b40b1f852bda000008111156103b45760405162461bcd60e51b815260040161020a90611562565b6040516323b872dd60e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906323b872dd90610404903390309086906004016114d4565b602060405180830381600087803b15801561041e57600080fd5b505af1158015610432573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104569190611464565b6104985760405162461bcd60e51b8152602060048201526013602482015272111052481d1c985b9cd9995c8819985a5b1959606a1b604482015260640161020a565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016102f2565b7f44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db546001600160a01b0316336001600160a01b0316146105865760405162461bcd60e51b815260206004820152603060248201527f4f6e6c79207468652070656e64696e6720476f7665726e6f722063616e20636f60448201526f6d706c6574652074686520636c61696d60801b606482015260840161020a565b61058f3361109f565b565b610599610d2d565b6105b55760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535805460028114156105f95760405162461bcd60e51b815260040161020a9061158c565b600282556106de6106166000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561067557600080fd5b505afa158015610689573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106ad919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6107bf6106f76000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561075657600080fd5b505afa15801561076a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061078e919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6108a26107d86000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a0823190602401602060405180830381600087803b15801561083957600080fd5b505af115801561084d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610871919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6109836108bb6000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561091a57600080fd5b505afa15801561092e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610952919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b5060019055565b69054b40b1f852bda000008111156109b45760405162461bcd60e51b815260040161020a90611562565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb90604401602060405180830381600087803b158015610a1c57600080fd5b505af1158015610a30573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a549190611464565b610a965760405162461bcd60e51b8152602060048201526013602482015272111052481d1c985b9cd9995c8819985a5b1959606a1b604482015260640161020a565b6040516323b872dd60e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906323b872dd906102f2903390309086906004016114d4565b69054b40b1f852bda00000811115610b105760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000166323b872dd3330610b5064e8d4a51000866115b4565b6040518463ffffffff1660e01b8152600401610b6e939291906114d4565b602060405180830381600087803b158015610b8857600080fd5b505af1158015610b9c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610bc09190611464565b6104985760405162461bcd60e51b81526020600482015260146024820152731554d110c81d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b69054b40b1f852bda00000811115610c2d5760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001663a9059cbb33610c6c64e8d4a51000856115b4565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401602060405180830381600087803b158015610cb257600080fd5b505af1158015610cc6573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cea9190611464565b610a965760405162461bcd60e51b81526020600482015260146024820152731554d110c81d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b6000610d456000805160206116078339815191525490565b6001600160a01b0316336001600160a01b031614905090565b69054b40b1f852bda00000811115610d885760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001663a9059cbb33610dc764e8d4a51000856115b4565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401600060405180830381600087803b158015610e0d57600080fd5b505af1158015610e21573d6000803e3d6000fd5b50506040516323b872dd60e01b81526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001692506323b872dd91506102f2903390309086906004016114d4565b610e7d610d2d565b610e995760405162461bcd60e51b815260040161020a9061152b565b610ec1817f44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db55565b806001600160a01b0316610ee16000805160206116078339815191525490565b6001600160a01b03167fa39cc5eb22d0f34d8beaefee8a3f17cc229c1a1d1ef87a5ad47313487b1c4f0d60405160405180910390a350565b610f21610d2d565b610f3d5760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac453580546002811415610f815760405162461bcd60e51b815260040161020a9061158c565b60028255610faf610f9e6000805160206116078339815191525490565b6001600160a01b0386169085611160565b50600190555050565b610fc0610d2d565b610fdc5760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535805460028114156110205760405162461bcd60e51b815260040161020a9061158c565b600282557f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f51b0fd46040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561107f57600080fd5b505af1158015611093573d6000803e3d6000fd5b50505050600182555050565b6001600160a01b0381166110f55760405162461bcd60e51b815260206004820152601a60248201527f4e657720476f7665726e6f722069732061646472657373283029000000000000604482015260640161020a565b806001600160a01b03166111156000805160206116078339815191525490565b6001600160a01b03167fc7c0c772add429241571afb3805861fb3cfa2af374534088b76cdb4325a87e9a60405160405180910390a36103878160008051602061160783398151915255565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526111b29084906111b7565b505050565b600061120c826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b03166112899092919063ffffffff16565b8051909150156111b2578080602001905181019061122a9190611464565b6111b25760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b606482015260840161020a565b606061129884846000856112a2565b90505b9392505050565b6060824710156113035760405162461bcd60e51b815260206004820152602660248201527f416464726573733a20696e73756666696369656e742062616c616e636520666f6044820152651c8818d85b1b60d21b606482015260840161020a565b843b6113515760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161020a565b600080866001600160a01b0316858760405161136d91906114b8565b60006040518083038185875af1925050503d80600081146113aa576040519150601f19603f3d011682016040523d82523d6000602084013e6113af565b606091505b50915091506113bf8282866113ca565b979650505050505050565b606083156113d957508161129b565b8251156113e95782518084602001fd5b8160405162461bcd60e51b815260040161020a91906114f8565b80356001600160a01b038116811461141a57600080fd5b919050565b60006020828403121561143157600080fd5b61129b82611403565b6000806040838503121561144d57600080fd5b61145683611403565b946020939093013593505050565b60006020828403121561147657600080fd5b8151801515811461129b57600080fd5b60006020828403121561149857600080fd5b5035919050565b6000602082840312156114b157600080fd5b5051919050565b600082516114ca8184602087016115d6565b9190910192915050565b6001600160a01b039384168152919092166020820152604081019190915260600190565b60208152600082518060208401526115178160408501602087016115d6565b601f01601f19169190910160400192915050565b6020808252601a908201527f43616c6c6572206973206e6f742074686520476f7665726e6f72000000000000604082015260600190565b60208082526010908201526f416d6f756e7420746f6f206c6172676560801b604082015260600190565b6020808252600e908201526d1499595b9d1c985b9d0818d85b1b60921b604082015260600190565b6000826115d157634e487b7160e01b600052601260045260246000fd5b500490565b60005b838110156115f15781810151838201526020016115d9565b83811115611600576000848401525b5050505056fe7bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4aa2646970667358221220b6bce11e20b239058951a0a661a143b4b69e509ce56e8e3e14707a6ae0a00c9564736f6c634300080700337bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4a", - "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100cf5760003560e01c8063bfc11ffd1161008c578063cb93905311610066578063cb93905314610182578063d38bfff414610195578063f3fef3a3146101a8578063f51b0fd4146101bb57600080fd5b8063bfc11ffd14610144578063c6b6816914610157578063c7af33521461016a57600080fd5b80630c340a24146100d457806335aa0b96146100f95780635981c7461461010e5780635d36b19014610121578063853828b6146101295780638a095a0f14610131575b600080fd5b6100dc6101c3565b6040516001600160a01b0390911681526020015b60405180910390f35b61010c610107366004611486565b6101e0565b005b61010c61011c366004611486565b61038a565b61010c6104eb565b61010c610591565b61010c61013f366004611486565b61098a565b61010c610152366004611486565b610ae6565b61010c610165366004611486565b610c03565b610172610d2d565b60405190151581526020016100f0565b61010c610190366004611486565b610d5e565b61010c6101a336600461141f565b610e75565b61010c6101b636600461143a565b610f19565b61010c610fb8565b60006101db6000805160206116078339815191525490565b905090565b69054b40b1f852bda000008111156102135760405162461bcd60e51b815260040161020a90611562565b60405180910390fd5b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000166323b872dd333061025364e8d4a51000866115b4565b6040518463ffffffff1660e01b8152600401610271939291906114d4565b600060405180830381600087803b15801561028b57600080fd5b505af115801561029f573d6000803e3d6000fd5b505060405163a9059cbb60e01b8152336004820152602481018490527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316925063a9059cbb91506044015b602060405180830381600087803b15801561030c57600080fd5b505af1158015610320573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103449190611464565b6103875760405162461bcd60e51b815260206004820152601460248201527313d554d1081d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b50565b69054b40b1f852bda000008111156103b45760405162461bcd60e51b815260040161020a90611562565b6040516323b872dd60e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906323b872dd90610404903390309086906004016114d4565b602060405180830381600087803b15801561041e57600080fd5b505af1158015610432573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104569190611464565b6104985760405162461bcd60e51b8152602060048201526013602482015272111052481d1c985b9cd9995c8819985a5b1959606a1b604482015260640161020a565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016102f2565b7f44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db546001600160a01b0316336001600160a01b0316146105865760405162461bcd60e51b815260206004820152603060248201527f4f6e6c79207468652070656e64696e6720476f7665726e6f722063616e20636f60448201526f6d706c6574652074686520636c61696d60801b606482015260840161020a565b61058f3361109f565b565b610599610d2d565b6105b55760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535805460028114156105f95760405162461bcd60e51b815260040161020a9061158c565b600282556106de6106166000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561067557600080fd5b505afa158015610689573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106ad919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6107bf6106f76000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561075657600080fd5b505afa15801561076a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061078e919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6108a26107d86000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a0823190602401602060405180830381600087803b15801561083957600080fd5b505af115801561084d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610871919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6109836108bb6000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561091a57600080fd5b505afa15801561092e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610952919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b5060019055565b69054b40b1f852bda000008111156109b45760405162461bcd60e51b815260040161020a90611562565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb90604401602060405180830381600087803b158015610a1c57600080fd5b505af1158015610a30573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a549190611464565b610a965760405162461bcd60e51b8152602060048201526013602482015272111052481d1c985b9cd9995c8819985a5b1959606a1b604482015260640161020a565b6040516323b872dd60e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906323b872dd906102f2903390309086906004016114d4565b69054b40b1f852bda00000811115610b105760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000166323b872dd3330610b5064e8d4a51000866115b4565b6040518463ffffffff1660e01b8152600401610b6e939291906114d4565b602060405180830381600087803b158015610b8857600080fd5b505af1158015610b9c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610bc09190611464565b6104985760405162461bcd60e51b81526020600482015260146024820152731554d110c81d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b69054b40b1f852bda00000811115610c2d5760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001663a9059cbb33610c6c64e8d4a51000856115b4565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401602060405180830381600087803b158015610cb257600080fd5b505af1158015610cc6573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cea9190611464565b610a965760405162461bcd60e51b81526020600482015260146024820152731554d110c81d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b6000610d456000805160206116078339815191525490565b6001600160a01b0316336001600160a01b031614905090565b69054b40b1f852bda00000811115610d885760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001663a9059cbb33610dc764e8d4a51000856115b4565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401600060405180830381600087803b158015610e0d57600080fd5b505af1158015610e21573d6000803e3d6000fd5b50506040516323b872dd60e01b81526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001692506323b872dd91506102f2903390309086906004016114d4565b610e7d610d2d565b610e995760405162461bcd60e51b815260040161020a9061152b565b610ec1817f44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db55565b806001600160a01b0316610ee16000805160206116078339815191525490565b6001600160a01b03167fa39cc5eb22d0f34d8beaefee8a3f17cc229c1a1d1ef87a5ad47313487b1c4f0d60405160405180910390a350565b610f21610d2d565b610f3d5760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac453580546002811415610f815760405162461bcd60e51b815260040161020a9061158c565b60028255610faf610f9e6000805160206116078339815191525490565b6001600160a01b0386169085611160565b50600190555050565b610fc0610d2d565b610fdc5760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535805460028114156110205760405162461bcd60e51b815260040161020a9061158c565b600282557f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f51b0fd46040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561107f57600080fd5b505af1158015611093573d6000803e3d6000fd5b50505050600182555050565b6001600160a01b0381166110f55760405162461bcd60e51b815260206004820152601a60248201527f4e657720476f7665726e6f722069732061646472657373283029000000000000604482015260640161020a565b806001600160a01b03166111156000805160206116078339815191525490565b6001600160a01b03167fc7c0c772add429241571afb3805861fb3cfa2af374534088b76cdb4325a87e9a60405160405180910390a36103878160008051602061160783398151915255565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526111b29084906111b7565b505050565b600061120c826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b03166112899092919063ffffffff16565b8051909150156111b2578080602001905181019061122a9190611464565b6111b25760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b606482015260840161020a565b606061129884846000856112a2565b90505b9392505050565b6060824710156113035760405162461bcd60e51b815260206004820152602660248201527f416464726573733a20696e73756666696369656e742062616c616e636520666f6044820152651c8818d85b1b60d21b606482015260840161020a565b843b6113515760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161020a565b600080866001600160a01b0316858760405161136d91906114b8565b60006040518083038185875af1925050503d80600081146113aa576040519150601f19603f3d011682016040523d82523d6000602084013e6113af565b606091505b50915091506113bf8282866113ca565b979650505050505050565b606083156113d957508161129b565b8251156113e95782518084602001fd5b8160405162461bcd60e51b815260040161020a91906114f8565b80356001600160a01b038116811461141a57600080fd5b919050565b60006020828403121561143157600080fd5b61129b82611403565b6000806040838503121561144d57600080fd5b61145683611403565b946020939093013593505050565b60006020828403121561147657600080fd5b8151801515811461129b57600080fd5b60006020828403121561149857600080fd5b5035919050565b6000602082840312156114b157600080fd5b5051919050565b600082516114ca8184602087016115d6565b9190910192915050565b6001600160a01b039384168152919092166020820152604081019190915260600190565b60208152600082518060208401526115178160408501602087016115d6565b601f01601f19169190910160400192915050565b6020808252601a908201527f43616c6c6572206973206e6f742074686520476f7665726e6f72000000000000604082015260600190565b60208082526010908201526f416d6f756e7420746f6f206c6172676560801b604082015260600190565b6020808252600e908201526d1499595b9d1c985b9d0818d85b1b60921b604082015260600190565b6000826115d157634e487b7160e01b600052601260045260246000fd5b500490565b60005b838110156115f15781810151838201526020016115d9565b83811115611600576000848401525b5050505056fe7bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4aa2646970667358221220b6bce11e20b239058951a0a661a143b4b69e509ce56e8e3e14707a6ae0a00c9564736f6c63430008070033", + "bytecode": "0x6101006040523480156200001257600080fd5b50604051620018ba380380620018ba833981016040819052620000359162000132565b6200004d336000805160206200189a83398151915255565b6000805160206200189a833981519152546040516001600160a01b03909116906000907fc7c0c772add429241571afb3805861fb3cfa2af374534088b76cdb4325a87e9a908290a36001600160a01b038416620000a957600080fd5b6001600160a01b038316620000bd57600080fd5b6001600160a01b038216620000d157600080fd5b6001600160a01b038116620000e557600080fd5b6001600160601b0319606094851b811660805292841b831660a05290831b821660c05290911b1660e0526200018f565b80516001600160a01b03811681146200012d57600080fd5b919050565b600080600080608085870312156200014957600080fd5b620001548562000115565b9350620001646020860162000115565b9250620001746040860162000115565b9150620001846060860162000115565b905092959194509250565b60805160601c60a05160601c60c05160601c60e05160601c61165c6200023e6000396000818161021d015281816107ed0152818161087b0152610d920152600081816108d00152818161095c01528181610b1a0152610c370152600081816102bd015281816104b40152818161070c0152818161079801528181610aad01528181610e3a01526110260152600081816103cb0152818161062b015281816106b701526109d0015261165c6000f3fe608060405234801561001057600080fd5b50600436106100cf5760003560e01c8063bfc11ffd1161008c578063cb93905311610066578063cb93905314610182578063d38bfff414610195578063f3fef3a3146101a8578063f51b0fd4146101bb57600080fd5b8063bfc11ffd14610144578063c6b6816914610157578063c7af33521461016a57600080fd5b80630c340a24146100d457806335aa0b96146100f95780635981c7461461010e5780635d36b19014610121578063853828b6146101295780638a095a0f14610131575b600080fd5b6100dc6101c3565b6040516001600160a01b0390911681526020015b60405180910390f35b61010c610107366004611486565b6101e0565b005b61010c61011c366004611486565b61038a565b61010c6104eb565b61010c610591565b61010c61013f366004611486565b61098a565b61010c610152366004611486565b610ae6565b61010c610165366004611486565b610c03565b610172610d2d565b60405190151581526020016100f0565b61010c610190366004611486565b610d5e565b61010c6101a336600461141f565b610e75565b61010c6101b636600461143a565b610f19565b61010c610fb8565b60006101db6000805160206116078339815191525490565b905090565b69054b40b1f852bda000008111156102135760405162461bcd60e51b815260040161020a90611562565b60405180910390fd5b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000166323b872dd333061025364e8d4a51000866115b4565b6040518463ffffffff1660e01b8152600401610271939291906114d4565b600060405180830381600087803b15801561028b57600080fd5b505af115801561029f573d6000803e3d6000fd5b505060405163a9059cbb60e01b8152336004820152602481018490527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316925063a9059cbb91506044015b602060405180830381600087803b15801561030c57600080fd5b505af1158015610320573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103449190611464565b6103875760405162461bcd60e51b815260206004820152601460248201527313d554d1081d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b50565b69054b40b1f852bda000008111156103b45760405162461bcd60e51b815260040161020a90611562565b6040516323b872dd60e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906323b872dd90610404903390309086906004016114d4565b602060405180830381600087803b15801561041e57600080fd5b505af1158015610432573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104569190611464565b6104985760405162461bcd60e51b8152602060048201526013602482015272111052481d1c985b9cd9995c8819985a5b1959606a1b604482015260640161020a565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016102f2565b7f44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db546001600160a01b0316336001600160a01b0316146105865760405162461bcd60e51b815260206004820152603060248201527f4f6e6c79207468652070656e64696e6720476f7665726e6f722063616e20636f60448201526f6d706c6574652074686520636c61696d60801b606482015260840161020a565b61058f3361109f565b565b610599610d2d565b6105b55760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535805460028114156105f95760405162461bcd60e51b815260040161020a9061158c565b600282556106de6106166000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561067557600080fd5b505afa158015610689573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106ad919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6107bf6106f76000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561075657600080fd5b505afa15801561076a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061078e919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6108a26107d86000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a0823190602401602060405180830381600087803b15801561083957600080fd5b505af115801561084d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610871919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6109836108bb6000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561091a57600080fd5b505afa15801561092e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610952919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b5060019055565b69054b40b1f852bda000008111156109b45760405162461bcd60e51b815260040161020a90611562565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb90604401602060405180830381600087803b158015610a1c57600080fd5b505af1158015610a30573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a549190611464565b610a965760405162461bcd60e51b8152602060048201526013602482015272111052481d1c985b9cd9995c8819985a5b1959606a1b604482015260640161020a565b6040516323b872dd60e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906323b872dd906102f2903390309086906004016114d4565b69054b40b1f852bda00000811115610b105760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000166323b872dd3330610b5064e8d4a51000866115b4565b6040518463ffffffff1660e01b8152600401610b6e939291906114d4565b602060405180830381600087803b158015610b8857600080fd5b505af1158015610b9c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610bc09190611464565b6104985760405162461bcd60e51b81526020600482015260146024820152731554d110c81d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b69054b40b1f852bda00000811115610c2d5760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001663a9059cbb33610c6c64e8d4a51000856115b4565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401602060405180830381600087803b158015610cb257600080fd5b505af1158015610cc6573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cea9190611464565b610a965760405162461bcd60e51b81526020600482015260146024820152731554d110c81d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b6000610d456000805160206116078339815191525490565b6001600160a01b0316336001600160a01b031614905090565b69054b40b1f852bda00000811115610d885760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001663a9059cbb33610dc764e8d4a51000856115b4565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401600060405180830381600087803b158015610e0d57600080fd5b505af1158015610e21573d6000803e3d6000fd5b50506040516323b872dd60e01b81526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001692506323b872dd91506102f2903390309086906004016114d4565b610e7d610d2d565b610e995760405162461bcd60e51b815260040161020a9061152b565b610ec1817f44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db55565b806001600160a01b0316610ee16000805160206116078339815191525490565b6001600160a01b03167fa39cc5eb22d0f34d8beaefee8a3f17cc229c1a1d1ef87a5ad47313487b1c4f0d60405160405180910390a350565b610f21610d2d565b610f3d5760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac453580546002811415610f815760405162461bcd60e51b815260040161020a9061158c565b60028255610faf610f9e6000805160206116078339815191525490565b6001600160a01b0386169085611160565b50600190555050565b610fc0610d2d565b610fdc5760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535805460028114156110205760405162461bcd60e51b815260040161020a9061158c565b600282557f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f51b0fd46040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561107f57600080fd5b505af1158015611093573d6000803e3d6000fd5b50505050600182555050565b6001600160a01b0381166110f55760405162461bcd60e51b815260206004820152601a60248201527f4e657720476f7665726e6f722069732061646472657373283029000000000000604482015260640161020a565b806001600160a01b03166111156000805160206116078339815191525490565b6001600160a01b03167fc7c0c772add429241571afb3805861fb3cfa2af374534088b76cdb4325a87e9a60405160405180910390a36103878160008051602061160783398151915255565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526111b29084906111b7565b505050565b600061120c826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b03166112899092919063ffffffff16565b8051909150156111b2578080602001905181019061122a9190611464565b6111b25760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b606482015260840161020a565b606061129884846000856112a2565b90505b9392505050565b6060824710156113035760405162461bcd60e51b815260206004820152602660248201527f416464726573733a20696e73756666696369656e742062616c616e636520666f6044820152651c8818d85b1b60d21b606482015260840161020a565b843b6113515760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161020a565b600080866001600160a01b0316858760405161136d91906114b8565b60006040518083038185875af1925050503d80600081146113aa576040519150601f19603f3d011682016040523d82523d6000602084013e6113af565b606091505b50915091506113bf8282866113ca565b979650505050505050565b606083156113d957508161129b565b8251156113e95782518084602001fd5b8160405162461bcd60e51b815260040161020a91906114f8565b80356001600160a01b038116811461141a57600080fd5b919050565b60006020828403121561143157600080fd5b61129b82611403565b6000806040838503121561144d57600080fd5b61145683611403565b946020939093013593505050565b60006020828403121561147657600080fd5b8151801515811461129b57600080fd5b60006020828403121561149857600080fd5b5035919050565b6000602082840312156114b157600080fd5b5051919050565b600082516114ca8184602087016115d6565b9190910192915050565b6001600160a01b039384168152919092166020820152604081019190915260600190565b60208152600082518060208401526115178160408501602087016115d6565b601f01601f19169190910160400192915050565b6020808252601a908201527f43616c6c6572206973206e6f742074686520476f7665726e6f72000000000000604082015260600190565b60208082526010908201526f416d6f756e7420746f6f206c6172676560801b604082015260600190565b6020808252600e908201526d1499595b9d1c985b9d0818d85b1b60921b604082015260600190565b6000826115d157634e487b7160e01b600052601260045260246000fd5b500490565b60005b838110156115f15781810151838201526020016115d9565b83811115611600576000848401525b5050505056fe7bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4aa264697066735822122077ae78e24a7b4a83cf33f0ba337a0085e6319972c844d438cd486ecdcf0ffbed64736f6c634300080700337bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4a", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100cf5760003560e01c8063bfc11ffd1161008c578063cb93905311610066578063cb93905314610182578063d38bfff414610195578063f3fef3a3146101a8578063f51b0fd4146101bb57600080fd5b8063bfc11ffd14610144578063c6b6816914610157578063c7af33521461016a57600080fd5b80630c340a24146100d457806335aa0b96146100f95780635981c7461461010e5780635d36b19014610121578063853828b6146101295780638a095a0f14610131575b600080fd5b6100dc6101c3565b6040516001600160a01b0390911681526020015b60405180910390f35b61010c610107366004611486565b6101e0565b005b61010c61011c366004611486565b61038a565b61010c6104eb565b61010c610591565b61010c61013f366004611486565b61098a565b61010c610152366004611486565b610ae6565b61010c610165366004611486565b610c03565b610172610d2d565b60405190151581526020016100f0565b61010c610190366004611486565b610d5e565b61010c6101a336600461141f565b610e75565b61010c6101b636600461143a565b610f19565b61010c610fb8565b60006101db6000805160206116078339815191525490565b905090565b69054b40b1f852bda000008111156102135760405162461bcd60e51b815260040161020a90611562565b60405180910390fd5b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000166323b872dd333061025364e8d4a51000866115b4565b6040518463ffffffff1660e01b8152600401610271939291906114d4565b600060405180830381600087803b15801561028b57600080fd5b505af115801561029f573d6000803e3d6000fd5b505060405163a9059cbb60e01b8152336004820152602481018490527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316925063a9059cbb91506044015b602060405180830381600087803b15801561030c57600080fd5b505af1158015610320573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103449190611464565b6103875760405162461bcd60e51b815260206004820152601460248201527313d554d1081d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b50565b69054b40b1f852bda000008111156103b45760405162461bcd60e51b815260040161020a90611562565b6040516323b872dd60e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906323b872dd90610404903390309086906004016114d4565b602060405180830381600087803b15801561041e57600080fd5b505af1158015610432573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104569190611464565b6104985760405162461bcd60e51b8152602060048201526013602482015272111052481d1c985b9cd9995c8819985a5b1959606a1b604482015260640161020a565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016102f2565b7f44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db546001600160a01b0316336001600160a01b0316146105865760405162461bcd60e51b815260206004820152603060248201527f4f6e6c79207468652070656e64696e6720476f7665726e6f722063616e20636f60448201526f6d706c6574652074686520636c61696d60801b606482015260840161020a565b61058f3361109f565b565b610599610d2d565b6105b55760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535805460028114156105f95760405162461bcd60e51b815260040161020a9061158c565b600282556106de6106166000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561067557600080fd5b505afa158015610689573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106ad919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6107bf6106f76000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561075657600080fd5b505afa15801561076a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061078e919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6108a26107d86000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a0823190602401602060405180830381600087803b15801561083957600080fd5b505af115801561084d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610871919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b6109836108bb6000805160206116078339815191525490565b6040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a082319060240160206040518083038186803b15801561091a57600080fd5b505afa15801561092e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610952919061149f565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611160565b5060019055565b69054b40b1f852bda000008111156109b45760405162461bcd60e51b815260040161020a90611562565b60405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb90604401602060405180830381600087803b158015610a1c57600080fd5b505af1158015610a30573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a549190611464565b610a965760405162461bcd60e51b8152602060048201526013602482015272111052481d1c985b9cd9995c8819985a5b1959606a1b604482015260640161020a565b6040516323b872dd60e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906323b872dd906102f2903390309086906004016114d4565b69054b40b1f852bda00000811115610b105760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000166323b872dd3330610b5064e8d4a51000866115b4565b6040518463ffffffff1660e01b8152600401610b6e939291906114d4565b602060405180830381600087803b158015610b8857600080fd5b505af1158015610b9c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610bc09190611464565b6104985760405162461bcd60e51b81526020600482015260146024820152731554d110c81d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b69054b40b1f852bda00000811115610c2d5760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001663a9059cbb33610c6c64e8d4a51000856115b4565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401602060405180830381600087803b158015610cb257600080fd5b505af1158015610cc6573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cea9190611464565b610a965760405162461bcd60e51b81526020600482015260146024820152731554d110c81d1c985b9cd9995c8819985a5b195960621b604482015260640161020a565b6000610d456000805160206116078339815191525490565b6001600160a01b0316336001600160a01b031614905090565b69054b40b1f852bda00000811115610d885760405162461bcd60e51b815260040161020a90611562565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001663a9059cbb33610dc764e8d4a51000856115b4565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401600060405180830381600087803b158015610e0d57600080fd5b505af1158015610e21573d6000803e3d6000fd5b50506040516323b872dd60e01b81526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001692506323b872dd91506102f2903390309086906004016114d4565b610e7d610d2d565b610e995760405162461bcd60e51b815260040161020a9061152b565b610ec1817f44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db55565b806001600160a01b0316610ee16000805160206116078339815191525490565b6001600160a01b03167fa39cc5eb22d0f34d8beaefee8a3f17cc229c1a1d1ef87a5ad47313487b1c4f0d60405160405180910390a350565b610f21610d2d565b610f3d5760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac453580546002811415610f815760405162461bcd60e51b815260040161020a9061158c565b60028255610faf610f9e6000805160206116078339815191525490565b6001600160a01b0386169085611160565b50600190555050565b610fc0610d2d565b610fdc5760405162461bcd60e51b815260040161020a9061152b565b7f53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535805460028114156110205760405162461bcd60e51b815260040161020a9061158c565b600282557f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f51b0fd46040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561107f57600080fd5b505af1158015611093573d6000803e3d6000fd5b50505050600182555050565b6001600160a01b0381166110f55760405162461bcd60e51b815260206004820152601a60248201527f4e657720476f7665726e6f722069732061646472657373283029000000000000604482015260640161020a565b806001600160a01b03166111156000805160206116078339815191525490565b6001600160a01b03167fc7c0c772add429241571afb3805861fb3cfa2af374534088b76cdb4325a87e9a60405160405180910390a36103878160008051602061160783398151915255565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526111b29084906111b7565b505050565b600061120c826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b03166112899092919063ffffffff16565b8051909150156111b2578080602001905181019061122a9190611464565b6111b25760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b606482015260840161020a565b606061129884846000856112a2565b90505b9392505050565b6060824710156113035760405162461bcd60e51b815260206004820152602660248201527f416464726573733a20696e73756666696369656e742062616c616e636520666f6044820152651c8818d85b1b60d21b606482015260840161020a565b843b6113515760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161020a565b600080866001600160a01b0316858760405161136d91906114b8565b60006040518083038185875af1925050503d80600081146113aa576040519150601f19603f3d011682016040523d82523d6000602084013e6113af565b606091505b50915091506113bf8282866113ca565b979650505050505050565b606083156113d957508161129b565b8251156113e95782518084602001fd5b8160405162461bcd60e51b815260040161020a91906114f8565b80356001600160a01b038116811461141a57600080fd5b919050565b60006020828403121561143157600080fd5b61129b82611403565b6000806040838503121561144d57600080fd5b61145683611403565b946020939093013593505050565b60006020828403121561147657600080fd5b8151801515811461129b57600080fd5b60006020828403121561149857600080fd5b5035919050565b6000602082840312156114b157600080fd5b5051919050565b600082516114ca8184602087016115d6565b9190910192915050565b6001600160a01b039384168152919092166020820152604081019190915260600190565b60208152600082518060208401526115178160408501602087016115d6565b601f01601f19169190910160400192915050565b6020808252601a908201527f43616c6c6572206973206e6f742074686520476f7665726e6f72000000000000604082015260600190565b60208082526010908201526f416d6f756e7420746f6f206c6172676560801b604082015260600190565b6020808252600e908201526d1499595b9d1c985b9d0818d85b1b60921b604082015260600190565b6000826115d157634e487b7160e01b600052601260045260246000fd5b500490565b60005b838110156115f15781810151838201526020016115d9565b83811115611600576000848401525b5050505056fe7bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4aa264697066735822122077ae78e24a7b4a83cf33f0ba337a0085e6319972c844d438cd486ecdcf0ffbed64736f6c63430008070033", "linkReferences": {}, "deployedLinkReferences": {} } From 3c13846abea6a79b39ea8f09e684c0dc4802f6a3 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 10 Apr 2023 18:09:19 +0400 Subject: [PATCH 62/83] Fix fork tests --- contracts/test/strategies/uniswap-v3.fork-test.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 9fb035054d..837d7d21f0 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -722,13 +722,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { ).to.be.revertedWith("Over max value loss threshold"); // should still be allowed to close the position - strategy - .connect(operator) - .closePosition( - tokenId, - Math.round(amount0Minted * 0.92), - Math.round(amount1Minted * 0.92) - ); + strategy.connect(operator).closePosition(tokenId, "0", "0"); }); it("netLostValue will catch possible pool tilts", async () => { From 778c35f6e0f2cde45896f0c00c8ceed501f2f96c Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 10 Apr 2023 18:09:37 +0400 Subject: [PATCH 63/83] Lint --- contracts/test/strategies/uniswap-v3.fork-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 837d7d21f0..8b28981c62 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -694,7 +694,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // Mint position const amount = "100000"; - const { tokenId, tx, amount0Minted, amount1Minted } = await mintLiquidity( + const { tokenId, tx } = await mintLiquidity( lowerTick, upperTick, amount, From 289a6dc37b0937cf5d6de36299bd25e9d64068d7 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:35:31 +0400 Subject: [PATCH 64/83] UniswapV3 strategy - M02 & M01 (#1284) * Ensure Uniswap V3 is never added as default strategy * Cleanup and update fork tests * Update unit tests * Add few more unit tests * Update revert message --- .../uniswap/UniswapV3LiquidityManager.sol | 102 ------------------ .../strategies/uniswap/UniswapV3Strategy.sol | 50 ++++----- contracts/contracts/vault/VaultAdmin.sol | 4 + contracts/contracts/vault/VaultCore.sol | 33 ++---- contracts/test/_fixture.js | 12 +-- .../test/strategies/uniswap-v3.fork-test.js | 18 ---- contracts/test/strategies/uniswap-v3.js | 64 +++++------ 7 files changed, 61 insertions(+), 222 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index fc6dcba975..9f19caad35 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -53,107 +53,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { value += amount1.scaleBy(18, Helpers.getDecimals(token1)); } - /*************************************** - Withdraw - ****************************************/ - /** - * @notice Calculates the amount liquidity that needs to be removed - * to Withdraw specified amount of the given asset. - * - * @param position Position object - * @param asset Token needed - * @param amount Minimum amount to liquidate - * - * @return liquidity Liquidity to burn - * @return minAmount0 Minimum amount0 to expect - * @return minAmount1 Minimum amount1 to expect - */ - function _calculateLiquidityToWithdraw( - Position memory position, - address asset, - uint256 amount - ) - internal - view - returns ( - uint128 liquidity, - uint256 minAmount0, - uint256 minAmount1 - ) - { - (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); - - // Total amount in Liquidity pools - (uint256 totalAmount0, uint256 totalAmount1) = helper - .getAmountsForLiquidity( - sqrtRatioX96, - position.sqrtRatioAX96, - position.sqrtRatioBX96, - position.liquidity - ); - - if (asset == token0) { - minAmount0 = amount; - minAmount1 = totalAmount1 / (totalAmount0 / amount); - liquidity = helper.getLiquidityForAmounts( - sqrtRatioX96, - position.sqrtRatioAX96, - position.sqrtRatioBX96, - amount, - minAmount1 - ); - } else if (asset == token1) { - minAmount0 = totalAmount0 / (totalAmount1 / amount); - minAmount1 = amount; - liquidity = helper.getLiquidityForAmounts( - sqrtRatioX96, - position.sqrtRatioAX96, - position.sqrtRatioBX96, - minAmount0, - amount - ); - } - } - - /** - * @notice Liquidiates active position to remove required amount of give asset - * @dev Doesn't have non-Reentrant modifier since it's supposed to be delegatecalled - * only from `UniswapV3Strategy.withdraw` which already has a nonReentrant check - * and the storage is shared between these two contract. - * - * @param asset Asset address - * @param amount Min amount of token to receive - */ - function withdrawAssetFromActivePositionOnlyVault( - address asset, - uint256 amount - ) external onlyVault { - Position memory position = tokenIdToPosition[activeTokenId]; - require(position.exists && position.liquidity > 0, "Liquidity error"); - - // Figure out liquidity to burn - ( - uint128 liquidity, - uint256 minAmount0, - uint256 minAmount1 - ) = _calculateLiquidityToWithdraw(position, asset, amount); - - // NOTE: The minAmount is calculated using the current pool price. - // It can be tilted and a large amount OUSD can be redeemed to make the strategy - // liquidate positions on the tilted pool. - // However, we don't plan on making this the default strategy. So, this method - // would never be invoked on prod. - // TODO: Should we still a slippage (just in case it becomes a default strategy in future)? - - // Liquidiate active position - _decreasePositionLiquidity( - position.tokenId, - liquidity, - minAmount0, - minAmount1 - ); - } - /*************************************** Rebalance ****************************************/ @@ -820,7 +719,6 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { { // Since this is called by the Vault, we cannot pass min redeem amounts // without complicating the code of the Vault. So, passing 0 instead. - // A better way return _closePosition(activeTokenId, 0, 0); // Intentionally skipping TVL check since removing liquidity won't cause it to fail diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 7ea737bbd9..8de92ef8ca 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -249,27 +249,30 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { ****************************************/ /// @inheritdoc InitializableAbstractStrategy - function deposit(address _asset, uint256 _amount) + function deposit(address, uint256) external override onlyVault nonReentrant { - onlyPoolTokens(_asset); - - if ( - _asset == token0 - ? (_amount > minDepositThreshold0) - : (_amount > minDepositThreshold1) - ) { - IVault(vaultAddress).depositToUniswapV3Reserve(_asset, _amount); - // Not emitting Deposit event since the Reserve strategy would do so - } + /** + * Uniswap V3 strategies are never meant to be default strategies. + * By design, they cannot hold any funds in the contract. When it needs + * funds to provide liquidity, it'll pull the required amounts from the + * reserve strategies. So, route any deposits to the reserve strategies + */ + revert("Direct deposits disabled on UniswapV3Strategy"); } /// @inheritdoc InitializableAbstractStrategy function depositAll() external override onlyVault nonReentrant { - _depositAll(); + /** + * Uniswap V3 strategies are never meant to be default strategies. + * By design, they cannot hold any funds in the contract. When it needs + * funds to provide liquidity, it'll pull the required amounts from the + * reserve strategies. So, route any deposits to the reserve strategies + */ + revert("Direct deposits disabled on UniswapV3Strategy"); } /// @inheritdoc InitializableAbstractStrategy @@ -280,27 +283,10 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { ) external override onlyVault nonReentrant { onlyPoolTokens(_asset); - IERC20 asset = IERC20(_asset); - uint256 selfBalance = asset.balanceOf(address(this)); - - if (selfBalance < amount) { - require(activeTokenId > 0, "Liquidity error"); - - // Delegatecall to `UniswapV3LiquidityManager` to remove - // liquidity from active LP position - // solhint-disable-next-line no-unused-vars - (bool success, bytes memory data) = address(_self).delegatecall( - abi.encodeWithSignature( - "withdrawAssetFromActivePositionOnlyVault(address,uint256)", - _asset, - amount - selfBalance - ) - ); - require(success, "DelegateCall to close position failed"); - } + require(activeTokenId == 0, "Active position still open"); - // Transfer requested amount - asset.safeTransfer(recipient, amount); + // Transfer requested amount, will revert when low on balance + IERC20(_asset).safeTransfer(recipient, amount); emit Withdrawal(_asset, _asset, amount); } diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index c4636782c3..c283e4418b 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -133,6 +133,10 @@ contract VaultAdmin is VaultStorage { if (_strategy != address(0)) { // Make sure the strategy meets some criteria require(strategies[_strategy].isSupported, "Strategy not approved"); + require( + !strategies[_strategy].isUniswapV3Strategy, + "UniswapV3Strategy not supported" + ); IStrategy strategy = IStrategy(_strategy); require(assets[_asset].isSupported, "Asset is not supported"); require( diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index aca7a4cef3..04423e2acf 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -206,8 +206,11 @@ contract VaultCore is VaultStorage { require(strategyAddr != address(0), "Liquidity error"); // Nothing in Vault, but something in Strategy, send from there - IStrategy strategy = IStrategy(strategyAddr); - strategy.withdraw(msg.sender, assetAddr, outputs[i]); + IStrategy(strategyAddr).withdraw( + msg.sender, + assetAddr, + outputs[i] + ); } } @@ -351,31 +354,7 @@ contract VaultCore is VaultStorage { address depositStrategyAddr = assetDefaultStrategies[assetAddr]; if (depositStrategyAddr != address(0) && allocateAmount > 0) { - IStrategy strategy; - - // `strategies` is initialized in `VaultAdmin` - // slither-disable-next-line uninitialized-state - if (strategies[depositStrategyAddr].isUniswapV3Strategy) { - address reserveStrategyAddr = IUniswapV3Strategy( - depositStrategyAddr - ).reserveStrategy(assetAddr); - - // Defensive check to make sure the address(0) or unsupported strategy - // isn't returned by `IUniswapV3Strategy.reserveStrategy()` - - // `strategies` is initialized in `VaultAdmin`. - // slither-disable-start uninitialized-state - require( - strategies[reserveStrategyAddr].isSupported, - "Invalid reserve strategy for asset" - ); - // slither-disable-end uninitialized-state - - // For Uniswap V3 Strategies, always deposit to reserve strategies - strategy = IStrategy(reserveStrategyAddr); - } else { - strategy = IStrategy(depositStrategyAddr); - } + IStrategy strategy = IStrategy(depositStrategyAddr); // Transfer asset to Strategy and call deposit method to // mint or take required action diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 1a38015b04..60e850addd 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1278,15 +1278,11 @@ function uniswapV3FixtureSetup() { // Approve Uniswap V3 Strategy await _approveStrategy(fixture, UniV3_USDC_USDT_Strategy, true); - } - - // Change default strategy to Uniswap V3 for both USDT and USDC - await _setDefaultStrategy(fixture, usdc, UniV3_USDC_USDT_Strategy); - await _setDefaultStrategy(fixture, usdt, UniV3_USDC_USDT_Strategy); - // await UniV3_USDC_USDT_Strategy.setSwapPriceThreshold(-1000, 1000); + // Change default strategy to reserve strategies for both USDT and USDC + await _setDefaultStrategy(fixture, usdc, mockStrategy); + await _setDefaultStrategy(fixture, usdt, mockStrategy); - if (!isFork) { // And a different one for DAI await _setDefaultStrategy(fixture, dai, mockStrategyDAI); @@ -1301,6 +1297,8 @@ function uniswapV3FixtureSetup() { ); } + // await UniV3_USDC_USDT_Strategy.setSwapPriceThreshold(-1000, 1000); + const { governorAddr, timelockAddr } = await getNamedAccounts(); const sGovernor = await ethers.provider.getSigner( isFork ? timelockAddr : governorAddr diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 8b28981c62..5ef49f2921 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -761,9 +761,6 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const currentSupply = await ousd.totalSupply(); const ousdBalance = await ousd.balanceOf(user.address); const tokenBalance = await asset.balanceOf(user.address); - const reserveTokenBalance = await reserveStrategy.checkBalance( - asset.address - ); // await asset.connect(user).approve(vault.address, tokenAmount) await vault.connect(user).mint(asset.address, tokenAmount, 0); @@ -772,21 +769,6 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { currentSupply.add(ousdAmount), "Total supply mismatch" ); - if (asset == dai) { - // DAI is unsupported and should not be deposited in reserve strategy - await expect(reserveStrategy).to.have.an.assetBalanceOf( - reserveTokenBalance, - asset, - "Expected reserve strategy to not support DAI" - ); - } else { - await expect(reserveStrategy).to.have.an.assetBalanceOf( - reserveTokenBalance.add(tokenAmount), - asset, - "Expected reserve strategy to have received the other token" - ); - } - await expect(user).to.have.an.approxBalanceWithToleranceOf( ousdBalance.add(ousdAmount), ousd, diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 9edebdff24..36148c29ef 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -163,6 +163,22 @@ describe("Uniswap V3 Strategy", function () { }); } + describe("Deposit", function () { + it("Should revert direct deposits", async () => { + const impersonatedVaultSigner = await impersonateAndFundContract( + vault.address + ); + + await expect( + strategy.connect(impersonatedVaultSigner).deposit(usdc.address, "1") + ).to.be.revertedWith("Direct deposits disabled on UniswapV3Strategy"); + + await expect( + strategy.connect(impersonatedVaultSigner).depositAll() + ).to.be.revertedWith("Direct deposits disabled on UniswapV3Strategy"); + }); + }); + describe("Redeem", function () { beforeEach(async () => { _destructureFixture(await uniswapV3Fixture()); @@ -1081,25 +1097,6 @@ describe("Uniswap V3 Strategy", function () { expect(newPos.netValue).to.be.lt(lastPos.netValue); }); - it("Should liquidate active position during withdraw", async () => { - const tokenId = await strategy.activeTokenId(); - const lastPos = await strategy.tokenIdToPosition(tokenId); - - await strategy - .connect(await impersonateAndFundContract(vault.address)) - .withdrawAssetFromActivePositionOnlyVault( - usdt.address, - usdtUnits("10000") - ); - - const newPos = await strategy.tokenIdToPosition(tokenId); - - expect(newPos.liquidity).to.be.lt(lastPos.liquidity); - expect(newPos.netValue).to.be.lt( - lastPos.netValue.sub(ousdUnits("10000")) - ); - }); - it("Should liquidate active position during withdrawAll", async () => { const tokenId = await strategy.activeTokenId(); @@ -1114,33 +1111,28 @@ describe("Uniswap V3 Strategy", function () { expect(pos.netValue).to.equal(0); }); - it("Only vault can do withdraw/withdrawAll", async () => { + it("Should revert if there's an active position during withdraw", async () => { const impersonatedVaultSigner = await impersonateAndFundContract( vault.address ); + const withdrawFuncSign = "withdraw(address,address,uint256)"; + await expect( - strategy - .connect(impersonatedVaultSigner) - .withdrawAssetFromActivePositionOnlyVault( - usdt.address, - usdtUnits("10000") - ) - ).to.not.be.reverted; + // prettier-ignore + strategy.connect(impersonatedVaultSigner)[withdrawFuncSign](vault.address, usdc.address, "10") + ).to.be.revertedWith("Active position still open"); + }); + + it("Only vault can do withdraw/withdrawAll", async () => { + const impersonatedVaultSigner = await impersonateAndFundContract( + vault.address + ); await expect(strategy.connect(impersonatedVaultSigner).withdrawAll()).to .not.be.reverted; for (const user of [governor, strategist, operator, daniel]) { - await expect( - strategy - .connect(user) - .withdrawAssetFromActivePositionOnlyVault( - usdt.address, - usdtUnits("10000") - ) - ).to.be.revertedWith("Caller is not the Vault"); - await expect(strategy.connect(user).withdrawAll()).to.be.revertedWith( "Caller is not the Vault" ); From a6e745a93a7b1706bfd9fce1e1e43b73e0d56941 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:35:48 +0400 Subject: [PATCH 65/83] Uniswap V3 - L01 (#1290) * Remove default values from implementation * Fix deployment script * Fix unit test deployment * Lint --- .../strategies/uniswap/UniswapV3Strategy.sol | 32 ++++++++++++++++--- .../uniswap/UniswapV3StrategyStorage.sol | 6 ++-- contracts/deploy/001_core.js | 8 +++-- .../deploy/051_uniswap_usdc_usdt_strategy.js | 20 +++--------- 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 8de92ef8ca..a07cb12a76 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -37,7 +37,9 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { address _nonfungiblePositionManager, address _helper, address _swapRouter, - address _operator + address _operator, + uint256 _maxTVL, + uint256 _maxValueLostThreshold ) external onlyGovernor initializer { // NOTE: _self should always be the address of the proxy. // This is used to do `delegecall` between the this contract and @@ -68,6 +70,8 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { ); _setOperator(_operator); + _setMaxTVL(_maxTVL); + _setMaxPositionValueLostThreshold(_maxValueLostThreshold); } /*************************************** @@ -183,20 +187,38 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @param _maxTVL Maximum amount the strategy can have deployed in the Uniswap pool */ function setMaxTVL(uint256 _maxTVL) external onlyGovernorOrStrategist { + _setMaxTVL(_maxTVL); + } + + /** + * @notice Change the maxTVL amount threshold + * @param _maxTVL Maximum amount the strategy can have deployed in the Uniswap pool + */ + function _setMaxTVL(uint256 _maxTVL) internal { maxTVL = _maxTVL; emit MaxTVLChanged(_maxTVL); } /** * @notice Maximum value of loss the LP positions can incur before strategy shuts off rebalances - * @param _maxLossThreshold Maximum amount in 18 decimals + * @param _maxValueLostThreshold Maximum amount in 18 decimals */ - function setMaxPositionValueLostThreshold(uint256 _maxLossThreshold) + function setMaxPositionValueLostThreshold(uint256 _maxValueLostThreshold) external onlyGovernorOrStrategist { - maxPositionValueLostThreshold = _maxLossThreshold; - emit MaxValueLostThresholdChanged(_maxLossThreshold); + _setMaxPositionValueLostThreshold(_maxValueLostThreshold); + } + + /** + * @notice Maximum value of loss the LP positions can incur before strategy shuts off rebalances + * @param _maxValueLostThreshold Maximum amount in 18 decimals + */ + function _setMaxPositionValueLostThreshold(uint256 _maxValueLostThreshold) + internal + { + maxPositionValueLostThreshold = _maxValueLostThreshold; + emit MaxValueLostThresholdChanged(_maxValueLostThreshold); } /** diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index eb2e3862d7..74ad13a627 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -105,7 +105,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { bool public swapsPaused = false; // True if Swaps are paused bool public rebalancePaused = false; // True if Swaps are paused - uint256 public maxTVL = 1000000 ether; // In USD, 18 decimals, defaults to 1M + uint256 public maxTVL; // In USD, 18 decimals // Deposits to reserve strategy when contract balance exceeds this amount uint256 public minDepositThreshold0; @@ -123,10 +123,10 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint256 public activeTokenId; // Sum of loss in value of tokens deployed to the pool - uint256 public netLostValue = 0; + uint256 public netLostValue; // Max value loss threshold after which rebalances aren't allowed - uint256 public maxPositionValueLostThreshold = 50000 ether; // default to 50k + uint256 public maxPositionValueLostThreshold; // Uniswap V3's Pool IUniswapV3Pool public pool; diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 52b6a301dc..17649bcf25 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -1030,13 +1030,17 @@ const deployUniswapV3Strategy = async () => { await withConfirmation( uniV3Strat .connect(sDeployer) - ["initialize(address,address,address,address,address,address)"]( + [ + "initialize(address,address,address,address,address,address,uint256,uint256)" + ]( vault.address, pool.address, manager.address, v3Helper.address, mockRouter.address, - operatorAddr + operatorAddr, + "1000000000000", + "50000000000" ) ); log("Initialized UniswapV3Strategy"); diff --git a/contracts/deploy/051_uniswap_usdc_usdt_strategy.js b/contracts/deploy/051_uniswap_usdc_usdt_strategy.js index 4196783e7e..fcee0c43cf 100644 --- a/contracts/deploy/051_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/051_uniswap_usdc_usdt_strategy.js @@ -81,7 +81,7 @@ module.exports = deploymentWithGovernanceProposal( // 4. Init and configure new Uniswap V3 strategy const initFunction = - "initialize(address,address,address,address,address,address)"; + "initialize(address,address,address,address,address,address,uint256,uint256)"; await withConfirmation( cUniV3_USDC_USDT_Strategy.connect(sDeployer)[initFunction]( cVaultProxy.address, // Vault @@ -90,6 +90,8 @@ module.exports = deploymentWithGovernanceProposal( dUniswapV3Helper.address, assetAddresses.UniV3SwapRouter, operatorAddr, + utils.parseEther("1000000", 18), // 1M, max TVL + utils.parseEther("50000", 18), // 50k, lost value threshold await getTxOpts() ) ); @@ -177,25 +179,13 @@ module.exports = deploymentWithGovernanceProposal( signature: "setMinDepositThreshold(address,uint256)", args: [assetAddresses.USDT, utils.parseUnits("30000", 6)], // 30k }, - // // 9. Set Max TVL - // { - // contract: cUniV3_USDC_USDT_Strategy, - // signature: "setMaxTVL(uint256)", - // args: [utils.parseEther("1000000", 18)], // 1M - // }, - // // 10. Set Max Loss threshold - // { - // contract: cUniV3_USDC_USDT_Strategy, - // signature: "setMaxPositionValueLostThreshold(uint256)", - // args: [utils.parseEther("50000", 18)], // 50k - // }, - // 11. Set Rebalance Price Threshold + // 9. Set Rebalance Price Threshold { contract: cUniV3_USDC_USDT_Strategy, signature: "setRebalancePriceThreshold(int24,int24)", args: [-1000, 1000], }, - // 12. Set Swap price threshold + // 10. Set Swap price threshold { contract: cUniV3_USDC_USDT_Strategy, signature: "setSwapPriceThreshold(int24,int24)", From 79fed4fdcc72e2619b3a73ee90d2e67dc5c77c0b Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:36:08 +0400 Subject: [PATCH 66/83] Collect fees even when closing empty positions (#1291) --- .../uniswap/UniswapV3LiquidityManager.sol | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 9f19caad35..edf4732739 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -656,18 +656,16 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { Position memory position = tokenIdToPosition[tokenId]; require(position.exists, "Invalid position"); - if (position.liquidity == 0) { - return (0, 0); + if (position.liquidity > 0) { + // Remove all liquidity + (amount0, amount1) = _decreasePositionLiquidity( + tokenId, + position.liquidity, + minAmount0, + minAmount1 + ); } - // Remove all liquidity - (amount0, amount1) = _decreasePositionLiquidity( - tokenId, - position.liquidity, - minAmount0, - minAmount1 - ); - if (position.tokenId == activeTokenId) { activeTokenId = 0; } From 9d984e5846e49c3a57493369e75fb5105b1c3732 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:36:40 +0400 Subject: [PATCH 67/83] Fix return values (#1292) --- .../contracts/strategies/uniswap/UniswapV3LiquidityManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index edf4732739..08b7566c9f 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -542,7 +542,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // Withdraw enough funds from Reserve strategies _ensureAssetBalances(desiredAmount0, desiredAmount1); - _increasePositionLiquidity( + (, amount0, amount1) = _increasePositionLiquidity( activeTokenId, desiredAmount0, desiredAmount1, From 0446638c5dde5499ba8aad07c706713a9c8b476e Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:36:53 +0400 Subject: [PATCH 68/83] Check for duplicate tokenID when minting (#1293) --- .../contracts/strategies/uniswap/UniswapV3LiquidityManager.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 08b7566c9f..507ad32c40 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -443,6 +443,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { (tokenId, liquidity, amount0, amount1) = positionManager.mint(params); + require(!tokenIdToPosition[tokenId].exists, "Duplicate position"); + ticksToTokenId[tickKey] = tokenId; tokenIdToPosition[tokenId] = Position({ exists: true, From 7ea638f5051021604a5319f55c627d24a64edf09 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:37:06 +0400 Subject: [PATCH 69/83] Remove `payable` keyword (#1296) --- contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index a07cb12a76..f653eaa737 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -504,7 +504,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @notice This is a catch all for all functions not declared here */ // solhint-disable-next-line no-complex-fallback - fallback() external payable { + fallback() external { bytes32 slot = liquidityManagerImplPosition; // solhint-disable-next-line no-inline-assembly assembly { From ec48808e49feebdb1c2a4dafe281a2014aa3996c Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:39:02 +0400 Subject: [PATCH 70/83] Uniswap V3 - L08 (#1297) * Deposit to reserve after closing a position * Fix bug --- .../strategies/uniswap/UniswapV3LiquidityManager.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index 507ad32c40..b3e855da18 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -701,7 +701,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { nonReentrant returns (uint256 amount0, uint256 amount1) { - return _closePosition(tokenId, minAmount0, minAmount1); + (amount0, amount1) = _closePosition(tokenId, minAmount0, minAmount1); + + _depositAll(); // Intentionally skipping TVL check since removing liquidity won't cause it to fail } @@ -719,7 +721,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { { // Since this is called by the Vault, we cannot pass min redeem amounts // without complicating the code of the Vault. So, passing 0 instead. - return _closePosition(activeTokenId, 0, 0); + (amount0, amount1) = _closePosition(activeTokenId, 0, 0); // Intentionally skipping TVL check since removing liquidity won't cause it to fail } From 6c4bbf61ba50e62921c416d634fa344f22515fe4 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:39:18 +0400 Subject: [PATCH 71/83] Use encodeWithSelector (#1298) --- contracts/contracts/interfaces/IUniswapV3Strategy.sol | 2 ++ contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/interfaces/IUniswapV3Strategy.sol b/contracts/contracts/interfaces/IUniswapV3Strategy.sol index 2bc1eaec40..259a34cce4 100644 --- a/contracts/contracts/interfaces/IUniswapV3Strategy.sol +++ b/contracts/contracts/interfaces/IUniswapV3Strategy.sol @@ -9,4 +9,6 @@ interface IUniswapV3Strategy is IStrategy { function token1() external view returns (address); function reserveStrategy(address token) external view returns (address); + + function closeActivePositionOnlyVault() external; } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index f653eaa737..b6721f72a6 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -322,7 +322,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { // liquidity from active LP position // solhint-disable-next-line no-unused-vars (bool success, bytes memory data) = address(_self).delegatecall( - abi.encodeWithSignature("closeActivePositionOnlyVault()") + abi.encodeWithSelector(IUniswapV3Strategy.closeActivePositionOnlyVault.selector) ); require(success, "DelegateCall to close position failed"); } From 0e8a804649c9ad9830d6e077f9a2eeba4b72f2cc Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:39:27 +0400 Subject: [PATCH 72/83] Set Governor address to zero on implementation contracts (#1299) --- .../strategies/uniswap/UniswapV3StrategyStorage.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 74ad13a627..1e78c83072 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -153,6 +153,12 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { // Future-proofing uint256[100] private __gap; + constructor() { + // Governor address is set on the proxy contract. There's no need to + // set a Governor for the implementation contract + _setGovernor(address(0)); + } + /*************************************** Modifiers ****************************************/ From f71b05ce6b76f12bac4edc7d9ad14b1994ec69bb Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:39:46 +0400 Subject: [PATCH 73/83] Uniswap - N14 - Remove unused imports (#1312) * Remove unused imports * Add back import --- .../strategies/uniswap/UniswapV3LiquidityManager.sol | 5 ----- contracts/contracts/utils/UniswapV3Helper.sol | 2 -- 2 files changed, 7 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index b3e855da18..a6543077be 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -3,19 +3,14 @@ pragma solidity ^0.8.0; import { UniswapV3StrategyStorage } from "./UniswapV3StrategyStorage.sol"; -import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; import { IVault } from "../../interfaces/IVault.sol"; -import { IStrategy } from "../../interfaces/IStrategy.sol"; -import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { Helpers } from "../../utils/Helpers.sol"; import { StableMath } from "../../utils/StableMath.sol"; -import "@openzeppelin/contracts/utils/Strings.sol"; - contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { using SafeERC20 for IERC20; using StableMath for uint256; diff --git a/contracts/contracts/utils/UniswapV3Helper.sol b/contracts/contracts/utils/UniswapV3Helper.sol index fde134402a..5bad37c35c 100644 --- a/contracts/contracts/utils/UniswapV3Helper.sol +++ b/contracts/contracts/utils/UniswapV3Helper.sol @@ -4,8 +4,6 @@ pragma solidity =0.7.6; import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import "@uniswap/v3-core/contracts/libraries/FixedPoint128.sol"; import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; -import "@uniswap/v3-core/contracts/libraries/Tick.sol"; -import "@uniswap/v3-periphery/contracts/libraries/PositionKey.sol"; import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; import { INonfungiblePositionManager } from "../interfaces/uniswap/v3/INonfungiblePositionManager.sol"; From d3be84cc5848d392920353ffebd2e4f954af3db7 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:51:18 +0400 Subject: [PATCH 74/83] Uniswap audit fixes (#1300) * N1 - Code simplification * Constants to upper case * N04 - Indexing event params * N05 - Fix comments * N12 - typos * N09 - Simplify require statements * N-08 - Remove storage gap * N-07 - Naming * N-06 - Naming of internal functions & variables * Fix comment --- .../uniswap/UniswapV3LiquidityManager.sol | 66 ++++++++++--------- .../strategies/uniswap/UniswapV3Strategy.sol | 21 +++--- .../uniswap/UniswapV3StrategyStorage.sol | 36 +++++----- contracts/contracts/vault/VaultCore.sol | 8 +-- contracts/contracts/vault/VaultStorage.sol | 4 +- contracts/test/strategies/uniswap-v3.js | 2 +- 6 files changed, 67 insertions(+), 70 deletions(-) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol index a6543077be..d5401ca79d 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol @@ -19,11 +19,11 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { Position Value ****************************************/ /** - * @notice Calculates the net value of the position exlcuding fees + * @notice Calculates the net value of the position excluding fees * @param tokenId tokenID of the Position NFT * @return posValue Value of position (in 18 decimals) */ - function getPositionValue(uint256 tokenId) + function _getPositionValue(uint256 tokenId) internal view returns (uint256 posValue) @@ -52,9 +52,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { Rebalance ****************************************/ /// Reverts if active position's value is greater than maxTVL - function ensureTVL() internal { + function _ensureTVL() internal { require( - getPositionValue(activeTokenId) <= maxTVL, + _getPositionValue(activeTokenId) <= maxTVL, "MaxTVL threshold has been reached" ); } @@ -64,7 +64,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { * @param sqrtPriceLimitX96 Desired swap price limit * @param swapZeroForOne True when swapping token0 for token1 */ - function swapsNotPausedAndWithinLimits( + function _swapsNotPausedAndWithinLimits( uint160 sqrtPriceLimitX96, bool swapZeroForOne ) internal { @@ -87,7 +87,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { } /// Reverts if rebalances are paused - function rebalanceNotPaused() internal { + function _rebalanceNotPaused() internal { require(!rebalancePaused, "Rebalances are paused"); } @@ -96,9 +96,10 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { * @param upperTick Upper tick index * @param lowerTick Lower tick inded */ - function rebalanceNotPausedAndWithinLimits(int24 lowerTick, int24 upperTick) - internal - { + function _rebalanceNotPausedAndWithinLimits( + int24 lowerTick, + int24 upperTick + ) internal { require(!rebalancePaused, "Rebalances are paused"); require( minRebalanceTick <= lowerTick && maxRebalanceTick >= upperTick, @@ -147,12 +148,12 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { * the storage. Also, updates netLostValue state * @param tokenId Token ID of the position */ - function updatePositionNetVal(uint256 tokenId) internal { + function _updatePositionNetVal(uint256 tokenId) internal { if (tokenId == 0) { return; } - uint256 currentVal = getPositionValue(tokenId); + uint256 currentVal = _getPositionValue(tokenId); uint256 lastVal = tokenIdToPosition[tokenId].netValue; if (currentVal == lastVal) { @@ -184,8 +185,8 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { * Reverts if netLostValue threshold is breached. * @param tokenId Token ID of the position */ - function ensureNetLossThreshold(uint256 tokenId) internal { - updatePositionNetVal(tokenId); + function _ensureNetValueLostThreshold(uint256 tokenId) internal { + _updatePositionNetVal(tokenId); require( netLostValue < maxPositionValueLostThreshold, "Over max value loss threshold" @@ -217,7 +218,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { int24 upperTick ) external onlyGovernorOrStrategistOrOperator nonReentrant { require(lowerTick < upperTick, "Invalid tick range"); - rebalanceNotPausedAndWithinLimits(lowerTick, upperTick); + _rebalanceNotPausedAndWithinLimits(lowerTick, upperTick); int48 tickKey = _getTickPositionKey(lowerTick, upperTick); uint256 tokenId = ticksToTokenId[tickKey]; @@ -259,7 +260,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { _depositAll(); // Final position value/sanity check - ensureTVL(); + _ensureTVL(); } struct SwapAndRebalanceParams { @@ -301,11 +302,11 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { nonReentrant { require(params.lowerTick < params.upperTick, "Invalid tick range"); - swapsNotPausedAndWithinLimits( + _swapsNotPausedAndWithinLimits( params.sqrtPriceLimitX96, params.swapZeroForOne ); - rebalanceNotPausedAndWithinLimits(params.lowerTick, params.upperTick); + _rebalanceNotPausedAndWithinLimits(params.lowerTick, params.upperTick); uint256 tokenId = ticksToTokenId[ _getTickPositionKey(params.lowerTick, params.upperTick) @@ -359,7 +360,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { _depositAll(); // Final position value/sanity check - ensureTVL(); + _ensureTVL(); } /*************************************** @@ -381,7 +382,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { if (lowerTick > upperTick) (lowerTick, upperTick) = (upperTick, lowerTick); key = int48(lowerTick) * 2**24; // Shift by 24 bits - key = key + int24(upperTick); + key = key + upperTick; } /** @@ -419,7 +420,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { require(ticksToTokenId[tickKey] == 0, "Duplicate position mint"); // Make sure liquidity management is disabled when value lost threshold is breached - ensureNetLossThreshold(0); + _ensureNetValueLostThreshold(0); INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ @@ -487,7 +488,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { require(position.exists, "No active position"); // Make sure liquidity management is disabled when value lost threshold is breached - ensureNetLossThreshold(tokenId); + _ensureNetValueLostThreshold(tokenId); INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager @@ -506,7 +507,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { position.liquidity += liquidity; // Update last known value - position.netValue = getPositionValue(tokenId); + position.netValue = _getPositionValue(tokenId); emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity); } @@ -534,7 +535,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { nonReentrant returns (uint256 amount0, uint256 amount1) { - rebalanceNotPaused(); + _rebalanceNotPaused(); // Withdraw enough funds from Reserve strategies _ensureAssetBalances(desiredAmount0, desiredAmount1); @@ -551,14 +552,14 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { _depositAll(); // Final position value/sanity check - ensureTVL(); + _ensureTVL(); } /** * @notice Removes liquidity of the position in the pool * * @param tokenId Position NFT's tokenId - * @param liquidity Amount of liquidity to remove form the position + * @param liquidity Amount of liquidity to remove from the position * @param minAmount0 Min amount of token0 to withdraw * @param minAmount1 Min amount of token1 to withdraw * @@ -576,7 +577,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { // Update net value loss (to capture the state value before updating it). // Also allows to close/decrease liquidity even if beyond the net loss threshold. - updatePositionNetVal(tokenId); + _updatePositionNetVal(tokenId); INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager @@ -592,7 +593,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { position.liquidity -= liquidity; // Update last known value - position.netValue = getPositionValue(tokenId); + position.netValue = _getPositionValue(tokenId); emit UniswapV3LiquidityRemoved( position.tokenId, @@ -605,7 +606,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { /** * @notice Removes liquidity of the active position in the pool * - * @param liquidity Amount of liquidity to remove form the position + * @param liquidity Amount of liquidity to remove from the position * @param minAmount0 Min amount of token0 to withdraw * @param minAmount1 Min amount of token1 to withdraw * @@ -622,7 +623,7 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { nonReentrant returns (uint256 amount0, uint256 amount1) { - rebalanceNotPaused(); + _rebalanceNotPaused(); (amount0, amount1) = _decreasePositionLiquidity( activeTokenId, @@ -789,8 +790,9 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { 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 > 0 && token1Needed > t1ReserveBal, + token1Needed > t1ReserveBal, "Cannot swap when the asset is available in reserve" ); // Additional amount of token0 required for swapping @@ -804,14 +806,14 @@ contract UniswapV3LiquidityManager is UniswapV3StrategyStorage { 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 > 0 && token0Needed > t0ReserveBal, + 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 - // Subtract token1 that we will get from swapping token0Needed = (swapMinAmountOut >= token0Needed) ? 0 : (token0Needed - swapMinAmountOut); diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index b6721f72a6..4efe1f44de 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -42,7 +42,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { uint256 _maxValueLostThreshold ) external onlyGovernor initializer { // NOTE: _self should always be the address of the proxy. - // This is used to do `delegecall` between the this contract and + // This is used to do `delegatecall` between the this contract and // `UniswapV3LiquidityManager` whenever it's required. _self = IUniswapV3Strategy(address(this)); @@ -104,7 +104,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { onlyGovernorOrStrategist nonReentrant { - onlyPoolTokens(_asset); + _onlyPoolTokens(_asset); require( IVault(vaultAddress).isStrategySupported(_reserveStrategy), @@ -151,7 +151,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { external onlyGovernorOrStrategist { - onlyPoolTokens(_asset); + _onlyPoolTokens(_asset); if (_asset == token0) { minDepositThreshold0 = _minThreshold; @@ -226,7 +226,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { * @dev Only governor can call it */ function resetLostValue() external onlyGovernor { - emit NetLossValueReset(msg.sender); + emit NetLostValueReset(msg.sender); emit NetLostValueChanged(0); netLostValue = 0; } @@ -303,7 +303,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { address _asset, uint256 amount ) external override onlyVault nonReentrant { - onlyPoolTokens(_asset); + _onlyPoolTokens(_asset); require(activeTokenId == 0, "Active position still open"); @@ -373,7 +373,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { override returns (uint256 balance) { - onlyPoolTokens(_asset); + _onlyPoolTokens(_asset); balance = IERC20(_asset).balanceOf(address(this)); if (activeTokenId > 0) { @@ -393,7 +393,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { /** * @dev Ensures that the asset address is either token0 or token1. */ - function onlyPoolTokens(address addr) internal view { + function _onlyPoolTokens(address addr) internal view { require(addr == token0 || addr == token1, "Unsupported asset"); } @@ -433,7 +433,8 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { } /** - * Removes all allowance of both the tokens from NonfungiblePositionManager + * Removes all allowance of both the tokens from NonfungiblePositionManager as + * well as from the Uniswap V3 Swap Router */ function resetAllowanceOfTokens() external onlyGovernor nonReentrant { IERC20(token0).safeApprove(address(positionManager), 0); @@ -491,7 +492,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { Address.isContract(newImpl), "new implementation is not a contract" ); - bytes32 position = liquidityManagerImplPosition; + bytes32 position = LIQUIDITY_MANAGER_IMPL_POSITION; // solhint-disable-next-line no-inline-assembly assembly { sstore(position, newImpl) @@ -505,7 +506,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { */ // solhint-disable-next-line no-complex-fallback fallback() external { - bytes32 slot = liquidityManagerImplPosition; + bytes32 slot = LIQUIDITY_MANAGER_IMPL_POSITION; // solhint-disable-next-line no-inline-assembly assembly { // Copy msg.data. We take full control of memory in this inline assembly diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 1e78c83072..b2f2dfaa3a 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -13,11 +13,14 @@ import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { - event OperatorChanged(address _address); - event LiquidityManagerImplementationUpgraded(address _newImpl); - event ReserveStrategyChanged(address asset, address reserveStrategy); + event OperatorChanged(address indexed _address); + event LiquidityManagerImplementationUpgraded(address indexed _newImpl); + event ReserveStrategyChanged( + address indexed asset, + address reserveStrategy + ); event MinDepositThresholdChanged( - address asset, + address indexed asset, uint256 minDepositThreshold ); event RebalancePauseStatusChanged(bool paused); @@ -31,7 +34,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { ); event MaxTVLChanged(uint256 maxTVL); event MaxValueLostThresholdChanged(uint256 amount); - event NetLossValueReset(address indexed _by); + event NetLostValueReset(address indexed _by); event NetLostValueChanged(uint256 currentNetLostValue); event PositionValueChanged( uint256 indexed tokenId, @@ -135,24 +138,21 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { INonfungiblePositionManager public positionManager; // A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch - IUniswapV3Helper internal helper; + IUniswapV3Helper public helper; // Uniswap Swap Router - ISwapRouter internal swapRouter; + ISwapRouter public swapRouter; // A lookup table to find token IDs of position using f(lowerTick, upperTick) - mapping(int48 => uint256) internal ticksToTokenId; + mapping(int48 => uint256) public ticksToTokenId; // Maps tokenIDs to their Position object mapping(uint256 => Position) public tokenIdToPosition; // keccak256("OUSD.UniswapV3Strategy.LiquidityManager.impl") - bytes32 constant liquidityManagerImplPosition = + bytes32 constant LIQUIDITY_MANAGER_IMPL_POSITION = 0xec676d52175f7cbb4e4ea392c6b70f8946575021aad20479602b98adc56ad62d; - // Future-proofing - uint256[100] private __gap; - constructor() { // Governor address is set on the proxy contract. There's no need to // set a Governor for the implementation contract @@ -192,23 +192,17 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { Shared functions ****************************************/ /** - * @notice Deposits back strategy token balances back to the reserve strategies + * @notice Deposits token balances in the contract back to the reserve strategies */ function _depositAll() internal { uint256 token0Bal = IERC20(token0).balanceOf(address(this)); uint256 token1Bal = IERC20(token1).balanceOf(address(this)); IVault vault = IVault(vaultAddress); - if ( - token0Bal > 0 && - (minDepositThreshold0 == 0 || token0Bal >= minDepositThreshold0) - ) { + if (token0Bal > 0 && token0Bal >= minDepositThreshold0) { vault.depositToUniswapV3Reserve(token0, token0Bal); } - if ( - token1Bal > 0 && - (minDepositThreshold1 == 0 || token1Bal >= minDepositThreshold1) - ) { + if (token1Bal > 0 && token1Bal >= minDepositThreshold1) { vault.depositToUniswapV3Reserve(token1, token1Bal); } // Not emitting Deposit events since the Reserve strategies would do so diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 04423e2acf..558cc7b0cd 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -434,7 +434,7 @@ contract VaultCore is VaultStorage { /** * @dev Internal to calculate total value of all assets held in Vault. - * @return value Total value in ETH (1e18) + * @return value Total value in USD (1e18) */ function _totalValueInVault() internal view returns (uint256 value) { for (uint256 y = 0; y < allAssets.length; y++) { @@ -449,7 +449,7 @@ contract VaultCore is VaultStorage { /** * @dev Internal to calculate total value of all assets held in Strategies. - * @return value Total value in ETH (1e18) + * @return value Total value in USD (1e18) */ function _totalValueInStrategies() internal view returns (uint256 value) { for (uint256 i = 0; i < allStrategies.length; i++) { @@ -460,7 +460,7 @@ contract VaultCore is VaultStorage { /** * @dev Internal to calculate total value of all assets held by strategy. * @param _strategyAddr Address of the strategy - * @return value Total value in ETH (1e18) + * @return value Total value in USD (1e18) */ function _totalValueInStrategy(address _strategyAddr) internal @@ -679,7 +679,7 @@ contract VaultCore is VaultStorage { */ // solhint-disable-next-line no-complex-fallback fallback() external payable { - bytes32 slot = adminImplPosition; + bytes32 slot = ADMIN_IMPL_POSITION; // solhint-disable-next-line no-inline-assembly assembly { // Copy msg.data. We take full control of memory in this inline assembly diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index ac0d991e3b..1497ef3d1c 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -89,7 +89,7 @@ contract VaultStorage is Initializable, Governable { OUSD internal oUSD; //keccak256("OUSD.vault.governor.admin.impl"); - bytes32 constant adminImplPosition = + bytes32 constant ADMIN_IMPL_POSITION = 0xa2bd3d3cf188a41358c8b401076eb59066b09dec5775650c0de4c55187d17bd9; // Address of the contract responsible for post rebase syncs with AMMs @@ -137,7 +137,7 @@ contract VaultStorage is Initializable, Governable { Address.isContract(newImpl), "new implementation is not a contract" ); - bytes32 position = adminImplPosition; + bytes32 position = ADMIN_IMPL_POSITION; // solhint-disable-next-line no-inline-assembly assembly { sstore(position, newImpl) diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index 36148c29ef..24d9212b83 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -440,7 +440,7 @@ describe("Uniswap V3 Strategy", function () { it("Should let Governor reset the lost value ", async () => { const tx = await strategy.connect(governor).resetLostValue(); - await expect(tx).to.have.emittedEvent("NetLossValueReset", [ + await expect(tx).to.have.emittedEvent("NetLostValueReset", [ governor.address, ]); From af9e4a5a09b85b6e448250410077b40b5659376f Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 24 May 2023 17:59:17 +0400 Subject: [PATCH 75/83] Update yarn.lock --- contracts/yarn.lock | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 3bd1243aaf..84aa959f67 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -3119,7 +3119,7 @@ chokidar@3.5.3: optionalDependencies: fsevents "~2.3.2" -chokidar@^3.4.0, chokidar@^3.4.3, chokidar@^3.5.2: +chokidar@^3.4.0, chokidar@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== @@ -5691,13 +5691,6 @@ hardhat-tracer@^2.2.2: chalk "^4.1.2" ethers "^5.6.1" -hardhat-watcher@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/hardhat-watcher/-/hardhat-watcher-2.1.1.tgz#8b05fec429ed45da11808bbf6054a90f3e34c51a" - integrity sha512-zilmvxAYD34IofBrwOliQn4z92UiDmt2c949DW4Gokf0vS0qk4YTfVCi/LmUBICThGygNANE3WfnRTpjCJGtDA== - dependencies: - chokidar "^3.4.3" - hardhat@^2.11.0: version "2.14.0" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.14.0.tgz#b60c74861494aeb1b50803cf04cc47865a42b87a" From 6c34043cf4663441e93e522a24b8b2811e5e6ca7 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Tue, 6 Jun 2023 14:55:03 +0400 Subject: [PATCH 76/83] Lint --- .../mocks/uniswap/v3/MockNonfungiblePositionManager.sol | 2 +- .../contracts/strategies/uniswap/UniswapV3Strategy.sol | 4 +++- .../strategies/uniswap/UniswapV3StrategyStorage.sol | 2 +- contracts/test/_fixture.js | 7 ------- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol index b11148b820..5e3ca804f3 100644 --- a/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol +++ b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol @@ -6,7 +6,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { IUniswapV3Helper } from "../../../interfaces/uniswap/v3/IUniswapV3Helper.sol"; import { IMockUniswapV3Pool } from "./MockUniswapV3Pool.sol"; - +// solhint-disable-next-line no-console import "hardhat/console.sol"; contract MockNonfungiblePositionManager { diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 4efe1f44de..070c277273 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -322,7 +322,9 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { // liquidity from active LP position // solhint-disable-next-line no-unused-vars (bool success, bytes memory data) = address(_self).delegatecall( - abi.encodeWithSelector(IUniswapV3Strategy.closeActivePositionOnlyVault.selector) + abi.encodeWithSelector( + IUniswapV3Strategy.closeActivePositionOnlyVault.selector + ) ); require(success, "DelegateCall to close position failed"); } diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index b2f2dfaa3a..2b9d41a021 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -154,7 +154,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { 0xec676d52175f7cbb4e4ea392c6b70f8946575021aad20479602b98adc56ad62d; constructor() { - // Governor address is set on the proxy contract. There's no need to + // Governor address is set on the proxy contract. There's no need to // set a Governor for the implementation contract _setGovernor(address(0)); } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index cf9e6e68f4..e139a1029a 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1505,12 +1505,6 @@ async function rebornFixture() { return fixture; } -function defaultFixtureSetup() { - return deployments.createFixture(async () => { - return await defaultFixture(); - }); -} - function uniswapV3FixtureSetup() { return deployments.createFixture(async () => { const fixture = await defaultFixture(); @@ -1640,7 +1634,6 @@ module.exports = { hackedVaultFixture, rebornFixture, uniswapV3FixtureSetup, - defaultFixtureSetup, withImpersonatedAccount, impersonateAndFundContract, impersonateAccount, From 3829c634485ffa64a0496e067171587dc6c912db Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Tue, 6 Jun 2023 15:09:23 +0400 Subject: [PATCH 77/83] Update deploy script ID --- ..._usdc_usdt_strategy.js => 065_uniswap_usdc_usdt_strategy.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contracts/deploy/{051_uniswap_usdc_usdt_strategy.js => 065_uniswap_usdc_usdt_strategy.js} (99%) diff --git a/contracts/deploy/051_uniswap_usdc_usdt_strategy.js b/contracts/deploy/065_uniswap_usdc_usdt_strategy.js similarity index 99% rename from contracts/deploy/051_uniswap_usdc_usdt_strategy.js rename to contracts/deploy/065_uniswap_usdc_usdt_strategy.js index fcee0c43cf..fe00b81a16 100644 --- a/contracts/deploy/051_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/065_uniswap_usdc_usdt_strategy.js @@ -3,7 +3,7 @@ const { utils } = require("ethers"); module.exports = deploymentWithGovernanceProposal( { - deployName: "051_uniswap_usdc_usdt_strategy", + deployName: "065_uniswap_usdc_usdt_strategy", forceDeploy: false, }, async ({ From 5f2670b9ec1c6166fd9e3a101567eef123575b83 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 21 Jun 2023 17:30:48 +0530 Subject: [PATCH 78/83] Fix unit tests --- ...usdc_usdt_strategy.js => 070_uniswap_usdc_usdt_strategy.js} | 2 +- contracts/test/strategies/uniswap-v3.fork-test.js | 3 +-- contracts/test/strategies/uniswap-v3.js | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) rename contracts/deploy/{065_uniswap_usdc_usdt_strategy.js => 070_uniswap_usdc_usdt_strategy.js} (99%) diff --git a/contracts/deploy/065_uniswap_usdc_usdt_strategy.js b/contracts/deploy/070_uniswap_usdc_usdt_strategy.js similarity index 99% rename from contracts/deploy/065_uniswap_usdc_usdt_strategy.js rename to contracts/deploy/070_uniswap_usdc_usdt_strategy.js index fe00b81a16..8c0e0dc7cc 100644 --- a/contracts/deploy/065_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/070_uniswap_usdc_usdt_strategy.js @@ -3,7 +3,7 @@ const { utils } = require("ethers"); module.exports = deploymentWithGovernanceProposal( { - deployName: "065_uniswap_usdc_usdt_strategy", + deployName: "070_uniswap_usdc_usdt_strategy", forceDeploy: false, }, async ({ diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 33363e1b26..a5bde62eec 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -21,8 +21,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { // This is needed to revert fixtures // The other tests as of now don't use proper fixtures // Rel: https://github.com/OriginProtocol/origin-dollar/issues/1259 - const f = defaultFixture(); - await f(); + await defaultFixture(); }); this.timeout(0); diff --git a/contracts/test/strategies/uniswap-v3.js b/contracts/test/strategies/uniswap-v3.js index c6708f6bd3..951340249e 100644 --- a/contracts/test/strategies/uniswap-v3.js +++ b/contracts/test/strategies/uniswap-v3.js @@ -19,8 +19,7 @@ describe("Uniswap V3 Strategy", function () { // This is needed to revert fixtures // The other tests as of now don't use proper fixtures // Rel: https://github.com/OriginProtocol/origin-dollar/issues/1259 - const f = defaultFixture(); - await f(); + await defaultFixture(); }); // Fixtures From f0689e2a92405ed1b16cf2d013a2f444cde9c6b9 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 21 Jun 2023 18:00:18 +0530 Subject: [PATCH 79/83] Use Aave Strategy as reserves --- contracts/deploy/070_uniswap_usdc_usdt_strategy.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/deploy/070_uniswap_usdc_usdt_strategy.js b/contracts/deploy/070_uniswap_usdc_usdt_strategy.js index 8c0e0dc7cc..2338842602 100644 --- a/contracts/deploy/070_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/070_uniswap_usdc_usdt_strategy.js @@ -57,8 +57,8 @@ module.exports = deploymentWithGovernanceProposal( dUniV3_USDC_USDT_Proxy.address ); - const cMorphoAaveProxy = await ethers.getContract( - "MorphoAaveStrategyProxy" + const cAaveProxy = await ethers.getContract( + "AaveStrategyProxy" ); const cHarvesterProxy = await ethers.getContract("HarvesterProxy"); @@ -159,13 +159,13 @@ module.exports = deploymentWithGovernanceProposal( { contract: cUniV3_USDC_USDT_Strategy, signature: "setReserveStrategy(address,address)", - args: [assetAddresses.USDC, cMorphoAaveProxy.address], + args: [assetAddresses.USDC, cAaveProxy.address], }, // 6. Set Reserve Strategy for USDT { contract: cUniV3_USDC_USDT_Strategy, signature: "setReserveStrategy(address,address)", - args: [assetAddresses.USDT, cMorphoAaveProxy.address], + args: [assetAddresses.USDT, cAaveProxy.address], }, // 7. Set Minimum Deposit threshold for USDC { From 79da02d4773a5d571c0a3481f1b500a464e1d822 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 21 Jun 2023 18:17:13 +0530 Subject: [PATCH 80/83] Fix fork tests --- .vscode/settings.json | 1 + .../deploy/070_uniswap_usdc_usdt_strategy.js | 4 +--- contracts/test/_fixture.js | 22 ++++++++++++++----- .../test/strategies/uniswap-v3.fork-test.js | 2 +- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 35f1a0328c..889622973f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { + "solidity.defaultCompiler": "localNodeModule", "solidity.packageDefaultDependenciesContractsDirectory": "contracts/contracts", "solidity.packageDefaultDependenciesDirectory": "contracts/node_modules", "solidity.compileUsingRemoteVersion": "v0.8.7+commit.e28d00a7", diff --git a/contracts/deploy/070_uniswap_usdc_usdt_strategy.js b/contracts/deploy/070_uniswap_usdc_usdt_strategy.js index 2338842602..5d924cbdd4 100644 --- a/contracts/deploy/070_uniswap_usdc_usdt_strategy.js +++ b/contracts/deploy/070_uniswap_usdc_usdt_strategy.js @@ -57,9 +57,7 @@ module.exports = deploymentWithGovernanceProposal( dUniV3_USDC_USDT_Proxy.address ); - const cAaveProxy = await ethers.getContract( - "AaveStrategyProxy" - ); + const cAaveProxy = await ethers.getContract("AaveStrategyProxy"); const cHarvesterProxy = await ethers.getContract("HarvesterProxy"); const cHarvester = await ethers.getContractAt( diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index ff3edb236e..cb01b3f50b 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1599,8 +1599,15 @@ function uniswapV3FixtureSetup() { mockStrategy, mockStrategy2, mockStrategyDAI, + vault, + aaveStrategy, } = fixture; + const { governorAddr, timelockAddr } = await getNamedAccounts(); + const sGovernor = await ethers.provider.getSigner( + isFork ? timelockAddr : governorAddr + ); + if (!isFork) { // Approve mockStrategy await _approveStrategy(fixture, mockStrategy); @@ -1626,15 +1633,20 @@ function uniswapV3FixtureSetup() { usdt.address, mockStrategy.address ); + } else { + // Withdraw everything to Vault + await vault.connect(sGovernor).withdrawAllFromStrategies(); + + await _setDefaultStrategy(fixture, usdc, aaveStrategy); + await _setDefaultStrategy(fixture, usdt, aaveStrategy); + await _setDefaultStrategy(fixture, dai, aaveStrategy); + + await vault.connect(sGovernor).allocate(); + await vault.connect(sGovernor).rebase(); } // await UniV3_USDC_USDT_Strategy.setSwapPriceThreshold(-1000, 1000); - const { governorAddr, timelockAddr } = await getNamedAccounts(); - const sGovernor = await ethers.provider.getSigner( - isFork ? timelockAddr : governorAddr - ); - if (!isFork) { UniV3_USDC_USDT_Strategy.connect(sGovernor) // 2 million diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index a5bde62eec..01ce5d01d1 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -39,7 +39,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { beforeEach(async () => { fixture = await uniswapV3Fixture(); - reserveStrategy = fixture.morphoAaveStrategy; + reserveStrategy = fixture.aaveStrategy; strategy = fixture.UniV3_USDC_USDT_Strategy; pool = fixture.UniV3_USDC_USDT_Pool; positionManager = fixture.UniV3PositionManager; From 0324ac1391fdd3c3533ae0dc5ae1c3399a3d5173 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 21 Jun 2023 22:27:54 +0530 Subject: [PATCH 81/83] Fix fork tests --- .../test/strategies/uniswap-v3.fork-test.js | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/contracts/test/strategies/uniswap-v3.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js index 01ce5d01d1..b92a4a22af 100644 --- a/contracts/test/strategies/uniswap-v3.fork-test.js +++ b/contracts/test/strategies/uniswap-v3.fork-test.js @@ -634,6 +634,11 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const lowerTick = activeTick; const upperTick = activeTick + 1; + const slot0 = await hre.network.provider.request({ + method: "eth_getStorageAt", + params: [pool.address, "0x0"], + }); + // Mint position const amount = "100000"; const { tokenId, tx, amount0Minted, amount1Minted, liquidityMinted } = @@ -644,15 +649,18 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(await strategy.activeTokenId()).to.equal(tokenId); expect(await strategy.netLostValue()).to.equal(0); - // Do some big swaps to move active tick - await _swap(matt, "100000", false); - await _swap(josh, "100000", false); - await _swap(franck, "100000", false); - await _swap(daniel, "100000", false); - await _swap(domen, "100000", false); + // Change active tick + await hre.network.provider.request({ + method: "hardhat_setStorageAt", + params: [ + pool.address, + "0x0", + `${slot0.slice(0, 20)}f111110000000000000000000000000000000000000000`, + ], + }); // Set threshold to a low value to see if it throws - await setMaxPositionValueLostThreshold("0.01"); + await setMaxPositionValueLostThreshold("0.00000000000001"); await expect( strategy .connect(operator) @@ -691,6 +699,11 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { const lowerTick = activeTick; const upperTick = activeTick + 1; + const slot0 = await hre.network.provider.request({ + method: "eth_getStorageAt", + params: [pool.address, "0x0"], + }); + // Mint position const amount = "100000"; const { tokenId, tx } = await mintLiquidity( @@ -705,15 +718,18 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { expect(await strategy.activeTokenId()).to.equal(tokenId); expect(await strategy.netLostValue()).to.equal(0); - // Do some big swaps to move active tick - await _swap(matt, "100000", false); - await _swap(josh, "100000", false); - await _swap(franck, "100000", false); - await _swap(daniel, "100000", false); - await _swap(domen, "100000", false); + // Change active tick + await hre.network.provider.request({ + method: "hardhat_setStorageAt", + params: [ + pool.address, + "0x0", + `${slot0.slice(0, 20)}f111110000000000000000000000000000000000000000`, + ], + }); // Set threshold to a low value to see if it throws - await setMaxPositionValueLostThreshold("0.01"); + await setMaxPositionValueLostThreshold("0.00000000000001"); await expect( strategy .connect(operator) @@ -721,7 +737,7 @@ forkOnlyDescribe("Uniswap V3 Strategy", function () { ).to.be.revertedWith("Over max value loss threshold"); // should still be allowed to close the position - strategy.connect(operator).closePosition(tokenId, "0", "0"); + await strategy.connect(operator).closePosition(tokenId, "0", "0"); }); it("netLostValue will catch possible pool tilts", async () => { From 042a32bb2fd6f5b1d8766cb3894374931e20133b Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 22 Jun 2023 12:12:32 +0530 Subject: [PATCH 82/83] Fix address --- contracts/test/vault/vault.fork-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/vault/vault.fork-test.js b/contracts/test/vault/vault.fork-test.js index 564252363b..57f2764759 100644 --- a/contracts/test/vault/vault.fork-test.js +++ b/contracts/test/vault/vault.fork-test.js @@ -306,7 +306,7 @@ forkOnlyDescribe("ForkTest: Vault", function () { "0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAaveStrategy // TODO: Hard-code these after deploy //"0x7A192DD9Cc4Ea9bdEdeC9992df74F1DA55e60a19", // LUSD MetaStrategy - "0x050c4FcA28725d975c2896682eBD2905D2E58E84", // USDC<>USDT Uniswap V3 Strategy + "0xF4632427B2877c4c12670B5c75F794BCe16281FA", // USDC<>USDT Uniswap V3 Strategy ]; for (const s of strategies) { From 5d4063d1c6612a490b3d8d0dc1b898e7b4f16b28 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Fri, 23 Jun 2023 17:04:29 +1000 Subject: [PATCH 83/83] Uniswap v3 strategy updates (#1652) * removed unused StableMath from UniswapV3Strategy * Updated Uniswap V3 Natspec * Added Uniswap V3 strategy diagrams --- .../contracts/strategies/uniswap/README.md | 15 + .../strategies/uniswap/UniswapV3Strategy.sol | 10 +- .../uniswap/UniswapV3StrategyStorage.sol | 55 +-- .../utils/InitializableAbstractStrategy.sol | 26 +- contracts/docs/UniswapV3StrategyHierarchy.svg | 94 +++++ contracts/docs/UniswapV3StrategySquashed.svg | 162 ++++++++ contracts/docs/UniswapV3StrategyStorage.svg | 359 ++++++++++++++++++ contracts/docs/generate.sh | 5 + 8 files changed, 684 insertions(+), 42 deletions(-) create mode 100644 contracts/contracts/strategies/uniswap/README.md create mode 100644 contracts/docs/UniswapV3StrategyHierarchy.svg create mode 100644 contracts/docs/UniswapV3StrategySquashed.svg create mode 100644 contracts/docs/UniswapV3StrategyStorage.svg diff --git a/contracts/contracts/strategies/uniswap/README.md b/contracts/contracts/strategies/uniswap/README.md new file mode 100644 index 0000000000..0ac339a4d8 --- /dev/null +++ b/contracts/contracts/strategies/uniswap/README.md @@ -0,0 +1,15 @@ +# Diagrams + +## Uniswap V3 Strategy + +### Hierarchy + +![Uniswap V3 Strategy Hierarchy](../../../docs/UniswapV3StrategyHierarchy.svg) + +### Squashed + +![Uniswap V3 Strategy Squashed](../../../docs/UniswapV3StrategySquashed.svg) + +### Storage + +![Uniswap V3 Strategy Storage](../../../docs/UniswapV3StrategyStorage.svg) diff --git a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol index 070c277273..21f1230ef3 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol @@ -16,11 +16,9 @@ import { IUniswapV3Helper } from "../../interfaces/uniswap/v3/IUniswapV3Helper.s import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; -import { StableMath } from "../../utils/StableMath.sol"; contract UniswapV3Strategy is UniswapV3StrategyStorage { using SafeERC20 for IERC20; - using StableMath for uint256; /** * @dev Initialize the contract @@ -365,7 +363,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { } /** - * @dev Only checks the active LP position and undeployed/undeposited balance held by the contract. + * @notice Only checks the active LP position and undeployed/undeposited balance held by the contract. * Doesn't return the balance held in the reserve strategies. * @inheritdoc InitializableAbstractStrategy */ @@ -403,7 +401,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { ERC721 management ****************************************/ - /// Callback function for whenever a NFT is transferred to this contract + /// @notice Callback function for whenever a NFT is transferred to this contract // solhint-disable-next-line max-line-length /// Ref: https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#IERC721Receiver-onERC721Received-address-address-uint256-bytes- function onERC721Received( @@ -435,7 +433,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { } /** - * Removes all allowance of both the tokens from NonfungiblePositionManager as + * @notice Removes all allowance of both the tokens from NonfungiblePositionManager as * well as from the Uniswap V3 Swap Router */ function resetAllowanceOfTokens() external onlyGovernor nonReentrant { @@ -486,7 +484,7 @@ contract UniswapV3Strategy is UniswapV3StrategyStorage { Proxy to liquidity management ****************************************/ /** - * @dev Sets the implementation for the liquidity manager + * @notice Sets the implementation for the liquidity manager * @param newImpl address of the implementation */ function setLiquidityManagerImpl(address newImpl) external onlyGovernor { diff --git a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol index 2b9d41a021..aef4f194e7 100644 --- a/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol +++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol @@ -91,24 +91,33 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint256 netValue; // Last recorded net value of the position } - // Set to the proxy address when initialized + /// @notice The strategy's proxy contract address + /// @dev is set when initialized IUniswapV3Strategy public _self; - // The address that can manage the positions on Uniswap V3 + /// @notice The address that can manage the positions on Uniswap V3 address public operatorAddr; - address public token0; // Token0 of Uniswap V3 Pool - address public token1; // Token1 of Uniswap V3 Pool + /// @notice Token0 of Uniswap V3 Pool + address public token0; + /// @notice Token1 of Uniswap V3 Pool + address public token1; // When the funds are not deployed in Uniswap V3 Pool, they will // be deposited to these reserve strategies - IStrategy public reserveStrategy0; // Reserve strategy for token0 - IStrategy public reserveStrategy1; // Reserve strategy for token1 + /// @notice Reserve strategy for token0 + IStrategy public reserveStrategy0; + /// @notice Reserve strategy for token1 + IStrategy public reserveStrategy1; - uint24 public poolFee; // Uniswap V3 Pool Fee - bool public swapsPaused = false; // True if Swaps are paused - bool public rebalancePaused = false; // True if Swaps are paused + /// @notice Uniswap V3 Pool Fee + uint24 public poolFee; + /// @notice True if Swaps are paused + bool public swapsPaused = false; + /// @notice True if liquidity rebalances are paused + bool public rebalancePaused = false; - uint256 public maxTVL; // In USD, 18 decimals + /// @notice Maximum amount the strategy can have deployed in the Uniswap pool. In OTokens to 18 decimals + uint256 public maxTVL; // Deposits to reserve strategy when contract balance exceeds this amount uint256 public minDepositThreshold0; @@ -122,34 +131,34 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { uint160 public minSwapPriceX96; uint160 public maxSwapPriceX96; - // Token ID of active Position on the pool. zero, if there are no active LP position + /// @notice Token ID of active Position on the pool. zero, if there are no active LP position uint256 public activeTokenId; - // Sum of loss in value of tokens deployed to the pool + /// @notice Sum of loss in value of tokens deployed to the pool uint256 public netLostValue; - // Max value loss threshold after which rebalances aren't allowed + /// @notice Max value loss threshold after which rebalances aren't allowed uint256 public maxPositionValueLostThreshold; - // Uniswap V3's Pool + /// @notice Uniswap V3's Pool IUniswapV3Pool public pool; - // Uniswap V3's PositionManager + /// @notice Uniswap V3's PositionManager INonfungiblePositionManager public positionManager; - // A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch + /// @notice A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch IUniswapV3Helper public helper; - // Uniswap Swap Router + /// @notice Uniswap Swap Router ISwapRouter public swapRouter; - // A lookup table to find token IDs of position using f(lowerTick, upperTick) + /// @notice A lookup table to find token IDs of position using f(lowerTick, upperTick) mapping(int48 => uint256) public ticksToTokenId; - // Maps tokenIDs to their Position object + /// @notice Maps tokenIDs to their Position object mapping(uint256 => Position) public tokenIdToPosition; - // keccak256("OUSD.UniswapV3Strategy.LiquidityManager.impl") + /// @notice keccak256("OUSD.UniswapV3Strategy.LiquidityManager.impl") bytes32 constant LIQUIDITY_MANAGER_IMPL_POSITION = 0xec676d52175f7cbb4e4ea392c6b70f8946575021aad20479602b98adc56ad62d; @@ -192,7 +201,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { Shared functions ****************************************/ /** - * @notice Deposits token balances in the contract back to the reserve strategies + * @dev Deposits token balances in the contract back to the reserve strategies */ function _depositAll() internal { uint256 token0Bal = IERC20(token0).balanceOf(address(this)); @@ -209,7 +218,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { } /** - * @notice Returns the balance of both tokens in a given position (including fees) + * @dev Returns the balance of both tokens in a given position (including fees) * @param tokenId tokenID of the Position NFT * @return amount0 Amount of token0 in position * @return amount1 Amount of token1 in position @@ -230,7 +239,7 @@ abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy { } /** - * @notice Returns the balance of both tokens in a given position (without fees) + * @dev Returns the balance of both tokens in a given position (without fees) * @param tokenId tokenID of the Position NFT * @return amount0 Amount of token0 in position * @return amount1 Amount of token1 in position diff --git a/contracts/contracts/utils/InitializableAbstractStrategy.sol b/contracts/contracts/utils/InitializableAbstractStrategy.sol index af07d57d9b..51e5d18a01 100644 --- a/contracts/contracts/utils/InitializableAbstractStrategy.sol +++ b/contracts/contracts/utils/InitializableAbstractStrategy.sol @@ -103,7 +103,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { } /** - * @dev Collect accumulated reward token and send to Harvester. + * @notice Collect accumulated reward token and send to Harvester. */ function collectRewardTokens() external virtual onlyHarvester nonReentrant { _collectRewardTokens(); @@ -163,7 +163,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { } /** - * @dev Set the reward token addresses. + * @notice Set the reward token addresses. * @param _rewardTokenAddresses Address array of the reward token */ function setRewardTokenAddresses(address[] calldata _rewardTokenAddresses) @@ -185,7 +185,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { } /** - * @dev Get the reward token addresses. + * @notice Get the reward token addresses. * @return address[] the reward token addresses. */ function getRewardTokenAddresses() @@ -197,7 +197,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { } /** - * @dev Provide support for asset by passing its pToken address. + * @notice Provide support for asset by passing its pToken address. * This method can only be called by the system Governor * @param _asset Address for the asset * @param _pToken Address for the corresponding platform token @@ -211,7 +211,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { } /** - * @dev Remove a supported asset by passing its index. + * @notice Remove a supported asset by passing its index. * This method can only be called by the system Governor * @param _assetIndex Index of the asset to be removed */ @@ -252,7 +252,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { } /** - * @dev Transfer token to governor. Intended for recovering tokens stuck in + * @notice Transfer token to governor. Intended for recovering tokens stuck in * strategy contracts, i.e. mistaken sends. * @param _asset Address for the asset * @param _amount Amount of the asset to transfer @@ -265,7 +265,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { } /** - * @dev Set the reward token addresses. + * @notice Set the reward token addresses. * @param _harvesterAddress Address of the harvester */ function setHarvesterAddress(address _harvesterAddress) @@ -291,25 +291,25 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { virtual; /** - * @dev Approve all the assets supported by the strategy + * @notice Approve all the assets supported by the strategy * to be moved around the platform. */ function safeApproveAllTokens() external virtual; /** - * @dev Deposit an amount of asset into the platform + * @notice Deposit an amount of asset into the platform * @param _asset Address for the asset * @param _amount Units of asset to deposit */ function deposit(address _asset, uint256 _amount) external virtual; /** - * @dev Deposit balance of all supported assets into the platform + * @notice Deposit balance of all supported assets into the platform */ function depositAll() external virtual; /** - * @dev Withdraw an amount of asset from the platform. + * @notice Withdraw an amount of asset from the platform. * @param _recipient Address to which the asset should be sent * @param _asset Address of the asset * @param _amount Units of asset to withdraw @@ -321,7 +321,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { ) external virtual; /** - * @dev Withdraw all assets from strategy sending assets to Vault. + * @notice Withdraw all assets from strategy sending assets to Vault. */ function withdrawAll() external virtual; @@ -338,7 +338,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { returns (uint256 balance); /** - * @dev Check if an asset is supported. + * @notice Check if an asset is supported. * @param _asset Address of the asset * @return bool Whether asset is supported */ diff --git a/contracts/docs/UniswapV3StrategyHierarchy.svg b/contracts/docs/UniswapV3StrategyHierarchy.svg new file mode 100644 index 0000000000..39db2d7dc4 --- /dev/null +++ b/contracts/docs/UniswapV3StrategyHierarchy.svg @@ -0,0 +1,94 @@ + + + + + + +UmlClassDiagram + + + +7 + +Governable +../contracts/governance/Governable.sol + + + +207 + +UniswapV3LiquidityManager +../contracts/strategies/uniswap/UniswapV3LiquidityManager.sol + + + +210 + +<<Abstract>> +UniswapV3StrategyStorage +../contracts/strategies/uniswap/UniswapV3StrategyStorage.sol + + + +207->210 + + + + + +209 + +UniswapV3Strategy +../contracts/strategies/uniswap/UniswapV3Strategy.sol + + + +209->210 + + + + + +156 + +<<Abstract>> +InitializableAbstractStrategy +../contracts/utils/InitializableAbstractStrategy.sol + + + +210->156 + + + + + +155 + +<<Abstract>> +Initializable +../contracts/utils/Initializable.sol + + + +156->7 + + + + + +156->155 + + + + + +156->156 + + + + + diff --git a/contracts/docs/UniswapV3StrategySquashed.svg b/contracts/docs/UniswapV3StrategySquashed.svg new file mode 100644 index 0000000000..a401150aed --- /dev/null +++ b/contracts/docs/UniswapV3StrategySquashed.svg @@ -0,0 +1,162 @@ + + + + + + +UmlClassDiagram + + + +209 + +UniswapV3Strategy +../contracts/strategies/uniswap/UniswapV3Strategy.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _reserved: int256[98] <<InitializableAbstractStrategy>> +Internal: +   assetsMapped: address[] <<InitializableAbstractStrategy>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   platformAddress: address <<InitializableAbstractStrategy>> +   vaultAddress: address <<InitializableAbstractStrategy>> +   assetToPToken: mapping(address=>address) <<InitializableAbstractStrategy>> +   _deprecated_rewardTokenAddress: address <<InitializableAbstractStrategy>> +   _deprecated_rewardLiquidationThreshold: uint256 <<InitializableAbstractStrategy>> +   harvesterAddress: address <<InitializableAbstractStrategy>> +   rewardTokenAddresses: address[] <<InitializableAbstractStrategy>> +   _self: IUniswapV3Strategy <<UniswapV3StrategyStorage>> +   operatorAddr: address <<UniswapV3StrategyStorage>> +   token0: address <<UniswapV3StrategyStorage>> +   token1: address <<UniswapV3StrategyStorage>> +   reserveStrategy0: IStrategy <<UniswapV3StrategyStorage>> +   reserveStrategy1: IStrategy <<UniswapV3StrategyStorage>> +   poolFee: uint24 <<UniswapV3StrategyStorage>> +   swapsPaused: bool <<UniswapV3StrategyStorage>> +   rebalancePaused: bool <<UniswapV3StrategyStorage>> +   maxTVL: uint256 <<UniswapV3StrategyStorage>> +   minDepositThreshold0: uint256 <<UniswapV3StrategyStorage>> +   minDepositThreshold1: uint256 <<UniswapV3StrategyStorage>> +   minRebalanceTick: int24 <<UniswapV3StrategyStorage>> +   maxRebalanceTick: int24 <<UniswapV3StrategyStorage>> +   minSwapPriceX96: uint160 <<UniswapV3StrategyStorage>> +   maxSwapPriceX96: uint160 <<UniswapV3StrategyStorage>> +   activeTokenId: uint256 <<UniswapV3StrategyStorage>> +   netLostValue: uint256 <<UniswapV3StrategyStorage>> +   maxPositionValueLostThreshold: uint256 <<UniswapV3StrategyStorage>> +   pool: IUniswapV3Pool <<UniswapV3StrategyStorage>> +   positionManager: INonfungiblePositionManager <<UniswapV3StrategyStorage>> +   helper: IUniswapV3Helper <<UniswapV3StrategyStorage>> +   swapRouter: ISwapRouter <<UniswapV3StrategyStorage>> +   ticksToTokenId: mapping(int48=>uint256) <<UniswapV3StrategyStorage>> +   tokenIdToPosition: mapping(uint256=>Position) <<UniswapV3StrategyStorage>> +   LIQUIDITY_MANAGER_IMPL_POSITION: bytes32 <<UniswapV3StrategyStorage>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _initialize(_platformAddress: address, _vaultAddress: address, _rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<InitializableAbstractStrategy>> +    _collectRewardTokens() <<InitializableAbstractStrategy>> +    _setPTokenAddress(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    _abstractSetPToken(_asset: address, address) <<UniswapV3Strategy>> +    _depositAll() <<UniswapV3StrategyStorage>> +    getPositionBalance(tokenId: uint256): (amount0: uint256, amount1: uint256) <<UniswapV3StrategyStorage>> +    getPositionPrincipal(tokenId: uint256): (amount0: uint256, amount1: uint256) <<UniswapV3StrategyStorage>> +    _setOperator(_operator: address) <<UniswapV3Strategy>> +    _setMaxTVL(_maxTVL: uint256) <<UniswapV3Strategy>> +    _setMaxPositionValueLostThreshold(_maxValueLostThreshold: uint256) <<UniswapV3Strategy>> +    _onlyPoolTokens(addr: address) <<UniswapV3Strategy>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    initialize(_platformAddress: address, _vaultAddress: address, _rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<onlyGovernor, initializer>> <<InitializableAbstractStrategy>> +    collectRewardTokens() <<UniswapV3Strategy>> +    setRewardTokenAddresses(_rewardTokenAddresses: address[]) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    getRewardTokenAddresses(): address[] <<InitializableAbstractStrategy>> +    setPTokenAddress(address, address) <<UniswapV3Strategy>> +    removePToken(uint256) <<UniswapV3Strategy>> +    setHarvesterAddress(_harvesterAddress: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    safeApproveAllTokens() <<onlyGovernor, nonReentrant>> <<UniswapV3Strategy>> +    deposit(address, uint256) <<onlyVault, nonReentrant>> <<UniswapV3Strategy>> +    depositAll() <<onlyVault, nonReentrant>> <<UniswapV3Strategy>> +    withdraw(recipient: address, _asset: address, amount: uint256) <<onlyVault, nonReentrant>> <<UniswapV3Strategy>> +    withdrawAll() <<onlyVault, nonReentrant>> <<UniswapV3Strategy>> +    checkBalance(_asset: address): (balance: uint256) <<UniswapV3Strategy>> +    supportsAsset(_asset: address): bool <<UniswapV3Strategy>> +    initialize(_vaultAddress: address, _poolAddress: address, _nonfungiblePositionManager: address, _helper: address, _swapRouter: address, _operator: address, _maxTVL: uint256, _maxValueLostThreshold: uint256) <<onlyGovernor, initializer>> <<UniswapV3Strategy>> +    setOperator(_operator: address) <<onlyGovernorOrStrategist>> <<UniswapV3Strategy>> +    setReserveStrategy(_asset: address, _reserveStrategy: address) <<onlyGovernorOrStrategist, nonReentrant>> <<UniswapV3Strategy>> +    reserveStrategy(_asset: address): (reserveStrategyAddr: address) <<UniswapV3Strategy>> +    setMinDepositThreshold(_asset: address, _minThreshold: uint256) <<onlyGovernorOrStrategist>> <<UniswapV3Strategy>> +    setRebalancePaused(_paused: bool) <<onlyGovernorOrStrategist>> <<UniswapV3Strategy>> +    setSwapsPaused(_paused: bool) <<onlyGovernorOrStrategist>> <<UniswapV3Strategy>> +    setMaxTVL(_maxTVL: uint256) <<onlyGovernorOrStrategist>> <<UniswapV3Strategy>> +    setMaxPositionValueLostThreshold(_maxValueLostThreshold: uint256) <<onlyGovernorOrStrategist>> <<UniswapV3Strategy>> +    resetLostValue() <<onlyGovernor>> <<UniswapV3Strategy>> +    setRebalancePriceThreshold(minTick: int24, maxTick: int24) <<onlyGovernorOrStrategist>> <<UniswapV3Strategy>> +    setSwapPriceThreshold(minTick: int24, maxTick: int24) <<onlyGovernorOrStrategist>> <<UniswapV3Strategy>> +    getPendingFees(): (amount0: uint256, amount1: uint256) <<UniswapV3Strategy>> +    onERC721Received(address, address, uint256, bytes): bytes4 <<UniswapV3Strategy>> +    resetAllowanceOfTokens() <<onlyGovernor, nonReentrant>> <<UniswapV3Strategy>> +    setLiquidityManagerImpl(newImpl: address) <<onlyGovernor>> <<UniswapV3Strategy>> +    null() <<UniswapV3Strategy>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> PTokenAdded(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> PTokenRemoved(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> Deposit(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> Withdrawal(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenCollected(recipient: address, rewardToken: address, amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenAddressesUpdated(_oldAddresses: address[], _newAddresses: address[]) <<InitializableAbstractStrategy>> +    <<event>> HarvesterAddressesUpdated(_oldHarvesterAddress: address, _newHarvesterAddress: address) <<InitializableAbstractStrategy>> +    <<event>> OperatorChanged(_address: address) <<UniswapV3StrategyStorage>> +    <<event>> LiquidityManagerImplementationUpgraded(_newImpl: address) <<UniswapV3StrategyStorage>> +    <<event>> ReserveStrategyChanged(asset: address, reserveStrategy: address) <<UniswapV3StrategyStorage>> +    <<event>> MinDepositThresholdChanged(asset: address, minDepositThreshold: uint256) <<UniswapV3StrategyStorage>> +    <<event>> RebalancePauseStatusChanged(paused: bool) <<UniswapV3StrategyStorage>> +    <<event>> SwapsPauseStatusChanged(paused: bool) <<UniswapV3StrategyStorage>> +    <<event>> RebalancePriceThresholdChanged(minTick: int24, maxTick: int24) <<UniswapV3StrategyStorage>> +    <<event>> SwapPriceThresholdChanged(minTick: int24, minSwapPriceX96: uint160, maxTick: int24, maxSwapPriceX96: uint160) <<UniswapV3StrategyStorage>> +    <<event>> MaxTVLChanged(maxTVL: uint256) <<UniswapV3StrategyStorage>> +    <<event>> MaxValueLostThresholdChanged(amount: uint256) <<UniswapV3StrategyStorage>> +    <<event>> NetLostValueReset(_by: address) <<UniswapV3StrategyStorage>> +    <<event>> NetLostValueChanged(currentNetLostValue: uint256) <<UniswapV3StrategyStorage>> +    <<event>> PositionValueChanged(tokenId: uint256, initialValue: uint256, currentValue: uint256, delta: int256) <<UniswapV3StrategyStorage>> +    <<event>> AssetSwappedForRebalancing(tokenIn: address, tokenOut: address, amountIn: uint256, amountOut: uint256) <<UniswapV3StrategyStorage>> +    <<event>> UniswapV3LiquidityAdded(tokenId: uint256, amount0Sent: uint256, amount1Sent: uint256, liquidityMinted: uint128) <<UniswapV3StrategyStorage>> +    <<event>> UniswapV3LiquidityRemoved(tokenId: uint256, amount0Received: uint256, amount1Received: uint256, liquidityBurned: uint128) <<UniswapV3StrategyStorage>> +    <<event>> UniswapV3PositionMinted(tokenId: uint256, lowerTick: int24, upperTick: int24) <<UniswapV3StrategyStorage>> +    <<event>> UniswapV3PositionClosed(tokenId: uint256, amount0Received: uint256, amount1Received: uint256) <<UniswapV3StrategyStorage>> +    <<event>> UniswapV3FeeCollected(tokenId: uint256, amount0: uint256, amount1: uint256) <<UniswapV3StrategyStorage>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> nonReentrantView() <<Governable>> +    <<modifier>> onlyVault() <<InitializableAbstractStrategy>> +    <<modifier>> onlyHarvester() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernor() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernorOrStrategist() <<InitializableAbstractStrategy>> +    <<modifier>> onlyGovernorOrStrategist() <<UniswapV3StrategyStorage>> +    <<modifier>> onlyGovernorOrStrategistOrOperator() <<UniswapV3StrategyStorage>> +    constructor() <<UniswapV3StrategyStorage>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> + + + diff --git a/contracts/docs/UniswapV3StrategyStorage.svg b/contracts/docs/UniswapV3StrategyStorage.svg new file mode 100644 index 0000000000..99c8364697 --- /dev/null +++ b/contracts/docs/UniswapV3StrategyStorage.svg @@ -0,0 +1,359 @@ + + + + + + +StorageDiagram + + + +6 + +UniswapV3Strategy <<Contract>> + +slot + +0 + +1-50 + +51 + +52 + +53 + +54 + +55 + +56 + +57 + +58 + +59-156 + +157 + +158 + +159 + +160 + +161 + +162 + +163 + +164 + +165 + +166 + +167 + +168 + +169 + +170 + +171 + +172 + +173 + +174 + +175 + +176 + +type: <inherited contract>.variable (bytes) + +unallocated (30) + +bool: Initializable.initializing (1) + +bool: Initializable.initialized (1) + +uint256[50]: Initializable.______gap (1600) + +unallocated (12) + +address: InitializableAbstractStrategy.platformAddress (20) + +unallocated (12) + +address: InitializableAbstractStrategy.vaultAddress (20) + +mapping(address=>address): InitializableAbstractStrategy.assetToPToken (32) + +address[]: InitializableAbstractStrategy.assetsMapped (32) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_rewardTokenAddress (20) + +uint256: InitializableAbstractStrategy._deprecated_rewardLiquidationThreshold (32) + +unallocated (12) + +address: InitializableAbstractStrategy.harvesterAddress (20) + +address[]: InitializableAbstractStrategy.rewardTokenAddresses (32) + +int256[98]: InitializableAbstractStrategy._reserved (3136) + +unallocated (12) + +IUniswapV3Strategy: UniswapV3StrategyStorage._self (20) + +unallocated (12) + +address: UniswapV3StrategyStorage.operatorAddr (20) + +unallocated (12) + +address: UniswapV3StrategyStorage.token0 (20) + +unallocated (12) + +address: UniswapV3StrategyStorage.token1 (20) + +unallocated (12) + +IStrategy: UniswapV3StrategyStorage.reserveStrategy0 (20) + +unallocated (7) + +bool: UniswapV3StrategyStorage.rebalancePaused (1) + +bool: UniswapV3StrategyStorage.swapsPaused (1) + +uint24: UniswapV3StrategyStorage.poolFee (3) + +IStrategy: UniswapV3StrategyStorage.reserveStrategy1 (20) + +uint256: UniswapV3StrategyStorage.maxTVL (32) + +uint256: UniswapV3StrategyStorage.minDepositThreshold0 (32) + +uint256: UniswapV3StrategyStorage.minDepositThreshold1 (32) + +unallocated (6) + +uint160: UniswapV3StrategyStorage.minSwapPriceX96 (20) + +int24: UniswapV3StrategyStorage.maxRebalanceTick (3) + +int24: UniswapV3StrategyStorage.minRebalanceTick (3) + +unallocated (12) + +uint160: UniswapV3StrategyStorage.maxSwapPriceX96 (20) + +uint256: UniswapV3StrategyStorage.activeTokenId (32) + +uint256: UniswapV3StrategyStorage.netLostValue (32) + +uint256: UniswapV3StrategyStorage.maxPositionValueLostThreshold (32) + +unallocated (12) + +IUniswapV3Pool: UniswapV3StrategyStorage.pool (20) + +unallocated (12) + +INonfungiblePositionManager: UniswapV3StrategyStorage.positionManager (20) + +unallocated (12) + +IUniswapV3Helper: UniswapV3StrategyStorage.helper (20) + +unallocated (12) + +ISwapRouter: UniswapV3StrategyStorage.swapRouter (20) + +mapping(int48=>uint256): UniswapV3StrategyStorage.ticksToTokenId (32) + +mapping(uint256=>Position): UniswapV3StrategyStorage.tokenIdToPosition (32) + + + +1 + +uint256[50]: ______gap <<Array>> + +slot + +1 + +2 + +3-48 + +49 + +50 + +type: variable (bytes) + +uint256 (32) + +uint256 (32) + +---- (1472) + +uint256 (32) + +uint256 (32) + + + +6:8->1 + + + + + +2 + +address[]: assetsMapped <<Array>> +0x4a11f94e20a93c79f6ec743a1954ec4fc2c08429ae2122118bf234b2185c81b8 + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +6:13->2 + + + + + +3 + +address[]: rewardTokenAddresses <<Array>> +0xa2999d817b6757290b50e8ecf3fa939673403dd35c97de392fdb343b4015ce9e + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +6:18->3 + + + + + +4 + +int256[98]: _reserved <<Array>> + +slot + +59 + +60 + +61-154 + +155 + +156 + +type: variable (bytes) + +int256 (32) + +int256 (32) + +---- (3008) + +int256 (32) + +int256 (32) + + + +6:24->4 + + + + + +5 + +Position <<Struct>> + +offset + +0 + +1 + +2 + +3 + +4 + +type: variable (bytes) + +uint256: tokenId (32) + +unallocated (9) + +bool: exists (1) + +int24: upperTick (3) + +int24: lowerTick (3) + +uint128: liquidity (16) + +unallocated (12) + +uint160: sqrtRatioAX96 (20) + +unallocated (12) + +uint160: sqrtRatioBX96 (20) + +uint256: netValue (32) + + + +6:57->5 + + + + + diff --git a/contracts/docs/generate.sh b/contracts/docs/generate.sh index 5f5badbd24..601094d788 100644 --- a/contracts/docs/generate.sh +++ b/contracts/docs/generate.sh @@ -67,6 +67,11 @@ sol2uml .. -v -hv -hf -he -hs -hl -b MorphoCompoundStrategy -o MorphoCompStrateg sol2uml .. -s -d 0 -b MorphoCompoundStrategy -o MorphoCompStrategySquashed.svg sol2uml storage .. -c MorphoCompoundStrategy -o MorphoCompStrategyStorage.svg +# contracts/strategies/uniswap/v3 +sol2uml .. -v -hv -hf -he -hs -hl -hi -b UniswapV3Strategy,UniswapV3LiquidityManager -o UniswapV3StrategyHierarchy.svg +sol2uml .. -s -d 0 -b UniswapV3Strategy -o UniswapV3StrategySquashed.svg +sol2uml storage .. -c UniswapV3Strategy -o UniswapV3StrategyStorage.svg + # contracts/timelock sol2uml .. -v -hv -hf -he -hs -hl -b Timelock -o TimelockHierarchy.svg sol2uml .. -s -d 0 -b Timelock -o TimelockSquashed.svg