-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Harvesting basic structure #644
base: main
Are you sure you want to change the base?
Changes from all commits
0d7757f
e2ce664
b99e36b
d290dfb
85cea50
ce71c43
4bdfe14
2375b73
0e7a1e8
3f06837
a7595ba
e2149b6
275a55e
b22bbff
1d74303
7f0a7e9
8d1411f
23250ef
41b097b
1944a0f
7f0268b
102f0e8
c73df03
6b7c755
147d811
c2b9812
34f29f3
7a2fef0
0d6bb80
91fbb31
c804302
895accb
6221937
5adfaef
383aa89
3a350a4
1f382f5
a806520
3f27f4f
227c5f2
6f010c2
3c2dd17
62b3b42
0cd07b7
7de3bdf
1ea636a
8045a41
a7c6a4b
9ed417f
d04795f
1bcf6c9
61215b6
7aa6d89
f2622d9
57d2780
fa89986
c543baa
cfb64a4
e404463
8bab561
6472e9d
dc63911
1b5e907
42e2cb7
dceee17
935f659
f4b8984
bc9a854
dd2e1a7
424ee0a
5c4eeae
7ae12b1
da454fa
2e03f73
808bcd5
65ededc
b08afda
7be8a26
62425b9
d25f17a
41e8ca5
1b5f932
16ce66b
6a0c7a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity 0.8.15; | ||
|
||
/** | ||
* @title HarvestManager | ||
* | ||
* @author Fujidao Labs | ||
* | ||
* @notice Contract that facilitates the harvest of rewards and strategies of FujiV2 providers. | ||
* | ||
* @dev Must have HARVESTER_ROLE. | ||
*/ | ||
|
||
import {IHarvestManager, Strategy} from "./interfaces/IHarvestManager.sol"; | ||
import {IVault} from "./interfaces/IVault.sol"; | ||
import {BorrowingVault} from "./vaults/borrowing/BorrowingVault.sol"; | ||
import {ILendingProvider} from "./interfaces/ILendingProvider.sol"; | ||
import {IHarvestable} from "./interfaces/IHarvestable.sol"; | ||
import {ISwapper} from "./interfaces/ISwapper.sol"; | ||
import {SystemAccessControl} from "./access/SystemAccessControl.sol"; | ||
import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; | ||
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; | ||
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
||
contract HarvestManager is IHarvestManager, SystemAccessControl { | ||
using SafeERC20 for IERC20; | ||
using Math for uint256; | ||
|
||
/// @dev Custom errors | ||
error HarvestManager__allowExecutor_noAllowChange(); | ||
error HarvestManager__zeroAddress(); | ||
error HarvestManager__setFee_noFeeChange(); | ||
error HarvestManager__setFee_feeTooHigh(); | ||
error HarvestManager__setFee_feeTooLow(); | ||
error HarvestManager__harvest_notValidExecutor(); | ||
error HarvestManager__harvest_notValidSwapper(); | ||
error HarvestManager__harvest_harvestAlreadyInProgress(); | ||
error HarvestManager__harvest_vaultNotAllowed(); | ||
|
||
address public immutable treasury; | ||
|
||
uint256 public constant MAX_FEE = 1e18; // 100% | ||
uint256 public constant MIN_FEE = 1e16; // 1% | ||
uint256 public constant BASE_FEE = 2e17; // 20% | ||
|
||
uint256 public protocolFee; | ||
|
||
/// @dev addresses allowed to harvest via this manager. | ||
mapping(address => bool) public allowedExecutor; | ||
|
||
address private currentVaultHarvest; | ||
|
||
constructor(address chief_, address treasury_) { | ||
__SystemAccessControl_init(chief_); | ||
treasury = treasury_; | ||
protocolFee = BASE_FEE; | ||
} | ||
|
||
/// @inheritdoc IHarvestManager | ||
function allowExecutor(address executor, bool allowed) external override onlyTimelock { | ||
if (executor == address(0)) { | ||
revert HarvestManager__zeroAddress(); | ||
} | ||
if (allowedExecutor[executor] == allowed) { | ||
revert HarvestManager__allowExecutor_noAllowChange(); | ||
} | ||
allowedExecutor[executor] = allowed; | ||
emit AllowExecutor(executor, allowed); | ||
} | ||
|
||
/// @inheritdoc IHarvestManager | ||
function setFee(uint256 fee) external override onlyTimelock { | ||
if (fee == protocolFee) { | ||
revert HarvestManager__setFee_noFeeChange(); | ||
} | ||
if (fee > MAX_FEE) { | ||
revert HarvestManager__setFee_feeTooHigh(); | ||
} | ||
if (fee < MIN_FEE) { | ||
revert HarvestManager__setFee_feeTooLow(); | ||
} | ||
protocolFee = fee; | ||
emit SetFee(fee); | ||
} | ||
|
||
/// @inheritdoc IHarvestManager | ||
function harvest( | ||
IVault vault, | ||
Strategy strategy, | ||
IHarvestable provider, | ||
ISwapper swapper, | ||
bytes memory data | ||
) | ||
external | ||
{ | ||
if (!allowedExecutor[msg.sender]) { | ||
revert HarvestManager__harvest_notValidExecutor(); | ||
} | ||
if ( | ||
(strategy == Strategy.ConvertToCollateral || strategy == Strategy.RepayDebt) | ||
&& !chief.allowedSwapper(address(swapper)) | ||
) { | ||
revert HarvestManager__harvest_notValidSwapper(); | ||
} | ||
|
||
if (!chief.isVaultActive(address(vault))) { | ||
revert HarvestManager__harvest_vaultNotAllowed(); | ||
} | ||
|
||
if (currentVaultHarvest != address(0)) { | ||
revert HarvestManager__harvest_harvestAlreadyInProgress(); | ||
} | ||
|
||
currentVaultHarvest = address(vault); | ||
|
||
vault.harvest(strategy, provider, swapper, data); | ||
} | ||
|
||
function completeHarvest( | ||
address vault, | ||
Strategy strategy, | ||
IHarvestable provider, | ||
ISwapper swapper, | ||
address[] memory tokens, | ||
uint256[] memory amounts | ||
) | ||
external | ||
returns (bytes memory data) | ||
{ | ||
if (msg.sender != currentVaultHarvest) { | ||
revert HarvestManager__harvest_vaultNotAllowed(); | ||
} | ||
|
||
IVault vault_ = IVault(vault); | ||
|
||
if (strategy == Strategy.ConvertToCollateral) { | ||
data = _convertToCollateral(tokens, amounts, swapper, vault_, strategy); | ||
} | ||
if (strategy == Strategy.RepayDebt) { | ||
data = _repayDebt(provider, tokens, amounts, swapper, vault_, strategy); | ||
} | ||
if (strategy == Strategy.Distribute) { | ||
data = _distribute(tokens, amounts, vault_); | ||
} | ||
|
||
currentVaultHarvest = address(0); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Completing the harvest through the HarvestManager should also emit an Event. |
||
|
||
/** | ||
* @notice Implements the ConvertToCollateral strategy by swapping the reward tokens by the vault's collateralAsset. | ||
* | ||
* @param tokens the reward tokens. | ||
* @param amounts the amounts of reward tokens. | ||
* @param swapper the swapper to use. | ||
* @param vault the vault that has harvested the rewards. | ||
* @param strategy the strategy that has been used to harvest the rewards. | ||
* | ||
* @return data the encoded data to be used in the vault's completeHarvest function. | ||
* | ||
* @dev In this case, the return data is a call to the deposit function | ||
*/ | ||
function _convertToCollateral( | ||
address[] memory tokens, | ||
uint256[] memory amounts, | ||
ISwapper swapper, | ||
IVault vault, | ||
Strategy strategy | ||
) | ||
internal | ||
returns (bytes memory data) | ||
{ | ||
uint256 totalAmount; | ||
address collateralAsset = vault.asset(); | ||
for (uint256 i = 0; i < tokens.length; i++) { | ||
IERC20(tokens[i]).safeTransferFrom(address(vault), address(this), amounts[i]); | ||
if (tokens[i] == collateralAsset) { | ||
totalAmount += amounts[i]; | ||
} else { | ||
totalAmount += _swap(swapper, tokens[i], collateralAsset, amounts[i], address(this)); | ||
} | ||
} | ||
uint256 treasuryAmount = totalAmount.mulDiv(protocolFee, 1e18); | ||
IERC20(collateralAsset).safeTransfer(treasury, treasuryAmount); | ||
IERC20(collateralAsset).safeTransfer(address(vault), totalAmount - treasuryAmount); | ||
data = abi.encodeWithSelector(vault.deposit.selector, totalAmount - treasuryAmount, vault); | ||
|
||
emit Harvest(address(vault), strategy, totalAmount - treasuryAmount, treasury, treasuryAmount); | ||
} | ||
|
||
/** | ||
* @notice Implements the RepayDebt strategy by swapping the reward tokens by the vault's debtAsset and repaying the debt. | ||
* | ||
* @param provider the lending provider. | ||
* @param tokens the reward tokens. | ||
* @param amounts the amounts of reward tokens. | ||
* @param swapper the swapper to use. | ||
* @param vault the vault that has harvested the rewards. | ||
* @param strategy the strategy that has been used to harvest the rewards. | ||
* | ||
* @return data the encoded data to be used in the vault's completeHarvest function. | ||
* | ||
* @dev In this case, the return data is a call to the payback function. | ||
* If the reward amounts are more than enought to payback the vault's total debt, the excess is sent to the treasury. | ||
*/ | ||
function _repayDebt( | ||
IHarvestable provider, | ||
address[] memory tokens, | ||
uint256[] memory amounts, | ||
ISwapper swapper, | ||
IVault vault, | ||
Strategy strategy | ||
) | ||
internal | ||
returns (bytes memory data) | ||
{ | ||
address debtAsset = BorrowingVault(payable(address(vault))).debtAsset(); | ||
uint256 providerDebt = | ||
ILendingProvider(address(provider)).getBorrowBalance(address(vault), vault); | ||
uint256 totalAmount; | ||
for (uint256 i = 0; i < tokens.length; i++) { | ||
IERC20(tokens[i]).safeTransferFrom(address(vault), address(this), amounts[i]); | ||
if (tokens[i] == debtAsset) { | ||
totalAmount += amounts[i]; | ||
} else { | ||
totalAmount += _swap(swapper, tokens[i], debtAsset, amounts[i], address(this)); | ||
} | ||
} | ||
|
||
uint256 treasuryAmount = totalAmount.mulDiv(protocolFee, 1e18); | ||
uint256 amountToRepay = totalAmount - treasuryAmount; | ||
|
||
if (amountToRepay > providerDebt) { | ||
amountToRepay = providerDebt; | ||
} | ||
|
||
IERC20(debtAsset).safeTransfer(treasury, totalAmount - amountToRepay); | ||
IERC20(debtAsset).safeTransfer(address(vault), amountToRepay); | ||
|
||
data = abi.encodeWithSelector( | ||
ILendingProvider(address(provider)).payback.selector, amountToRepay, vault | ||
); | ||
emit Harvest(address(vault), strategy, amountToRepay, treasury, totalAmount - amountToRepay); | ||
} | ||
|
||
//TODO | ||
function _distribute( | ||
address[] memory tokens, | ||
uint256[] memory amounts, | ||
IVault vault | ||
) | ||
internal | ||
returns (bytes memory data) | ||
{} | ||
|
||
/** | ||
* @notice Swaps `assetIn` for `assetOut` using `swapper`. | ||
* | ||
* @param swapper ISwapper to be used to swap rewards. | ||
* @param assetIn to be swapped. | ||
* @param assetOut to be received. | ||
* @param amountIn uint256 amount of `assetIn` to be swapped. | ||
* @param receiver address to receive `assetOut`. | ||
* | ||
* @dev Treasury will receive any leftovers after the swap | ||
*/ | ||
function _swap( | ||
ISwapper swapper, | ||
address assetIn, | ||
address assetOut, | ||
uint256 amountIn, | ||
address receiver | ||
) | ||
internal | ||
returns (uint256 amountOut) | ||
{ | ||
amountOut = swapper.getAmountOut(assetIn, assetOut, amountIn); | ||
|
||
IERC20(assetIn).safeIncreaseAllowance(address(swapper), amountIn); | ||
swapper.swap(assetIn, assetOut, amountIn, amountOut, receiver, treasury, 0); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,15 +25,19 @@ import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; | |
import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol"; | ||
import {IVault} from "../interfaces/IVault.sol"; | ||
import {ILendingProvider} from "../interfaces/ILendingProvider.sol"; | ||
import {IHarvestManager, Strategy} from "../interfaces/IHarvestManager.sol"; | ||
import {IHarvestable} from "../interfaces/IHarvestable.sol"; | ||
import {IERC4626} from "openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; | ||
import {VaultPermissions} from "../vaults/VaultPermissions.sol"; | ||
import {SystemAccessControl} from "../access/SystemAccessControl.sol"; | ||
import {PausableVault} from "./PausableVault.sol"; | ||
import {ISwapper} from "../interfaces/ISwapper.sol"; | ||
|
||
abstract contract BaseVault is ERC20, SystemAccessControl, PausableVault, VaultPermissions, IVault { | ||
using Math for uint256; | ||
using Address for address; | ||
using SafeERC20 for IERC20Metadata; | ||
using SafeERC20 for IERC20; | ||
|
||
/// @dev Custom Errors | ||
error BaseVault__constructor_invalidInput(); | ||
|
@@ -51,6 +55,11 @@ abstract contract BaseVault is ERC20, SystemAccessControl, PausableVault, VaultP | |
error BaseVault__redeem_slippageTooHigh(); | ||
error BaseVault__useIncreaseWithdrawAllowance(); | ||
error BaseVault__useDecreaseWithdrawAllowance(); | ||
error BaseVault__harvest_invalidProvider(); | ||
error BaseVault__harvest_strategyNotImplemented(); | ||
error BaseVault__harvest_notValidSwapper(); | ||
error BaseVault__harvest_noRewards(); | ||
error BaseVault__harvest_invalidRewards(); | ||
|
||
/** | ||
* @dev `VERSION` of this vault. | ||
|
@@ -986,4 +995,80 @@ abstract contract BaseVault is ERC20, SystemAccessControl, PausableVault, VaultP | |
revert BaseVault__checkRebalanceFee_excessFee(); | ||
} | ||
} | ||
|
||
/// @inheritdoc IVault | ||
function harvest( | ||
Strategy strategy, | ||
IHarvestable provider, | ||
ISwapper swapper, | ||
bytes memory data | ||
) | ||
external | ||
virtual | ||
hasRole(msg.sender, HARVESTER_ROLE) | ||
returns (address[] memory tokens, uint256[] memory amounts) | ||
{ | ||
//TODO maybe move this verification to the HarvestManager | ||
//check strategy is valid for this vault | ||
if (strategy == Strategy.RepayDebt) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to do it now, but I think this strategy check could be "pushed" to the HarvestManager. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same applies to BaseVaultUpgradeable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea. I will add a TODO comment to be aware of this when pushing down the contract size |
||
revert BaseVault__harvest_strategyNotImplemented(); | ||
} | ||
//check provider is valid | ||
if (!_isValidProvider(address(provider))) { | ||
revert BaseVault__harvest_invalidProvider(); | ||
} | ||
|
||
//collect rewards from provider | ||
(tokens, amounts) = _harvest(provider, data); | ||
|
||
//TODO create an abstract contract "Harvestable" that inherits "IHarvestable" that is inherited by a provider that is "harvestable". | ||
//In there we can put repeated logic and checks, that we could offboard from the vault contracts. | ||
if (tokens.length != amounts.length) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this check was done here because you did not want to repeat this at the providers. However, for optimization note in the future I think we can create an abstract contract "Harvestable" that inherits "IHarvestable" that is inherited by a provider that is "harvestable". In there we can put repeated logic and checks, that we could offboard from the vault contracts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same applies to BaseVaultUpgradeable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense :) Will add a TODO comment here as well |
||
revert BaseVault__harvest_invalidRewards(); | ||
} | ||
|
||
for (uint256 i = 0; i < tokens.length; i++) { | ||
//transfer rewards to recipient | ||
IERC20(tokens[i]).safeIncreaseAllowance(msg.sender, amounts[i]); | ||
} | ||
|
||
bytes memory callData = IHarvestManager(msg.sender).completeHarvest( | ||
address(this), strategy, provider, swapper, tokens, amounts | ||
); | ||
_completeHarvest(address(provider), callData); | ||
} | ||
|
||
/** | ||
* @dev Harvests rewards from `provider`. | ||
* | ||
* @param provider address of provider to harvest from | ||
* @param data bytes data to be used by the harvest function of the provider, specific to each provider way of harvesting | ||
*/ | ||
function _harvest( | ||
IHarvestable provider, | ||
bytes memory data | ||
) | ||
internal | ||
returns (address[] memory tokens, uint256[] memory amounts) | ||
{ | ||
bytes memory callData = abi.encodeWithSelector(provider.harvest.selector, data); | ||
(bytes memory returnData) = address(provider).functionDelegateCall( | ||
callData, string(abi.encodePacked("harvest", ": delegate call failed")) | ||
); | ||
(tokens, amounts) = abi.decode(returnData, (address[], uint256[])); | ||
|
||
emit Harvest(address(provider), tokens, amounts); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should be some type of event emitted here, essentially for all the tokens and amounts harvested and the provider. Same applies for the upgradeable vault. |
||
|
||
function _completeHarvest( | ||
address provider, | ||
bytes memory data | ||
) | ||
internal | ||
returns (bytes memory returnData) | ||
{ | ||
returnData = address(provider).functionDelegateCall( | ||
data, string(abi.encodePacked("completeHarvest", ": delegate call failed")) | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider also adding a check at Chief.sol if the vault is a "valid" vault.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a verification using the vault status (chief.isVaultActive). what do you think?