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

Feat: performance fee implementation #13

Merged
merged 7 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 49 additions & 3 deletions src/FourSixTwoSixAgg.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.0;
import {Context} from "@openzeppelin/utils/Context.sol";
import {ERC20, IERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {ERC4626, IERC4626} from "@openzeppelin/token/ERC20/extensions/ERC4626.sol";
import {ERC4626, IERC4626, Math} from "@openzeppelin/token/ERC20/extensions/ERC4626.sol";
import {AccessControlEnumerable} from "@openzeppelin/access/AccessControlEnumerable.sol";
import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol";
import {BalanceForwarder} from "./BalanceForwarder.sol";
Expand All @@ -27,6 +27,10 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
error InvalidStrategyAsset();
error StrategyAlreadyExist();
error AlreadyRemoved();
error PerformanceFeeAlreadySet();
error MaxPerformanceFeeExceeded();
error FeeRecipientNotSet();
error FeeRecipientAlreadySet();

uint8 internal constant REENTRANCYLOCK__UNLOCKED = 1;
uint8 internal constant REENTRANCYLOCK__LOCKED = 2;
Expand All @@ -41,7 +45,12 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
bytes32 public constant STRATEGY_ADDER_ROLE_ADMIN_ROLE = keccak256("STRATEGY_ADDER_ROLE_ADMIN_ROLE");
bytes32 public constant STRATEGY_REMOVER_ROLE = keccak256("STRATEGY_REMOVER_ROLE");
bytes32 public constant STRATEGY_REMOVER_ROLE_ADMIN_ROLE = keccak256("STRATEGY_REMOVER_ROLE_ADMIN_ROLE");
bytes32 public constant PERFORMANCE_FEE_MANAGER_ROLE = keccak256("PERFORMANCE_FEE_MANAGER_ROLE");
bytes32 public constant PERFORMANCE_FEE_MANAGER_ROLE_ADMIN_ROLE =
keccak256("PERFORMANCE_FEE_MANAGER_ROLE_ADMIN_ROLE");

/// @dev The maximum performanceFee the vault can have is 50%
uint256 internal constant MAX_PERFORMANCE_FEE = 0.5e18;
uint256 public constant INTEREST_SMEAR = 2 weeks;

ESRSlot internal esrSlot;
Expand All @@ -52,6 +61,10 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
uint256 public totalAllocated;
/// @dev Total amount of allocation points across all strategies including the cash reserve.
uint256 public totalAllocationPoints;
/// @dev fee rate
uint256 public performanceFee;
/// @dev fee recipient address
address public feeRecipient;

/// @dev An array of strategy addresses to withdraw from
address[] public withdrawalQueue;
Expand Down Expand Up @@ -137,6 +150,25 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
_setRoleAdmin(WITHDRAW_QUEUE_REORDERER_ROLE, WITHDRAW_QUEUE_REORDERER_ROLE_ADMIN_ROLE);
_setRoleAdmin(STRATEGY_ADDER_ROLE, STRATEGY_ADDER_ROLE_ADMIN_ROLE);
_setRoleAdmin(STRATEGY_REMOVER_ROLE, STRATEGY_REMOVER_ROLE_ADMIN_ROLE);
_setRoleAdmin(PERFORMANCE_FEE_MANAGER_ROLE, PERFORMANCE_FEE_MANAGER_ROLE_ADMIN_ROLE);
}

/// @notice Set performance fee recipient address
/// @notice @param _newFeeRecipient Recipient address
function setFeeRecipient(address _newFeeRecipient) external onlyRole(PERFORMANCE_FEE_MANAGER_ROLE) {
if (_newFeeRecipient == feeRecipient) revert FeeRecipientAlreadySet();

feeRecipient = _newFeeRecipient;
}

/// @notice Set performance fee (1e18 == 100%)
/// @notice @param _newFee Fee rate
function setPerformanceFee(uint256 _newFee) external onlyRole(PERFORMANCE_FEE_MANAGER_ROLE) {
if (_newFee > MAX_PERFORMANCE_FEE) revert MaxPerformanceFeeExceeded();
if (feeRecipient == address(0)) revert FeeRecipientNotSet();
if (_newFee == performanceFee) revert PerformanceFeeAlreadySet();

performanceFee = _newFee;
}

/// @notice Enables balance forwarding for sender
Expand Down Expand Up @@ -426,7 +458,6 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
IERC4626 strategy = IERC4626(withdrawalQueue[i]);

_harvest(address(strategy));
_gulp();

Strategy storage strategyStorage = strategies[address(strategy)];

Expand All @@ -447,6 +478,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
strategy.withdraw(withdrawAmount, address(this), address(this));
}

_gulp();

if (assetsRetrieved < assets) {
revert NotEnoughAssets();
}
Expand All @@ -456,6 +489,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn

