Skip to content
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

Open
wants to merge 84 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
0d7757f
feat(protocol): add harvest function
pedrovalido Jun 26, 2023
e2ce664
feat(protocol): add harvest function
pedrovalido Jun 28, 2023
b99e36b
feat(protocol): add harvest to mocks for test purposes
pedrovalido Jun 28, 2023
d290dfb
test(protocol): test harvest mocking
pedrovalido Jun 28, 2023
85cea50
feat(protocol): lay down harvest base structure
pedrovalido Jun 28, 2023
ce71c43
feat(protocol): add getHarvestToken function
pedrovalido Jun 30, 2023
4bdfe14
feat(protocol): add getHarvestToken function for compile errors, stil…
pedrovalido Jun 30, 2023
2375b73
feat(protocol): add check for valid provider
pedrovalido Jun 30, 2023
0e7a1e8
feat(protocol): revert pay debt with rewards in BaseBault
pedrovalido Jun 30, 2023
3f06837
Merge branch 'main' into protocol/feat/harvesting-basic-struct
pedrovalido Jun 30, 2023
a7595ba
fix(protocol): merge refactor
pedrovalido Jun 30, 2023
e2149b6
feat(protocol): add view functions for harvest
pedrovalido Jul 6, 2023
275a55e
feat(protocol): include swapper in harvest
pedrovalido Jul 6, 2023
b22bbff
feat(protocol): implement swap for collateral strategy
pedrovalido Jul 6, 2023
1d74303
feat(protocol): compoundV3 interface for rewards
pedrovalido Jul 6, 2023
7f0a7e9
feat(protocol): add view functions for harvest
pedrovalido Jul 6, 2023
8d1411f
feat(protocol): add view functions for harvest
pedrovalido Jul 6, 2023
23250ef
test(protocol): include swapper in harvest
pedrovalido Jul 6, 2023
41b097b
test(protocol): test initial harvesting implementation
pedrovalido Jul 6, 2023
1944a0f
feat(protocol): add claim functions
pedrovalido Jul 6, 2023
7f0268b
feat(protocol): implement harvest for CompoundV3
pedrovalido Jul 6, 2023
102f0e8
feat(protocol): add interface for harvest functions in ILendingProviders
pedrovalido Jul 7, 2023
c73df03
feat(protocol): add interface for harvest functions in ILendingProviders
pedrovalido Jul 7, 2023
6b7c755
feat(protocol): add interface for harvest functions in ILendingProviders
pedrovalido Jul 7, 2023
147d811
refactor(protocol): adapt harvest to interfaces, improve code readabi…
pedrovalido Jul 7, 2023
c2b9812
feat(protocol): use new IHarvestable interface
pedrovalido Jul 7, 2023
34f29f3
feat(protocol): use new IHarvestable interface
pedrovalido Jul 7, 2023
7a2fef0
fix(protocol): adapt tests to changes
pedrovalido Jul 7, 2023
0d6bb80
refactor(protocol): remove harvest functions from non IHarvestable pr…
pedrovalido Jul 7, 2023
91fbb31
fix(protocol): merge conflicts
pedrovalido Jul 7, 2023
c804302
feat(protocol): add IHarvestManager interface
pedrovalido Jul 10, 2023
895accb
refactor(protocol): remove harvest complexities from vault
pedrovalido Jul 11, 2023
6221937
feat(protocol): remove strategy from provider
pedrovalido Jul 11, 2023
5adfaef
fix(protocol): adapt providers to new changes in interface
pedrovalido Jul 11, 2023
383aa89
fix(protocol): adapt test to interface
pedrovalido Jul 11, 2023
3a350a4
refactor(protocol): remove harvest complexities from vault
pedrovalido Jul 11, 2023
1f382f5
refactor(protocol): harvest will suffer changes, remove test for now
pedrovalido Jul 11, 2023
a806520
feat(protocol): harvest logic in HarvestManager
pedrovalido Jul 11, 2023
3f27f4f
feat(protocol): start repaydebt strategy
pedrovalido Jul 12, 2023
227c5f2
feat(protocol): return delegate call returnData
pedrovalido Jul 12, 2023
6f010c2
fix(protocol): complete harvest call
pedrovalido Jul 14, 2023
3c2dd17
docs(protocol): small comments
pedrovalido Jul 14, 2023
62b3b42
test(protocol): test convertToCollateral strategy with newly implemen…
pedrovalido Jul 14, 2023
0cd07b7
feat(protocol): complete harvest function in harvestmanager
pedrovalido Jul 17, 2023
7de3bdf
feat(protocol): complete harvest function in harvestmanager
pedrovalido Jul 17, 2023
1ea636a
refactor(protocol): complete harvest function in harvestmanager
pedrovalido Jul 17, 2023
8045a41
fix(protocol): no need to send anymore
pedrovalido Jul 17, 2023
a7c6a4b
test(protocol): test convertToCollateral strategy with swap rewards i…
pedrovalido Jul 17, 2023
9ed417f
Merge branch 'main' into protocol/feat/harvesting-basic-struct
pedrovalido Jul 18, 2023
d04795f
feat(protocol): pass vault as an address
pedrovalido Jul 19, 2023
1bcf6c9
feat(protocol): pass vault as an address
pedrovalido Jul 19, 2023
61215b6
fix(protocol): no need to pass vault as argument
pedrovalido Jul 19, 2023
7aa6d89
feat(protocol): implement harvest for upgradable contracts
pedrovalido Jul 19, 2023
f2622d9
test(protocol): test repayDebt strategy
pedrovalido Jul 19, 2023
57d2780
feat(protocol): implement repayDebt strategy
pedrovalido Jul 19, 2023
fa89986
fix(protocol): repaydebt strategy amount transfer
pedrovalido Jul 19, 2023
c543baa
refactor(protocol): repaydebt strategy
pedrovalido Jul 19, 2023
cfb64a4
docs(protocol): harvest functions
pedrovalido Jul 19, 2023
e404463
fix(protocol): fix test, roll block more than one works
pedrovalido Jul 19, 2023
8bab561
feat(protocol): move structs outside of interface
pedrovalido Jul 21, 2023
6472e9d
fix(protocol): struct outside of interface, get rewards in harvest no…
pedrovalido Jul 21, 2023
dc63911
fix(protocol): remove previewHarvest for now
pedrovalido Jul 24, 2023
1b5e907
fix(protocol): no need to cast to address
pedrovalido Jul 24, 2023
42e2cb7
fix(protocol): rewards are scaled by 10
pedrovalido Jul 24, 2023
dceee17
test(protocol): test strategy repayDebt and convertToCollateral
pedrovalido Jul 24, 2023
935f659
fix(protocol): uniswap getAmountOut when path is bigger than 2
pedrovalido Jul 25, 2023
f4b8984
Merge branch 'main' into protocol/feat/harvesting-basic-struct
pedrovalido Jul 26, 2023
bc9a854
feat(protocol): add fee functions to harvest
pedrovalido Jul 26, 2023
dd2e1a7
feat(protocol): add protocol fee getter
pedrovalido Jul 26, 2023
424ee0a
feat(protocol): add protocol fee to harvest
pedrovalido Jul 26, 2023
5c4eeae
test(protocol): test protocol fee in harvest
pedrovalido Jul 26, 2023
7ae12b1
fix(protocol): remove unused test
pedrovalido Jul 26, 2023
da454fa
feat(protocol): add comp oracle price feed
pedrovalido Jul 26, 2023
2e03f73
refactor(protocol): remove radiant wip harvest, will be in separate P…
pedrovalido Jul 26, 2023
808bcd5
fix(protocol): assert value of treasury amount
pedrovalido Jul 26, 2023
65ededc
fix(protocol): unlike the docs, this function does not scale rewards …
pedrovalido Jul 26, 2023
b08afda
test(protocol): test protocol fee in harvest of compound
pedrovalido Jul 26, 2023
7be8a26
feat(protocol): add vault is active verification
pedrovalido Aug 1, 2023
62425b9
fix(protocol): remove variable initialization to 0
pedrovalido Aug 1, 2023
d25f17a
docs(protocol): add suggestion to decrease contract size
pedrovalido Aug 1, 2023
41e8ca5
docs(protocol): add suggestion to decrease contract size
pedrovalido Aug 1, 2023
1b5f932
feat(protocol): emit harvest events
pedrovalido Aug 1, 2023
16ce66b
feat(protocol): add vault gain to harvest event
pedrovalido Aug 1, 2023
6a0c7a2
fix(protocol): remove repeated IERC20
pedrovalido Aug 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions packages/protocol/src/HarvestManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// 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 (currentVaultHarvest != address(0)) {
revert HarvestManager__harvest_harvestAlreadyInProgress();
}

