From a7362d74303fe61a733cd2716a96b2a2cf5fde15 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:19:42 +0200 Subject: [PATCH 01/23] fix: refactor swapRewardTokens as per @jmonteer comment --- contracts/Joint.sol | 48 ++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/contracts/Joint.sol b/contracts/Joint.sol index c0ac8a6..ae94997 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -291,16 +291,9 @@ abstract contract Joint { (uint256 currentBalanceA, uint256 currentBalanceB) = _closePosition(); // 2. SELL REWARDS FOR WANT - tokenAmount[] memory swappedToAmounts = swapRewardTokens(); - for (uint256 i = 0; i < swappedToAmounts.length; i++) { - address rewardSwappedTo = swappedToAmounts[i].token; - uint256 rewardSwapOutAmount = swappedToAmounts[i].amount; - if (rewardSwappedTo == tokenA) { - currentBalanceA = currentBalanceA + rewardSwapOutAmount; - } else if (rewardSwappedTo == tokenB) { - currentBalanceB = currentBalanceB + rewardSwapOutAmount; - } - } + (uint256 rewardsSwappedToA, uint256 rewardsSwappedToB) = swapRewardTokens(); + currentBalanceA += rewardsSwappedToA; + currentBalanceB += rewardsSwappedToB; // 3. REBALANCE PORTFOLIO // Calculate rebalance operation @@ -679,36 +672,28 @@ abstract contract Joint { function withdrawLP() internal virtual {} - struct tokenAmount { - address token; - uint256 amount; - } - /* * @notice * Function available internally swapping amounts necessary to swap rewards - * @return tokenAmount array of the swap path followed + * @return amounts exchanged to tokenA and tokenB */ function swapRewardTokens() internal virtual - returns (tokenAmount[] memory) + returns (uint256 swappedToA, uint256 swappedToB) { - tokenAmount[] memory _swapToAmounts = new tokenAmount[]( - rewardTokens.length - ); + for (uint256 i = 0; i < rewardTokens.length; i++) { address reward = rewardTokens[i]; uint256 _rewardBal = IERC20(reward).balanceOf(address(this)); // If the reward token is either A or B, don't swap if (reward == tokenA || reward == tokenB || _rewardBal == 0) { - _swapToAmounts[i] = tokenAmount(reward, 0); + continue; // If the referenceToken is either A or B, swap rewards against it - } else if (tokenA == referenceToken || tokenB == referenceToken) { - _swapToAmounts[i] = tokenAmount( - referenceToken, - swap(reward, referenceToken, _rewardBal) - ); + } else if (tokenA == referenceToken) { + swappedToA += swap(reward, referenceToken, _rewardBal); + } else if (tokenB == referenceToken) { + swappedToB += swap(reward, referenceToken, _rewardBal); } else { // Assume that position has already been liquidated (uint256 ratioA, uint256 ratioB) = getRatios( @@ -718,13 +703,14 @@ abstract contract Joint { investedB ); address swapTo = (ratioA >= ratioB) ? tokenB : tokenA; - _swapToAmounts[i] = tokenAmount( - swapTo, - swap(reward, swapTo, _rewardBal) - ); + if (ratioA >= ratioB) { + swappedToB += swap(reward, tokenB, _rewardBal); + } else { + swappedToA += swap(reward, tokenA, _rewardBal); + } } } - return _swapToAmounts; + return (swappedToA, swappedToB); } function swap( From 6c7ff3e8243acac4b043da592c510eee741e0f1a Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:21:28 +0200 Subject: [PATCH 02/23] =?UTF-8?q?fix:=20removed=20else=20case=20in=20findS?= =?UTF-8?q?wapTo=20as=20it=20wasn=C2=B4t=20adding=20any=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/Joint.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/Joint.sol b/contracts/Joint.sol index ae94997..d97d185 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -630,13 +630,11 @@ abstract contract Joint { return tokenB; } else if (tokenB == token) { return tokenA; - } else if (_isReward(token)) { + } else { if (tokenA == referenceToken || tokenB == referenceToken) { return referenceToken; } return tokenA; - } else { - revert("!swapTo"); } } From c85bdd8d3070a565b6db6bfc7180c60324e66257 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:29:38 +0200 Subject: [PATCH 03/23] fix: gas savings as per @jmonteer comments --- contracts/Joint.sol | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contracts/Joint.sol b/contracts/Joint.sol index d97d185..71d5588 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -649,11 +649,13 @@ abstract contract Joint { internal view returns (address[] memory _path) - { + { + address _tokenA = tokenA; + address _tokenB = tokenB; bool isReferenceToken = _token_in == address(referenceToken) || _token_out == address(referenceToken); - bool is_internal = (_token_in == tokenA && _token_out == tokenB) || - (_token_in == tokenB && _token_out == tokenA); + bool is_internal = (_token_in == _tokenA && _token_out == _tokenB) || + (_token_in == _tokenB && _token_out == _tokenA); _path = new address[](isReferenceToken || is_internal ? 2 : 3); _path[0] = _token_in; if (isReferenceToken || is_internal) { @@ -680,17 +682,18 @@ abstract contract Joint { virtual returns (uint256 swappedToA, uint256 swappedToB) { - + address _tokenA = tokenA; + address _tokenB = tokenB; for (uint256 i = 0; i < rewardTokens.length; i++) { address reward = rewardTokens[i]; uint256 _rewardBal = IERC20(reward).balanceOf(address(this)); // If the reward token is either A or B, don't swap - if (reward == tokenA || reward == tokenB || _rewardBal == 0) { + if (reward == _tokenA || reward == _tokenB || _rewardBal == 0) { continue; // If the referenceToken is either A or B, swap rewards against it - } else if (tokenA == referenceToken) { + } else if (_tokenA == referenceToken) { swappedToA += swap(reward, referenceToken, _rewardBal); - } else if (tokenB == referenceToken) { + } else if (_tokenB == referenceToken) { swappedToB += swap(reward, referenceToken, _rewardBal); } else { // Assume that position has already been liquidated @@ -700,11 +703,11 @@ abstract contract Joint { investedA, investedB ); - address swapTo = (ratioA >= ratioB) ? tokenB : tokenA; + address swapTo = (ratioA >= ratioB) ? _tokenB : _tokenA; if (ratioA >= ratioB) { - swappedToB += swap(reward, tokenB, _rewardBal); + swappedToB += swap(reward, _tokenB, _rewardBal); } else { - swappedToA += swap(reward, tokenA, _rewardBal); + swappedToA += swap(reward, _tokenA, _rewardBal); } } } From e7ae27f792c2de089bef089b54bd26d13332b0c3 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:30:23 +0200 Subject: [PATCH 04/23] fix: change modifier of setCRVPool to onlyGovernance --- contracts/DEXes/UniV3StablesJoint.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index 0d50f0e..0305e7b 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -183,7 +183,7 @@ contract UniV3StablesJoint is NoHedgeJoint { * Function available for vault managers to set the CRV pool to use for swaps * @param newPool, new CRV pool address to use */ - function setCRVPool(address newPool) external onlyVaultManagers { + function setCRVPool(address newPool) external onlyGovernance { crvPool = newPool; } From 3a40e2a768b2d5efbc49d675916d72e70d91ef34 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:34:02 +0200 Subject: [PATCH 05/23] fix: minor gas savings --- contracts/DEXes/UniV3StablesJoint.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index 0305e7b..861d130 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -316,10 +316,10 @@ contract UniV3StablesJoint is NoHedgeJoint { uint256 amount1Owed, bytes calldata data ) external { + IUniswapV3Pool _pool = IUniswapV3Pool(pool); // Only the pool can use this function - require(msg.sender == pool); // dev: callback only called by pool + require(msg.sender == _pool); // dev: callback only called by pool // Send the required funds to the pool - IUniswapV3Pool _pool = IUniswapV3Pool(pool); IERC20(_pool.token0()).safeTransfer(address(_pool), amount0Owed); IERC20(_pool.token1()).safeTransfer(address(_pool), amount1Owed); } From 66492cebbc4756eeb58f4b187d9ec7e7dc626c44 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:34:57 +0200 Subject: [PATCH 06/23] fix: convert pool to address --- contracts/DEXes/UniV3StablesJoint.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index 861d130..1929c2a 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -318,7 +318,7 @@ contract UniV3StablesJoint is NoHedgeJoint { ) external { IUniswapV3Pool _pool = IUniswapV3Pool(pool); // Only the pool can use this function - require(msg.sender == _pool); // dev: callback only called by pool + require(msg.sender == address(_pool)); // dev: callback only called by pool // Send the required funds to the pool IERC20(_pool.token0()).safeTransfer(address(_pool), amount0Owed); IERC20(_pool.token1()).safeTransfer(address(_pool), amount1Owed); From 799f98c3511dc8ff44f789b9b89c2016cb1895be Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:46:57 +0200 Subject: [PATCH 07/23] chore: change getReward for collectOwedTokens and burn 0 LP before harvesting the rewards manually --- contracts/DEXes/UniV3StablesJoint.sol | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index 1929c2a..657c670 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -363,7 +363,7 @@ contract UniV3StablesJoint is NoHedgeJoint { * Function claiming the earned rewards for the joint, sends the tokens to the joint * contract */ - function getReward() internal override { + function collectOwedTokens() internal { IUniswapV3Pool(pool).collect( address(this), minTick, @@ -373,6 +373,8 @@ contract UniV3StablesJoint is NoHedgeJoint { ); } + function getReward() internal override {} + /* * @notice * Function used internally to open the LP position in the uni v3 pool: @@ -441,7 +443,7 @@ contract UniV3StablesJoint is NoHedgeJoint { */ function burnLP(uint256 amount) internal override { IUniswapV3Pool(pool).burn(minTick, maxTick, uint128(amount)); - getReward(); + collectOwedTokens(); // If entire position is closed, re-set the min and max ticks IUniswapV3Pool.PositionInfo memory positionInfo = _positionInfo(); if (positionInfo.liquidity == 0){ @@ -479,13 +481,8 @@ contract UniV3StablesJoint is NoHedgeJoint { int24 _minTick, int24 _maxTick ) external onlyVaultManagers { - IUniswapV3Pool(pool).collect( - address(this), - _minTick, - _maxTick, - type(uint128).max, - type(uint128).max - ); + IUniswapV3Pool(pool).burn(minTick, maxTick, 0); + collectOwedTokens(); } /* From 310c7f7e15140a4aa93c5b9789192f89b9ca6061 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:47:47 +0200 Subject: [PATCH 08/23] fix: minor gas saving --- contracts/DEXes/UniV3StablesJoint.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index 657c670..af861e7 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -338,10 +338,10 @@ contract UniV3StablesJoint is NoHedgeJoint { int256 amount1Delta, bytes calldata data ) external { + IUniswapV3Pool _pool = IUniswapV3Pool(pool); // Only the pool can use this function - require(msg.sender == address(pool)); // dev: callback only called by pool + require(msg.sender == address(_pool)); // dev: callback only called by pool - IUniswapV3Pool _pool = IUniswapV3Pool(pool); uint256 amountIn; address tokenIn; From 7fdfe5c2d718cc9e67bb6fd4666946d1a5221065 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:49:07 +0200 Subject: [PATCH 09/23] feat: add revert if CRV index is not found --- contracts/DEXes/UniV3StablesJoint.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index af861e7..68b757f 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -612,6 +612,8 @@ contract UniV3StablesJoint is NoHedgeJoint { return int128(1); } else if (_pool.coins(2) == _token) { return int128(2); + } else { + revert(); } } From e4105863a1f42c74d04203db72b5637ed49df3a0 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:50:21 +0200 Subject: [PATCH 10/23] fix: comment typo --- contracts/DEXes/UniV3StablesJoint.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index 68b757f..f321a40 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -567,7 +567,7 @@ contract UniV3StablesJoint is NoHedgeJoint { // Order of swap bool zeroForOne = _tokenFrom < _tokenTo; - // Use the uniswap helper view to simluate the swapin the uni v3 pool + // Use the uniswap helper view to simulate the swap in the uni v3 pool (int256 _amount0, int256 _amount1, , ) = UniswapHelperViews.simulateSwap( // pool to use IUniswapV3Pool(pool), From 4007572eaaefe8f7a9a7ee0226cde40a6dd5a044 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 16:56:38 +0200 Subject: [PATCH 11/23] fix: comment typo --- contracts/Joint.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/Joint.sol b/contracts/Joint.sol index 71d5588..691902c 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -864,8 +864,7 @@ abstract contract Joint { /* * @notice * Function available to governance sweeping a specified token but tokenA and B - * @param expectedBalanceA, expected balance of tokenA to receive - * @param expectedBalanceB, expected balance of tokenB to receive + * @param _token, address of the token to sweep */ function sweep(address _token) external onlyGovernance { require(_token != address(tokenA)); From 384526e443a7471c8114b84b2273cab7d4cb0396 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 18 May 2022 19:46:49 +0200 Subject: [PATCH 12/23] chore: removed unused line --- contracts/Joint.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Joint.sol b/contracts/Joint.sol index 691902c..b01ba85 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -703,7 +703,7 @@ abstract contract Joint { investedA, investedB ); - address swapTo = (ratioA >= ratioB) ? _tokenB : _tokenA; + if (ratioA >= ratioB) { swappedToB += swap(reward, _tokenB, _rewardBal); } else { From 4ce2a151899ae5d13b4aad92cc8f12732e7b93b5 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 10:05:47 +0200 Subject: [PATCH 13/23] fix: turn harvest and harvestTrigger virtual so any inheriting Joint can override it --- contracts/Joint.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/Joint.sol b/contracts/Joint.sol index b01ba85..dfcb82c 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -375,11 +375,11 @@ abstract contract Joint { } // Keepers will claim and sell rewards mid-epoch (otherwise we sell only in the end) - function harvest() external onlyKeepers { + function harvest() external virtual onlyKeepers { getReward(); } - function harvestTrigger() external view returns (bool) { + function harvestTrigger() external view virtual returns (bool) { return balanceOfRewardToken()[0] > minRewardToHarvest; } From be431e83f70f8172663b8159b59542a7bff82e37 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 10:08:09 +0200 Subject: [PATCH 14/23] fix: change findSwapTo argument to from_token --- contracts/Joint.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/Joint.sol b/contracts/Joint.sol index dfcb82c..ab3e6ac 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -625,10 +625,10 @@ abstract contract Joint { * @param token, address of the token to swap from * @return address of the token to swap to */ - function findSwapTo(address token) internal view returns (address) { - if (tokenA == token) { + function findSwapTo(address from_token) internal view returns (address) { + if (tokenA == from_token) { return tokenB; - } else if (tokenB == token) { + } else if (tokenB == from_token) { return tokenA; } else { if (tokenA == referenceToken || tokenB == referenceToken) { From c2e4356d23fc4c0b2fe59aa2159e699e0931eff6 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 10:22:41 +0200 Subject: [PATCH 15/23] fix: re-organized the burn + collect functions --- contracts/DEXes/UniV3StablesJoint.sol | 45 ++++++++++++++------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index f321a40..2fab8c7 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -360,21 +360,14 @@ contract UniV3StablesJoint is NoHedgeJoint { /* * @notice - * Function claiming the earned rewards for the joint, sends the tokens to the joint - * contract + * Function used internally to collect the accrued fees by burn 0 of the LP position + * and collecting the owed tokens (only fees as no LP has been burnt) + * @return balance of tokens in the LP (invested amounts) */ - function collectOwedTokens() internal { - IUniswapV3Pool(pool).collect( - address(this), - minTick, - maxTick, - type(uint128).max, - type(uint128).max - ); + function getReward() internal override { + _burnAndCollect(0, minTick, maxTick); } - function getReward() internal override {} - /* * @notice * Function used internally to open the LP position in the uni v3 pool: @@ -442,8 +435,7 @@ contract UniV3StablesJoint is NoHedgeJoint { * @param amount, amount of liquidity to burn */ function burnLP(uint256 amount) internal override { - IUniswapV3Pool(pool).burn(minTick, maxTick, uint128(amount)); - collectOwedTokens(); + _burnAndCollect(amount, minTick, maxTick); // If entire position is closed, re-set the min and max ticks IUniswapV3Pool.PositionInfo memory positionInfo = _positionInfo(); if (positionInfo.liquidity == 0){ @@ -457,6 +449,7 @@ contract UniV3StablesJoint is NoHedgeJoint { * Function available to vault managers to burn the LP manually, if for any reason * the ticks have been set to 0 (or any different value from the original LP), we make * sure we can always get out of the position + * This function can be used to only collect fees by passing a 0 amount to burn * @param _amount, amount of liquidity to burn * @param _minTick, lower limit of position * @param _maxTick, upper limit of position @@ -466,23 +459,31 @@ contract UniV3StablesJoint is NoHedgeJoint { int24 _minTick, int24 _maxTick ) external onlyVaultManagers { - IUniswapV3Pool(pool).burn(_minTick, _maxTick, uint128(_amount)); + _burnAndCollect(_amount, _minTick, _maxTick); } /* * @notice - * Function available to vault managers to collect the pending rewards manually, - * if for any reason the ticks have been set to 0 (or any different value from the - * original LP), we make sure we can always get the rewards back + * Function available internally to burn the LP amount specified, for position + * defined by minTick and maxTick specified and collect the owed tokens + * @param _amount, amount of liquidity to burn * @param _minTick, lower limit of position * @param _maxTick, upper limit of position */ - function collectRewardsManually( + function _burnAndCollect( + uint256 _amount, int24 _minTick, int24 _maxTick - ) external onlyVaultManagers { - IUniswapV3Pool(pool).burn(minTick, maxTick, 0); - collectOwedTokens(); + ) internal { + IUniswapV3Pool _pool = IUniswapV3Pool(pool); + _pool.burn(_minTick, _maxTick, uint128(_amount)); + _pool.collect( + address(this), + _minTick, + _maxTick, + type(uint128).max, + type(uint128).max + ); } /* From 35bbdf27c1ddbf7cf0adf04cf461187fd9c77043 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 10:23:38 +0200 Subject: [PATCH 16/23] fix: remove setCRVPool function to avoid address injection --- contracts/DEXes/UniV3StablesJoint.sol | 9 --------- 1 file changed, 9 deletions(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index 2fab8c7..b566459 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -178,15 +178,6 @@ contract UniV3StablesJoint is NoHedgeJoint { return positionInfo.liquidity; } - /* - * @notice - * Function available for vault managers to set the CRV pool to use for swaps - * @param newPool, new CRV pool address to use - */ - function setCRVPool(address newPool) external onlyGovernance { - crvPool = newPool; - } - /* * @notice * Function available for vault managers to set the boolean value deciding wether From 5291fc62905274bbef57b1f5a2aa249d0812b4b2 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 10:26:41 +0200 Subject: [PATCH 17/23] fix: added force parameter to setTicksManually --- contracts/DEXes/UniV3StablesJoint.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index b566459..bc46bd4 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -206,7 +206,11 @@ contract UniV3StablesJoint is NoHedgeJoint { * @param _minTick, lower limit of position * @param _maxTick, upper limit of position */ - function setTicksManually(int24 _minTick, int24 _maxTick) external onlyVaultManagers { + function setTicksManually(int24 _minTick, int24 _maxTick, bool forceChange) external onlyVaultManagers { + IUniswapV3Pool.PositionInfo memory positionInfo = _positionInfo(); + if (positionInfo.liquidity > 0 && !forceChange) { + revert(); + } minTick = _minTick; maxTick = _maxTick; } From 6bb769d8c438b4201e08604796da45f5814dc99d Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 10:29:30 +0200 Subject: [PATCH 18/23] fix: edited the if condition and natspec on setTicksManually --- contracts/DEXes/UniV3StablesJoint.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index bc46bd4..87d791b 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -202,13 +202,13 @@ contract UniV3StablesJoint is NoHedgeJoint { * @notice * Function available for vault managers to set min & max values of the position. If, * for any reason the ticks are not the value they should be, we always have the option - * to re-set them back to the necessary value + * to re-set them back to the necessary value using the force parameter * @param _minTick, lower limit of position - * @param _maxTick, upper limit of position + * @param _minTick, lower limit of position + * @param forceChange, force parameter to ensure this function is not called randomly */ function setTicksManually(int24 _minTick, int24 _maxTick, bool forceChange) external onlyVaultManagers { - IUniswapV3Pool.PositionInfo memory positionInfo = _positionInfo(); - if (positionInfo.liquidity > 0 && !forceChange) { + if ((investedA > 0 || investedB > 0) && !forceChange) { revert(); } minTick = _minTick; From 2ed73bca13ee458b2c03dd0c4a96e80754a5a3b1 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 10:30:59 +0200 Subject: [PATCH 19/23] fix: re-factor findSwapTo --- contracts/Joint.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/Joint.sol b/contracts/Joint.sol index ab3e6ac..2f2fe35 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -630,12 +630,11 @@ abstract contract Joint { return tokenB; } else if (tokenB == from_token) { return tokenA; - } else { - if (tokenA == referenceToken || tokenB == referenceToken) { - return referenceToken; - } - return tokenA; } + if (tokenA == referenceToken || tokenB == referenceToken) { + return referenceToken; + } + return tokenA; } /* From 34a7e4e83c4018c82c271932f4c67e9e38a71e7b Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 10:41:11 +0200 Subject: [PATCH 20/23] feat: implemented swapTokenForTokenManually --- contracts/DEXes/UniV3StablesJoint.sol | 27 +++++++++++++++++++++++++++ contracts/Joint.sol | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/contracts/DEXes/UniV3StablesJoint.sol b/contracts/DEXes/UniV3StablesJoint.sol index 87d791b..b33bddc 100644 --- a/contracts/DEXes/UniV3StablesJoint.sol +++ b/contracts/DEXes/UniV3StablesJoint.sol @@ -631,4 +631,31 @@ contract UniV3StablesJoint is NoHedgeJoint { ); return IUniswapV3Pool(pool).positions(key); } + + /* + * @notice + * Function used by governance to swap tokens manually if needed, can be used when closing + * the LP position manually and need some re-balancing before sending funds back to the + * providers + * @param swapPath, path of addresses to swap, should be 2 and always tokenA <> tokenB + * @param swapInAmount, amount of swapPath[0] to swap for swapPath[1] + * @param minOutAmount, minimum amount of want out + * @return swapped amount + */ + function swapTokenForTokenManually( + address[] memory swapPath, + uint256 swapInAmount, + uint256 minOutAmount + ) external onlyGovernance override returns (uint256) { + address _tokenA = tokenA; + address _tokenB = tokenB; + require(swapPath.length == 2); + require(swapPath[0] == _tokenA || swapPath[1] == _tokenA); + require(swapPath[0] == _tokenB || swapPath[1] == _tokenB); + return swap( + swapPath[0], + swapPath[1], + swapInAmount + ); + } } diff --git a/contracts/Joint.sol b/contracts/Joint.sol index 2f2fe35..eaadccc 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -858,7 +858,7 @@ abstract contract Joint { address[] memory swapPath, uint256 swapInAmount, uint256 minOutAmount - ) external onlyGovernance returns (uint256) {} + ) external onlyGovernance virtual returns (uint256) {} /* * @notice From ab7882578d70a675796ba9735576eac549be63ff Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 16:34:21 +0200 Subject: [PATCH 21/23] chore: minor changes to tests to make them more robust --- tests/conftest.py | 4 ++-- tests/nohedge/UNIV3_test_open_position_and_harvest.py | 2 +- tests/utils/actions.py | 5 +++-- tests/utils/utils.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e71981b..f5aed1a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -231,8 +231,8 @@ def tokenA(request, chain): # "YFI", # YFI # "WETH", # WETH # 'LINK', # LINK - # 'USDT', # USDT - 'DAI', # DAI + 'USDT', # USDT + # 'DAI', # DAI # "USDC", # USDC # "WFTM", # "MIM", diff --git a/tests/nohedge/UNIV3_test_open_position_and_harvest.py b/tests/nohedge/UNIV3_test_open_position_and_harvest.py index ce78230..a55ac40 100644 --- a/tests/nohedge/UNIV3_test_open_position_and_harvest.py +++ b/tests/nohedge/UNIV3_test_open_position_and_harvest.py @@ -329,7 +329,7 @@ def test_choppy_harvest_UNIV3( if swap_dex == "crv": utils.crv_re_peg_pool(joint.crvPool(), token_out, token_in, token_out_whale, prev_reserve) - actions.gov_start_epoch_univ3(gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB, keep_dr = False) + actions.gov_start_epoch_univ3(gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB, keep_dr = False, check=False) # assert 0 print("etas", providerA.estimatedTotalAssets(), providerB.estimatedTotalAssets()) diff --git a/tests/utils/actions.py b/tests/utils/actions.py index 124be6a..36f0d1d 100644 --- a/tests/utils/actions.py +++ b/tests/utils/actions.py @@ -9,7 +9,7 @@ def user_deposit(user, vault, token, amount): vault.deposit(amount, {"from": user}) assert token.balanceOf(vault.address) == amount -def gov_start_epoch_univ3(gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB, keep_dr=False): +def gov_start_epoch_univ3(gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB, keep_dr=False, check=True): # the first harvest sends funds (tokenA) to joint contract and waits for tokenB funds # the second harvest sends funds (tokenB) to joint contract AND invests them (if there is enough TokenA) providerA.harvest({"from": gov}) @@ -19,7 +19,8 @@ def gov_start_epoch_univ3(gov, providerA, providerB, joint, vaultA, vaultB, amou vaultA.updateStrategyDebtRatio(providerA, 0, {"from": gov}) vaultB.updateStrategyDebtRatio(providerB, 0, {"from": gov}) - checks.epoch_started_univ3(providerA, providerB, joint, amountA, amountB) + if check: + checks.epoch_started_univ3(providerA, providerB, joint, amountA, amountB) def gov_start_epoch(gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB): # the first harvest sends funds (tokenA) to joint contract and waits for tokenB funds diff --git a/tests/utils/utils.py b/tests/utils/utils.py index ce54e24..00fddfa 100644 --- a/tests/utils/utils.py +++ b/tests/utils/utils.py @@ -245,7 +245,7 @@ def crv_ensure_bad_trade(crv_pool, token_in, token_out, token_in_whale): reserve_token_from = crv_pool.balances(index_from) reserve_token_to = crv_pool.balances(index_to) - sell_amount = reserve_token_to / 2 + sell_amount = reserve_token_to * 0.8 sell_amount = sell_amount / (10**token_out.decimals()) * (10**token_in.decimals()) token_in.approve(crv_pool, 0, {"from": token_in_whale}) From da7d6d9b831c6dc83cac3d8bda8186061e305dcd Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 19 May 2022 17:25:38 +0200 Subject: [PATCH 22/23] feat: new test for the manual operation of the strategy --- tests/nohedge/UNIV3_test_manual_operation.py | 226 +++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 tests/nohedge/UNIV3_test_manual_operation.py diff --git a/tests/nohedge/UNIV3_test_manual_operation.py b/tests/nohedge/UNIV3_test_manual_operation.py new file mode 100644 index 0000000..8895889 --- /dev/null +++ b/tests/nohedge/UNIV3_test_manual_operation.py @@ -0,0 +1,226 @@ +from functools import _lru_cache_wrapper +from utils import actions, checks, utils +import pytest +from brownie import Contract, chain + +@pytest.mark.parametrize("swap_from", ["a", "b"]) +def test_return_loose_to_providers_manually( + chain, + tokenA, + tokenB, + vaultA, + vaultB, + providerA, + providerB, + joint, + user, + amountA, + amountB, + RELATIVE_APPROX, + gov, + tokenA_whale, + tokenB_whale, + hedge_type, + dex, + uni_v3_pool, + router, + uniswap_helper_views, + testing_library, + univ3_pool_fee, + joint_to_use, + weth, + swap_from +): + checks.check_run_test("nohedge", hedge_type) + checks.check_run_test("UNIV3", dex) + # Deposit to the vault + actions.user_deposit(user, vaultA, tokenA, amountA) + actions.user_deposit(user, vaultB, tokenB, amountB) + + # Harvest 1: Send funds through the strategy + chain.sleep(1) + actions.gov_start_epoch_univ3(gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB) + + (initial_amount_A, initial_amount_B) = joint.balanceOfTokensInLP() + + # All balance should be invested + assert tokenA.balanceOf(joint) == 0 + assert tokenB.balanceOf(joint) == 0 + assert joint.pendingRewards() == (0,0) + + # Trade a small amount to generate rewards + token_in = tokenA if swap_from == "a" else tokenB + token_out = tokenB if swap_from == "a" else tokenA + token_in_whale = tokenA_whale if swap_from == "a" else tokenB_whale + token_out_whale = tokenB_whale if swap_from == "a" else tokenA_whale + + sell_amount = 1_000 * (10**token_in.decimals()) + utils.univ3_sell_token(token_in, token_out, router, token_in_whale, sell_amount, univ3_pool_fee) + + # We have generated rewards + pending_rewards = joint.pendingRewards() + assert pending_rewards != (0, 0) + + reward_gains = pending_rewards[0] if pending_rewards[0] > 0 else pending_rewards[1] + # Claim rewards manually + tx = joint.burnLPManually(0, joint.minTick(), joint.maxTick(), {"from": gov}) + if reward_gains == pending_rewards[0]: + assert tx.events["Collect"]["amount0"] == reward_gains + else: + assert tx.events["Collect"]["amount1"] == reward_gains + # Remove liquidity manually + tx = joint.removeLiquidityManually(joint.balanceOfPool(), 0, 0, {"from": gov}) + + # All balance should be in joint + assert tokenA.balanceOf(joint) > 0 + assert tokenB.balanceOf(joint) > 0 + + # Send back to providers + joint.returnLooseToProvidersManually() + assert tokenA.balanceOf(joint) == 0 + assert tokenB.balanceOf(joint) == 0 + + # All tokens accounted for + assert pytest.approx(tokenA.balanceOf(providerA), rel=1e-3) == amountA + assert pytest.approx(tokenB.balanceOf(providerB), rel=1e-3) == amountB + +def test_liquidate_position_manually( + chain, + tokenA, + tokenB, + vaultA, + vaultB, + providerA, + providerB, + joint, + user, + amountA, + amountB, + RELATIVE_APPROX, + gov, + tokenA_whale, + tokenB_whale, + hedge_type, + dex, + uni_v3_pool, + router, + uniswap_helper_views, + testing_library, + univ3_pool_fee, + joint_to_use, + weth, +): + checks.check_run_test("nohedge", hedge_type) + checks.check_run_test("UNIV3", dex) + + # Deposit to the vault + actions.user_deposit(user, vaultA, tokenA, amountA) + actions.user_deposit(user, vaultB, tokenB, amountB) + + # Harvest 1: Send funds through the strategy + chain.sleep(1) + actions.gov_start_epoch_univ3(gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB) + + (initial_amount_A, initial_amount_B) = joint.balanceOfTokensInLP() + + # CLose position manually + joint.liquidatePositionManually(0, 0) + + actions.gov_end_epoch(gov, providerA, providerB, joint, vaultA, vaultB) + + assert joint.investedA() == 0 + assert joint.investedB() == 0 + + for (vault, strat) in zip([vaultA, vaultB], [providerA, providerB]): + assert vault.strategies(strat)["totalLoss"] >= 0 + assert vault.strategies(strat)["totalGain"] == 0 + assert vault.strategies(strat)["totalDebt"] == 0 + +@pytest.mark.parametrize("swap_from", ["a", "b"]) +@pytest.mark.parametrize("swap_dex", ["uni", "crv"]) +def test_manual_swaps( + chain, + tokenA, + tokenB, + vaultA, + vaultB, + providerA, + providerB, + joint, + user, + amountA, + amountB, + RELATIVE_APPROX, + gov, + tokenA_whale, + tokenB_whale, + hedge_type, + dex, + uni_v3_pool, + router, + uniswap_helper_views, + testing_library, + univ3_pool_fee, + joint_to_use, + weth, + swap_from, + swap_dex +): + checks.check_run_test("nohedge", hedge_type) + checks.check_run_test("UNIV3", dex) + + if swap_dex == "uni": + joint.setUseCRVPool(False, {"from": gov}) + else: + joint.setUseCRVPool(True, {"from": gov}) + + # Deposit to the vault + actions.user_deposit(user, vaultA, tokenA, amountA) + actions.user_deposit(user, vaultB, tokenB, amountB) + + # Harvest 1: Send funds through the strategy + chain.sleep(1) + actions.gov_start_epoch_univ3(gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB) + + # Trade a small amount to generate rewards + token_in = tokenA if swap_from == "a" else tokenB + token_out = tokenB if swap_from == "a" else tokenA + token_in_whale = tokenA_whale if swap_from == "a" else tokenB_whale + + sell_amount = 1_000 * (10**token_in.decimals()) + utils.univ3_sell_token(token_in, token_out, router, token_in_whale, sell_amount, univ3_pool_fee) + + tx = joint.removeLiquidityManually(joint.balanceOfPool(), 0, 0, {"from": gov}) + + if swap_from == "a": + path = [tokenA, tokenB] + amount = joint.balanceOfA() - joint.investedA() + else: + path = [tokenB, tokenA] + amount = joint.balanceOfB() - joint.investedB() + + joint.swapTokenForTokenManually( + path, + amount, + 0, + {"from": gov} + ) + + # All balance should be in joint + assert pytest.approx(joint.balanceOfA(), rel=RELATIVE_APPROX) == joint.investedA() + assert pytest.approx(joint.balanceOfB(), rel=RELATIVE_APPROX) == joint.investedB() + + # Send back to providers + joint.returnLooseToProvidersManually() + + assert joint.investedA() > 0 + assert joint.investedB() > 0 + + # All tokens accounted for + assert pytest.approx(tokenA.balanceOf(providerA), rel=1e-3) == amountA + assert pytest.approx(tokenB.balanceOf(providerB), rel=1e-3) == amountB + + actions.gov_end_epoch(gov, providerA, providerB, joint, vaultA, vaultB) + + assert joint.investedA() == 0 + assert joint.investedB() == 0 From 40910706dadd1881514d1862c1d2e50f03eaf145 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 20 May 2022 19:31:35 +0200 Subject: [PATCH 23/23] fix: remove modifier and empty implementation from swap manually --- contracts/Joint.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Joint.sol b/contracts/Joint.sol index eaadccc..5f2438c 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -858,7 +858,7 @@ abstract contract Joint { address[] memory swapPath, uint256 swapInAmount, uint256 minOutAmount - ) external onlyGovernance virtual returns (uint256) {} + ) external virtual returns (uint256); /* * @notice