function _gulp() internal {
ESRSlot memory esrSlotCache = updateInterestAndReturnESRSlotCache();

if (totalAssetsDeposited == 0) return;
uint256 toGulp = totalAssetsAllocatable() - totalAssetsDeposited - esrSlotCache.interestLeft;

uint256 maxGulp = type(uint168).max - esrSlotCache.interestLeft;
Expand Down Expand Up @@ -550,13 +585,24 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
uint256 yield = underlyingBalance - strategyData.allocated;
strategies[strategy].allocated = uint120(underlyingBalance);
totalAllocated += yield;
// TODO possible performance fee

_accruePerformanceFee(yield);
} else {
// TODO handle losses
revert NegativeYield();
}
}

function _accruePerformanceFee(uint256 _yield) internal {
if (feeRecipient == address(0) || performanceFee == 0) return;

// `feeAssets` will be rounded down to 0 if `yield * performanceFee < 1e18`.
uint256 feeAssets = Math.mulDiv(_yield, performanceFee, 1e18, Math.Rounding.Down);
uint256 feeShares = _convertToShares(feeAssets, Math.Rounding.Down);

if (feeShares != 0) _mint(feeRecipient, feeShares);
}

/// @dev Override _afterTokenTransfer hook to call IBalanceTracker.balanceTrackerHook()
/// @dev Calling .balanceTrackerHook() passing the address total balance
/// @param from Address sending the amount
Expand Down
8 changes: 8 additions & 0 deletions test/common/FourSixTwoSixAggBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ contract FourSixTwoSixAggBase is EVaultTestBase {
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_REORDERER_ROLE_ADMIN_ROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE_ADMIN_ROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE_ADMIN_ROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.PERFORMANCE_FEE_MANAGER_ROLE_ADMIN_ROLE(), deployer);

// grant roles to manager
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.ALLOCATION_ADJUSTER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_REORDERER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.PERFORMANCE_FEE_MANAGER_ROLE(), manager);

vm.stopPrank();
}
Expand Down Expand Up @@ -71,16 +73,22 @@ contract FourSixTwoSixAggBase is EVaultTestBase {
fourSixTwoSixAgg.getRoleAdmin(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE()),
fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE_ADMIN_ROLE()
);
assertEq(
fourSixTwoSixAgg.getRoleAdmin(fourSixTwoSixAgg.PERFORMANCE_FEE_MANAGER_ROLE()),
fourSixTwoSixAgg.PERFORMANCE_FEE_MANAGER_ROLE_ADMIN_ROLE()
);

assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.ALLOCATION_ADJUSTER_ROLE_ADMIN_ROLE(), deployer));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_REORDERER_ROLE_ADMIN_ROLE(), deployer));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE_ADMIN_ROLE(), deployer));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE_ADMIN_ROLE(), deployer));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.PERFORMANCE_FEE_MANAGER_ROLE_ADMIN_ROLE(), deployer));

assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.ALLOCATION_ADJUSTER_ROLE(), manager));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_REORDERER_ROLE(), manager));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE(), manager));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE(), manager));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.PERFORMANCE_FEE_MANAGER_ROLE(), manager));
}

function _addStrategy(address from, address strategy, uint256 allocationPoints) internal {
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/BalanceForwarderE2ETest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ contract BalanceForwarderE2ETest is FourSixTwoSixAggBase {
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_REORDERER_ROLE_ADMIN_ROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE_ADMIN_ROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE_ADMIN_ROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.PERFORMANCE_FEE_MANAGER_ROLE_ADMIN_ROLE(), deployer);

// grant roles to manager
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.ALLOCATION_ADJUSTER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_REORDERER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.PERFORMANCE_FEE_MANAGER_ROLE(), manager);
vm.stopPrank();

uint256 initialStrategyAllocationPoints = 500e18;
Expand Down
142 changes: 142 additions & 0 deletions test/e2e/PerformanceFeeE2ETest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {
FourSixTwoSixAggBase,
FourSixTwoSixAgg,
console2,
EVault,
IEVault,
IRMTestDefault,
TestERC20
} from "../common/FourSixTwoSixAggBase.t.sol";