Copy link
Contributor

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.

Copy link
Contributor Author

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?

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_);
}
if (strategy == Strategy.RepayDebt) {
data = _repayDebt(provider, tokens, amounts, swapper, vault_);
}
if (strategy == Strategy.Distribute) {
data = _distribute(tokens, amounts, vault_);
}

currentVaultHarvest = address(0);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completing the harvest through the HarvestManager should also emit an Event.
This should not overlap with the information on the event that should also be emitted on the vault itself.
I probably suggest that this event here focuses on the treasury profit and what the vault "gained" by the strategy.


/**
* @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.
*
* @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
)
internal
returns (bytes memory data)
{
uint256 totalAmount = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to explicitly declare a new stack variable = zero, it defaults to zero. The following should be good:

uint256 totalAmount; 

The same comment applies in repayDebt(...)

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);
}

/**
* @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.
*
* @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
)
internal
returns (bytes memory data)
{
address debtAsset = BorrowingVault(payable(address(vault))).debtAsset();
uint256 providerDebt =
ILendingProvider(address(provider)).getBorrowBalance(address(vault), vault);
uint256 totalAmount = 0;
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
);
}

//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);
}
}
80 changes: 80 additions & 0 deletions packages/protocol/src/abstracts/BaseVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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.
Expand Down Expand Up @@ -986,4 +995,75 @@ 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)
{
//check strategy is valid for this vault
if (strategy == Strategy.RepayDebt) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same applies to BaseVaultUpgradeable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);

if (tokens.length != amounts.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same applies to BaseVaultUpgradeable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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[]));
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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"))
);
}
}
Loading