Skip to content

Commit

Permalink
Merge pull request #20 from euler-xyz/feat/hooks
Browse files Browse the repository at this point in the history
Feat: hooks system
  • Loading branch information
haythemsellami authored Jun 11, 2024
2 parents 40e0a02 + 3bab3fa commit a9631b7
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 17 deletions.
37 changes: 28 additions & 9 deletions src/FourSixTwoSixAgg.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol";
import {BalanceForwarder, IBalanceForwarder} from "./BalanceForwarder.sol";
import {IRewardStreams} from "reward-streams/interfaces/IRewardStreams.sol";
import {SafeCast} from "@openzeppelin/utils/math/SafeCast.sol";
import {Hooks} from "./Hooks.sol";

/// @dev Do NOT use with fee on transfer tokens
/// @dev Do NOT use with rebasing tokens
/// @dev Based on https://github.com/euler-xyz/euler-vault-kit/blob/master/src/Synths/EulerSavingsRate.sol
/// @dev inspired by Yearn v3 ❤️
contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEnumerable {
contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEnumerable, Hooks {
using SafeERC20 for IERC20;
using SafeCast for uint256;

Expand Down Expand Up @@ -48,8 +49,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
bytes32 public constant STRATEGY_ADDER_ROLE_ADMINROLE = keccak256("STRATEGY_ADDER_ROLE_ADMINROLE");
bytes32 public constant STRATEGY_REMOVER_ROLE = keccak256("STRATEGY_REMOVER_ROLE");
bytes32 public constant STRATEGY_REMOVER_ROLE_ADMINROLE = keccak256("STRATEGY_REMOVER_ROLE_ADMINROLE");
bytes32 public constant TREASURY_MANAGER_ROLE = keccak256("TREASURY_MANAGER_ROLE");
bytes32 public constant TREASURY_MANAGER_ROLE_ADMINROLE = keccak256("TREASURY_MANAGER_ROLE_ADMINROLE");
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
bytes32 public constant MANAGER_ROLE_ADMINROLE = keccak256("MANAGER_ROLE_ADMINROLE");

/// @dev The maximum performanceFee the vault can have is 50%
uint256 internal constant MAX_PERFORMANCE_FEE = 0.5e18;
Expand Down Expand Up @@ -179,12 +180,12 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
_setRoleAdmin(WITHDRAW_QUEUE_MANAGER_ROLE, WITHDRAW_QUEUE_MANAGER_ROLE_ADMINROLE);
_setRoleAdmin(STRATEGY_ADDER_ROLE, STRATEGY_ADDER_ROLE_ADMINROLE);
_setRoleAdmin(STRATEGY_REMOVER_ROLE, STRATEGY_REMOVER_ROLE_ADMINROLE);
_setRoleAdmin(TREASURY_MANAGER_ROLE, TREASURY_MANAGER_ROLE_ADMINROLE);
_setRoleAdmin(MANAGER_ROLE, MANAGER_ROLE_ADMINROLE);
}

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

emit SetFeeRecipient(feeRecipient, _newFeeRecipient);
Expand All @@ -194,7 +195,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn

/// @notice Set performance fee (1e18 == 100%)
/// @notice @param _newFee Fee rate
function setPerformanceFee(uint256 _newFee) external onlyRole(TREASURY_MANAGER_ROLE) {
function setPerformanceFee(uint256 _newFee) external onlyRole(MANAGER_ROLE) {
if (_newFee > MAX_PERFORMANCE_FEE) revert MaxPerformanceFeeExceeded();
if (feeRecipient == address(0)) revert FeeRecipientNotSet();
if (_newFee == performanceFee) revert PerformanceFeeAlreadySet();
Expand All @@ -206,7 +207,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn

/// @notice Opt in to strategy rewards
/// @param _strategy Strategy address
function optInStrategyRewards(address _strategy) external onlyRole(TREASURY_MANAGER_ROLE) {
function optInStrategyRewards(address _strategy) external onlyRole(MANAGER_ROLE) {
if (!strategies[_strategy].active) revert InactiveStrategy();

IBalanceForwarder(_strategy).enableBalanceForwarder();
Expand All @@ -216,7 +217,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn

/// @notice Opt out of strategy rewards
/// @param _strategy Strategy address
function optOutStrategyRewards(address _strategy) external onlyRole(TREASURY_MANAGER_ROLE) {
function optOutStrategyRewards(address _strategy) external onlyRole(MANAGER_ROLE) {
IBalanceForwarder(_strategy).disableBalanceForwarder();

emit OptOutStrategyRewards(_strategy);
Expand All @@ -234,7 +235,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
address _reward,
address _recipient,
bool _forfeitRecentReward
) external onlyRole(TREASURY_MANAGER_ROLE) {
) external onlyRole(MANAGER_ROLE) {
address rewardStreams = IBalanceForwarder(_strategy).balanceTrackerAddress();

IRewardStreams(rewardStreams).claimReward(_rewarded, _reward, _recipient, _forfeitRecentReward);
Expand Down Expand Up @@ -367,6 +368,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
revert StrategyAlreadyExist();
}

_callHookTarget(ADD_STRATEGY, _msgSender());

strategies[_strategy] =
Strategy({allocated: 0, allocationPoints: _allocationPoints.toUint120(), active: true, cap: 0});

Expand All @@ -389,6 +392,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
revert AlreadyRemoved();
}

_callHookTarget(REMOVE_STRATEGY, _msgSender());

totalAllocationPoints -= strategyStorage.allocationPoints;
strategyStorage.active = false;
strategyStorage.allocationPoints = 0;
Expand Down Expand Up @@ -513,6 +518,14 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
return super.redeem(shares, receiver, owner);
}

/// @notice Set hooks contract and hooked functions.
/// @dev This funtion should be overriden to implement access control.
/// @param _hookTarget Hooks contract.
/// @param _hookedFns Hooked functions.
function setHooksConfig(address _hookTarget, uint32 _hookedFns) public override onlyRole(MANAGER_ROLE) {
super.setHooksConfig(_hookTarget, _hookedFns);
}

/// @notice update accrued interest.
/// @return struct ESRSlot struct.
function _updateInterestAccrued() internal returns (ESRSlot memory) {
Expand Down Expand Up @@ -545,6 +558,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
/// @dev Increate the total assets deposited, and call IERC4626._deposit()
/// @dev See {IERC4626-_deposit}.
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
_callHookTarget(DEPOSIT, caller);

totalAssetsDeposited += assets;

super._deposit(caller, receiver, assets, shares);
Expand All @@ -558,6 +573,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
internal
override
{
_callHookTarget(WITHDRAW, caller);

totalAssetsDeposited -= assets;
uint256 assetsRetrieved = IERC20(asset()).balanceOf(address(this));

Expand Down Expand Up @@ -640,6 +657,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
return; //nothing to rebalance as this is the cash reserve
}

_callHookTarget(REBALANCE, _msgSender());

_harvest(_strategy);

Strategy memory strategyData = strategies[_strategy];
Expand Down
76 changes: 76 additions & 0 deletions src/Hooks.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {HooksLib, HooksType} from "./lib/HooksLib.sol";
import {IHookTarget} from "evk/src/interfaces/IHookTarget.sol";

abstract contract Hooks {
using HooksLib for HooksType;

error InvalidHooksTarget();
error NotHooksContract();
error InvalidHookedFns();
error EmptyError();

uint32 public constant DEPOSIT = 1 << 0;
uint32 public constant WITHDRAW = 1 << 1;
uint32 public constant REBALANCE = 1 << 2;
uint32 public constant ADD_STRATEGY = 1 << 3;
uint32 public constant REMOVE_STRATEGY = 1 << 4;

uint32 constant ACTIONS_COUNTER = 1 << 5;

/// @dev Contract with hooks implementation
address public hookTarget;
/// @dev Hooked functions
HooksType public hookedFns;

/// @notice Get the hooks contract and the hooked functions.
/// @return address Hooks contract.
/// @return uint32 Hooked functions.
function getHooksConfig() external view returns (address, uint32) {
return (hookTarget, hookedFns.toUint32());
}

/// @notice Set hooks contract and hooked functions.
/// @dev This funtion should be overriden to implement access control.
/// @param _hookTarget Hooks contract.
/// @param _hookedFns Hooked functions.
function setHooksConfig(address _hookTarget, uint32 _hookedFns) public virtual {
if (_hookTarget != address(0) && IHookTarget(_hookTarget).isHookTarget() != IHookTarget.isHookTarget.selector) {
revert NotHooksContract();
}
if (_hookedFns != 0 && _hookTarget == address(0)) {
revert InvalidHooksTarget();
}
if (_hookedFns >= ACTIONS_COUNTER) revert InvalidHookedFns();

hookTarget = _hookTarget;
hookedFns = HooksType.wrap(_hookedFns);
}

/// @notice Checks whether a hook has been installed for the function and if so, invokes the hook target.
/// @param _fn Function to check hook for.
/// @param _caller Caller's address.
function _callHookTarget(uint32 _fn, address _caller) internal {
if (hookedFns.isNotSet(_fn)) return;

address target = hookTarget;

(bool success, bytes memory data) = target.call(abi.encodePacked(msg.data, _caller));

if (!success) _revertBytes(data);
}

/// @dev Revert with call error or EmptyError
/// @param _errorMsg call revert message
function _revertBytes(bytes memory _errorMsg) private pure {
if (_errorMsg.length > 0) {
assembly {
revert(add(32, _errorMsg), mload(_errorMsg))
}
}

revert EmptyError();
}
}
25 changes: 25 additions & 0 deletions src/lib/HooksLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

/// @title HooksLib
/// @custom:security-contact [email protected]
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice Library for `Hooks` custom type
/// @dev This is copied from https://github.com/euler-xyz/euler-vault-kit/blob/30b0b9e36b0a912fe430c7482e9b3bb12d180a4e/src/EVault/shared/types/Flags.sol
library HooksLib {
/// @dev Are *all* of the Hooks in bitMask set?
function isSet(HooksType self, uint32 bitMask) internal pure returns (bool) {
return (HooksType.unwrap(self) & bitMask) == bitMask;
}

/// @dev Are *none* of the Hooks in bitMask set?
function isNotSet(HooksType self, uint32 bitMask) internal pure returns (bool) {
return (HooksType.unwrap(self) & bitMask) == 0;
}

function toUint32(HooksType self) internal pure returns (uint32) {
return HooksType.unwrap(self);
}
}

type HooksType is uint32;
13 changes: 7 additions & 6 deletions test/common/FourSixTwoSixAggBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity ^0.8.0;

import "evk/test/unit/evault/EVaultTestBase.t.sol";
import {FourSixTwoSixAgg} from "../../src/FourSixTwoSixAgg.sol";
import {IHookTarget} from "evk/src/interfaces/IHookTarget.sol";
import {Hooks} from "../../src/Hooks.sol";

contract FourSixTwoSixAggBase is EVaultTestBase {
uint256 public constant CASH_RESERVE_ALLOCATION_POINTS = 1000e18;
Expand Down Expand Up @@ -38,14 +40,14 @@ contract FourSixTwoSixAggBase is EVaultTestBase {
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_MANAGER_ROLE_ADMINROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE_ADMINROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE_ADMINROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.TREASURY_MANAGER_ROLE_ADMINROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.MANAGER_ROLE_ADMINROLE(), deployer);

// grant roles to manager
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_MANAGER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_MANAGER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.TREASURY_MANAGER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.MANAGER_ROLE(), manager);

vm.stopPrank();
}
Expand Down Expand Up @@ -74,21 +76,20 @@ contract FourSixTwoSixAggBase is EVaultTestBase {
fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE_ADMINROLE()
);
assertEq(
fourSixTwoSixAgg.getRoleAdmin(fourSixTwoSixAgg.TREASURY_MANAGER_ROLE()),
fourSixTwoSixAgg.TREASURY_MANAGER_ROLE_ADMINROLE()
fourSixTwoSixAgg.getRoleAdmin(fourSixTwoSixAgg.MANAGER_ROLE()), fourSixTwoSixAgg.MANAGER_ROLE_ADMINROLE()
);

assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_MANAGER_ROLE_ADMINROLE(), deployer));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_MANAGER_ROLE_ADMINROLE(), deployer));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE_ADMINROLE(), deployer));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE_ADMINROLE(), deployer));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.TREASURY_MANAGER_ROLE_ADMINROLE(), deployer));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.MANAGER_ROLE_ADMINROLE(), deployer));

assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_MANAGER_ROLE(), manager));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_MANAGER_ROLE(), manager));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE(), manager));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE(), manager));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.TREASURY_MANAGER_ROLE(), manager));
assertTrue(fourSixTwoSixAgg.hasRole(fourSixTwoSixAgg.MANAGER_ROLE(), manager));
}

function _addStrategy(address from, address strategy, uint256 allocationPoints) internal {
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/BalanceForwarderE2ETest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ contract BalanceForwarderE2ETest is FourSixTwoSixAggBase {
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_MANAGER_ROLE_ADMINROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE_ADMINROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE_ADMINROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.TREASURY_MANAGER_ROLE_ADMINROLE(), deployer);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.MANAGER_ROLE_ADMINROLE(), deployer);

// grant roles to manager
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_MANAGER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.WITHDRAW_QUEUE_MANAGER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_ADDER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.STRATEGY_REMOVER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.TREASURY_MANAGER_ROLE(), manager);
fourSixTwoSixAgg.grantRole(fourSixTwoSixAgg.MANAGER_ROLE(), manager);
vm.stopPrank();

uint256 initialStrategyAllocationPoints = 500e18;
Expand Down
Loading

0 comments on commit a9631b7

Please sign in to comment.