contract PerformanceFeeE2ETest is FourSixTwoSixAggBase {
uint256 user1InitialBalance = 100000e18;

address feeRecipient;

function setUp() public virtual override {
super.setUp();

uint256 initialStrategyAllocationPoints = 500e18;
_addStrategy(manager, address(eTST), initialStrategyAllocationPoints);

assetTST.mint(user1, user1InitialBalance);

feeRecipient = makeAddr("FEE_RECIPIENT");
}

function testSetPerformanceFee() public {
assertEq(fourSixTwoSixAgg.performanceFee(), 0);

uint256 newPerformanceFee = 3e17;

vm.startPrank(manager);
fourSixTwoSixAgg.setFeeRecipient(feeRecipient);
fourSixTwoSixAgg.setPerformanceFee(newPerformanceFee);
vm.stopPrank();

assertEq(fourSixTwoSixAgg.performanceFee(), newPerformanceFee);
assertEq(fourSixTwoSixAgg.feeRecipient(), feeRecipient);
}

function testHarvestWithFeeEnabled() public {
uint256 newPerformanceFee = 3e17;

vm.startPrank(manager);
fourSixTwoSixAgg.setFeeRecipient(feeRecipient);
fourSixTwoSixAgg.setPerformanceFee(newPerformanceFee);
vm.stopPrank();

uint256 amountToDeposit = 10000e18;

// deposit into aggregator
{
uint256 balanceBefore = fourSixTwoSixAgg.balanceOf(user1);
uint256 totalSupplyBefore = fourSixTwoSixAgg.totalSupply();
uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited();
uint256 userAssetBalanceBefore = assetTST.balanceOf(user1);

vm.startPrank(user1);
assetTST.approve(address(fourSixTwoSixAgg), amountToDeposit);
fourSixTwoSixAgg.deposit(amountToDeposit, user1);
vm.stopPrank();

assertEq(fourSixTwoSixAgg.balanceOf(user1), balanceBefore + amountToDeposit);
assertEq(fourSixTwoSixAgg.totalSupply(), totalSupplyBefore + amountToDeposit);
assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore + amountToDeposit);
assertEq(assetTST.balanceOf(user1), userAssetBalanceBefore - amountToDeposit);
}

// rebalance into strategy
vm.warp(block.timestamp + 86400);
{
FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST));

assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), strategyBefore.allocated);

uint256 expectedStrategyCash = fourSixTwoSixAgg.totalAssetsAllocatable() * strategyBefore.allocationPoints
/ fourSixTwoSixAgg.totalAllocationPoints();

vm.prank(user1);
fourSixTwoSixAgg.rebalance(address(eTST));

assertEq(fourSixTwoSixAgg.totalAllocated(), expectedStrategyCash);
assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), expectedStrategyCash);
assertEq((fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, expectedStrategyCash);
}

vm.warp(block.timestamp + 86400);
// mock an increase of strategy balance by 10%
uint256 yield;
{
uint256 aggrCurrentStrategyShareBalance = eTST.balanceOf(address(fourSixTwoSixAgg));
uint256 aggrCurrentStrategyUnderlyingBalance = eTST.convertToAssets(aggrCurrentStrategyShareBalance);
uint256 aggrNewStrategyUnderlyingBalance = aggrCurrentStrategyUnderlyingBalance * 11e17 / 1e18;
yield = aggrNewStrategyUnderlyingBalance - aggrCurrentStrategyUnderlyingBalance;
assetTST.mint(address(eTST), yield);
eTST.skim(type(uint256).max, address(fourSixTwoSixAgg));
}

uint256 expectedPerformanceFee = yield * fourSixTwoSixAgg.performanceFee() / 1e18;
uint256 expectedPerformanceFeeShares = fourSixTwoSixAgg.convertToShares(expectedPerformanceFee);

// harvest
vm.prank(user1);
fourSixTwoSixAgg.harvest(address(eTST));

assertEq(fourSixTwoSixAgg.balanceOf(feeRecipient), expectedPerformanceFeeShares);

// full withdraw, will have to withdraw from strategy as cash reserve is not enough
{
uint256 amountToWithdraw = fourSixTwoSixAgg.balanceOf(user1);
uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited();
uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply();
uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1);
uint256 expectedAssetTST = fourSixTwoSixAgg.convertToAssets(fourSixTwoSixAgg.balanceOf(user1));

vm.prank(user1);
fourSixTwoSixAgg.redeem(amountToWithdraw, user1, user1);

assertApproxEqAbs(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - expectedAssetTST, 1);
assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw);
assertApproxEqAbs(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + expectedAssetTST, 1);
}

// full withdraw of recipient fees
{
uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited();
uint256 assetTSTBalanceBefore = assetTST.balanceOf(feeRecipient);

uint256 feeShares = fourSixTwoSixAgg.balanceOf(feeRecipient);
uint256 expectedAssets = fourSixTwoSixAgg.convertToAssets(feeShares);
vm.prank(feeRecipient);
fourSixTwoSixAgg.redeem(feeShares, feeRecipient, feeRecipient);

assertApproxEqAbs(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - expectedAssets, 1);
assertEq(fourSixTwoSixAgg.totalSupply(), 0);
assertApproxEqAbs(assetTST.balanceOf(feeRecipient), assetTSTBalanceBefore + expectedAssets, 1);
}
}
}
Loading