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/contracts/interfaces/IUniswapV3Strategy.sol b/contracts/contracts/interfaces/IUniswapV3Strategy.sol
new file mode 100644
index 0000000000..259a34cce4
--- /dev/null
+++ b/contracts/contracts/interfaces/IUniswapV3Strategy.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: agpl-3.0
+pragma solidity ^0.8.0;
+
+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);
+
+ function closeActivePositionOnlyVault() external;
+}
diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol
index db5c7166ab..1596fc85e4 100644
--- a/contracts/contracts/interfaces/IVault.sol
+++ b/contracts/contracts/interfaces/IVault.sol
@@ -76,8 +76,12 @@ interface IVault {
function approveStrategy(address _addr) external;
+ function approveUniswapV3Strategy(address _addr) external;
+
function removeStrategy(address _addr) external;
+ function isStrategySupported(address _addr) external view returns (bool);
+
function setAssetDefaultStrategy(address _asset, address _strategy)
external;
@@ -170,4 +174,9 @@ interface IVault {
function setNetOusdMintForStrategyThreshold(uint256 _threshold) external;
function netOusdMintedForStrategy() external view returns (int256);
+
+ function depositToUniswapV3Reserve(address asset, uint256 amount) external;
+
+ function withdrawFromUniswapV3Reserve(address asset, uint256 amount)
+ external;
}
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/interfaces/uniswap/v3/INonfungiblePositionManager.sol b/contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol
new file mode 100644
index 0000000000..c117bbbf15
--- /dev/null
+++ b/contracts/contracts/interfaces/uniswap/v3/INonfungiblePositionManager.sol
@@ -0,0 +1,146 @@
+// 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..38a4108992
--- /dev/null
+++ b/contracts/contracts/interfaces/uniswap/v3/IUniswapV3Helper.sol
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: agpl-3.0
+pragma solidity ^0.8.0;
+
+import { INonfungiblePositionManager } from "./INonfungiblePositionManager.sol";
+
+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);
+
+ function positionFees(
+ INonfungiblePositionManager positionManager,
+ address poolAddress,
+ uint256 tokenId
+ ) external view returns (uint256 amount0, uint256 amount1);
+
+ 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/mocks/MockStrategy.sol b/contracts/contracts/mocks/MockStrategy.sol
new file mode 100644
index 0000000000..ca39d89b9d
--- /dev/null
+++ b/contracts/contracts/mocks/MockStrategy.sol
@@ -0,0 +1,74 @@
+// 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 {
+ // Do nothing
+ }
+
+ function depositAll() public onlyVault {
+ // Do nothing
+ }
+
+ function withdraw(address _asset, uint256 _amount) public onlyVault {
+ 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
+ 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/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 342f074a8e..6cf74c542e 100644
--- a/contracts/contracts/mocks/MockMintableUniswapPair.sol
+++ b/contracts/contracts/mocks/uniswap/v2/MockMintableUniswapPair.sol
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
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 7030e1e746..504abc6f84 100644
--- a/contracts/contracts/mocks/MockUniswapPair.sol
+++ b/contracts/contracts/mocks/uniswap/v2/MockUniswapPair.sol
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
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 74%
rename from contracts/contracts/mocks/MockUniswapRouter.sol
rename to contracts/contracts/mocks/uniswap/v2/MockUniswapRouter.sol
index 5f57948fbd..30f3d9bcd4 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";
@@ -121,4 +121,36 @@ 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/mocks/uniswap/v3/MockNonfungiblePositionManager.sol b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol
new file mode 100644
index 0000000000..5e3ca804f3
--- /dev/null
+++ b/contracts/contracts/mocks/uniswap/v3/MockNonfungiblePositionManager.sol
@@ -0,0 +1,269 @@
+// 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";
+// solhint-disable-next-line no-console
+import "hardhat/console.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 helper;
+ IMockUniswapV3Pool internal mockPool;
+
+ uint256 internal tokenCount = 0;
+
+ constructor(address _helper, address _mockPool) {
+ helper = 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),
+ p.fee,
+ p.tickLower,
+ p.tickUpper,
+ p.liquidity,
+ 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) = helper.getLiquidityForAmounts(
+ mockPool.mockSqrtPriceX96(),
+ helper.getSqrtRatioAtTick(p.tickLower),
+ helper.getSqrtRatioAtTick(p.tickUpper),
+ params.amount0Desired,
+ params.amount1Desired
+ );
+
+ (amount0, amount1) = helper.getAmountsForLiquidity(
+ mockPool.mockSqrtPriceX96(),
+ helper.getSqrtRatioAtTick(p.tickLower),
+ helper.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) = helper.getLiquidityForAmounts(
+ mockPool.mockSqrtPriceX96(),
+ helper.getSqrtRatioAtTick(p.tickLower),
+ helper.getSqrtRatioAtTick(p.tickUpper),
+ params.amount0Desired,
+ params.amount1Desired
+ );
+
+ (amount0, amount1) = helper.getAmountsForLiquidity(
+ mockPool.mockSqrtPriceX96(),
+ helper.getSqrtRatioAtTick(p.tickLower),
+ helper.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..295990df33
--- /dev/null
+++ b/contracts/contracts/mocks/uniswap/v3/MockUniswapV3Pool.sol
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: agpl-3.0
+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;
+ address public immutable token1;
+ uint24 public immutable fee;
+
+ uint160 public mockSqrtPriceX96;
+ int24 public mockTick;
+ IUniswapV3Helper internal helper;
+
+ constructor(
+ address _token0,
+ address _token1,
+ uint24 _fee,
+ address _helper
+ ) {
+ token0 = _token0;
+ token1 = _token1;
+ fee = _fee;
+ helper = 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 = helper.getSqrtRatioAtTick(tick);
+ }
+
+ function setVal(uint160 sqrtPriceX96, int24 tick) public {
+ 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
+ )
+ {
+ //
+ }
+
+ function feeGrowthGlobal0X128() public view returns (uint256) {
+ return 0;
+ }
+
+ function feeGrowthGlobal1X128() public view returns (uint256) {
+ return 0;
+ }
+}
+
+interface IMockUniswapV3Pool {
+ function setTick(int24 tick) external;
+
+ function mockSqrtPriceX96() external returns (uint160);
+}
diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol
index e7ac9fe1b4..f98ef930fb 100644
--- a/contracts/contracts/proxies/Proxies.sol
+++ b/contracts/contracts/proxies/Proxies.sol
@@ -88,12 +88,19 @@ contract ConvexLUSDMetaStrategyProxy is InitializeGovernedUpgradeabilityProxy {
}
/**
- * @notice MorphoAaveStrategyProxy delegates calls to a MorphoCompoundStrategy implementation
+ * @notice MorphoAaveStrategyProxy delegates calls to a MorphoAaveStrategy implementation
*/
contract MorphoAaveStrategyProxy is InitializeGovernedUpgradeabilityProxy {
}
+/**
+ * @notice UniV3_USDC_USDT_Proxy delegates calls to a UniswapV3Strategy implementation
+ */
+contract UniV3_USDC_USDT_Proxy is InitializeGovernedUpgradeabilityProxy {
+
+}
+
/**
* @notice OETHProxy delegates calls to nowhere for now
*/
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
+
+
+
+### Squashed
+
+
+
+### Storage
+
+
diff --git a/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol
new file mode 100644
index 0000000000..d5401ca79d
--- /dev/null
+++ b/contracts/contracts/strategies/uniswap/UniswapV3LiquidityManager.sol
@@ -0,0 +1,950 @@
+// SPDX-License-Identifier: agpl-3.0
+pragma solidity ^0.8.0;
+
+import { UniswapV3StrategyStorage } from "./UniswapV3StrategyStorage.sol";
+
+import { INonfungiblePositionManager } from "../../interfaces/uniswap/v3/INonfungiblePositionManager.sol";
+import { IVault } from "../../interfaces/IVault.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";
+
+contract UniswapV3LiquidityManager is UniswapV3StrategyStorage {
+ using SafeERC20 for IERC20;
+ using StableMath for uint256;
+
+ /***************************************
+ Position Value
+ ****************************************/
+ /**
+ * @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)
+ 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));
+ }
+
+ /***************************************
+ Rebalance
+ ****************************************/
+ /// Reverts if active position's value is greater than maxTVL
+ function _ensureTVL() internal {
+ require(
+ _getPositionValue(activeTokenId) <= maxTVL,
+ "MaxTVL threshold has been reached"
+ );
+ }
+
+ /**
+ * @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
+ ) internal {
+ 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"
+ );
+ }
+
+ /// 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 {
+ require(!rebalancePaused, "Rebalances are paused");
+ require(
+ minRebalanceTick <= lowerTick && maxRebalanceTick >= upperTick,
+ "Rebalance position out of bounds"
+ );
+ }
+
+ /**
+ * @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.
+ *
+ * @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) {
+ // 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);
+ }
+
+ /**
+ * @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;
+ }
+
+ uint256 currentVal = _getPositionValue(tokenId);
+ uint256 lastVal = tokenIdToPosition[tokenId].netValue;
+
+ if (currentVal == lastVal) {
+ // No change in value
+ return;
+ }
+
+ if (currentVal > lastVal) {
+ _setNetLostValue(currentVal - lastVal, true);
+ } else {
+ _setNetLostValue(lastVal - currentVal, false);
+ }
+
+ // 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;
+ }
+
+ /**
+ * @notice Updates the value of the current position.
+ * Reverts if netLostValue threshold is breached.
+ * @param tokenId Token ID of the position
+ */
+ function _ensureNetValueLostThreshold(uint256 tokenId) internal {
+ _updatePositionNetVal(tokenId);
+ require(
+ netLostValue < maxPositionValueLostThreshold,
+ "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. 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
+ * @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");
+ _rebalanceNotPausedAndWithinLimits(lowerTick, upperTick);
+
+ int48 tickKey = _getTickPositionKey(lowerTick, upperTick);
+ uint256 tokenId = ticksToTokenId[tickKey];
+
+ if (activeTokenId > 0 && activeTokenId != tokenId) {
+ // Close any active position (if it's not the same)
+ _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();
+
+ // Final position value/sanity check
+ _ensureTVL();
+ }
+
+ 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;
+ }
+
+ /**
+ * @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
+ nonReentrant
+ {
+ 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)
+ ];
+
+ if (activeTokenId > 0 && activeTokenId != tokenId) {
+ // Close any active position (if it's not the same)
+ _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();
+
+ // Final position value/sanity check
+ _ensureTVL();
+ }
+
+ /***************************************
+ 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 + 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");
+
+ // Make sure liquidity management is disabled when value lost threshold is breached
+ _ensureNetValueLostThreshold(0);
+
+ 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);
+
+ require(!tokenIdToPosition[tokenId].exists, "Duplicate position");
+
+ ticksToTokenId[tickKey] = tokenId;
+ tokenIdToPosition[tokenId] = Position({
+ exists: true,
+ tokenId: tokenId,
+ liquidity: liquidity,
+ lowerTick: lowerTick,
+ upperTick: upperTick,
+ sqrtRatioAX96: helper.getSqrtRatioAtTick(lowerTick),
+ sqrtRatioBX96: helper.getSqrtRatioAtTick(upperTick),
+ netValue: _getValueOfTokens(amount0, amount1)
+ });
+
+ emit UniswapV3PositionMinted(tokenId, lowerTick, upperTick);
+ emit UniswapV3LiquidityAdded(tokenId, amount0, amount1, liquidity);
+ }
+
+ /**
+ * @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,
+ 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");
+
+ // Make sure liquidity management is disabled when value lost threshold is breached
+ _ensureNetValueLostThreshold(tokenId);
+
+ 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;
+ // Update last known value
+ position.netValue = _getPositionValue(tokenId);
+
+ 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,
+ uint256 minAmount0,
+ uint256 minAmount1
+ )
+ external
+ onlyGovernorOrStrategistOrOperator
+ nonReentrant
+ returns (uint256 amount0, uint256 amount1)
+ {
+ _rebalanceNotPaused();
+
+ // Withdraw enough funds from Reserve strategies
+ _ensureAssetBalances(desiredAmount0, desiredAmount1);
+
+ (, amount0, amount1) = _increasePositionLiquidity(
+ activeTokenId,
+ desiredAmount0,
+ desiredAmount1,
+ minAmount0,
+ minAmount1
+ );
+
+ // Deposit
+ _depositAll();
+
+ // Final position value/sanity check
+ _ensureTVL();
+ }
+
+ /**
+ * @notice Removes liquidity of the position in the pool
+ *
+ * @param tokenId Position NFT's tokenId
+ * @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
+ *
+ * @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");
+
+ // 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
+ .DecreaseLiquidityParams({
+ tokenId: position.tokenId,
+ liquidity: liquidity,
+ amount0Min: minAmount0,
+ amount1Min: minAmount1,
+ deadline: block.timestamp
+ });
+
+ (amount0, amount1) = positionManager.decreaseLiquidity(params);
+
+ position.liquidity -= liquidity;
+ // Update last known value
+ position.netValue = _getPositionValue(tokenId);
+
+ emit UniswapV3LiquidityRemoved(
+ position.tokenId,
+ amount0,
+ amount1,
+ liquidity
+ );
+ }
+
+ /**
+ * @notice Removes liquidity of the active position in the pool
+ *
+ * @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
+ *
+ * @return amount0 Amount of token0 received after liquidation
+ * @return amount1 Amount of token1 received after liquidation
+ */
+ function decreaseActivePositionLiquidity(
+ uint128 liquidity,
+ uint256 minAmount0,
+ uint256 minAmount1
+ )
+ external
+ onlyGovernorOrStrategistOrOperator
+ nonReentrant
+ returns (uint256 amount0, uint256 amount1)
+ {
+ _rebalanceNotPaused();
+
+ (amount0, amount1) = _decreasePositionLiquidity(
+ activeTokenId,
+ liquidity,
+ minAmount0,
+ minAmount1
+ );
+
+ // Deposit
+ _depositAll();
+
+ // Intentionally skipping TVL check since removing liquidity won't cause it to fail
+ }
+
+ /**
+ * @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) {
+ // 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)
+ {
+ (amount0, amount1) = _closePosition(tokenId, minAmount0, minAmount1);
+
+ _depositAll();
+
+ // Intentionally skipping TVL check since removing liquidity won't cause it to fail
+ }
+
+ /**
+ * @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.
+ (amount0, amount1) = _closePosition(activeTokenId, 0, 0);
+
+ // Intentionally skipping TVL check since removing liquidity won't cause it to fail
+ }
+
+ /***************************************
+ 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
+ );
+ }
+ }
+
+ /**
+ * @dev Swaps one token for other and then provides liquidity to pools.
+ *
+ * @param desiredAmount0 Minimum amount of token0 needed
+ * @param desiredAmount1 Minimum amount of token1 needed
+ * @param swapAmountIn Amount of tokens to swap
+ * @param swapMinAmountOut Minimum amount of other tokens expected
+ * @param sqrtPriceLimitX96 Max price limit for swap
+ * @param swapZeroForOne True if swapping from token0 to token1
+ */
+ function _ensureAssetsBySwapping(
+ uint256 desiredAmount0,
+ uint256 desiredAmount1,
+ uint256 swapAmountIn,
+ uint256 swapMinAmountOut,
+ uint160 sqrtPriceLimitX96,
+ bool swapZeroForOne
+ ) internal {
+ require(!swapsPaused, "Swaps are paused");
+
+ uint256 token0Balance = IERC20(token0).balanceOf(address(this));
+ uint256 token1Balance = IERC20(token1).balanceOf(address(this));
+
+ uint256 token0Needed = desiredAmount0 > token0Balance
+ ? desiredAmount0 - token0Balance
+ : 0;
+ uint256 token1Needed = desiredAmount1 > token1Balance
+ ? desiredAmount1 - token1Balance
+ : 0;
+
+ if (swapZeroForOne) {
+ // Amount available in reserve strategies
+ uint256 t1ReserveBal = reserveStrategy1.checkBalance(token1);
+
+ // Only swap when asset isn't available in reserve as well
+ require(token1Needed > 0, "No need for swap");
+ require(
+ token1Needed > t1ReserveBal,
+ "Cannot swap when the asset is available in reserve"
+ );
+ // Additional amount of token0 required for swapping
+ token0Needed += swapAmountIn;
+ // Subtract token1 that we will get from swapping
+ token1Needed = (swapMinAmountOut >= token1Needed)
+ ? 0
+ : (token1Needed - swapMinAmountOut);
+ } else {
+ // Amount available in reserve strategies
+ uint256 t0ReserveBal = reserveStrategy0.checkBalance(token0);
+
+ // Only swap when asset isn't available in reserve as well
+ require(token0Needed > 0, "No need for swap");
+ require(
+ token0Needed > t0ReserveBal,
+ "Cannot swap when the asset is available in reserve"
+ );
+ // Additional amount of token1 required for swapping
+ token1Needed += swapAmountIn;
+ // Subtract token0 that we will get from swapping
+ token0Needed = (swapMinAmountOut >= token0Needed)
+ ? 0
+ : (token0Needed - swapMinAmountOut);
+ }
+
+ // Fund strategy from reserve strategies
+ if (token0Needed > 0) {
+ IVault(vaultAddress).withdrawFromUniswapV3Reserve(
+ token0,
+ token0Needed
+ );
+ }
+
+ if (token1Needed > 0) {
+ IVault(vaultAddress).withdrawFromUniswapV3Reserve(
+ token1,
+ token1Needed
+ );
+ }
+
+ // Swap it
+ uint256 amountReceived = swapRouter.exactInputSingle(
+ ISwapRouter.ExactInputSingleParams({
+ tokenIn: swapZeroForOne ? token0 : token1,
+ tokenOut: swapZeroForOne ? token1 : token0,
+ fee: poolFee,
+ recipient: address(this),
+ deadline: block.timestamp,
+ amountIn: swapAmountIn,
+ amountOutMinimum: swapMinAmountOut,
+ sqrtPriceLimitX96: sqrtPriceLimitX96
+ })
+ );
+
+ emit AssetSwappedForRebalancing(
+ swapZeroForOne ? token0 : token1,
+ swapZeroForOne ? token1 : token0,
+ swapAmountIn,
+ amountReceived
+ );
+ }
+
+ /**
+ * @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
+ */
+ 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);
+
+ // Reset loss counter to include value of fee collected
+ _setNetLostValue(_getValueOfTokens(amount0, amount1), true);
+
+ 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
+ ****************************************/
+ function _abstractSetPToken(address, address) internal override {
+ revert("NO_IMPL");
+ }
+
+ function safeApproveAllTokens() external override {
+ revert("NO_IMPL");
+ }
+
+ function deposit(address, uint256) external override {
+ revert("NO_IMPL");
+ }
+
+ function depositAll() external override {
+ revert("NO_IMPL");
+ }
+
+ function withdrawAll() external override {
+ revert("NO_IMPL");
+ }
+
+ function withdraw(
+ address,
+ address,
+ uint256
+ ) external override {
+ revert("NO_IMPL");
+ }
+
+ function checkBalance(address) external view override returns (uint256) {
+ revert("NO_IMPL");
+ }
+
+ function supportsAsset(address) external view override returns (bool) {
+ revert("NO_IMPL");
+ }
+
+ function setPTokenAddress(address, address) external override {
+ revert("NO_IMPL");
+ }
+
+ function removePToken(uint256) external override {
+ revert("NO_IMPL");
+ }
+
+ function collectRewardTokens() external override {
+ 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..21f1230ef3
--- /dev/null
+++ b/contracts/contracts/strategies/uniswap/UniswapV3Strategy.sol
@@ -0,0 +1,541 @@
+// SPDX-License-Identifier: agpl-3.0
+pragma solidity ^0.8.0;
+
+import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol";
+import { UniswapV3StrategyStorage } from "./UniswapV3StrategyStorage.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,
+ uint256 _maxTVL,
+ uint256 _maxValueLostThreshold
+ ) external onlyGovernor initializer {
+ // NOTE: _self should always be the address of the proxy.
+ // This is used to do `delegatecall` between the this contract and
+ // `UniswapV3LiquidityManager` whenever it's required.
+ _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
+ );
+
+ _setOperator(_operator);
+ _setMaxTVL(_maxTVL);
+ _setMaxPositionValueLostThreshold(_maxValueLostThreshold);
+ }
+
+ /***************************************
+ 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
+ {
+ _onlyPoolTokens(_asset);
+
+ require(
+ IVault(vaultAddress).isStrategySupported(_reserveStrategy),
+ "Unsupported strategy"
+ );
+
+ require(
+ IStrategy(_reserveStrategy).supportsAsset(_asset),
+ "Invalid strategy for asset"
+ );
+
+ if (_asset == token0) {
+ reserveStrategy0 = IStrategy(_reserveStrategy);
+ } else if (_asset == token1) {
+ reserveStrategy1 = IStrategy(_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
+ returns (address reserveStrategyAddr)
+ {
+ if (_asset == token0) {
+ reserveStrategyAddr = address(reserveStrategy0);
+ } else if (_asset == token1) {
+ reserveStrategyAddr = address(reserveStrategy1);
+ }
+ }
+
+ /**
+ * @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);
+
+ if (_asset == token0) {
+ minDepositThreshold0 = _minThreshold;
+ } else if (_asset == token1) {
+ minDepositThreshold1 = _minThreshold;
+ }
+ emit MinDepositThresholdChanged(_asset, _minThreshold);
+ }
+
+ /**
+ * @notice Toggle rebalance methods
+ * @param _paused True if rebalance has to be paused
+ */
+ function setRebalancePaused(bool _paused)
+ external
+ onlyGovernorOrStrategist
+ {
+ rebalancePaused = _paused;
+ 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);
+ }
+
+ /**
+ * @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 {
+ _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 _maxValueLostThreshold Maximum amount in 18 decimals
+ */
+ function setMaxPositionValueLostThreshold(uint256 _maxValueLostThreshold)
+ external
+ onlyGovernorOrStrategist
+ {
+ _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);
+ }
+
+ /**
+ * @notice Reset loss counter
+ * @dev Only governor can call it
+ */
+ function resetLostValue() external onlyGovernor {
+ emit NetLostValueReset(msg.sender);
+ emit NetLostValueChanged(0);
+ netLostValue = 0;
+ }
+
+ /**
+ * @notice Change the rebalance price threshold
+ * @param minTick Minimum price tick index
+ * @param maxTick Maximum price tick index
+ */
+ function setRebalancePriceThreshold(int24 minTick, int24 maxTick)
+ external
+ onlyGovernorOrStrategist
+ {
+ require(minTick < maxTick, "Invalid threshold");
+ minRebalanceTick = minTick;
+ maxRebalanceTick = maxTick;
+ 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,
+ minSwapPriceX96,
+ maxTick,
+ maxSwapPriceX96
+ );
+ }
+
+ /***************************************
+ Deposit/Withdraw
+ ****************************************/
+
+ /// @inheritdoc InitializableAbstractStrategy
+ function deposit(address, uint256)
+ external
+ override
+ onlyVault
+ nonReentrant
+ {
+ /**
+ * 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 {
+ /**
+ * 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 withdraw(
+ address recipient,
+ address _asset,
+ uint256 amount
+ ) external override onlyVault nonReentrant {
+ _onlyPoolTokens(_asset);
+
+ require(activeTokenId == 0, "Active position still open");
+
+ // Transfer requested amount, will revert when low on balance
+ IERC20(_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) {
+ // 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.encodeWithSelector(
+ IUniswapV3Strategy.closeActivePositionOnlyVault.selector
+ )
+ );
+ require(success, "DelegateCall to close position failed");
+ }
+
+ for (uint256 i = 0; i < 2; i++) {
+ IERC20 tokenContract = IERC20(assetsMapped[i]);
+ uint256 tokenBalance = tokenContract.balanceOf(address(this));
+
+ if (tokenBalance > 0) {
+ tokenContract.safeTransfer(vaultAddress, tokenBalance);
+ emit Withdrawal(assetsMapped[i], assetsMapped[i], tokenBalance);
+ }
+ }
+ }
+
+ /***************************************
+ 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) {
+ require(tokenIdToPosition[activeTokenId].exists, "Invalid token");
+
+ (amount0, amount1) = helper.positionFees(
+ positionManager,
+ address(pool),
+ activeTokenId
+ );
+ }
+ }
+
+ /**
+ * @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
+ */
+ function checkBalance(address _asset)
+ external
+ view
+ override
+ returns (uint256 balance)
+ {
+ _onlyPoolTokens(_asset);
+ balance = IERC20(_asset).balanceOf(address(this));
+
+ if (activeTokenId > 0) {
+ require(tokenIdToPosition[activeTokenId].exists, "Invalid token");
+ (uint256 amount0, uint256 amount1) = getPositionBalance(
+ activeTokenId
+ );
+
+ if (_asset == token0) {
+ balance += amount0;
+ } else if (_asset == token1) {
+ balance += amount1;
+ }
+ }
+ }
+
+ /**
+ * @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
+ ****************************************/
+
+ /// @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(
+ address,
+ address,
+ uint256,
+ bytes calldata
+ ) external returns (bytes4) {
+ 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);
+ IERC20(token0).safeApprove(address(swapRouter), type(uint256).max);
+ IERC20(token1).safeApprove(address(swapRouter), type(uint256).max);
+ }
+
+ /**
+ * @notice 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);
+ 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);
+ IERC20(_asset).safeApprove(address(positionManager), type(uint256).max);
+ IERC20(_asset).safeApprove(address(swapRouter), type(uint256).max);
+ }
+
+ /// @inheritdoc InitializableAbstractStrategy
+ function supportsAsset(address _asset)
+ external
+ view
+ override
+ returns (bool)
+ {
+ return _asset == token0 || _asset == token1;
+ }
+
+ /***************************************
+ 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");
+ }
+
+ /// @inheritdoc InitializableAbstractStrategy
+ function collectRewardTokens() external override {
+ // Do nothing
+ }
+
+ /***************************************
+ Proxy to liquidity management
+ ****************************************/
+ /**
+ * @notice Sets the implementation for the liquidity manager
+ * @param newImpl address of the implementation
+ */
+ function setLiquidityManagerImpl(address newImpl) external onlyGovernor {
+ require(
+ Address.isContract(newImpl),
+ "new implementation is not a contract"
+ );
+ bytes32 position = LIQUIDITY_MANAGER_IMPL_POSITION;
+ // solhint-disable-next-line no-inline-assembly
+ assembly {
+ sstore(position, newImpl)
+ }
+ emit LiquidityManagerImplementationUpgraded(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 {
+ 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
+ // 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..aef4f194e7
--- /dev/null
+++ b/contracts/contracts/strategies/uniswap/UniswapV3StrategyStorage.sol
@@ -0,0 +1,260 @@
+// 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 { 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";
+import { IUniswapV3Strategy } from "../../interfaces/IUniswapV3Strategy.sol";
+import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
+
+abstract contract UniswapV3StrategyStorage is InitializableAbstractStrategy {
+ event OperatorChanged(address indexed _address);
+ event LiquidityManagerImplementationUpgraded(address indexed _newImpl);
+ event ReserveStrategyChanged(
+ address indexed asset,
+ address reserveStrategy
+ );
+ event MinDepositThresholdChanged(
+ address indexed asset,
+ uint256 minDepositThreshold
+ );
+ 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 MaxValueLostThresholdChanged(uint256 amount);
+ event NetLostValueReset(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,
+ 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 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;
+ uint256 netValue; // Last recorded net value of the position
+ }
+
+ /// @notice The strategy's proxy contract address
+ /// @dev is set when initialized
+ IUniswapV3Strategy public _self;
+
+ /// @notice The address that can manage the positions on Uniswap V3
+ address public operatorAddr;
+ /// @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
+ /// @notice Reserve strategy for token0
+ IStrategy public reserveStrategy0;
+ /// @notice Reserve strategy for token1
+ IStrategy public reserveStrategy1;
+
+ /// @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;
+
+ /// @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;
+ uint256 public minDepositThreshold1;
+
+ // 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;
+
+ /// @notice Token ID of active Position on the pool. zero, if there are no active LP position
+ uint256 public activeTokenId;
+
+ /// @notice Sum of loss in value of tokens deployed to the pool
+ uint256 public netLostValue;
+
+ /// @notice Max value loss threshold after which rebalances aren't allowed
+ uint256 public maxPositionValueLostThreshold;
+
+ /// @notice Uniswap V3's Pool
+ IUniswapV3Pool public pool;
+
+ /// @notice Uniswap V3's PositionManager
+ INonfungiblePositionManager public positionManager;
+
+ /// @notice A deployed contract that's used to call methods of Uniswap V3's libraries despite version mismatch
+ IUniswapV3Helper public helper;
+
+ /// @notice Uniswap Swap Router
+ ISwapRouter public swapRouter;
+
+ /// @notice A lookup table to find token IDs of position using f(lowerTick, upperTick)
+ mapping(int48 => uint256) public ticksToTokenId;
+
+ /// @notice Maps tokenIDs to their Position object
+ mapping(uint256 => Position) public tokenIdToPosition;
+
+ /// @notice keccak256("OUSD.UniswapV3Strategy.LiquidityManager.impl")
+ bytes32 constant LIQUIDITY_MANAGER_IMPL_POSITION =
+ 0xec676d52175f7cbb4e4ea392c6b70f8946575021aad20479602b98adc56ad62d;
+
+ 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
+ ****************************************/
+
+ /**
+ * @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"
+ );
+ _;
+ }
+
+ /***************************************
+ Shared functions
+ ****************************************/
+ /**
+ * @dev 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 && token0Bal >= minDepositThreshold0) {
+ vault.depositToUniswapV3Reserve(token0, token0Bal);
+ }
+ if (token1Bal > 0 && token1Bal >= minDepositThreshold1) {
+ vault.depositToUniswapV3Reserve(token1, token1Bal);
+ }
+ // Not emitting Deposit events since the Reserve strategies would do so
+ }
+
+ /**
+ * @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
+ */
+ 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
+ );
+ }
+
+ /**
+ * @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
+ */
+ 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/InitializableAbstractStrategy.sol b/contracts/contracts/utils/InitializableAbstractStrategy.sol
index 26f7838df4..51e5d18a01 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 {
@@ -105,7 +103,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable {
}
/**
- * @dev Collect accumulated reward token and send to Vault.
+ * @notice Collect accumulated reward token and send to Harvester.
*/
function collectRewardTokens() external virtual onlyHarvester nonReentrant {
_collectRewardTokens();
@@ -165,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)
@@ -187,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()
@@ -199,24 +197,25 @@ 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
*/
function setPTokenAddress(address _asset, address _pToken)
external
+ virtual
onlyGovernor
{
_setPTokenAddress(_asset, _pToken);
}
/**
- * @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
*/
- 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];
@@ -253,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
@@ -266,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)
@@ -281,26 +280,36 @@ 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;
+ /**
+ * @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
@@ -312,12 +321,12 @@ 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;
/**
- * @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
@@ -329,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/contracts/utils/UniswapV3Helper.sol b/contracts/contracts/utils/UniswapV3Helper.sol
new file mode 100644
index 0000000000..5bad37c35c
--- /dev/null
+++ b/contracts/contracts/utils/UniswapV3Helper.sol
@@ -0,0 +1,302 @@
+// SPDX-License-Identifier: agpl-3.0
+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-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.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.
+ */
+contract UniswapV3Helper {
+ function getAmountsForLiquidity(
+ uint160 sqrtRatioX96,
+ uint160 sqrtRatioAX96,
+ uint160 sqrtRatioBX96,
+ uint128 liquidity
+ ) external 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
+ ) external pure returns (uint128 liquidity) {
+ return
+ LiquidityAmounts.getLiquidityForAmounts(
+ sqrtRatioX96,
+ sqrtRatioAX96,
+ sqrtRatioBX96,
+ amount0,
+ amount1
+ );
+ }
+
+ function getSqrtRatioAtTick(int24 tick)
+ external
+ pure
+ returns (uint160 sqrtPriceX96)
+ {
+ 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 positionTotal(
+ INonfungiblePositionManager positionManager,
+ address poolAddress,
+ uint256 tokenId,
+ uint160 sqrtRatioX96
+ ) external view returns (uint256 amount0, uint256 amount1) {
+ return
+ PositionValue.total(
+ positionManager,
+ poolAddress,
+ tokenId,
+ 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
+/// @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/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol
index c44e8061b6..6c2b0f094f 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 {
@@ -41,6 +42,15 @@ contract VaultAdmin is VaultStorage {
_;
}
+ modifier onlyUniswapV3Strategies() {
+ Strategy memory strategy = strategies[msg.sender];
+ require(
+ strategy.isSupported && strategy.isUniswapV3Strategy,
+ "Caller is not Uniswap V3 Strategy"
+ );
+ _;
+ }
+
/***************************************
Configuration
****************************************/
@@ -126,6 +136,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(
@@ -205,8 +219,29 @@ contract VaultAdmin is VaultStorage {
* @param _addr Address of the strategy to add
*/
function approveStrategy(address _addr) external onlyGovernor {
+ _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 UniswapV3Strategy
+ */
+ 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,
+ _deprecated: 0,
+ isUniswapV3Strategy: isUniswapV3
+ });
allStrategies.push(_addr);
emit StrategyApproved(_addr);
}
@@ -215,7 +250,6 @@ contract VaultAdmin is VaultStorage {
* @notice 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");
@@ -253,6 +287,52 @@ 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.
+ * @param _strategyToAddress Address of Strategy to move assets to.
+ * @param _assets Array of asset address that will be moved
+ * @param _amounts Array of amounts of each corresponding asset to move.
+ */
+ function reallocate(
+ address _strategyFromAddress,
+ address _strategyToAddress,
+ address[] calldata _assets,
+ uint256[] calldata _amounts
+ ) external onlyGovernorOrStrategist {
+ require(
+ strategies[_strategyToAddress].isSupported,
+ "Invalid to Strategy"
+ );
+ require(_assets.length == _amounts.length, "Parameter length mismatch");
+ _withdrawFromStrategy(
+ _strategyToAddress,
+ _strategyFromAddress,
+ _assets,
+ _amounts
+ );
+
+ IStrategy strategyTo = IStrategy(_strategyToAddress);
+ for (uint256 i = 0; i < _assets.length; i++) {
+ require(strategyTo.supportsAsset(_assets[i]), "Asset unsupported");
+ }
+ // Tell new Strategy to deposit into protocol
+ strategyTo.depositAll();
+ }
+
/**
* @notice Deposit multiple assets from the vault into the strategy.
* @param _strategyToAddress Address of the Strategy to deposit assets into.
@@ -462,6 +542,74 @@ 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 depositToUniswapV3Reserve(address asset, uint256 amount)
+ external
+ onlyUniswapV3Strategies
+ nonReentrant
+ {
+ _depositToUniswapV3Reserve(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 _depositToUniswapV3Reserve(
+ address v3Strategy,
+ address asset,
+ uint256 amount
+ ) internal {
+ require(strategies[v3Strategy].isSupported, "Strategy not approved");
+ address reserveStrategy = IUniswapV3Strategy(v3Strategy)
+ .reserveStrategy(asset);
+ require(
+ 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 Uniswap V3 Strategy
+ * @dev Only callable by whitelisted Uniswap V3 strategies
+ * @param asset Address of the token
+ * @param amount Amount of token1 required
+ */
+ function withdrawFromUniswapV3Reserve(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);
+ }
+
/***************************************
Utils
****************************************/
diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol
index e6731e76b8..48bfc7eb59 100644
--- a/contracts/contracts/vault/VaultCore.sol
+++ b/contracts/contracts/vault/VaultCore.sol
@@ -319,7 +319,8 @@ contract VaultCore is VaultStorage {
// strategy
uint256 assetCount = allAssets.length;
for (uint256 i = 0; i < assetCount; ++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;
@@ -330,18 +331,17 @@ contract VaultCore is VaultStorage {
vaultBufferModifier
);
- address depositStrategyAddr = assetDefaultStrategies[
- address(asset)
- ];
+ address depositStrategyAddr = assetDefaultStrategies[assetAddr];
if (depositStrategyAddr != address(0) && allocateAmount > 0) {
IStrategy strategy = IStrategy(depositStrategyAddr);
+
// 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
);
@@ -416,7 +416,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) {
uint256 assetCount = allAssets.length;
@@ -431,7 +431,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) {
uint256 stratCount = allStrategies.length;
@@ -443,7 +443,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
@@ -779,7 +779,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 859b99cf31..e4f6b99990 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";
@@ -48,6 +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
+ );
// Assets supported by the Vault, i.e. Stablecoins
enum UnitConversion {
@@ -70,6 +76,8 @@ contract VaultStorage is Initializable, Governable {
struct Strategy {
bool isSupported;
uint256 _deprecated; // Deprecated storage slot
+ // Set to true if the Strategy is an instance of `UniswapV3Strategy`
+ bool isUniswapV3Strategy;
}
/// @dev mapping of strategy contracts to their configiration
mapping(address => Strategy) internal strategies;
@@ -97,7 +105,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
@@ -149,7 +157,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/deploy/000_mock.js b/contracts/deploy/000_mock.js
index 373ae79553..d293b307b5 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;
@@ -355,6 +356,10 @@ 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);
+
await deploy("MockFrxETHMinter", {
from: deployerAddr,
args: [(await ethers.getContract("MocksfrxETH")).address],
@@ -365,6 +370,26 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => {
return true;
};
+async function deployMocksForUniswapV3Strategy(deploy, deployerAddr) {
+ const v3Helper = await deploy("UniswapV3Helper", {
+ 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", "unit_tests"];
deployMocks.skip = () => isMainnetOrFork;
diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js
index 9d6517d382..778cfbe47c 100644
--- a/contracts/deploy/001_core.js
+++ b/contracts/deploy/001_core.js
@@ -710,6 +710,15 @@ const configureStrategies = async (harvesterProxy, oethHarvesterProxy) => {
threePool.connect(sGovernor).setHarvesterAddress(harvesterProxy.address)
);
+ const uniV3UsdcUsdtProxy = await ethers.getContract("UniV3_USDC_USDT_Proxy");
+ const uniV3UsdcUsdt = await ethers.getContractAt(
+ "UniswapV3Strategy",
+ uniV3UsdcUsdtProxy.address
+ );
+ await withConfirmation(
+ uniV3UsdcUsdt.connect(sGovernor).setHarvesterAddress(harvesterProxy.address)
+ );
+
// OETH Strategies
const fraxEthStrategyProxy = await ethers.getContract("FraxETHStrategyProxy");
const fraxEthStrategy = await ethers.getContractAt(
@@ -1189,6 +1198,107 @@ 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 v3Helper = await ethers.getContract("UniswapV3Helper");
+
+ 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,
+ [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 dUniswapV3Strategy = await deployWithConfirmation("UniswapV3Strategy");
+ const dUniswapV3LiquidityManager = await deployWithConfirmation(
+ "UniswapV3LiquidityManager"
+ );
+
+ await deployWithConfirmation("UniV3_USDC_USDT_Proxy");
+ const uniV3Proxy = await ethers.getContract("UniV3_USDC_USDT_Proxy");
+
+ await withConfirmation(
+ uniV3Proxy["initialize(address,address,bytes)"](
+ dUniswapV3Strategy.address,
+ deployerAddr,
+ []
+ )
+ );
+ log("Initialized UniV3_USDC_USDT_Proxy");
+
+ const uniV3Strat = await ethers.getContractAt(
+ "UniswapV3Strategy",
+ uniV3Proxy.address
+ );
+ await withConfirmation(
+ uniV3Strat
+ .connect(sDeployer)
+ [
+ "initialize(address,address,address,address,address,address,uint256,uint256)"
+ ](
+ vault.address,
+ pool.address,
+ manager.address,
+ v3Helper.address,
+ mockRouter.address,
+ operatorAddr,
+ "1000000000000",
+ "50000000000"
+ )
+ );
+ log("Initialized UniswapV3Strategy");
+
+ await withConfirmation(
+ uniV3Strat
+ .connect(sDeployer)
+ .setLiquidityManagerImpl(dUniswapV3LiquidityManager.address)
+ );
+ log("Initialized UniswapV3LiquidityManager");
+
+ await withConfirmation(
+ uniV3Strat.connect(sDeployer).transferGovernance(governorAddr)
+ );
+ 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(
+ uniV3Strat
+ .connect(sGovernor) // Claim governance with governor
+ .claimGovernance()
+ );
+ log("Claimed governance for UniV3_USDC_USDT_Strategy");
+ }
+
+ return uniV3Strat;
+};
+
const main = async () => {
console.log("Running 001_core deployment...");
await deployOracles();
@@ -1201,6 +1311,7 @@ const main = async () => {
await deployConvexStrategy();
await deployConvexOUSDMetaStrategy();
await deployConvexLUSDMetaStrategy();
+ await deployUniswapV3Strategy();
await deployFraxEthStrategy();
const [harvesterProxy, oethHarvesterProxy] = await deployHarvesters();
await configureVault();
@@ -1218,7 +1329,7 @@ const main = async () => {
main.id = "001_core";
main.dependencies = ["mocks"];
-main.tags = ["unit_tests"];
+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 4fe79afda2..44cb23a379 100644
--- a/contracts/deploy/004_single_asset_staking.js
+++ b/contracts/deploy/004_single_asset_staking.js
@@ -191,7 +191,7 @@ const singleAssetStaking = async ({ getNamedAccounts, deployments }) => {
singleAssetStaking.id = deployName;
singleAssetStaking.dependencies = ["core"];
-
+singleAssetStaking.tags = ["unit_tests"];
/**
* The contract is no longer in use and isn't expected to be updated
*/
diff --git a/contracts/deploy/005_compensation_claims.js b/contracts/deploy/005_compensation_claims.js
index 169f3317c5..edfea63f21 100644
--- a/contracts/deploy/005_compensation_claims.js
+++ b/contracts/deploy/005_compensation_claims.js
@@ -77,7 +77,7 @@ const compensationClaimsDeploy = async ({ getNamedAccounts }) => {
compensationClaimsDeploy.id = deployName;
compensationClaimsDeploy.dependencies = ["core"];
-
+compensationClaimsDeploy.tags = ["unit_tests"];
/**
* The contract is no longer in use and isn't expected to be updated
*/
diff --git a/contracts/deploy/070_uniswap_usdc_usdt_strategy.js b/contracts/deploy/070_uniswap_usdc_usdt_strategy.js
new file mode 100644
index 0000000000..5d924cbdd4
--- /dev/null
+++ b/contracts/deploy/070_uniswap_usdc_usdt_strategy.js
@@ -0,0 +1,195 @@
+const { deploymentWithGovernanceProposal } = require("../utils/deploy");
+const { utils } = require("ethers");
+
+module.exports = deploymentWithGovernanceProposal(
+ {
+ deployName: "070_uniswap_usdc_usdt_strategy",
+ forceDeploy: false,
+ },
+ async ({
+ assetAddresses,
+ deployWithConfirmation,
+ ethers,
+ getTxOpts,
+ withConfirmation,
+ }) => {
+ 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 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(
+ "UniswapV3Strategy"
+ );
+ const dUniV3PoolLiquidityManager = await deployWithConfirmation(
+ "UniswapV3LiquidityManager"
+ );
+ const cUniV3_USDC_USDT_Strategy = await ethers.getContractAt(
+ "UniswapV3Strategy",
+ dUniV3_USDC_USDT_Proxy.address
+ );
+
+ const cAaveProxy = await ethers.getContract("AaveStrategyProxy");
+
+ const cHarvesterProxy = await ethers.getContract("HarvesterProxy");
+ const cHarvester = await ethers.getContractAt(
+ "Harvester",
+ cHarvesterProxy.address
+ );
+
+ // 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,uint256,uint256)";
+ await withConfirmation(
+ cUniV3_USDC_USDT_Strategy.connect(sDeployer)[initFunction](
+ cVaultProxy.address, // Vault
+ assetAddresses.UniV3_USDC_USDT_Pool, // Pool address
+ assetAddresses.UniV3PositionManager, // NonfungiblePositionManager
+ dUniswapV3Helper.address,
+ assetAddresses.UniV3SwapRouter,
+ operatorAddr,
+ utils.parseEther("1000000", 18), // 1M, max TVL
+ utils.parseEther("50000", 18), // 50k, lost value threshold
+ await getTxOpts()
+ )
+ );
+
+ // 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)
+ .transferGovernance(timelockAddr, 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],
+ },
+ // 5. Set Reserve Strategy for USDC
+ {
+ contract: cUniV3_USDC_USDT_Strategy,
+ signature: "setReserveStrategy(address,address)",
+ args: [assetAddresses.USDC, cAaveProxy.address],
+ },
+ // 6. Set Reserve Strategy for USDT
+ {
+ contract: cUniV3_USDC_USDT_Strategy,
+ signature: "setReserveStrategy(address,address)",
+ args: [assetAddresses.USDT, cAaveProxy.address],
+ },
+ // 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 Rebalance Price Threshold
+ {
+ contract: cUniV3_USDC_USDT_Strategy,
+ signature: "setRebalancePriceThreshold(int24,int24)",
+ args: [-1000, 1000],
+ },
+ // 10. Set Swap price threshold
+ {
+ contract: cUniV3_USDC_USDT_Strategy,
+ signature: "setSwapPriceThreshold(int24,int24)",
+ args: [-1000, 1000],
+ },
+ ],
+ };
+ }
+);
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 @@
+
+
+
+
+
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 @@
+
+
+
+
+
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 @@
+
+
+
+
+
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
diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js
index bd47eba154..56a1f6029d 100644
--- a/contracts/hardhat.config.js
+++ b/contracts/hardhat.config.js
@@ -58,6 +58,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";
@@ -209,12 +210,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: {
@@ -338,6 +347,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/package.json b/contracts/package.json
index 063f29fbda..85e8cbe231 100644
--- a/contracts/package.json
+++ b/contracts/package.json
@@ -41,8 +41,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",
"debug": "^4.3.4",
"dotenv": "^10.0.0",
diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js
index 70c19bfcfc..cb01b3f50b 100644
--- a/contracts/test/_fixture.js
+++ b/contracts/test/_fixture.js
@@ -39,7 +39,7 @@ const defaultFixture = deployments.createFixture(async () => {
}
);
- const { governorAddr, timelockAddr } = await getNamedAccounts();
+ const { governorAddr, timelockAddr, operatorAddr } = await getNamedAccounts();
const ousdProxy = await ethers.getContract("OUSDProxy");
const vaultProxy = await ethers.getContract("VaultProxy");
@@ -120,6 +120,33 @@ const defaultFixture = deployments.createFixture(async () => {
const buybackProxy = await ethers.getContract("BuybackProxy");
const buyback = await ethers.getContractAt("Buyback", buybackProxy.address);
+ const UniV3_USDC_USDT_Proxy = await ethers.getContract(
+ "UniV3_USDC_USDT_Proxy"
+ );
+ const UniV3_USDC_USDT_Strategy = await ethers.getContractAt(
+ Array.from(
+ new Set([
+ ...(
+ await ethers.getContractFactory("UniswapV3Strategy")
+ ).interface.format("full"),
+ ...(
+ await ethers.getContractFactory("UniswapV3LiquidityManager")
+ ).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
+ );
+ const UniV3Helper = await ethers.getContract("UniswapV3Helper");
+
let usdt,
dai,
tusd,
@@ -175,6 +202,12 @@ const defaultFixture = deployments.createFixture(async () => {
cvxRewardPool,
LUSDMetaStrategyProxy,
LUSDMetaStrategy,
+ UniV3PositionManager,
+ UniV3_USDC_USDT_Pool,
+ UniV3SwapRouter,
+ mockStrategy,
+ mockStrategy2,
+ mockStrategyDAI,
oethHarvester,
oethDripper,
ConvexEthMetaStrategyProxy,
@@ -242,6 +275,20 @@ const defaultFixture = deployments.createFixture(async () => {
morphoAaveStrategyProxy.address
);
+ UniV3PositionManager = await ethers.getContractAt(
+ "INonfungiblePositionManager",
+ addresses.mainnet.UniV3PositionManager
+ );
+
+ UniV3_USDC_USDT_Pool = await ethers.getContractAt(
+ "IUniswapV3Pool",
+ addresses.mainnet.UniV3_USDC_USDT_Pool
+ );
+
+ UniV3SwapRouter = await ethers.getContractAt(
+ "ISwapRouter",
+ addresses.mainnet.UniV3SwapRouter
+ );
const oethMorphoAaveStrategyProxy = await ethers.getContract(
"OETHMorphoAaveStrategyProxy"
);
@@ -374,6 +421,16 @@ const defaultFixture = deployments.createFixture(async () => {
LUSDMetaStrategyProxy.address
);
+ UniV3PositionManager = await ethers.getContract(
+ "MockNonfungiblePositionManager"
+ );
+ UniV3_USDC_USDT_Pool = await ethers.getContract("MockUniswapV3Pool");
+ mockStrategy = await ethers.getContract("MockStrategy");
+ mockStrategy2 = await ethers.getContract("MockStrategy2");
+ mockStrategyDAI = await ethers.getContract("MockStrategyDAI");
+
+ UniV3SwapRouter = await ethers.getContract("MockUniswapRouter");
+
const fraxEthStrategyProxy = await ethers.getContract(
"FraxETHStrategyProxy"
);
@@ -402,6 +459,7 @@ const defaultFixture = deployments.createFixture(async () => {
let governor = signers[1];
const strategist = signers[0];
const adjuster = signers[0];
+ let operator = signers[3];
let timelock;
const [matt, josh, anna, domen, daniel, franck] = signers.slice(4);
@@ -409,6 +467,7 @@ const defaultFixture = deployments.createFixture(async () => {
if (isFork) {
governor = await impersonateAndFundContract(governorAddr);
timelock = await impersonateAndFundContract(timelockAddr);
+ operator = await impersonateAndFundContract(operatorAddr);
}
await fundAccounts();
if (isFork) {
@@ -444,6 +503,7 @@ const defaultFixture = deployments.createFixture(async () => {
domen,
daniel,
franck,
+ operator,
timelock,
// Contracts
ousd,
@@ -516,6 +576,17 @@ const defaultFixture = deployments.createFixture(async () => {
flipper,
buyback,
wousd,
+
+ // Uniswap V3 Strategy
+ UniV3PositionManager,
+ UniV3_USDC_USDT_Pool,
+ UniV3_USDC_USDT_Strategy,
+ UniV3Helper,
+ UniV3SwapRouter,
+ mockStrategy,
+ mockStrategyDAI,
+ mockStrategy2,
+
//OETH
oethVault,
oeth,
@@ -1076,6 +1147,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],
@@ -1097,6 +1169,8 @@ async function _hardhatSetBalance(address, amount = "10000") {
}
async function impersonateAndFundContract(address, amount = "100000") {
+ address = address?.address || address; // Support passing contracts as well
+
await impersonateAccount(address);
await _hardhatSetBalance(address, amount);
@@ -1513,6 +1587,124 @@ async function rebornFixture() {
return fixture;
}
+function uniswapV3FixtureSetup() {
+ return deployments.createFixture(async () => {
+ const fixture = await defaultFixture();
+
+ const {
+ usdc,
+ usdt,
+ dai,
+ UniV3_USDC_USDT_Strategy,
+ 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);
+ await _approveStrategy(fixture, mockStrategy2);
+ await _approveStrategy(fixture, mockStrategyDAI);
+
+ // Approve Uniswap V3 Strategy
+ await _approveStrategy(fixture, UniV3_USDC_USDT_Strategy, true);
+
+ // Change default strategy to reserve strategies for both USDT and USDC
+ await _setDefaultStrategy(fixture, usdc, mockStrategy);
+ await _setDefaultStrategy(fixture, usdt, mockStrategy);
+
+ // 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
+ );
+ } 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);
+
+ if (!isFork) {
+ UniV3_USDC_USDT_Strategy.connect(sGovernor)
+ // 2 million
+ .setMaxTVL(utils.parseUnits("2", 24));
+ UniV3_USDC_USDT_Strategy.connect(sGovernor).setRebalancePriceThreshold(
+ -10000,
+ 10000
+ );
+ } 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
+ ).setMaxPositionValueLostThreshold(utils.parseUnits("50000", 18));
+ UniV3_USDC_USDT_Strategy.connect(sGovernor).setMaxTVL(
+ utils.parseUnits("2000000", 18)
+ ); // 2M
+ }
+
+ return fixture;
+ });
+}
+
+async function _approveStrategy(fixture, strategy, isUniswapV3) {
+ const { vault, harvester } = fixture;
+ const { governorAddr, timelockAddr } = await getNamedAccounts();
+ const sGovernor = await ethers.provider.getSigner(
+ isFork ? timelockAddr : 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, timelockAddr } = await getNamedAccounts();
+ const sGovernor = await ethers.provider.getSigner(
+ isFork ? timelockAddr : governorAddr
+ );
+ await vault
+ .connect(sGovernor)
+ .setAssetDefaultStrategy(asset.address, strategy.address);
+}
+
async function replaceContractAt(targetAddress, mockContract) {
const signer = (await hre.ethers.getSigners())[0];
const mockCode = await signer.provider.getCode(mockContract.address);
@@ -1545,6 +1737,7 @@ module.exports = {
aaveVaultFixture,
hackedVaultFixture,
rebornFixture,
+ uniswapV3FixtureSetup,
withImpersonatedAccount,
impersonateAndFundContract,
impersonateAccount,
diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js
index f5e30b0b2a..e6ce2aa8f7 100644
--- a/contracts/test/helpers.js
+++ b/contracts/test/helpers.js
@@ -44,6 +44,27 @@ 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) {
@@ -63,6 +84,21 @@ chai.Assertion.addMethod(
*
* @param {Contract} contract - The token contract to check the balance of.
*/
+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) {
@@ -449,6 +485,10 @@ 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,
+ UniV3SwapRouter: addresses.mainnet.UniV3SwapRouter,
};
} else {
const addressMap = {
@@ -487,6 +527,12 @@ 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.fork-test.js b/contracts/test/strategies/uniswap-v3.fork-test.js
new file mode 100644
index 0000000000..b92a4a22af
--- /dev/null
+++ b/contracts/test/strategies/uniswap-v3.fork-test.js
@@ -0,0 +1,865 @@
+const { expect } = require("chai");
+const {
+ uniswapV3FixtureSetup,
+ impersonateAndFundContract,
+ defaultFixture,
+} = require("../_fixture");
+const {
+ forkOnlyDescribe,
+ units,
+ ousdUnits,
+ usdcUnitsFormat,
+ usdtUnitsFormat,
+ daiUnits,
+ daiUnitsFormat,
+ getBlockTimestamp,
+} = require("../helpers");
+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
+ await defaultFixture();
+ });
+
+ this.timeout(0);
+
+ const uniswapV3Fixture = uniswapV3FixtureSetup();
+
+ let fixture;
+ let vault, ousd, usdc, usdt, dai;
+ let reserveStrategy, strategy, pool, positionManager, v3Helper, swapRouter;
+ let timelock;
+ // governor,
+ // strategist,
+ // harvester
+ let operator, josh, matt, daniel, domen, franck;
+
+ beforeEach(async () => {
+ fixture = await uniswapV3Fixture();
+ reserveStrategy = fixture.aaveStrategy;
+ strategy = fixture.UniV3_USDC_USDT_Strategy;
+ pool = fixture.UniV3_USDC_USDT_Pool;
+ positionManager = fixture.UniV3PositionManager;
+ v3Helper = fixture.UniV3Helper;
+ swapRouter = 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;
+ timelock = fixture.timelock;
+ josh = fixture.josh;
+ matt = fixture.matt;
+ daniel = fixture.daniel;
+ domen = fixture.domen;
+ franck = fixture.franck;
+ });
+
+ async function setRebalancePriceThreshold(lowerTick, upperTick) {
+ await strategy
+ .connect(timelock)
+ .setRebalancePriceThreshold(lowerTick, upperTick);
+ }
+
+ async function setMaxTVL(maxTvl) {
+ await strategy.connect(timelock).setMaxTVL(utils.parseUnits(maxTvl, 18));
+ }
+
+ async function setMaxPositionValueLostThreshold(maxLossThreshold) {
+ await strategy
+ .connect(timelock)
+ .setMaxPositionValueLostThreshold(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.
+ // Gotta update the tests before that
+
+ 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,
+ maxUSDC.mul(9900).div(10000),
+ maxUSDT.mul(9900).div(10000),
+ 0,
+ 0,
+ 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,
+ };
+ };
+
+ const increaseLiquidity = async (tokenId, usdcAmount, usdtAmount) => {
+ 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,
+ 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();
+
+ const [tokenId, amount0Minted, amount1Minted, liquidityMinted] =
+ events.find((e) => e.event == "UniswapV3LiquidityAdded").args;
+
+ return {
+ tokenId,
+ amount0Minted,
+ amount1Minted,
+ liquidityMinted,
+ tx,
+ };
+ };
+
+ 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(1e6);
+ 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
+ 100, // fee
+ user.address, // recipient
+ (await getBlockTimestamp()) + 10, // 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);
+
+ const [, activeTick] = await pool.slot0();
+ const lowerTick = activeTick - 1000;
+ const upperTick = activeTick + 1000;
+
+ const { tokenId, amount0Minted, amount1Minted, liquidityMinted, tx } =
+ await mintLiquidity(lowerTick, upperTick, "100000", "100000");
+
+ // 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");
+
+ // 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(storedPosition.netValue).to.equal(
+ amount0Minted.add(amount1Minted).mul(BigNumber.from(1e12))
+ );
+ 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
+ .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.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);
+ 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(storedPosition.netValue).to.equal(
+ amount0Minted.add(amount1Minted).mul(BigNumber.from(1e12))
+ );
+ 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(storedPosition.netValue).to.equal(
+ amount0Minted.add(amount1Minted).mul(BigNumber.from(1e12))
+ );
+ 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);
+
+ const [, activeTick] = await pool.slot0();
+ const lowerTick = activeTick - 1003;
+ const upperTick = activeTick + 1005;
+
+ const amount = "100000";
+ const amountUnits = BigNumber.from(amount).mul(10 ** 6);
+
+ // Mint position
+ 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(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 {
+ 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);
+ 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"
+ );
+ });
+
+ 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");
+ 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),
+ "Should have no active position"
+ );
+
+ // Check balance on strategy
+ const usdcBalAfter = await strategy.checkBalance(usdc.address);
+ const usdtBalAfter = await strategy.checkBalance(usdt.address);
+ await expect(strategy).to.have.an.approxBalanceOf(usdcBalAfter, usdc);
+ await expect(strategy).to.have.an.approxBalanceOf(usdtBalAfter, usdt);
+ });
+
+ it("Should collect fees", 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.activeTokenId()).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 fee amounts
+ let [fee0, fee1] = await strategy.getPendingFees();
+ expect(fee0).to.be.gt(0);
+ expect(fee1).to.be.gt(0);
+
+ // Collect fees
+ await strategy.connect(operator).collectFees();
+ [fee0, fee1] = await strategy.getPendingFees();
+ expect(fee0).to.equal(0);
+ expect(fee1).to.equal(0);
+ });
+
+ it("Should not mint if net loss threshold is breached", async () => {
+ const [, activeTick] = await pool.slot0();
+ 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 } =
+ 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);
+
+ // 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.00000000000001");
+ 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 setMaxPositionValueLostThreshold("1000000000000000");
+ const {
+ tx: increaseTx,
+ amount0Added,
+ amount1Added,
+ liquidityAdded,
+ } = await increaseLiquidity(tokenId, "1", "1");
+ await expect(increaseTx).to.have.emittedEvent("PositionValueChanged");
+
+ 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)
+ );
+ });
+
+ 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;
+
+ const slot0 = await hre.network.provider.request({
+ method: "eth_getStorageAt",
+ params: [pool.address, "0x0"],
+ });
+
+ // Mint position
+ const amount = "100000";
+ const { tokenId, tx } = 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);
+
+ // 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.00000000000001");
+ 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
+ await strategy.connect(operator).closePosition(tokenId, "0", "0");
+ });
+
+ it("netLostValue will catch possible pool tilts", async () => {
+ const [, activeTick] = await pool.slot0();
+ const lowerTick = activeTick;
+ const upperTick = activeTick + 1;
+ let drainLoops = 10;
+ while (drainLoops > 0) {
+ const amount = "10000";
+ await mintLiquidity(lowerTick, upperTick, amount, amount);
+ // Mint position
+ // Do some big swaps to move active tick
+ await _swap(daniel, "500000", false);
+ await _swap(josh, "500000", false);
+
+ await mintLiquidity(lowerTick, upperTick, amount, amount);
+
+ expect(await strategy.netLostValue()).gte(
+ 0,
+ "Expected netLostValue to be greater than 0"
+ );
+
+ await _swap(daniel, "500000", true);
+ await _swap(josh, "500000", true);
+ drainLoops--;
+ }
+ });
+ });
+
+ 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);
+
+ // 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"
+ );
+ 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.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
new file mode 100644
index 0000000000..951340249e
--- /dev/null
+++ b/contracts/test/strategies/uniswap-v3.js
@@ -0,0 +1,1679 @@
+const { expect } = require("chai");
+const {
+ uniswapV3FixtureSetup,
+ impersonateAndFundContract,
+ defaultFixture,
+} = require("../_fixture");
+const {
+ units,
+ ousdUnits,
+ expectApproxSupply,
+ usdcUnits,
+ usdtUnits,
+} = require("../helpers");
+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
+ await defaultFixture();
+ });
+
+ // 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);
+
+ await fixture.UniV3_USDC_USDT_Strategy.connect(
+ governor
+ ).setMaxPositionValueLostThreshold(ousdUnits("50000", 18));
+
+ await fixture.UniV3_USDC_USDT_Strategy.connect(
+ governor
+ ).setSwapPriceThreshold(-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;
+ });
+
+ // let fixture;
+ let vault, ousd, usdc, usdt, dai;
+ let reserveStrategy,
+ strategy,
+ 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);
+ };
+
+ 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;
+ // swapRotuer = _fixture.UniV3SwapRouter;
+ ousd = _fixture.ousd;
+ usdc = _fixture.usdc;
+ usdt = _fixture.usdt;
+ dai = _fixture.dai;
+ vault = _fixture.vault;
+ 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(async () => {
+ _destructureFixture(await uniswapV3Fixture());
+ 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("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());
+ });
+
+ 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.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);
+
+ // 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("Admin functions", () => {
+ beforeEach(async () => {
+ _destructureFixture(await uniswapV3Fixture());
+ });
+
+ 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");
+ await expect(
+ strategy.connect(josh).setOperator(addr1)
+ ).to.be.revertedWith("Caller is not the Strategist or Governor");
+ });
+ });
+
+ describe("setReserveStrategy()", () => {
+ beforeEach(async () => {
+ _destructureFixture(await uniswapV3Fixture());
+ });
+
+ 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()", () => {
+ beforeEach(async () => {
+ _destructureFixture(await uniswapV3Fixture());
+ });
+
+ 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(governor)
+ .setMinDepositThreshold(dai.address, "2000")
+ ).to.be.revertedWith("Unsupported asset");
+ });
+ });
+ });
+
+ 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;
+ 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()", () => {
+ 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;
+ 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("setMaxTVL()", async () => {
+ it("Should change max TVL", async () => {
+ const newVal = ousdUnits("12345");
+ const tx = await strategy.connect(governor).setMaxTVL(newVal);
+
+ 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("NetLostValueReset", [
+ 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 () {
+ 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();
+ };
+
+ 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());
+ });
+
+ 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);
+
+ const reserve0Bal = await reserveStrategy.checkBalance(usdc.address);
+ const reserve1Bal = await reserveStrategy.checkBalance(usdt.address);
+
+ // Mint
+ const { events } = await mintLiquidity({
+ amount0: "100000",
+ amount1: "100000",
+ 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)
+ );
+
+ // 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 () => {
+ 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 () => {
+ await 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);
+ await 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);
+ await 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(ousdUnits("100000"));
+
+ await expect(
+ strategy
+ .connect(operator)
+ .rebalance(
+ usdcUnits("100000"),
+ usdtUnits("100000"),
+ "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);
+
+ await 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 () => {
+ await expect(
+ strategy
+ .connect(operator)
+ .rebalance(
+ usdcUnits("100000"),
+ usdtUnits("100000"),
+ "0",
+ "0",
+ "0",
+ "0",
+ "200",
+ "100"
+ )
+ ).to.be.revertedWith("Invalid tick range");
+ });
+ });
+
+ describe("IncreaseLiquidity", () => {
+ beforeEach(async () => {
+ _destructureFixture(await activePositionFixture());
+ });
+
+ 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;
+
+ // 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 expect(
+ strategy
+ .connect(user)
+ .increaseActivePositionLiquidity("1", "1", "0", "0")
+ ).to.not.be.reverted;
+ }
+
+ 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 () => {
+ await strategy
+ .connect(operator)
+ .closePosition(await strategy.activeTokenId(), 0, 0);
+ await 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);
+ await 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);
+
+ await 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"));
+
+ await expect(
+ strategy
+ .connect(operator)
+ .increaseActivePositionLiquidity("1", "1", "0", "0")
+ ).to.be.revertedWith("MaxTVL threshold has been reached");
+ });
+ });
+
+ describe("DecreaseLiquidity/ClosePosition", () => {
+ beforeEach(async () => {
+ _destructureFixture(await activePositionFixture());
+ });
+
+ it("Should close active position during a mint", async () => {
+ const tokenId = await strategy.activeTokenId();
+
+ // Mint position in a different tick range
+ await mintLiquidity({
+ amount0: "100000",
+ amount1: "100000",
+ lowerTick: "-50",
+ upperTick: "5000",
+ });
+
+ 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 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 withdrawAll", async () => {
+ const tokenId = await strategy.activeTokenId();
+
+ await strategy
+ .connect(await impersonateAndFundContract(vault.address))
+ .withdrawAll();
+
+ 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("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(
+ // 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).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("Swap And Rebalance", () => {
+ let impersonatedVault;
+ beforeEach(async () => {
+ _destructureFixture(await liquidityManagerFixture());
+ impersonatedVault = await impersonateAndFundContract(vault.address);
+ });
+
+ 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);
+
+ const amount = "100000";
+ const swapAmountIn = "100000";
+ const swapMinAmountOut = "100000";
+ const sqrtPriceLimitX96 = await helper.getSqrtRatioAtTick(2);
+ const swapZeroForOne = true;
+
+ 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 swap token1 for token0 during rebalance", async () => {
+ // Move all USDC out of reserve
+ await drainFromReserve(usdc);
+
+ 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 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 & Balances", () => {
+ beforeEach(async () => {
+ _destructureFixture(await activePositionFixture());
+ });
+
+ it("Should include active position in balances", async () => {
+ 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 () => {
+ 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();
+ 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]) {
+ await 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", () => {
+ 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("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,
+ });
+
+ const currentValue = (await strategy.checkBalance(usdc.address))
+ .add(await strategy.checkBalance(usdt.address))
+ .mul(1e12);
+
+ expect(currentValue).to.be.gt(initialValue);
+
+ // 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
+ 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 revert if beyond threshold (during mint)", async () => {
+ 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");
+ });
+
+ it("Should revert if beyond threshold (when increasing liquidity)", async () => {
+ await _setNetLossVal("999999999999999999999999999999999999999999999");
+
+ await expect(
+ strategy
+ .connect(operator)
+ .increaseActivePositionLiquidity("1", "1", "0", "0")
+ ).to.be.revertedWith("Over max value loss threshold");
+ });
+ });
+ });
+});
diff --git a/contracts/test/vault/vault.fork-test.js b/contracts/test/vault/vault.fork-test.js
index 9888e372be..57f2764759 100644
--- a/contracts/test/vault/vault.fork-test.js
+++ b/contracts/test/vault/vault.fork-test.js
@@ -306,6 +306,7 @@ forkOnlyDescribe("ForkTest: Vault", function () {
"0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAaveStrategy
// TODO: Hard-code these after deploy
//"0x7A192DD9Cc4Ea9bdEdeC9992df74F1DA55e60a19", // LUSD MetaStrategy
+ "0xF4632427B2877c4c12670B5c75F794BCe16281FA", // USDC<>USDT Uniswap V3 Strategy
];
for (const s of strategies) {
@@ -332,6 +333,7 @@ forkOnlyDescribe("ForkTest: Vault", function () {
"0x9c459eeb3FA179a40329b81C1635525e9A0Ef094",
"0x5A4eEe58744D1430876d5cA93cAB5CcB763C037D", // Morpho
"0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAave
+ "0x050c4FcA28725d975c2896682eBD2905D2E58E84", // USDC<>USDT Uniswap V3 Strategy
]).to.include(await vault.assetDefaultStrategies(usdt.address));
});
@@ -344,6 +346,7 @@ forkOnlyDescribe("ForkTest: Vault", function () {
"0x9c459eeb3FA179a40329b81C1635525e9A0Ef094",
"0x5A4eEe58744D1430876d5cA93cAB5CcB763C037D", // Morpho
"0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAave
+ "0x050c4FcA28725d975c2896682eBD2905D2E58E84", // USDC<>USDT Uniswap V3 Strategy
]).to.include(await vault.assetDefaultStrategies(usdc.address));
});
@@ -356,6 +359,7 @@ forkOnlyDescribe("ForkTest: Vault", function () {
"0x9c459eeb3FA179a40329b81C1635525e9A0Ef094",
"0x5A4eEe58744D1430876d5cA93cAB5CcB763C037D", // Morpho
"0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAave
+ "0x050c4FcA28725d975c2896682eBD2905D2E58E84", // USDC<>USDT Uniswap V3 Strategy
]).to.include(await vault.assetDefaultStrategies(dai.address));
});
diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js
index 130f4ffcb2..e8dfcce045 100644
--- a/contracts/utils/addresses.js
+++ b/contracts/utils/addresses.js
@@ -148,6 +148,13 @@ 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.UniV3SwapRouter =
+ "0xE592427A0AEce92De3Edee1F18E0157C05861564";
+
// OUSD Governance
addresses.mainnet.GovernorFive = "0x3cdd07c16614059e66344a7b579dab4f9516c0b6";
addresses.mainnet.Timelock = "0x35918cDE7233F2dD33fA41ae3Cb6aE0e42E0e69F";
diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js
index 88d797a86b..d70dabd98b 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 {
diff --git a/contracts/yarn.lock b/contracts/yarn.lock
index e9f470f90e..f8e56e6c94 100644
--- a/contracts/yarn.lock
+++ b/contracts/yarn.lock
@@ -1183,10 +1183,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"
@@ -1563,22 +1563,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"
@@ -3078,7 +3082,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==
@@ -5676,13 +5680,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.14.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.14.1.tgz#4dd252717f4987d8221c4f6fd08233b7f4251fd8"