Skip to content

Commit

Permalink
Merge pull request #13 from euler-xyz/feat/fees
Browse files Browse the repository at this point in the history
Feat: performance fee implementation
  • Loading branch information
haythemsellami authored May 29, 2024
2 parents 142e57b + b34d179 commit 3173f75
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 3 deletions.
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);
}
}
}

0 comments on commit 3173f75

Please sign in to comment.