From 9c3660754c0e2b84ff7edbb98963f1d1dd4f18cd Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:35:11 -0700 Subject: [PATCH] build: refund accountant (#27) * build: refund rewards * test: reward accountant * feat: vault manager * fix: report flow * fix: rename to refund * test: organize * feat: remove max fee cap * fix: wavey review * fix: functions * fix: scripts * fix: dont check activation --- .../accountants/HealthCheckAccountant.sol | 171 ++- contracts/accountants/RefundAccountant.sol | 122 ++ .../debtAllocators/GenericDebtAllocator.sol | 2 - scripts/deploy_accountant.py | 25 +- scripts/deploy_address_provider.py | 3 +- scripts/deploy_allocator_factory.py | 2 + scripts/deploy_registry.py | 3 +- .../test_generic_accountant.py | 0 .../test_healthcheck_accountant.py | 245 ++- tests/accountants/test_refund_accountant.py | 1310 +++++++++++++++++ tests/conftest.py | 41 + .../test_generic_debt_allocator.py | 5 - tests/{ => registry}/test_registry.py | 0 tests/{ => registry}/test_registry_factory.py | 0 tests/{ => registry}/test_release_registry.py | 0 15 files changed, 1859 insertions(+), 70 deletions(-) create mode 100644 contracts/accountants/RefundAccountant.sol rename tests/{ => accountants}/test_generic_accountant.py (100%) rename tests/{ => accountants}/test_healthcheck_accountant.py (81%) create mode 100644 tests/accountants/test_refund_accountant.py rename tests/{ => debtAllocators}/test_generic_debt_allocator.py (98%) rename tests/{ => registry}/test_registry.py (100%) rename tests/{ => registry}/test_registry_factory.py (100%) rename tests/{ => registry}/test_release_registry.py (100%) diff --git a/contracts/accountants/HealthCheckAccountant.sol b/contracts/accountants/HealthCheckAccountant.sol index 8400382..8116870 100644 --- a/contracts/accountants/HealthCheckAccountant.sol +++ b/contracts/accountants/HealthCheckAccountant.sol @@ -7,6 +7,9 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; +/// @title Health Check Accountant. +/// @dev Will charge fees, issue refunds, and run health check on any reported +/// gains or losses during a strategy's report. contract HealthCheckAccountant { using SafeERC20 for ERC20; @@ -17,18 +20,24 @@ contract HealthCheckAccountant { event UpdateDefaultFeeConfig(Fee defaultFeeConfig); /// @notice An event emitted when the future fee manager is set. - event SetFutureFeeManager(address futureFeeManager); + event SetFutureFeeManager(address indexed futureFeeManager); /// @notice An event emitted when a new fee manager is accepted. - event NewFeeManager(address feeManager); + event NewFeeManager(address indexed feeManager); + + /// @notice An event emitted when a new vault manager is set. + event UpdateVaultManager(address indexed newVaultManager); /// @notice An event emitted when the fee recipient is updated. - event UpdateFeeRecipient(address oldFeeRecipient, address newFeeRecipient); + event UpdateFeeRecipient( + address indexed oldFeeRecipient, + address indexed newFeeRecipient + ); /// @notice An event emitted when a custom fee configuration is updated. event UpdateCustomFeeConfig( - address vault, - address strategy, + address indexed vault, + address indexed strategy, Fee custom_config ); @@ -42,7 +51,7 @@ contract HealthCheckAccountant { event UpdateMaxLoss(uint256 maxLoss); /// @notice An event emitted when rewards are distributed. - event DistributeRewards(address token, uint256 rewards); + event DistributeRewards(address indexed token, uint256 rewards); /// @notice Enum defining change types (added or removed). enum ChangeType { @@ -66,8 +75,29 @@ contract HealthCheckAccountant { _; } + modifier onlyVaultOrFeeManagers() { + _checkVaultOrFeeManager(); + _; + } + + modifier onlyAddedVaults() { + _checkVaultIsAdded(); + _; + } + function _checkFeeManager() internal view virtual { - require(feeManager == msg.sender, "!fee manager"); + require(msg.sender == feeManager, "!fee manager"); + } + + function _checkVaultOrFeeManager() internal view virtual { + require( + msg.sender == feeManager || msg.sender == vaultManager, + "!vault manager" + ); + } + + function _checkVaultIsAdded() internal view virtual { + require(vaults[msg.sender], "vault not added"); } /// @notice Constant defining the maximum basis points. @@ -91,8 +121,12 @@ contract HealthCheckAccountant { /// @notice The address of the fee recipient. address public feeRecipient; + /// @notice The amount of max loss to use when redeeming from vaults. uint256 public maxLoss; + /// @notice An address that can add or remove vaults. + address public vaultManager; + /// @notice Mapping to track added vaults. mapping(address => bool) public vaults; @@ -103,7 +137,7 @@ contract HealthCheckAccountant { mapping(address => mapping(address => Fee)) public customConfig; /// @notice Mapping vault => strategy => flag to use a custom config. - mapping(address => mapping(address => uint256)) internal _custom; + mapping(address => mapping(address => uint256)) internal _useCustomConfig; constructor( address _feeManager, @@ -125,8 +159,6 @@ contract HealthCheckAccountant { 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; @@ -157,15 +189,17 @@ contract HealthCheckAccountant { address strategy, uint256 gain, uint256 loss - ) external returns (uint256 totalFees, uint256 totalRefunds) { - // Make sure this is a valid vault. - require(vaults[msg.sender], "!authorized"); - + ) + public + virtual + onlyAddedVaults + returns (uint256 totalFees, uint256 totalRefunds) + { // Declare the config to use Fee memory fee; // Check if there is a custom config to use. - if (_custom[msg.sender][strategy] != 0) { + if (_useCustomConfig[msg.sender][strategy] != 0) { fee = customConfig[msg.sender][strategy]; } else { // Otherwise use the default. @@ -190,12 +224,18 @@ contract HealthCheckAccountant { // Only charge performance fees if there is a gain. if (gain > 0) { - require( - gain <= (strategyParams.current_debt * (fee.maxGain)) / MAX_BPS, - "too much gain" - ); + // Setting `maxGain` to 0 will disable the healthcheck on profits. + if (fee.maxGain > 0) { + require( + gain <= + (strategyParams.current_debt * (fee.maxGain)) / MAX_BPS, + "too much gain" + ); + } + totalFees += (gain * (fee.performanceFee)) / MAX_BPS; } else { + // Setting `maxLoss` to 10_000 will disable the healthcheck on losses. if (fee.maxLoss < MAX_BPS) { require( loss <= @@ -216,7 +256,7 @@ contract HealthCheckAccountant { if (totalRefunds > 0) { // Approve the vault to pull the underlying asset. - ERC20(asset).safeApprove(msg.sender, totalRefunds); + _checkAllowance(msg.sender, asset, totalRefunds); } } } @@ -235,7 +275,7 @@ contract HealthCheckAccountant { * @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 { + function addVault(address vault) external virtual onlyVaultOrFeeManagers { // Ensure the vault has not already been added. require(!vaults[vault], "already added"); @@ -248,17 +288,26 @@ contract HealthCheckAccountant { * @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 { + function removeVault( + address vault + ) external virtual onlyVaultOrFeeManagers { // Ensure the vault has been previously added. require(vaults[vault], "not added"); + address asset = IVault(vault).asset(); + // Remove any allowances left. + if (ERC20(asset).allowance(address(this), vault) != 0) { + ERC20(asset).safeApprove(vault, 0); + } + vaults[vault] = false; emit VaultChanged(vault, ChangeType.REMOVED); } /** - * @notice Function to update the default fee configuration used for all strategies. + * @notice Function to update the default fee configuration used for + all strategies that don't have a custom config set. * @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. @@ -273,7 +322,7 @@ contract HealthCheckAccountant { uint16 defaultMaxFee, uint16 defaultMaxGain, uint16 defaultMaxLoss - ) external onlyFeeManager { + ) external virtual onlyFeeManager { // Check for threshold and limit conditions. require( defaultManagement <= MANAGEMENT_FEE_THRESHOLD, @@ -283,8 +332,6 @@ contract HealthCheckAccountant { 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. @@ -320,7 +367,7 @@ contract HealthCheckAccountant { uint16 customMaxFee, uint16 customMaxGain, uint16 customMaxLoss - ) external onlyFeeManager { + ) external virtual onlyFeeManager { // Ensure the vault has been added. require(vaults[vault], "vault not added"); // Check for threshold and limit conditions. @@ -332,8 +379,6 @@ contract HealthCheckAccountant { 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. @@ -347,7 +392,7 @@ contract HealthCheckAccountant { }); // Set the custom flag. - _custom[vault][strategy] = 1; + _useCustomConfig[vault][strategy] = 1; emit UpdateCustomFeeConfig( vault, @@ -364,15 +409,15 @@ contract HealthCheckAccountant { function removeCustomConfig( address vault, address strategy - ) external onlyFeeManager { + ) external virtual onlyFeeManager { // Ensure custom fees are set for the specified vault and strategy. - require(_custom[vault][strategy] != 0, "No custom fees set"); + require(_useCustomConfig[vault][strategy] != 0, "No custom fees set"); // Set all the strategy's custom fees to 0. delete customConfig[vault][strategy]; // Clear the custom flag. - _custom[vault][strategy] = 0; + _useCustomConfig[vault][strategy] = 0; // Emit relevant event. emit RemovedCustomFeeConfig(vault, strategy); @@ -387,11 +432,11 @@ contract HealthCheckAccountant { * @param strategy Address of the strategy * @return If a custom fee config is set. */ - function custom( + function useCustomConfig( address vault, address strategy - ) external view returns (bool) { - return _custom[vault][strategy] != 0; + ) external view virtual returns (bool) { + return _useCustomConfig[vault][strategy] != 0; } /** @@ -402,7 +447,7 @@ contract HealthCheckAccountant { function withdrawUnderlying( address vault, uint256 amount - ) external onlyFeeManager { + ) external virtual onlyFeeManager { IVault(vault).withdraw(amount, address(this), address(this), maxLoss); } @@ -410,7 +455,7 @@ contract HealthCheckAccountant { * @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 { + function setMaxLoss(uint256 _maxLoss) external virtual onlyFeeManager { // Ensure that the provided `maxLoss` does not exceed 100% (in basis points). require(_maxLoss <= MAX_BPS, "higher than 100%"); @@ -424,7 +469,7 @@ contract HealthCheckAccountant { * @notice Function to distribute all accumulated fees to the designated recipient. * @param token The token to distribute. */ - function distribute(address token) external { + function distribute(address token) external virtual { distribute(token, ERC20(token).balanceOf(address(this))); } @@ -433,7 +478,10 @@ contract HealthCheckAccountant { * @param token The token to distribute. * @param amount amount of token to distribute. */ - function distribute(address token, uint256 amount) public onlyFeeManager { + function distribute( + address token, + uint256 amount + ) public virtual onlyFeeManager { ERC20(token).safeTransfer(feeRecipient, amount); emit DistributeRewards(token, amount); @@ -445,7 +493,7 @@ contract HealthCheckAccountant { */ function setFutureFeeManager( address _futureFeeManager - ) external onlyFeeManager { + ) external virtual onlyFeeManager { // Ensure the futureFeeManager is not a zero address. require(_futureFeeManager != address(0), "ZERO ADDRESS"); futureFeeManager = _futureFeeManager; @@ -457,7 +505,7 @@ contract HealthCheckAccountant { * @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 { + function acceptFeeManager() external virtual { // Make sure the sender is the future fee manager. require(msg.sender == futureFeeManager, "not future fee manager"); feeManager = futureFeeManager; @@ -466,11 +514,25 @@ contract HealthCheckAccountant { emit NewFeeManager(msg.sender); } + /** + * @notice Function to set a new vault manager. + * @param newVaultManager Address to add or remove vaults. + */ + function setVaultManager( + address newVaultManager + ) external virtual onlyFeeManager { + vaultManager = newVaultManager; + + emit UpdateVaultManager(newVaultManager); + } + /** * @notice Function to set a new address to receive distributed rewards. * @param newFeeRecipient Address to receive distributed fees. */ - function setFeeRecipient(address newFeeRecipient) external onlyFeeManager { + function setFeeRecipient( + address newFeeRecipient + ) external virtual onlyFeeManager { // Ensure the newFeeRecipient is not a zero address. require(newFeeRecipient != address(0), "ZERO ADDRESS"); address oldRecipient = feeRecipient; @@ -479,12 +541,31 @@ contract HealthCheckAccountant { emit UpdateFeeRecipient(oldRecipient, newFeeRecipient); } + /** + * @dev Internal safe function to make sure the contract you want to + * interact with has enough allowance to pull the desired tokens. + * + * @param _contract The address of the contract that will move the token. + * @param _token The ERC-20 token that will be getting spent. + * @param _amount The amount of `_token` to be spent. + */ + function _checkAllowance( + address _contract, + address _token, + uint256 _amount + ) internal { + if (ERC20(_token).allowance(address(this), _contract) < _amount) { + ERC20(_token).approve(_contract, 0); + ERC20(_token).approve(_contract, _amount); + } + } + /** * @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) { + function performanceFeeThreshold() external pure virtual returns (uint16) { return PERFORMANCE_FEE_THRESHOLD; } @@ -493,7 +574,7 @@ contract HealthCheckAccountant { * @dev This function provides the maximum management fee that the accountant can charge. * @return The maximum management fee. */ - function managementFeeThreshold() external view returns (uint16) { + function managementFeeThreshold() external pure virtual returns (uint16) { return MANAGEMENT_FEE_THRESHOLD; } } diff --git a/contracts/accountants/RefundAccountant.sol b/contracts/accountants/RefundAccountant.sol new file mode 100644 index 0000000..8eb5738 --- /dev/null +++ b/contracts/accountants/RefundAccountant.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity 0.8.18; + +import {HealthCheckAccountant, ERC20, SafeERC20, IVault} from "./HealthCheckAccountant.sol"; + +/// @title Refund Accountant +/// @dev Allows for configurable refunds to be given to specific strategies for a vault. +/// This can be used to auto compound reward into vault or provide retroactive refunds +/// from a previous loss. +contract RefundAccountant is HealthCheckAccountant { + using SafeERC20 for ERC20; + + /// @notice An event emitted when a refund is added for a strategy. + event UpdateRefund( + address indexed vault, + address indexed strategy, + bool refund, + uint256 amount + ); + + /// @notice Struct to hold refund info for a strategy. + struct Refund { + // If the accountant should refund on the report. + bool refund; + // The amount if any to refund. + uint248 amount; + } + + /// @notice Mapping of vault => strategy => struct if there is a reward refund to give. + mapping(address => mapping(address => Refund)) public refund; + + constructor( + address _feeManager, + address _feeRecipient, + uint16 defaultManagement, + uint16 defaultPerformance, + uint16 defaultRefund, + uint16 defaultMaxFee, + uint16 defaultMaxGain, + uint16 defaultMaxLoss + ) + HealthCheckAccountant( + _feeManager, + _feeRecipient, + defaultManagement, + defaultPerformance, + defaultRefund, + defaultMaxFee, + defaultMaxGain, + defaultMaxLoss + ) + {} + + /** + * @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 + ) + public + virtual + override + returns (uint256 totalFees, uint256 totalRefunds) + { + (totalFees, totalRefunds) = super.report(strategy, gain, loss); + + Refund memory refundConfig = refund[msg.sender][strategy]; + // Check if the strategy is being given a refund. + if (refundConfig.refund) { + // Add it to the existing refunds. + totalRefunds += uint256(refundConfig.amount); + + // Make sure the vault is approved correctly. + _checkAllowance( + msg.sender, + IVault(msg.sender).asset(), + totalRefunds + ); + + // Always reset the refund amount so it can't be reused. + delete refund[msg.sender][strategy]; + } + } + + /** + * @notice Set a strategy to use to refund a reward amount for + * aut compounding reward tokens. + * + * @param _vault Address of the vault to refund. + * @param _strategy Address of the strategy to refund during the report. + * @param _refund Bool to turn it on or off. + * @param _amount Amount to refund per report. + */ + function setRefund( + address _vault, + address _strategy, + bool _refund, + uint256 _amount + ) external virtual onlyFeeManager { + require(vaults[_vault], "not added"); + require( + IVault(_vault).strategies(_strategy).activation != 0, + "!active" + ); + require(_refund || _amount == 0, "no refund and non zero amount"); + + refund[_vault][_strategy] = Refund({ + refund: _refund, + amount: uint248(_amount) + }); + + emit UpdateRefund(_vault, _strategy, _refund, uint256(_amount)); + } +} diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index db56955..653b2fe 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -300,8 +300,6 @@ contract GenericDebtAllocator is Governance { uint256 _targetRatio, uint256 _maxRatio ) external virtual onlyGovernance { - // Make sure the strategy is added to the vault. - require(IVault(vault).strategies(_strategy).activation != 0, "!active"); // Make sure a minimumChange has been set. require(minimumChange != 0, "!minimum"); // Cannot be more than 100%. diff --git a/scripts/deploy_accountant.py b/scripts/deploy_accountant.py index cf45663..5de8297 100644 --- a/scripts/deploy_accountant.py +++ b/scripts/deploy_accountant.py @@ -34,12 +34,11 @@ def deploy_accountant(): print(f"Salt we are using {salt}") print("Init balance:", deployer.balance / 1e18) - if ( - input( - "Would you like to deploy a Generic Accountant or a HealthCheck Accountant? g/h " - ).lower() - == "g" - ): + version = input( + "Would you like to deploy a Generic Accountant, HealthCheck Accountant or a Refund Accountant? g/h/r " + ).lower() + + if version == "g": print("Deploying a Generic accountant.") print("Enter the default amounts to use in Base Points. (100% == 10_000)") @@ -65,10 +64,15 @@ def deploy_accountant(): ) else: - print("Deploying a HealthCheck accountant.") - print("Enter the default amounts to use in Base Points. (100% == 10_000)") + if version == "h": + print("Deploying a HealthCheck accountant.") + accountant = project.HealthCheckAccountant - accountant = project.HealthCheckAccountant + else: + print("Deploying a Refund accountant.") + accountant = project.RefundAccountant + + print("Enter the default amounts to use in Base Points. (100% == 10_000)") management_fee = input("Default management fee? ") assert int(management_fee) <= 200 @@ -112,9 +116,10 @@ def deploy_accountant(): address = event[0].addr + print("------------------") print(f"Deployed the Accountant to {address}") print("------------------") - print(f"Encoded Constructor to use for verifaction {constructor.hex()}") + print(f"Encoded Constructor to use for verifaction {constructor.hex()[2:]}") def main(): diff --git a/scripts/deploy_address_provider.py b/scripts/deploy_address_provider.py index d29c501..e32e6bb 100644 --- a/scripts/deploy_address_provider.py +++ b/scripts/deploy_address_provider.py @@ -52,9 +52,10 @@ def deploy_address_provider(): address = event[0].addr + print("------------------") print(f"Deployed the address provider to {address}") print("------------------") - print(f"Encoded Constructor to use for verifaction {constructor.hex()}") + print(f"Encoded Constructor to use for verifaction {constructor.hex()[2:]}") def main(): diff --git a/scripts/deploy_allocator_factory.py b/scripts/deploy_allocator_factory.py index 7db744a..80d958e 100644 --- a/scripts/deploy_allocator_factory.py +++ b/scripts/deploy_allocator_factory.py @@ -47,7 +47,9 @@ def deploy_allocator_factory(): address = event[0].addr + print("------------------") print(f"Deployed the Factory to {address}") + print("------------------") def main(): diff --git a/scripts/deploy_registry.py b/scripts/deploy_registry.py index a2b856c..14b0993 100644 --- a/scripts/deploy_registry.py +++ b/scripts/deploy_registry.py @@ -80,9 +80,10 @@ def deploy_release_and_factory(): deployed_factory = factory.at(factory_event[0].addr) + print("------------------") print(f"Deployed Registry Factory to {deployed_factory.address}") print("------------------") - print(f"Encoded Constructor to use for verifaction {factory_constructor.hex()}") + print(f"Encoded Constructor to use for verifaction {factory_constructor.hex()[2:]}") def main(): diff --git a/tests/test_generic_accountant.py b/tests/accountants/test_generic_accountant.py similarity index 100% rename from tests/test_generic_accountant.py rename to tests/accountants/test_generic_accountant.py diff --git a/tests/test_healthcheck_accountant.py b/tests/accountants/test_healthcheck_accountant.py similarity index 81% rename from tests/test_healthcheck_accountant.py rename to tests/accountants/test_healthcheck_accountant.py index d6d951c..2d324ea 100644 --- a/tests/test_healthcheck_accountant.py +++ b/tests/accountants/test_healthcheck_accountant.py @@ -15,7 +15,7 @@ def test_setup(daddy, vault, strategy, healthcheck_accountant, fee_recipient): assert accountant.defaultConfig().maxGain == 10_000 assert accountant.defaultConfig().maxLoss == 0 assert accountant.vaults(vault.address) == False - assert accountant.custom(vault.address, strategy.address) == False + assert accountant.useCustomConfig(vault.address, strategy.address) == False assert accountant.customConfig(vault.address, strategy.address).managementFee == 0 assert accountant.customConfig(vault.address, strategy.address).performanceFee == 0 assert accountant.customConfig(vault.address, strategy.address).refundRatio == 0 @@ -27,6 +27,7 @@ def test_setup(daddy, vault, strategy, healthcheck_accountant, fee_recipient): def test_add_vault( daddy, vault, + user, strategy, healthcheck_accountant, amount, @@ -59,9 +60,12 @@ def test_add_vault( deposit_into_vault(vault, amount) provide_strategy_with_debt(daddy, strategy, vault, amount) - with ape.reverts("!authorized"): + with ape.reverts("vault not added"): accountant.report(strategy, 0, 0, sender=vault) + with ape.reverts("!vault manager"): + accountant.addVault(vault.address, sender=user) + # set vault in accountant tx = accountant.addVault(vault.address, sender=daddy) @@ -82,7 +86,80 @@ def test_add_vault( def test_remove_vault( daddy, vault, + user, + strategy, + healthcheck_accountant, + amount, + deposit_into_vault, + provide_strategy_with_debt, +): + accountant = healthcheck_accountant + assert accountant.vaults(vault.address) == False + + new_management = 0 + new_performance = 1_000 + new_refund = 0 + new_max_fee = 0 + new_max_gain = 10_000 + new_max_loss = 0 + + tx = accountant.updateDefaultConfig( + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + sender=daddy, + ) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert accountant.vaults(vault.address) == False + + # set vault in accountant + tx = accountant.addVault(vault.address, sender=daddy) + + event = list(tx.decode_logs(accountant.VaultChanged)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].change == ChangeType.ADDED + assert accountant.vaults(vault.address) == True + + # Should work + tx = accountant.report(strategy, 1_000, 0, sender=vault) + fees, refunds = tx.return_value + assert fees == 100 + assert refunds == 0 + + with ape.reverts("!vault manager"): + accountant.removeVault(vault.address, sender=user) + + tx = accountant.removeVault(vault.address, sender=daddy) + + event = list(tx.decode_logs(accountant.VaultChanged)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].change == ChangeType.REMOVED + assert accountant.vaults(vault.address) == False + + # Should now not be able to report. + with ape.reverts("vault not added"): + accountant.report(strategy, 0, 0, sender=vault) + + +def test_remove_vault__non_zero_allomance( + daddy, + vault, + user, strategy, + asset, healthcheck_accountant, amount, deposit_into_vault, @@ -132,8 +209,16 @@ def test_remove_vault( assert fees == 100 assert refunds == 0 + asset.approve(vault, 19, sender=accountant) + assert asset.allowance(accountant, vault) != 0 + + with ape.reverts("!vault manager"): + accountant.removeVault(vault.address, sender=user) + tx = accountant.removeVault(vault.address, sender=daddy) + assert asset.allowance(accountant, vault) == 0 + event = list(tx.decode_logs(accountant.VaultChanged)) assert len(event) == 1 @@ -142,7 +227,155 @@ def test_remove_vault( assert accountant.vaults(vault.address) == False # Should now not be able to report. - with ape.reverts("!authorized"): + with ape.reverts("vault not added"): + accountant.report(strategy, 0, 0, sender=vault) + + +def test_add_vault__vault_manager( + daddy, + vault_manager, + vault, + user, + strategy, + healthcheck_accountant, + amount, + deposit_into_vault, + provide_strategy_with_debt, +): + accountant = healthcheck_accountant + assert accountant.vaults(vault.address) == False + + with ape.reverts("!fee manager"): + accountant.setVaultManager(vault_manager, sender=user) + + tx = accountant.setVaultManager(vault_manager, sender=daddy) + + event = list(tx.decode_logs(accountant.UpdateVaultManager))[0] + assert event.newVaultManager == vault_manager.address + + new_management = 0 + new_performance = 1_000 + new_refund = 0 + new_max_fee = 0 + new_max_gain = 10_000 + new_max_loss = 0 + + tx = accountant.updateDefaultConfig( + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + sender=daddy, + ) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + with ape.reverts("vault not added"): + accountant.report(strategy, 0, 0, sender=vault) + + with ape.reverts("!vault manager"): + accountant.addVault(vault.address, sender=user) + + # set vault in accountant + tx = accountant.addVault(vault.address, sender=vault_manager) + + event = list(tx.decode_logs(accountant.VaultChanged)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].change == ChangeType.ADDED + assert accountant.vaults(vault.address) == True + + # Should work now + tx = accountant.report(strategy, 1_000, 0, sender=vault) + fees, refunds = tx.return_value + assert fees == 100 + assert refunds == 0 + + +def test_remove_vault__vault_manager( + daddy, + vault_manager, + vault, + user, + strategy, + healthcheck_accountant, + amount, + deposit_into_vault, + provide_strategy_with_debt, +): + accountant = healthcheck_accountant + assert accountant.vaults(vault.address) == False + + with ape.reverts("!fee manager"): + accountant.setVaultManager(vault_manager, sender=user) + + tx = accountant.setVaultManager(vault_manager, sender=daddy) + + event = list(tx.decode_logs(accountant.UpdateVaultManager))[0] + assert event.newVaultManager == vault_manager.address + + new_management = 0 + new_performance = 1_000 + new_refund = 0 + new_max_fee = 0 + new_max_gain = 10_000 + new_max_loss = 0 + + tx = accountant.updateDefaultConfig( + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + sender=daddy, + ) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert accountant.vaults(vault.address) == False + + with ape.reverts("!vault manager"): + accountant.removeVault(vault.address, sender=user) + + # set vault in accountant + tx = accountant.addVault(vault.address, sender=vault_manager) + + event = list(tx.decode_logs(accountant.VaultChanged)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].change == ChangeType.ADDED + assert accountant.vaults(vault.address) == True + + # Should work + tx = accountant.report(strategy, 1_000, 0, sender=vault) + fees, refunds = tx.return_value + assert fees == 100 + assert refunds == 0 + + tx = accountant.removeVault(vault.address, sender=vault_manager) + + event = list(tx.decode_logs(accountant.VaultChanged)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].change == ChangeType.REMOVED + assert accountant.vaults(vault.address) == False + + # Should now not be able to report. + with ape.reverts("vault not added"): accountant.report(strategy, 0, 0, sender=vault) @@ -286,7 +519,7 @@ def test_remove_custom_config(daddy, vault, strategy, healthcheck_accountant): sender=daddy, ) - assert accountant.custom(vault.address, strategy.address) == True + assert accountant.useCustomConfig(vault.address, strategy.address) == True assert ( accountant.customConfig(vault.address, strategy.address) != accountant.defaultConfig() @@ -862,9 +1095,9 @@ def test_report_profit__custom_zero_max_gain__reverts( ): accountant = healthcheck_accountant accountant.addVault(vault.address, sender=daddy) - # SEt max gain to 0% + # SEt max gain to 1% accountant.setCustomConfig( - vault.address, strategy.address, 200, 2_000, 0, 100, 0, 0, sender=daddy + vault.address, strategy.address, 200, 2_000, 0, 100, 1, 0, sender=daddy ) config = list(accountant.customConfig(vault.address, strategy.address)) diff --git a/tests/accountants/test_refund_accountant.py b/tests/accountants/test_refund_accountant.py new file mode 100644 index 0000000..8bccef7 --- /dev/null +++ b/tests/accountants/test_refund_accountant.py @@ -0,0 +1,1310 @@ +import ape +from ape import chain +from utils.constants import ChangeType, ZERO_ADDRESS, MAX_BPS, MAX_INT + + +def test_setup(daddy, vault, strategy, refund_accountant, fee_recipient): + accountant = refund_accountant + assert accountant.feeManager() == daddy + assert accountant.futureFeeManager() == ZERO_ADDRESS + assert accountant.feeRecipient() == fee_recipient + assert accountant.defaultConfig().managementFee == 100 + assert accountant.defaultConfig().performanceFee == 1_000 + assert accountant.defaultConfig().refundRatio == 0 + assert accountant.defaultConfig().maxFee == 0 + assert accountant.defaultConfig().maxGain == 10_000 + assert accountant.defaultConfig().maxLoss == 0 + assert accountant.vaults(vault.address) == False + assert accountant.useCustomConfig(vault.address, strategy.address) == False + assert accountant.customConfig(vault.address, strategy.address).managementFee == 0 + assert accountant.customConfig(vault.address, strategy.address).performanceFee == 0 + assert accountant.customConfig(vault.address, strategy.address).refundRatio == 0 + assert accountant.customConfig(vault.address, strategy.address).maxFee == 0 + assert accountant.customConfig(vault.address, strategy.address).maxGain == 0 + assert accountant.customConfig(vault.address, strategy.address).maxLoss == 0 + assert accountant.refund(vault.address, strategy.address) == (False, 0) + + +def test_add_reward_refund(daddy, vault, strategy, refund_accountant): + accountant = refund_accountant + assert accountant.refund(vault.address, strategy.address) == (False, 0) + + amount = int(100) + + with ape.reverts("not added"): + accountant.setRefund(vault, strategy, True, amount, sender=daddy) + + accountant.addVault(vault, sender=daddy) + + with ape.reverts("!active"): + accountant.setRefund(vault, strategy, True, amount, sender=daddy) + + vault.add_strategy(strategy, sender=daddy) + + tx = accountant.setRefund(vault, strategy, True, amount, sender=daddy) + + event = list(tx.decode_logs(accountant.UpdateRefund)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].strategy == strategy.address + assert event[0].refund == True + assert event[0].amount == amount + assert accountant.refund(vault.address, strategy.address) == (True, 100) + + with ape.reverts("no refund and non zero amount"): + accountant.setRefund(vault, strategy, False, amount, sender=daddy) + + tx = accountant.setRefund(vault, strategy, False, 0, sender=daddy) + + event = list(tx.decode_logs(accountant.UpdateRefund)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].strategy == strategy.address + assert event[0].refund == False + assert event[0].amount == 0 + assert accountant.refund(vault.address, strategy.address) == (False, 0) + + +def test_reward_refund( + daddy, vault, strategy, refund_accountant, user, asset, deposit_into_vault +): + accountant = refund_accountant + assert accountant.refund(vault.address, strategy.address) == (False, 0) + + amount = int(100) + accountant.addVault(vault, sender=daddy) + vault.add_strategy(strategy, sender=daddy) + + tx = accountant.setRefund(vault, strategy, True, amount, sender=daddy) + + event = list(tx.decode_logs(accountant.UpdateRefund)) + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].strategy == strategy.address + assert event[0].refund == True + assert event[0].amount == amount + assert accountant.refund(vault.address, strategy.address) == (True, 100) + + vault.set_accountant(accountant, sender=daddy) + + user_balance = asset.balanceOf(user) + to_deposit = user_balance // 2 + + # Deposit into vault + deposit_into_vault(vault, to_deposit) + + # Fund the accountant for a refund. Over fund to make sure it only sends amount. + asset.transfer(accountant, user_balance - to_deposit, sender=user) + + assert vault.totalAssets() == to_deposit + assert vault.totalIdle() == to_deposit + assert vault.profitUnlockingRate() == 0 + assert vault.fullProfitUnlockDate() == 0 + + tx = vault.process_report(strategy, sender=daddy) + + event = list(tx.decode_logs(vault.StrategyReported))[0] + + assert event.strategy == strategy.address + assert event.total_fees == 0 + assert event.gain == 0 + assert event.loss == 0 + assert event.total_refunds == amount + assert event.current_debt == 0 + + assert vault.totalAssets() == amount + to_deposit + assert vault.totalIdle() == amount + to_deposit + assert vault.profitUnlockingRate() > 0 + assert vault.fullProfitUnlockDate() > 0 + + # Make sure the amounts got reset. + assert accountant.refund(vault.address, strategy.address) == (False, 0) + tx = accountant.report(strategy, 0, 0, sender=vault) + assert tx.return_value == (0, 0) + + +def test_reward_refund__with_gain( + daddy, + vault, + strategy, + refund_accountant, + user, + asset, + deposit_into_vault, + provide_strategy_with_debt, +): + accountant = refund_accountant + # Set performance fee to 10% and 0 management fee + accountant.updateDefaultConfig(0, 1_000, 0, 10_000, 10_000, 0, sender=daddy) + assert accountant.refund(vault.address, strategy.address) == (False, 0) + + accountant.addVault(vault, sender=daddy) + vault.add_strategy(strategy, sender=daddy) + + user_balance = asset.balanceOf(user) + to_deposit = user_balance // 2 + + refund = to_deposit // 10 + gain = to_deposit // 10 + loss = 0 + + tx = accountant.setRefund(vault, strategy, True, refund, sender=daddy) + + event = list(tx.decode_logs(accountant.UpdateRefund)) + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].strategy == strategy.address + assert event[0].refund == True + assert event[0].amount == refund + assert accountant.refund(vault.address, strategy.address) == (True, refund) + + vault.set_accountant(accountant, sender=daddy) + + # Deposit into vault + deposit_into_vault(vault, to_deposit) + # Give strategy debt. + provide_strategy_with_debt(daddy, strategy, vault, int(to_deposit)) + # simulate profit. + asset.transfer(strategy, gain, sender=user) + + # Fund the accountant for a refund. Over fund to make sure it only sends amount. + asset.transfer(accountant, refund, sender=user) + + assert vault.totalAssets() == to_deposit + assert vault.totalIdle() == 0 + assert vault.totalDebt() == to_deposit + assert vault.profitUnlockingRate() == 0 + assert vault.fullProfitUnlockDate() == 0 + + tx = vault.process_report(strategy, sender=daddy) + + event = list(tx.decode_logs(vault.StrategyReported))[0] + + assert event.strategy == strategy.address + assert event.total_fees == gain // 10 + assert event.gain == gain + assert event.loss == 0 + assert event.total_refunds == refund + assert event.current_debt == to_deposit + gain + + assert vault.totalAssets() == refund + to_deposit + gain + assert vault.totalIdle() == refund + assert vault.profitUnlockingRate() > 0 + assert vault.fullProfitUnlockDate() > 0 + + # Make sure the amounts got reset. + assert accountant.refund(vault.address, strategy.address) == (False, 0) + tx = accountant.report(strategy, 0, 0, sender=vault) + assert tx.return_value == (0, 0) + + +def test_reward_refund__with_loss__and_refund( + daddy, + vault, + strategy, + refund_accountant, + user, + asset, + deposit_into_vault, + provide_strategy_with_debt, +): + accountant = refund_accountant + # Set refund ratio to 100% + accountant.updateDefaultConfig( + 0, 1_000, 10_000, 10_000, 10_000, 10_000, sender=daddy + ) + assert accountant.refund(vault.address, strategy.address) == (False, 0) + + accountant.addVault(vault, sender=daddy) + vault.add_strategy(strategy, sender=daddy) + + user_balance = asset.balanceOf(user) + to_deposit = user_balance // 2 + + refund = to_deposit // 10 + gain = 0 + loss = to_deposit // 10 + + tx = accountant.setRefund(vault, strategy, True, refund, sender=daddy) + + event = list(tx.decode_logs(accountant.UpdateRefund)) + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].strategy == strategy.address + assert event[0].refund == True + assert event[0].amount == refund + assert accountant.refund(vault.address, strategy.address) == (True, refund) + + vault.set_accountant(accountant, sender=daddy) + + # Deposit into vault + deposit_into_vault(vault, to_deposit) + # Give strategy debt. + provide_strategy_with_debt(daddy, strategy, vault, int(to_deposit)) + # simulate loss. + asset.transfer(user, loss, sender=strategy) + + # Fund the accountant for a refund. Over fund to make sure it only sends amount. + asset.transfer(accountant, refund + loss, sender=user) + + assert vault.totalAssets() == to_deposit + assert vault.totalIdle() == 0 + assert vault.totalDebt() == to_deposit + assert vault.profitUnlockingRate() == 0 + assert vault.fullProfitUnlockDate() == 0 + + tx = vault.process_report(strategy, sender=daddy) + + event = list(tx.decode_logs(vault.StrategyReported))[0] + + assert event.strategy == strategy.address + assert event.total_fees == 0 + assert event.gain == gain + assert event.loss == loss + assert event.total_refunds == refund + loss + assert event.current_debt == to_deposit - loss + + assert vault.totalAssets() == refund + to_deposit + assert vault.totalIdle() == refund + loss + assert vault.profitUnlockingRate() > 0 + assert vault.fullProfitUnlockDate() > 0 + + # Make sure the amounts got reset. + assert accountant.refund(vault.address, strategy.address) == (False, 0) + tx = accountant.report(strategy, 0, 0, sender=vault) + assert tx.return_value == (0, 0) + + +def test_add_vault( + daddy, + vault, + strategy, + refund_accountant, + amount, + deposit_into_vault, + provide_strategy_with_debt, +): + accountant = refund_accountant + assert accountant.vaults(vault.address) == False + + new_management = 0 + new_performance = 1_000 + new_refund = 0 + new_max_fee = 0 + new_max_gain = 10_000 + new_max_loss = 0 + + tx = accountant.updateDefaultConfig( + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + sender=daddy, + ) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + with ape.reverts("vault not added"): + accountant.report(strategy, 0, 0, sender=vault) + + # set vault in accountant + tx = accountant.addVault(vault.address, sender=daddy) + + event = list(tx.decode_logs(accountant.VaultChanged)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].change == ChangeType.ADDED + assert accountant.vaults(vault.address) == True + + # Should work now + tx = accountant.report(strategy, 1_000, 0, sender=vault) + fees, refunds = tx.return_value + assert fees == 100 + assert refunds == 0 + + +def test_remove_vault( + daddy, + vault, + strategy, + refund_accountant, + amount, + deposit_into_vault, + provide_strategy_with_debt, +): + accountant = refund_accountant + assert accountant.vaults(vault.address) == False + + new_management = 0 + new_performance = 1_000 + new_refund = 0 + new_max_fee = 0 + new_max_gain = 10_000 + new_max_loss = 0 + + tx = accountant.updateDefaultConfig( + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + sender=daddy, + ) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert accountant.vaults(vault.address) == False + + # set vault in accountant + tx = accountant.addVault(vault.address, sender=daddy) + + event = list(tx.decode_logs(accountant.VaultChanged)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].change == ChangeType.ADDED + assert accountant.vaults(vault.address) == True + + # Should work + tx = accountant.report(strategy, 1_000, 0, sender=vault) + fees, refunds = tx.return_value + assert fees == 100 + assert refunds == 0 + + tx = accountant.removeVault(vault.address, sender=daddy) + + event = list(tx.decode_logs(accountant.VaultChanged)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].change == ChangeType.REMOVED + assert accountant.vaults(vault.address) == False + + # Should now not be able to report. + with ape.reverts("vault not added"): + accountant.report(strategy, 0, 0, sender=vault) + + +def test_set_default_config(daddy, vault, strategy, refund_accountant): + accountant = refund_accountant + assert accountant.defaultConfig().managementFee == 100 + assert accountant.defaultConfig().performanceFee == 1_000 + assert accountant.defaultConfig().refundRatio == 0 + assert accountant.defaultConfig().maxFee == 0 + assert accountant.defaultConfig().maxGain == 10_000 + assert accountant.defaultConfig().maxLoss == 0 + + new_management = 20 + new_performance = 2_000 + new_refund = 13 + new_max_fee = 18 + new_max_gain = 19 + new_max_loss = 27 + + tx = accountant.updateDefaultConfig( + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + sender=daddy, + ) + + event = list(tx.decode_logs(accountant.UpdateDefaultFeeConfig)) + + assert len(event) == 1 + config = list(event[0].defaultFeeConfig) + assert config[0] == new_management + assert config[1] == new_performance + assert config[2] == new_refund + assert config[3] == new_max_fee + assert config[4] == new_max_gain + assert config[5] == new_max_loss + + assert accountant.defaultConfig().managementFee == new_management + assert accountant.defaultConfig().performanceFee == new_performance + assert accountant.defaultConfig().refundRatio == new_refund + assert accountant.defaultConfig().maxFee == new_max_fee + assert accountant.defaultConfig().maxGain == new_max_gain + assert accountant.defaultConfig().maxLoss == new_max_loss + + +def test_set_custom_config(daddy, vault, strategy, refund_accountant): + accountant = refund_accountant + accountant.addVault(vault.address, sender=daddy) + + assert accountant.customConfig(vault.address, strategy.address) == ( + 0, + 0, + 0, + 0, + 0, + 0, + ) + + new_management = 20 + new_performance = 2_000 + new_refund = 13 + new_max_fee = 18 + new_max_gain = 19 + new_max_loss = 27 + + tx = accountant.setCustomConfig( + vault.address, + strategy.address, + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + sender=daddy, + ) + + event = list(tx.decode_logs(accountant.UpdateCustomFeeConfig)) + + assert len(event) == 1 + assert event[0].vault == vault.address + assert event[0].strategy == strategy.address + config = list(event[0].custom_config) + + assert config[0] == new_management + assert config[1] == new_performance + assert config[2] == new_refund + assert config[3] == new_max_fee + assert config[4] == new_max_gain + assert config[5] == new_max_loss + + assert ( + accountant.customConfig(vault.address, strategy.address) + != accountant.defaultConfig() + ) + assert accountant.customConfig(vault.address, strategy.address) == ( + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + ) + + +def test_remove_custom_config(daddy, vault, strategy, refund_accountant): + accountant = refund_accountant + accountant.addVault(vault.address, sender=daddy) + + assert accountant.customConfig(vault.address, strategy.address) == ( + 0, + 0, + 0, + 0, + 0, + 0, + ) + + with ape.reverts("No custom fees set"): + accountant.removeCustomConfig(vault.address, strategy.address, sender=daddy) + + new_management = 20 + new_performance = 2_000 + new_refund = 13 + new_max_fee = 18 + new_max_gain = 19 + new_max_loss = 27 + + accountant.setCustomConfig( + vault.address, + strategy.address, + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + sender=daddy, + ) + + assert accountant.useCustomConfig(vault.address, strategy.address) == True + assert ( + accountant.customConfig(vault.address, strategy.address) + != accountant.defaultConfig() + ) + assert accountant.customConfig(vault.address, strategy.address) == ( + new_management, + new_performance, + new_refund, + new_max_fee, + new_max_gain, + new_max_loss, + ) + + tx = accountant.removeCustomConfig(vault.address, strategy.address, sender=daddy) + + event = list(tx.decode_logs(accountant.RemovedCustomFeeConfig)) + + assert event[0].strategy == strategy.address + assert event[0].vault == vault.address + assert len(event) == 1 + + assert accountant.customConfig(vault.address, strategy.address) == ( + 0, + 0, + 0, + 0, + 0, + 0, + ) + + +def test_set_fee_manager(refund_accountant, daddy, user): + accountant = refund_accountant + assert accountant.feeManager() == daddy + assert accountant.futureFeeManager() == ZERO_ADDRESS + + with ape.reverts("!fee manager"): + accountant.setFutureFeeManager(user, sender=user) + + with ape.reverts("not future fee manager"): + accountant.acceptFeeManager(sender=user) + + with ape.reverts("not future fee manager"): + accountant.acceptFeeManager(sender=daddy) + + with ape.reverts("ZERO ADDRESS"): + accountant.setFutureFeeManager(ZERO_ADDRESS, sender=daddy) + + tx = accountant.setFutureFeeManager(user, sender=daddy) + + event = list(tx.decode_logs(accountant.SetFutureFeeManager)) + + assert len(event) == 1 + assert event[0].futureFeeManager == user.address + + assert accountant.feeManager() == daddy + assert accountant.futureFeeManager() == user + + with ape.reverts("not future fee manager"): + accountant.acceptFeeManager(sender=daddy) + + tx = accountant.acceptFeeManager(sender=user) + + event = list(tx.decode_logs(accountant.NewFeeManager)) + + assert len(event) == 1 + assert event[0].feeManager == user.address + + assert accountant.feeManager() == user + assert accountant.futureFeeManager() == ZERO_ADDRESS + + +def test_set_fee_recipient(refund_accountant, daddy, user, fee_recipient): + accountant = refund_accountant + assert accountant.feeManager() == daddy + assert accountant.feeRecipient() == fee_recipient + + with ape.reverts("!fee manager"): + accountant.setFeeRecipient(user, sender=user) + + with ape.reverts("!fee manager"): + accountant.setFeeRecipient(user, sender=fee_recipient) + + with ape.reverts("ZERO ADDRESS"): + accountant.setFeeRecipient(ZERO_ADDRESS, sender=daddy) + + tx = accountant.setFeeRecipient(user, sender=daddy) + + event = list(tx.decode_logs(accountant.UpdateFeeRecipient)) + + assert len(event) == 1 + assert event[0].oldFeeRecipient == fee_recipient.address + assert event[0].newFeeRecipient == user.address + + assert accountant.feeRecipient() == user + + +def test_distribute( + refund_accountant, + daddy, + user, + vault, + fee_recipient, + deposit_into_vault, + amount, +): + accountant = refund_accountant + deposit_into_vault(vault, amount) + + assert vault.balanceOf(user) == amount + assert vault.balanceOf(accountant.address) == 0 + assert vault.balanceOf(daddy.address) == 0 + assert vault.balanceOf(fee_recipient.address) == 0 + + vault.transfer(accountant.address, amount, sender=user) + + assert vault.balanceOf(user) == 0 + assert vault.balanceOf(accountant.address) == amount + assert vault.balanceOf(daddy.address) == 0 + assert vault.balanceOf(fee_recipient.address) == 0 + + with ape.reverts("!fee manager"): + accountant.distribute(vault.address, sender=user) + + tx = accountant.distribute(vault.address, sender=daddy) + + event = list(tx.decode_logs(accountant.DistributeRewards)) + + assert len(event) == 1 + assert event[0].token == vault.address + assert event[0].rewards == amount + + assert vault.balanceOf(user) == 0 + assert vault.balanceOf(accountant.address) == 0 + assert vault.balanceOf(daddy.address) == 0 + assert vault.balanceOf(fee_recipient.address) == amount + + +def test_withdraw_underlying( + refund_accountant, daddy, user, vault, asset, deposit_into_vault, amount +): + accountant = refund_accountant + deposit_into_vault(vault, amount) + + assert vault.balanceOf(user) == amount + assert vault.balanceOf(accountant.address) == 0 + assert asset.balanceOf(accountant.address) == 0 + + vault.transfer(accountant.address, amount, sender=user) + + assert vault.balanceOf(user) == 0 + assert vault.balanceOf(accountant.address) == amount + assert asset.balanceOf(accountant.address) == 0 + + with ape.reverts("!fee manager"): + accountant.withdrawUnderlying(vault.address, amount, sender=user) + + tx = accountant.withdrawUnderlying(vault.address, amount, sender=daddy) + + assert vault.balanceOf(user) == 0 + assert vault.balanceOf(accountant.address) == 0 + assert asset.balanceOf(accountant.address) == amount + + +def test_report_profit( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + config = list(accountant.defaultConfig()) + + accountant.addVault(vault.address, sender=daddy) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + gain = amount // 10 + loss = 0 + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Management fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + assert expected_management_fees + expected_performance_fees == fees + assert refunds == 0 + + +def test_report_no_profit( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + config = list(accountant.defaultConfig()) + + accountant.addVault(vault.address, sender=daddy) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + gain = 0 + loss = 0 + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Management fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + assert expected_management_fees + expected_performance_fees == fees + assert refunds == 0 + + +def test_report_max_fee( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + # SEt max fee of 10% of gain + accountant.updateDefaultConfig(100, 1_000, 0, 100, 10_000, 0, sender=daddy) + config = list(accountant.defaultConfig()) + + accountant.addVault(vault.address, sender=daddy) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + gain = amount // 10 + loss = 0 + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Management fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + # The real fees charged should be less than what would be expected + assert expected_management_fees + expected_performance_fees > fees + assert fees == gain * config[3] / MAX_BPS + assert refunds == 0 + + +def test_report_refund( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + # SEt refund ratio to 100% + accountant.updateDefaultConfig(100, 1_000, 10_000, 0, 10_000, 10_000, sender=daddy) + config = list(accountant.defaultConfig()) + + accountant.addVault(vault.address, sender=daddy) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + gain = 0 + loss = amount // 10 + + # make sure accountant has the funds + asset.mint(accountant.address, loss, sender=daddy) + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Management fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + expected_refunds = loss * config[2] / MAX_BPS + + # The real fees charged should be less than what would be expected + assert expected_management_fees + expected_performance_fees == fees + assert expected_refunds == refunds + assert asset.allowance(accountant.address, vault.address) == expected_refunds + + +def test_report_refund_not_enough_asset( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + # SEt refund ratio to 100% + accountant.updateDefaultConfig(100, 1_000, 10_000, 0, 10_000, 10_000, sender=daddy) + config = list(accountant.defaultConfig()) + + accountant.addVault(vault.address, sender=daddy) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + gain = 0 + loss = amount // 10 + + # make sure accountant has the funds + asset.mint(accountant.address, loss // 2, sender=daddy) + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Management fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + expected_refunds = loss // 2 + + # The real fees charged should be less than what would be expected + assert expected_management_fees + expected_performance_fees == fees + assert expected_refunds == refunds + assert asset.allowance(accountant.address, vault.address) == expected_refunds + + +def test_report_profit__custom_config( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + accountant.addVault(vault.address, sender=daddy) + accountant.setCustomConfig( + vault.address, strategy.address, 200, 2_000, 0, 0, 10_000, 0, sender=daddy + ) + config = list(accountant.customConfig(vault.address, strategy.address)) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + gain = amount // 10 + loss = 0 + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Management fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + assert expected_management_fees + expected_performance_fees == fees + + +def test_report_no_profit__custom_config( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + accountant.addVault(vault.address, sender=daddy) + accountant.setCustomConfig( + vault.address, strategy.address, 200, 2_000, 0, 0, 10_000, 0, sender=daddy + ) + config = list(accountant.customConfig(vault.address, strategy.address)) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + gain = 0 + loss = 0 + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Managmeent fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + assert expected_management_fees + expected_performance_fees == fees + assert refunds == 0 + + +def test_report_max_fee__custom_config( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + accountant.addVault(vault.address, sender=daddy) + # SEt max fee of 10% of gain + accountant.setCustomConfig( + vault.address, strategy.address, 200, 2_000, 0, 100, 10_000, 0, sender=daddy + ) + config = list(accountant.customConfig(vault.address, strategy.address)) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + gain = amount // 10 + loss = 0 + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Management fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + # The real fees charged should be less than what would be expected + assert expected_management_fees + expected_performance_fees > fees + assert fees == gain * config[3] / MAX_BPS + assert refunds == 0 + + +def test_report_profit__custom_zero_max_gain__reverts( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + accountant.addVault(vault.address, sender=daddy) + # SEt max gain to 1% + accountant.setCustomConfig( + vault.address, strategy.address, 200, 2_000, 0, 100, 1, 0, sender=daddy + ) + config = list(accountant.customConfig(vault.address, strategy.address)) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + gain = amount // 10 + loss = 0 + + with ape.reverts("too much gain"): + accountant.report(strategy.address, gain, loss, sender=vault.address) + + +def test_report_loss__custom_zero_max_loss__reverts( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + accountant.addVault(vault.address, sender=daddy) + # SEt max gain to 0% + accountant.setCustomConfig( + vault.address, strategy.address, 200, 2_000, 0, 100, 0, 0, sender=daddy + ) + config = list(accountant.customConfig(vault.address, strategy.address)) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + gain = 0 + loss = 1 + + with ape.reverts("too much loss"): + accountant.report(strategy.address, gain, loss, sender=vault.address) + + +def test_report_refund__custom_config( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + accountant.addVault(vault.address, sender=daddy) + # SEt refund ratio to 100% + accountant.setCustomConfig( + vault.address, + strategy.address, + 200, + 2_000, + 10_000, + 0, + 10_000, + 10_000, + sender=daddy, + ) + config = list(accountant.customConfig(vault.address, strategy.address)) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + gain = 0 + loss = amount // 10 + + # make sure accountant has the funds + asset.mint(accountant.address, loss, sender=daddy) + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Management fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + expected_refunds = loss * config[2] / MAX_BPS + + # The real fees charged should be less than what would be expected + assert expected_management_fees + expected_performance_fees == fees + assert expected_refunds == refunds + assert asset.allowance(accountant.address, vault.address) == expected_refunds + + +def test_report_refund_not_enough_asset__custom_config( + refund_accountant, + daddy, + vault, + strategy, + amount, + user, + deposit_into_vault, + provide_strategy_with_debt, + asset, +): + accountant = refund_accountant + accountant.addVault(vault.address, sender=daddy) + # SEt refund ratio to 100% + accountant.setCustomConfig( + vault.address, + strategy.address, + 200, + 2_000, + 10_000, + 0, + 10_000, + 10_000, + sender=daddy, + ) + config = list(accountant.customConfig(vault.address, strategy.address)) + + vault.add_strategy(strategy.address, sender=daddy) + vault.update_max_debt_for_strategy(strategy.address, MAX_INT, sender=daddy) + + deposit_into_vault(vault, amount) + provide_strategy_with_debt(daddy, strategy, vault, amount) + + assert vault.strategies(strategy.address).current_debt == amount + + gain = 0 + loss = amount // 10 + + # make sure accountant has the funds + asset.mint(accountant.address, loss // 2, sender=daddy) + + # Skip a year + chain.pending_timestamp = ( + vault.strategies(strategy.address).last_report + 31_556_952 - 1 + ) + chain.mine(timestamp=chain.pending_timestamp) + + tx = accountant.report(strategy.address, gain, loss, sender=vault.address) + + fees, refunds = tx.return_value + + # Management fees + expected_management_fees = amount * config[0] // MAX_BPS + # Perf fees + expected_performance_fees = gain * config[1] // MAX_BPS + + expected_refunds = loss // 2 + + # The real fees charged should be less than what would be expected + assert expected_management_fees + expected_performance_fees == fees + assert expected_refunds == refunds + assert asset.allowance(accountant.address, vault.address) == expected_refunds diff --git a/tests/conftest.py b/tests/conftest.py index d644f46..6803d60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,6 +30,11 @@ def user(accounts): return accounts[9] +@pytest.fixture(scope="session") +def vault_manager(accounts): + return accounts[7] + + @pytest.fixture(scope="session") def create_token(project, daddy, user, amount): def create_token( @@ -265,6 +270,35 @@ def deploy_healthcheck_accountant( yield deploy_healthcheck_accountant +@pytest.fixture(scope="session") +def deploy_refund_accountant(project, daddy, fee_recipient): + def deploy_refund_accountant( + manager=daddy, + fee_recipient=fee_recipient, + management_fee=100, + performance_fee=1_000, + refund_ratio=0, + max_fee=0, + max_gain=10_000, + max_loss=0, + ): + accountant = daddy.deploy( + project.RefundAccountant, + manager, + fee_recipient, + management_fee, + performance_fee, + refund_ratio, + max_fee, + max_gain, + max_loss, + ) + + return accountant + + yield deploy_refund_accountant + + @pytest.fixture(scope="session") def generic_accountant(deploy_generic_accountant): generic_accountant = deploy_generic_accountant() @@ -279,6 +313,13 @@ def healthcheck_accountant(deploy_healthcheck_accountant): yield healthcheck_accountant +@pytest.fixture(scope="session") +def refund_accountant(deploy_refund_accountant): + refund_accountant = deploy_refund_accountant() + + yield refund_accountant + + @pytest.fixture(scope="session") def set_fees_for_strategy(): def set_fees_for_strategy( diff --git a/tests/test_generic_debt_allocator.py b/tests/debtAllocators/test_generic_debt_allocator.py similarity index 98% rename from tests/test_generic_debt_allocator.py rename to tests/debtAllocators/test_generic_debt_allocator.py index dc7a6a9..a682562 100644 --- a/tests/test_generic_debt_allocator.py +++ b/tests/debtAllocators/test_generic_debt_allocator.py @@ -92,11 +92,6 @@ def test_set_ratios( with ape.reverts("!governance"): generic_debt_allocator.setStrategyDebtRatios(strategy, target, max, sender=user) - with ape.reverts("!active"): - generic_debt_allocator.setStrategyDebtRatios( - strategy, target, max, sender=daddy - ) - vault.add_strategy(strategy.address, sender=daddy) with ape.reverts("!minimum"): diff --git a/tests/test_registry.py b/tests/registry/test_registry.py similarity index 100% rename from tests/test_registry.py rename to tests/registry/test_registry.py diff --git a/tests/test_registry_factory.py b/tests/registry/test_registry_factory.py similarity index 100% rename from tests/test_registry_factory.py rename to tests/registry/test_registry_factory.py diff --git a/tests/test_release_registry.py b/tests/registry/test_release_registry.py similarity index 100% rename from tests/test_release_registry.py rename to tests/registry/test_release_registry.py