diff --git a/contracts/accountants/HealthCheckAccountant.vy b/contracts/accountants/HealthCheckAccountant.vy deleted file mode 100644 index d834ae1..0000000 --- a/contracts/accountants/HealthCheckAccountant.vy +++ /dev/null @@ -1,482 +0,0 @@ -# @version 0.3.7 - -""" -@title HealthCheck Accountant -@license GNU AGPLv3 -@author yearn.finance -@notice - This contract is meant to serve as the accountant role - for a Yearn V3 Vault. - https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultV3.vy - - It is designed to be able to be added to any number of vaults with any - underlying tokens. There is a default fee config that will be used for - any strategy that reports through a vault that has been added to this - accountant. But also gives the ability for the feeManager to choose - custom values for any value for any given strategy they want to. - - Funds received from the vaults can either be distributed to a specified - feeRecipient or redeemed for the underlying asset and held within this - contract until distributed. -""" -from vyper.interfaces import ERC20 - -### INTERFACES ### -struct StrategyParams: - activation: uint256 - last_report: uint256 - current_debt: uint256 - max_debt: uint256 - -interface IVault: - def asset() -> address: view - def strategies(strategy: address) -> StrategyParams: view - def withdraw(amount: uint256, receiver: address, owner: address) -> uint256: nonpayable - -### EVENTS ### - -event VaultChanged: - vault: address - change: ChangeType - -event UpdateDefaultFeeConfig: - defaultFeeConfig: Fee - -event SetFutureFeeManager: - futureFeeManager: address - -event NewFeeManager: - feeManager: address - -event UpdateFeeRecipient: - oldFeeRecipient: address - newFeeRecipient: address - -event UpdateCustomFeeConfig: - vault: address - strategy: address - custom_config: Fee - -event RemovedCustomFeeConfig: - vault: indexed(address) - strategy: indexed(address) - -event DistributeRewards: - token: address - rewards: uint256 - -### ENUMS ### - -enum ChangeType: - ADDED - REMOVED - -### STRUCTS ### - -# Struct that holds all needed amounts to charge fees -# and issue refunds. All amounts are expressed in Basis points. -# i.e. 10_000 == 100%. -struct Fee: - # Annual management fee to charge on strategy debt. - managementFee: uint16 - # Performance fee to charge on reported gain. - performanceFee: uint16 - # Ratio of reported loss to attempt to refund. - refundRatio: uint16 - # Max percent of the reported gain that the accountant can take. - # A maxFee of 0 will mean none is enforced. - maxFee: uint16 - # Max acceptable gain for a strategy. - maxGain: uint16 - # The max acceptable loss for a strategy. - maxLoss: uint16 - -### CONSTANTS ### - -# 100% in basis points. -MAX_BPS: constant(uint256) = 10_000 - -# NOTE: A four-century period will be missing 3 of its 100 Julian leap years, leaving 97. -# So the average year has 365 + 97/400 = 365.2425 days -# ERROR(Julian): -0.0078 -# ERROR(Gregorian): -0.0003 -# A day = 24 * 60 * 60 sec = 86400 sec -# 365.2425 * 86400 = 31556952.0 -SECS_PER_YEAR: constant(uint256) = 31_556_952 # 365.2425 days - -PERFORMANCE_FEE_THRESHOLD: constant(uint16) = 5_000 - -MANAGEMENT_FEE_THRESHOLD: constant(uint16) = 200 - -### STORAGE ### - -# Address in charge of the accountant. -feeManager: public(address) -# Address to become the fee manager. -futureFeeManager: public(address) -# Address to distribute the accumulated fees to. -feeRecipient: public(address) - -# Mapping of vaults that this serves as an accountant for. -vaults: public(HashMap[address, bool]) -# Default config to use unless a custom one is set. -defaultConfig: public(Fee) -# Mapping vault => strategy => custom Fee config -fees: public(HashMap[address, HashMap[address, Fee]]) -# Mapping vault => strategy => flag to use a custom config. -custom: public(HashMap[address, HashMap[address, bool]]) - -@external -def __init__( - fee_manager: address, - fee_recipient: address, - default_management: uint16, - default_performance: uint16, - default_refund: uint16, - default_max_fee: uint16, - default_max_gain: uint16, - default_max_loss: uint16 -): - """ - @notice Initialize the accountant and default fee config. - @param fee_manager Address to be in charge of this accountant. - @param default_management Default annual management fee to charge. - @param default_performance Default performance fee to charge. - @param default_refund Default refund ratio to give back on losses. - @param default_max_fee Default max fee to allow as a percent of gain. - @param default_max_gain Default max percent gain a strategy can report. - @param default_max_loss Default max percent loss a strategy can report. - """ - assert fee_manager != empty(address), "ZERO ADDRESS" - assert fee_recipient != empty(address), "ZERO ADDRESS" - assert default_management <= MANAGEMENT_FEE_THRESHOLD, "exceeds management fee threshold" - assert default_performance <= PERFORMANCE_FEE_THRESHOLD, "exceeds performance fee threshold" - assert convert(default_max_fee, uint256) <= MAX_BPS, "too high" - assert convert(default_max_gain, uint256) <= MAX_BPS, "too high" - assert convert(default_max_loss, uint256) <= MAX_BPS, "too high" - - # Set initial addresses - self.feeManager = fee_manager - self.feeRecipient = fee_recipient - - # Set the default fee config - self.defaultConfig = Fee({ - managementFee: default_management, - performanceFee: default_performance, - refundRatio: default_refund, - maxFee: default_max_fee, - maxGain: default_max_gain, - maxLoss: default_max_loss - }) - - log UpdateDefaultFeeConfig(self.defaultConfig) - - -@external -def report(strategy: address, gain: uint256, loss: uint256) -> (uint256, uint256): - """ - @notice To be called by a vault during the process_report in which the accountant - will charge fees based on the gain or loss the strategy is reporting. - @dev Can only be called by a vault that has been added to this accountant. - Will default to the defaultConfig for all amounts unless a custom config - has been set for a specific strategy. - @param strategy The strategy that is reporting. - @param gain The profit the strategy is reporting if any. - @param loss The loss the strategy is reporting if any. - """ - # Make sure this is a valid vault. - assert self.vaults[msg.sender], "!authorized" - - # Declare the config to use - fee: Fee = empty(Fee) - - # Check if it there is a custom config to use. - if self.custom[msg.sender][strategy]: - fee = self.fees[msg.sender][strategy] - else: - # Otherwise use the default. - fee = self.defaultConfig - - total_fees: uint256 = 0 - total_refunds: uint256 = 0 - - # Retrieve the strategies params from the vault. - strategy_params: StrategyParams = IVault(msg.sender).strategies(strategy) - - # Charge management fees no matter gain or loss. - if fee.managementFee > 0: - # Time since last harvest. - duration: uint256 = block.timestamp - strategy_params.last_report - # managementFee is an annual amount, so charge based on the time passed. - total_fees = ( - strategy_params.current_debt - * duration - * convert(fee.managementFee, uint256) - / MAX_BPS - / SECS_PER_YEAR - ) - - # Only charge performance fees if there is a gain. - if gain > 0: - assert gain <= strategy_params.current_debt * convert(fee.maxGain, uint256) / MAX_BPS, "too much gain" - total_fees += (gain * convert(fee.performanceFee, uint256)) / MAX_BPS - - else: - if fee.maxLoss < 10_000: - assert loss <= strategy_params.current_debt * convert(fee.maxLoss, uint256) / MAX_BPS, "too much loss" - - # Means we should have a loss. - if fee.refundRatio > 0: - # Cache the underlying asset the vault uses. - asset: address = IVault(msg.sender).asset() - # Give back either all we have or based on refund ratio. - total_refunds = min(loss * convert(fee.refundRatio, uint256) / MAX_BPS, ERC20(asset).balanceOf(self)) - - if total_refunds > 0: - # Approve the vault to pull the underlying asset. - self.erc20_safe_approve(asset, msg.sender, total_refunds) - - # 0 Max fee means it is not enforced. - if fee.maxFee > 0: - # Ensure fee does not exceed the maxFee %. - total_fees = min(gain * convert(fee.maxFee, uint256) / MAX_BPS, total_fees) - - return (total_fees, total_refunds) - - -@internal -def erc20_safe_approve(token: address, spender: address, amount: uint256): - # Used only to approve tokens that are not the type managed by this vault. - # Used to handle non-compliant tokens like USDT - assert ERC20(token).approve(spender, amount, default_return_value=True), "approval failed" - -@internal -def _erc20_safe_transfer(token: address, receiver: address, amount: uint256): - # Used only to send tokens that are not the type managed by this vault. - assert ERC20(token).transfer(receiver, amount, default_return_value=True), "transfer failed" - -@external -def addVault(vault: address): - """ - @notice Add a new vault for this accountant to charge fees for. - @dev This is not used to set any of the fees for the specific - vault or strategy. Each fee will be set separately. - @param vault The address of a vault to allow to use this accountant. - """ - assert msg.sender == self.feeManager, "not fee manager" - assert not self.vaults[vault], "already added" - - self.vaults[vault] = True - - log VaultChanged(vault, ChangeType.ADDED) - - -@external -def removeVault(vault: address): - """ - @notice Removes a vault for this accountant to charge fee for. - @param vault The address of a vault to allow to use this accountant. - """ - assert msg.sender == self.feeManager, "not fee manager" - assert self.vaults[vault], "not added" - - self.vaults[vault] = False - - log VaultChanged(vault, ChangeType.REMOVED) - - -@external -def updateDefaultConfig( - default_management: uint16, - default_performance: uint16, - default_refund: uint16, - default_max_fee: uint16, - default_max_gain: uint16, - default_max_loss: uint16 -): - """ - @notice Update the default config used for all strategies. - @param default_management Default annual management fee to charge. - @param default_performance Default performance fee to charge. - @param default_refund Default refund ratio to give back on losses. - @param default_max_fee Default max fee to allow as a percent of gain. - @param default_max_gain Default max percent gain a strategy can report. - @param default_max_loss Default max percent loss a strategy can report. - """ - assert msg.sender == self.feeManager, "not fee manager" - assert default_management <= MANAGEMENT_FEE_THRESHOLD, "exceeds management fee threshold" - assert default_performance <= PERFORMANCE_FEE_THRESHOLD, "exceeds performance fee threshold" - assert convert(default_max_fee, uint256) <= MAX_BPS, "too high" - assert convert(default_max_gain, uint256) <= MAX_BPS, "too high" - assert convert(default_max_loss, uint256) <= MAX_BPS, "too high" - - self.defaultConfig = Fee({ - managementFee: default_management, - performanceFee: default_performance, - refundRatio: default_refund, - maxFee: default_max_fee, - maxGain: default_max_gain, - maxLoss: default_max_loss - }) - - log UpdateDefaultFeeConfig(self.defaultConfig) - - -@external -def setCustomConfig( - vault: address, - strategy: address, - custom_management: uint16, - custom_performance: uint16, - custom_refund: uint16, - custom_max_fee: uint16, - custom_max_gain: uint16, - custom_max_loss: uint16 -): - """ - @notice Used to set a custom fee amounts for a specific strategy. - In a specific vault. - @dev Setting this will cause the default config to be overridden. - @param vault The vault the strategy is hooked up to. - @param strategy The strategy to customize. - @param custom_management Custom annual management fee to charge. - @param custom_performance Custom performance fee to charge. - @param custom_refund Custom refund ratio to give back on losses. - @param custom_max_fee Custom max fee to allow as a percent of gain. - @param custom_max_gain Default max percent gain a strategy can report. - @param custom_max_loss Default max percent loss a strategy can report. - """ - assert msg.sender == self.feeManager, "not fee manager" - assert self.vaults[vault], "vault not added" - assert custom_management <= MANAGEMENT_FEE_THRESHOLD, "exceeds management fee threshold" - assert custom_performance <= PERFORMANCE_FEE_THRESHOLD, "exceeds performance fee threshold" - assert convert(custom_max_fee, uint256) <= MAX_BPS, "too high" - assert convert(custom_max_gain, uint256) <= MAX_BPS, "too high" - assert convert(custom_max_loss, uint256) <= MAX_BPS, "too high" - - # Set this strategies custom config. - self.fees[vault][strategy] = Fee({ - managementFee: custom_management, - performanceFee: custom_performance, - refundRatio: custom_refund, - maxFee: custom_max_fee, - maxGain: custom_max_gain, - maxLoss: custom_max_loss - }) - - # Set custom flag. - self.custom[vault][strategy] = True - - log UpdateCustomFeeConfig(vault, strategy, self.fees[vault][strategy]) - - -@external -def removeCustomConfig(vault: address, strategy: address): - """ - @notice Removes a previously set custom config for a strategy. - @param strategy The strategy to remove custom setting for. - """ - assert msg.sender == self.feeManager, "not fee manager" - assert self.custom[vault][strategy], "No custom fees set" - - # Set all the strategies custom fees to 0. - self.fees[vault][strategy] = empty(Fee) - - # Set custom flag. - self.custom[vault][strategy] = False - - # Emit relevant event. - log RemovedCustomFeeConfig(vault, strategy) - - -@external -def withdrawUnderlying(vault: address, amount: uint256): - """ - @notice Can be used by the fee manager to simply withdraw the underlying - asset from a vault it charges fees for. - @dev Refunds are payed in the underlying but fees are charged in the vaults - token. So management may want to fee some funds to allow for refunds to - work across all vaults of the same underlying. - @param vault The vault to redeem from. - @param amount The amount in the underlying to withdraw. - """ - assert msg.sender == self.feeManager, "not fee manager" - IVault(vault).withdraw(amount, self, self) - - -@external -def distribute(token: address) -> uint256: - """ - @notice used to withdraw accumulated fees to the designated recipient. - @dev This can be used to withdraw the vault tokens or underlying tokens - that had previously been withdrawn. - @param token The token to distribute. - @return The amount of token distributed. - """ - assert msg.sender == self.feeManager, "not fee manager" - - rewards: uint256 = ERC20(token).balanceOf(self) - self._erc20_safe_transfer(token, self.feeRecipient, rewards) - - log DistributeRewards(token, rewards) - return rewards - -@external -def setFutureFeeManager(futureFeeManager: address): - """ - @notice Step 1 of 2 to set a new feeManager. - @dev The address is set to futureFeeManager and will need to - call accept_feeManager in order to update the actual feeManager. - @param futureFeeManager Address to set to futureFeeManager. - """ - assert msg.sender == self.feeManager, "not fee manager" - assert futureFeeManager != empty(address), "ZERO ADDRESS" - self.futureFeeManager = futureFeeManager - - log SetFutureFeeManager(futureFeeManager) - - -@external -def acceptFeeManager(): - """ - @notice to be called by the futureFeeManager to accept the role change. - """ - assert msg.sender == self.futureFeeManager, "not future fee manager" - self.feeManager = self.futureFeeManager - self.futureFeeManager = empty(address) - - log NewFeeManager(msg.sender) - - -@external -def setFeeRecipient(newFeeRecipient: address): - """ - @notice Set a new address to receive distributed rewards. - @param newFeeRecipient Address to receive distributed fees. - """ - assert msg.sender == self.feeManager, "not fee manager" - assert newFeeRecipient != empty(address), "ZERO ADDRESS" - oldFeeRecipient: address = self.feeRecipient - self.feeRecipient = newFeeRecipient - - log UpdateFeeRecipient(oldFeeRecipient, newFeeRecipient) - - -@view -@external -def performanceFeeThreshold() -> uint16: - """ - @notice External function to get the max a performance fee can be. - @return Max performance fee the accountant can charge. - """ - return PERFORMANCE_FEE_THRESHOLD - - -@view -@external -def managementFeeThreshold() -> uint16: - """ - @notice External function to get the max a management fee can be. - @return Max management fee the accountant can charge. - """ - return MANAGEMENT_FEE_THRESHOLD diff --git a/contracts/accountants/HelathCheckAccountant.sol b/contracts/accountants/HelathCheckAccountant.sol new file mode 100644 index 0000000..3cffe48 --- /dev/null +++ b/contracts/accountants/HelathCheckAccountant.sol @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity 0.8.18; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "@yearn-vaults/interfaces/IVault.sol"; + +contract HealthCheckAccountant { + using SafeERC20 for ERC20; + + /// @notice An event emitted when a vault is added or removed. + event VaultChanged(address indexed vault, ChangeType change); + + /// @notice An event emitted when the default fee configuration is updated. + event UpdateDefaultFeeConfig(Fee defaultFeeConfig); + + /// @notice An event emitted when the future fee manager is set. + event SetFutureFeeManager(address futureFeeManager); + + /// @notice An event emitted when a new fee manager is accepted. + event NewFeeManager(address feeManager); + + /// @notice An event emitted when the fee recipient is updated. + event UpdateFeeRecipient(address oldFeeRecipient, address newFeeRecipient); + + /// @notice An event emitted when a custom fee configuration is updated. + event UpdateCustomFeeConfig( + address vault, + address strategy, + Fee custom_config + ); + + /// @notice An event emitted when a custom fee configuration is removed. + event RemovedCustomFeeConfig( + address indexed vault, + address indexed strategy + ); + + /// @notice An event emitted when the `maxLoss` parameter is updated. + event UpdateMaxLoss(uint256 maxLoss); + + /// @notice An event emitted when rewards are distributed. + event DistributeRewards(address token, uint256 rewards); + + /// @notice Enum defining change types (added or removed). + enum ChangeType { + ADDED, + REMOVED + } + + /// @notice Struct representing fee details. + struct Fee { + uint16 managementFee; // Annual management fee to charge. + uint16 performanceFee; // Performance fee to charge. + uint16 refundRatio; // Refund ratio to give back on losses. + uint16 maxFee; // Max fee allowed as a percent of gain. + uint16 maxGain; // Max percent gain a strategy can report. + uint16 maxLoss; // Max percent loss a strategy can report. + } + + modifier onlyFeeManager() { + _checkFeeManager(); + _; + } + + function _checkFeeManager() internal view virtual { + require(feeManager == msg.sender, "!fee manager"); + } + + /// @notice Constant defining the maximum basis points. + uint256 internal constant MAX_BPS = 10_000; + + /// @notice Constant defining the number of seconds in a year. + uint256 internal constant SECS_PER_YEAR = 31_556_952; + + /// @notice Constant defining the performance fee threshold. + uint16 internal constant PERFORMANCE_FEE_THRESHOLD = 5_000; + + /// @notice Constant defining the management fee threshold. + uint16 internal constant MANAGEMENT_FEE_THRESHOLD = 200; + + /// @notice The address of the fee manager. + address public feeManager; + + /// @notice The address of the future fee manager. + address public futureFeeManager; + + /// @notice The address of the fee recipient. + address public feeRecipient; + + uint256 public maxLoss; + + /// @notice Mapping to track added vaults. + mapping(address => bool) public vaults; + + /// @notice The default fee configuration. + Fee public defaultConfig; + + /// @notice Mapping vault => strategy => custom Fee config if any. + mapping(address => mapping(address => Fee)) public fees; + + /// @notice Mapping vault => strategy => flag to use a custom config. + mapping(address => mapping(address => bool)) public custom; + + constructor( + address _feeManager, + address _feeRecipient, + uint16 defaultManagement, + uint16 defaultPerformance, + uint16 defaultRefund, + uint16 defaultMaxFee, + uint16 defaultMaxGain, + uint16 defaultMaxLoss + ) { + require(_feeManager != address(0), "ZERO ADDRESS"); + require(_feeRecipient != address(0), "ZERO ADDRESS"); + require( + defaultManagement <= MANAGEMENT_FEE_THRESHOLD, + "exceeds management fee threshold" + ); + require( + defaultPerformance <= PERFORMANCE_FEE_THRESHOLD, + "exceeds performance fee threshold" + ); + require(defaultMaxFee <= MAX_BPS, "too high"); + require(defaultMaxGain <= MAX_BPS, "too high"); + require(defaultMaxLoss <= MAX_BPS, "too high"); + + feeManager = _feeManager; + feeRecipient = _feeRecipient; + + defaultConfig = Fee({ + managementFee: defaultManagement, + performanceFee: defaultPerformance, + refundRatio: defaultRefund, + maxFee: defaultMaxFee, + maxGain: defaultMaxGain, + maxLoss: defaultMaxLoss + }); + + emit UpdateDefaultFeeConfig(defaultConfig); + } + + /** + * @notice Called by a vault when a `strategy` is reporting. + * @dev The msg.sender must have been added to the `vaults` mapping. + * @param strategy Address of the strategy reporting. + * @param gain Amount of the gain if any. + * @param loss Amount of the loss if any. + * @return totalFees if any to charge. + * @return totalRefunds if any for the vault to pull. + */ + function report( + address strategy, + uint256 gain, + uint256 loss + ) external returns (uint256 totalFees, uint256 totalRefunds) { + // Make sure this is a valid vault. + require(vaults[msg.sender], "!authorized"); + + // Declare the config to use + Fee memory fee; + + // Check if there is a custom config to use. + if (custom[msg.sender][strategy]) { + fee = fees[msg.sender][strategy]; + } else { + // Otherwise use the default. + fee = defaultConfig; + } + + // Retrieve the strategy's params from the vault. + IVault.StrategyParams memory strategyParams = IVault(msg.sender) + .strategies(strategy); + + // Charge management fees no matter gain or loss. + if (fee.managementFee > 0) { + // Time since the last harvest. + uint256 duration = block.timestamp - strategyParams.last_report; + // managementFee is an annual amount, so charge based on the time passed. + totalFees = ((strategyParams.current_debt * + duration * + (fee.managementFee)) / + MAX_BPS / + SECS_PER_YEAR); + } + + // Only charge performance fees if there is a gain. + if (gain > 0) { + require( + gain <= (strategyParams.current_debt * (fee.maxGain)) / MAX_BPS, + "too much gain" + ); + totalFees += (gain * (fee.performanceFee)) / MAX_BPS; + } else { + if (fee.maxLoss < MAX_BPS) { + require( + loss <= + (strategyParams.current_debt * (fee.maxLoss)) / MAX_BPS, + "too much loss" + ); + } + + // Means we should have a loss. + if (fee.refundRatio > 0) { + // Cache the underlying asset the vault uses. + address asset = IVault(msg.sender).asset(); + // Give back either all we have or based on the refund ratio. + totalRefunds = Math.min( + (loss * (fee.refundRatio)) / MAX_BPS, + ERC20(asset).balanceOf(address(this)) + ); + + if (totalRefunds > 0) { + // Approve the vault to pull the underlying asset. + ERC20(asset).safeApprove(msg.sender, totalRefunds); + } + } + } + + // 0 Max fee means it is not enforced. + if (fee.maxFee > 0) { + // Ensure fee does not exceed the maxFee %. + totalFees = Math.min((gain * (fee.maxFee)) / MAX_BPS, totalFees); + } + + return (totalFees, totalRefunds); + } + + /** + * @notice Function to add a new vault for this accountant to charge fees for. + * @dev This is not used to set any of the fees for the specific vault or strategy. Each fee will be set separately. + * @param vault The address of a vault to allow to use this accountant. + */ + function addVault(address vault) external onlyFeeManager { + // Ensure the vault has not already been added. + require(!vaults[vault], "already added"); + + vaults[vault] = true; + + emit VaultChanged(vault, ChangeType.ADDED); + } + + /** + * @notice Function to remove a vault from this accountant's fee charging list. + * @param vault The address of the vault to be removed from this accountant. + */ + function removeVault(address vault) external onlyFeeManager { + // Ensure the vault has been previously added. + require(vaults[vault], "not added"); + + vaults[vault] = false; + + emit VaultChanged(vault, ChangeType.REMOVED); + } + + /** + * @notice Function to update the default fee configuration used for all strategies. + * @param defaultManagement Default annual management fee to charge. + * @param defaultPerformance Default performance fee to charge. + * @param defaultRefund Default refund ratio to give back on losses. + * @param defaultMaxFee Default max fee to allow as a percent of gain. + * @param defaultMaxGain Default max percent gain a strategy can report. + * @param defaultMaxLoss Default max percent loss a strategy can report. + */ + function updateDefaultConfig( + uint16 defaultManagement, + uint16 defaultPerformance, + uint16 defaultRefund, + uint16 defaultMaxFee, + uint16 defaultMaxGain, + uint16 defaultMaxLoss + ) external onlyFeeManager { + // Check for threshold and limit conditions. + require( + defaultManagement <= MANAGEMENT_FEE_THRESHOLD, + "exceeds management fee threshold" + ); + require( + defaultPerformance <= PERFORMANCE_FEE_THRESHOLD, + "exceeds performance fee threshold" + ); + require(defaultMaxFee <= MAX_BPS, "too high"); + require(defaultMaxGain <= MAX_BPS, "too high"); + require(defaultMaxLoss <= MAX_BPS, "too high"); + + // Update the default fee configuration. + defaultConfig = Fee({ + managementFee: defaultManagement, + performanceFee: defaultPerformance, + refundRatio: defaultRefund, + maxFee: defaultMaxFee, + maxGain: defaultMaxGain, + maxLoss: defaultMaxLoss + }); + + emit UpdateDefaultFeeConfig(defaultConfig); + } + + /** + * @notice Function to set a custom fee configuration for a specific strategy in a specific vault. + * @param vault The vault the strategy is hooked up to. + * @param strategy The strategy to customize. + * @param customManagement Custom annual management fee to charge. + * @param customPerformance Custom performance fee to charge. + * @param customRefund Custom refund ratio to give back on losses. + * @param customMaxFee Custom max fee to allow as a percent of gain. + * @param customMaxGain Custom max percent gain a strategy can report. + * @param customMaxLoss Custom max percent loss a strategy can report. + */ + function setCustomConfig( + address vault, + address strategy, + uint16 customManagement, + uint16 customPerformance, + uint16 customRefund, + uint16 customMaxFee, + uint16 customMaxGain, + uint16 customMaxLoss + ) external onlyFeeManager { + // Ensure the vault has been added. + require(vaults[vault], "vault not added"); + // Check for threshold and limit conditions. + require( + customManagement <= MANAGEMENT_FEE_THRESHOLD, + "exceeds management fee threshold" + ); + require( + customPerformance <= PERFORMANCE_FEE_THRESHOLD, + "exceeds performance fee threshold" + ); + require(customMaxFee <= MAX_BPS, "too high"); + require(customMaxGain <= MAX_BPS, "too high"); + require(customMaxLoss <= MAX_BPS, "too high"); + + // Set the strategy's custom config. + fees[vault][strategy] = Fee({ + managementFee: customManagement, + performanceFee: customPerformance, + refundRatio: customRefund, + maxFee: customMaxFee, + maxGain: customMaxGain, + maxLoss: customMaxLoss + }); + + // Set the custom flag. + custom[vault][strategy] = true; + + emit UpdateCustomFeeConfig(vault, strategy, fees[vault][strategy]); + } + + /** + * @notice Function to remove a previously set custom fee configuration for a strategy. + * @param vault The vault to remove custom setting for. + * @param strategy The strategy to remove custom setting for. + */ + function removeCustomConfig( + address vault, + address strategy + ) external onlyFeeManager { + // Ensure custom fees are set for the specified vault and strategy. + require(custom[vault][strategy], "No custom fees set"); + + // Set all the strategy's custom fees to 0. + delete fees[vault][strategy]; + + // Clear the custom flag. + custom[vault][strategy] = false; + + // Emit relevant event. + emit RemovedCustomFeeConfig(vault, strategy); + } + + /** + * @notice Function to withdraw the underlying asset from a vault. + * @param vault The vault to withdraw from. + * @param amount The amount in the underlying to withdraw. + */ + function withdrawUnderlying( + address vault, + uint256 amount + ) external onlyFeeManager { + IVault(vault).withdraw(amount, address(this), address(this), maxLoss); + } + + /** + * @notice Sets the `maxLoss` parameter to be used on withdraws. + * @param _maxLoss The amount in basis points to set as the maximum loss. + */ + function setMaxLoss(uint256 _maxLoss) external onlyFeeManager { + // Ensure that the provided `maxLoss` does not exceed 100% (in basis points). + require(_maxLoss <= MAX_BPS, "higher than 100%"); + + maxLoss = _maxLoss; + + // Emit an event to signal the update of the `maxLoss` parameter. + emit UpdateMaxLoss(_maxLoss); + } + + /** + * @notice Function to distribute all accumulated fees to the designated recipient. + * @param token The token to distribute. + */ + function distribute(address token) external { + distribute(token, ERC20(token).balanceOf(address(this))); + } + + /** + * @notice Function to distribute accumulated fees to the designated recipient. + * @param token The token to distribute. + * @param amount amount of token to distribute. + */ + function distribute(address token, uint256 amount) public onlyFeeManager { + ERC20(token).safeTransfer(feeRecipient, amount); + + emit DistributeRewards(token, amount); + } + + /** + * @notice Function to set a future fee manager address. + * @param _futureFeeManager The address to set as the future fee manager. + */ + function setFutureFeeManager( + address _futureFeeManager + ) external onlyFeeManager { + // Ensure the futureFeeManager is not a zero address. + require(_futureFeeManager != address(0), "ZERO ADDRESS"); + futureFeeManager = _futureFeeManager; + + emit SetFutureFeeManager(_futureFeeManager); + } + + /** + * @notice Function to accept the role change and become the new fee manager. + * @dev This function allows the future fee manager to accept the role change and become the new fee manager. + */ + function acceptFeeManager() external { + // Make sure the sender is the future fee manager. + require(msg.sender == futureFeeManager, "not future fee manager"); + feeManager = futureFeeManager; + futureFeeManager = address(0); + + emit NewFeeManager(msg.sender); + } + + /** + * @notice Function to set a new address to receive distributed rewards. + * @param newFeeRecipient Address to receive distributed fees. + */ + function setFeeRecipient(address newFeeRecipient) external onlyFeeManager { + // Ensure the newFeeRecipient is not a zero address. + require(newFeeRecipient != address(0), "ZERO ADDRESS"); + address oldRecipient = feeRecipient; + feeRecipient = newFeeRecipient; + + emit UpdateFeeRecipient(oldRecipient, newFeeRecipient); + } + + /** + * @notice View function to get the max a performance fee can be. + * @dev This function provides the maximum performance fee that the accountant can charge. + * @return The maximum performance fee. + */ + function performanceFeeThreshold() external view returns (uint16) { + return PERFORMANCE_FEE_THRESHOLD; + } + + /** + * @notice View function to get the max a management fee can be. + * @dev This function provides the maximum management fee that the accountant can charge. + * @return The maximum management fee. + */ + function managementFeeThreshold() external view returns (uint16) { + return MANAGEMENT_FEE_THRESHOLD; + } +}