From 54d3027b29a03b84a71980fc11ba9365be6c2c88 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 30 Nov 2023 17:30:53 -0700 Subject: [PATCH 01/35] build: yield manager --- .../YieldManager/StrategyManager.sol | 127 +++++++ .../YieldManager/YieldManager.sol | 342 ++++++++++++++++++ 2 files changed, 469 insertions(+) create mode 100644 contracts/debtAllocators/YieldManager/StrategyManager.sol create mode 100644 contracts/debtAllocators/YieldManager/YieldManager.sol diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol new file mode 100644 index 0000000..9855a8b --- /dev/null +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.18; + +import {IStrategy} from "@tokenized-strategy/interfaces/IStrategy.sol"; + +// Allow for an array of calls + +contract StrategyManager { + struct StrategyInfo { + bool active; + address owner; + address debtManager; + } + + modifier onlyStrategyOwner(address _strategy) { + _checkStrategyManager(_strategy); + _; + } + + /// @notice Checks if the msg sender is the governance. + function _checkStrategyManager(address _strategy) internal view virtual { + require(strategyInfo[_strategy].owner == msg.sender, "!governance"); + } + + function _checkStrategyDebtManager( + address _strategy + ) internal view virtual { + require( + strategyInfo[_strategy].debtManager == msg.sender || + strategyInfo[_strategy].owner == msg.sender, + "!debt manager" + ); + } + + mapping(address => StrategyInfo) public strategyInfo; + + mapping(bytes4 => bool) public allowedSelectors; + + constructor(bytes4[] memory _allowedSelectors) { + for (uint256 i = 0; i < _allowedSelectors.length; ++i) { + allowedSelectors[_allowedSelectors[i]]; + } + } + + function manageNewStrategy( + address _strategy, + address _debtManager + ) external { + require(!strategyInfo[_strategy].active, "already active"); + // Cache the current strategy management. + address currentManager = IStrategy(_strategy).management(); + + // Accept management of the strategy. + IStrategy(_strategy).acceptManagement(); + + // Store the owner of the strategy. + strategyInfo[_strategy] = StrategyInfo({ + active: true, + owner: currentManager, + debtManager: _debtManager + }); + } + + function updateStrategyOwner( + address _strategy, + address _newOwner + ) external onlyStrategyOwner(_strategy) { + require(_newOwner != address(0), "ZERO ADDRESS"); + strategyInfo[_strategy].owner = _newOwner; + } + + // This gets rid of the benefits of two step transfers. + function removeManagement( + address _strategy, + address _newManager + ) external onlyStrategyOwner(_strategy) { + require(strategyInfo[_strategy].active, "not active"); + + delete strategyInfo[_strategy]; + + IStrategy(_strategy).setPendingManagement(_newManager); + } + + function forwardCalls( + address _strategy, + bytes[] memory _calldataArray + ) external returns (bytes[] memory _returnData) { + uint256 _length = _calldataArray.length; + _returnData = new bytes[](_length); + for (uint256 i = 0; i < _length; ++i) { + _returnData[i] = forwardCall(_strategy, _calldataArray[i]); + } + } + + function forwardCall( + address _strategy, + bytes memory _calldata + ) public returns (bytes memory) { + bytes4 selector; + + assembly { + // Copy the first 4 bytes of the memory array to the result variable + selector := mload(add(_calldata, 32)) + } + + if (allowedSelectors[selector]) { + _checkStrategyDebtManager(_strategy); + } else { + _checkStrategyManager(_strategy); + } + + (bool success, bytes memory result) = _strategy.call(_calldata); + + // If the call reverted. Return the error. + if (!success) { + assembly { + let ptr := mload(0x40) + let size := returndatasize() + returndatacopy(ptr, 0, size) + revert(ptr, size) + } + } + + // Return the result. + return result; + } +} diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol new file mode 100644 index 0000000..1b13b89 --- /dev/null +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.18; + +import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; +import {AprOracle} from "@periphery/AprOracle/AprOracle.sol"; + +import {Governance} from "@periphery/utils/Governance.sol"; + +import {StrategyManager, IStrategy} from "./StrategyManager.sol"; + +/** + * @title YearnV3 Yield Debt Allocator + * @author yearn.finance + * @notice + * This Debt Allocator is meant to be used alongside + * a Yearn V3 vault to allocate funds to the optimal strategy. + */ +contract YieldDebtAllocator is Governance { + // Struct that contains the address of the strategy and its best allocation. + struct Allocation { + // Address of the strategy. + address strategy; + // Debt for the strategy to end with. + // Can take 79 Billion 18 decimal tokens. + uint96 newDebt; + } + + modifier onlyAllocatorsOrOpen() { + _isAllocatorOrOpen(); + _; + } + + function _isAllocatorOrOpen() internal view { + require(allocators[msg.sender] || open, "!allocator or open"); + } + + // Contract that holds the logic and oracles for each strategy. + AprOracle internal constant aprOracle = + AprOracle(0x02b0210fC1575b38147B232b40D7188eF14C04f2); + + mapping(address => bool) public allocators; + + bool public open; + + address public immutable strategyManager; + + constructor( + address _governance, + address _strategyManager + ) Governance(_governance) { + strategyManager = _strategyManager; + } + + /** + * @notice Update a `_vault`s allocation of debt. + * @dev This takes the address of a vault and an array of + * its strategies and their specific allocation. + * + * The `_newAllocations` array should: + * - Contain all strategies that hold any amount of debt from the vault + * even if the debt wont be adjusted in order to get the correct + * on chain APR. + * - Be ordered so that all debt decreases are at the beginning of the array + * and debt increases at the end. + * + * It is expected that the proposer does all needed checks for values such + * as max_debt, maxWithdraw, min total Idle etc. that are enforced on debt + * updates at the vault level. + * + * @param _vault The address of the vault to propose an allocation for. + * @param _newAllocations Array of strategies and their new proposed allocation. + */ + function updateAllocationPermissioned( + address _vault, + Allocation[] memory _newAllocations + ) public onlyGovernance { + // Move funds + _allocate(_vault, _newAllocations); + } + + /** + * @notice Update a `_vault`s allocation of debt. + * @dev This takes the address of a vault and an array of + * its strategies and their specific allocation. + * + * The `_newAllocations` array should: + * - Contain all strategies that hold any amount of debt from the vault + * even if the debt wont be adjusted in order to get the correct + * on chain APR. + * - Be ordered so that all debt decreases are at the beginning of the array + * and debt increases at the end. + * + * It is expected that the proposer does all needed checks for values such + * as max_debt, maxWithdraw, min total Idle etc. that are enforced on debt + * updates at the vault level. + * + * @param _vault The address of the vault to propose an allocation for. + * @param _newAllocations Array of strategies and their new proposed allocation. + */ + function updateAllocation( + address _vault, + Allocation[] memory _newAllocations + ) + external + onlyAllocatorsOrOpen + returns (uint256 _currentApr, uint256 _newApr) + { + // Get the total assets the vault has. + uint256 _totalAssets = IVault(_vault).totalAssets(); + + // If 0 nothing to do. + if (_totalAssets == 0) return (0, 0); + + // Always first account for the amount idle in the vault. + uint256 _accountedFor = IVault(_vault).totalIdle(); + address _strategy; + uint256 _currentDebt; + uint256 _newDebt; + for (uint256 i = 0; i < _newAllocations.length; ++i) { + _strategy = _newAllocations[i].strategy; + _newDebt = uint256(_newAllocations[i].newDebt); + // Get the debt the strategy current has. + _currentDebt = IVault(_vault).strategies(_strategy).current_debt; + // Add to what we have accounted for. + _accountedFor += _currentDebt; + + // Get the current weighted APR the strategy is earning + uint256 _strategyApr = (aprOracle.getStrategyApr(_strategy, 0) * + _currentDebt); + + // Add to the amount currently being earned. + _currentApr += _strategyApr; + + // If no change move to the next strategy. + if (_newDebt == _currentDebt) { + // We assume the new apr will be the same as current. + _newApr += _strategyApr; + continue; + } + + if (_currentDebt > _newDebt) { + // We need to report profits and have them immediately unlock to not loose out on locked profit. + // NOTE: Should this all be put in the strategy manager + + // Get the current unlock rate. + uint256 profitUnlock = IStrategy(_strategy) + .profitMaxUnlockTime(); + + // Create array fo call data for the strategy manager to use + bytes[] memory _calldataArray = new bytes[](3); + + // Set profit unlock to 0. + _calldataArray[0] = abi.encodeCall( + IStrategy(_strategy).setProfitMaxUnlockTime, + 0 + ); + // Report profits. + _calldataArray[1] = abi.encodeWithSelector( + IStrategy(_strategy).report.selector + ); + // Set profit unlock back to original. + _calldataArray[2] = abi.encodeCall( + IStrategy(_strategy).setProfitMaxUnlockTime, + profitUnlock + ); + + // Forward all calls to strategy. + StrategyManager(strategyManager).forwardCalls( + _strategy, + _calldataArray + ); + + // If we are pulling all debt from a strategy OR we are decreasing + // debt and the strategy has any unrealised losses we first need to + // report the strategy. + if ( + _newDebt == 0 || + IVault(_vault).assess_share_of_unrealised_losses( + _strategy, + _currentDebt + ) != + 0 + ) { + IVault(_vault).process_report(_strategy); + } + } + + // TODO: validate losses based on ending totalAssets + // Allocate the new debt. + IVault(_vault).update_debt(_strategy, _newDebt); + + // Get the new APR + _newApr += aprOracle.getStrategyApr(_strategy, 0) * _newDebt; + } + + // Make sure the ending amounts are the same otherwise rates could be wrong. + require(_totalAssets == _accountedFor, "cheater"); + + // Adjust both rates based on the total assets to get the weighted APR. + _currentApr /= _totalAssets; + _newApr /= _totalAssets; + + require(_newApr > _currentApr, "fail"); + } + + /** + * @notice Validates that all assets of a vault are accounted for in + * the proposed allocation array. + * + * If not the APR calculation will not be correct. + * + * @param _vault The address of the vault to propose an allocation for. + * @param _newAllocations Array of strategies and their new proposed allocation. + */ + function validateAllocation( + address _vault, + Allocation[] memory _newAllocations + ) external view returns (bool) { + // Get the total assets the vault has. + uint256 _totalAssets = IVault(_vault).totalAssets(); + + // If 0 nothing to do. + if (_totalAssets == 0) return false; + + // Always first account for the amount idle in the vault. + uint256 _accountedFor = IVault(_vault).totalIdle(); + for (uint256 i = 0; i < _newAllocations.length; ++i) { + // Add the debt for each strategy in the array. + _accountedFor += IVault(_vault) + .strategies(_newAllocations[i].strategy) + .current_debt; + } + + // Make sure the ending amounts are the same. + return _totalAssets == _accountedFor; + } + + /** + * @notice Get the current apr the vault is earning and the expected + * APR based on the proposed changes. + * + * @param _vault The address of the vault to propose an allocation for. + * @param _newAllocations Array of strategies and their new proposed allocation. + */ + function getCurrentAndExpectedApr( + address _vault, + Allocation[] memory _newAllocations + ) external view returns (uint256 _currentApr, uint256 _expectedApr) { + // Get the total assets the vault has. + uint256 _totalAssets = IVault(_vault).totalAssets(); + + // If 0 nothing to do. + if (_totalAssets == 0) return (0, 0); + + uint256 _newDebt; + address _strategy; + uint256 _currentDebt; + for (uint256 i = 0; i < _newAllocations.length; ++i) { + _newDebt = uint256(_newAllocations[i].newDebt); + _strategy = _newAllocations[i].strategy; + _currentDebt = IVault(_vault).strategies(_strategy).current_debt; + + // Get the current weighted APR the strategy is earning + uint256 _strategyApr = (aprOracle.getStrategyApr(_strategy, 0) * + _currentDebt); + + // Add to the amount currently being earned. + _currentApr += _strategyApr; + + // If the strategies debt is not changing. + if (_currentDebt == _newDebt) { + // No need to call the APR oracle again. + _expectedApr += _strategyApr; + } else { + // We add what its expected to yield and its new expected debt + _expectedApr += (aprOracle.getStrategyApr( + _strategy, + int256(_newDebt) - int256(_currentDebt) + ) * _newDebt); + } + } + + // Adjust both based on the total assets to get the weighted APR. + _currentApr /= _totalAssets; + _expectedApr /= _totalAssets; + } + + /** + * @notice Allocate a vaults debt based on the new proposed Allocation. + * + * @param _vault The address of the vault to propose an allocation for. + * @param _newAllocations Array of strategies and their new proposed allocation. + */ + function _allocate( + address _vault, + Allocation[] memory _newAllocations + ) internal { + address _strategy; + uint256 _newDebt; + for (uint256 i = 0; i < _newAllocations.length; ++i) { + _strategy = _newAllocations[i].strategy; + _newDebt = uint256(_newAllocations[i].newDebt); + + // Get the current amount the strategy holds. + uint256 _currentDebt = IVault(_vault) + .strategies(_strategy) + .current_debt; + + // If no change move to the next strategy. + if (_newDebt == _currentDebt) continue; + + // If we are pulling all debt from a strategy OR we are decreasing + // debt and the strategy has any unrealised losses we first need to + // report the strategy. + if ( + _newDebt == 0 || + (_currentDebt > _newDebt && + IVault(_vault).assess_share_of_unrealised_losses( + _strategy, + _currentDebt + ) != + 0) + ) { + IVault(_vault).process_report(_strategy); + } + + // Allocate the new debt. + IVault(_vault).update_debt(_strategy, _newDebt); + } + } + + function setAllocators( + address _address, + bool _allowed + ) external onlyGovernance { + allocators[_address] = _allowed; + } + + function setOpen(bool _open) external onlyGovernance { + open = _open; + } +} From dd5f7d651f17594bfc7597da456ad081d98074ae Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 4 Dec 2023 16:58:25 -0700 Subject: [PATCH 02/35] fix: check loss and outsource reporting --- .../YieldManager/StrategyManager.sol | 30 ++++++++ .../YieldManager/YieldManager.sol | 69 +++++++++++-------- 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol index 9855a8b..c258e38 100644 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -81,6 +81,36 @@ contract StrategyManager { IStrategy(_strategy).setPendingManagement(_newManager); } + function reportFullProfit(address _strategy) external { + // Get the current unlock rate. + uint256 profitUnlock = IStrategy(_strategy).profitMaxUnlockTime(); + + if (profitUnlock != 0) { + // Set profit unlock to 0. + forwardCall( + _strategy, + abi.encodeCall(IStrategy(_strategy).setProfitMaxUnlockTime, 0) + ); + } + + // Report profits. + forwardCall( + _strategy, + abi.encodeWithSelector(IStrategy(_strategy).report.selector) + ); + + if (profitUnlock != 0) { + // Set profit unlock back to original. + forwardCall( + _strategy, + abi.encodeCall( + IStrategy(_strategy).setProfitMaxUnlockTime, + profitUnlock + ) + ); + } + } + function forwardCalls( address _strategy, bytes[] memory _calldataArray diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 1b13b89..9ac8dee 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -16,6 +16,9 @@ import {StrategyManager, IStrategy} from "./StrategyManager.sol"; * a Yearn V3 vault to allocate funds to the optimal strategy. */ contract YieldDebtAllocator is Governance { + /// @notice An event emitted when the max debt update loss is updated. + event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); + // Struct that contains the address of the strategy and its best allocation. struct Allocation { // Address of the strategy. @@ -34,6 +37,8 @@ contract YieldDebtAllocator is Governance { require(allocators[msg.sender] || open, "!allocator or open"); } + uint256 internal constant MAX_BPS = 10_000; + // Contract that holds the logic and oracles for each strategy. AprOracle internal constant aprOracle = AprOracle(0x02b0210fC1575b38147B232b40D7188eF14C04f2); @@ -42,6 +47,9 @@ contract YieldDebtAllocator is Governance { bool public open; + /// @notice Max loss to accept on debt updates in basis points. + uint256 public maxDebtUpdateLoss; + address public immutable strategyManager; constructor( @@ -140,35 +148,7 @@ contract YieldDebtAllocator is Governance { if (_currentDebt > _newDebt) { // We need to report profits and have them immediately unlock to not loose out on locked profit. - // NOTE: Should this all be put in the strategy manager - - // Get the current unlock rate. - uint256 profitUnlock = IStrategy(_strategy) - .profitMaxUnlockTime(); - - // Create array fo call data for the strategy manager to use - bytes[] memory _calldataArray = new bytes[](3); - - // Set profit unlock to 0. - _calldataArray[0] = abi.encodeCall( - IStrategy(_strategy).setProfitMaxUnlockTime, - 0 - ); - // Report profits. - _calldataArray[1] = abi.encodeWithSelector( - IStrategy(_strategy).report.selector - ); - // Set profit unlock back to original. - _calldataArray[2] = abi.encodeCall( - IStrategy(_strategy).setProfitMaxUnlockTime, - profitUnlock - ); - - // Forward all calls to strategy. - StrategyManager(strategyManager).forwardCalls( - _strategy, - _calldataArray - ); + StrategyManager(strategyManager).reportFullProfit(_strategy); // If we are pulling all debt from a strategy OR we are decreasing // debt and the strategy has any unrealised losses we first need to @@ -185,10 +165,24 @@ contract YieldDebtAllocator is Governance { } } - // TODO: validate losses based on ending totalAssets // Allocate the new debt. IVault(_vault).update_debt(_strategy, _newDebt); + // Validate losses based on ending totalAssets + if (_currentDebt > _newDebt) { + uint256 afterAssets = IVault(_vault).totalAssets(); + + // If a loss was realized. + if (afterAssets < _totalAssets) { + // Make sure its within the range. + require( + _totalAssets - afterAssets <= + (_currentDebt * maxDebtUpdateLoss) / MAX_BPS, + "too much loss" + ); + } + } + // Get the new APR _newApr += aprOracle.getStrategyApr(_strategy, 0) * _newDebt; } @@ -339,4 +333,19 @@ contract YieldDebtAllocator is Governance { function setOpen(bool _open) external onlyGovernance { open = _open; } + + /** + * @notice Set the max loss in Basis points to allow on debt updates. + * @dev Withdrawing during debt updates use {redeem} which allows for 100% loss. + * This can be used to assure a loss is not realized on redeem outside the tolerance. + * @param _maxDebtUpdateLoss The max loss to accept on debt updates. + */ + function setMaxDebtUpdateLoss( + uint256 _maxDebtUpdateLoss + ) external virtual onlyGovernance { + require(_maxDebtUpdateLoss <= MAX_BPS, "higher than max"); + maxDebtUpdateLoss = _maxDebtUpdateLoss; + + emit UpdateMaxDebtUpdateLoss(_maxDebtUpdateLoss); + } } From 8002529789587b71d562e9388c229900efd5fe8e Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 5 Dec 2023 14:39:51 -0700 Subject: [PATCH 03/35] fix: yield not apr --- .../YieldManager/StrategyManager.sol | 27 ++++----- .../YieldManager/YieldManager.sol | 56 +++++++++---------- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol index c258e38..0ef5d01 100644 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -17,6 +17,11 @@ contract StrategyManager { _; } + modifier onlyStrategyOwnerOrDebtManager(address _strategy) { + _checkStrategyDebtManager(_strategy); + _; + } + /// @notice Checks if the msg sender is the governance. function _checkStrategyManager(address _strategy) internal view virtual { require(strategyInfo[_strategy].owner == msg.sender, "!governance"); @@ -81,33 +86,23 @@ contract StrategyManager { IStrategy(_strategy).setPendingManagement(_newManager); } - function reportFullProfit(address _strategy) external { + function reportFullProfit( + address _strategy + ) external onlyStrategyOwnerOrDebtManager(_strategy) { // Get the current unlock rate. uint256 profitUnlock = IStrategy(_strategy).profitMaxUnlockTime(); if (profitUnlock != 0) { // Set profit unlock to 0. - forwardCall( - _strategy, - abi.encodeCall(IStrategy(_strategy).setProfitMaxUnlockTime, 0) - ); + IStrategy(_strategy).setProfitMaxUnlockTime(0); } // Report profits. - forwardCall( - _strategy, - abi.encodeWithSelector(IStrategy(_strategy).report.selector) - ); + IStrategy(_strategy).report(); if (profitUnlock != 0) { // Set profit unlock back to original. - forwardCall( - _strategy, - abi.encodeCall( - IStrategy(_strategy).setProfitMaxUnlockTime, - profitUnlock - ) - ); + IStrategy(_strategy).setProfitMaxUnlockTime(profitUnlock); } } diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 9ac8dee..42c1bb4 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -9,7 +9,7 @@ import {Governance} from "@periphery/utils/Governance.sol"; import {StrategyManager, IStrategy} from "./StrategyManager.sol"; /** - * @title YearnV3 Yield Debt Allocator + * @title YearnV3 Yield Yield Based Debt Allocator * @author yearn.finance * @notice * This Debt Allocator is meant to be used alongside @@ -111,7 +111,7 @@ contract YieldDebtAllocator is Governance { ) external onlyAllocatorsOrOpen - returns (uint256 _currentApr, uint256 _newApr) + returns (uint256 _currentYield, uint256 _afterYield) { // Get the total assets the vault has. uint256 _totalAssets = IVault(_vault).totalAssets(); @@ -137,19 +137,20 @@ contract YieldDebtAllocator is Governance { _currentDebt); // Add to the amount currently being earned. - _currentApr += _strategyApr; + _currentYield += _strategyApr; // If no change move to the next strategy. if (_newDebt == _currentDebt) { // We assume the new apr will be the same as current. - _newApr += _strategyApr; + _afterYield += _strategyApr; continue; } if (_currentDebt > _newDebt) { - // We need to report profits and have them immediately unlock to not loose out on locked profit. + // We need to report profits and have them immediately unlock to not lose out on locked profit. StrategyManager(strategyManager).reportFullProfit(_strategy); + uint256 loss; // If we are pulling all debt from a strategy OR we are decreasing // debt and the strategy has any unrealised losses we first need to // report the strategy. @@ -161,19 +162,18 @@ contract YieldDebtAllocator is Governance { ) != 0 ) { - IVault(_vault).process_report(_strategy); + (, loss) = IVault(_vault).process_report(_strategy); } - } - // Allocate the new debt. - IVault(_vault).update_debt(_strategy, _newDebt); + // Allocate the new debt. + IVault(_vault).update_debt(_strategy, _newDebt); - // Validate losses based on ending totalAssets - if (_currentDebt > _newDebt) { + // Validate losses based on ending totalAssets uint256 afterAssets = IVault(_vault).totalAssets(); - // If a loss was realized. - if (afterAssets < _totalAssets) { + // NOTE: doesn't count for previous losses + // If a loss was realized on just the debt update. + if (afterAssets + loss < _totalAssets) { // Make sure its within the range. require( _totalAssets - afterAssets <= @@ -181,20 +181,22 @@ contract YieldDebtAllocator is Governance { "too much loss" ); } + } else { + // Just Allocate the new debt. + IVault(_vault).update_debt(_strategy, _newDebt); } // Get the new APR - _newApr += aprOracle.getStrategyApr(_strategy, 0) * _newDebt; + if (_newDebt != 0) { + _afterYield += + aprOracle.getStrategyApr(_strategy, 0) * + _newDebt; + } } // Make sure the ending amounts are the same otherwise rates could be wrong. require(_totalAssets == _accountedFor, "cheater"); - - // Adjust both rates based on the total assets to get the weighted APR. - _currentApr /= _totalAssets; - _newApr /= _totalAssets; - - require(_newApr > _currentApr, "fail"); + require(_afterYield > _currentYield, "fail"); } /** @@ -236,10 +238,10 @@ contract YieldDebtAllocator is Governance { * @param _vault The address of the vault to propose an allocation for. * @param _newAllocations Array of strategies and their new proposed allocation. */ - function getCurrentAndExpectedApr( + function getCurrentAndExpectedYield( address _vault, Allocation[] memory _newAllocations - ) external view returns (uint256 _currentApr, uint256 _expectedApr) { + ) external view returns (uint256 _currentYield, uint256 _expectedYield) { // Get the total assets the vault has. uint256 _totalAssets = IVault(_vault).totalAssets(); @@ -259,24 +261,20 @@ contract YieldDebtAllocator is Governance { _currentDebt); // Add to the amount currently being earned. - _currentApr += _strategyApr; + _currentYield += _strategyApr; // If the strategies debt is not changing. if (_currentDebt == _newDebt) { // No need to call the APR oracle again. - _expectedApr += _strategyApr; + _expectedYield += _strategyApr; } else { // We add what its expected to yield and its new expected debt - _expectedApr += (aprOracle.getStrategyApr( + _expectedYield += (aprOracle.getStrategyApr( _strategy, int256(_newDebt) - int256(_currentDebt) ) * _newDebt); } } - - // Adjust both based on the total assets to get the weighted APR. - _currentApr /= _totalAssets; - _expectedApr /= _totalAssets; } /** From dcc343a263e7d390bb56ba317009c02dcd354459 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 11 Dec 2023 18:58:06 -0700 Subject: [PATCH 04/35] test: yield manager --- contracts/Mocks/MockOracle.sol | 6 + contracts/Mocks/MockTokenizedStrategy.sol | 36 ++ .../YieldManager/StrategyManager.sol | 138 ++++- .../YieldManager/YieldManager.sol | 155 +++-- tests/conftest.py | 66 ++- .../yield/test_strategy_manager.py | 304 ++++++++++ .../yield/test_yield_manager.py | 546 ++++++++++++++++++ 7 files changed, 1194 insertions(+), 57 deletions(-) create mode 100644 contracts/Mocks/MockOracle.sol create mode 100644 contracts/Mocks/MockTokenizedStrategy.sol create mode 100644 tests/debtAllocators/yield/test_strategy_manager.py create mode 100644 tests/debtAllocators/yield/test_yield_manager.py diff --git a/contracts/Mocks/MockOracle.sol b/contracts/Mocks/MockOracle.sol new file mode 100644 index 0000000..2886262 --- /dev/null +++ b/contracts/Mocks/MockOracle.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.18; + +import {AprOracle} from "@periphery/AprOracle/AprOracle.sol"; + +contract MockOracle is AprOracle {} diff --git a/contracts/Mocks/MockTokenizedStrategy.sol b/contracts/Mocks/MockTokenizedStrategy.sol new file mode 100644 index 0000000..4e8cfdc --- /dev/null +++ b/contracts/Mocks/MockTokenizedStrategy.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.18; + +import {MockTokenizedStrategy} from "@yearn-vaults/test/mocks/ERC4626/MockTokenizedStrategy.sol"; + +contract MockTokenized is MockTokenizedStrategy { + uint256 public apr; + uint256 public loss; + + constructor( + address _asset, + string memory _name, + address _management, + address _keeper, + uint256 _apr + ) MockTokenizedStrategy(_asset, _name, _management, _keeper) { + apr = _apr; + } + + function aprAfterDebtChange( + address, + int256 + ) external view returns (uint256) { + return apr; + } + + function setApr(uint256 _apr) external { + apr = _apr; + } + + function realizeLoss(uint256 _amount) external { + strategyStorage().asset.transfer(msg.sender, _amount); + strategyStorage().totalIdle -= _amount; + strategyStorage().totalDebt += _amount; + } +} diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol index 0ef5d01..8fc6129 100644 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -3,30 +3,44 @@ pragma solidity 0.8.18; import {IStrategy} from "@tokenized-strategy/interfaces/IStrategy.sol"; -// Allow for an array of calls - +/// @notice Holds the `management` role of a V3 strategy so that a +/// debt allocator can call both reports and change the profit unlock time. contract StrategyManager { + /// @notice Emitted when a new strategy is added to the manager. + event StrategyAdded( + address indexed strategy, + address indexed owner, + address indexed debtManager + ); + + /// @notice Emitted when a strategy is removed. + event StrategyRemoved(address indexed strategy, address indexed newManager); + + /// @notice holds info for a strategy that is managed. struct StrategyInfo { bool active; address owner; address debtManager; } + /// @notice Only the `_strategy` specific owner can call. modifier onlyStrategyOwner(address _strategy) { - _checkStrategyManager(_strategy); + _checkStrategyOwner(_strategy); _; } + /// @notice Only the `_strategy` owner of its debt manager can call. modifier onlyStrategyOwnerOrDebtManager(address _strategy) { _checkStrategyDebtManager(_strategy); _; } - /// @notice Checks if the msg sender is the governance. - function _checkStrategyManager(address _strategy) internal view virtual { - require(strategyInfo[_strategy].owner == msg.sender, "!governance"); + /// @notice Checks if the msg sender is the owner of the strategy. + function _checkStrategyOwner(address _strategy) internal view virtual { + require(strategyInfo[_strategy].owner == msg.sender, "!owner"); } + /// @notice Checks if the msg sender is the debt manager or the strategy owner. function _checkStrategyDebtManager( address _strategy ) internal view virtual { @@ -37,16 +51,27 @@ contract StrategyManager { ); } + /// @notice strategy address => struct with info. mapping(address => StrategyInfo) public strategyInfo; + /// @notice function selector => bool if a debt manager can call that. mapping(bytes4 => bool) public allowedSelectors; + /** + * @notice Add any of the allowed selectors for a debt manager to call + * to the mapping. + */ constructor(bytes4[] memory _allowedSelectors) { for (uint256 i = 0; i < _allowedSelectors.length; ++i) { - allowedSelectors[_allowedSelectors[i]]; + allowedSelectors[_allowedSelectors[i]] = true; } } + /** + * @notice Manage a new strategy, setting the debt manager and marking it as active. + * @param _strategy The address of the strategy. + * @param _debtManager The address of the debt manager. + */ function manageNewStrategy( address _strategy, address _debtManager @@ -64,28 +89,75 @@ contract StrategyManager { owner: currentManager, debtManager: _debtManager }); + + emit StrategyAdded(_strategy, currentManager, _debtManager); } + /** + * @notice Updates the owner of a strategy. + * @param _strategy The address of the strategy. + * @param _newOwner The address of the new owner. + */ function updateStrategyOwner( address _strategy, address _newOwner ) external onlyStrategyOwner(_strategy) { - require(_newOwner != address(0), "ZERO ADDRESS"); + require( + _newOwner != address(0) && + _newOwner != address(this) && + _newOwner != _strategy, + "bad address" + ); strategyInfo[_strategy].owner = _newOwner; } - // This gets rid of the benefits of two step transfers. + /** + * @notice Updates the debt manager of a strategy. + * @param _strategy The address of the strategy. + * @param _newDebtManager The address of the new owner. + */ + function updateDebtManager( + address _strategy, + address _newDebtManager + ) external onlyStrategyOwner(_strategy) { + strategyInfo[_strategy].debtManager = _newDebtManager; + } + + /** + * @notice Removes the management of a strategy, transferring it to the `owner`. + * @param _strategy The address of the strategy. + */ + function removeManagement(address _strategy) external { + removeManagement(_strategy, msg.sender); + } + + /** + * @notice Removes the management of a strategy, transferring it to a new manager. + * @param _strategy The address of the strategy. + * @param _newManager The address of the new manager. + */ function removeManagement( address _strategy, address _newManager - ) external onlyStrategyOwner(_strategy) { - require(strategyInfo[_strategy].active, "not active"); + ) public onlyStrategyOwner(_strategy) { + require( + _newManager != address(0) && + _newManager != address(this) && + _newManager != _strategy, + "bad address" + ); delete strategyInfo[_strategy]; IStrategy(_strategy).setPendingManagement(_newManager); + + emit StrategyRemoved(_strategy, _newManager); } + /** + * @notice Reports full profit for a strategy. + * @param _strategy The address of the strategy. + */ function reportFullProfit( address _strategy ) external onlyStrategyOwnerOrDebtManager(_strategy) { @@ -106,6 +178,12 @@ contract StrategyManager { } } + /** + * @notice Forwards multiple calls to a strategy. + * @param _strategy The address of the strategy. + * @param _calldataArray An array of calldata for each call. + * @return _returnData An array of return data from each call. + */ function forwardCalls( address _strategy, bytes[] memory _calldataArray @@ -117,6 +195,12 @@ contract StrategyManager { } } + /** + * @notice Forwards a single call to a strategy. + * @param _strategy The address of the strategy. + * @param _calldata The calldata for the call. + * @return _returnData The return data from the call. + */ function forwardCall( address _strategy, bytes memory _calldata @@ -124,14 +208,14 @@ contract StrategyManager { bytes4 selector; assembly { - // Copy the first 4 bytes of the memory array to the result variable + // Copy the first 4 bytes of the memory array to the selector variable selector := mload(add(_calldata, 32)) } if (allowedSelectors[selector]) { _checkStrategyDebtManager(_strategy); } else { - _checkStrategyManager(_strategy); + _checkStrategyOwner(_strategy); } (bool success, bytes memory result) = _strategy.call(_calldata); @@ -149,4 +233,32 @@ contract StrategyManager { // Return the result. return result; } + + /** + * @notice Calls any target contract as the manager of the strategy. + * @param _strategy The address of the strategy. + * @param _target The address of the target contract. + * @param _calldata The calldata for the call. + * @return result The return data from the call. + */ + function genericCall( + address _strategy, + address _target, + bytes memory _calldata + ) external virtual onlyStrategyOwner(_strategy) returns (bytes memory) { + (bool success, bytes memory result) = _target.call(_calldata); + + // If the call reverted. Return the error. + if (!success) { + assembly { + let ptr := mload(0x40) + let size := returndatasize() + returndatacopy(ptr, 0, size) + revert(ptr, size) + } + } + + // Return the result. + return result; + } } diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 42c1bb4..9635f3d 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -6,7 +6,7 @@ import {AprOracle} from "@periphery/AprOracle/AprOracle.sol"; import {Governance} from "@periphery/utils/Governance.sol"; -import {StrategyManager, IStrategy} from "./StrategyManager.sol"; +import {StrategyManager} from "./StrategyManager.sol"; /** * @title YearnV3 Yield Yield Based Debt Allocator @@ -15,7 +15,7 @@ import {StrategyManager, IStrategy} from "./StrategyManager.sol"; * This Debt Allocator is meant to be used alongside * a Yearn V3 vault to allocate funds to the optimal strategy. */ -contract YieldDebtAllocator is Governance { +contract YieldManager is Governance { /// @notice An event emitted when the max debt update loss is updated. event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); @@ -28,35 +28,42 @@ contract YieldDebtAllocator is Governance { uint96 newDebt; } + /// @notice Only allow the sender to be an allocator if not opened. modifier onlyAllocatorsOrOpen() { _isAllocatorOrOpen(); _; } + /// @notice Check if it has been opened or is an allocator. function _isAllocatorOrOpen() internal view { require(allocators[msg.sender] || open, "!allocator or open"); } uint256 internal constant MAX_BPS = 10_000; - // Contract that holds the logic and oracles for each strategy. + /// @notice Contract that holds the logic and oracles for each strategy. AprOracle internal constant aprOracle = AprOracle(0x02b0210fC1575b38147B232b40D7188eF14C04f2); - mapping(address => bool) public allocators; - + /// @notice Flag to set to allow anyone to propose allocations. bool public open; /// @notice Max loss to accept on debt updates in basis points. uint256 public maxDebtUpdateLoss; + /// @notice Address that should hold the strategies `management` role. address public immutable strategyManager; + /// @notice Addresses that are allowed to propose allocations. + mapping(address => bool) public allocators; + constructor( address _governance, address _strategyManager ) Governance(_governance) { strategyManager = _strategyManager; + // Default to 1 BP loss + maxDebtUpdateLoss = 1; } /** @@ -65,15 +72,11 @@ contract YieldDebtAllocator is Governance { * its strategies and their specific allocation. * * The `_newAllocations` array should: - * - Contain all strategies that hold any amount of debt from the vault - * even if the debt wont be adjusted in order to get the correct - * on chain APR. * - Be ordered so that all debt decreases are at the beginning of the array * and debt increases at the end. * - * It is expected that the proposer does all needed checks for values such - * as max_debt, maxWithdraw, min total Idle etc. that are enforced on debt - * updates at the vault level. + * This will not do any APR checks and assumes the sender has completed + * any and all necessary checks before sending. * * @param _vault The address of the vault to propose an allocation for. * @param _newAllocations Array of strategies and their new proposed allocation. @@ -121,9 +124,12 @@ contract YieldDebtAllocator is Governance { // Always first account for the amount idle in the vault. uint256 _accountedFor = IVault(_vault).totalIdle(); + // Create local variables used through loops. address _strategy; uint256 _currentDebt; uint256 _newDebt; + uint256 _loss; + uint256 _gain; for (uint256 i = 0; i < _newAllocations.length; ++i) { _strategy = _newAllocations[i].strategy; _newDebt = uint256(_newAllocations[i].newDebt); @@ -146,34 +152,48 @@ contract YieldDebtAllocator is Governance { continue; } + // If we are withdrawing. if (_currentDebt > _newDebt) { - // We need to report profits and have them immediately unlock to not lose out on locked profit. - StrategyManager(strategyManager).reportFullProfit(_strategy); - - uint256 loss; - // If we are pulling all debt from a strategy OR we are decreasing - // debt and the strategy has any unrealised losses we first need to - // report the strategy. - if ( - _newDebt == 0 || + // If we are pulling all debt from a strategy. + if (_newDebt == 0) { + // We need to report profits and have them immediately unlock to not lose out on locked profit. + StrategyManager(strategyManager).reportFullProfit( + _strategy + ); + + // Report profits on the vault. + (uint256 reportedProfit, uint256 reportedLoss) = IVault( + _vault + ).process_report(_strategy); + + // Track for debt reduction loss checks. + _loss += reportedLoss; + _gain += reportedProfit; + } else if ( + // We cannot decrease debt if the strategy has any unrealised losses. IVault(_vault).assess_share_of_unrealised_losses( _strategy, _currentDebt - ) != - 0 + ) != 0 ) { - (, loss) = IVault(_vault).process_report(_strategy); + // Realize the loss. + (, uint256 reportedLoss) = IVault(_vault).process_report( + _strategy + ); + // Track for debt reduction loss checks. + _loss += reportedLoss; } // Allocate the new debt. IVault(_vault).update_debt(_strategy, _newDebt); - // Validate losses based on ending totalAssets - uint256 afterAssets = IVault(_vault).totalAssets(); + // Validate losses based on ending totalAssets adjusted for any realized loss or gain. + uint256 afterAssets = IVault(_vault).totalAssets() + + _loss - + _gain; - // NOTE: doesn't count for previous losses // If a loss was realized on just the debt update. - if (afterAssets + loss < _totalAssets) { + if (afterAssets < _totalAssets) { // Make sure its within the range. require( _totalAssets - afterAssets <= @@ -182,7 +202,7 @@ contract YieldDebtAllocator is Governance { ); } } else { - // Just Allocate the new debt. + // If adding just Allocate the new debt. IVault(_vault).update_debt(_strategy, _newDebt); } @@ -232,9 +252,11 @@ contract YieldDebtAllocator is Governance { } /** - * @notice Get the current apr the vault is earning and the expected + * @notice Get the current weighted yield the vault is earning and the expected * APR based on the proposed changes. * + * Must divide by the totalAssets to get the APR as 1e18. + * * @param _vault The address of the vault to propose an allocation for. * @param _newAllocations Array of strategies and their new proposed allocation. */ @@ -289,6 +311,9 @@ contract YieldDebtAllocator is Governance { ) internal { address _strategy; uint256 _newDebt; + uint256 _loss; + uint256 _gain; + uint256 _totalAssets = IVault(_vault).totalAssets(); for (uint256 i = 0; i < _newAllocations.length; ++i) { _strategy = _newAllocations[i].strategy; _newDebt = uint256(_newAllocations[i].newDebt); @@ -301,33 +326,77 @@ contract YieldDebtAllocator is Governance { // If no change move to the next strategy. if (_newDebt == _currentDebt) continue; - // If we are pulling all debt from a strategy OR we are decreasing - // debt and the strategy has any unrealised losses we first need to - // report the strategy. - if ( - _newDebt == 0 || - (_currentDebt > _newDebt && + if (_newDebt < _currentDebt) { + // If we are pulling all debt from a strategy. + if (_newDebt == 0) { + // We need to report profits and have them immediately unlock to not lose out on locked profit. + StrategyManager(strategyManager).reportFullProfit( + _strategy + ); + + // Report on the vault. + (uint256 reportedProfit, uint256 reportedLoss) = IVault( + _vault + ).process_report(_strategy); + + // Track for debt reduction loss checks. + _loss += reportedLoss; + _gain += reportedProfit; + } else if ( + // We cannot decrease debt if the strategy has any unrealised losses. IVault(_vault).assess_share_of_unrealised_losses( _strategy, _currentDebt - ) != - 0) - ) { - IVault(_vault).process_report(_strategy); - } + ) != 0 + ) { + // Realize the loss. + (, uint256 reportedLoss) = IVault(_vault).process_report( + _strategy + ); + // Track for debt reduction loss checks. + _loss += reportedLoss; + } + + // Allocate the new debt. + IVault(_vault).update_debt(_strategy, _newDebt); + + // Validate losses based on ending totalAssets adjusted for any realized loss or gain. + uint256 afterAssets = IVault(_vault).totalAssets() + + _loss - + _gain; - // Allocate the new debt. - IVault(_vault).update_debt(_strategy, _newDebt); + // If a loss was realized on just the debt update. + if (afterAssets < _totalAssets) { + // Make sure its within the range. + require( + _totalAssets - afterAssets <= + (_currentDebt * maxDebtUpdateLoss) / MAX_BPS, + "too much loss" + ); + } + } else { + // If adding just Allocate the new debt. + IVault(_vault).update_debt(_strategy, _newDebt); + } } } - function setAllocators( + /** + * @notice Sets the permission for an allocator. + * @param _address The address of the allocator. + * @param _allowed The permission to set for the allocator. + */ + function setAllocator( address _address, bool _allowed ) external onlyGovernance { allocators[_address] = _allowed; } + /** + * @notice Sets the open status of the contract. + * @param _open The new open status to set. + */ function setOpen(bool _open) external onlyGovernance { open = _open; } diff --git a/tests/conftest.py b/tests/conftest.py index 5faa409..d59fd2c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ import pytest -from ape import accounts, project +from ape import accounts, project, networks from utils.constants import MAX_INT, WEEK, ROLES from web3 import Web3, HTTPProvider from hexbytes import HexBytes @@ -26,8 +26,13 @@ def security(accounts): @pytest.fixture(scope="session") +<<<<<<< HEAD def management(accounts): yield accounts[3] +======= +def keeper(accounts): + yield accounts[4] +>>>>>>> test: yield manager @pytest.fixture(scope="session") @@ -206,6 +211,24 @@ def strategy(asset, create_strategy): yield strategy +@pytest.fixture(scope="session") +def deploy_mock_tokenized(project, daddy, asset, management, keeper): + def deploy_mock_tokenized(name="name", apr=0): + mock_tokenized = daddy.deploy( + project.MockTokenized, asset, name, management, keeper, apr + ) + return mock_tokenized + + yield deploy_mock_tokenized + + +@pytest.fixture(scope="session") +def mock_tokenized(deploy_mock_tokenized): + mock_tokenized = deploy_mock_tokenized() + + yield mock_tokenized + + @pytest.fixture(scope="function") def create_vault_and_strategy(strategy, vault, deposit_into_vault): def create_vault_and_strategy(account, amount_into_vault): @@ -443,3 +466,44 @@ def role_manager( ) return role_manager +def deploy_strategy_manager(project, daddy): + def deploy_strategy_manager(): + strategy_manager = daddy.deploy( + project.StrategyManager, ["0x2606a10b", "0xdf69b22a"] + ) + + return strategy_manager + + yield deploy_strategy_manager + + +@pytest.fixture(scope="session") +def strategy_manager(deploy_strategy_manager): + strategy_manager = deploy_strategy_manager() + + yield strategy_manager + + +@pytest.fixture(scope="session") +def deploy_yield_manager(project, daddy, strategy_manager): + def deploy_yield_manager(): + yield_manager = daddy.deploy(project.YieldManager, daddy, strategy_manager) + + return yield_manager + + yield deploy_yield_manager + + +@pytest.fixture(scope="session") +def yield_manager(deploy_yield_manager): + yield_manager = deploy_yield_manager() + + yield yield_manager + + +@pytest.fixture(scope="session") +def apr_oracle(project): + oracle = project.MockOracle + address = "0x02b0210fC1575b38147B232b40D7188eF14C04f2" + networks.provider.set_code(address, oracle.contract_type.runtime_bytecode.bytecode) + yield oracle.at(address) diff --git a/tests/debtAllocators/yield/test_strategy_manager.py b/tests/debtAllocators/yield/test_strategy_manager.py new file mode 100644 index 0000000..a0dbfe8 --- /dev/null +++ b/tests/debtAllocators/yield/test_strategy_manager.py @@ -0,0 +1,304 @@ +import ape +from ape import chain, project +from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES + +allowed_selectors = ["0x2606a10b", "0xdf69b22a"] + + +def test_strategy_manager_setup(strategy_manager, mock_tokenized): + assert strategy_manager.strategyInfo(mock_tokenized).active == False + assert strategy_manager.strategyInfo(mock_tokenized).owner == ZERO_ADDRESS + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == ZERO_ADDRESS + for selector in allowed_selectors: + assert strategy_manager.allowedSelectors(selector) + # Check random selector + assert strategy_manager.allowedSelectors("0x9bbefdb6") == False + + +def test_add_new_strategy(strategy_manager, mock_tokenized, yield_manager, management): + assert strategy_manager.strategyInfo(mock_tokenized).active == False + assert strategy_manager.strategyInfo(mock_tokenized).owner == ZERO_ADDRESS + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == ZERO_ADDRESS + + with ape.reverts("!pending"): + strategy_manager.manageNewStrategy( + mock_tokenized, yield_manager, sender=management + ) + + mock_tokenized.setPendingManagement(strategy_manager, sender=management) + + tx = strategy_manager.manageNewStrategy( + mock_tokenized, yield_manager, sender=management + ) + + assert mock_tokenized.management() == strategy_manager + + event = list(tx.decode_logs(strategy_manager.StrategyAdded))[0] + + assert event.strategy == mock_tokenized + assert event.owner == management + assert event.debtManager == yield_manager + + assert strategy_manager.strategyInfo(mock_tokenized).active == True + assert strategy_manager.strategyInfo(mock_tokenized).owner == management + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + + # cannot add it again + with ape.reverts("already active"): + strategy_manager.manageNewStrategy( + mock_tokenized, yield_manager, sender=management + ) + + +def test_remove_strategy( + strategy_manager, mock_tokenized, yield_manager, management, user +): + # Will revert on modifier if not yet added. + with ape.reverts("!owner"): + strategy_manager.removeManagement(mock_tokenized, user, sender=management) + + mock_tokenized.setPendingManagement(strategy_manager, sender=management) + + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + + assert mock_tokenized.management() == strategy_manager + assert strategy_manager.strategyInfo(mock_tokenized).active == True + assert strategy_manager.strategyInfo(mock_tokenized).owner == management + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + + with ape.reverts("!owner"): + strategy_manager.removeManagement(mock_tokenized, user, sender=user) + + with ape.reverts("!owner"): + strategy_manager.removeManagement(mock_tokenized, sender=user) + + with ape.reverts("bad address"): + strategy_manager.removeManagement( + mock_tokenized, ZERO_ADDRESS, sender=management + ) + + with ape.reverts("bad address"): + strategy_manager.removeManagement( + mock_tokenized, mock_tokenized, sender=management + ) + + with ape.reverts("bad address"): + strategy_manager.removeManagement( + mock_tokenized, strategy_manager, sender=management + ) + + tx = strategy_manager.removeManagement(mock_tokenized, user, sender=management) + + event = list(tx.decode_logs(strategy_manager.StrategyRemoved))[0] + + assert event.strategy == mock_tokenized + assert event.newManager == user + + assert strategy_manager.strategyInfo(mock_tokenized).active == False + assert strategy_manager.strategyInfo(mock_tokenized).owner == ZERO_ADDRESS + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == ZERO_ADDRESS + + assert mock_tokenized.management() == strategy_manager + assert mock_tokenized.pendingManagement() == user + mock_tokenized.acceptManagement(sender=user) + assert mock_tokenized.management() == user + + +def test_update_owner( + strategy_manager, mock_tokenized, yield_manager, management, user +): + with ape.reverts("!owner"): + strategy_manager.updateStrategyOwner(mock_tokenized, user, sender=management) + + mock_tokenized.setPendingManagement(strategy_manager, sender=management) + + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + + assert mock_tokenized.management() == strategy_manager + assert strategy_manager.strategyInfo(mock_tokenized).active == True + assert strategy_manager.strategyInfo(mock_tokenized).owner == management + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + + with ape.reverts("!owner"): + strategy_manager.updateStrategyOwner(mock_tokenized, user, sender=user) + + with ape.reverts("bad address"): + strategy_manager.updateStrategyOwner( + mock_tokenized, ZERO_ADDRESS, sender=management + ) + + with ape.reverts("bad address"): + strategy_manager.updateStrategyOwner( + mock_tokenized, mock_tokenized, sender=management + ) + + with ape.reverts("bad address"): + strategy_manager.updateStrategyOwner( + mock_tokenized, strategy_manager, sender=management + ) + + strategy_manager.updateStrategyOwner(mock_tokenized, user, sender=management) + + assert strategy_manager.strategyInfo(mock_tokenized).active == True + assert strategy_manager.strategyInfo(mock_tokenized).owner == user + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + assert mock_tokenized.management() == strategy_manager + + +def test_update_debt_manager( + strategy_manager, mock_tokenized, yield_manager, management, user +): + mock_tokenized.setPendingManagement(strategy_manager, sender=management) + + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + + assert mock_tokenized.management() == strategy_manager + assert strategy_manager.strategyInfo(mock_tokenized).active == True + assert strategy_manager.strategyInfo(mock_tokenized).owner == management + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + + with ape.reverts("!owner"): + strategy_manager.updateDebtManager(mock_tokenized, user, sender=user) + + strategy_manager.updateDebtManager(mock_tokenized, user, sender=management) + + assert strategy_manager.strategyInfo(mock_tokenized).active == True + assert strategy_manager.strategyInfo(mock_tokenized).owner == management + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == user + assert mock_tokenized.management() == strategy_manager + + +def test_record_full_profit( + strategy_manager, mock_tokenized, management, yield_manager, asset, user +): + mock_tokenized.setProfitMaxUnlockTime(int(60 * 60 * 24), sender=management) + mock_tokenized.setPendingManagement(strategy_manager, sender=management) + + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + + assert mock_tokenized.management() == strategy_manager + assert strategy_manager.strategyInfo(mock_tokenized).active == True + assert strategy_manager.strategyInfo(mock_tokenized).owner == management + assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + + # deposit into the strategy + to_deposit = asset.balanceOf(user) // 2 + profit = asset.balanceOf(user) - to_deposit + + asset.approve(mock_tokenized, to_deposit, sender=user) + mock_tokenized.deposit(to_deposit, user, sender=user) + + assert mock_tokenized.totalAssets() == to_deposit + assert mock_tokenized.totalSupply() == to_deposit + + # simulate profit + asset.transfer(mock_tokenized, profit, sender=user) + + assert mock_tokenized.totalAssets() == to_deposit + assert mock_tokenized.totalSupply() == to_deposit + current_unlock_time = mock_tokenized.profitMaxUnlockTime() + assert current_unlock_time != 0 + assert mock_tokenized.pricePerShare() == 10 ** asset.decimals() + + with ape.reverts("!debt manager"): + strategy_manager.reportFullProfit(mock_tokenized, sender=user) + + strategy_manager.reportFullProfit(mock_tokenized, sender=yield_manager) + + # Profit should be fully unlocked + assert mock_tokenized.totalAssets() == to_deposit + profit + assert mock_tokenized.totalSupply() == to_deposit + assert current_unlock_time == mock_tokenized.profitMaxUnlockTime() + assert mock_tokenized.pricePerShare() > 10 ** asset.decimals() + + +def test_forward_call( + strategy_manager, mock_tokenized, management, yield_manager, asset, user +): + mock_tokenized.setPendingManagement(strategy_manager, sender=management) + + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + + assert mock_tokenized.profitMaxUnlockTime() == 0 + assert mock_tokenized.performanceFee() == 0 + + # Yield manager can change profit max unlock + new_unlock_time = int(69) + calldata = mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)) + + with ape.reverts("!debt manager"): + strategy_manager.forwardCall(mock_tokenized, calldata, sender=user) + + tx = strategy_manager.forwardCall(mock_tokenized, calldata, sender=management) + + event = list(tx.decode_logs(mock_tokenized.UpdateProfitMaxUnlockTime))[0] + assert event.newProfitMaxUnlockTime == new_unlock_time + assert mock_tokenized.profitMaxUnlockTime() == new_unlock_time + + new_unlock_time = int(6699) + calldata = mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)) + + tx = strategy_manager.forwardCall(mock_tokenized, calldata, sender=yield_manager) + + event = list(tx.decode_logs(mock_tokenized.UpdateProfitMaxUnlockTime))[0] + assert event.newProfitMaxUnlockTime == new_unlock_time + assert mock_tokenized.profitMaxUnlockTime() == new_unlock_time + + # Only management can change a performance fee. + new_fee = int(2_000) + calldata = mock_tokenized.setPerformanceFee.encode_input(new_fee) + + with ape.reverts("!owner"): + strategy_manager.forwardCall(mock_tokenized, calldata, sender=user) + + with ape.reverts("!owner"): + strategy_manager.forwardCall(mock_tokenized, calldata, sender=yield_manager) + + tx = strategy_manager.forwardCall(mock_tokenized, calldata, sender=management) + + event = list(tx.decode_logs(mock_tokenized.UpdatePerformanceFee))[0] + assert event.newPerformanceFee == new_fee + assert mock_tokenized.performanceFee() == new_fee + + # We get the correct return data + new_unlock_time = int(1e25) + calldata = mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)) + + with ape.reverts("too long"): + strategy_manager.forwardCall(mock_tokenized, calldata, sender=yield_manager) + + +def test_forward_calls( + strategy_manager, mock_tokenized, management, yield_manager, asset, user +): + mock_tokenized.setPendingManagement(strategy_manager, sender=management) + + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + + assert mock_tokenized.profitMaxUnlockTime() == 0 + assert mock_tokenized.performanceFee() == 0 + + # Yield manager can change profit max unlock + new_unlock_time = int(69) + new_fee = int(2_000) + calldatas = [ + mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)), + mock_tokenized.setPerformanceFee.encode_input(new_fee), + ] + + # Only management can change a performance fee. + with ape.reverts("!debt manager"): + strategy_manager.forwardCalls(mock_tokenized, calldatas, sender=user) + + with ape.reverts("!owner"): + strategy_manager.forwardCalls(mock_tokenized, calldatas, sender=yield_manager) + + tx = strategy_manager.forwardCalls(mock_tokenized, calldatas, sender=management) + + event = list(tx.decode_logs(mock_tokenized.UpdatePerformanceFee))[0] + assert event.newPerformanceFee == new_fee + assert mock_tokenized.performanceFee() == new_fee + + event = list(tx.decode_logs(mock_tokenized.UpdateProfitMaxUnlockTime))[0] + assert event.newProfitMaxUnlockTime == new_unlock_time + assert mock_tokenized.profitMaxUnlockTime() == new_unlock_time diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py new file mode 100644 index 0000000..06d6f41 --- /dev/null +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -0,0 +1,546 @@ +import ape +from ape import chain, project, networks +from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES + + +def setup_vault(vault, strategies, oracle, chad): + for strategy in strategies: + vault.add_strategy(strategy, sender=chad) + vault.update_max_debt_for_strategy(strategy, 2**256 - 1, sender=chad) + management = strategy.management() + oracle.setOracle(strategy, strategy, sender=management) + + +def test_yield_manager_setup(yield_manager, daddy, management, strategy_manager): + assert yield_manager.strategyManager() == strategy_manager + assert yield_manager.governance() == daddy + assert yield_manager.open() == False + assert yield_manager.maxDebtUpdateLoss() == 1 + assert yield_manager.allocators(management) == False + + +def test_setters(yield_manager, daddy, management): + assert yield_manager.allocators(management) == False + assert yield_manager.open() == False + assert yield_manager.maxDebtUpdateLoss() == 1 + + with ape.reverts("!governance"): + yield_manager.setAllocator(management, True, sender=management) + + yield_manager.setAllocator(management, True, sender=daddy) + + assert yield_manager.allocators(management) == True + + yield_manager.setAllocator(management, False, sender=daddy) + + assert yield_manager.allocators(management) == False + + loss = int(8) + with ape.reverts("!governance"): + yield_manager.setMaxDebtUpdateLoss(loss, sender=management) + + yield_manager.setMaxDebtUpdateLoss(loss, sender=daddy) + + assert yield_manager.maxDebtUpdateLoss() == loss + + with ape.reverts("!governance"): + yield_manager.setOpen(True, sender=management) + + yield_manager.setOpen(True, sender=daddy) + + assert yield_manager.open() + + +def test_update_allocation( + apr_oracle, + yield_manager, + vault, + management, + strategy_manager, + daddy, + user, + amount, + asset, + deploy_mock_tokenized, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + # Can just pass in one at to allocate + allocation = [(strategy_two, amount)] + + with ape.reverts("!allocator or open"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + yield_manager.setAllocator(user, True, sender=daddy) + + # Must give allocator the debt role + with ape.reverts("not allowed"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + vault.set_role( + yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy + ) + + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + (before, now) = tx.return_value + + assert before == 0 + # assert now == int(1e17 * amount) + assert vault.totalIdle() == 0 + assert vault.totalDebt() == amount + assert vault.strategies(strategy_two).current_debt == amount + + allocation = [] + with ape.reverts("cheater"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + allocation = [(strategy_one, amount)] + with ape.reverts("no funds to deposit"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + allocation = [(strategy_two, amount // 2)] + with ape.reverts("fail"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + # strategy one is now earning more + strategy_one.setApr(int(1.5e17), sender=management) + + # only move part + to_move = amount // 2 + # will revert if in the wrong order + allocation = [(strategy_one, to_move), (strategy_two, amount - to_move)] + with ape.reverts("no funds to deposit"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + allocation = [(strategy_two, amount - to_move), (strategy_one, to_move)] + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == amount + assert vault.totalAssets() == amount + assert vault.strategies(strategy_one).current_debt == to_move + assert vault.strategies(strategy_two).current_debt == amount - to_move + + # Try and move all + allocation = [(strategy_two, 0), (strategy_one, amount)] + # Strategy manager isnt the strategies management + with ape.reverts("!debt manager"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + strategy_two.setPendingManagement(strategy_manager, sender=management) + strategy_two.setProfitMaxUnlockTime(int(200), sender=management) + strategy_manager.manageNewStrategy(strategy_two, yield_manager, sender=management) + + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 + assert len(list(tx.decode_logs(strategy_two.UpdateProfitMaxUnlockTime))) == 2 + assert len(list(tx.decode_logs(strategy_two.Reported))) == 1 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == amount + assert vault.totalAssets() == amount + assert vault.strategies(strategy_one).current_debt == amount + assert vault.strategies(strategy_two).current_debt == 0 + + # Try and move all them all back + allocation = [(strategy_one, 1), (strategy_two, amount)] + with ape.reverts("fail"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + +def test_update_allocation_pending_profit( + apr_oracle, + yield_manager, + vault, + management, + strategy_manager, + daddy, + user, + amount, + asset, + deploy_mock_tokenized, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + vault.add_role( + yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy + ) + strategy_one.setPendingManagement(strategy_manager, sender=management) + strategy_one.setProfitMaxUnlockTime(int(200), sender=management) + strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=management) + yield_manager.setAllocator(user, True, sender=daddy) + + profit = amount // 10 + amount = amount - profit + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + vault.update_debt(strategy_one, amount, sender=daddy) + + assert vault.totalAssets() == amount + assert vault.totalDebt() == amount + assert vault.strategies(strategy_one).current_debt == amount + + asset.transfer(strategy_one, profit, sender=user) + + allocation = [(strategy_one, 0), (strategy_two, amount + profit)] + + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 + assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 + assert len(list(tx.decode_logs(strategy_one.Reported))) == 1 + assert len(list(tx.decode_logs(strategy_two.UpdateProfitMaxUnlockTime))) == 2 + assert vault.totalAssets() == amount + profit + assert vault.totalDebt() == amount + profit + assert vault.strategies(strategy_one).current_debt == 0 + assert vault.strategies(strategy_two).current_debt == amount + profit + assert strategy_one.balanceOf(vault) == 0 + + +def test_update_allocation_pending_loss( + apr_oracle, + yield_manager, + vault, + management, + strategy_manager, + daddy, + user, + amount, + asset, + deploy_mock_tokenized, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + vault.add_role( + yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy + ) + strategy_one.setPendingManagement(strategy_manager, sender=management) + strategy_one.setProfitMaxUnlockTime(int(200), sender=management) + strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=management) + yield_manager.setAllocator(user, True, sender=daddy) + + loss = amount // 10 + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + vault.update_debt(strategy_one, amount, sender=daddy) + + assert vault.totalAssets() == amount + assert vault.totalDebt() == amount + assert vault.strategies(strategy_one).current_debt == amount + + asset.transfer(user, loss, sender=strategy_one) + + allocation = [(strategy_one, 0), (strategy_two, amount)] + + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 + assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 + assert vault.totalAssets() == amount - loss + assert vault.totalDebt() == amount - loss + assert vault.strategies(strategy_one).current_debt == 0 + assert vault.strategies(strategy_two).current_debt == amount - loss + assert strategy_one.balanceOf(vault) == 0 + + +def test_update_allocation_pending_loss_move_half( + apr_oracle, + yield_manager, + vault, + management, + strategy_manager, + daddy, + user, + keeper, + amount, + asset, + deploy_mock_tokenized, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + vault.add_role( + yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy + ) + strategy_one.setPendingManagement(strategy_manager, sender=management) + strategy_one.setProfitMaxUnlockTime(int(200), sender=management) + strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=management) + yield_manager.setAllocator(user, True, sender=daddy) + + loss = amount // 10 + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + vault.update_debt(strategy_one, amount, sender=daddy) + + assert vault.totalAssets() == amount + assert vault.totalDebt() == amount + assert vault.strategies(strategy_one).current_debt == amount + + # Record strategy loss + asset.transfer(user, loss, sender=strategy_one) + strategy_one.report(sender=keeper) + + to_move = amount // 2 + allocation = [(strategy_one, amount - to_move), (strategy_two, amount)] + + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 + assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 + assert vault.totalAssets() == amount - loss + assert vault.totalDebt() == amount - loss + assert vault.strategies(strategy_one).current_debt == amount - to_move + assert vault.strategies(strategy_two).current_debt == to_move - loss + assert strategy_one.balanceOf(vault) != 0 + + +def test_update_allocation_loss_on_withdraw( + apr_oracle, + yield_manager, + vault, + management, + strategy_manager, + daddy, + user, + keeper, + amount, + asset, + deploy_mock_tokenized, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + vault.add_role( + yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy + ) + strategy_one.setPendingManagement(strategy_manager, sender=management) + strategy_one.setProfitMaxUnlockTime(int(200), sender=management) + strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=management) + yield_manager.setAllocator(user, True, sender=daddy) + + loss = amount // 10 + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + vault.update_debt(strategy_one, amount, sender=daddy) + + assert vault.totalAssets() == amount + assert vault.totalDebt() == amount + assert vault.strategies(strategy_one).current_debt == amount + + # simulate strategy loss + strategy_one.realizeLoss(loss, sender=daddy) + + allocation = [(strategy_one, 1), (strategy_two, amount)] + + with ape.reverts("too much loss"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + yield_manager.setMaxDebtUpdateLoss(1_000, sender=daddy) + + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 + assert len(list(tx.decode_logs(vault.StrategyReported))) == 0 + assert vault.totalAssets() == amount - loss + 1 + assert vault.totalDebt() == amount - loss + 1 + assert vault.strategies(strategy_one).current_debt == 1 + assert vault.strategies(strategy_two).current_debt == amount - loss + + +def test_validate_allocation( + apr_oracle, yield_manager, vault, daddy, user, amount, asset, deploy_mock_tokenized +): + strategy_one = deploy_mock_tokenized("One") + strategy_two = deploy_mock_tokenized("two") + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + # Can validate the allocation with no strategies when all is idle + assert vault.totalIdle() == amount + + assert yield_manager.validateAllocation(vault, []) + assert yield_manager.validateAllocation(vault, [(strategy_one, amount)]) + assert yield_manager.validateAllocation( + vault, [(strategy_one, amount), (strategy_two, 0)] + ) + + vault.update_debt(strategy_one, amount // 2, sender=daddy) + + assert yield_manager.validateAllocation(vault, []) == False + assert yield_manager.validateAllocation(vault, [(strategy_one, amount)]) + assert yield_manager.validateAllocation( + vault, [(strategy_one, amount), (strategy_two, 0)] + ) + + # Now will be false + vault.update_debt(strategy_two, vault.totalIdle() // 2, sender=daddy) + + assert yield_manager.validateAllocation(vault, []) == False + assert yield_manager.validateAllocation(vault, [(strategy_one, amount)]) == False + assert yield_manager.validateAllocation( + vault, [(strategy_one, amount), (strategy_two, 0)] + ) + + +def test_get_current_and_expected( + apr_oracle, + yield_manager, + vault, + management, + strategy_manager, + daddy, + user, + keeper, + amount, + asset, + deploy_mock_tokenized, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + allocation = [] + (current, expected) = yield_manager.getCurrentAndExpectedYield(vault, allocation) + + assert current == 0 + assert expected == 0 + + allocation = [(strategy_one, 0), (strategy_two, 0)] + (current, expected) = yield_manager.getCurrentAndExpectedYield(vault, allocation) + assert current == 0 + assert expected == 0 + + allocation = [(strategy_one, amount), (strategy_two, 0)] + (current, expected) = yield_manager.getCurrentAndExpectedYield(vault, allocation) + assert current == 0 + assert expected != 0 + + vault.update_debt(strategy_one, amount, sender=daddy) + + allocation = [(strategy_one, 0), (strategy_two, amount)] + (current, new_expected) = yield_manager.getCurrentAndExpectedYield( + vault, allocation + ) + assert current == expected + assert expected != 0 + assert expected > 0 + + assert vault.totalAssets() == amount + assert vault.totalDebt() == amount + assert vault.strategies(strategy_one).current_debt == amount + + +def test_update_allocation_permissioned( + apr_oracle, + yield_manager, + vault, + management, + strategy_manager, + daddy, + user, + amount, + asset, + deploy_mock_tokenized, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + vault.set_role( + yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy + ) + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + # Can just pass in one at to allocate + allocation = [(strategy_two, amount)] + + with ape.reverts("!governance"): + yield_manager.updateAllocationPermissioned(vault, allocation, sender=user) + + yield_manager.setAllocator(user, True, sender=daddy) + + # Still cant allocate even with allocator role + with ape.reverts("!governance"): + yield_manager.updateAllocationPermissioned(vault, allocation, sender=user) + + tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) + + # assert now == int(1e17 * amount) + assert vault.totalIdle() == 0 + assert vault.totalDebt() == amount + assert vault.strategies(strategy_two).current_debt == amount + + allocation = [(strategy_one, amount)] + with ape.reverts("no funds to deposit"): + yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) + + # strategy one is now earning more + strategy_one.setApr(int(1.5e17), sender=management) + + # only move part + to_move = amount // 2 + # will revert if in the wrong order + allocation = [(strategy_one, to_move), (strategy_two, amount - to_move)] + with ape.reverts("no funds to deposit"): + yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) + + allocation = [(strategy_two, amount - to_move), (strategy_one, to_move)] + tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) + + assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == amount + assert vault.totalAssets() == amount + assert vault.strategies(strategy_one).current_debt == to_move + assert vault.strategies(strategy_two).current_debt == amount - to_move + + # Try and move all + allocation = [(strategy_two, 0), (strategy_one, amount)] + # Strategy manager isnt the strategies management + with ape.reverts("!debt manager"): + yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) + + strategy_two.setPendingManagement(strategy_manager, sender=management) + strategy_two.setProfitMaxUnlockTime(int(200), sender=management) + strategy_manager.manageNewStrategy(strategy_two, yield_manager, sender=management) + + tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) + + assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 + assert len(list(tx.decode_logs(strategy_two.UpdateProfitMaxUnlockTime))) == 2 + assert len(list(tx.decode_logs(strategy_two.Reported))) == 1 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == amount + assert vault.totalAssets() == amount + assert vault.strategies(strategy_one).current_debt == amount + assert vault.strategies(strategy_two).current_debt == 0 From f51b9e2276eca2d0c5507b6b2ea8d24bb96b990b Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 12 Dec 2023 10:19:08 -0700 Subject: [PATCH 05/35] feat: add events --- .../YieldManager/YieldManager.sol | 10 +++++++++ .../yield/test_yield_manager.py | 22 +++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 9635f3d..c6a755e 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -16,6 +16,12 @@ import {StrategyManager} from "./StrategyManager.sol"; * a Yearn V3 vault to allocate funds to the optimal strategy. */ contract YieldManager is Governance { + /// @notice Emitted when a allocators status is updated. + event UpdateAllocator(address indexed allocator, bool status); + + /// @notice Emitted when the open flag is updated. + event UpdateOpen(bool status); + /// @notice An event emitted when the max debt update loss is updated. event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); @@ -391,6 +397,8 @@ contract YieldManager is Governance { bool _allowed ) external onlyGovernance { allocators[_address] = _allowed; + + emit UpdateAllocator(_address, _allowed); } /** @@ -399,6 +407,8 @@ contract YieldManager is Governance { */ function setOpen(bool _open) external onlyGovernance { open = _open; + + emit UpdateOpen(_open); } /** diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 06d6f41..c7e4715 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -27,27 +27,41 @@ def test_setters(yield_manager, daddy, management): with ape.reverts("!governance"): yield_manager.setAllocator(management, True, sender=management) - yield_manager.setAllocator(management, True, sender=daddy) + tx = yield_manager.setAllocator(management, True, sender=daddy) + event = list(tx.decode_logs(yield_manager.UpdateAllocator))[0] + + assert event.allocator == management + assert event.status == True assert yield_manager.allocators(management) == True - yield_manager.setAllocator(management, False, sender=daddy) + tx = yield_manager.setAllocator(management, False, sender=daddy) + + event = list(tx.decode_logs(yield_manager.UpdateAllocator))[0] + assert event.allocator == management + assert event.status == False assert yield_manager.allocators(management) == False loss = int(8) with ape.reverts("!governance"): yield_manager.setMaxDebtUpdateLoss(loss, sender=management) - yield_manager.setMaxDebtUpdateLoss(loss, sender=daddy) + tx = yield_manager.setMaxDebtUpdateLoss(loss, sender=daddy) + event = list(tx.decode_logs(yield_manager.UpdateMaxDebtUpdateLoss))[0] + + assert event.newMaxDebtUpdateLoss == loss assert yield_manager.maxDebtUpdateLoss() == loss with ape.reverts("!governance"): yield_manager.setOpen(True, sender=management) - yield_manager.setOpen(True, sender=daddy) + tx = yield_manager.setOpen(True, sender=daddy) + + event = list(tx.decode_logs(yield_manager.UpdateOpen))[0] + assert event.status == True assert yield_manager.open() From 71db754b6ca9f6e807e0d1bec769063c77f71633 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Wed, 13 Dec 2023 08:56:53 -0700 Subject: [PATCH 06/35] fix: remove generic call --- .../YieldManager/StrategyManager.sol | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol index 8fc6129..e323ef7 100644 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -233,32 +233,4 @@ contract StrategyManager { // Return the result. return result; } - - /** - * @notice Calls any target contract as the manager of the strategy. - * @param _strategy The address of the strategy. - * @param _target The address of the target contract. - * @param _calldata The calldata for the call. - * @return result The return data from the call. - */ - function genericCall( - address _strategy, - address _target, - bytes memory _calldata - ) external virtual onlyStrategyOwner(_strategy) returns (bytes memory) { - (bool success, bytes memory result) = _target.call(_calldata); - - // If the call reverted. Return the error. - if (!success) { - assembly { - let ptr := mload(0x40) - let size := returndatasize() - returndatacopy(ptr, 0, size) - revert(ptr, size) - } - } - - // Return the result. - return result; - } } From 56c9148f1cab81b7f9477e8026c14bcfdb76a6c7 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Wed, 13 Dec 2023 17:25:13 -0700 Subject: [PATCH 07/35] fix: permissions --- .../YieldManager/StrategyManager.sol | 31 +++++++++--- .../YieldManager/YieldManager.sol | 35 ++++++++++---- tests/conftest.py | 2 +- .../yield/test_strategy_manager.py | 41 ++++++++-------- .../yield/test_yield_manager.py | 47 +++++++++++++++---- 5 files changed, 110 insertions(+), 46 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol index e323ef7..aa39a39 100644 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.18; +import {Governance} from "@periphery/utils/Governance.sol"; import {IStrategy} from "@tokenized-strategy/interfaces/IStrategy.sol"; /// @notice Holds the `management` role of a V3 strategy so that a /// debt allocator can call both reports and change the profit unlock time. -contract StrategyManager { +contract StrategyManager is Governance { /// @notice Emitted when a new strategy is added to the manager. event StrategyAdded( address indexed strategy, @@ -61,14 +62,17 @@ contract StrategyManager { * @notice Add any of the allowed selectors for a debt manager to call * to the mapping. */ - constructor(bytes4[] memory _allowedSelectors) { + constructor( + address _governance, + bytes4[] memory _allowedSelectors + ) Governance(_governance) { for (uint256 i = 0; i < _allowedSelectors.length; ++i) { allowedSelectors[_allowedSelectors[i]] = true; } } /** - * @notice Manage a new strategy, setting the debt manager and marking it as active. + * @notice Add a new strategy, using the current `management` as the owner. * @param _strategy The address of the strategy. * @param _debtManager The address of the debt manager. */ @@ -76,9 +80,22 @@ contract StrategyManager { address _strategy, address _debtManager ) external { - require(!strategyInfo[_strategy].active, "already active"); - // Cache the current strategy management. address currentManager = IStrategy(_strategy).management(); + manageNewStrategy(_strategy, _debtManager, currentManager); + } + + /** + * @notice Manage a new strategy, setting the debt manager and marking it as active. + * @param _strategy The address of the strategy. + * @param _debtManager The address of the debt manager. + * @param _owner The address in charge of the strategy now. + */ + function manageNewStrategy( + address _strategy, + address _debtManager, + address _owner + ) public onlyGovernance { + require(!strategyInfo[_strategy].active, "already active"); // Accept management of the strategy. IStrategy(_strategy).acceptManagement(); @@ -86,11 +103,11 @@ contract StrategyManager { // Store the owner of the strategy. strategyInfo[_strategy] = StrategyInfo({ active: true, - owner: currentManager, + owner: _owner, debtManager: _debtManager }); - emit StrategyAdded(_strategy, currentManager, _debtManager); + emit StrategyAdded(_strategy, _owner, _debtManager); } /** diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index c6a755e..4cc85b2 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -4,9 +4,7 @@ pragma solidity 0.8.18; import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; import {AprOracle} from "@periphery/AprOracle/AprOracle.sol"; -import {Governance} from "@periphery/utils/Governance.sol"; - -import {StrategyManager} from "./StrategyManager.sol"; +import {StrategyManager, Governance} from "./StrategyManager.sol"; /** * @title YearnV3 Yield Yield Based Debt Allocator @@ -16,12 +14,15 @@ import {StrategyManager} from "./StrategyManager.sol"; * a Yearn V3 vault to allocate funds to the optimal strategy. */ contract YieldManager is Governance { - /// @notice Emitted when a allocators status is updated. - event UpdateAllocator(address indexed allocator, bool status); - /// @notice Emitted when the open flag is updated. event UpdateOpen(bool status); + /// @notice Emitted when a vaults status is updated. + event UpdateVault(address indexed vault, bool status); + + /// @notice Emitted when a allocators status is updated. + event UpdateAllocator(address indexed allocator, bool status); + /// @notice An event emitted when the max debt update loss is updated. event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); @@ -42,7 +43,7 @@ contract YieldManager is Governance { /// @notice Check if it has been opened or is an allocator. function _isAllocatorOrOpen() internal view { - require(allocators[msg.sender] || open, "!allocator or open"); + require(open || allocators[msg.sender], "!allocator or open"); } uint256 internal constant MAX_BPS = 10_000; @@ -60,6 +61,9 @@ contract YieldManager is Governance { /// @notice Address that should hold the strategies `management` role. address public immutable strategyManager; + /// @notice Mapping for vaults that can be allocated for. + mapping(address => bool) public vaults; + /// @notice Addresses that are allowed to propose allocations. mapping(address => bool) public allocators; @@ -122,6 +126,7 @@ contract YieldManager is Governance { onlyAllocatorsOrOpen returns (uint256 _currentYield, uint256 _afterYield) { + require(vaults[_vault], "vault not added"); // Get the total assets the vault has. uint256 _totalAssets = IVault(_vault).totalAssets(); @@ -214,9 +219,8 @@ contract YieldManager is Governance { // Get the new APR if (_newDebt != 0) { - _afterYield += - aprOracle.getStrategyApr(_strategy, 0) * - _newDebt; + _afterYield += (aprOracle.getStrategyApr(_strategy, 0) * + _newDebt); } } @@ -401,6 +405,17 @@ contract YieldManager is Governance { emit UpdateAllocator(_address, _allowed); } + /** + * @notice Sets the mapping of vaults allowed. + * @param _vault The address of the _vault. + * @param _allowed The permission to set for the _vault. + */ + function setVault(address _vault, bool _allowed) external onlyGovernance { + vaults[_vault] = _allowed; + + emit UpdateVault(_vault, _allowed); + } + /** * @notice Sets the open status of the contract. * @param _open The new open status to set. diff --git a/tests/conftest.py b/tests/conftest.py index d59fd2c..5043549 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -469,7 +469,7 @@ def role_manager( def deploy_strategy_manager(project, daddy): def deploy_strategy_manager(): strategy_manager = daddy.deploy( - project.StrategyManager, ["0x2606a10b", "0xdf69b22a"] + project.StrategyManager, daddy, ["0x2606a10b", "0xdf69b22a"] ) return strategy_manager diff --git a/tests/debtAllocators/yield/test_strategy_manager.py b/tests/debtAllocators/yield/test_strategy_manager.py index a0dbfe8..d970238 100644 --- a/tests/debtAllocators/yield/test_strategy_manager.py +++ b/tests/debtAllocators/yield/test_strategy_manager.py @@ -15,21 +15,24 @@ def test_strategy_manager_setup(strategy_manager, mock_tokenized): assert strategy_manager.allowedSelectors("0x9bbefdb6") == False -def test_add_new_strategy(strategy_manager, mock_tokenized, yield_manager, management): +def test_add_new_strategy( + strategy_manager, mock_tokenized, daddy, yield_manager, management +): assert strategy_manager.strategyInfo(mock_tokenized).active == False assert strategy_manager.strategyInfo(mock_tokenized).owner == ZERO_ADDRESS assert strategy_manager.strategyInfo(mock_tokenized).debtManager == ZERO_ADDRESS - with ape.reverts("!pending"): + with ape.reverts("!governance"): strategy_manager.manageNewStrategy( mock_tokenized, yield_manager, sender=management ) + with ape.reverts("!pending"): + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) + mock_tokenized.setPendingManagement(strategy_manager, sender=management) - tx = strategy_manager.manageNewStrategy( - mock_tokenized, yield_manager, sender=management - ) + tx = strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) assert mock_tokenized.management() == strategy_manager @@ -45,13 +48,11 @@ def test_add_new_strategy(strategy_manager, mock_tokenized, yield_manager, manag # cannot add it again with ape.reverts("already active"): - strategy_manager.manageNewStrategy( - mock_tokenized, yield_manager, sender=management - ) + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) def test_remove_strategy( - strategy_manager, mock_tokenized, yield_manager, management, user + strategy_manager, mock_tokenized, yield_manager, management, user, daddy ): # Will revert on modifier if not yet added. with ape.reverts("!owner"): @@ -59,7 +60,7 @@ def test_remove_strategy( mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) assert mock_tokenized.management() == strategy_manager assert strategy_manager.strategyInfo(mock_tokenized).active == True @@ -105,14 +106,14 @@ def test_remove_strategy( def test_update_owner( - strategy_manager, mock_tokenized, yield_manager, management, user + strategy_manager, mock_tokenized, yield_manager, management, user, daddy ): with ape.reverts("!owner"): strategy_manager.updateStrategyOwner(mock_tokenized, user, sender=management) mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) assert mock_tokenized.management() == strategy_manager assert strategy_manager.strategyInfo(mock_tokenized).active == True @@ -146,11 +147,11 @@ def test_update_owner( def test_update_debt_manager( - strategy_manager, mock_tokenized, yield_manager, management, user + strategy_manager, mock_tokenized, yield_manager, management, user, daddy ): mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) assert mock_tokenized.management() == strategy_manager assert strategy_manager.strategyInfo(mock_tokenized).active == True @@ -169,12 +170,12 @@ def test_update_debt_manager( def test_record_full_profit( - strategy_manager, mock_tokenized, management, yield_manager, asset, user + strategy_manager, mock_tokenized, management, yield_manager, asset, user, daddy ): mock_tokenized.setProfitMaxUnlockTime(int(60 * 60 * 24), sender=management) mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) assert mock_tokenized.management() == strategy_manager assert strategy_manager.strategyInfo(mock_tokenized).active == True @@ -213,11 +214,11 @@ def test_record_full_profit( def test_forward_call( - strategy_manager, mock_tokenized, management, yield_manager, asset, user + strategy_manager, mock_tokenized, management, yield_manager, asset, user, daddy ): mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) assert mock_tokenized.profitMaxUnlockTime() == 0 assert mock_tokenized.performanceFee() == 0 @@ -269,11 +270,11 @@ def test_forward_call( def test_forward_calls( - strategy_manager, mock_tokenized, management, yield_manager, asset, user + strategy_manager, mock_tokenized, management, yield_manager, daddy, user ): mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=management) + strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) assert mock_tokenized.profitMaxUnlockTime() == 0 assert mock_tokenized.performanceFee() == 0 diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index c7e4715..3207916 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -11,19 +11,40 @@ def setup_vault(vault, strategies, oracle, chad): oracle.setOracle(strategy, strategy, sender=management) -def test_yield_manager_setup(yield_manager, daddy, management, strategy_manager): +def test_yield_manager_setup(yield_manager, daddy, vault, management, strategy_manager): assert yield_manager.strategyManager() == strategy_manager assert yield_manager.governance() == daddy assert yield_manager.open() == False assert yield_manager.maxDebtUpdateLoss() == 1 assert yield_manager.allocators(management) == False + assert yield_manager.vaults(vault) == False -def test_setters(yield_manager, daddy, management): +def test_setters(yield_manager, daddy, vault, management): + assert yield_manager.vaults(vault) == False assert yield_manager.allocators(management) == False assert yield_manager.open() == False assert yield_manager.maxDebtUpdateLoss() == 1 + with ape.reverts("!governance"): + yield_manager.setVault(vault, True, sender=management) + + tx = yield_manager.setVault(vault, True, sender=daddy) + + event = list(tx.decode_logs(yield_manager.UpdateVault))[0] + + assert event.vault == vault + assert event.status == True + assert yield_manager.vaults(vault) == True + + tx = yield_manager.setVault(vault, False, sender=daddy) + + event = list(tx.decode_logs(yield_manager.UpdateVault))[0] + + assert event.vault == vault + assert event.status == False + assert yield_manager.vaults(management) == False + with ape.reverts("!governance"): yield_manager.setAllocator(management, True, sender=management) @@ -93,6 +114,11 @@ def test_update_allocation( yield_manager.setAllocator(user, True, sender=daddy) + with ape.reverts("vault not added"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + yield_manager.setVault(vault, True, sender=daddy) + # Must give allocator the debt role with ape.reverts("not allowed"): yield_manager.updateAllocation(vault, allocation, sender=user) @@ -151,7 +177,7 @@ def test_update_allocation( strategy_two.setPendingManagement(strategy_manager, sender=management) strategy_two.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_two, yield_manager, sender=management) + strategy_manager.manageNewStrategy(strategy_two, yield_manager, sender=daddy) tx = yield_manager.updateAllocation(vault, allocation, sender=user) @@ -186,12 +212,13 @@ def test_update_allocation_pending_profit( strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVault(vault, True, sender=daddy) vault.add_role( yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy ) strategy_one.setPendingManagement(strategy_manager, sender=management) strategy_one.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=management) + strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) yield_manager.setAllocator(user, True, sender=daddy) profit = amount // 10 @@ -239,12 +266,13 @@ def test_update_allocation_pending_loss( strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVault(vault, True, sender=daddy) vault.add_role( yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy ) strategy_one.setPendingManagement(strategy_manager, sender=management) strategy_one.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=management) + strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) yield_manager.setAllocator(user, True, sender=daddy) loss = amount // 10 @@ -290,12 +318,13 @@ def test_update_allocation_pending_loss_move_half( strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVault(vault, True, sender=daddy) vault.add_role( yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy ) strategy_one.setPendingManagement(strategy_manager, sender=management) strategy_one.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=management) + strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) yield_manager.setAllocator(user, True, sender=daddy) loss = amount // 10 @@ -344,12 +373,13 @@ def test_update_allocation_loss_on_withdraw( strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVault(vault, True, sender=daddy) vault.add_role( yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy ) strategy_one.setPendingManagement(strategy_manager, sender=management) strategy_one.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=management) + strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) yield_manager.setAllocator(user, True, sender=daddy) loss = amount // 10 @@ -389,6 +419,7 @@ def test_validate_allocation( strategy_one = deploy_mock_tokenized("One") strategy_two = deploy_mock_tokenized("two") setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVault(vault, True, sender=daddy) asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) @@ -546,7 +577,7 @@ def test_update_allocation_permissioned( strategy_two.setPendingManagement(strategy_manager, sender=management) strategy_two.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_two, yield_manager, sender=management) + strategy_manager.manageNewStrategy(strategy_two, yield_manager, sender=daddy) tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) From 2a7b1e9126cdbf2286c69cfcc4f0090f523443ce Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 14 Dec 2023 19:08:24 -0700 Subject: [PATCH 08/35] fix: deployments --- .../YieldManager/YieldManager.sol | 5 +-- tests/conftest.py | 32 ++++++------------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 4cc85b2..c5722a9 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -69,9 +69,10 @@ contract YieldManager is Governance { constructor( address _governance, - address _strategyManager + bytes4[] memory _selectors ) Governance(_governance) { - strategyManager = _strategyManager; + // Deploy a new strategy manager + strategyManager = address(new StrategyManager(_governance, _selectors)); // Default to 1 BP loss maxDebtUpdateLoss = 1; } diff --git a/tests/conftest.py b/tests/conftest.py index 5043549..db66500 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,14 +26,8 @@ def security(accounts): @pytest.fixture(scope="session") -<<<<<<< HEAD def management(accounts): yield accounts[3] -======= -def keeper(accounts): - yield accounts[4] ->>>>>>> test: yield manager - @pytest.fixture(scope="session") def fee_recipient(accounts): @@ -54,10 +48,10 @@ def user(accounts): def vault_manager(accounts): return accounts[7] + @pytest.fixture(scope="session") +def keeper(accounts): + yield accounts[8] -@pytest.fixture(scope="session") -def strategy_manager(accounts): - return accounts[8] @pytest.fixture(scope="session") @@ -466,28 +460,20 @@ def role_manager( ) return role_manager -def deploy_strategy_manager(project, daddy): - def deploy_strategy_manager(): - strategy_manager = daddy.deploy( - project.StrategyManager, daddy, ["0x2606a10b", "0xdf69b22a"] - ) - return strategy_manager - yield deploy_strategy_manager - - -@pytest.fixture(scope="session") -def strategy_manager(deploy_strategy_manager): - strategy_manager = deploy_strategy_manager() +def strategy_manager(project, yield_manager): + strategy_manager = project.StrategyManager.at(yield_manager.strategyManager()) yield strategy_manager @pytest.fixture(scope="session") -def deploy_yield_manager(project, daddy, strategy_manager): +def deploy_yield_manager(project, daddy): def deploy_yield_manager(): - yield_manager = daddy.deploy(project.YieldManager, daddy, strategy_manager) + yield_manager = daddy.deploy( + project.YieldManager, daddy, ["0x2606a10b", "0xdf69b22a"] + ) return yield_manager From c42bfefd2a16f69cdc70a5b5eaaa759f83340a24 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 15 Dec 2023 12:10:32 -0700 Subject: [PATCH 09/35] fix: workflow --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index db66500..d95eb0b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,12 +48,12 @@ def user(accounts): def vault_manager(accounts): return accounts[7] - @pytest.fixture(scope="session") + +@pytest.fixture(scope="session") def keeper(accounts): yield accounts[8] - @pytest.fixture(scope="session") def create_token(project, daddy, user, amount): def create_token( From 16c0acfdaba5211df73fdd18a7f89a7d63a1ee17 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 18 Dec 2023 10:12:37 -0700 Subject: [PATCH 10/35] fix: rebase tests --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index d95eb0b..c7ac9c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,6 +29,7 @@ def security(accounts): def management(accounts): yield accounts[3] + @pytest.fixture(scope="session") def fee_recipient(accounts): return accounts[4] @@ -462,6 +463,7 @@ def role_manager( return role_manager +@pytest.fixture(scope="session") def strategy_manager(project, yield_manager): strategy_manager = project.StrategyManager.at(yield_manager.strategyManager()) From 637886a5c3c7b3eabfa8c7de27abb2595f161400 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 18 Dec 2023 12:59:16 -0700 Subject: [PATCH 11/35] feat: use allocator --- .../debtAllocators/GenericDebtAllocator.sol | 16 +- .../YieldManager/StrategyManager.sol | 6 +- .../YieldManager/YieldManager.sol | 164 ++++++--------- .../test_generic_debt_allocator.py | 8 +- .../yield/test_yield_manager.py | 192 ++++++++++++------ 5 files changed, 212 insertions(+), 174 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index 535f543..8e73d42 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -6,6 +6,8 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Governance} from "@periphery/utils/Governance.sol"; import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; +// TODO: Getters + /** * @title YearnV3 Generic Debt Allocator * @author yearn.finance @@ -358,7 +360,7 @@ contract GenericDebtAllocator is Governance { * @param _address The address to set mapping for. * @param _allowed If the address can call {update_debt}. */ - function setKeepers( + function setKeeper( address _address, bool _allowed ) external virtual onlyGovernance { @@ -428,4 +430,16 @@ contract GenericDebtAllocator is Governance { emit UpdateMaxAcceptableBaseFee(_maxAcceptableBaseFee); } + + function getStrategyTargetRatio( + address _strategy + ) external view virtual returns (uint256) { + return configs[_strategy].targetRatio; + } + + function getStrategyMaxRatio( + address _strategy + ) external view virtual returns (uint256) { + return configs[_strategy].maxRatio; + } } diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol index aa39a39..5e0aca4 100644 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -181,15 +181,15 @@ contract StrategyManager is Governance { // Get the current unlock rate. uint256 profitUnlock = IStrategy(_strategy).profitMaxUnlockTime(); - if (profitUnlock != 0) { + if (profitUnlock != 1) { // Set profit unlock to 0. - IStrategy(_strategy).setProfitMaxUnlockTime(0); + IStrategy(_strategy).setProfitMaxUnlockTime(1); } // Report profits. IStrategy(_strategy).report(); - if (profitUnlock != 0) { + if (profitUnlock != 1) { // Set profit unlock back to original. IStrategy(_strategy).setProfitMaxUnlockTime(profitUnlock); } diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index c5722a9..7071c03 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -6,6 +6,8 @@ import {AprOracle} from "@periphery/AprOracle/AprOracle.sol"; import {StrategyManager, Governance} from "./StrategyManager.sol"; +import {GenericDebtAllocator} from "../GenericDebtAllocator.sol"; + /** * @title YearnV3 Yield Yield Based Debt Allocator * @author yearn.finance @@ -18,10 +20,10 @@ contract YieldManager is Governance { event UpdateOpen(bool status); /// @notice Emitted when a vaults status is updated. - event UpdateVault(address indexed vault, bool status); + event UpdateVaultAllocator(address indexed vault, address allocator); - /// @notice Emitted when a allocators status is updated. - event UpdateAllocator(address indexed allocator, bool status); + /// @notice Emitted when a proposers status is updated. + event UpdateProposer(address indexed proposer, bool status); /// @notice An event emitted when the max debt update loss is updated. event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); @@ -36,14 +38,14 @@ contract YieldManager is Governance { } /// @notice Only allow the sender to be an allocator if not opened. - modifier onlyAllocatorsOrOpen() { - _isAllocatorOrOpen(); + modifier onlyProposersOrOpen() { + _isProposerOrOpen(); _; } /// @notice Check if it has been opened or is an allocator. - function _isAllocatorOrOpen() internal view { - require(open || allocators[msg.sender], "!allocator or open"); + function _isProposerOrOpen() internal view { + require(open || proposers[msg.sender], "!allocator or open"); } uint256 internal constant MAX_BPS = 10_000; @@ -61,11 +63,11 @@ contract YieldManager is Governance { /// @notice Address that should hold the strategies `management` role. address public immutable strategyManager; - /// @notice Mapping for vaults that can be allocated for. - mapping(address => bool) public vaults; + /// @notice Mapping for vaults that can be allocated for => its debt allocator. + mapping(address => address) public vaultAllocator; /// @notice Addresses that are allowed to propose allocations. - mapping(address => bool) public allocators; + mapping(address => bool) public proposers; constructor( address _governance, @@ -124,10 +126,12 @@ contract YieldManager is Governance { Allocation[] memory _newAllocations ) external - onlyAllocatorsOrOpen + onlyProposersOrOpen returns (uint256 _currentYield, uint256 _afterYield) { - require(vaults[_vault], "vault not added"); + address allocator = vaultAllocator[_vault]; + require(allocator != address(0), "vault not added"); + // Get the total assets the vault has. uint256 _totalAssets = IVault(_vault).totalAssets(); @@ -140,8 +144,6 @@ contract YieldManager is Governance { address _strategy; uint256 _currentDebt; uint256 _newDebt; - uint256 _loss; - uint256 _gain; for (uint256 i = 0; i < _newAllocations.length; ++i) { _strategy = _newAllocations[i].strategy; _newDebt = uint256(_newAllocations[i].newDebt); @@ -172,15 +174,6 @@ contract YieldManager is Governance { StrategyManager(strategyManager).reportFullProfit( _strategy ); - - // Report profits on the vault. - (uint256 reportedProfit, uint256 reportedLoss) = IVault( - _vault - ).process_report(_strategy); - - // Track for debt reduction loss checks. - _loss += reportedLoss; - _gain += reportedProfit; } else if ( // We cannot decrease debt if the strategy has any unrealised losses. IVault(_vault).assess_share_of_unrealised_losses( @@ -189,39 +182,23 @@ contract YieldManager is Governance { ) != 0 ) { // Realize the loss. - (, uint256 reportedLoss) = IVault(_vault).process_report( - _strategy - ); - // Track for debt reduction loss checks. - _loss += reportedLoss; - } - - // Allocate the new debt. - IVault(_vault).update_debt(_strategy, _newDebt); - - // Validate losses based on ending totalAssets adjusted for any realized loss or gain. - uint256 afterAssets = IVault(_vault).totalAssets() + - _loss - - _gain; - - // If a loss was realized on just the debt update. - if (afterAssets < _totalAssets) { - // Make sure its within the range. - require( - _totalAssets - afterAssets <= - (_currentDebt * maxDebtUpdateLoss) / MAX_BPS, - "too much loss" - ); + IVault(_vault).process_report(_strategy); } - } else { - // If adding just Allocate the new debt. - IVault(_vault).update_debt(_strategy, _newDebt); } + uint256 _targetRatio = (_newDebt * MAX_BPS) / _totalAssets; + // Update allocation. + GenericDebtAllocator(allocator).setStrategyDebtRatios( + _strategy, + _targetRatio + ); + // Get the new APR if (_newDebt != 0) { - _afterYield += (aprOracle.getStrategyApr(_strategy, 0) * - _newDebt); + _afterYield += (aprOracle.getStrategyApr( + _strategy, + int256(_newDebt) - int256(_currentDebt) + ) * _newDebt); } } @@ -320,39 +297,31 @@ contract YieldManager is Governance { address _vault, Allocation[] memory _newAllocations ) internal { + address allocator = vaultAllocator[_vault]; + require(allocator != address(0), "vault not added"); address _strategy; uint256 _newDebt; - uint256 _loss; - uint256 _gain; + uint256 _currentDebt; uint256 _totalAssets = IVault(_vault).totalAssets(); for (uint256 i = 0; i < _newAllocations.length; ++i) { _strategy = _newAllocations[i].strategy; _newDebt = uint256(_newAllocations[i].newDebt); - - // Get the current amount the strategy holds. - uint256 _currentDebt = IVault(_vault) - .strategies(_strategy) - .current_debt; + // Get the debt the strategy current has. + _currentDebt = IVault(_vault).strategies(_strategy).current_debt; // If no change move to the next strategy. - if (_newDebt == _currentDebt) continue; + if (_newDebt == _currentDebt) { + continue; + } - if (_newDebt < _currentDebt) { + // If we are withdrawing. + if (_currentDebt > _newDebt) { // If we are pulling all debt from a strategy. if (_newDebt == 0) { // We need to report profits and have them immediately unlock to not lose out on locked profit. StrategyManager(strategyManager).reportFullProfit( _strategy ); - - // Report on the vault. - (uint256 reportedProfit, uint256 reportedLoss) = IVault( - _vault - ).process_report(_strategy); - - // Track for debt reduction loss checks. - _loss += reportedLoss; - _gain += reportedProfit; } else if ( // We cannot decrease debt if the strategy has any unrealised losses. IVault(_vault).assess_share_of_unrealised_losses( @@ -361,60 +330,45 @@ contract YieldManager is Governance { ) != 0 ) { // Realize the loss. - (, uint256 reportedLoss) = IVault(_vault).process_report( - _strategy - ); - // Track for debt reduction loss checks. - _loss += reportedLoss; - } - - // Allocate the new debt. - IVault(_vault).update_debt(_strategy, _newDebt); - - // Validate losses based on ending totalAssets adjusted for any realized loss or gain. - uint256 afterAssets = IVault(_vault).totalAssets() + - _loss - - _gain; - - // If a loss was realized on just the debt update. - if (afterAssets < _totalAssets) { - // Make sure its within the range. - require( - _totalAssets - afterAssets <= - (_currentDebt * maxDebtUpdateLoss) / MAX_BPS, - "too much loss" - ); + IVault(_vault).process_report(_strategy); } - } else { - // If adding just Allocate the new debt. - IVault(_vault).update_debt(_strategy, _newDebt); } + + uint256 _targetRatio = (_newDebt * MAX_BPS) / _totalAssets; + // Update allocation. + GenericDebtAllocator(allocator).setStrategyDebtRatios( + _strategy, + _targetRatio + ); } } /** - * @notice Sets the permission for an allocator. - * @param _address The address of the allocator. - * @param _allowed The permission to set for the allocator. + * @notice Sets the permission for a proposer. + * @param _address The address of the proposer. + * @param _allowed The permission to set for the proposer. */ - function setAllocator( + function setProposer( address _address, bool _allowed ) external onlyGovernance { - allocators[_address] = _allowed; + proposers[_address] = _allowed; - emit UpdateAllocator(_address, _allowed); + emit UpdateProposer(_address, _allowed); } /** * @notice Sets the mapping of vaults allowed. * @param _vault The address of the _vault. - * @param _allowed The permission to set for the _vault. + * @param _allocator The vault specific debt allocator. */ - function setVault(address _vault, bool _allowed) external onlyGovernance { - vaults[_vault] = _allowed; + function setVaultAllocator( + address _vault, + address _allocator + ) external onlyGovernance { + vaultAllocator[_vault] = _allocator; - emit UpdateVault(_vault, _allowed); + emit UpdateVaultAllocator(_vault, _allocator); } /** diff --git a/tests/debtAllocators/test_generic_debt_allocator.py b/tests/debtAllocators/test_generic_debt_allocator.py index 130ecc4..eade9ea 100644 --- a/tests/debtAllocators/test_generic_debt_allocator.py +++ b/tests/debtAllocators/test_generic_debt_allocator.py @@ -29,9 +29,9 @@ def test_set_keepers(generic_debt_allocator, daddy, vault, strategy, user): assert generic_debt_allocator.keepers(user) == False with ape.reverts("!governance"): - generic_debt_allocator.setKeepers(user, True, sender=user) + generic_debt_allocator.setKeeper(user, True, sender=user) - tx = generic_debt_allocator.setKeepers(user, True, sender=daddy) + tx = generic_debt_allocator.setKeeper(user, True, sender=daddy) event = list(tx.decode_logs(generic_debt_allocator.UpdateKeeper))[0] @@ -39,7 +39,7 @@ def test_set_keepers(generic_debt_allocator, daddy, vault, strategy, user): assert event.allowed == True assert generic_debt_allocator.keepers(user) == True - tx = generic_debt_allocator.setKeepers(daddy, False, sender=daddy) + tx = generic_debt_allocator.setKeeper(daddy, False, sender=daddy) event = list(tx.decode_logs(generic_debt_allocator.UpdateKeeper))[0] @@ -306,7 +306,7 @@ def test_update_debt( assert vault.totalIdle() == 0 assert vault.totalDebt() == amount - generic_debt_allocator.setKeepers(user, True, sender=daddy) + generic_debt_allocator.setKeeper(user, True, sender=daddy) generic_debt_allocator.update_debt(strategy, 0, sender=user) diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 3207916..d977bfc 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -1,6 +1,6 @@ import ape from ape import chain, project, networks -from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES +from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES, MAX_BPS def setup_vault(vault, strategies, oracle, chad): @@ -16,53 +16,47 @@ def test_yield_manager_setup(yield_manager, daddy, vault, management, strategy_m assert yield_manager.governance() == daddy assert yield_manager.open() == False assert yield_manager.maxDebtUpdateLoss() == 1 - assert yield_manager.allocators(management) == False - assert yield_manager.vaults(vault) == False + assert yield_manager.proposers(management) == False + assert yield_manager.vaultAllocator(vault) == ZERO_ADDRESS -def test_setters(yield_manager, daddy, vault, management): - assert yield_manager.vaults(vault) == False - assert yield_manager.allocators(management) == False +def test_setters(yield_manager, daddy, vault, generic_debt_allocator, management): + assert yield_manager.vaultAllocator(vault) == ZERO_ADDRESS + assert yield_manager.proposers(management) == False assert yield_manager.open() == False assert yield_manager.maxDebtUpdateLoss() == 1 with ape.reverts("!governance"): - yield_manager.setVault(vault, True, sender=management) + yield_manager.setVaultAllocator( + vault, generic_debt_allocator, sender=management + ) - tx = yield_manager.setVault(vault, True, sender=daddy) + tx = yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - event = list(tx.decode_logs(yield_manager.UpdateVault))[0] + event = list(tx.decode_logs(yield_manager.UpdateVaultAllocator))[0] assert event.vault == vault - assert event.status == True - assert yield_manager.vaults(vault) == True - - tx = yield_manager.setVault(vault, False, sender=daddy) - - event = list(tx.decode_logs(yield_manager.UpdateVault))[0] - - assert event.vault == vault - assert event.status == False - assert yield_manager.vaults(management) == False + assert event.allocator == generic_debt_allocator + assert yield_manager.vaultAllocator(vault) == generic_debt_allocator with ape.reverts("!governance"): - yield_manager.setAllocator(management, True, sender=management) + yield_manager.setProposer(management, True, sender=management) - tx = yield_manager.setAllocator(management, True, sender=daddy) + tx = yield_manager.setProposer(management, True, sender=daddy) - event = list(tx.decode_logs(yield_manager.UpdateAllocator))[0] + event = list(tx.decode_logs(yield_manager.UpdateProposer))[0] - assert event.allocator == management + assert event.proposer == management assert event.status == True - assert yield_manager.allocators(management) == True + assert yield_manager.proposers(management) == True - tx = yield_manager.setAllocator(management, False, sender=daddy) + tx = yield_manager.setProposer(management, False, sender=daddy) - event = list(tx.decode_logs(yield_manager.UpdateAllocator))[0] + event = list(tx.decode_logs(yield_manager.UpdateProposer))[0] - assert event.allocator == management + assert event.proposer == management assert event.status == False - assert yield_manager.allocators(management) == False + assert yield_manager.proposers(management) == False loss = int(8) with ape.reverts("!governance"): @@ -97,6 +91,7 @@ def test_update_allocation( amount, asset, deploy_mock_tokenized, + generic_debt_allocator, ): # Strategy two will have the higher apr strategy_one = deploy_mock_tokenized("One", int(1e16)) @@ -112,26 +107,37 @@ def test_update_allocation( with ape.reverts("!allocator or open"): yield_manager.updateAllocation(vault, allocation, sender=user) - yield_manager.setAllocator(user, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) with ape.reverts("vault not added"): yield_manager.updateAllocation(vault, allocation, sender=user) - yield_manager.setVault(vault, True, sender=daddy) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - # Must give allocator the debt role - with ape.reverts("not allowed"): + # Must give allocator the keeper role + with ape.reverts("!keeper"): yield_manager.updateAllocation(vault, allocation, sender=user) - vault.set_role( - yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy - ) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) + vault.add_role(generic_debt_allocator, ROLES.DEBT_MANAGER, sender=daddy) tx = yield_manager.updateAllocation(vault, allocation, sender=user) (before, now) = tx.return_value + assert generic_debt_allocator.configs(strategy_two).targetRatio == MAX_BPS + assert generic_debt_allocator.configs(strategy_one).targetRatio == 0 + assert generic_debt_allocator.shouldUpdateDebt(strategy_one)[0] == False + assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == True + assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[ + 1 + ] == vault.update_debt.encode_input(strategy_two.address, amount) assert before == 0 + + generic_debt_allocator.update_debt(strategy_two, amount, sender=daddy) + assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + # assert now == int(1e17 * amount) assert vault.totalIdle() == 0 assert vault.totalDebt() == amount @@ -142,7 +148,7 @@ def test_update_allocation( yield_manager.updateAllocation(vault, allocation, sender=user) allocation = [(strategy_one, amount)] - with ape.reverts("no funds to deposit"): + with ape.reverts("ratio too high"): yield_manager.updateAllocation(vault, allocation, sender=user) allocation = [(strategy_two, amount // 2)] @@ -156,13 +162,36 @@ def test_update_allocation( to_move = amount // 2 # will revert if in the wrong order allocation = [(strategy_one, to_move), (strategy_two, amount - to_move)] - with ape.reverts("no funds to deposit"): + with ape.reverts("ratio too high"): yield_manager.updateAllocation(vault, allocation, sender=user) allocation = [(strategy_two, amount - to_move), (strategy_one, to_move)] tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 + assert generic_debt_allocator.configs(strategy_two).targetRatio != MAX_BPS + assert generic_debt_allocator.configs(strategy_one).targetRatio != 0 + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool_one == False + assert bool_two == True + assert bytes_two == vault.update_debt.encode_input( + strategy_two.address, amount - to_move + ) + + generic_debt_allocator.update_debt(strategy_two, amount - to_move, sender=daddy) + + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert bool_one == True + assert bytes_one == vault.update_debt.encode_input(strategy_one, to_move) + + generic_debt_allocator.update_debt(strategy_one, to_move, sender=daddy) + + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool_one == False + assert bool_two == False + + # assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 assert vault.totalIdle() == 0 assert vault.totalDebt() == amount assert vault.totalAssets() == amount @@ -181,9 +210,30 @@ def test_update_allocation( tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 assert len(list(tx.decode_logs(strategy_two.UpdateProfitMaxUnlockTime))) == 2 - assert len(list(tx.decode_logs(strategy_two.Reported))) == 1 + + assert generic_debt_allocator.configs(strategy_two).targetRatio == 0 + assert generic_debt_allocator.configs(strategy_one).targetRatio == MAX_BPS + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool_one == False + assert bool_two == True + assert bytes_two == vault.update_debt.encode_input(strategy_two.address, 0) + + generic_debt_allocator.update_debt(strategy_two, 0, sender=daddy) + + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert bool_one == True + assert bytes_one == vault.update_debt.encode_input(strategy_one, amount) + + generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool_one == False + assert bool_two == False + + # assert len(list(tx.decode_logs(strategy_two.Reported))) == 1 assert vault.totalIdle() == 0 assert vault.totalDebt() == amount assert vault.totalAssets() == amount @@ -207,19 +257,20 @@ def test_update_allocation_pending_profit( amount, asset, deploy_mock_tokenized, + generic_debt_allocator, ): # Strategy two will have the higher apr strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVault(vault, True, sender=daddy) - vault.add_role( - yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy - ) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) + vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) strategy_one.setPendingManagement(strategy_manager, sender=management) strategy_one.setProfitMaxUnlockTime(int(200), sender=management) strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) - yield_manager.setAllocator(user, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) profit = amount // 10 amount = amount - profit @@ -261,19 +312,20 @@ def test_update_allocation_pending_loss( amount, asset, deploy_mock_tokenized, + generic_debt_allocator, ): # Strategy two will have the higher apr strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVault(vault, True, sender=daddy) - vault.add_role( - yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy - ) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) + vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) strategy_one.setPendingManagement(strategy_manager, sender=management) strategy_one.setProfitMaxUnlockTime(int(200), sender=management) strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) - yield_manager.setAllocator(user, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) loss = amount // 10 @@ -313,19 +365,20 @@ def test_update_allocation_pending_loss_move_half( amount, asset, deploy_mock_tokenized, + generic_debt_allocator, ): # Strategy two will have the higher apr strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVault(vault, True, sender=daddy) - vault.add_role( - yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy - ) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) + vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) strategy_one.setPendingManagement(strategy_manager, sender=management) strategy_one.setProfitMaxUnlockTime(int(200), sender=management) strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) - yield_manager.setAllocator(user, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) loss = amount // 10 @@ -356,6 +409,7 @@ def test_update_allocation_pending_loss_move_half( assert strategy_one.balanceOf(vault) != 0 +""" def test_update_allocation_loss_on_withdraw( apr_oracle, yield_manager, @@ -368,19 +422,22 @@ def test_update_allocation_loss_on_withdraw( amount, asset, deploy_mock_tokenized, + generic_debt_allocator ): # Strategy two will have the higher apr strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVault(vault, True, sender=daddy) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) vault.add_role( - yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy + yield_manager, ROLES.REPORTING_MANAGER, sender=daddy ) strategy_one.setPendingManagement(strategy_manager, sender=management) strategy_one.setProfitMaxUnlockTime(int(200), sender=management) strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) - yield_manager.setAllocator(user, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) loss = amount // 10 @@ -411,15 +468,24 @@ def test_update_allocation_loss_on_withdraw( assert vault.totalDebt() == amount - loss + 1 assert vault.strategies(strategy_one).current_debt == 1 assert vault.strategies(strategy_two).current_debt == amount - loss +""" def test_validate_allocation( - apr_oracle, yield_manager, vault, daddy, user, amount, asset, deploy_mock_tokenized + apr_oracle, + yield_manager, + vault, + daddy, + user, + amount, + asset, + deploy_mock_tokenized, + generic_debt_allocator, ): strategy_one = deploy_mock_tokenized("One") strategy_two = deploy_mock_tokenized("two") setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVault(vault, True, sender=daddy) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) @@ -514,11 +580,15 @@ def test_update_allocation_permissioned( amount, asset, deploy_mock_tokenized, + generic_debt_allocator, ): # Strategy two will have the higher apr strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) vault.set_role( yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy ) @@ -532,7 +602,7 @@ def test_update_allocation_permissioned( with ape.reverts("!governance"): yield_manager.updateAllocationPermissioned(vault, allocation, sender=user) - yield_manager.setAllocator(user, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) # Still cant allocate even with allocator role with ape.reverts("!governance"): From 3785c34fc0c750e974ebd1045de06f684533debc Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 18 Dec 2023 14:48:34 -0700 Subject: [PATCH 12/35] feat: manage strategy --- .../debtAllocators/GenericDebtAllocator.sol | 12 +- .../YieldManager/StrategyManager.sol | 109 +++++------------- .../YieldManager/YieldManager.sol | 16 ++- tests/conftest.py | 4 +- .../yield/test_strategy_manager.py | 6 - .../yield/test_yield_manager.py | 8 +- 6 files changed, 52 insertions(+), 103 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index 8e73d42..4ae47e4 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -6,8 +6,6 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Governance} from "@periphery/utils/Governance.sol"; import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; -// TODO: Getters - /** * @title YearnV3 Generic Debt Allocator * @author yearn.finance @@ -431,12 +429,22 @@ contract GenericDebtAllocator is Governance { emit UpdateMaxAcceptableBaseFee(_maxAcceptableBaseFee); } + /** + * @notice Get a strategies target debt ratio. + * @param _strategy Address of the strategy. + * @return The strategies current targetRatio. + */ function getStrategyTargetRatio( address _strategy ) external view virtual returns (uint256) { return configs[_strategy].targetRatio; } + /** + * @notice Get a strategies max debt ratio. + * @param _strategy Address of the strategy. + * @return The strategies current maxRatio. + */ function getStrategyMaxRatio( address _strategy ) external view virtual returns (uint256) { diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol index 5e0aca4..d31f667 100644 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -8,106 +8,82 @@ import {IStrategy} from "@tokenized-strategy/interfaces/IStrategy.sol"; /// debt allocator can call both reports and change the profit unlock time. contract StrategyManager is Governance { /// @notice Emitted when a new strategy is added to the manager. - event StrategyAdded( - address indexed strategy, - address indexed owner, - address indexed debtManager - ); + event StrategyAdded(address indexed strategy, address indexed owner); /// @notice Emitted when a strategy is removed. event StrategyRemoved(address indexed strategy, address indexed newManager); - /// @notice holds info for a strategy that is managed. - struct StrategyInfo { - bool active; - address owner; - address debtManager; - } - /// @notice Only the `_strategy` specific owner can call. modifier onlyStrategyOwner(address _strategy) { _checkStrategyOwner(_strategy); _; } - /// @notice Only the `_strategy` owner of its debt manager can call. - modifier onlyStrategyOwnerOrDebtManager(address _strategy) { - _checkStrategyDebtManager(_strategy); + /// @notice Strategy must be added and debt manager is calling. + modifier onlyStrategyAndDebtManager(address _strategy) { + _checkStrategyAndDebtManager(_strategy); _; } /// @notice Checks if the msg sender is the owner of the strategy. function _checkStrategyOwner(address _strategy) internal view virtual { - require(strategyInfo[_strategy].owner == msg.sender, "!owner"); + require(strategyOwner[_strategy] == msg.sender, "!owner"); } - /// @notice Checks if the msg sender is the debt manager or the strategy owner. - function _checkStrategyDebtManager( + /// @notice Checks if the msg sender is the debt manager and the strategy is added. + function _checkStrategyAndDebtManager( address _strategy ) internal view virtual { require( - strategyInfo[_strategy].debtManager == msg.sender || - strategyInfo[_strategy].owner == msg.sender, + yieldManager == msg.sender && + strategyOwner[_strategy] != address(0), "!debt manager" ); } - /// @notice strategy address => struct with info. - mapping(address => StrategyInfo) public strategyInfo; + /// @notice Debt manager contract that can call this manager. + address public immutable yieldManager; - /// @notice function selector => bool if a debt manager can call that. - mapping(bytes4 => bool) public allowedSelectors; + /// @notice strategy address => struct with info. + mapping(address => address) public strategyOwner; - /** - * @notice Add any of the allowed selectors for a debt manager to call - * to the mapping. - */ - constructor( - address _governance, - bytes4[] memory _allowedSelectors - ) Governance(_governance) { - for (uint256 i = 0; i < _allowedSelectors.length; ++i) { - allowedSelectors[_allowedSelectors[i]] = true; - } + constructor(address _governance) Governance(_governance) { + yieldManager = msg.sender; } /** * @notice Add a new strategy, using the current `management` as the owner. * @param _strategy The address of the strategy. - * @param _debtManager The address of the debt manager. */ - function manageNewStrategy( - address _strategy, - address _debtManager - ) external { + function manageNewStrategy(address _strategy) external { address currentManager = IStrategy(_strategy).management(); - manageNewStrategy(_strategy, _debtManager, currentManager); + manageNewStrategy(_strategy, currentManager); } /** * @notice Manage a new strategy, setting the debt manager and marking it as active. * @param _strategy The address of the strategy. - * @param _debtManager The address of the debt manager. * @param _owner The address in charge of the strategy now. */ function manageNewStrategy( address _strategy, - address _debtManager, address _owner ) public onlyGovernance { - require(!strategyInfo[_strategy].active, "already active"); + require( + _owner != address(0) && + _owner != address(this) && + _owner != _strategy, + "bad address" + ); + require(strategyOwner[_strategy] == address(0), "already active"); // Accept management of the strategy. IStrategy(_strategy).acceptManagement(); // Store the owner of the strategy. - strategyInfo[_strategy] = StrategyInfo({ - active: true, - owner: _owner, - debtManager: _debtManager - }); + strategyOwner[_strategy] = _owner; - emit StrategyAdded(_strategy, _owner, _debtManager); + emit StrategyAdded(_strategy, _owner); } /** @@ -125,19 +101,7 @@ contract StrategyManager is Governance { _newOwner != _strategy, "bad address" ); - strategyInfo[_strategy].owner = _newOwner; - } - - /** - * @notice Updates the debt manager of a strategy. - * @param _strategy The address of the strategy. - * @param _newDebtManager The address of the new owner. - */ - function updateDebtManager( - address _strategy, - address _newDebtManager - ) external onlyStrategyOwner(_strategy) { - strategyInfo[_strategy].debtManager = _newDebtManager; + strategyOwner[_strategy] = _newOwner; } /** @@ -164,7 +128,7 @@ contract StrategyManager is Governance { "bad address" ); - delete strategyInfo[_strategy]; + delete strategyOwner[_strategy]; IStrategy(_strategy).setPendingManagement(_newManager); @@ -177,7 +141,7 @@ contract StrategyManager is Governance { */ function reportFullProfit( address _strategy - ) external onlyStrategyOwnerOrDebtManager(_strategy) { + ) external onlyStrategyAndDebtManager(_strategy) { // Get the current unlock rate. uint256 profitUnlock = IStrategy(_strategy).profitMaxUnlockTime(); @@ -221,20 +185,7 @@ contract StrategyManager is Governance { function forwardCall( address _strategy, bytes memory _calldata - ) public returns (bytes memory) { - bytes4 selector; - - assembly { - // Copy the first 4 bytes of the memory array to the selector variable - selector := mload(add(_calldata, 32)) - } - - if (allowedSelectors[selector]) { - _checkStrategyDebtManager(_strategy); - } else { - _checkStrategyOwner(_strategy); - } - + ) public onlyStrategyOwner(_strategy) returns (bytes memory) { (bool success, bytes memory result) = _strategy.call(_calldata); // If the call reverted. Return the error. diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 7071c03..58615f7 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -22,7 +22,7 @@ contract YieldManager is Governance { /// @notice Emitted when a vaults status is updated. event UpdateVaultAllocator(address indexed vault, address allocator); - /// @notice Emitted when a proposers status is updated. + /// @notice Emitted when a proposer status is updated. event UpdateProposer(address indexed proposer, bool status); /// @notice An event emitted when the max debt update loss is updated. @@ -45,7 +45,7 @@ contract YieldManager is Governance { /// @notice Check if it has been opened or is an allocator. function _isProposerOrOpen() internal view { - require(open || proposers[msg.sender], "!allocator or open"); + require(proposer[msg.sender] || open, "!allocator or open"); } uint256 internal constant MAX_BPS = 10_000; @@ -67,14 +67,11 @@ contract YieldManager is Governance { mapping(address => address) public vaultAllocator; /// @notice Addresses that are allowed to propose allocations. - mapping(address => bool) public proposers; + mapping(address => bool) public proposer; - constructor( - address _governance, - bytes4[] memory _selectors - ) Governance(_governance) { + constructor(address _governance) Governance(_governance) { // Deploy a new strategy manager - strategyManager = address(new StrategyManager(_governance, _selectors)); + strategyManager = address(new StrategyManager(_governance)); // Default to 1 BP loss maxDebtUpdateLoss = 1; } @@ -186,6 +183,7 @@ contract YieldManager is Governance { } } + // Get the target based on the new debt. uint256 _targetRatio = (_newDebt * MAX_BPS) / _totalAssets; // Update allocation. GenericDebtAllocator(allocator).setStrategyDebtRatios( @@ -352,7 +350,7 @@ contract YieldManager is Governance { address _address, bool _allowed ) external onlyGovernance { - proposers[_address] = _allowed; + proposer[_address] = _allowed; emit UpdateProposer(_address, _allowed); } diff --git a/tests/conftest.py b/tests/conftest.py index c7ac9c4..041cc40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -473,9 +473,7 @@ def strategy_manager(project, yield_manager): @pytest.fixture(scope="session") def deploy_yield_manager(project, daddy): def deploy_yield_manager(): - yield_manager = daddy.deploy( - project.YieldManager, daddy, ["0x2606a10b", "0xdf69b22a"] - ) + yield_manager = daddy.deploy(project.YieldManager, daddy) return yield_manager diff --git a/tests/debtAllocators/yield/test_strategy_manager.py b/tests/debtAllocators/yield/test_strategy_manager.py index d970238..7fc1c34 100644 --- a/tests/debtAllocators/yield/test_strategy_manager.py +++ b/tests/debtAllocators/yield/test_strategy_manager.py @@ -2,17 +2,11 @@ from ape import chain, project from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES -allowed_selectors = ["0x2606a10b", "0xdf69b22a"] - def test_strategy_manager_setup(strategy_manager, mock_tokenized): assert strategy_manager.strategyInfo(mock_tokenized).active == False assert strategy_manager.strategyInfo(mock_tokenized).owner == ZERO_ADDRESS assert strategy_manager.strategyInfo(mock_tokenized).debtManager == ZERO_ADDRESS - for selector in allowed_selectors: - assert strategy_manager.allowedSelectors(selector) - # Check random selector - assert strategy_manager.allowedSelectors("0x9bbefdb6") == False def test_add_new_strategy( diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index d977bfc..00a89b6 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -16,13 +16,13 @@ def test_yield_manager_setup(yield_manager, daddy, vault, management, strategy_m assert yield_manager.governance() == daddy assert yield_manager.open() == False assert yield_manager.maxDebtUpdateLoss() == 1 - assert yield_manager.proposers(management) == False + assert yield_manager.proposer(management) == False assert yield_manager.vaultAllocator(vault) == ZERO_ADDRESS def test_setters(yield_manager, daddy, vault, generic_debt_allocator, management): assert yield_manager.vaultAllocator(vault) == ZERO_ADDRESS - assert yield_manager.proposers(management) == False + assert yield_manager.proposer(management) == False assert yield_manager.open() == False assert yield_manager.maxDebtUpdateLoss() == 1 @@ -48,7 +48,7 @@ def test_setters(yield_manager, daddy, vault, generic_debt_allocator, management assert event.proposer == management assert event.status == True - assert yield_manager.proposers(management) == True + assert yield_manager.proposer(management) == True tx = yield_manager.setProposer(management, False, sender=daddy) @@ -56,7 +56,7 @@ def test_setters(yield_manager, daddy, vault, generic_debt_allocator, management assert event.proposer == management assert event.status == False - assert yield_manager.proposers(management) == False + assert yield_manager.proposer(management) == False loss = int(8) with ape.reverts("!governance"): From 3885b320ec30b4943cd270cbfb73891109b03f70 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 18 Dec 2023 18:00:40 -0700 Subject: [PATCH 13/35] test: update tests --- .../YieldManager/StrategyManager.sol | 8 +- .../YieldManager/YieldManager.sol | 8 +- .../yield/test_strategy_manager.py | 114 ++++-------- .../yield/test_yield_manager.py | 175 +++++++++--------- 4 files changed, 129 insertions(+), 176 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol index d31f667..c93d61b 100644 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -20,8 +20,8 @@ contract StrategyManager is Governance { } /// @notice Strategy must be added and debt manager is calling. - modifier onlyStrategyAndDebtManager(address _strategy) { - _checkStrategyAndDebtManager(_strategy); + modifier onlyStrategyAndYieldManager(address _strategy) { + _checkStrategyAndYieldManager(_strategy); _; } @@ -31,7 +31,7 @@ contract StrategyManager is Governance { } /// @notice Checks if the msg sender is the debt manager and the strategy is added. - function _checkStrategyAndDebtManager( + function _checkStrategyAndYieldManager( address _strategy ) internal view virtual { require( @@ -141,7 +141,7 @@ contract StrategyManager is Governance { */ function reportFullProfit( address _strategy - ) external onlyStrategyAndDebtManager(_strategy) { + ) external onlyStrategyAndYieldManager(_strategy) { // Get the current unlock rate. uint256 profitUnlock = IStrategy(_strategy).profitMaxUnlockTime(); diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 58615f7..ed3bb2c 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -171,7 +171,9 @@ contract YieldManager is Governance { StrategyManager(strategyManager).reportFullProfit( _strategy ); - } else if ( + } + + if ( // We cannot decrease debt if the strategy has any unrealised losses. IVault(_vault).assess_share_of_unrealised_losses( _strategy, @@ -184,7 +186,9 @@ contract YieldManager is Governance { } // Get the target based on the new debt. - uint256 _targetRatio = (_newDebt * MAX_BPS) / _totalAssets; + uint256 _targetRatio = _newDebt < _totalAssets + ? (_newDebt * MAX_BPS) / _totalAssets + : MAX_BPS; // Update allocation. GenericDebtAllocator(allocator).setStrategyDebtRatios( _strategy, diff --git a/tests/debtAllocators/yield/test_strategy_manager.py b/tests/debtAllocators/yield/test_strategy_manager.py index 7fc1c34..7a7e61b 100644 --- a/tests/debtAllocators/yield/test_strategy_manager.py +++ b/tests/debtAllocators/yield/test_strategy_manager.py @@ -3,30 +3,27 @@ from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES -def test_strategy_manager_setup(strategy_manager, mock_tokenized): - assert strategy_manager.strategyInfo(mock_tokenized).active == False - assert strategy_manager.strategyInfo(mock_tokenized).owner == ZERO_ADDRESS - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == ZERO_ADDRESS +def test_strategy_manager_setup(strategy_manager, mock_tokenized, daddy, yield_manager): + assert strategy_manager.strategyOwner(mock_tokenized) == ZERO_ADDRESS + assert strategy_manager.yieldManager() == yield_manager + assert strategy_manager.governance() == daddy def test_add_new_strategy( strategy_manager, mock_tokenized, daddy, yield_manager, management ): - assert strategy_manager.strategyInfo(mock_tokenized).active == False - assert strategy_manager.strategyInfo(mock_tokenized).owner == ZERO_ADDRESS - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == ZERO_ADDRESS + assert strategy_manager.strategyOwner(mock_tokenized) == ZERO_ADDRESS + assert strategy_manager.yieldManager() == yield_manager with ape.reverts("!governance"): - strategy_manager.manageNewStrategy( - mock_tokenized, yield_manager, sender=management - ) + strategy_manager.manageNewStrategy(mock_tokenized, sender=management) with ape.reverts("!pending"): - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) + strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) mock_tokenized.setPendingManagement(strategy_manager, sender=management) - tx = strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) + tx = strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) assert mock_tokenized.management() == strategy_manager @@ -34,15 +31,13 @@ def test_add_new_strategy( assert event.strategy == mock_tokenized assert event.owner == management - assert event.debtManager == yield_manager - assert strategy_manager.strategyInfo(mock_tokenized).active == True - assert strategy_manager.strategyInfo(mock_tokenized).owner == management - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + assert strategy_manager.strategyOwner(mock_tokenized) == management + assert strategy_manager.yieldManager() == yield_manager # cannot add it again - with ape.reverts("already active"): - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) + with ape.reverts("bad address"): + strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) def test_remove_strategy( @@ -54,12 +49,11 @@ def test_remove_strategy( mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) + strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) assert mock_tokenized.management() == strategy_manager - assert strategy_manager.strategyInfo(mock_tokenized).active == True - assert strategy_manager.strategyInfo(mock_tokenized).owner == management - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + assert strategy_manager.strategyOwner(mock_tokenized) == management + assert strategy_manager.yieldManager() == yield_manager with ape.reverts("!owner"): strategy_manager.removeManagement(mock_tokenized, user, sender=user) @@ -89,9 +83,7 @@ def test_remove_strategy( assert event.strategy == mock_tokenized assert event.newManager == user - assert strategy_manager.strategyInfo(mock_tokenized).active == False - assert strategy_manager.strategyInfo(mock_tokenized).owner == ZERO_ADDRESS - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == ZERO_ADDRESS + assert strategy_manager.strategyOwner(mock_tokenized) == ZERO_ADDRESS assert mock_tokenized.management() == strategy_manager assert mock_tokenized.pendingManagement() == user @@ -107,12 +99,11 @@ def test_update_owner( mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) + strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) assert mock_tokenized.management() == strategy_manager - assert strategy_manager.strategyInfo(mock_tokenized).active == True - assert strategy_manager.strategyInfo(mock_tokenized).owner == management - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + assert strategy_manager.strategyOwner(mock_tokenized) == management + assert strategy_manager.yieldManager() == yield_manager with ape.reverts("!owner"): strategy_manager.updateStrategyOwner(mock_tokenized, user, sender=user) @@ -134,32 +125,8 @@ def test_update_owner( strategy_manager.updateStrategyOwner(mock_tokenized, user, sender=management) - assert strategy_manager.strategyInfo(mock_tokenized).active == True - assert strategy_manager.strategyInfo(mock_tokenized).owner == user - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager - assert mock_tokenized.management() == strategy_manager - - -def test_update_debt_manager( - strategy_manager, mock_tokenized, yield_manager, management, user, daddy -): - mock_tokenized.setPendingManagement(strategy_manager, sender=management) - - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) - - assert mock_tokenized.management() == strategy_manager - assert strategy_manager.strategyInfo(mock_tokenized).active == True - assert strategy_manager.strategyInfo(mock_tokenized).owner == management - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager - - with ape.reverts("!owner"): - strategy_manager.updateDebtManager(mock_tokenized, user, sender=user) - - strategy_manager.updateDebtManager(mock_tokenized, user, sender=management) - - assert strategy_manager.strategyInfo(mock_tokenized).active == True - assert strategy_manager.strategyInfo(mock_tokenized).owner == management - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == user + assert strategy_manager.strategyOwner(mock_tokenized) == user + assert strategy_manager.yieldManager() == yield_manager assert mock_tokenized.management() == strategy_manager @@ -169,12 +136,11 @@ def test_record_full_profit( mock_tokenized.setProfitMaxUnlockTime(int(60 * 60 * 24), sender=management) mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) + strategy_manager.manageNewStrategy(mock_tokenized, management, sender=daddy) assert mock_tokenized.management() == strategy_manager - assert strategy_manager.strategyInfo(mock_tokenized).active == True - assert strategy_manager.strategyInfo(mock_tokenized).owner == management - assert strategy_manager.strategyInfo(mock_tokenized).debtManager == yield_manager + assert strategy_manager.strategyOwner(mock_tokenized) == management + assert strategy_manager.yieldManager() == yield_manager # deposit into the strategy to_deposit = asset.balanceOf(user) // 2 @@ -200,7 +166,13 @@ def test_record_full_profit( strategy_manager.reportFullProfit(mock_tokenized, sender=yield_manager) - # Profit should be fully unlocked + # Profit should be fully locked + assert mock_tokenized.totalAssets() == to_deposit + profit + assert mock_tokenized.totalSupply() == to_deposit + profit + + chain.pending_timestamp += 10 + chain.mine(1) + assert mock_tokenized.totalAssets() == to_deposit + profit assert mock_tokenized.totalSupply() == to_deposit assert current_unlock_time == mock_tokenized.profitMaxUnlockTime() @@ -212,16 +184,15 @@ def test_forward_call( ): mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) + strategy_manager.manageNewStrategy(mock_tokenized, management, sender=daddy) assert mock_tokenized.profitMaxUnlockTime() == 0 assert mock_tokenized.performanceFee() == 0 - # Yield manager can change profit max unlock new_unlock_time = int(69) calldata = mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)) - with ape.reverts("!debt manager"): + with ape.reverts("!owner"): strategy_manager.forwardCall(mock_tokenized, calldata, sender=user) tx = strategy_manager.forwardCall(mock_tokenized, calldata, sender=management) @@ -233,12 +204,6 @@ def test_forward_call( new_unlock_time = int(6699) calldata = mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)) - tx = strategy_manager.forwardCall(mock_tokenized, calldata, sender=yield_manager) - - event = list(tx.decode_logs(mock_tokenized.UpdateProfitMaxUnlockTime))[0] - assert event.newProfitMaxUnlockTime == new_unlock_time - assert mock_tokenized.profitMaxUnlockTime() == new_unlock_time - # Only management can change a performance fee. new_fee = int(2_000) calldata = mock_tokenized.setPerformanceFee.encode_input(new_fee) @@ -255,20 +220,13 @@ def test_forward_call( assert event.newPerformanceFee == new_fee assert mock_tokenized.performanceFee() == new_fee - # We get the correct return data - new_unlock_time = int(1e25) - calldata = mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)) - - with ape.reverts("too long"): - strategy_manager.forwardCall(mock_tokenized, calldata, sender=yield_manager) - def test_forward_calls( strategy_manager, mock_tokenized, management, yield_manager, daddy, user ): mock_tokenized.setPendingManagement(strategy_manager, sender=management) - strategy_manager.manageNewStrategy(mock_tokenized, yield_manager, sender=daddy) + strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) assert mock_tokenized.profitMaxUnlockTime() == 0 assert mock_tokenized.performanceFee() == 0 @@ -282,7 +240,7 @@ def test_forward_calls( ] # Only management can change a performance fee. - with ape.reverts("!debt manager"): + with ape.reverts("!owner"): strategy_manager.forwardCalls(mock_tokenized, calldatas, sender=user) with ape.reverts("!owner"): diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 00a89b6..d7f2c8f 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -290,15 +290,19 @@ def test_update_allocation_pending_profit( tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 - assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 assert len(list(tx.decode_logs(strategy_one.Reported))) == 1 assert len(list(tx.decode_logs(strategy_two.UpdateProfitMaxUnlockTime))) == 2 - assert vault.totalAssets() == amount + profit - assert vault.totalDebt() == amount + profit - assert vault.strategies(strategy_one).current_debt == 0 - assert vault.strategies(strategy_two).current_debt == amount + profit - assert strategy_one.balanceOf(vault) == 0 + + assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy_one, 0) + + vault.update_debt(strategy_one, 0, sender=daddy) + + (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy_two, amount) def test_update_allocation_pending_loss( @@ -344,13 +348,18 @@ def test_update_allocation_pending_loss( tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 - assert vault.totalAssets() == amount - loss - assert vault.totalDebt() == amount - loss - assert vault.strategies(strategy_one).current_debt == 0 - assert vault.strategies(strategy_two).current_debt == amount - loss - assert strategy_one.balanceOf(vault) == 0 + + assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy_one, 0) + + vault.update_debt(strategy_one, 0, sender=daddy) + + (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy_two, amount - loss) def test_update_allocation_pending_loss_move_half( @@ -396,79 +405,22 @@ def test_update_allocation_pending_loss_move_half( strategy_one.report(sender=keeper) to_move = amount // 2 - allocation = [(strategy_one, amount - to_move), (strategy_two, amount)] + allocation = [(strategy_one, amount - to_move), (strategy_two, to_move)] tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 - assert vault.totalAssets() == amount - loss - assert vault.totalDebt() == amount - loss - assert vault.strategies(strategy_one).current_debt == amount - to_move - assert vault.strategies(strategy_two).current_debt == to_move - loss - assert strategy_one.balanceOf(vault) != 0 - - -""" -def test_update_allocation_loss_on_withdraw( - apr_oracle, - yield_manager, - vault, - management, - strategy_manager, - daddy, - user, - keeper, - amount, - asset, - deploy_mock_tokenized, - generic_debt_allocator -): - # Strategy two will have the higher apr - strategy_one = deploy_mock_tokenized("One", int(1e16)) - strategy_two = deploy_mock_tokenized("two", int(1e17)) - setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.add_role( - yield_manager, ROLES.REPORTING_MANAGER, sender=daddy - ) - strategy_one.setPendingManagement(strategy_manager, sender=management) - strategy_one.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) - yield_manager.setProposer(user, True, sender=daddy) - - loss = amount // 10 + assert vault.totalAssets() < amount - asset.approve(vault, amount, sender=user) - vault.deposit(amount, user, sender=user) - - vault.update_debt(strategy_one, amount, sender=daddy) - - assert vault.totalAssets() == amount - assert vault.totalDebt() == amount - assert vault.strategies(strategy_one).current_debt == amount - - # simulate strategy loss - strategy_one.realizeLoss(loss, sender=daddy) - - allocation = [(strategy_one, 1), (strategy_two, amount)] - - with ape.reverts("too much loss"): - yield_manager.updateAllocation(vault, allocation, sender=user) - - yield_manager.setMaxDebtUpdateLoss(1_000, sender=daddy) + assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert bool == True - tx = yield_manager.updateAllocation(vault, allocation, sender=user) + vault.update_debt(strategy_one, amount - to_move, sender=daddy) - assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 - assert len(list(tx.decode_logs(vault.StrategyReported))) == 0 - assert vault.totalAssets() == amount - loss + 1 - assert vault.totalDebt() == amount - loss + 1 - assert vault.strategies(strategy_one).current_debt == 1 - assert vault.strategies(strategy_two).current_debt == amount - loss -""" + (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy_two, to_move - loss) def test_validate_allocation( @@ -610,13 +562,15 @@ def test_update_allocation_permissioned( tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) - # assert now == int(1e17 * amount) - assert vault.totalIdle() == 0 - assert vault.totalDebt() == amount - assert vault.strategies(strategy_two).current_debt == amount + assert generic_debt_allocator.shouldUpdateDebt(strategy_one)[0] == False + (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy_two, amount) + + vault.update_debt(strategy_two, amount, sender=daddy) allocation = [(strategy_one, amount)] - with ape.reverts("no funds to deposit"): + with ape.reverts("ratio too high"): yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) # strategy one is now earning more @@ -626,18 +580,34 @@ def test_update_allocation_permissioned( to_move = amount // 2 # will revert if in the wrong order allocation = [(strategy_one, to_move), (strategy_two, amount - to_move)] - with ape.reverts("no funds to deposit"): + with ape.reverts("ratio too high"): yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) allocation = [(strategy_two, amount - to_move), (strategy_one, to_move)] tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) - assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 - assert vault.totalIdle() == 0 - assert vault.totalDebt() == amount - assert vault.totalAssets() == amount - assert vault.strategies(strategy_one).current_debt == to_move - assert vault.strategies(strategy_two).current_debt == amount - to_move + assert generic_debt_allocator.configs(strategy_two).targetRatio != MAX_BPS + assert generic_debt_allocator.configs(strategy_one).targetRatio != 0 + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool_one == False + assert bool_two == True + assert bytes_two == vault.update_debt.encode_input( + strategy_two.address, amount - to_move + ) + + vault.update_debt(strategy_two, amount - to_move, sender=daddy) + + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert bool_one == True + assert bytes_one == vault.update_debt.encode_input(strategy_one, to_move) + + vault.update_debt(strategy_one, to_move, sender=daddy) + + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool_one == False + assert bool_two == False # Try and move all allocation = [(strategy_two, 0), (strategy_one, amount)] @@ -651,9 +621,30 @@ def test_update_allocation_permissioned( tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) - assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 assert len(list(tx.decode_logs(strategy_two.UpdateProfitMaxUnlockTime))) == 2 assert len(list(tx.decode_logs(strategy_two.Reported))) == 1 + + assert generic_debt_allocator.configs(strategy_two).targetRatio == 0 + assert generic_debt_allocator.configs(strategy_one).targetRatio == MAX_BPS + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool_one == False + assert bool_two == True + assert bytes_two == vault.update_debt.encode_input(strategy_two.address, 0) + + vault.update_debt(strategy_two, 0, sender=daddy) + + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert bool_one == True + assert bytes_one == vault.update_debt.encode_input(strategy_one, amount) + + vault.update_debt(strategy_one, amount, sender=daddy) + + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool_one == False + assert bool_two == False + assert vault.totalIdle() == 0 assert vault.totalDebt() == amount assert vault.totalAssets() == amount From fae7ee0ec5ac421b30a2ccb134abf1708dd3ddad Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 18 Dec 2023 18:13:57 -0700 Subject: [PATCH 14/35] fix: fixes --- .../YieldManager/StrategyManager.sol | 6 ++-- .../YieldManager/YieldManager.sol | 31 ++++++------------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol index c93d61b..56b1392 100644 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ b/contracts/debtAllocators/YieldManager/StrategyManager.sol @@ -77,8 +77,10 @@ contract StrategyManager is Governance { ); require(strategyOwner[_strategy] == address(0), "already active"); - // Accept management of the strategy. - IStrategy(_strategy).acceptManagement(); + if (IStrategy(_strategy).management() != address(this)) { + // Accept management of the strategy. + IStrategy(_strategy).acceptManagement(); + } // Store the owner of the strategy. strategyOwner[_strategy] = _owner; diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index ed3bb2c..6631bd9 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -57,9 +57,6 @@ contract YieldManager is Governance { /// @notice Flag to set to allow anyone to propose allocations. bool public open; - /// @notice Max loss to accept on debt updates in basis points. - uint256 public maxDebtUpdateLoss; - /// @notice Address that should hold the strategies `management` role. address public immutable strategyManager; @@ -72,8 +69,6 @@ contract YieldManager is Governance { constructor(address _governance) Governance(_governance) { // Deploy a new strategy manager strategyManager = address(new StrategyManager(_governance)); - // Default to 1 BP loss - maxDebtUpdateLoss = 1; } /** @@ -189,6 +184,7 @@ contract YieldManager is Governance { uint256 _targetRatio = _newDebt < _totalAssets ? (_newDebt * MAX_BPS) / _totalAssets : MAX_BPS; + // Update allocation. GenericDebtAllocator(allocator).setStrategyDebtRatios( _strategy, @@ -324,7 +320,9 @@ contract YieldManager is Governance { StrategyManager(strategyManager).reportFullProfit( _strategy ); - } else if ( + } + + if ( // We cannot decrease debt if the strategy has any unrealised losses. IVault(_vault).assess_share_of_unrealised_losses( _strategy, @@ -336,7 +334,11 @@ contract YieldManager is Governance { } } - uint256 _targetRatio = (_newDebt * MAX_BPS) / _totalAssets; + // Get the target based on the new debt. + uint256 _targetRatio = _newDebt < _totalAssets + ? (_newDebt * MAX_BPS) / _totalAssets + : MAX_BPS; + // Update allocation. GenericDebtAllocator(allocator).setStrategyDebtRatios( _strategy, @@ -382,19 +384,4 @@ contract YieldManager is Governance { emit UpdateOpen(_open); } - - /** - * @notice Set the max loss in Basis points to allow on debt updates. - * @dev Withdrawing during debt updates use {redeem} which allows for 100% loss. - * This can be used to assure a loss is not realized on redeem outside the tolerance. - * @param _maxDebtUpdateLoss The max loss to accept on debt updates. - */ - function setMaxDebtUpdateLoss( - uint256 _maxDebtUpdateLoss - ) external virtual onlyGovernance { - require(_maxDebtUpdateLoss <= MAX_BPS, "higher than max"); - maxDebtUpdateLoss = _maxDebtUpdateLoss; - - emit UpdateMaxDebtUpdateLoss(_maxDebtUpdateLoss); - } } From 3faefbaae71aa71b6a8c3ed94f35d7b93151e614 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 19 Dec 2023 18:42:06 -0700 Subject: [PATCH 15/35] fix: remove event --- .../debtAllocators/YieldManager/YieldManager.sol | 9 +++------ tests/debtAllocators/yield/test_yield_manager.py | 13 ------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 6631bd9..91a9029 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -18,15 +18,12 @@ import {GenericDebtAllocator} from "../GenericDebtAllocator.sol"; contract YieldManager is Governance { /// @notice Emitted when the open flag is updated. event UpdateOpen(bool status); - - /// @notice Emitted when a vaults status is updated. - event UpdateVaultAllocator(address indexed vault, address allocator); - + /// @notice Emitted when a proposer status is updated. event UpdateProposer(address indexed proposer, bool status); - /// @notice An event emitted when the max debt update loss is updated. - event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); + /// @notice Emitted when a vaults status is updated. + event UpdateVaultAllocator(address indexed vault, address allocator); // Struct that contains the address of the strategy and its best allocation. struct Allocation { diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index d7f2c8f..13babbe 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -15,7 +15,6 @@ def test_yield_manager_setup(yield_manager, daddy, vault, management, strategy_m assert yield_manager.strategyManager() == strategy_manager assert yield_manager.governance() == daddy assert yield_manager.open() == False - assert yield_manager.maxDebtUpdateLoss() == 1 assert yield_manager.proposer(management) == False assert yield_manager.vaultAllocator(vault) == ZERO_ADDRESS @@ -24,7 +23,6 @@ def test_setters(yield_manager, daddy, vault, generic_debt_allocator, management assert yield_manager.vaultAllocator(vault) == ZERO_ADDRESS assert yield_manager.proposer(management) == False assert yield_manager.open() == False - assert yield_manager.maxDebtUpdateLoss() == 1 with ape.reverts("!governance"): yield_manager.setVaultAllocator( @@ -58,17 +56,6 @@ def test_setters(yield_manager, daddy, vault, generic_debt_allocator, management assert event.status == False assert yield_manager.proposer(management) == False - loss = int(8) - with ape.reverts("!governance"): - yield_manager.setMaxDebtUpdateLoss(loss, sender=management) - - tx = yield_manager.setMaxDebtUpdateLoss(loss, sender=daddy) - - event = list(tx.decode_logs(yield_manager.UpdateMaxDebtUpdateLoss))[0] - - assert event.newMaxDebtUpdateLoss == loss - assert yield_manager.maxDebtUpdateLoss() == loss - with ape.reverts("!governance"): yield_manager.setOpen(True, sender=management) From 429d82fcca86cbc97106fd6ed18b75b464eaee66 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 19 Dec 2023 21:05:21 -0700 Subject: [PATCH 16/35] feat: just a keeper contract --- ape-config.yaml | 2 +- contracts/Mocks/MockTokenizedStrategy.sol | 2 + .../debtAllocators/YieldManager/Keeper.sol | 128 +++++++++ .../YieldManager/StrategyManager.sol | 206 -------------- .../YieldManager/YieldManager.sol | 26 +- tests/conftest.py | 22 +- .../yield/test_keeper_contract.py | 165 +++++++++++ .../yield/test_strategy_manager.py | 257 ------------------ .../yield/test_yield_manager.py | 57 ++-- 9 files changed, 348 insertions(+), 517 deletions(-) create mode 100644 contracts/debtAllocators/YieldManager/Keeper.sol delete mode 100644 contracts/debtAllocators/YieldManager/StrategyManager.sol create mode 100644 tests/debtAllocators/yield/test_keeper_contract.py delete mode 100644 tests/debtAllocators/yield/test_strategy_manager.py diff --git a/ape-config.yaml b/ape-config.yaml index 858dc57..79ef5d9 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -12,7 +12,7 @@ default_ecosystem: ethereum dependencies: - name: openzeppelin github: OpenZeppelin/openzeppelin-contracts - version: 4.8.2 + ref: 4.8.2 - name: yearn-vaults github: yearn/yearn-vaults-v3 ref: v3.0.1 diff --git a/contracts/Mocks/MockTokenizedStrategy.sol b/contracts/Mocks/MockTokenizedStrategy.sol index 4e8cfdc..7680c3a 100644 --- a/contracts/Mocks/MockTokenizedStrategy.sol +++ b/contracts/Mocks/MockTokenizedStrategy.sol @@ -33,4 +33,6 @@ contract MockTokenized is MockTokenizedStrategy { strategyStorage().totalIdle -= _amount; strategyStorage().totalDebt += _amount; } + + function tendThis(uint256) external {} } diff --git a/contracts/debtAllocators/YieldManager/Keeper.sol b/contracts/debtAllocators/YieldManager/Keeper.sol new file mode 100644 index 0000000..58350ea --- /dev/null +++ b/contracts/debtAllocators/YieldManager/Keeper.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.18; + +import {Governance} from "@periphery/utils/Governance.sol"; +import {IStrategy} from "@tokenized-strategy/interfaces/IStrategy.sol"; + +/// @notice Holds the `keeper` role of a V3 strategy so that a +/// multiple addresses can call report. +contract Keeper is Governance { + /// @notice Emitted when a strategy is removed. + event StrategyRemoved(address indexed strategy); + + /// @notice An event emitted when a keeper is added or removed. + event UpdateKeeper(address indexed keeper, bool allowed); + + /// @notice Emitted when a new strategy is added to the manager. + event StrategyAdded(address indexed strategy, address indexed owner); + + /// @notice Only the `_strategy` specific owner can call. + modifier onlyStrategyOwner(address _strategy) { + _checkStrategyOwner(_strategy); + _; + } + + /// @notice Only the keepers can call. + modifier onlyKeepers(address _strategy) { + _checkKeepers(_strategy); + _; + } + + /// @notice Checks if the msg sender is the owner of the strategy. + function _checkStrategyOwner(address _strategy) internal view virtual { + require(strategyOwner[_strategy] == msg.sender, "!owner"); + } + + /// @notice Checks if the msg sender is a keeper and the strategy is added. + function _checkKeepers(address _strategy) internal view virtual { + require( + keepers[msg.sender] && strategyOwner[_strategy] != address(0), + "!keeper" + ); + } + + /// @notice Address check for keepers allowed to call. + mapping(address => bool) public keepers; + + /// @notice strategy address => struct with info. + mapping(address => address) public strategyOwner; + + constructor(address _governance) Governance(_governance) {} + + /** + * @notice Add a new strategy, using the current `management` as the owner. + * @param _strategy The address of the strategy. + */ + function addNewStrategy(address _strategy) external onlyGovernance { + address currentManager = IStrategy(_strategy).management(); + require(strategyOwner[_strategy] == address(0), "already active"); + require(IStrategy(_strategy).keeper() == address(this), "!keeper"); + + // Store the owner of the strategy. + strategyOwner[_strategy] = currentManager; + + emit StrategyAdded(_strategy, currentManager); + } + + /** + * @notice Updates the owner of a strategy. + * @param _strategy The address of the strategy. + * @param _newOwner The address of the new owner. + */ + function updateStrategyOwner( + address _strategy, + address _newOwner + ) external onlyStrategyOwner(_strategy) { + require( + _newOwner != address(0) && + _newOwner != address(this) && + _newOwner != _strategy, + "bad address" + ); + strategyOwner[_strategy] = _newOwner; + } + + /** + * @notice Removes the management of a strategy, transferring it to the `owner`. + * @param _strategy The address of the strategy. + */ + function removeStrategy( + address _strategy + ) external onlyStrategyOwner(_strategy) { + delete strategyOwner[_strategy]; + + emit StrategyRemoved(_strategy); + } + + /** + * @notice Reports full profit for a strategy. + * @param _strategy The address of the strategy. + */ + function report(address _strategy) external onlyKeepers(_strategy) { + // Report profits. + IStrategy(_strategy).report(); + } + + /** + * @notice Tends a strategy. + * @param _strategy The address of the strategy. + */ + function tend(address _strategy) external onlyKeepers(_strategy) { + // Tend. + IStrategy(_strategy).tend(); + } + + /** + * @notice Set if a keeper can update debt. + * @param _address The address to set mapping for. + * @param _allowed If the address can call {update_debt}. + */ + function setKeeper( + address _address, + bool _allowed + ) external virtual onlyGovernance { + keepers[_address] = _allowed; + + emit UpdateKeeper(_address, _allowed); + } +} diff --git a/contracts/debtAllocators/YieldManager/StrategyManager.sol b/contracts/debtAllocators/YieldManager/StrategyManager.sol deleted file mode 100644 index 56b1392..0000000 --- a/contracts/debtAllocators/YieldManager/StrategyManager.sol +++ /dev/null @@ -1,206 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.18; - -import {Governance} from "@periphery/utils/Governance.sol"; -import {IStrategy} from "@tokenized-strategy/interfaces/IStrategy.sol"; - -/// @notice Holds the `management` role of a V3 strategy so that a -/// debt allocator can call both reports and change the profit unlock time. -contract StrategyManager is Governance { - /// @notice Emitted when a new strategy is added to the manager. - event StrategyAdded(address indexed strategy, address indexed owner); - - /// @notice Emitted when a strategy is removed. - event StrategyRemoved(address indexed strategy, address indexed newManager); - - /// @notice Only the `_strategy` specific owner can call. - modifier onlyStrategyOwner(address _strategy) { - _checkStrategyOwner(_strategy); - _; - } - - /// @notice Strategy must be added and debt manager is calling. - modifier onlyStrategyAndYieldManager(address _strategy) { - _checkStrategyAndYieldManager(_strategy); - _; - } - - /// @notice Checks if the msg sender is the owner of the strategy. - function _checkStrategyOwner(address _strategy) internal view virtual { - require(strategyOwner[_strategy] == msg.sender, "!owner"); - } - - /// @notice Checks if the msg sender is the debt manager and the strategy is added. - function _checkStrategyAndYieldManager( - address _strategy - ) internal view virtual { - require( - yieldManager == msg.sender && - strategyOwner[_strategy] != address(0), - "!debt manager" - ); - } - - /// @notice Debt manager contract that can call this manager. - address public immutable yieldManager; - - /// @notice strategy address => struct with info. - mapping(address => address) public strategyOwner; - - constructor(address _governance) Governance(_governance) { - yieldManager = msg.sender; - } - - /** - * @notice Add a new strategy, using the current `management` as the owner. - * @param _strategy The address of the strategy. - */ - function manageNewStrategy(address _strategy) external { - address currentManager = IStrategy(_strategy).management(); - manageNewStrategy(_strategy, currentManager); - } - - /** - * @notice Manage a new strategy, setting the debt manager and marking it as active. - * @param _strategy The address of the strategy. - * @param _owner The address in charge of the strategy now. - */ - function manageNewStrategy( - address _strategy, - address _owner - ) public onlyGovernance { - require( - _owner != address(0) && - _owner != address(this) && - _owner != _strategy, - "bad address" - ); - require(strategyOwner[_strategy] == address(0), "already active"); - - if (IStrategy(_strategy).management() != address(this)) { - // Accept management of the strategy. - IStrategy(_strategy).acceptManagement(); - } - - // Store the owner of the strategy. - strategyOwner[_strategy] = _owner; - - emit StrategyAdded(_strategy, _owner); - } - - /** - * @notice Updates the owner of a strategy. - * @param _strategy The address of the strategy. - * @param _newOwner The address of the new owner. - */ - function updateStrategyOwner( - address _strategy, - address _newOwner - ) external onlyStrategyOwner(_strategy) { - require( - _newOwner != address(0) && - _newOwner != address(this) && - _newOwner != _strategy, - "bad address" - ); - strategyOwner[_strategy] = _newOwner; - } - - /** - * @notice Removes the management of a strategy, transferring it to the `owner`. - * @param _strategy The address of the strategy. - */ - function removeManagement(address _strategy) external { - removeManagement(_strategy, msg.sender); - } - - /** - * @notice Removes the management of a strategy, transferring it to a new manager. - * @param _strategy The address of the strategy. - * @param _newManager The address of the new manager. - */ - function removeManagement( - address _strategy, - address _newManager - ) public onlyStrategyOwner(_strategy) { - require( - _newManager != address(0) && - _newManager != address(this) && - _newManager != _strategy, - "bad address" - ); - - delete strategyOwner[_strategy]; - - IStrategy(_strategy).setPendingManagement(_newManager); - - emit StrategyRemoved(_strategy, _newManager); - } - - /** - * @notice Reports full profit for a strategy. - * @param _strategy The address of the strategy. - */ - function reportFullProfit( - address _strategy - ) external onlyStrategyAndYieldManager(_strategy) { - // Get the current unlock rate. - uint256 profitUnlock = IStrategy(_strategy).profitMaxUnlockTime(); - - if (profitUnlock != 1) { - // Set profit unlock to 0. - IStrategy(_strategy).setProfitMaxUnlockTime(1); - } - - // Report profits. - IStrategy(_strategy).report(); - - if (profitUnlock != 1) { - // Set profit unlock back to original. - IStrategy(_strategy).setProfitMaxUnlockTime(profitUnlock); - } - } - - /** - * @notice Forwards multiple calls to a strategy. - * @param _strategy The address of the strategy. - * @param _calldataArray An array of calldata for each call. - * @return _returnData An array of return data from each call. - */ - function forwardCalls( - address _strategy, - bytes[] memory _calldataArray - ) external returns (bytes[] memory _returnData) { - uint256 _length = _calldataArray.length; - _returnData = new bytes[](_length); - for (uint256 i = 0; i < _length; ++i) { - _returnData[i] = forwardCall(_strategy, _calldataArray[i]); - } - } - - /** - * @notice Forwards a single call to a strategy. - * @param _strategy The address of the strategy. - * @param _calldata The calldata for the call. - * @return _returnData The return data from the call. - */ - function forwardCall( - address _strategy, - bytes memory _calldata - ) public onlyStrategyOwner(_strategy) returns (bytes memory) { - (bool success, bytes memory result) = _strategy.call(_calldata); - - // If the call reverted. Return the error. - if (!success) { - assembly { - let ptr := mload(0x40) - let size := returndatasize() - returndatacopy(ptr, 0, size) - revert(ptr, size) - } - } - - // Return the result. - return result; - } -} diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 91a9029..f73bbec 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -4,8 +4,7 @@ pragma solidity 0.8.18; import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; import {AprOracle} from "@periphery/AprOracle/AprOracle.sol"; -import {StrategyManager, Governance} from "./StrategyManager.sol"; - +import {Keeper, Governance} from "./Keeper.sol"; import {GenericDebtAllocator} from "../GenericDebtAllocator.sol"; /** @@ -18,7 +17,7 @@ import {GenericDebtAllocator} from "../GenericDebtAllocator.sol"; contract YieldManager is Governance { /// @notice Emitted when the open flag is updated. event UpdateOpen(bool status); - + /// @notice Emitted when a proposer status is updated. event UpdateProposer(address indexed proposer, bool status); @@ -55,17 +54,16 @@ contract YieldManager is Governance { bool public open; /// @notice Address that should hold the strategies `management` role. - address public immutable strategyManager; - - /// @notice Mapping for vaults that can be allocated for => its debt allocator. - mapping(address => address) public vaultAllocator; + address public immutable keeper; /// @notice Addresses that are allowed to propose allocations. mapping(address => bool) public proposer; - constructor(address _governance) Governance(_governance) { - // Deploy a new strategy manager - strategyManager = address(new StrategyManager(_governance)); + /// @notice Mapping for vaults that can be allocated for => its debt allocator. + mapping(address => address) public vaultAllocator; + + constructor(address _keeper, address _governance) Governance(_governance) { + keeper = _keeper; } /** @@ -160,9 +158,7 @@ contract YieldManager is Governance { // If we are pulling all debt from a strategy. if (_newDebt == 0) { // We need to report profits and have them immediately unlock to not lose out on locked profit. - StrategyManager(strategyManager).reportFullProfit( - _strategy - ); + Keeper(keeper).report(_strategy); } if ( @@ -314,9 +310,7 @@ contract YieldManager is Governance { // If we are pulling all debt from a strategy. if (_newDebt == 0) { // We need to report profits and have them immediately unlock to not lose out on locked profit. - StrategyManager(strategyManager).reportFullProfit( - _strategy - ); + Keeper(keeper).report(_strategy); } if ( diff --git a/tests/conftest.py b/tests/conftest.py index 041cc40..e8882cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,7 +51,7 @@ def vault_manager(accounts): @pytest.fixture(scope="session") -def keeper(accounts): +def strategy_manager(accounts): yield accounts[8] @@ -464,16 +464,26 @@ def role_manager( @pytest.fixture(scope="session") -def strategy_manager(project, yield_manager): - strategy_manager = project.StrategyManager.at(yield_manager.strategyManager()) +def deploy_keeper(project, daddy): + def deploy_keeper(): + keeper = daddy.deploy(project.Keeper, daddy) + + return keeper + + yield deploy_keeper + + +@pytest.fixture(scope="session") +def keeper(deploy_keeper): + keeper = deploy_keeper() - yield strategy_manager + yield keeper @pytest.fixture(scope="session") -def deploy_yield_manager(project, daddy): +def deploy_yield_manager(project, daddy, keeper): def deploy_yield_manager(): - yield_manager = daddy.deploy(project.YieldManager, daddy) + yield_manager = daddy.deploy(project.YieldManager, keeper, daddy) return yield_manager diff --git a/tests/debtAllocators/yield/test_keeper_contract.py b/tests/debtAllocators/yield/test_keeper_contract.py new file mode 100644 index 0000000..24ab246 --- /dev/null +++ b/tests/debtAllocators/yield/test_keeper_contract.py @@ -0,0 +1,165 @@ +import ape +from ape import chain, project +from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES + + +def test_keeper_setup(keeper, mock_tokenized, daddy, yield_manager): + assert keeper.strategyOwner(mock_tokenized) == ZERO_ADDRESS + assert keeper.keepers(yield_manager) == False + assert keeper.governance() == daddy + + +def test_add_new_strategy(keeper, mock_tokenized, daddy, yield_manager, management): + assert keeper.strategyOwner(mock_tokenized) == ZERO_ADDRESS + assert keeper.keepers(yield_manager) == False + + with ape.reverts("!governance"): + keeper.addNewStrategy(mock_tokenized, sender=management) + + with ape.reverts("!keeper"): + keeper.addNewStrategy(mock_tokenized, sender=daddy) + + mock_tokenized.setKeeper(keeper, sender=management) + + tx = keeper.addNewStrategy(mock_tokenized, sender=daddy) + + assert mock_tokenized.keeper() == keeper + + event = list(tx.decode_logs(keeper.StrategyAdded))[0] + + assert event.strategy == mock_tokenized + assert event.owner == management + + assert keeper.strategyOwner(mock_tokenized) == management + + # cannot add it again + with ape.reverts("already active"): + keeper.addNewStrategy(mock_tokenized, sender=daddy) + + +def test_remove_strategy( + keeper, mock_tokenized, yield_manager, management, user, daddy +): + # Will revert on modifier if not yet added. + with ape.reverts("!owner"): + keeper.removeStrategy(mock_tokenized, sender=management) + + mock_tokenized.setKeeper(keeper, sender=management) + + keeper.addNewStrategy(mock_tokenized, sender=daddy) + + assert mock_tokenized.keeper() == keeper + assert keeper.strategyOwner(mock_tokenized) == management + + with ape.reverts("!owner"): + keeper.removeStrategy(mock_tokenized, sender=user) + + tx = keeper.removeStrategy(mock_tokenized, sender=management) + + event = list(tx.decode_logs(keeper.StrategyRemoved))[0] + + assert event.strategy == mock_tokenized + + assert keeper.strategyOwner(mock_tokenized) == ZERO_ADDRESS + assert mock_tokenized.management() == management + + +def test_update_owner(keeper, mock_tokenized, yield_manager, management, user, daddy): + with ape.reverts("!owner"): + keeper.updateStrategyOwner(mock_tokenized, user, sender=management) + + mock_tokenized.setKeeper(keeper, sender=management) + + keeper.addNewStrategy(mock_tokenized, sender=daddy) + + assert mock_tokenized.keeper() == keeper + assert keeper.strategyOwner(mock_tokenized) == management + + with ape.reverts("!owner"): + keeper.updateStrategyOwner(mock_tokenized, user, sender=user) + + with ape.reverts("bad address"): + keeper.updateStrategyOwner(mock_tokenized, ZERO_ADDRESS, sender=management) + + with ape.reverts("bad address"): + keeper.updateStrategyOwner(mock_tokenized, mock_tokenized, sender=management) + + with ape.reverts("bad address"): + keeper.updateStrategyOwner(mock_tokenized, keeper, sender=management) + + keeper.updateStrategyOwner(mock_tokenized, user, sender=management) + + assert keeper.strategyOwner(mock_tokenized) == user + assert mock_tokenized.keeper() == keeper + + +def test_report(keeper, mock_tokenized, management, yield_manager, asset, user, daddy): + mock_tokenized.setKeeper(keeper, sender=management) + + keeper.addNewStrategy(mock_tokenized, sender=daddy) + + assert mock_tokenized.keeper() == keeper + assert keeper.strategyOwner(mock_tokenized) == management + + # deposit into the strategy + to_deposit = asset.balanceOf(user) // 2 + profit = asset.balanceOf(user) - to_deposit + + asset.approve(mock_tokenized, to_deposit, sender=user) + mock_tokenized.deposit(to_deposit, user, sender=user) + + assert mock_tokenized.totalAssets() == to_deposit + assert mock_tokenized.totalSupply() == to_deposit + + # simulate profit + asset.transfer(mock_tokenized, profit, sender=user) + + assert mock_tokenized.totalAssets() == to_deposit + assert mock_tokenized.totalSupply() == to_deposit + current_unlock_time = mock_tokenized.profitMaxUnlockTime() + assert current_unlock_time == 0 + assert mock_tokenized.pricePerShare() == 10 ** asset.decimals() + + with ape.reverts("!keeper"): + keeper.report(mock_tokenized, sender=yield_manager) + + keeper.setKeeper(yield_manager, True, sender=daddy) + + keeper.report(mock_tokenized, sender=yield_manager) + + # Profit should be fully unlocked + assert mock_tokenized.totalAssets() == to_deposit + profit + assert mock_tokenized.totalSupply() == to_deposit + assert mock_tokenized.pricePerShare() > 10 ** asset.decimals() + + +def test_tend(keeper, mock_tokenized, management, yield_manager, asset, user, daddy): + mock_tokenized.setKeeper(keeper, sender=management) + + keeper.addNewStrategy(mock_tokenized, sender=daddy) + + assert mock_tokenized.keeper() == keeper + assert keeper.strategyOwner(mock_tokenized) == management + + # deposit into the strategy + to_deposit = asset.balanceOf(user) // 2 + profit = asset.balanceOf(user) - to_deposit + + asset.approve(mock_tokenized, to_deposit, sender=user) + mock_tokenized.deposit(to_deposit, user, sender=user) + + assert mock_tokenized.totalAssets() == to_deposit + assert mock_tokenized.totalSupply() == to_deposit + + # simulate profit + asset.transfer(mock_tokenized, profit, sender=user) + + assert mock_tokenized.totalAssets() == to_deposit + assert mock_tokenized.totalSupply() == to_deposit + + with ape.reverts("!keeper"): + keeper.tend(mock_tokenized, sender=yield_manager) + + keeper.setKeeper(yield_manager, True, sender=daddy) + + keeper.tend(mock_tokenized, sender=yield_manager) diff --git a/tests/debtAllocators/yield/test_strategy_manager.py b/tests/debtAllocators/yield/test_strategy_manager.py deleted file mode 100644 index 7a7e61b..0000000 --- a/tests/debtAllocators/yield/test_strategy_manager.py +++ /dev/null @@ -1,257 +0,0 @@ -import ape -from ape import chain, project -from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES - - -def test_strategy_manager_setup(strategy_manager, mock_tokenized, daddy, yield_manager): - assert strategy_manager.strategyOwner(mock_tokenized) == ZERO_ADDRESS - assert strategy_manager.yieldManager() == yield_manager - assert strategy_manager.governance() == daddy - - -def test_add_new_strategy( - strategy_manager, mock_tokenized, daddy, yield_manager, management -): - assert strategy_manager.strategyOwner(mock_tokenized) == ZERO_ADDRESS - assert strategy_manager.yieldManager() == yield_manager - - with ape.reverts("!governance"): - strategy_manager.manageNewStrategy(mock_tokenized, sender=management) - - with ape.reverts("!pending"): - strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) - - mock_tokenized.setPendingManagement(strategy_manager, sender=management) - - tx = strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) - - assert mock_tokenized.management() == strategy_manager - - event = list(tx.decode_logs(strategy_manager.StrategyAdded))[0] - - assert event.strategy == mock_tokenized - assert event.owner == management - - assert strategy_manager.strategyOwner(mock_tokenized) == management - assert strategy_manager.yieldManager() == yield_manager - - # cannot add it again - with ape.reverts("bad address"): - strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) - - -def test_remove_strategy( - strategy_manager, mock_tokenized, yield_manager, management, user, daddy -): - # Will revert on modifier if not yet added. - with ape.reverts("!owner"): - strategy_manager.removeManagement(mock_tokenized, user, sender=management) - - mock_tokenized.setPendingManagement(strategy_manager, sender=management) - - strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) - - assert mock_tokenized.management() == strategy_manager - assert strategy_manager.strategyOwner(mock_tokenized) == management - assert strategy_manager.yieldManager() == yield_manager - - with ape.reverts("!owner"): - strategy_manager.removeManagement(mock_tokenized, user, sender=user) - - with ape.reverts("!owner"): - strategy_manager.removeManagement(mock_tokenized, sender=user) - - with ape.reverts("bad address"): - strategy_manager.removeManagement( - mock_tokenized, ZERO_ADDRESS, sender=management - ) - - with ape.reverts("bad address"): - strategy_manager.removeManagement( - mock_tokenized, mock_tokenized, sender=management - ) - - with ape.reverts("bad address"): - strategy_manager.removeManagement( - mock_tokenized, strategy_manager, sender=management - ) - - tx = strategy_manager.removeManagement(mock_tokenized, user, sender=management) - - event = list(tx.decode_logs(strategy_manager.StrategyRemoved))[0] - - assert event.strategy == mock_tokenized - assert event.newManager == user - - assert strategy_manager.strategyOwner(mock_tokenized) == ZERO_ADDRESS - - assert mock_tokenized.management() == strategy_manager - assert mock_tokenized.pendingManagement() == user - mock_tokenized.acceptManagement(sender=user) - assert mock_tokenized.management() == user - - -def test_update_owner( - strategy_manager, mock_tokenized, yield_manager, management, user, daddy -): - with ape.reverts("!owner"): - strategy_manager.updateStrategyOwner(mock_tokenized, user, sender=management) - - mock_tokenized.setPendingManagement(strategy_manager, sender=management) - - strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) - - assert mock_tokenized.management() == strategy_manager - assert strategy_manager.strategyOwner(mock_tokenized) == management - assert strategy_manager.yieldManager() == yield_manager - - with ape.reverts("!owner"): - strategy_manager.updateStrategyOwner(mock_tokenized, user, sender=user) - - with ape.reverts("bad address"): - strategy_manager.updateStrategyOwner( - mock_tokenized, ZERO_ADDRESS, sender=management - ) - - with ape.reverts("bad address"): - strategy_manager.updateStrategyOwner( - mock_tokenized, mock_tokenized, sender=management - ) - - with ape.reverts("bad address"): - strategy_manager.updateStrategyOwner( - mock_tokenized, strategy_manager, sender=management - ) - - strategy_manager.updateStrategyOwner(mock_tokenized, user, sender=management) - - assert strategy_manager.strategyOwner(mock_tokenized) == user - assert strategy_manager.yieldManager() == yield_manager - assert mock_tokenized.management() == strategy_manager - - -def test_record_full_profit( - strategy_manager, mock_tokenized, management, yield_manager, asset, user, daddy -): - mock_tokenized.setProfitMaxUnlockTime(int(60 * 60 * 24), sender=management) - mock_tokenized.setPendingManagement(strategy_manager, sender=management) - - strategy_manager.manageNewStrategy(mock_tokenized, management, sender=daddy) - - assert mock_tokenized.management() == strategy_manager - assert strategy_manager.strategyOwner(mock_tokenized) == management - assert strategy_manager.yieldManager() == yield_manager - - # deposit into the strategy - to_deposit = asset.balanceOf(user) // 2 - profit = asset.balanceOf(user) - to_deposit - - asset.approve(mock_tokenized, to_deposit, sender=user) - mock_tokenized.deposit(to_deposit, user, sender=user) - - assert mock_tokenized.totalAssets() == to_deposit - assert mock_tokenized.totalSupply() == to_deposit - - # simulate profit - asset.transfer(mock_tokenized, profit, sender=user) - - assert mock_tokenized.totalAssets() == to_deposit - assert mock_tokenized.totalSupply() == to_deposit - current_unlock_time = mock_tokenized.profitMaxUnlockTime() - assert current_unlock_time != 0 - assert mock_tokenized.pricePerShare() == 10 ** asset.decimals() - - with ape.reverts("!debt manager"): - strategy_manager.reportFullProfit(mock_tokenized, sender=user) - - strategy_manager.reportFullProfit(mock_tokenized, sender=yield_manager) - - # Profit should be fully locked - assert mock_tokenized.totalAssets() == to_deposit + profit - assert mock_tokenized.totalSupply() == to_deposit + profit - - chain.pending_timestamp += 10 - chain.mine(1) - - assert mock_tokenized.totalAssets() == to_deposit + profit - assert mock_tokenized.totalSupply() == to_deposit - assert current_unlock_time == mock_tokenized.profitMaxUnlockTime() - assert mock_tokenized.pricePerShare() > 10 ** asset.decimals() - - -def test_forward_call( - strategy_manager, mock_tokenized, management, yield_manager, asset, user, daddy -): - mock_tokenized.setPendingManagement(strategy_manager, sender=management) - - strategy_manager.manageNewStrategy(mock_tokenized, management, sender=daddy) - - assert mock_tokenized.profitMaxUnlockTime() == 0 - assert mock_tokenized.performanceFee() == 0 - - new_unlock_time = int(69) - calldata = mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)) - - with ape.reverts("!owner"): - strategy_manager.forwardCall(mock_tokenized, calldata, sender=user) - - tx = strategy_manager.forwardCall(mock_tokenized, calldata, sender=management) - - event = list(tx.decode_logs(mock_tokenized.UpdateProfitMaxUnlockTime))[0] - assert event.newProfitMaxUnlockTime == new_unlock_time - assert mock_tokenized.profitMaxUnlockTime() == new_unlock_time - - new_unlock_time = int(6699) - calldata = mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)) - - # Only management can change a performance fee. - new_fee = int(2_000) - calldata = mock_tokenized.setPerformanceFee.encode_input(new_fee) - - with ape.reverts("!owner"): - strategy_manager.forwardCall(mock_tokenized, calldata, sender=user) - - with ape.reverts("!owner"): - strategy_manager.forwardCall(mock_tokenized, calldata, sender=yield_manager) - - tx = strategy_manager.forwardCall(mock_tokenized, calldata, sender=management) - - event = list(tx.decode_logs(mock_tokenized.UpdatePerformanceFee))[0] - assert event.newPerformanceFee == new_fee - assert mock_tokenized.performanceFee() == new_fee - - -def test_forward_calls( - strategy_manager, mock_tokenized, management, yield_manager, daddy, user -): - mock_tokenized.setPendingManagement(strategy_manager, sender=management) - - strategy_manager.manageNewStrategy(mock_tokenized, sender=daddy) - - assert mock_tokenized.profitMaxUnlockTime() == 0 - assert mock_tokenized.performanceFee() == 0 - - # Yield manager can change profit max unlock - new_unlock_time = int(69) - new_fee = int(2_000) - calldatas = [ - mock_tokenized.setProfitMaxUnlockTime.encode_input(int(new_unlock_time)), - mock_tokenized.setPerformanceFee.encode_input(new_fee), - ] - - # Only management can change a performance fee. - with ape.reverts("!owner"): - strategy_manager.forwardCalls(mock_tokenized, calldatas, sender=user) - - with ape.reverts("!owner"): - strategy_manager.forwardCalls(mock_tokenized, calldatas, sender=yield_manager) - - tx = strategy_manager.forwardCalls(mock_tokenized, calldatas, sender=management) - - event = list(tx.decode_logs(mock_tokenized.UpdatePerformanceFee))[0] - assert event.newPerformanceFee == new_fee - assert mock_tokenized.performanceFee() == new_fee - - event = list(tx.decode_logs(mock_tokenized.UpdateProfitMaxUnlockTime))[0] - assert event.newProfitMaxUnlockTime == new_unlock_time - assert mock_tokenized.profitMaxUnlockTime() == new_unlock_time diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 13babbe..82fc990 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -8,11 +8,12 @@ def setup_vault(vault, strategies, oracle, chad): vault.add_strategy(strategy, sender=chad) vault.update_max_debt_for_strategy(strategy, 2**256 - 1, sender=chad) management = strategy.management() + strategy.setProfitMaxUnlockTime(1, sender=management) oracle.setOracle(strategy, strategy, sender=management) -def test_yield_manager_setup(yield_manager, daddy, vault, management, strategy_manager): - assert yield_manager.strategyManager() == strategy_manager +def test_yield_manager_setup(yield_manager, daddy, vault, management, keeper): + assert yield_manager.keeper() == keeper assert yield_manager.governance() == daddy assert yield_manager.open() == False assert yield_manager.proposer(management) == False @@ -72,7 +73,7 @@ def test_update_allocation( yield_manager, vault, management, - strategy_manager, + keeper, daddy, user, amount, @@ -84,6 +85,7 @@ def test_update_allocation( strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) @@ -188,17 +190,14 @@ def test_update_allocation( # Try and move all allocation = [(strategy_two, 0), (strategy_one, amount)] # Strategy manager isnt the strategies management - with ape.reverts("!debt manager"): + with ape.reverts("!keeper"): yield_manager.updateAllocation(vault, allocation, sender=user) - strategy_two.setPendingManagement(strategy_manager, sender=management) - strategy_two.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_two, yield_manager, sender=daddy) + strategy_two.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_two, sender=daddy) tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert len(list(tx.decode_logs(strategy_two.UpdateProfitMaxUnlockTime))) == 2 - assert generic_debt_allocator.configs(strategy_two).targetRatio == 0 assert generic_debt_allocator.configs(strategy_one).targetRatio == MAX_BPS (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) @@ -238,7 +237,7 @@ def test_update_allocation_pending_profit( yield_manager, vault, management, - strategy_manager, + keeper, daddy, user, amount, @@ -254,9 +253,9 @@ def test_update_allocation_pending_profit( generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) generic_debt_allocator.setMinimumChange(1, sender=daddy) vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - strategy_one.setPendingManagement(strategy_manager, sender=management) - strategy_one.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) + strategy_one.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_one, sender=daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) yield_manager.setProposer(user, True, sender=daddy) profit = amount // 10 @@ -278,7 +277,6 @@ def test_update_allocation_pending_profit( tx = yield_manager.updateAllocation(vault, allocation, sender=user) assert len(list(tx.decode_logs(strategy_one.Reported))) == 1 - assert len(list(tx.decode_logs(strategy_two.UpdateProfitMaxUnlockTime))) == 2 assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) @@ -297,7 +295,7 @@ def test_update_allocation_pending_loss( yield_manager, vault, management, - strategy_manager, + keeper, daddy, user, amount, @@ -313,9 +311,9 @@ def test_update_allocation_pending_loss( generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) generic_debt_allocator.setMinimumChange(1, sender=daddy) vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - strategy_one.setPendingManagement(strategy_manager, sender=management) - strategy_one.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) + strategy_one.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_one, sender=daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) yield_manager.setProposer(user, True, sender=daddy) loss = amount // 10 @@ -354,10 +352,9 @@ def test_update_allocation_pending_loss_move_half( yield_manager, vault, management, - strategy_manager, + keeper, daddy, user, - keeper, amount, asset, deploy_mock_tokenized, @@ -371,9 +368,9 @@ def test_update_allocation_pending_loss_move_half( generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) generic_debt_allocator.setMinimumChange(1, sender=daddy) vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - strategy_one.setPendingManagement(strategy_manager, sender=management) - strategy_one.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_one, yield_manager, sender=daddy) + strategy_one.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_one, sender=daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) yield_manager.setProposer(user, True, sender=daddy) loss = amount // 10 @@ -461,10 +458,9 @@ def test_get_current_and_expected( yield_manager, vault, management, - strategy_manager, + keeper, daddy, user, - keeper, amount, asset, deploy_mock_tokenized, @@ -513,7 +509,7 @@ def test_update_allocation_permissioned( yield_manager, vault, management, - strategy_manager, + keeper, daddy, user, amount, @@ -599,16 +595,15 @@ def test_update_allocation_permissioned( # Try and move all allocation = [(strategy_two, 0), (strategy_one, amount)] # Strategy manager isnt the strategies management - with ape.reverts("!debt manager"): + with ape.reverts("!keeper"): yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) - strategy_two.setPendingManagement(strategy_manager, sender=management) - strategy_two.setProfitMaxUnlockTime(int(200), sender=management) - strategy_manager.manageNewStrategy(strategy_two, yield_manager, sender=daddy) + strategy_two.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_two, sender=daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) - assert len(list(tx.decode_logs(strategy_two.UpdateProfitMaxUnlockTime))) == 2 assert len(list(tx.decode_logs(strategy_two.Reported))) == 1 assert generic_debt_allocator.configs(strategy_two).targetRatio == 0 From 2ee855d9af8286e378754233a9dd0248b85c2d99 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Wed, 20 Dec 2023 11:41:02 -0700 Subject: [PATCH 17/35] fix: removal --- contracts/debtAllocators/YieldManager/Keeper.sol | 9 +++++---- tests/debtAllocators/yield/test_keeper_contract.py | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/Keeper.sol b/contracts/debtAllocators/YieldManager/Keeper.sol index 58350ea..6db6c9d 100644 --- a/contracts/debtAllocators/YieldManager/Keeper.sol +++ b/contracts/debtAllocators/YieldManager/Keeper.sol @@ -83,12 +83,13 @@ contract Keeper is Governance { } /** - * @notice Removes the management of a strategy, transferring it to the `owner`. + * @notice Removes the strategy. * @param _strategy The address of the strategy. */ - function removeStrategy( - address _strategy - ) external onlyStrategyOwner(_strategy) { + function removeStrategy(address _strategy) external { + // Only governance or the strategy owner can call. + if (msg.sender != governance) _checkStrategyOwner(_strategy); + delete strategyOwner[_strategy]; emit StrategyRemoved(_strategy); diff --git a/tests/debtAllocators/yield/test_keeper_contract.py b/tests/debtAllocators/yield/test_keeper_contract.py index 24ab246..3ae3561 100644 --- a/tests/debtAllocators/yield/test_keeper_contract.py +++ b/tests/debtAllocators/yield/test_keeper_contract.py @@ -10,6 +10,7 @@ def test_keeper_setup(keeper, mock_tokenized, daddy, yield_manager): def test_add_new_strategy(keeper, mock_tokenized, daddy, yield_manager, management): + mock_tokenized.setKeeper(daddy, sender=management) assert keeper.strategyOwner(mock_tokenized) == ZERO_ADDRESS assert keeper.keepers(yield_manager) == False @@ -37,9 +38,7 @@ def test_add_new_strategy(keeper, mock_tokenized, daddy, yield_manager, manageme keeper.addNewStrategy(mock_tokenized, sender=daddy) -def test_remove_strategy( - keeper, mock_tokenized, yield_manager, management, user, daddy -): +def test_remove_strategy(keeper, mock_tokenized, management, user, daddy): # Will revert on modifier if not yet added. with ape.reverts("!owner"): keeper.removeStrategy(mock_tokenized, sender=management) From 533726075095565f42ec4fc4c5a5688aeec6f81e Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Wed, 20 Dec 2023 12:08:28 -0700 Subject: [PATCH 18/35] feat: report when going to 0 --- .../debtAllocators/GenericDebtAllocator.sol | 5 ++ .../test_generic_debt_allocator.py | 6 +- .../yield/test_yield_manager.py | 65 ++++++++++++++----- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index 4ae47e4..a66d3d5 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -156,6 +156,11 @@ contract GenericDebtAllocator is Governance { ) external virtual onlyKeepers { IVault _vault = IVault(vault); + // If going to 0 record full balance first. + if (_targetDebt == 0) { + _vault.process_report(_strategy); + } + // Cache initial values in case of loss. uint256 initialDebt = _vault.strategies(_strategy).current_debt; uint256 initialAssets = _vault.totalAssets(); diff --git a/tests/debtAllocators/test_generic_debt_allocator.py b/tests/debtAllocators/test_generic_debt_allocator.py index eade9ea..d41e90a 100644 --- a/tests/debtAllocators/test_generic_debt_allocator.py +++ b/tests/debtAllocators/test_generic_debt_allocator.py @@ -297,7 +297,11 @@ def test_update_debt( with ape.reverts("not allowed"): generic_debt_allocator.update_debt(strategy, amount, sender=daddy) - vault.add_role(generic_debt_allocator, ROLES.DEBT_MANAGER, sender=daddy) + vault.add_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) generic_debt_allocator.update_debt(strategy, amount, sender=daddy) diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 82fc990..0a7663a 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -109,7 +109,11 @@ def test_update_allocation( generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.add_role(generic_debt_allocator, ROLES.DEBT_MANAGER, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) tx = yield_manager.updateAllocation(vault, allocation, sender=user) @@ -253,6 +257,11 @@ def test_update_allocation_pending_profit( generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) generic_debt_allocator.setMinimumChange(1, sender=daddy) vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) strategy_one.setKeeper(keeper, sender=management) keeper.addNewStrategy(strategy_one, sender=daddy) keeper.setKeeper(yield_manager, True, sender=daddy) @@ -264,7 +273,7 @@ def test_update_allocation_pending_profit( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - vault.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -283,11 +292,15 @@ def test_update_allocation_pending_profit( assert bool == True assert bytes == vault.update_debt.encode_input(strategy_one, 0) - vault.update_debt(strategy_one, 0, sender=daddy) + tx = generic_debt_allocator.update_debt(strategy_one, 0, sender=daddy) + + event = list(tx.decode_logs(vault.StrategyReported)) + assert len(event) == 1 + assert event[0].gain == profit (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True - assert bytes == vault.update_debt.encode_input(strategy_two, amount) + assert bytes == vault.update_debt.encode_input(strategy_two, amount + profit) def test_update_allocation_pending_loss( @@ -311,6 +324,11 @@ def test_update_allocation_pending_loss( generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) generic_debt_allocator.setMinimumChange(1, sender=daddy) vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) strategy_one.setKeeper(keeper, sender=management) keeper.addNewStrategy(strategy_one, sender=daddy) keeper.setKeeper(yield_manager, True, sender=daddy) @@ -321,7 +339,7 @@ def test_update_allocation_pending_loss( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - vault.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -340,7 +358,7 @@ def test_update_allocation_pending_loss( assert bool == True assert bytes == vault.update_debt.encode_input(strategy_one, 0) - vault.update_debt(strategy_one, 0, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, 0, sender=daddy) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True @@ -368,6 +386,11 @@ def test_update_allocation_pending_loss_move_half( generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) generic_debt_allocator.setMinimumChange(1, sender=daddy) vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) strategy_one.setKeeper(keeper, sender=management) keeper.addNewStrategy(strategy_one, sender=daddy) keeper.setKeeper(yield_manager, True, sender=daddy) @@ -378,7 +401,7 @@ def test_update_allocation_pending_loss_move_half( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - vault.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -400,7 +423,7 @@ def test_update_allocation_pending_loss_move_half( (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) assert bool == True - vault.update_debt(strategy_one, amount - to_move, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount - to_move, sender=daddy) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True @@ -422,6 +445,11 @@ def test_validate_allocation( strategy_two = deploy_mock_tokenized("two") setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) @@ -435,7 +463,7 @@ def test_validate_allocation( vault, [(strategy_one, amount), (strategy_two, 0)] ) - vault.update_debt(strategy_one, amount // 2, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount // 2, sender=daddy) assert yield_manager.validateAllocation(vault, []) == False assert yield_manager.validateAllocation(vault, [(strategy_one, amount)]) @@ -444,7 +472,9 @@ def test_validate_allocation( ) # Now will be false - vault.update_debt(strategy_two, vault.totalIdle() // 2, sender=daddy) + generic_debt_allocator.update_debt( + strategy_two, vault.totalIdle() // 2, sender=daddy + ) assert yield_manager.validateAllocation(vault, []) == False assert yield_manager.validateAllocation(vault, [(strategy_one, amount)]) == False @@ -524,8 +554,11 @@ def test_update_allocation_permissioned( yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) generic_debt_allocator.setMinimumChange(1, sender=daddy) + vault.set_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) vault.set_role( - yield_manager, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, ) asset.approve(vault, amount, sender=user) @@ -550,7 +583,7 @@ def test_update_allocation_permissioned( assert bool == True assert bytes == vault.update_debt.encode_input(strategy_two, amount) - vault.update_debt(strategy_two, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_two, amount, sender=daddy) allocation = [(strategy_one, amount)] with ape.reverts("ratio too high"): @@ -579,13 +612,13 @@ def test_update_allocation_permissioned( strategy_two.address, amount - to_move ) - vault.update_debt(strategy_two, amount - to_move, sender=daddy) + generic_debt_allocator.update_debt(strategy_two, amount - to_move, sender=daddy) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, to_move) - vault.update_debt(strategy_one, to_move, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, to_move, sender=daddy) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) @@ -614,13 +647,13 @@ def test_update_allocation_permissioned( assert bool_two == True assert bytes_two == vault.update_debt.encode_input(strategy_two.address, 0) - vault.update_debt(strategy_two, 0, sender=daddy) + generic_debt_allocator.update_debt(strategy_two, 0, sender=daddy) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, amount) - vault.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) From 232b754f39bee038a7c8d22119ccff018fb4d72b Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 21 Dec 2023 09:38:20 -0700 Subject: [PATCH 19/35] feat: script and changes --- .../YieldManager/YieldManager.sol | 58 +++++++++-------- scripts/deploy_role_manager.py | 2 +- scripts/deploy_yield_manager.py | 63 +++++++++++++++++++ tests/conftest.py | 4 +- 4 files changed, 97 insertions(+), 30 deletions(-) create mode 100644 scripts/deploy_yield_manager.py diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index f73bbec..9a51d16 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.18; import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; @@ -48,7 +48,7 @@ contract YieldManager is Governance { /// @notice Contract that holds the logic and oracles for each strategy. AprOracle internal constant aprOracle = - AprOracle(0x02b0210fC1575b38147B232b40D7188eF14C04f2); + AprOracle(0xF012fBb9283e03994A7829fCE994a105cC066c14); /// @notice Flag to set to allow anyone to propose allocations. bool public open; @@ -62,7 +62,7 @@ contract YieldManager is Governance { /// @notice Mapping for vaults that can be allocated for => its debt allocator. mapping(address => address) public vaultAllocator; - constructor(address _keeper, address _governance) Governance(_governance) { + constructor(address _governance, address _keeper) Governance(_governance) { keeper = _keeper; } @@ -146,13 +146,6 @@ contract YieldManager is Governance { // Add to the amount currently being earned. _currentYield += _strategyApr; - // If no change move to the next strategy. - if (_newDebt == _currentDebt) { - // We assume the new apr will be the same as current. - _afterYield += _strategyApr; - continue; - } - // If we are withdrawing. if (_currentDebt > _newDebt) { // If we are pulling all debt from a strategy. @@ -178,14 +171,24 @@ contract YieldManager is Governance { ? (_newDebt * MAX_BPS) / _totalAssets : MAX_BPS; - // Update allocation. - GenericDebtAllocator(allocator).setStrategyDebtRatios( - _strategy, - _targetRatio - ); + // If different than the current target. + if ( + GenericDebtAllocator(allocator).getStrategyTargetRatio( + _strategy + ) != _targetRatio + ) { + // Update allocation. + GenericDebtAllocator(allocator).setStrategyDebtRatios( + _strategy, + _targetRatio + ); + } - // Get the new APR - if (_newDebt != 0) { + // Add the expected change in APR based on new debt. + if (_newDebt == _currentDebt) { + // We assume the new apr will be the same as current. + _afterYield += _strategyApr; + } else if (_newDebt != 0) { _afterYield += (aprOracle.getStrategyApr( _strategy, int256(_newDebt) - int256(_currentDebt) @@ -300,11 +303,6 @@ contract YieldManager is Governance { // Get the debt the strategy current has. _currentDebt = IVault(_vault).strategies(_strategy).current_debt; - // If no change move to the next strategy. - if (_newDebt == _currentDebt) { - continue; - } - // If we are withdrawing. if (_currentDebt > _newDebt) { // If we are pulling all debt from a strategy. @@ -330,11 +328,17 @@ contract YieldManager is Governance { ? (_newDebt * MAX_BPS) / _totalAssets : MAX_BPS; - // Update allocation. - GenericDebtAllocator(allocator).setStrategyDebtRatios( - _strategy, - _targetRatio - ); + if ( + GenericDebtAllocator(allocator).getStrategyTargetRatio( + _strategy + ) != _targetRatio + ) { + // Update allocation. + GenericDebtAllocator(allocator).setStrategyDebtRatios( + _strategy, + _targetRatio + ); + } } } diff --git a/scripts/deploy_role_manager.py b/scripts/deploy_role_manager.py index e547d14..0501041 100644 --- a/scripts/deploy_role_manager.py +++ b/scripts/deploy_role_manager.py @@ -3,7 +3,7 @@ from hexbytes import HexBytes import hashlib -deployer = accounts.load("v3_deployer") +deployer = accounts.load("") def deploy_role_manager(): diff --git a/scripts/deploy_yield_manager.py b/scripts/deploy_yield_manager.py new file mode 100644 index 0000000..7ada604 --- /dev/null +++ b/scripts/deploy_yield_manager.py @@ -0,0 +1,63 @@ +from ape import project, accounts, Contract, chain, networks, managers, compilers +from ape.utils import ZERO_ADDRESS +from hexbytes import HexBytes +import hashlib + +deployer = accounts.load("") + + +def deploy_yield_manager(): + + print("Deploying Yield Manager on ChainID", chain.chain_id) + + if input("Do you want to continue? ") == "n": + return + + yield_manager = project.YieldManager + deployer_contract = project.Deployer.at( + "0x8D85e7c9A4e369E53Acc8d5426aE1568198b0112" + ) + + salt_string = "Yield Manager test" + + # Create a SHA-256 hash object + hash_object = hashlib.sha256() + # Update the hash object with the string data + hash_object.update(salt_string.encode("utf-8")) + # Get the hexadecimal representation of the hash + hex_hash = hash_object.hexdigest() + # Convert the hexadecimal hash to an integer + salt = int(hex_hash, 16) + + print(f"Salt we are using {salt}") + print("Init balance:", deployer.balance / 1e18) + + print(f"Deploying the Yield Manager...") + print("Enter the addresses to use on deployment.") + + gov = input("Governance? ") + keeper = input("Keeper? ") + + constructor = yield_manager.constructor.encode_input( + gov, + keeper, + ) + + deploy_bytecode = HexBytes( + HexBytes(yield_manager.contract_type.deployment_bytecode.bytecode) + constructor + ) + + tx = deployer_contract.deploy(deploy_bytecode, salt, sender=deployer) + + event = list(tx.decode_logs(deployer_contract.Deployed)) + + address = event[0].addr + + print("------------------") + print(f"Deployed the Yield Manager to {address}") + print("------------------") + print(f"Encoded Constructor to use for verifaction {constructor.hex()[2:]}") + + +def main(): + deploy_yield_manager() diff --git a/tests/conftest.py b/tests/conftest.py index e8882cf..fc2a761 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -483,7 +483,7 @@ def keeper(deploy_keeper): @pytest.fixture(scope="session") def deploy_yield_manager(project, daddy, keeper): def deploy_yield_manager(): - yield_manager = daddy.deploy(project.YieldManager, keeper, daddy) + yield_manager = daddy.deploy(project.YieldManager, daddy, keeper) return yield_manager @@ -500,6 +500,6 @@ def yield_manager(deploy_yield_manager): @pytest.fixture(scope="session") def apr_oracle(project): oracle = project.MockOracle - address = "0x02b0210fC1575b38147B232b40D7188eF14C04f2" + address = "0xF012fBb9283e03994A7829fCE994a105cC066c14" networks.provider.set_code(address, oracle.contract_type.runtime_bytecode.bytecode) yield oracle.at(address) From ac75d6fce310f73294e7415704198c05bb4cb932 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 21 Dec 2023 15:24:04 -0700 Subject: [PATCH 20/35] fix: make virtual --- .../debtAllocators/GenericDebtAllocator.sol | 6 +++--- .../debtAllocators/YieldManager/Keeper.sol | 10 +++++----- .../YieldManager/YieldManager.sol | 20 ++++++++++++------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index a66d3d5..84c003b 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -82,13 +82,13 @@ contract GenericDebtAllocator is Governance { // Can't be more than 10_000. uint256 public debtRatio; + /// @notice Time to wait between debt updates. + uint256 public minimumWait; + /// @notice The minimum amount denominated in asset that will // need to be moved to trigger a debt update. uint256 public minimumChange; - /// @notice Time to wait between debt updates. - uint256 public minimumWait; - /// @notice Max loss to accept on debt updates in basis points. uint256 public maxDebtUpdateLoss; diff --git a/contracts/debtAllocators/YieldManager/Keeper.sol b/contracts/debtAllocators/YieldManager/Keeper.sol index 6db6c9d..df78427 100644 --- a/contracts/debtAllocators/YieldManager/Keeper.sol +++ b/contracts/debtAllocators/YieldManager/Keeper.sol @@ -53,7 +53,7 @@ contract Keeper is Governance { * @notice Add a new strategy, using the current `management` as the owner. * @param _strategy The address of the strategy. */ - function addNewStrategy(address _strategy) external onlyGovernance { + function addNewStrategy(address _strategy) external virtual onlyGovernance { address currentManager = IStrategy(_strategy).management(); require(strategyOwner[_strategy] == address(0), "already active"); require(IStrategy(_strategy).keeper() == address(this), "!keeper"); @@ -72,7 +72,7 @@ contract Keeper is Governance { function updateStrategyOwner( address _strategy, address _newOwner - ) external onlyStrategyOwner(_strategy) { + ) external virtual onlyStrategyOwner(_strategy) { require( _newOwner != address(0) && _newOwner != address(this) && @@ -86,7 +86,7 @@ contract Keeper is Governance { * @notice Removes the strategy. * @param _strategy The address of the strategy. */ - function removeStrategy(address _strategy) external { + function removeStrategy(address _strategy) external virtual { // Only governance or the strategy owner can call. if (msg.sender != governance) _checkStrategyOwner(_strategy); @@ -99,7 +99,7 @@ contract Keeper is Governance { * @notice Reports full profit for a strategy. * @param _strategy The address of the strategy. */ - function report(address _strategy) external onlyKeepers(_strategy) { + function report(address _strategy) external virtual onlyKeepers(_strategy) { // Report profits. IStrategy(_strategy).report(); } @@ -108,7 +108,7 @@ contract Keeper is Governance { * @notice Tends a strategy. * @param _strategy The address of the strategy. */ - function tend(address _strategy) external onlyKeepers(_strategy) { + function tend(address _strategy) external virtual onlyKeepers(_strategy) { // Tend. IStrategy(_strategy).tend(); } diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 9a51d16..aab5c12 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -84,7 +84,7 @@ contract YieldManager is Governance { function updateAllocationPermissioned( address _vault, Allocation[] memory _newAllocations - ) public onlyGovernance { + ) public virtual onlyGovernance { // Move funds _allocate(_vault, _newAllocations); } @@ -113,6 +113,7 @@ contract YieldManager is Governance { Allocation[] memory _newAllocations ) external + virtual onlyProposersOrOpen returns (uint256 _currentYield, uint256 _afterYield) { @@ -213,7 +214,7 @@ contract YieldManager is Governance { function validateAllocation( address _vault, Allocation[] memory _newAllocations - ) external view returns (bool) { + ) external view virtual returns (bool) { // Get the total assets the vault has. uint256 _totalAssets = IVault(_vault).totalAssets(); @@ -245,7 +246,12 @@ contract YieldManager is Governance { function getCurrentAndExpectedYield( address _vault, Allocation[] memory _newAllocations - ) external view returns (uint256 _currentYield, uint256 _expectedYield) { + ) + external + view + virtual + returns (uint256 _currentYield, uint256 _expectedYield) + { // Get the total assets the vault has. uint256 _totalAssets = IVault(_vault).totalAssets(); @@ -290,7 +296,7 @@ contract YieldManager is Governance { function _allocate( address _vault, Allocation[] memory _newAllocations - ) internal { + ) internal virtual { address allocator = vaultAllocator[_vault]; require(allocator != address(0), "vault not added"); address _strategy; @@ -350,7 +356,7 @@ contract YieldManager is Governance { function setProposer( address _address, bool _allowed - ) external onlyGovernance { + ) external virtual onlyGovernance { proposer[_address] = _allowed; emit UpdateProposer(_address, _allowed); @@ -364,7 +370,7 @@ contract YieldManager is Governance { function setVaultAllocator( address _vault, address _allocator - ) external onlyGovernance { + ) external virtual onlyGovernance { vaultAllocator[_vault] = _allocator; emit UpdateVaultAllocator(_vault, _allocator); @@ -374,7 +380,7 @@ contract YieldManager is Governance { * @notice Sets the open status of the contract. * @param _open The new open status to set. */ - function setOpen(bool _open) external onlyGovernance { + function setOpen(bool _open) external virtual onlyGovernance { open = _open; emit UpdateOpen(_open); From 3cbfec6e618be9482969687e72985f6059d87549 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 21 Dec 2023 16:51:09 -0700 Subject: [PATCH 21/35] fix: formatting --- contracts/Managers/RoleManager.sol | 20 +++++++++---------- .../debtAllocators/GenericDebtAllocator.sol | 9 +++++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/contracts/Managers/RoleManager.sol b/contracts/Managers/RoleManager.sol index 843e5cf..38a4973 100644 --- a/contracts/Managers/RoleManager.sol +++ b/contracts/Managers/RoleManager.sol @@ -11,6 +11,16 @@ import {GenericDebtAllocatorFactory} from "../debtAllocators/GenericDebtAllocato /// @title Yearn V3 Vault Role Manager. contract RoleManager is Governance2Step { + /// @notice Emitted when a new vault has been deployed or added. + event AddedNewVault( + address indexed vault, + address indexed debtAllocator, + uint256 rating + ); + + /// @notice Emitted when a vault is removed. + event RemovedVault(address indexed vault); + /// @notice Emitted when a new address is set for a position. event UpdatePositionHolder( bytes32 indexed position, @@ -23,16 +33,6 @@ contract RoleManager is Governance2Step { /// @notice Emitted when the defaultProfitMaxUnlock variable is updated. event UpdateDefaultProfitMaxUnlock(uint256 newDefaultProfitMaxUnlock); - /// @notice Emitted when a new vault has been deployed or added. - event AddedNewVault( - address indexed vault, - address indexed debtAllocator, - uint256 rating - ); - - /// @notice Emitted when a vault is removed. - event RemovedVault(address indexed vault); - /// @notice Config that holds all vault info. struct VaultConfig { address asset; diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index 84c003b..e3bc6ac 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -123,16 +123,17 @@ contract GenericDebtAllocator is Governance { uint256 _minimumChange ) public virtual { require(address(vault) == address(0), "!initialized"); + // Set initial variables. vault = _vault; governance = _governance; + minimumChange = _minimumChange; + + // Default max loss on debt updates to 1 BP. + maxDebtUpdateLoss = 1; // Default to allow governance to be a keeper. keepers[_governance] = true; - - minimumChange = _minimumChange; // Default max base fee to uint256 max maxAcceptableBaseFee = type(uint256).max; - // Default max loss on debt updates to 1 BP. - maxDebtUpdateLoss = 1; } /** From fc3b236c152d06d93bc4dcffff3b8bd14d6608b4 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Wed, 27 Dec 2023 18:30:26 -0700 Subject: [PATCH 22/35] feat: check max redeem and deposit --- .github/workflows/test.yaml | 2 +- contracts/Mocks/MockTokenizedStrategy.sol | 16 ++ .../debtAllocators/GenericDebtAllocator.sol | 2 +- .../debtAllocators/YieldManager/Keeper.sol | 3 +- .../YieldManager/YieldManager.sol | 144 ++++++++-- .../yield/test_yield_manager.py | 270 +++++++++++++++++- 6 files changed, 404 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2cbf43a..3471a2f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 16 - - uses: ApeWorX/github-action@v2.4 + - uses: ApeWorX/github-action@v2.0 with: python-version: '3.10' diff --git a/contracts/Mocks/MockTokenizedStrategy.sol b/contracts/Mocks/MockTokenizedStrategy.sol index 7680c3a..8709463 100644 --- a/contracts/Mocks/MockTokenizedStrategy.sol +++ b/contracts/Mocks/MockTokenizedStrategy.sol @@ -6,6 +6,7 @@ import {MockTokenizedStrategy} from "@yearn-vaults/test/mocks/ERC4626/MockTokeni contract MockTokenized is MockTokenizedStrategy { uint256 public apr; uint256 public loss; + uint256 public limit; constructor( address _asset, @@ -35,4 +36,19 @@ contract MockTokenized is MockTokenizedStrategy { } function tendThis(uint256) external {} + + function availableWithdrawLimit( + address _owner + ) public view virtual override returns (uint256) { + if (limit != 0) { + uint256 _totalAssets = strategyStorage().totalIdle; + return _totalAssets > limit ? _totalAssets - limit : 0; + } else { + return super.availableWithdrawLimit(_owner); + } + } + + function setLimit(uint256 _limit) external { + limit = _limit; + } } diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index e3bc6ac..edd4fe5 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -82,7 +82,7 @@ contract GenericDebtAllocator is Governance { // Can't be more than 10_000. uint256 public debtRatio; - /// @notice Time to wait between debt updates. + /// @notice Time to wait between debt updates in seconds. uint256 public minimumWait; /// @notice The minimum amount denominated in asset that will diff --git a/contracts/debtAllocators/YieldManager/Keeper.sol b/contracts/debtAllocators/YieldManager/Keeper.sol index df78427..69d255a 100644 --- a/contracts/debtAllocators/YieldManager/Keeper.sol +++ b/contracts/debtAllocators/YieldManager/Keeper.sol @@ -54,10 +54,11 @@ contract Keeper is Governance { * @param _strategy The address of the strategy. */ function addNewStrategy(address _strategy) external virtual onlyGovernance { - address currentManager = IStrategy(_strategy).management(); require(strategyOwner[_strategy] == address(0), "already active"); require(IStrategy(_strategy).keeper() == address(this), "!keeper"); + address currentManager = IStrategy(_strategy).management(); + // Store the owner of the strategy. strategyOwner[_strategy] = currentManager; diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index aab5c12..4ff59a5 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -90,23 +90,24 @@ contract YieldManager is Governance { } /** - * @notice Update a `_vault`s allocation of debt. + * @notice Update a `_vault`s target allocation of debt. * @dev This takes the address of a vault and an array of - * its strategies and their specific allocation. + * its strategies and their specific target allocations. * * The `_newAllocations` array should: * - Contain all strategies that hold any amount of debt from the vault * even if the debt wont be adjusted in order to get the correct - * on chain APR. + * on chain rate. * - Be ordered so that all debt decreases are at the beginning of the array * and debt increases at the end. - * - * It is expected that the proposer does all needed checks for values such - * as max_debt, maxWithdraw, min total Idle etc. that are enforced on debt - * updates at the vault level. + * - Account for all limiting values such as the vaults max_debt and min_total_idle + * as well as the strategies maxDeposit/maxRedeem that are enforced on debt updates. + * - Account for the expected differences in amounts caused unrealised losses or profits. * * @param _vault The address of the vault to propose an allocation for. * @param _newAllocations Array of strategies and their new proposed allocation. + * @return _currentRate The current weighted rate that the collective strategies are earning. + * @return _expectedRate The expected weighted rate that the collective strategies would earn. */ function updateAllocation( address _vault, @@ -115,7 +116,7 @@ contract YieldManager is Governance { external virtual onlyProposersOrOpen - returns (uint256 _currentYield, uint256 _afterYield) + returns (uint256 _currentRate, uint256 _expectedRate) { address allocator = vaultAllocator[_vault]; require(allocator != address(0), "vault not added"); @@ -140,18 +141,18 @@ contract YieldManager is Governance { // Add to what we have accounted for. _accountedFor += _currentDebt; - // Get the current weighted APR the strategy is earning - uint256 _strategyApr = (aprOracle.getStrategyApr(_strategy, 0) * + // Get the current weighted rate the strategy is earning + uint256 _strategyRate = (aprOracle.getStrategyApr(_strategy, 0) * _currentDebt); // Add to the amount currently being earned. - _currentYield += _strategyApr; + _currentRate += _strategyRate; // If we are withdrawing. if (_currentDebt > _newDebt) { // If we are pulling all debt from a strategy. if (_newDebt == 0) { - // We need to report profits and have them immediately unlock to not lose out on locked profit. + // We need to report profits to have them start to unlock. Keeper(keeper).report(_strategy); } @@ -163,8 +164,27 @@ contract YieldManager is Governance { ) != 0 ) { // Realize the loss. - IVault(_vault).process_report(_strategy); + (, uint256 _loss) = IVault(_vault).process_report( + _strategy + ); + // Update balances. + _currentDebt -= _loss; + _totalAssets -= _loss; + _accountedFor -= _loss; } + + // Make sure we the vault can withdraw that amount. + require( + _maxWithdraw(_vault, _strategy) >= _currentDebt - _newDebt, + "max withdraw" + ); + } else if (_currentDebt < _newDebt) { + // Make sure the vault can deposit the desired amount. + require( + IVault(_strategy).maxDeposit(_vault) >= + _newDebt - _currentDebt, + "max deposit" + ); } // Get the target based on the new debt. @@ -185,21 +205,24 @@ contract YieldManager is Governance { ); } - // Add the expected change in APR based on new debt. + // If the new and current debt are the same. if (_newDebt == _currentDebt) { - // We assume the new apr will be the same as current. - _afterYield += _strategyApr; + // We assume the new rate will be the same as current. + _expectedRate += _strategyRate; } else if (_newDebt != 0) { - _afterYield += (aprOracle.getStrategyApr( + _expectedRate += (aprOracle.getStrategyApr( _strategy, - int256(_newDebt) - int256(_currentDebt) + int256(_newDebt) - int256(_currentDebt) // Debt change. ) * _newDebt); } } + // Make sure the minimum_total_idle was respected. + _checkMinimumTotalIdle(_vault, allocator); // Make sure the ending amounts are the same otherwise rates could be wrong. require(_totalAssets == _accountedFor, "cheater"); - require(_afterYield > _currentYield, "fail"); + // Make sure we expect to earn more than we currently are. + require(_expectedRate > _currentRate, "fail"); } /** @@ -235,13 +258,15 @@ contract YieldManager is Governance { } /** - * @notice Get the current weighted yield the vault is earning and the expected - * APR based on the proposed changes. + * @notice Get the current weighted yield rate the vault is earning + * and the expected rate based on the proposed changes. * * Must divide by the totalAssets to get the APR as 1e18. * * @param _vault The address of the vault to propose an allocation for. * @param _newAllocations Array of strategies and their new proposed allocation. + * @return _currentRate The current weighted rate that the collective strategies are earning. + * @return _expectedRate The expected weighted rate that the collective strategies would earn. */ function getCurrentAndExpectedYield( address _vault, @@ -250,7 +275,7 @@ contract YieldManager is Governance { external view virtual - returns (uint256 _currentYield, uint256 _expectedYield) + returns (uint256 _currentRate, uint256 _expectedRate) { // Get the total assets the vault has. uint256 _totalAssets = IVault(_vault).totalAssets(); @@ -266,20 +291,20 @@ contract YieldManager is Governance { _strategy = _newAllocations[i].strategy; _currentDebt = IVault(_vault).strategies(_strategy).current_debt; - // Get the current weighted APR the strategy is earning - uint256 _strategyApr = (aprOracle.getStrategyApr(_strategy, 0) * + // Get the current weighted rate the strategy is earning + uint256 _strategyRate = (aprOracle.getStrategyApr(_strategy, 0) * _currentDebt); // Add to the amount currently being earned. - _currentYield += _strategyApr; + _currentRate += _strategyRate; // If the strategies debt is not changing. if (_currentDebt == _newDebt) { // No need to call the APR oracle again. - _expectedYield += _strategyApr; + _expectedRate += _strategyRate; } else { - // We add what its expected to yield and its new expected debt - _expectedYield += (aprOracle.getStrategyApr( + // We add the expected rate with the new debt. + _expectedRate += (aprOracle.getStrategyApr( _strategy, int256(_newDebt) - int256(_currentDebt) ) * _newDebt); @@ -325,8 +350,26 @@ contract YieldManager is Governance { ) != 0 ) { // Realize the loss. - IVault(_vault).process_report(_strategy); + (, uint256 _loss) = IVault(_vault).process_report( + _strategy + ); + // Update balances. + _currentDebt -= _loss; + _totalAssets -= _loss; } + + // Make sure we the vault can withdraw that amount. + require( + _maxWithdraw(_vault, _strategy) >= _currentDebt - _newDebt, + "max withdraw" + ); + } else if (_currentDebt < _newDebt) { + // Make sure the vault can deposit the desired amount. + require( + IVault(_strategy).maxDeposit(_vault) >= + _newDebt - _currentDebt, + "max deposit" + ); } // Get the target based on the new debt. @@ -348,6 +391,49 @@ contract YieldManager is Governance { } } + /** + * @dev Helper function to get the max a vault can withdraw from a strategy to + * avoid stack to deep. + * + * Uses maxRedeem and convertToAssets since that is what the vault uses. + */ + function _maxWithdraw( + address _vault, + address _strategy + ) internal view virtual returns (uint256) { + return + IVault(_strategy).convertToAssets( + IVault(_strategy).maxRedeem(_vault) + ); + } + + /** + * @dev Helper function to check that the minimum_total_idle of the vault + * is accounted for in the allocation given. + * + * The expected Rate could be wrong if it allocated funds not allowed to be deployed. + * + * Use a separate function to avoid stack to deep. + */ + function _checkMinimumTotalIdle( + address _vault, + address _allocator + ) internal view virtual { + uint256 totalRatio = GenericDebtAllocator(_allocator).debtRatio(); + uint256 minIdle = IVault(_vault).minimum_total_idle(); + + // No need if minIdle is 0. + if (minIdle != 0) { + // Make sure we wouldn't allocate more than allowed. + require( + // Use 1e18 precision for more exact checks. + 1e18 - (1e14 * totalRatio) >= + (minIdle * 1e18) / IVault(_vault).totalAssets(), + "min idle" + ); + } + } + /** * @notice Sets the permission for a proposer. * @param _address The address of the proposer. diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 0a7663a..d4ed7a4 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -144,6 +144,9 @@ def test_update_allocation( with ape.reverts("ratio too high"): yield_manager.updateAllocation(vault, allocation, sender=user) + print( + f"Max redeem is {strategy_two.maxRedeem(vault)}, Max withdraw is {strategy_two.convertToAssets(strategy_two.maxRedeem(vault))}, amount is {amount}" + ) allocation = [(strategy_two, amount // 2)] with ape.reverts("fail"): yield_manager.updateAllocation(vault, allocation, sender=user) @@ -184,7 +187,6 @@ def test_update_allocation( assert bool_one == False assert bool_two == False - # assert len(list(tx.decode_logs(vault.DebtUpdated))) == 2 assert vault.totalIdle() == 0 assert vault.totalDebt() == amount assert vault.totalAssets() == amount @@ -412,6 +414,71 @@ def test_update_allocation_pending_loss_move_half( strategy_one.report(sender=keeper) to_move = amount // 2 + allocation = [(strategy_one, amount - loss - to_move), (strategy_two, to_move)] + + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 + assert vault.totalAssets() < amount + + assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert bool == True + + generic_debt_allocator.update_debt(strategy_one, amount - to_move, sender=daddy) + + (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy_two, to_move - loss) + + +def test_update_allocation_pending_loss_move_all( + apr_oracle, + yield_manager, + vault, + management, + keeper, + daddy, + user, + amount, + asset, + deploy_mock_tokenized, + generic_debt_allocator, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) + vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) + strategy_one.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_one, sender=daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) + + loss = amount // 10 + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + + assert vault.totalAssets() == amount + assert vault.totalDebt() == amount + assert vault.strategies(strategy_one).current_debt == amount + + # Record strategy loss + asset.transfer(user, loss, sender=strategy_one) + strategy_one.report(sender=keeper) + + to_move = amount allocation = [(strategy_one, amount - to_move), (strategy_two, to_move)] tx = yield_manager.updateAllocation(vault, allocation, sender=user) @@ -665,3 +732,204 @@ def test_update_allocation_permissioned( assert vault.totalAssets() == amount assert vault.strategies(strategy_one).current_debt == amount assert vault.strategies(strategy_two).current_debt == 0 + + +def test_update_allocation__max_withdraw( + apr_oracle, + yield_manager, + vault, + management, + keeper, + daddy, + user, + amount, + asset, + deploy_mock_tokenized, + generic_debt_allocator, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) + vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) + strategy_one.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_one, sender=daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + + assert vault.totalAssets() == amount + assert vault.totalDebt() == amount + assert vault.strategies(strategy_one).current_debt == amount + + limit = amount // 2 + + # Simulate a max withdraw limit + strategy_one.setLimit(limit, sender=daddy) + + # Try and move all + allocation = [(strategy_one, 0), (strategy_two, amount)] + + with ape.reverts("max withdraw"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + # Can withdraw up to the limit + allocation = [(strategy_one, amount - limit), (strategy_two, limit)] + yield_manager.updateAllocation(vault, allocation, sender=user) + + # lower the limit to 0 + strategy_one.setLimit(0, sender=daddy) + + # Now can move everything. + allocation = [(strategy_one, 0), (strategy_two, amount)] + yield_manager.updateAllocation(vault, allocation, sender=user) + + +def test_update_allocation__max_deposit( + apr_oracle, + yield_manager, + vault, + management, + keeper, + daddy, + user, + amount, + asset, + deploy_mock_tokenized, + generic_debt_allocator, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) + vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) + strategy_one.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_one, sender=daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + + assert vault.totalAssets() == amount + assert vault.totalDebt() == amount + assert vault.strategies(strategy_one).current_debt == amount + + limit = amount // 2 + + # Simulate a max deposit limit + strategy_two.setMaxDebt(limit, sender=daddy) + + # Try and move all + allocation = [(strategy_one, 0), (strategy_two, amount)] + + with ape.reverts("max deposit"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + # Can deposit up to the limit + allocation = [(strategy_one, amount - limit), (strategy_two, limit)] + yield_manager.updateAllocation(vault, allocation, sender=user) + + # Increase the limit + strategy_two.setMaxDebt(2**256 - 1, sender=daddy) + + # Now can move everything. + allocation = [(strategy_one, 0), (strategy_two, amount)] + yield_manager.updateAllocation(vault, allocation, sender=user) + + +def test_update_allocation__min_idle( + apr_oracle, + yield_manager, + vault, + management, + keeper, + daddy, + user, + amount, + asset, + deploy_mock_tokenized, + generic_debt_allocator, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) + generic_debt_allocator.setMinimumChange(1, sender=daddy) + vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) + strategy_one.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_one, sender=daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) + + asset.approve(vault, amount, sender=user) + vault.deposit(amount, user, sender=user) + + assert vault.totalAssets() == amount + assert vault.totalDebt() == 0 + + min_idle = amount // 2 + + # add a minimum total idle requirement + vault.set_minimum_total_idle(min_idle, sender=daddy) + + # Try and move all + allocation = [(strategy_one, amount)] + + with ape.reverts("min idle"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + # Even just over the limit reverts. + allocation = [(strategy_one, int(amount - min_idle + 1e18))] + + with ape.reverts("min idle"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + # Can deposit up to the limit + allocation = [(strategy_one, amount - min_idle)] + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatios))[0] + assert event.newTargetRatio == 5_000 + + # lower the min idle to 0 + vault.set_minimum_total_idle(0, sender=daddy) + + # Now can move everything. + allocation = [(strategy_one, 0), (strategy_two, amount)] + tx = yield_manager.updateAllocation(vault, allocation, sender=user) + + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatios)) + + assert len(event) == 2 + assert event[0].newTargetRatio == 0 + assert event[1].newTargetRatio == 10_000 From 610019a57942a683eb4c8a65b21ee16faf015fea Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 28 Dec 2023 12:56:37 -0700 Subject: [PATCH 23/35] feat: dont duplicate vaults --- contracts/Managers/RoleManager.sol | 58 ++++- .../yield/test_yield_manager.py | 3 - tests/manager/test_role_manager.py | 218 ++++++++++++++++-- 3 files changed, 250 insertions(+), 29 deletions(-) diff --git a/contracts/Managers/RoleManager.sol b/contracts/Managers/RoleManager.sol index 38a4973..5e01218 100644 --- a/contracts/Managers/RoleManager.sol +++ b/contracts/Managers/RoleManager.sol @@ -11,6 +11,9 @@ import {GenericDebtAllocatorFactory} from "../debtAllocators/GenericDebtAllocato /// @title Yearn V3 Vault Role Manager. contract RoleManager is Governance2Step { + /// @notice Revert message for when a vault has already been deployed. + error AlreadyDeployed(address _vault); + /// @notice Emitted when a new vault has been deployed or added. event AddedNewVault( address indexed vault, @@ -109,6 +112,9 @@ contract RoleManager is Governance2Step { mapping(bytes32 => Position) internal _positions; /// @notice Mapping of vault addresses to its config. mapping(address => VaultConfig) public vaultConfig; + /// @notice Mapping of underlying asset, api version and rating to vault. + mapping(address => mapping(string => mapping(uint256 => address))) + public _assetToVault; constructor( address _governance, @@ -261,6 +267,12 @@ contract RoleManager is Governance2Step { _profitMaxUnlockTime ); + // Check that a vault does not exist for that asset, api and rating. + // This reverts late to not waste gas when used correctly. + string memory _apiVersion = IVault(_vault).apiVersion(); + if (_assetToVault[_asset][_apiVersion][_rating] != address(0)) + revert AlreadyDeployed(_assetToVault[_asset][_apiVersion][_rating]); + // Deploy a new debt allocator for the vault. address _debtAllocator = _deployAllocator(_vault); @@ -282,6 +294,9 @@ contract RoleManager is Governance2Step { index: vaults.length }); + // Add the vault to the mapping. + _assetToVault[_asset][_apiVersion][_rating] = _vault; + // Add the vault to the array. vaults.push(_vault); @@ -437,6 +452,11 @@ contract RoleManager is Governance2Step { address _debtAllocator ) public virtual onlyPositionHolder(DADDY) { require(_rating > 0 && _rating < 6, "rating out of range"); + // Check that a vault does not exist for that asset, api and rating. + address _asset = IVault(_vault).asset(); + string memory _apiVersion = IVault(_vault).apiVersion(); + if (_assetToVault[_asset][_apiVersion][_rating] != address(0)) + revert AlreadyDeployed(_assetToVault[_asset][_apiVersion][_rating]); // If not the current role manager. if (IVault(_vault).role_manager() != address(this)) { @@ -450,6 +470,7 @@ contract RoleManager is Governance2Step { // Check if the vault has been endorsed yet in the registry. if (!Registry(registry).isEndorsed(_vault)) { // If not endorse it. + // NOTE: This will revert if adding a vault of an older version. Registry(registry).endorseMultiStrategyVault(_vault); } @@ -463,12 +484,15 @@ contract RoleManager is Governance2Step { // Add the vault config to the mapping. vaultConfig[_vault] = VaultConfig({ - asset: IVault(_vault).asset(), + asset: _asset, rating: _rating, debtAllocator: _debtAllocator, index: vaults.length }); + // Add the vault to the mapping. + _assetToVault[_asset][_apiVersion][_rating] = _vault; + // Add the vault to the array. vaults.push(_vault); @@ -522,24 +546,29 @@ contract RoleManager is Governance2Step { function removeVault( address _vault ) external virtual onlyPositionHolder(BRAIN) { + // Get the vault specific config. + VaultConfig memory config = vaultConfig[_vault]; // Make sure the vault has been added to the role manager. - require(vaultConfig[_vault].asset != address(0), "vault not added"); + require(config.asset != address(0), "vault not added"); // Transfer the role manager position. IVault(_vault).transfer_role_manager(chad); - // Index that the vault is in the array. - uint256 index = vaultConfig[_vault].index; // Address of the vault to replace it with. address vaultToMove = vaults[vaults.length - 1]; // Move the last vault to the index of `_vault` - vaults[index] = vaultToMove; - vaultConfig[vaultToMove].index = index; + vaults[config.index] = vaultToMove; + vaultConfig[vaultToMove].index = config.index; // Remove the last item. vaults.pop(); + // Delete the vault from the mapping. + delete _assetToVault[config.asset][IVault(_vault).apiVersion()][ + config.rating + ]; + // Delete the config for `_vault`. delete vaultConfig[_vault]; @@ -611,6 +640,23 @@ contract RoleManager is Governance2Step { return vaults; } + /** + * @notice Get the vault for a specific asset, api and rating. + * @dev This will return address(0) if one has not been added or deployed. + * + * @param _asset The underlying asset used. + * @param _apiVersion The version of the vault. + * @param _rating The rating of the vault. + * @return The vault for the specified `_asset`, `_apiVersion` and `_rating`. + */ + function getVault( + address _asset, + string memory _apiVersion, + uint256 _rating + ) external view virtual returns (address) { + return _assetToVault[_asset][_apiVersion][_rating]; + } + /** * @notice Check if a vault is managed by this contract. * @dev This will check if the `asset` variable in the struct has been diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index d4ed7a4..76bbb8c 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -144,9 +144,6 @@ def test_update_allocation( with ape.reverts("ratio too high"): yield_manager.updateAllocation(vault, allocation, sender=user) - print( - f"Max redeem is {strategy_two.maxRedeem(vault)}, Max withdraw is {strategy_two.convertToAssets(strategy_two.maxRedeem(vault))}, amount is {amount}" - ) allocation = [(strategy_two, amount // 2)] with ape.reverts("fail"): yield_manager.updateAllocation(vault, allocation, sender=user) diff --git a/tests/manager/test_role_manager.py b/tests/manager/test_role_manager.py index caac85b..dbdecb8 100644 --- a/tests/manager/test_role_manager.py +++ b/tests/manager/test_role_manager.py @@ -36,6 +36,7 @@ def test_role_manager_setup( assert role_manager.ratingToString(6) == "" assert role_manager.chad() == daddy assert role_manager.getAllVaults() == [] + assert role_manager.getVault(asset, vault.apiVersion(), 1) == ZERO_ADDRESS assert role_manager.getDaddy() == daddy assert role_manager.getBrain() == brain assert role_manager.getSecurity() == security @@ -273,15 +274,18 @@ def test_deploy_new_vault( vault_factory, generic_debt_allocator_factory, ): + rating = int(1) + deposit_limit = int(100e18) + profit_unlock = int(695) + release_registry.newRelease(vault_factory.address, sender=daddy) assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - rating = int(1) - deposit_limit = int(100e18) - profit_unlock = int(695) - with ape.reverts("rating out of range"): role_manager.newVault(asset, 0, deposit_limit, profit_unlock, sender=daddy) @@ -334,6 +338,7 @@ def test_deploy_new_vault( assert vault_debt_allocator == debt_allocator assert index == 0 assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault assert role_manager.vaults(index) == vault assert role_manager.isVaultsRoleManager(vault) == True assert role_manager.getDebtAllocator(vault) == debt_allocator @@ -368,6 +373,73 @@ def test_deploy_new_vault( assert debt_allocator.governance() == brain +def test_deploy_new_vault__duplicate_reverts( + role_manager, + daddy, + asset, + healthcheck_accountant, + registry, + release_registry, + vault_factory, + generic_debt_allocator_factory, +): + rating = int(1) + deposit_limit = int(100e18) + profit_unlock = int(695) + + release_registry.newRelease(vault_factory.address, sender=daddy) + assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) + assert registry.numAssets() == 0 + assert registry.numEndorsedVaults(asset) == 0 + + # ADd the role manager as an endorser + registry.setEndorser(role_manager, True, sender=daddy) + assert registry.endorsers(role_manager) + + healthcheck_accountant.setVaultManager(role_manager, sender=daddy) + + tx = role_manager.newVault( + asset, rating, deposit_limit, profit_unlock, sender=daddy + ) + + event = list(tx.decode_logs(registry.NewEndorsedVault))[0] + + vault = project.dependencies["yearn-vaults"]["v3.0.1"].VaultV3.at(event.vault) + + event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + + assert event.vault == vault + debt_allocator = project.GenericDebtAllocator.at(event.allocator) + + (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( + vault + ) + + assert vault_asset == asset + assert vault_rating == rating + assert vault_debt_allocator == debt_allocator + assert index == 0 + assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault + assert role_manager.vaults(index) == vault + assert role_manager.isVaultsRoleManager(vault) == True + assert role_manager.getDebtAllocator(vault) == debt_allocator + assert role_manager.getRating(vault) == rating + assert registry.numAssets() == 1 + assert registry.numEndorsedVaults(asset) == 1 + assert registry.getAllEndorsedVaults() == [[vault]] + + # Try and deploy a new one of the same settings. + with ape.reverts(to_bytes32(f"Already Deployed {vault.address}")): + role_manager.newVault(asset, rating, deposit_limit, profit_unlock, sender=daddy) + + # can with a different rating. + role_manager.newVault(asset, rating + 1, deposit_limit, profit_unlock, sender=daddy) + + def test_deploy_new_vault__default_values( role_manager, daddy, @@ -383,13 +455,16 @@ def test_deploy_new_vault__default_values( vault_factory, generic_debt_allocator_factory, ): + rating = int(2) + release_registry.newRelease(vault_factory.address, sender=daddy) assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - rating = int(2) - with ape.reverts("rating out of range"): role_manager.newVault(asset, 0, sender=daddy) @@ -439,6 +514,7 @@ def test_deploy_new_vault__default_values( assert vault_debt_allocator == debt_allocator assert index == 0 assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault assert role_manager.vaults(index) == vault assert role_manager.isVaultsRoleManager(vault) == True assert role_manager.getDebtAllocator(vault) == debt_allocator @@ -507,12 +583,17 @@ def test_add_new_vault__endorsed( daddy=daddy, ) + name = " ksjdfl" + symbol = "sdfa" + rating = int(1) + assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - name = " ksjdfl" - symbol = "sdfa" tx = registry.newEndorsedVault(asset, name, symbol, daddy, 100, sender=daddy) event = list(tx.decode_logs(registry.NewEndorsedVault))[0] @@ -522,8 +603,6 @@ def test_add_new_vault__endorsed( assert registry.numEndorsedVaults(asset) == 1 assert registry.getAllEndorsedVaults() == [[vault]] - rating = int(1) - with ape.reverts("!allowed"): role_manager.addNewVault(vault, rating, sender=user) @@ -560,6 +639,7 @@ def test_add_new_vault__endorsed( assert vault_debt_allocator == debt_allocator assert index == 0 assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault assert role_manager.vaults(index) == vault assert role_manager.isVaultsRoleManager(vault) == True assert role_manager.getDebtAllocator(vault) == debt_allocator @@ -616,6 +696,8 @@ def test_add_new_vault__not_endorsed( name = " ksjdfl" symbol = "sdfa" + rating = int(1) + tx = vault_factory.deploy_new_vault(asset, name, symbol, daddy, 100, sender=daddy) event = list(tx.decode_logs(vault_factory.NewVault))[0] @@ -624,11 +706,12 @@ def test_add_new_vault__not_endorsed( ) assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - rating = int(1) - with ape.reverts("!allowed"): role_manager.addNewVault(vault, rating, sender=user) @@ -665,6 +748,7 @@ def test_add_new_vault__not_endorsed( assert vault_debt_allocator == debt_allocator assert index == 0 assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault assert role_manager.vaults(index) == vault assert role_manager.isVaultsRoleManager(vault) == True assert role_manager.getDebtAllocator(vault) == debt_allocator @@ -724,6 +808,8 @@ def test_add_new_vault__with_debt_allocator( name = " ksjdfl" symbol = "sdfa" + rating = int(1) + tx = vault_factory.deploy_new_vault(asset, name, symbol, daddy, 100, sender=daddy) event = list(tx.decode_logs(vault_factory.NewVault))[0] @@ -732,10 +818,12 @@ def test_add_new_vault__with_debt_allocator( ) assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - rating = int(1) tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=daddy) event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault @@ -773,6 +861,7 @@ def test_add_new_vault__with_debt_allocator( assert vault_debt_allocator == debt_allocator assert index == 0 assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault assert role_manager.vaults(index) == vault assert role_manager.isVaultsRoleManager(vault) == True assert role_manager.getDebtAllocator(vault) == debt_allocator @@ -832,6 +921,7 @@ def test_add_new_vault__with_accountant( name = " ksjdfl" symbol = "sdfa" + rating = int(1) tx = vault_factory.deploy_new_vault(asset, name, symbol, daddy, 100, sender=daddy) event = list(tx.decode_logs(vault_factory.NewVault))[0] @@ -840,10 +930,12 @@ def test_add_new_vault__with_accountant( ) assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - rating = int(1) tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=daddy) event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault @@ -885,6 +977,7 @@ def test_add_new_vault__with_accountant( assert vault_debt_allocator == debt_allocator assert index == 0 assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault assert role_manager.vaults(index) == vault assert role_manager.isVaultsRoleManager(vault) == True assert role_manager.getDebtAllocator(vault) == debt_allocator @@ -918,6 +1011,82 @@ def test_add_new_vault__with_accountant( assert debt_allocator.governance() == daddy +def test_add_new_vault__duplicate_reverts( + role_manager, + daddy, + asset, + healthcheck_accountant, + registry, + release_registry, + vault_factory, + generic_debt_allocator_factory, +): + setup_role_manager( + role_manager=role_manager, + release_registry=release_registry, + registry=registry, + vault_factory=vault_factory, + accountant=healthcheck_accountant, + daddy=daddy, + ) + + rating = int(1) + deposit_limit = int(100e18) + profit_unlock = int(695) + + assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) + assert registry.numAssets() == 0 + assert registry.numEndorsedVaults(asset) == 0 + + tx = role_manager.newVault( + asset, rating, deposit_limit, profit_unlock, sender=daddy + ) + + event = list(tx.decode_logs(registry.NewEndorsedVault))[0] + + vault = project.dependencies["yearn-vaults"]["v3.0.1"].VaultV3.at(event.vault) + + event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + + assert event.vault == vault + debt_allocator = project.GenericDebtAllocator.at(event.allocator) + + (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( + vault + ) + + assert vault_asset == asset + assert vault_rating == rating + assert vault_debt_allocator == debt_allocator + assert index == 0 + assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault + assert role_manager.vaults(index) == vault + assert role_manager.isVaultsRoleManager(vault) == True + assert role_manager.getDebtAllocator(vault) == debt_allocator + assert role_manager.getRating(vault) == rating + + name = " ksjdfl" + symbol = "sdfa" + + # Deploy a new vault with the same asset + tx = vault_factory.deploy_new_vault(asset, name, symbol, daddy, 100, sender=daddy) + + event = list(tx.decode_logs(vault_factory.NewVault))[0] + new_vault = project.dependencies["yearn-vaults"]["v3.0.1"].VaultV3.at( + event.vault_address + ) + + with ape.reverts(to_bytes32(f"Already Deployed {vault.address}")): + role_manager.addNewVault(new_vault, rating, debt_allocator, sender=daddy) + + # Can add it with a different rating. + role_manager.addNewVault(vault, rating + 1, debt_allocator, sender=daddy) + + def test_new_debt_allocator__deploys_one( role_manager, daddy, @@ -942,12 +1111,12 @@ def test_new_debt_allocator__deploys_one( daddy=daddy, ) + rating = int(2) + assert role_manager.getAllVaults() == [] assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - rating = int(2) - # Deploy a vault tx = role_manager.newVault(asset, rating, sender=daddy) @@ -966,6 +1135,7 @@ def test_new_debt_allocator__deploys_one( assert vault_debt_allocator == debt_allocator assert index == 0 assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault assert role_manager.vaults(index) == vault assert role_manager.isVaultsRoleManager(vault) == True assert role_manager.getDebtAllocator(vault) == debt_allocator @@ -1058,12 +1228,12 @@ def test_new_debt_allocator__already_deployed( daddy=daddy, ) + rating = int(2) + assert role_manager.getAllVaults() == [] assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - rating = int(2) - # Deploy a vault tx = role_manager.newVault(asset, rating, sender=daddy) @@ -1082,6 +1252,7 @@ def test_new_debt_allocator__already_deployed( assert vault_debt_allocator == debt_allocator assert index == 0 assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault assert role_manager.vaults(index) == vault assert role_manager.isVaultsRoleManager(vault) == True assert role_manager.getDebtAllocator(vault) == debt_allocator @@ -1175,12 +1346,15 @@ def test_remove_vault( daddy=daddy, ) + rating = int(2) + assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - rating = int(2) - # Deploy a vault tx = role_manager.newVault(asset, rating, sender=daddy) @@ -1199,6 +1373,7 @@ def test_remove_vault( assert vault_debt_allocator == debt_allocator assert index == 0 assert role_manager.getAllVaults() == [vault] + assert role_manager.getVault(asset, vault_factory.apiVersion(), rating) == vault assert role_manager.vaults(index) == vault assert role_manager.isVaultsRoleManager(vault) == True assert role_manager.getDebtAllocator(vault) == debt_allocator @@ -1242,6 +1417,9 @@ def test_remove_vault( assert vault_debt_allocator == ZERO_ADDRESS assert index == 0 assert role_manager.getAllVaults() == [] + assert ( + role_manager.getVault(asset, vault_factory.apiVersion(), rating) == ZERO_ADDRESS + ) assert role_manager.isVaultsRoleManager(vault) == False assert role_manager.getDebtAllocator(vault) == ZERO_ADDRESS assert role_manager.getRating(vault) == 0 From d41e2d7f97be0050a79b01a16792eba7ed14ed5b Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 28 Dec 2023 14:01:06 -0700 Subject: [PATCH 24/35] chore: change dep --- ape-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ape-config.yaml b/ape-config.yaml index 79ef5d9..35386f2 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -20,7 +20,7 @@ dependencies: - test/ - name: tokenized-strategy github: yearn/tokenized-strategy - ref: v3.0.1 + ref: v3.0.2 contracts_folder: src exclude: - test/ @@ -35,7 +35,7 @@ solidity: import_remapping: - "@openzeppelin/contracts=openzeppelin/v4.8.2" - "@yearn-vaults=yearn-vaults/v3.0.1" - - "@tokenized-strategy=tokenized-strategy/v3.0.1" + - "@tokenized-strategy=tokenized-strategy/v3.0.2" - "@periphery=periphery/master" ethereum: From fe15fa45216612fd83212b9b752b221cc4427adc Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 28 Dec 2023 14:15:39 -0700 Subject: [PATCH 25/35] fix: versions --- .github/workflows/test.yaml | 2 +- ape-config.yaml | 4 ++-- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3471a2f..2cbf43a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 16 - - uses: ApeWorX/github-action@v2.0 + - uses: ApeWorX/github-action@v2.4 with: python-version: '3.10' diff --git a/ape-config.yaml b/ape-config.yaml index 35386f2..79ef5d9 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -20,7 +20,7 @@ dependencies: - test/ - name: tokenized-strategy github: yearn/tokenized-strategy - ref: v3.0.2 + ref: v3.0.1 contracts_folder: src exclude: - test/ @@ -35,7 +35,7 @@ solidity: import_remapping: - "@openzeppelin/contracts=openzeppelin/v4.8.2" - "@yearn-vaults=yearn-vaults/v3.0.1" - - "@tokenized-strategy=tokenized-strategy/v3.0.2" + - "@tokenized-strategy=tokenized-strategy/v3.0.1" - "@periphery=periphery/master" ethereum: diff --git a/requirements.txt b/requirements.txt index 8cc4d8b..c21f6df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ black==22.3.0 -eth-ape>=0.6.7 +eth-ape>=0.7.0 pysha3==1.0.2 \ No newline at end of file From d1171493827bd29c68a9eb8f348506532f42efc8 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 28 Dec 2023 14:18:15 -0700 Subject: [PATCH 26/35] chore: pin ape version --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2cbf43a..4ac956f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,6 +19,7 @@ jobs: - uses: ApeWorX/github-action@v2.4 with: python-version: '3.10' + ape-version-pin: "==0.7.0" - name: install vyper run: pip install git+https://github.com/vyperlang/vyper From f4d901ceb05477ec526a56ee6f3754af87c54f5c Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 5 Jan 2024 11:08:36 -0700 Subject: [PATCH 27/35] chore: downgrade ape --- .github/workflows/test.yaml | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4ac956f..36067c4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,10 +16,10 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 16 - - uses: ApeWorX/github-action@v2.4 + - uses: ApeWorX/github-action@v2.0 with: python-version: '3.10' - ape-version-pin: "==0.7.0" + ape-version-pin: "==0.6.27" - name: install vyper run: pip install git+https://github.com/vyperlang/vyper diff --git a/requirements.txt b/requirements.txt index c21f6df..009de13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ black==22.3.0 -eth-ape>=0.7.0 +eth-ape==0.6.27 pysha3==1.0.2 \ No newline at end of file From a29e6cfac2a99d7115e5c5d97f1bd6fbfa63c714 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 5 Jan 2024 12:05:29 -0700 Subject: [PATCH 28/35] chore: pin all versions --- .github/workflows/test.yaml | 1 + ape-config.yaml | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 36067c4..fc429b1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,6 +20,7 @@ jobs: with: python-version: '3.10' ape-version-pin: "==0.6.27" + ape-plugins-list: 'solidity==0.6.11 vyper==0.6.13 infura==0.6.5 hardhat==0.6.13 etherscan==0.6.11' - name: install vyper run: pip install git+https://github.com/vyperlang/vyper diff --git a/ape-config.yaml b/ape-config.yaml index 79ef5d9..59a7dcc 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -2,10 +2,15 @@ name: yearn-v3-vault-periphery plugins: - name: solidity + version: 0.6.11 - name: vyper + version: 0.6.13 - name: etherscan + version: 0.6.11 - name: hardhat + version: 0.6.13 - name: infura + version: 0.6.5 default_ecosystem: ethereum From 5ff8880bd5a4a1b13599019fbb213e8a1a79002a Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 5 Jan 2024 16:09:25 -0700 Subject: [PATCH 29/35] feat: increase and decrease --- .../debtAllocators/GenericDebtAllocator.sol | 90 +++++++-- .../debtAllocators/YieldManager/Keeper.sol | 31 +-- .../YieldManager/YieldManager.sol | 54 +++-- .../test_generic_debt_allocator.py | 189 +++++++++++++++--- .../yield/test_yield_manager.py | 36 ++-- 5 files changed, 295 insertions(+), 105 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index edd4fe5..933ccb4 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -30,7 +30,7 @@ import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; */ contract GenericDebtAllocator is Governance { /// @notice An event emitted when a strategies debt ratios are Updated. - event UpdateStrategyDebtRatios( + event UpdateStrategyDebtRatio( address indexed strategy, uint256 newTargetRatio, uint256 newMaxRatio, @@ -55,13 +55,17 @@ contract GenericDebtAllocator is Governance { /// @notice Struct for each strategies info. struct Config { // The ideal percent in Basis Points the strategy should have. - uint256 targetRatio; + uint16 targetRatio; // The max percent of assets the strategy should hold. - uint256 maxRatio; + uint16 maxRatio; // Timestamp of the last time debt was updated. // The debt updates must be done through this allocator // for this to be used. - uint256 lastUpdate; + uint128 lastUpdate; + // We have an extra 96 bits in the slot. + // So we declare the variable in the struct so it can be + // used if this contract is inherited. + uint96 open; } modifier onlyKeepers() { @@ -100,7 +104,7 @@ contract GenericDebtAllocator is Governance { mapping(address => bool) public keepers; /// @notice Mapping of strategy => its config. - mapping(address => Config) public configs; + mapping(address => Config) internal _configs; constructor( address _vault, @@ -154,7 +158,7 @@ contract GenericDebtAllocator is Governance { function update_debt( address _strategy, uint256 _targetDebt - ) external virtual onlyKeepers { + ) public virtual onlyKeepers { IVault _vault = IVault(vault); // If going to 0 record full balance first. @@ -179,7 +183,7 @@ contract GenericDebtAllocator is Governance { } // Update the last time the strategies debt was updated. - configs[_strategy].lastUpdate = block.timestamp; + _configs[_strategy].lastUpdate = uint128(block.timestamp); } /** @@ -195,7 +199,7 @@ contract GenericDebtAllocator is Governance { */ function shouldUpdateDebt( address _strategy - ) external view virtual returns (bool, bytes memory) { + ) public view virtual returns (bool, bytes memory) { // Check the base fee isn't too high. if (block.basefee > maxAcceptableBaseFee) { return (false, bytes("Base Fee")); @@ -209,7 +213,7 @@ contract GenericDebtAllocator is Governance { require(params.activation != 0, "!active"); // Get the strategy specific debt config. - Config memory config = configs[_strategy]; + Config memory config = getConfig(_strategy); if (block.timestamp - config.lastUpdate <= minimumWait) { return (false, bytes("min wait")); @@ -301,6 +305,33 @@ contract GenericDebtAllocator is Governance { return (false, bytes("Below Min")); } + /** + * @notice Increase a strategies target debt ratio. + * @dev `setStrategyDebtRatio` functions will do all needed checks. + * @param _strategy The address of the strategy to increase the debt ratio for. + * @param _increase The amount in Basis Points to increase it. + */ + function increaseStrategyDebtRatio( + address _strategy, + uint256 _increase + ) external virtual { + uint256 _currentRatio = getConfig(_strategy).targetRatio; + setStrategyDebtRatio(_strategy, _currentRatio + _increase); + } + + /** + * @notice Decrease a strategies target debt ratio. + * @param _strategy The address of the strategy to decrease the debt ratio for. + * @param _decrease The amount in Basis Points to decrease it. + */ + function decreaseStrategyDebtRatio( + address _strategy, + uint256 _decrease + ) external virtual { + uint256 _currentRatio = getConfig(_strategy).targetRatio; + setStrategyDebtRatio(_strategy, _currentRatio - _decrease); + } + /** * @notice Sets a new target debt ratio for a strategy. * @dev This will default to a 10% increase for max debt. @@ -308,12 +339,12 @@ contract GenericDebtAllocator is Governance { * @param _strategy Address of the strategy to set. * @param _targetRatio Amount in Basis points to allocate. */ - function setStrategyDebtRatios( + function setStrategyDebtRatio( address _strategy, uint256 _targetRatio - ) external virtual { + ) public virtual { uint256 maxRatio = Math.min((_targetRatio * 11_000) / MAX_BPS, MAX_BPS); - setStrategyDebtRatios(_strategy, _targetRatio, maxRatio); + setStrategyDebtRatio(_strategy, _targetRatio, maxRatio); } /** @@ -325,7 +356,7 @@ contract GenericDebtAllocator is Governance { * @param _targetRatio Amount in Basis points to allocate. * @param _maxRatio Max ratio to give on debt increases. */ - function setStrategyDebtRatios( + function setStrategyDebtRatio( address _strategy, uint256 _targetRatio, uint256 _maxRatio @@ -337,21 +368,24 @@ contract GenericDebtAllocator is Governance { // Max cannot be lower than the target. require(_maxRatio >= _targetRatio, "max ratio"); + // Get the current config. + Config memory config = getConfig(_strategy); + // Get what will be the new total debt ratio. - uint256 newDebtRatio = debtRatio - - configs[_strategy].targetRatio + - _targetRatio; + uint256 newDebtRatio = debtRatio - config.targetRatio + _targetRatio; // Make sure it is under 100% allocated require(newDebtRatio <= MAX_BPS, "ratio too high"); - // Write to storage. - configs[_strategy].targetRatio = _targetRatio; - configs[_strategy].maxRatio = _maxRatio; + // Update local config. + config.targetRatio = uint16(_targetRatio); + config.maxRatio = uint16(_maxRatio); + // Write to storage. + _configs[_strategy] = config; debtRatio = newDebtRatio; - emit UpdateStrategyDebtRatios( + emit UpdateStrategyDebtRatio( _strategy, _targetRatio, _maxRatio, @@ -435,6 +469,18 @@ contract GenericDebtAllocator is Governance { emit UpdateMaxAcceptableBaseFee(_maxAcceptableBaseFee); } + /** + * @notice Get a strategies full config. + * @dev Used for customizations by inheriting the contract. + * @param _strategy Address of the strategy. + * @return The strategies current Config. + */ + function getConfig( + address _strategy + ) public view virtual returns (Config memory) { + return _configs[_strategy]; + } + /** * @notice Get a strategies target debt ratio. * @param _strategy Address of the strategy. @@ -443,7 +489,7 @@ contract GenericDebtAllocator is Governance { function getStrategyTargetRatio( address _strategy ) external view virtual returns (uint256) { - return configs[_strategy].targetRatio; + return getConfig(_strategy).targetRatio; } /** @@ -454,6 +500,6 @@ contract GenericDebtAllocator is Governance { function getStrategyMaxRatio( address _strategy ) external view virtual returns (uint256) { - return configs[_strategy].maxRatio; + return getConfig(_strategy).maxRatio; } } diff --git a/contracts/debtAllocators/YieldManager/Keeper.sol b/contracts/debtAllocators/YieldManager/Keeper.sol index 69d255a..611dc97 100644 --- a/contracts/debtAllocators/YieldManager/Keeper.sol +++ b/contracts/debtAllocators/YieldManager/Keeper.sol @@ -23,8 +23,8 @@ contract Keeper is Governance { } /// @notice Only the keepers can call. - modifier onlyKeepers(address _strategy) { - _checkKeepers(_strategy); + modifier onlyKeepers() { + _checkKeepers(); _; } @@ -33,12 +33,9 @@ contract Keeper is Governance { require(strategyOwner[_strategy] == msg.sender, "!owner"); } - /// @notice Checks if the msg sender is a keeper and the strategy is added. - function _checkKeepers(address _strategy) internal view virtual { - require( - keepers[msg.sender] && strategyOwner[_strategy] != address(0), - "!keeper" - ); + /// @notice Checks if the msg sender is a keeper. + function _checkKeepers() internal view virtual { + require(keepers[msg.sender], "!keeper"); } /// @notice Address check for keepers allowed to call. @@ -100,18 +97,24 @@ contract Keeper is Governance { * @notice Reports full profit for a strategy. * @param _strategy The address of the strategy. */ - function report(address _strategy) external virtual onlyKeepers(_strategy) { - // Report profits. - IStrategy(_strategy).report(); + function report(address _strategy) external virtual onlyKeepers { + // If the strategy has been added to the keeper. + if (strategyOwner[_strategy] != address(0)) { + // Report profits. + IStrategy(_strategy).report(); + } } /** * @notice Tends a strategy. * @param _strategy The address of the strategy. */ - function tend(address _strategy) external virtual onlyKeepers(_strategy) { - // Tend. - IStrategy(_strategy).tend(); + function tend(address _strategy) external virtual onlyKeepers { + // If the strategy has been added to the keeper. + if (strategyOwner[_strategy] != address(0)) { + // Tend. + IStrategy(_strategy).tend(); + } } /** diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 4ff59a5..314e6a1 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -66,29 +66,6 @@ contract YieldManager is Governance { keeper = _keeper; } - /** - * @notice Update a `_vault`s allocation of debt. - * @dev This takes the address of a vault and an array of - * its strategies and their specific allocation. - * - * The `_newAllocations` array should: - * - Be ordered so that all debt decreases are at the beginning of the array - * and debt increases at the end. - * - * This will not do any APR checks and assumes the sender has completed - * any and all necessary checks before sending. - * - * @param _vault The address of the vault to propose an allocation for. - * @param _newAllocations Array of strategies and their new proposed allocation. - */ - function updateAllocationPermissioned( - address _vault, - Allocation[] memory _newAllocations - ) public virtual onlyGovernance { - // Move funds - _allocate(_vault, _newAllocations); - } - /** * @notice Update a `_vault`s target allocation of debt. * @dev This takes the address of a vault and an array of @@ -152,7 +129,7 @@ contract YieldManager is Governance { if (_currentDebt > _newDebt) { // If we are pulling all debt from a strategy. if (_newDebt == 0) { - // We need to report profits to have them start to unlock. + // Try to report profits to have them start to unlock. Keeper(keeper).report(_strategy); } @@ -179,6 +156,11 @@ contract YieldManager is Governance { "max withdraw" ); } else if (_currentDebt < _newDebt) { + // Make sure the strategy is allowed that much. + require( + IVault(_vault).strategies(_strategy).max_debt >= _newDebt, + "max debt" + ); // Make sure the vault can deposit the desired amount. require( IVault(_strategy).maxDeposit(_vault) >= @@ -199,7 +181,7 @@ contract YieldManager is Governance { ) != _targetRatio ) { // Update allocation. - GenericDebtAllocator(allocator).setStrategyDebtRatios( + GenericDebtAllocator(allocator).setStrategyDebtRatio( _strategy, _targetRatio ); @@ -313,15 +295,24 @@ contract YieldManager is Governance { } /** - * @notice Allocate a vaults debt based on the new proposed Allocation. + * @notice Update a `_vault`s allocation of debt. + * @dev This takes the address of a vault and an array of + * its strategies and their specific allocation. + * + * The `_newAllocations` array should: + * - Be ordered so that all debt decreases are at the beginning of the array + * and debt increases at the end. + * + * This will not do any APR checks and assumes the sender has completed + * any and all necessary checks before sending. * * @param _vault The address of the vault to propose an allocation for. * @param _newAllocations Array of strategies and their new proposed allocation. */ - function _allocate( + function updateAllocationPermissioned( address _vault, Allocation[] memory _newAllocations - ) internal virtual { + ) external virtual onlyGovernance { address allocator = vaultAllocator[_vault]; require(allocator != address(0), "vault not added"); address _strategy; @@ -364,6 +355,11 @@ contract YieldManager is Governance { "max withdraw" ); } else if (_currentDebt < _newDebt) { + // Make sure the strategy is allowed that much. + require( + IVault(_vault).strategies(_strategy).max_debt >= _newDebt, + "max debt" + ); // Make sure the vault can deposit the desired amount. require( IVault(_strategy).maxDeposit(_vault) >= @@ -383,7 +379,7 @@ contract YieldManager is Governance { ) != _targetRatio ) { // Update allocation. - GenericDebtAllocator(allocator).setStrategyDebtRatios( + GenericDebtAllocator(allocator).setStrategyDebtRatio( _strategy, _targetRatio ); diff --git a/tests/debtAllocators/test_generic_debt_allocator.py b/tests/debtAllocators/test_generic_debt_allocator.py index d41e90a..675b624 100644 --- a/tests/debtAllocators/test_generic_debt_allocator.py +++ b/tests/debtAllocators/test_generic_debt_allocator.py @@ -18,7 +18,7 @@ def test_setup(generic_debt_allocator_factory, user, strategy, vault): assert generic_debt_allocator.governance() == user assert generic_debt_allocator.keepers(user) == True assert generic_debt_allocator.vault() == vault.address - assert generic_debt_allocator.configs(strategy) == (0, 0, 0) + assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) assert generic_debt_allocator.debtRatio() == 0 with ape.reverts("!active"): generic_debt_allocator.shouldUpdateDebt(strategy) @@ -49,7 +49,7 @@ def test_set_keepers(generic_debt_allocator, daddy, vault, strategy, user): def test_set_minimum_change(generic_debt_allocator, daddy, vault, strategy, user): - assert generic_debt_allocator.configs(strategy) == (0, 0, 0) + assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) assert generic_debt_allocator.minimumChange() == 0 minimum = int(1e17) @@ -69,7 +69,7 @@ def test_set_minimum_change(generic_debt_allocator, daddy, vault, strategy, user def test_set_minimum_wait(generic_debt_allocator, daddy, vault, strategy, user): - assert generic_debt_allocator.configs(strategy) == (0, 0, 0) + assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) assert generic_debt_allocator.minimumWait() == 0 minimum = int(1e17) @@ -86,7 +86,7 @@ def test_set_minimum_wait(generic_debt_allocator, daddy, vault, strategy, user): def test_set_max_debt_update_loss(generic_debt_allocator, daddy, vault, strategy, user): - assert generic_debt_allocator.configs(strategy) == (0, 0, 0) + assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) assert generic_debt_allocator.maxDebtUpdateLoss() == 1 max = int(69) @@ -108,69 +108,208 @@ def test_set_max_debt_update_loss(generic_debt_allocator, daddy, vault, strategy def test_set_ratios( generic_debt_allocator, daddy, vault, strategy, create_strategy, user ): - assert generic_debt_allocator.configs(strategy) == (0, 0, 0) + assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) minimum = int(1e17) max = int(6_000) target = int(5_000) with ape.reverts("!keeper"): - generic_debt_allocator.setStrategyDebtRatios(strategy, target, max, sender=user) + generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=user) vault.add_strategy(strategy.address, sender=daddy) with ape.reverts("!minimum"): - generic_debt_allocator.setStrategyDebtRatios( - strategy, target, max, sender=daddy - ) + generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=daddy) generic_debt_allocator.setMinimumChange(minimum, sender=daddy) with ape.reverts("max too high"): - generic_debt_allocator.setStrategyDebtRatios( + generic_debt_allocator.setStrategyDebtRatio( strategy, target, int(10_001), sender=daddy ) with ape.reverts("max ratio"): - generic_debt_allocator.setStrategyDebtRatios( + generic_debt_allocator.setStrategyDebtRatio( strategy, int(max + 1), max, sender=daddy ) - tx = generic_debt_allocator.setStrategyDebtRatios( + tx = generic_debt_allocator.setStrategyDebtRatio( strategy, target, max, sender=daddy ) - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatios))[0] + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] assert event.newTargetRatio == target assert event.newMaxRatio == max assert event.newTotalDebtRatio == target assert generic_debt_allocator.debtRatio() == target - assert generic_debt_allocator.configs(strategy) == (target, max, 0) + assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) new_strategy = create_strategy() vault.add_strategy(new_strategy, sender=daddy) with ape.reverts("ratio too high"): - generic_debt_allocator.setStrategyDebtRatios( + generic_debt_allocator.setStrategyDebtRatio( new_strategy, int(10_000), int(10_000), sender=daddy ) target = int(8_000) - tx = generic_debt_allocator.setStrategyDebtRatios(strategy, target, sender=daddy) + tx = generic_debt_allocator.setStrategyDebtRatio(strategy, target, sender=daddy) - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatios))[0] + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] assert event.newTargetRatio == target assert event.newMaxRatio == target * 1.1 assert event.newTotalDebtRatio == target assert generic_debt_allocator.debtRatio() == target - assert generic_debt_allocator.configs(strategy) == (target, target * 1.1, 0) + assert generic_debt_allocator.getConfig(strategy) == (target, target * 1.1, 0, 0) + + +def test_increase_debt_ratio( + generic_debt_allocator, daddy, vault, strategy, create_strategy, user +): + assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + + minimum = int(1e17) + target = int(5_000) + increase = int(5_000) + max = target * 1.1 + + with ape.reverts("!keeper"): + generic_debt_allocator.increaseStrategyDebtRatio( + strategy, increase, sender=user + ) + + vault.add_strategy(strategy.address, sender=daddy) + + with ape.reverts("!minimum"): + generic_debt_allocator.increaseStrategyDebtRatio( + strategy, increase, sender=daddy + ) + + generic_debt_allocator.setMinimumChange(minimum, sender=daddy) + + tx = generic_debt_allocator.increaseStrategyDebtRatio( + strategy, increase, sender=daddy + ) + + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + new_strategy = create_strategy() + vault.add_strategy(new_strategy, sender=daddy) + + with ape.reverts("ratio too high"): + generic_debt_allocator.increaseStrategyDebtRatio( + new_strategy, int(5_001), sender=daddy + ) + + target = int(8_000) + max = target * 1.1 + increase = int(3_000) + tx = generic_debt_allocator.increaseStrategyDebtRatio( + strategy, increase, sender=daddy + ) + + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + target = int(10_000) + max = int(10_000) + increase = int(2_000) + tx = generic_debt_allocator.increaseStrategyDebtRatio( + strategy, increase, sender=daddy + ) + + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + +def test_decrease_debt_ratio( + generic_debt_allocator, daddy, vault, strategy, create_strategy, user +): + assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + + minimum = int(1e17) + target = int(5_000) + max = target * 1.1 + + vault.add_strategy(strategy.address, sender=daddy) + generic_debt_allocator.setMinimumChange(minimum, sender=daddy) + + # Underflow + with ape.reverts(): + generic_debt_allocator.decreaseStrategyDebtRatio(strategy, target, sender=daddy) + + # Add the target + tx = generic_debt_allocator.increaseStrategyDebtRatio( + strategy, target, sender=daddy + ) + + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + target = int(2_000) + max = target * 1.1 + decrease = int(3_000) + + with ape.reverts("!keeper"): + generic_debt_allocator.decreaseStrategyDebtRatio( + strategy, decrease, sender=user + ) + + tx = generic_debt_allocator.decreaseStrategyDebtRatio( + strategy, decrease, sender=daddy + ) + + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + target = int(0) + max = int(0) + decrease = int(2_000) + tx = generic_debt_allocator.decreaseStrategyDebtRatio( + strategy, decrease, sender=daddy + ) + + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) def test_should_update_debt( generic_debt_allocator, vault, strategy, daddy, deposit_into_vault, amount ): - assert generic_debt_allocator.configs(strategy.address) == (0, 0, 0) + assert generic_debt_allocator.getConfig(strategy.address) == (0, 0, 0, 0) vault.add_role(generic_debt_allocator, ROLES.DEBT_MANAGER, sender=daddy) with ape.reverts("!active"): @@ -183,7 +322,7 @@ def test_should_update_debt( max = int(5_000) generic_debt_allocator.setMinimumChange(minimum, sender=daddy) - generic_debt_allocator.setStrategyDebtRatios(strategy, target, max, sender=daddy) + generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=daddy) # Vault has no assets so should be false. (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) @@ -215,7 +354,7 @@ def test_should_update_debt( assert bytes == ("Below Min").encode("utf-8") # Update the ratio to make true - generic_debt_allocator.setStrategyDebtRatios( + generic_debt_allocator.setStrategyDebtRatio( strategy, int(target + 1), int(target + 1), sender=daddy ) @@ -268,7 +407,7 @@ def test_should_update_debt( # Lower the target and minimum generic_debt_allocator.setMinimumChange(int(1), sender=daddy) - generic_debt_allocator.setStrategyDebtRatios( + generic_debt_allocator.setStrategyDebtRatio( strategy, int(target // 2), int(target // 2), sender=daddy ) @@ -280,7 +419,7 @@ def test_should_update_debt( def test_update_debt( generic_debt_allocator, vault, strategy, daddy, user, deposit_into_vault, amount ): - assert generic_debt_allocator.configs(strategy) == (0, 0, 0) + assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) deposit_into_vault(vault, amount) assert vault.totalIdle() == amount @@ -305,7 +444,7 @@ def test_update_debt( generic_debt_allocator.update_debt(strategy, amount, sender=daddy) - timestamp = generic_debt_allocator.configs(strategy)[2] + timestamp = generic_debt_allocator.getConfig(strategy)[2] assert timestamp != 0 assert vault.totalIdle() == 0 assert vault.totalDebt() == amount @@ -314,6 +453,6 @@ def test_update_debt( generic_debt_allocator.update_debt(strategy, 0, sender=user) - assert generic_debt_allocator.configs(strategy)[2] != timestamp + assert generic_debt_allocator.getConfig(strategy)[2] != timestamp assert vault.totalIdle() == amount assert vault.totalDebt() == 0 diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 76bbb8c..6272706 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -115,12 +115,20 @@ def test_update_allocation( sender=daddy, ) + # Set max debt for strategy to 0. + vault.update_max_debt_for_strategy(strategy_two, 0, sender=daddy) + + with ape.reverts("max debt"): + yield_manager.updateAllocation(vault, allocation, sender=user) + + vault.update_max_debt_for_strategy(strategy_two, 2**256 - 1, sender=daddy) + tx = yield_manager.updateAllocation(vault, allocation, sender=user) (before, now) = tx.return_value - assert generic_debt_allocator.configs(strategy_two).targetRatio == MAX_BPS - assert generic_debt_allocator.configs(strategy_one).targetRatio == 0 + assert generic_debt_allocator.getConfig(strategy_two).targetRatio == MAX_BPS + assert generic_debt_allocator.getConfig(strategy_one).targetRatio == 0 assert generic_debt_allocator.shouldUpdateDebt(strategy_one)[0] == False assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == True assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[ @@ -161,8 +169,8 @@ def test_update_allocation( allocation = [(strategy_two, amount - to_move), (strategy_one, to_move)] tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert generic_debt_allocator.configs(strategy_two).targetRatio != MAX_BPS - assert generic_debt_allocator.configs(strategy_one).targetRatio != 0 + assert generic_debt_allocator.getConfig(strategy_two).targetRatio != MAX_BPS + assert generic_debt_allocator.getConfig(strategy_one).targetRatio != 0 (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False @@ -192,17 +200,15 @@ def test_update_allocation( # Try and move all allocation = [(strategy_two, 0), (strategy_one, amount)] - # Strategy manager isnt the strategies management - with ape.reverts("!keeper"): - yield_manager.updateAllocation(vault, allocation, sender=user) strategy_two.setKeeper(keeper, sender=management) keeper.addNewStrategy(strategy_two, sender=daddy) tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert generic_debt_allocator.configs(strategy_two).targetRatio == 0 - assert generic_debt_allocator.configs(strategy_one).targetRatio == MAX_BPS + assert generic_debt_allocator.getConfig(strategy_two).targetRatio == 0 + assert generic_debt_allocator.getConfig(strategy_one).targetRatio == MAX_BPS + (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False @@ -666,8 +672,8 @@ def test_update_allocation_permissioned( allocation = [(strategy_two, amount - to_move), (strategy_one, to_move)] tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) - assert generic_debt_allocator.configs(strategy_two).targetRatio != MAX_BPS - assert generic_debt_allocator.configs(strategy_one).targetRatio != 0 + assert generic_debt_allocator.getConfig(strategy_two).targetRatio != MAX_BPS + assert generic_debt_allocator.getConfig(strategy_one).targetRatio != 0 (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False @@ -703,8 +709,8 @@ def test_update_allocation_permissioned( assert len(list(tx.decode_logs(strategy_two.Reported))) == 1 - assert generic_debt_allocator.configs(strategy_two).targetRatio == 0 - assert generic_debt_allocator.configs(strategy_one).targetRatio == MAX_BPS + assert generic_debt_allocator.getConfig(strategy_two).targetRatio == 0 + assert generic_debt_allocator.getConfig(strategy_one).targetRatio == MAX_BPS (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False @@ -915,7 +921,7 @@ def test_update_allocation__min_idle( allocation = [(strategy_one, amount - min_idle)] tx = yield_manager.updateAllocation(vault, allocation, sender=user) - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatios))[0] + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] assert event.newTargetRatio == 5_000 # lower the min idle to 0 @@ -925,7 +931,7 @@ def test_update_allocation__min_idle( allocation = [(strategy_one, 0), (strategy_two, amount)] tx = yield_manager.updateAllocation(vault, allocation, sender=user) - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatios)) + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio)) assert len(event) == 2 assert event[0].newTargetRatio == 0 From aa027df095efc5a23507f8ca0a41fc73753c32c0 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 5 Jan 2024 17:14:38 -0700 Subject: [PATCH 30/35] fix: formatting --- contracts/Managers/RoleManager.sol | 1 + contracts/debtAllocators/YieldManager/Keeper.sol | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/Managers/RoleManager.sol b/contracts/Managers/RoleManager.sol index 5e01218..2d15b85 100644 --- a/contracts/Managers/RoleManager.sol +++ b/contracts/Managers/RoleManager.sol @@ -452,6 +452,7 @@ contract RoleManager is Governance2Step { address _debtAllocator ) public virtual onlyPositionHolder(DADDY) { require(_rating > 0 && _rating < 6, "rating out of range"); + // Check that a vault does not exist for that asset, api and rating. address _asset = IVault(_vault).asset(); string memory _apiVersion = IVault(_vault).apiVersion(); diff --git a/contracts/debtAllocators/YieldManager/Keeper.sol b/contracts/debtAllocators/YieldManager/Keeper.sol index 611dc97..8a9e459 100644 --- a/contracts/debtAllocators/YieldManager/Keeper.sol +++ b/contracts/debtAllocators/YieldManager/Keeper.sol @@ -18,23 +18,23 @@ contract Keeper is Governance { /// @notice Only the `_strategy` specific owner can call. modifier onlyStrategyOwner(address _strategy) { - _checkStrategyOwner(_strategy); + _isStrategyOwner(_strategy); _; } /// @notice Only the keepers can call. modifier onlyKeepers() { - _checkKeepers(); + _isKeeper(); _; } /// @notice Checks if the msg sender is the owner of the strategy. - function _checkStrategyOwner(address _strategy) internal view virtual { + function _isStrategyOwner(address _strategy) internal view virtual { require(strategyOwner[_strategy] == msg.sender, "!owner"); } /// @notice Checks if the msg sender is a keeper. - function _checkKeepers() internal view virtual { + function _isKeeper() internal view virtual { require(keepers[msg.sender], "!keeper"); } @@ -86,7 +86,7 @@ contract Keeper is Governance { */ function removeStrategy(address _strategy) external virtual { // Only governance or the strategy owner can call. - if (msg.sender != governance) _checkStrategyOwner(_strategy); + if (msg.sender != governance) _isStrategyOwner(_strategy); delete strategyOwner[_strategy]; From 4a0b4d636664a9515f9fb9ee6f9a8d5e919654d9 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Sun, 7 Jan 2024 10:00:35 -0700 Subject: [PATCH 31/35] fix: naming --- .../debtAllocators/GenericDebtAllocator.sol | 18 ++++++++++-------- .../YieldManager/YieldManager.sol | 2 +- .../test_generic_debt_allocator.py | 18 +++++++++--------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index 933ccb4..ef0fee2 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -82,10 +82,6 @@ contract GenericDebtAllocator is Governance { /// @notice Address of the vault this serves as allocator for. address public vault; - /// @notice Total debt ratio currently allocated in basis points. - // Can't be more than 10_000. - uint256 public debtRatio; - /// @notice Time to wait between debt updates in seconds. uint256 public minimumWait; @@ -93,6 +89,10 @@ contract GenericDebtAllocator is Governance { // need to be moved to trigger a debt update. uint256 public minimumChange; + /// @notice Total debt ratio currently allocated in basis points. + // Can't be more than 10_000. + uint256 public totalDebtRatio; + /// @notice Max loss to accept on debt updates in basis points. uint256 public maxDebtUpdateLoss; @@ -372,10 +372,12 @@ contract GenericDebtAllocator is Governance { Config memory config = getConfig(_strategy); // Get what will be the new total debt ratio. - uint256 newDebtRatio = debtRatio - config.targetRatio + _targetRatio; + uint256 newTotalDebtRatio = totalDebtRatio - + config.targetRatio + + _targetRatio; // Make sure it is under 100% allocated - require(newDebtRatio <= MAX_BPS, "ratio too high"); + require(newTotalDebtRatio <= MAX_BPS, "ratio too high"); // Update local config. config.targetRatio = uint16(_targetRatio); @@ -383,13 +385,13 @@ contract GenericDebtAllocator is Governance { // Write to storage. _configs[_strategy] = config; - debtRatio = newDebtRatio; + totalDebtRatio = newTotalDebtRatio; emit UpdateStrategyDebtRatio( _strategy, _targetRatio, _maxRatio, - newDebtRatio + newTotalDebtRatio ); } diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 314e6a1..0290b19 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -415,7 +415,7 @@ contract YieldManager is Governance { address _vault, address _allocator ) internal view virtual { - uint256 totalRatio = GenericDebtAllocator(_allocator).debtRatio(); + uint256 totalRatio = GenericDebtAllocator(_allocator).totalDebtRatio(); uint256 minIdle = IVault(_vault).minimum_total_idle(); // No need if minIdle is 0. diff --git a/tests/debtAllocators/test_generic_debt_allocator.py b/tests/debtAllocators/test_generic_debt_allocator.py index 675b624..278f4b0 100644 --- a/tests/debtAllocators/test_generic_debt_allocator.py +++ b/tests/debtAllocators/test_generic_debt_allocator.py @@ -19,7 +19,7 @@ def test_setup(generic_debt_allocator_factory, user, strategy, vault): assert generic_debt_allocator.keepers(user) == True assert generic_debt_allocator.vault() == vault.address assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - assert generic_debt_allocator.debtRatio() == 0 + assert generic_debt_allocator.totalDebtRatio() == 0 with ape.reverts("!active"): generic_debt_allocator.shouldUpdateDebt(strategy) @@ -143,7 +143,7 @@ def test_set_ratios( assert event.newTargetRatio == target assert event.newMaxRatio == max assert event.newTotalDebtRatio == target - assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.totalDebtRatio() == target assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) new_strategy = create_strategy() @@ -161,7 +161,7 @@ def test_set_ratios( assert event.newTargetRatio == target assert event.newMaxRatio == target * 1.1 assert event.newTotalDebtRatio == target - assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.totalDebtRatio() == target assert generic_debt_allocator.getConfig(strategy) == (target, target * 1.1, 0, 0) @@ -198,7 +198,7 @@ def test_increase_debt_ratio( assert event.newTargetRatio == target assert event.newMaxRatio == max assert event.newTotalDebtRatio == target - assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.totalDebtRatio() == target assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) new_strategy = create_strategy() @@ -221,7 +221,7 @@ def test_increase_debt_ratio( assert event.newTargetRatio == target assert event.newMaxRatio == max assert event.newTotalDebtRatio == target - assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.totalDebtRatio() == target assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) target = int(10_000) @@ -236,7 +236,7 @@ def test_increase_debt_ratio( assert event.newTargetRatio == target assert event.newMaxRatio == max assert event.newTotalDebtRatio == target - assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.totalDebtRatio() == target assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) @@ -266,7 +266,7 @@ def test_decrease_debt_ratio( assert event.newTargetRatio == target assert event.newMaxRatio == max assert event.newTotalDebtRatio == target - assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.totalDebtRatio() == target assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) target = int(2_000) @@ -287,7 +287,7 @@ def test_decrease_debt_ratio( assert event.newTargetRatio == target assert event.newMaxRatio == max assert event.newTotalDebtRatio == target - assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.totalDebtRatio() == target assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) target = int(0) @@ -302,7 +302,7 @@ def test_decrease_debt_ratio( assert event.newTargetRatio == target assert event.newMaxRatio == max assert event.newTotalDebtRatio == target - assert generic_debt_allocator.debtRatio() == target + assert generic_debt_allocator.totalDebtRatio() == target assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) From 5332c50559166399b23a750efc716d24db0e02b2 Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:26:12 -0700 Subject: [PATCH 32/35] build: use central allocator factory (#31) * build: use central factory * chore: comments * test: update for factory * fix: bump by 20% --- contracts/Managers/RoleManager.sol | 2 +- .../debtAllocators/GenericDebtAllocator.sol | 104 +++--- .../GenericDebtAllocatorFactory.sol | 137 ++++++-- tests/conftest.py | 14 +- .../test_generic_debt_allocator.py | 174 ++++++---- .../yield/test_yield_manager.py | 311 +++++++++--------- tests/manager/test_role_manager.py | 18 +- 7 files changed, 447 insertions(+), 313 deletions(-) diff --git a/contracts/Managers/RoleManager.sol b/contracts/Managers/RoleManager.sol index 2d15b85..ca55991 100644 --- a/contracts/Managers/RoleManager.sol +++ b/contracts/Managers/RoleManager.sol @@ -318,7 +318,7 @@ contract RoleManager is Governance2Step { if (factory != address(0)) { // Deploy a new debt allocator for the vault with Brain as the gov. _debtAllocator = GenericDebtAllocatorFactory(factory) - .newGenericDebtAllocator(_vault, getPositionHolder(BRAIN)); + .newGenericDebtAllocator(_vault); } else { // If no factory is set we should be using one central allocator. _debtAllocator = getPositionHolder(DEBT_ALLOCATOR); diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index ef0fee2..55d377d 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -3,9 +3,10 @@ pragma solidity 0.8.18; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Governance} from "@periphery/utils/Governance.sol"; import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; +import {GenericDebtAllocatorFactory} from "./GenericDebtAllocatorFactory.sol"; + /** * @title YearnV3 Generic Debt Allocator * @author yearn.finance @@ -28,7 +29,7 @@ import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; * And will pull funds from the strategy when it has `minimumChange` * more than its `maxRatio`. */ -contract GenericDebtAllocator is Governance { +contract GenericDebtAllocator { /// @notice An event emitted when a strategies debt ratios are Updated. event UpdateStrategyDebtRatio( address indexed strategy, @@ -44,7 +45,7 @@ contract GenericDebtAllocator is Governance { event UpdateMinimumChange(uint256 newMinimumChange); /// @notice An event emitted when a keeper is added or removed. - event UpdateKeeper(address indexed keeper, bool allowed); + event UpdateManager(address indexed manager, bool allowed); /// @notice An event emitted when the max debt update loss is updated. event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); @@ -68,13 +69,47 @@ contract GenericDebtAllocator is Governance { uint96 open; } + /// @notice Make sure the caller is governance. + modifier onlyGovernance() { + _isGovernance(); + _; + } + + /// @notice Make sure the caller is governance or a manager. + modifier onlyManagers() { + _isManager(); + _; + } + + /// @notice Make sure the caller is a keeper modifier onlyKeepers() { _isKeeper(); _; } + /// @notice Check the Factories governance address. + function _isGovernance() internal view virtual { + require( + msg.sender == GenericDebtAllocatorFactory(factory).governance(), + "!governance" + ); + } + + /// @notice Check is either factories governance or local manager. + function _isManager() internal view virtual { + require( + managers[msg.sender] || + msg.sender == GenericDebtAllocatorFactory(factory).governance(), + "!manager" + ); + } + + /// @notice Check is one of the allowed keepers. function _isKeeper() internal view virtual { - require(keepers[msg.sender], "!keeper"); + require( + GenericDebtAllocatorFactory(factory).keepers(msg.sender), + "!keeper" + ); } uint256 internal constant MAX_BPS = 10_000; @@ -82,6 +117,9 @@ contract GenericDebtAllocator is Governance { /// @notice Address of the vault this serves as allocator for. address public vault; + /// @notice Address to get permissioned roles from. + address public factory; + /// @notice Time to wait between debt updates in seconds. uint256 public minimumWait; @@ -101,41 +139,31 @@ contract GenericDebtAllocator is Governance { uint256 public maxAcceptableBaseFee; /// @notice Mapping of addresses that are allowed to update debt. - mapping(address => bool) public keepers; + mapping(address => bool) public managers; /// @notice Mapping of strategy => its config. mapping(address => Config) internal _configs; - constructor( - address _vault, - address _governance, - uint256 _minimumChange - ) Governance(_governance) { - initialize(_vault, _governance, _minimumChange); + constructor(address _vault, uint256 _minimumChange) { + initialize(_vault, _minimumChange); } /** * @notice Initializes the debt allocator. * @dev Should be called atomically after cloning. * @param _vault Address of the vault this allocates debt for. - * @param _governance Address to govern this contract. * @param _minimumChange The minimum in asset that must be moved. */ - function initialize( - address _vault, - address _governance, - uint256 _minimumChange - ) public virtual { + function initialize(address _vault, uint256 _minimumChange) public virtual { require(address(vault) == address(0), "!initialized"); + // Set the factory to retrieve roles from. + factory = msg.sender; // Set initial variables. vault = _vault; - governance = _governance; minimumChange = _minimumChange; // Default max loss on debt updates to 1 BP. maxDebtUpdateLoss = 1; - // Default to allow governance to be a keeper. - keepers[_governance] = true; // Default max base fee to uint256 max maxAcceptableBaseFee = type(uint256).max; } @@ -191,8 +219,6 @@ contract GenericDebtAllocator is Governance { * @dev This should be called by a keeper to decide if a strategies * debt should be updated and if so by how much. * - * NOTE: This cannot be used to withdraw down to 0 debt. - * * @param _strategy Address of the strategy to check. * @return . Bool representing if the debt should be updated. * @return . Calldata if `true` or reason if `false`. @@ -334,7 +360,7 @@ contract GenericDebtAllocator is Governance { /** * @notice Sets a new target debt ratio for a strategy. - * @dev This will default to a 10% increase for max debt. + * @dev This will default to a 20% increase for max debt. * * @param _strategy Address of the strategy to set. * @param _targetRatio Amount in Basis points to allocate. @@ -343,7 +369,7 @@ contract GenericDebtAllocator is Governance { address _strategy, uint256 _targetRatio ) public virtual { - uint256 maxRatio = Math.min((_targetRatio * 11_000) / MAX_BPS, MAX_BPS); + uint256 maxRatio = Math.min((_targetRatio * 12_000) / MAX_BPS, MAX_BPS); setStrategyDebtRatio(_strategy, _targetRatio, maxRatio); } @@ -360,7 +386,7 @@ contract GenericDebtAllocator is Governance { address _strategy, uint256 _targetRatio, uint256 _maxRatio - ) public virtual onlyKeepers { + ) public virtual onlyManagers { // Make sure a minimumChange has been set. require(minimumChange != 0, "!minimum"); // Cannot be more than 100%. @@ -395,20 +421,6 @@ contract GenericDebtAllocator is Governance { ); } - /** - * @notice Set if a keeper can update debt. - * @param _address The address to set mapping for. - * @param _allowed If the address can call {update_debt}. - */ - function setKeeper( - address _address, - bool _allowed - ) external virtual onlyGovernance { - keepers[_address] = _allowed; - - emit UpdateKeeper(_address, _allowed); - } - /** * @notice Set the minimum change variable for a strategy. * @dev This is the amount of debt that will needed to be @@ -471,6 +483,20 @@ contract GenericDebtAllocator is Governance { emit UpdateMaxAcceptableBaseFee(_maxAcceptableBaseFee); } + /** + * @notice Set if a manager can update ratios. + * @param _address The address to set mapping for. + * @param _allowed If the address can call {update_debt}. + */ + function setManager( + address _address, + bool _allowed + ) external virtual onlyGovernance { + managers[_address] = _allowed; + + emit UpdateManager(_address, _allowed); + } + /** * @notice Get a strategies full config. * @dev Used for customizations by inheriting the contract. diff --git a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol b/contracts/debtAllocators/GenericDebtAllocatorFactory.sol index be5759c..3bc4c6d 100644 --- a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol +++ b/contracts/debtAllocators/GenericDebtAllocatorFactory.sol @@ -1,21 +1,64 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity 0.8.18; -import {GenericDebtAllocator} from "./GenericDebtAllocator.sol"; import {Clonable} from "@periphery/utils/Clonable.sol"; +import {RoleManager} from "../Managers/RoleManager.sol"; +import {Governance} from "@periphery/utils/Governance.sol"; +import {GenericDebtAllocator} from "./GenericDebtAllocator.sol"; /** * @title YearnV3 Generic Debt Allocator Factory * @author yearn.finance * @notice - * Factory for anyone to easily deploy their own generic - * debt allocator for a Yearn V3 Vault. + * Factory to deploy a debt allocator for a YearnV3 vault. */ -contract GenericDebtAllocatorFactory is Clonable { +contract GenericDebtAllocatorFactory is Governance, Clonable { + /// @notice Revert message for when a debt allocator already exists. + error AlreadyDeployed(address _allocator); + + /// @notice An even emitted when a new `roleManager` is set. + event UpdateRoleManager(address indexed roleManager); + + /// @notice An event emitted when a keeper is added or removed. + event UpdateKeeper(address indexed keeper, bool allowed); + + /// @notice An event emitted when a new debt allocator is added or deployed. event NewDebtAllocator(address indexed allocator, address indexed vault); - constructor() { - original = address(new GenericDebtAllocator(address(1), address(2), 0)); + /// @notice Only allow `governance` or the `roleManager`. + modifier onlyAuthorized() { + _isAuthorized(); + _; + } + + /// @notice Check is `governance` or the `roleManager`. + function _isAuthorized() internal view { + require( + msg.sender == roleManager || msg.sender == governance, + "!authorized" + ); + } + + /// @notice Address that is the roleManager for all vaults and can deploy allocators. + address public roleManager; + + /// @notice Mapping of addresses that are allowed to update debt. + mapping(address => bool) public keepers; + + constructor( + address _governance, + address _roleManager + ) Governance(_governance) { + // Deploy a dummy allocator as the original. + original = address(new GenericDebtAllocator(address(1), 0)); + + // Set the initial role manager. + roleManager = _roleManager; + emit UpdateRoleManager(_roleManager); + + // Default to allow governance to be a keeper. + keepers[_governance] = true; + emit UpdateKeeper(_governance, true); } /** @@ -29,43 +72,79 @@ contract GenericDebtAllocatorFactory is Clonable { function newGenericDebtAllocator( address _vault ) external virtual returns (address) { - return newGenericDebtAllocator(_vault, msg.sender, 0); - } - - /** - * @notice Clones a new debt allocator. - * @dev defaults to 0 for the `minimumChange`. - * - * @param _vault The vault for the allocator to be hooked to. - * @param _governance Address to serve as governance. - * @return Address of the new generic debt allocator - */ - function newGenericDebtAllocator( - address _vault, - address _governance - ) external virtual returns (address) { - return newGenericDebtAllocator(_vault, _governance, 0); + return newGenericDebtAllocator(_vault, 0); } /** * @notice Clones a new debt allocator. * @param _vault The vault for the allocator to be hooked to. - * @param _governance Address to serve as governance. + * @param _minimumChange The minimum amount needed to trigger debt update. * @return newAllocator Address of the new generic debt allocator */ function newGenericDebtAllocator( address _vault, - address _governance, uint256 _minimumChange ) public virtual returns (address newAllocator) { + // Clone new allocator off the original. newAllocator = _clone(); - GenericDebtAllocator(newAllocator).initialize( - _vault, - _governance, - _minimumChange - ); + // Initialize the new allocator. + GenericDebtAllocator(newAllocator).initialize(_vault, _minimumChange); + // Emit event. emit NewDebtAllocator(newAllocator, _vault); } + + /** + * @notice Check if a strategy's debt should be updated. + * @dev This should be called by a keeper to decide if a strategies + * debt should be updated and if so by how much. + * + * @param _vault Address of the vault. + * @param _strategy Address of the strategy to check. + * @return . Bool representing if the debt should be updated. + * @return . Calldata if `true` or reason if `false`. + */ + function shouldUpdateDebt( + address _vault, + address _strategy + ) public view virtual returns (bool, bytes memory) { + return + GenericDebtAllocator(allocator(_vault)).shouldUpdateDebt(_strategy); + } + + /** + * @notice Helper function to easily get a vaults debt allocator from the role manager. + * @param _vault Address of the vault to get the allocator for. + * @return Address of the vaults debt allocator if one exists. + */ + function allocator(address _vault) public view virtual returns (address) { + return RoleManager(roleManager).getDebtAllocator(_vault); + } + + /** + * @notice Update the Role Manager address. + * @param _roleManager New role manager address. + */ + function setRoleManager( + address _roleManager + ) external virtual onlyGovernance { + roleManager = _roleManager; + + emit UpdateRoleManager(_roleManager); + } + + /** + * @notice Set if a keeper can update debt. + * @param _address The address to set mapping for. + * @param _allowed If the address can call {update_debt}. + */ + function setKeeper( + address _address, + bool _allowed + ) external virtual onlyGovernance { + keepers[_address] = _allowed; + + emit UpdateKeeper(_address, _allowed); + } } diff --git a/tests/conftest.py b/tests/conftest.py index fc2a761..dd1eec5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ import pytest from ape import accounts, project, networks -from utils.constants import MAX_INT, WEEK, ROLES +from utils.constants import MAX_INT, WEEK, ROLES, ZERO_ADDRESS from web3 import Web3, HTTPProvider from hexbytes import HexBytes import os @@ -395,9 +395,11 @@ def address_provider(deploy_address_provider): @pytest.fixture(scope="session") -def deploy_generic_debt_allocator_factory(project, daddy): +def deploy_generic_debt_allocator_factory(project, daddy, brain): def deploy_generic_debt_allocator_factory(gov=daddy): - generic_debt_allocator_factory = gov.deploy(project.GenericDebtAllocatorFactory) + generic_debt_allocator_factory = gov.deploy( + project.GenericDebtAllocatorFactory, brain, ZERO_ADDRESS + ) return generic_debt_allocator_factory @@ -413,9 +415,7 @@ def generic_debt_allocator_factory(deploy_generic_debt_allocator_factory): @pytest.fixture(scope="session") def generic_debt_allocator(generic_debt_allocator_factory, project, vault, daddy): - tx = generic_debt_allocator_factory.newGenericDebtAllocator( - vault, daddy, sender=daddy - ) + tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=daddy) event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] @@ -446,6 +446,7 @@ def deploy_role_manager( def role_manager( deploy_role_manager, daddy, + brain, healthcheck_accountant, generic_debt_allocator_factory, registry, @@ -459,6 +460,7 @@ def role_manager( role_manager.setPositionHolder( role_manager.ALLOCATOR_FACTORY(), generic_debt_allocator_factory, sender=daddy ) + generic_debt_allocator_factory.setRoleManager(role_manager, sender=brain) return role_manager diff --git a/tests/debtAllocators/test_generic_debt_allocator.py b/tests/debtAllocators/test_generic_debt_allocator.py index 278f4b0..64d8671 100644 --- a/tests/debtAllocators/test_generic_debt_allocator.py +++ b/tests/debtAllocators/test_generic_debt_allocator.py @@ -3,10 +3,12 @@ from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES -def test_setup(generic_debt_allocator_factory, user, strategy, vault): - tx = generic_debt_allocator_factory.newGenericDebtAllocator( - vault, user, 0, sender=user - ) +def test_setup(generic_debt_allocator_factory, brain, user, strategy, vault): + assert generic_debt_allocator_factory.governance() == brain + assert generic_debt_allocator_factory.keepers(brain) == True + assert generic_debt_allocator_factory.roleManager() == ZERO_ADDRESS + + tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, 0, sender=user) events = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator)) @@ -15,8 +17,8 @@ def test_setup(generic_debt_allocator_factory, user, strategy, vault): generic_debt_allocator = project.GenericDebtAllocator.at(events[0].allocator) - assert generic_debt_allocator.governance() == user - assert generic_debt_allocator.keepers(user) == True + assert generic_debt_allocator.managers(brain) == False + assert generic_debt_allocator.factory() == generic_debt_allocator_factory.address assert generic_debt_allocator.vault() == vault.address assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) assert generic_debt_allocator.totalDebtRatio() == 0 @@ -24,31 +26,57 @@ def test_setup(generic_debt_allocator_factory, user, strategy, vault): generic_debt_allocator.shouldUpdateDebt(strategy) -def test_set_keepers(generic_debt_allocator, daddy, vault, strategy, user): - assert generic_debt_allocator.keepers(daddy) == True - assert generic_debt_allocator.keepers(user) == False +def test_set_keepers( + generic_debt_allocator_factory, generic_debt_allocator, brain, user +): + assert generic_debt_allocator_factory.keepers(brain) == True + assert generic_debt_allocator_factory.keepers(user) == False with ape.reverts("!governance"): - generic_debt_allocator.setKeeper(user, True, sender=user) + generic_debt_allocator_factory.setKeeper(user, True, sender=user) - tx = generic_debt_allocator.setKeeper(user, True, sender=daddy) + tx = generic_debt_allocator_factory.setKeeper(user, True, sender=brain) - event = list(tx.decode_logs(generic_debt_allocator.UpdateKeeper))[0] + event = list(tx.decode_logs(generic_debt_allocator_factory.UpdateKeeper))[0] assert event.keeper == user assert event.allowed == True - assert generic_debt_allocator.keepers(user) == True + assert generic_debt_allocator_factory.keepers(user) == True + + tx = generic_debt_allocator_factory.setKeeper(brain, False, sender=brain) + + event = list(tx.decode_logs(generic_debt_allocator_factory.UpdateKeeper))[0] + + assert event.keeper == brain + assert event.allowed == False + assert generic_debt_allocator_factory.keepers(brain) == False + + +def test_set_managers(generic_debt_allocator, brain, user): + assert generic_debt_allocator.managers(brain) == False + assert generic_debt_allocator.managers(user) == False + + with ape.reverts("!governance"): + generic_debt_allocator.setManager(user, True, sender=user) + + tx = generic_debt_allocator.setManager(user, True, sender=brain) + + event = list(tx.decode_logs(generic_debt_allocator.UpdateManager))[0] + + assert event.manager == user + assert event.allowed == True + assert generic_debt_allocator.managers(user) == True - tx = generic_debt_allocator.setKeeper(daddy, False, sender=daddy) + tx = generic_debt_allocator.setManager(user, False, sender=brain) - event = list(tx.decode_logs(generic_debt_allocator.UpdateKeeper))[0] + event = list(tx.decode_logs(generic_debt_allocator.UpdateManager))[0] - assert event.keeper == daddy + assert event.manager == user assert event.allowed == False - assert generic_debt_allocator.keepers(daddy) == False + assert generic_debt_allocator.managers(user) == False -def test_set_minimum_change(generic_debt_allocator, daddy, vault, strategy, user): +def test_set_minimum_change(generic_debt_allocator, brain, strategy, user): assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) assert generic_debt_allocator.minimumChange() == 0 @@ -58,9 +86,9 @@ def test_set_minimum_change(generic_debt_allocator, daddy, vault, strategy, user generic_debt_allocator.setMinimumChange(minimum, sender=user) with ape.reverts("zero"): - generic_debt_allocator.setMinimumChange(0, sender=daddy) + generic_debt_allocator.setMinimumChange(0, sender=brain) - tx = generic_debt_allocator.setMinimumChange(minimum, sender=daddy) + tx = generic_debt_allocator.setMinimumChange(minimum, sender=brain) event = list(tx.decode_logs(generic_debt_allocator.UpdateMinimumChange))[0] @@ -68,7 +96,7 @@ def test_set_minimum_change(generic_debt_allocator, daddy, vault, strategy, user assert generic_debt_allocator.minimumChange() == minimum -def test_set_minimum_wait(generic_debt_allocator, daddy, vault, strategy, user): +def test_set_minimum_wait(generic_debt_allocator, brain, strategy, user): assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) assert generic_debt_allocator.minimumWait() == 0 @@ -77,7 +105,7 @@ def test_set_minimum_wait(generic_debt_allocator, daddy, vault, strategy, user): with ape.reverts("!governance"): generic_debt_allocator.setMinimumWait(minimum, sender=user) - tx = generic_debt_allocator.setMinimumWait(minimum, sender=daddy) + tx = generic_debt_allocator.setMinimumWait(minimum, sender=brain) event = list(tx.decode_logs(generic_debt_allocator.UpdateMinimumWait))[0] @@ -85,7 +113,7 @@ def test_set_minimum_wait(generic_debt_allocator, daddy, vault, strategy, user): assert generic_debt_allocator.minimumWait() == minimum -def test_set_max_debt_update_loss(generic_debt_allocator, daddy, vault, strategy, user): +def test_set_max_debt_update_loss(generic_debt_allocator, brain, strategy, user): assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) assert generic_debt_allocator.maxDebtUpdateLoss() == 1 @@ -95,9 +123,9 @@ def test_set_max_debt_update_loss(generic_debt_allocator, daddy, vault, strategy generic_debt_allocator.setMaxDebtUpdateLoss(max, sender=user) with ape.reverts("higher than max"): - generic_debt_allocator.setMaxDebtUpdateLoss(10_001, sender=daddy) + generic_debt_allocator.setMaxDebtUpdateLoss(10_001, sender=brain) - tx = generic_debt_allocator.setMaxDebtUpdateLoss(max, sender=daddy) + tx = generic_debt_allocator.setMaxDebtUpdateLoss(max, sender=brain) event = list(tx.decode_logs(generic_debt_allocator.UpdateMaxDebtUpdateLoss))[0] @@ -106,7 +134,7 @@ def test_set_max_debt_update_loss(generic_debt_allocator, daddy, vault, strategy def test_set_ratios( - generic_debt_allocator, daddy, vault, strategy, create_strategy, user + generic_debt_allocator, brain, daddy, vault, strategy, create_strategy, user ): assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) @@ -114,28 +142,28 @@ def test_set_ratios( max = int(6_000) target = int(5_000) - with ape.reverts("!keeper"): + with ape.reverts("!manager"): generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=user) vault.add_strategy(strategy.address, sender=daddy) with ape.reverts("!minimum"): - generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=daddy) + generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) - generic_debt_allocator.setMinimumChange(minimum, sender=daddy) + generic_debt_allocator.setMinimumChange(minimum, sender=brain) with ape.reverts("max too high"): generic_debt_allocator.setStrategyDebtRatio( - strategy, target, int(10_001), sender=daddy + strategy, target, int(10_001), sender=brain ) with ape.reverts("max ratio"): generic_debt_allocator.setStrategyDebtRatio( - strategy, int(max + 1), max, sender=daddy + strategy, int(max + 1), max, sender=brain ) tx = generic_debt_allocator.setStrategyDebtRatio( - strategy, target, max, sender=daddy + strategy, target, max, sender=brain ) event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] @@ -150,32 +178,32 @@ def test_set_ratios( vault.add_strategy(new_strategy, sender=daddy) with ape.reverts("ratio too high"): generic_debt_allocator.setStrategyDebtRatio( - new_strategy, int(10_000), int(10_000), sender=daddy + new_strategy, int(10_000), int(10_000), sender=brain ) target = int(8_000) - tx = generic_debt_allocator.setStrategyDebtRatio(strategy, target, sender=daddy) + tx = generic_debt_allocator.setStrategyDebtRatio(strategy, target, sender=brain) event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] assert event.newTargetRatio == target - assert event.newMaxRatio == target * 1.1 + assert event.newMaxRatio == target * 1.2 assert event.newTotalDebtRatio == target assert generic_debt_allocator.totalDebtRatio() == target - assert generic_debt_allocator.getConfig(strategy) == (target, target * 1.1, 0, 0) + assert generic_debt_allocator.getConfig(strategy) == (target, target * 1.2, 0, 0) def test_increase_debt_ratio( - generic_debt_allocator, daddy, vault, strategy, create_strategy, user + generic_debt_allocator, brain, daddy, vault, strategy, create_strategy, user ): assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) minimum = int(1e17) target = int(5_000) increase = int(5_000) - max = target * 1.1 + max = target * 1.2 - with ape.reverts("!keeper"): + with ape.reverts("!manager"): generic_debt_allocator.increaseStrategyDebtRatio( strategy, increase, sender=user ) @@ -184,13 +212,13 @@ def test_increase_debt_ratio( with ape.reverts("!minimum"): generic_debt_allocator.increaseStrategyDebtRatio( - strategy, increase, sender=daddy + strategy, increase, sender=brain ) - generic_debt_allocator.setMinimumChange(minimum, sender=daddy) + generic_debt_allocator.setMinimumChange(minimum, sender=brain) tx = generic_debt_allocator.increaseStrategyDebtRatio( - strategy, increase, sender=daddy + strategy, increase, sender=brain ) event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] @@ -206,14 +234,14 @@ def test_increase_debt_ratio( with ape.reverts("ratio too high"): generic_debt_allocator.increaseStrategyDebtRatio( - new_strategy, int(5_001), sender=daddy + new_strategy, int(5_001), sender=brain ) target = int(8_000) - max = target * 1.1 + max = target * 1.2 increase = int(3_000) tx = generic_debt_allocator.increaseStrategyDebtRatio( - strategy, increase, sender=daddy + strategy, increase, sender=brain ) event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] @@ -228,7 +256,7 @@ def test_increase_debt_ratio( max = int(10_000) increase = int(2_000) tx = generic_debt_allocator.increaseStrategyDebtRatio( - strategy, increase, sender=daddy + strategy, increase, sender=brain ) event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] @@ -241,24 +269,24 @@ def test_increase_debt_ratio( def test_decrease_debt_ratio( - generic_debt_allocator, daddy, vault, strategy, create_strategy, user + generic_debt_allocator, brain, vault, strategy, daddy, create_strategy, user ): assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) minimum = int(1e17) target = int(5_000) - max = target * 1.1 + max = target * 1.2 vault.add_strategy(strategy.address, sender=daddy) - generic_debt_allocator.setMinimumChange(minimum, sender=daddy) + generic_debt_allocator.setMinimumChange(minimum, sender=brain) # Underflow with ape.reverts(): - generic_debt_allocator.decreaseStrategyDebtRatio(strategy, target, sender=daddy) + generic_debt_allocator.decreaseStrategyDebtRatio(strategy, target, sender=brain) # Add the target tx = generic_debt_allocator.increaseStrategyDebtRatio( - strategy, target, sender=daddy + strategy, target, sender=brain ) event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] @@ -270,16 +298,16 @@ def test_decrease_debt_ratio( assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) target = int(2_000) - max = target * 1.1 + max = target * 1.2 decrease = int(3_000) - with ape.reverts("!keeper"): + with ape.reverts("!manager"): generic_debt_allocator.decreaseStrategyDebtRatio( strategy, decrease, sender=user ) tx = generic_debt_allocator.decreaseStrategyDebtRatio( - strategy, decrease, sender=daddy + strategy, decrease, sender=brain ) event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] @@ -294,7 +322,7 @@ def test_decrease_debt_ratio( max = int(0) decrease = int(2_000) tx = generic_debt_allocator.decreaseStrategyDebtRatio( - strategy, decrease, sender=daddy + strategy, decrease, sender=brain ) event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] @@ -307,7 +335,7 @@ def test_decrease_debt_ratio( def test_should_update_debt( - generic_debt_allocator, vault, strategy, daddy, deposit_into_vault, amount + generic_debt_allocator, vault, strategy, brain, daddy, deposit_into_vault, amount ): assert generic_debt_allocator.getConfig(strategy.address) == (0, 0, 0, 0) vault.add_role(generic_debt_allocator, ROLES.DEBT_MANAGER, sender=daddy) @@ -321,8 +349,8 @@ def test_should_update_debt( target = int(5_000) max = int(5_000) - generic_debt_allocator.setMinimumChange(minimum, sender=daddy) - generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=daddy) + generic_debt_allocator.setMinimumChange(minimum, sender=brain) + generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) # Vault has no assets so should be false. (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) @@ -345,7 +373,7 @@ def test_should_update_debt( print("Made ", vault.update_debt.encode_input(strategy.address, amount // 2)) assert bytes == vault.update_debt.encode_input(strategy.address, amount // 2) - generic_debt_allocator.update_debt(strategy, amount // 2, sender=daddy) + generic_debt_allocator.update_debt(strategy, amount // 2, sender=brain) chain.mine(1) # Should now be false again once allocated @@ -355,7 +383,7 @@ def test_should_update_debt( # Update the ratio to make true generic_debt_allocator.setStrategyDebtRatio( - strategy, int(target + 1), int(target + 1), sender=daddy + strategy, int(target + 1), int(target + 1), sender=brain ) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) @@ -365,13 +393,13 @@ def test_should_update_debt( ) # Set a minimumWait time - generic_debt_allocator.setMinimumWait(MAX_INT, sender=daddy) + generic_debt_allocator.setMinimumWait(MAX_INT, sender=brain) # Should now be false (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) assert bool == False assert bytes == ("min wait").encode("utf-8") - generic_debt_allocator.setMinimumWait(0, sender=daddy) + generic_debt_allocator.setMinimumWait(0, sender=brain) # Lower the max debt so its == to current debt vault.update_max_debt_for_strategy(strategy, int(amount // 2), sender=daddy) @@ -399,16 +427,16 @@ def test_should_update_debt( vault.set_minimum_total_idle(0, sender=daddy) # increase the minimum so its false again - generic_debt_allocator.setMinimumChange(int(1e30), sender=daddy) + generic_debt_allocator.setMinimumChange(int(1e30), sender=brain) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) assert bool == False assert bytes == ("Below Min").encode("utf-8") # Lower the target and minimum - generic_debt_allocator.setMinimumChange(int(1), sender=daddy) + generic_debt_allocator.setMinimumChange(int(1), sender=brain) generic_debt_allocator.setStrategyDebtRatio( - strategy, int(target // 2), int(target // 2), sender=daddy + strategy, int(target // 2), int(target // 2), sender=brain ) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) @@ -417,7 +445,15 @@ def test_should_update_debt( def test_update_debt( - generic_debt_allocator, vault, strategy, daddy, user, deposit_into_vault, amount + generic_debt_allocator_factory, + generic_debt_allocator, + vault, + strategy, + brain, + daddy, + user, + deposit_into_vault, + amount, ): assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) deposit_into_vault(vault, amount) @@ -434,7 +470,7 @@ def test_update_debt( # This reverts by the vault with ape.reverts("not allowed"): - generic_debt_allocator.update_debt(strategy, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy, amount, sender=brain) vault.add_role( generic_debt_allocator, @@ -442,14 +478,14 @@ def test_update_debt( sender=daddy, ) - generic_debt_allocator.update_debt(strategy, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy, amount, sender=brain) timestamp = generic_debt_allocator.getConfig(strategy)[2] assert timestamp != 0 assert vault.totalIdle() == 0 assert vault.totalDebt() == amount - generic_debt_allocator.setKeeper(user, True, sender=daddy) + generic_debt_allocator_factory.setKeeper(user, True, sender=brain) generic_debt_allocator.update_debt(strategy, 0, sender=user) diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 6272706..6d218ab 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -12,6 +12,39 @@ def setup_vault(vault, strategies, oracle, chad): oracle.setOracle(strategy, strategy, sender=management) +def full_setup( + deploy_mock_tokenized, + vault, + apr_oracle, + daddy, + brain, + yield_manager, + generic_debt_allocator, + keeper, + management, + user, +): + # Strategy two will have the higher apr + strategy_one = deploy_mock_tokenized("One", int(1e16)) + strategy_two = deploy_mock_tokenized("two", int(1e17)) + setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) + yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + generic_debt_allocator.setManager(yield_manager, True, sender=brain) + generic_debt_allocator.setMinimumChange(1, sender=brain) + vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) + vault.set_role( + generic_debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) + strategy_one.setKeeper(keeper, sender=management) + keeper.addNewStrategy(strategy_one, sender=daddy) + keeper.setKeeper(yield_manager, True, sender=daddy) + yield_manager.setProposer(user, True, sender=daddy) + + return (strategy_one, strategy_two) + + def test_yield_manager_setup(yield_manager, daddy, vault, management, keeper): assert yield_manager.keeper() == keeper assert yield_manager.governance() == daddy @@ -75,6 +108,7 @@ def test_update_allocation( management, keeper, daddy, + brain, user, amount, asset, @@ -103,12 +137,12 @@ def test_update_allocation( yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - # Must give allocator the keeper role - with ape.reverts("!keeper"): + # Must give allocator the manager role + with ape.reverts("!manager"): yield_manager.updateAllocation(vault, allocation, sender=user) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) + generic_debt_allocator.setManager(yield_manager, True, sender=brain) + generic_debt_allocator.setMinimumChange(1, sender=brain) vault.set_role( generic_debt_allocator, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, @@ -136,7 +170,7 @@ def test_update_allocation( ] == vault.update_debt.encode_input(strategy_two.address, amount) assert before == 0 - generic_debt_allocator.update_debt(strategy_two, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_two, amount, sender=brain) assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False # assert now == int(1e17 * amount) @@ -179,13 +213,13 @@ def test_update_allocation( strategy_two.address, amount - to_move ) - generic_debt_allocator.update_debt(strategy_two, amount - to_move, sender=daddy) + generic_debt_allocator.update_debt(strategy_two, amount - to_move, sender=brain) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, to_move) - generic_debt_allocator.update_debt(strategy_one, to_move, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, to_move, sender=brain) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) @@ -215,13 +249,13 @@ def test_update_allocation( assert bool_two == True assert bytes_two == vault.update_debt.encode_input(strategy_two.address, 0) - generic_debt_allocator.update_debt(strategy_two, 0, sender=daddy) + generic_debt_allocator.update_debt(strategy_two, 0, sender=brain) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, amount) - generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) @@ -248,29 +282,25 @@ def test_update_allocation_pending_profit( management, keeper, daddy, + brain, user, amount, asset, deploy_mock_tokenized, generic_debt_allocator, ): - # Strategy two will have the higher apr - strategy_one = deploy_mock_tokenized("One", int(1e16)) - strategy_two = deploy_mock_tokenized("two", int(1e17)) - setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - vault.set_role( - generic_debt_allocator, - ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, - sender=daddy, + strategy_one, strategy_two = full_setup( + deploy_mock_tokenized=deploy_mock_tokenized, + vault=vault, + apr_oracle=apr_oracle, + daddy=daddy, + brain=brain, + yield_manager=yield_manager, + generic_debt_allocator=generic_debt_allocator, + keeper=keeper, + management=management, + user=user, ) - strategy_one.setKeeper(keeper, sender=management) - keeper.addNewStrategy(strategy_one, sender=daddy) - keeper.setKeeper(yield_manager, True, sender=daddy) - yield_manager.setProposer(user, True, sender=daddy) profit = amount // 10 amount = amount - profit @@ -278,7 +308,7 @@ def test_update_allocation_pending_profit( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -297,7 +327,7 @@ def test_update_allocation_pending_profit( assert bool == True assert bytes == vault.update_debt.encode_input(strategy_one, 0) - tx = generic_debt_allocator.update_debt(strategy_one, 0, sender=daddy) + tx = generic_debt_allocator.update_debt(strategy_one, 0, sender=brain) event = list(tx.decode_logs(vault.StrategyReported)) assert len(event) == 1 @@ -315,36 +345,32 @@ def test_update_allocation_pending_loss( management, keeper, daddy, + brain, user, amount, asset, deploy_mock_tokenized, generic_debt_allocator, ): - # Strategy two will have the higher apr - strategy_one = deploy_mock_tokenized("One", int(1e16)) - strategy_two = deploy_mock_tokenized("two", int(1e17)) - setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - vault.set_role( - generic_debt_allocator, - ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, - sender=daddy, + strategy_one, strategy_two = full_setup( + deploy_mock_tokenized=deploy_mock_tokenized, + vault=vault, + apr_oracle=apr_oracle, + daddy=daddy, + brain=brain, + yield_manager=yield_manager, + generic_debt_allocator=generic_debt_allocator, + keeper=keeper, + management=management, + user=user, ) - strategy_one.setKeeper(keeper, sender=management) - keeper.addNewStrategy(strategy_one, sender=daddy) - keeper.setKeeper(yield_manager, True, sender=daddy) - yield_manager.setProposer(user, True, sender=daddy) loss = amount // 10 asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -363,7 +389,7 @@ def test_update_allocation_pending_loss( assert bool == True assert bytes == vault.update_debt.encode_input(strategy_one, 0) - generic_debt_allocator.update_debt(strategy_one, 0, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, 0, sender=brain) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True @@ -377,36 +403,32 @@ def test_update_allocation_pending_loss_move_half( management, keeper, daddy, + brain, user, amount, asset, deploy_mock_tokenized, generic_debt_allocator, ): - # Strategy two will have the higher apr - strategy_one = deploy_mock_tokenized("One", int(1e16)) - strategy_two = deploy_mock_tokenized("two", int(1e17)) - setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - vault.set_role( - generic_debt_allocator, - ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, - sender=daddy, + strategy_one, strategy_two = full_setup( + deploy_mock_tokenized=deploy_mock_tokenized, + vault=vault, + apr_oracle=apr_oracle, + daddy=daddy, + brain=brain, + yield_manager=yield_manager, + generic_debt_allocator=generic_debt_allocator, + keeper=keeper, + management=management, + user=user, ) - strategy_one.setKeeper(keeper, sender=management) - keeper.addNewStrategy(strategy_one, sender=daddy) - keeper.setKeeper(yield_manager, True, sender=daddy) - yield_manager.setProposer(user, True, sender=daddy) loss = amount // 10 asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -428,7 +450,7 @@ def test_update_allocation_pending_loss_move_half( (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) assert bool == True - generic_debt_allocator.update_debt(strategy_one, amount - to_move, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount - to_move, sender=brain) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True @@ -442,36 +464,32 @@ def test_update_allocation_pending_loss_move_all( management, keeper, daddy, + brain, user, amount, asset, deploy_mock_tokenized, generic_debt_allocator, ): - # Strategy two will have the higher apr - strategy_one = deploy_mock_tokenized("One", int(1e16)) - strategy_two = deploy_mock_tokenized("two", int(1e17)) - setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - vault.set_role( - generic_debt_allocator, - ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, - sender=daddy, + strategy_one, strategy_two = full_setup( + deploy_mock_tokenized=deploy_mock_tokenized, + vault=vault, + apr_oracle=apr_oracle, + daddy=daddy, + brain=brain, + yield_manager=yield_manager, + generic_debt_allocator=generic_debt_allocator, + keeper=keeper, + management=management, + user=user, ) - strategy_one.setKeeper(keeper, sender=management) - keeper.addNewStrategy(strategy_one, sender=daddy) - keeper.setKeeper(yield_manager, True, sender=daddy) - yield_manager.setProposer(user, True, sender=daddy) loss = amount // 10 asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -493,7 +511,7 @@ def test_update_allocation_pending_loss_move_all( (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) assert bool == True - generic_debt_allocator.update_debt(strategy_one, amount - to_move, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount - to_move, sender=brain) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True @@ -505,6 +523,7 @@ def test_validate_allocation( yield_manager, vault, daddy, + brain, user, amount, asset, @@ -533,7 +552,7 @@ def test_validate_allocation( vault, [(strategy_one, amount), (strategy_two, 0)] ) - generic_debt_allocator.update_debt(strategy_one, amount // 2, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount // 2, sender=brain) assert yield_manager.validateAllocation(vault, []) == False assert yield_manager.validateAllocation(vault, [(strategy_one, amount)]) @@ -543,7 +562,7 @@ def test_validate_allocation( # Now will be false generic_debt_allocator.update_debt( - strategy_two, vault.totalIdle() // 2, sender=daddy + strategy_two, vault.totalIdle() // 2, sender=brain ) assert yield_manager.validateAllocation(vault, []) == False @@ -611,26 +630,25 @@ def test_update_allocation_permissioned( management, keeper, daddy, + brain, user, amount, asset, deploy_mock_tokenized, generic_debt_allocator, ): - # Strategy two will have the higher apr - strategy_one = deploy_mock_tokenized("One", int(1e16)) - strategy_two = deploy_mock_tokenized("two", int(1e17)) - setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.set_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - vault.set_role( - generic_debt_allocator, - ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, - sender=daddy, + strategy_one, strategy_two = full_setup( + deploy_mock_tokenized=deploy_mock_tokenized, + vault=vault, + apr_oracle=apr_oracle, + daddy=daddy, + brain=brain, + yield_manager=yield_manager, + generic_debt_allocator=generic_debt_allocator, + keeper=keeper, + management=management, + user=user, ) - asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) @@ -653,7 +671,7 @@ def test_update_allocation_permissioned( assert bool == True assert bytes == vault.update_debt.encode_input(strategy_two, amount) - generic_debt_allocator.update_debt(strategy_two, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_two, amount, sender=brain) allocation = [(strategy_one, amount)] with ape.reverts("ratio too high"): @@ -682,13 +700,13 @@ def test_update_allocation_permissioned( strategy_two.address, amount - to_move ) - generic_debt_allocator.update_debt(strategy_two, amount - to_move, sender=daddy) + generic_debt_allocator.update_debt(strategy_two, amount - to_move, sender=brain) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, to_move) - generic_debt_allocator.update_debt(strategy_one, to_move, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, to_move, sender=brain) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) @@ -697,9 +715,6 @@ def test_update_allocation_permissioned( # Try and move all allocation = [(strategy_two, 0), (strategy_one, amount)] - # Strategy manager isnt the strategies management - with ape.reverts("!keeper"): - yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) strategy_two.setKeeper(keeper, sender=management) keeper.addNewStrategy(strategy_two, sender=daddy) @@ -717,13 +732,13 @@ def test_update_allocation_permissioned( assert bool_two == True assert bytes_two == vault.update_debt.encode_input(strategy_two.address, 0) - generic_debt_allocator.update_debt(strategy_two, 0, sender=daddy) + generic_debt_allocator.update_debt(strategy_two, 0, sender=brain) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, amount) - generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) @@ -744,34 +759,30 @@ def test_update_allocation__max_withdraw( management, keeper, daddy, + brain, user, amount, asset, deploy_mock_tokenized, generic_debt_allocator, ): - # Strategy two will have the higher apr - strategy_one = deploy_mock_tokenized("One", int(1e16)) - strategy_two = deploy_mock_tokenized("two", int(1e17)) - setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - vault.set_role( - generic_debt_allocator, - ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, - sender=daddy, + strategy_one, strategy_two = full_setup( + deploy_mock_tokenized=deploy_mock_tokenized, + vault=vault, + apr_oracle=apr_oracle, + daddy=daddy, + brain=brain, + yield_manager=yield_manager, + generic_debt_allocator=generic_debt_allocator, + keeper=keeper, + management=management, + user=user, ) - strategy_one.setKeeper(keeper, sender=management) - keeper.addNewStrategy(strategy_one, sender=daddy) - keeper.setKeeper(yield_manager, True, sender=daddy) - yield_manager.setProposer(user, True, sender=daddy) asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -807,34 +818,30 @@ def test_update_allocation__max_deposit( management, keeper, daddy, + brain, user, amount, asset, deploy_mock_tokenized, generic_debt_allocator, ): - # Strategy two will have the higher apr - strategy_one = deploy_mock_tokenized("One", int(1e16)) - strategy_two = deploy_mock_tokenized("two", int(1e17)) - setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - vault.set_role( - generic_debt_allocator, - ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, - sender=daddy, + strategy_one, strategy_two = full_setup( + deploy_mock_tokenized=deploy_mock_tokenized, + vault=vault, + apr_oracle=apr_oracle, + daddy=daddy, + brain=brain, + yield_manager=yield_manager, + generic_debt_allocator=generic_debt_allocator, + keeper=keeper, + management=management, + user=user, ) - strategy_one.setKeeper(keeper, sender=management) - keeper.addNewStrategy(strategy_one, sender=daddy) - keeper.setKeeper(yield_manager, True, sender=daddy) - yield_manager.setProposer(user, True, sender=daddy) asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=daddy) + generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -870,29 +877,25 @@ def test_update_allocation__min_idle( management, keeper, daddy, + brain, user, amount, asset, deploy_mock_tokenized, generic_debt_allocator, ): - # Strategy two will have the higher apr - strategy_one = deploy_mock_tokenized("One", int(1e16)) - strategy_two = deploy_mock_tokenized("two", int(1e17)) - setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setKeeper(yield_manager, True, sender=daddy) - generic_debt_allocator.setMinimumChange(1, sender=daddy) - vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) - vault.set_role( - generic_debt_allocator, - ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, - sender=daddy, + strategy_one, strategy_two = full_setup( + deploy_mock_tokenized=deploy_mock_tokenized, + vault=vault, + apr_oracle=apr_oracle, + daddy=daddy, + brain=brain, + yield_manager=yield_manager, + generic_debt_allocator=generic_debt_allocator, + keeper=keeper, + management=management, + user=user, ) - strategy_one.setKeeper(keeper, sender=management) - keeper.addNewStrategy(strategy_one, sender=daddy) - keeper.setKeeper(yield_manager, True, sender=daddy) - yield_manager.setProposer(user, True, sender=daddy) asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) diff --git a/tests/manager/test_role_manager.py b/tests/manager/test_role_manager.py index dbdecb8..5a6d74f 100644 --- a/tests/manager/test_role_manager.py +++ b/tests/manager/test_role_manager.py @@ -370,7 +370,6 @@ def test_deploy_new_vault( # Check debt allocator assert debt_allocator.vault() == vault - assert debt_allocator.governance() == brain def test_deploy_new_vault__duplicate_reverts( @@ -546,7 +545,6 @@ def test_deploy_new_vault__default_values( # Check debt allocator assert debt_allocator.vault() == vault - assert debt_allocator.governance() == brain def setup_role_manager( @@ -667,7 +665,6 @@ def test_add_new_vault__endorsed( # Check debt allocator assert debt_allocator.vault() == vault - assert debt_allocator.governance() == brain def test_add_new_vault__not_endorsed( @@ -779,7 +776,6 @@ def test_add_new_vault__not_endorsed( # Check debt allocator assert debt_allocator.vault() == vault - assert debt_allocator.governance() == brain def test_add_new_vault__with_debt_allocator( @@ -824,7 +820,7 @@ def test_add_new_vault__with_debt_allocator( assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=daddy) + tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=brain) event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault debt_allocator = project.GenericDebtAllocator.at(event.allocator) @@ -892,7 +888,6 @@ def test_add_new_vault__with_debt_allocator( # Check debt allocator assert debt_allocator.vault() == vault - assert debt_allocator.governance() == daddy def test_add_new_vault__with_accountant( @@ -936,7 +931,7 @@ def test_add_new_vault__with_accountant( assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=daddy) + tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=brain) event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault debt_allocator = project.GenericDebtAllocator.at(event.allocator) @@ -1008,7 +1003,6 @@ def test_add_new_vault__with_accountant( # Check debt allocator assert debt_allocator.vault() == vault - assert debt_allocator.governance() == daddy def test_add_new_vault__duplicate_reverts( @@ -1156,7 +1150,6 @@ def test_new_debt_allocator__deploys_one( # Check debt allocator assert debt_allocator.vault() == vault - assert debt_allocator.governance() == brain # Update to a new debt allocator with ape.reverts("!allowed"): @@ -1172,7 +1165,6 @@ def test_new_debt_allocator__deploys_one( assert new_debt_allocator != debt_allocator assert new_debt_allocator.vault() == vault - assert new_debt_allocator.governance() == brain assert new_debt_allocator.maxAcceptableBaseFee() == MAX_INT (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( @@ -1273,9 +1265,7 @@ def test_new_debt_allocator__already_deployed( # Check debt allocator assert debt_allocator.vault() == vault - assert debt_allocator.governance() == brain - - tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=daddy) + tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=brain) event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] new_debt_allocator = project.GenericDebtAllocator.at(event.allocator) @@ -1290,7 +1280,6 @@ def test_new_debt_allocator__already_deployed( assert new_debt_allocator != debt_allocator assert new_debt_allocator.vault() == vault - assert new_debt_allocator.governance() == daddy assert new_debt_allocator.maxAcceptableBaseFee() == MAX_INT (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( @@ -1394,7 +1383,6 @@ def test_remove_vault( # Check debt allocator assert debt_allocator.vault() == vault - assert debt_allocator.governance() == brain # Remove the vault with ape.reverts("!allowed"): From 3c3eb3cbc3d154e7a08e75cb0ef094c768c82dae Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 8 Jan 2024 16:34:10 -0700 Subject: [PATCH 33/35] chore: renaming --- contracts/Managers/RoleManager.sol | 32 +- ...ricDebtAllocator.sol => DebtAllocator.sol} | 46 +- .../debtAllocators/DebtAllocatorFactory.sol | 120 +++++ .../GenericDebtAllocatorFactory.sol | 150 ------ .../YieldManager/YieldManager.sol | 18 +- tests/conftest.py | 33 +- tests/debtAllocators/test_debt_allocator.py | 466 +++++++++++++++++ .../test_generic_debt_allocator.py | 494 ------------------ .../yield/test_yield_manager.py | 216 ++++---- tests/manager/test_role_manager.py | 108 ++-- 10 files changed, 803 insertions(+), 880 deletions(-) rename contracts/debtAllocators/{GenericDebtAllocator.sol => DebtAllocator.sol} (91%) create mode 100644 contracts/debtAllocators/DebtAllocatorFactory.sol delete mode 100644 contracts/debtAllocators/GenericDebtAllocatorFactory.sol create mode 100644 tests/debtAllocators/test_debt_allocator.py delete mode 100644 tests/debtAllocators/test_generic_debt_allocator.py diff --git a/contracts/Managers/RoleManager.sol b/contracts/Managers/RoleManager.sol index ca55991..4ec4529 100644 --- a/contracts/Managers/RoleManager.sol +++ b/contracts/Managers/RoleManager.sol @@ -7,7 +7,7 @@ import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Governance2Step} from "@periphery/utils/Governance2Step.sol"; import {HealthCheckAccountant} from "../accountants/HealthCheckAccountant.sol"; -import {GenericDebtAllocatorFactory} from "../debtAllocators/GenericDebtAllocatorFactory.sol"; +import {DebtAllocatorFactory} from "../debtAllocators/DebtAllocatorFactory.sol"; /// @title Yearn V3 Vault Role Manager. contract RoleManager is Governance2Step { @@ -21,8 +21,11 @@ contract RoleManager is Governance2Step { uint256 rating ); - /// @notice Emitted when a vault is removed. - event RemovedVault(address indexed vault); + /// @notice Emitted when a vaults debt allocator is updated. + event UpdateDebtAllocator( + address indexed vault, + address indexed debtAllocator + ); /// @notice Emitted when a new address is set for a position. event UpdatePositionHolder( @@ -30,12 +33,21 @@ contract RoleManager is Governance2Step { address indexed newAddress ); + /// @notice Emitted when a vault is removed. + event RemovedVault(address indexed vault); + /// @notice Emitted when a new set of roles is set for a position event UpdatePositionRoles(bytes32 indexed position, uint256 newRoles); /// @notice Emitted when the defaultProfitMaxUnlock variable is updated. event UpdateDefaultProfitMaxUnlock(uint256 newDefaultProfitMaxUnlock); + /// @notice Position struct + struct Position { + address holder; + uint96 roles; + } + /// @notice Config that holds all vault info. struct VaultConfig { address asset; @@ -44,12 +56,6 @@ contract RoleManager is Governance2Step { uint256 index; } - /// @notice Position struct - struct Position { - address holder; - uint96 roles; - } - /// @notice Only allow either governance or the position holder to call. modifier onlyPositionHolder(bytes32 _positionId) { _isPositionHolder(_positionId); @@ -317,8 +323,9 @@ contract RoleManager is Governance2Step { // If we have a factory set. if (factory != address(0)) { // Deploy a new debt allocator for the vault with Brain as the gov. - _debtAllocator = GenericDebtAllocatorFactory(factory) - .newGenericDebtAllocator(_vault); + _debtAllocator = DebtAllocatorFactory(factory).newDebtAllocator( + _vault + ); } else { // If no factory is set we should be using one central allocator. _debtAllocator = getPositionHolder(DEBT_ALLOCATOR); @@ -537,6 +544,9 @@ contract RoleManager is Governance2Step { // Update the vaults config. vaultConfig[_vault].debtAllocator = _debtAllocator; + + // Emit event. + emit UpdateDebtAllocator(_vault, _debtAllocator); } /** diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/DebtAllocator.sol similarity index 91% rename from contracts/debtAllocators/GenericDebtAllocator.sol rename to contracts/debtAllocators/DebtAllocator.sol index 55d377d..aad03d5 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/DebtAllocator.sol @@ -4,14 +4,13 @@ pragma solidity 0.8.18; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; - -import {GenericDebtAllocatorFactory} from "./GenericDebtAllocatorFactory.sol"; +import {DebtAllocatorFactory} from "./DebtAllocatorFactory.sol"; /** - * @title YearnV3 Generic Debt Allocator + * @title YearnV3 Debt Allocator * @author yearn.finance * @notice - * This Generic Debt Allocator is meant to be used alongside + * This Debt Allocator is meant to be used alongside * a Yearn V3 vault to provide the needed triggers for a keeper * to perform automated debt updates for the vaults strategies. * @@ -29,7 +28,7 @@ import {GenericDebtAllocatorFactory} from "./GenericDebtAllocatorFactory.sol"; * And will pull funds from the strategy when it has `minimumChange` * more than its `maxRatio`. */ -contract GenericDebtAllocator { +contract DebtAllocator { /// @notice An event emitted when a strategies debt ratios are Updated. event UpdateStrategyDebtRatio( address indexed strategy, @@ -50,9 +49,6 @@ contract GenericDebtAllocator { /// @notice An event emitted when the max debt update loss is updated. event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); - /// @notice An event emitted when the max base fee is updated. - event UpdateMaxAcceptableBaseFee(uint256 newMaxAcceptableBaseFee); - /// @notice Struct for each strategies info. struct Config { // The ideal percent in Basis Points the strategy should have. @@ -90,7 +86,7 @@ contract GenericDebtAllocator { /// @notice Check the Factories governance address. function _isGovernance() internal view virtual { require( - msg.sender == GenericDebtAllocatorFactory(factory).governance(), + msg.sender == DebtAllocatorFactory(factory).governance(), "!governance" ); } @@ -99,17 +95,14 @@ contract GenericDebtAllocator { function _isManager() internal view virtual { require( managers[msg.sender] || - msg.sender == GenericDebtAllocatorFactory(factory).governance(), + msg.sender == DebtAllocatorFactory(factory).governance(), "!manager" ); } /// @notice Check is one of the allowed keepers. function _isKeeper() internal view virtual { - require( - GenericDebtAllocatorFactory(factory).keepers(msg.sender), - "!keeper" - ); + require(DebtAllocatorFactory(factory).keepers(msg.sender), "!keeper"); } uint256 internal constant MAX_BPS = 10_000; @@ -134,10 +127,6 @@ contract GenericDebtAllocator { /// @notice Max loss to accept on debt updates in basis points. uint256 public maxDebtUpdateLoss; - /// @notice Max the chains base fee can be during debt update. - // Will default to max uint256 and need to be set to be used. - uint256 public maxAcceptableBaseFee; - /// @notice Mapping of addresses that are allowed to update debt. mapping(address => bool) public managers; @@ -164,8 +153,6 @@ contract GenericDebtAllocator { // Default max loss on debt updates to 1 BP. maxDebtUpdateLoss = 1; - // Default max base fee to uint256 max - maxAcceptableBaseFee = type(uint256).max; } /** @@ -227,7 +214,7 @@ contract GenericDebtAllocator { address _strategy ) public view virtual returns (bool, bytes memory) { // Check the base fee isn't too high. - if (block.basefee > maxAcceptableBaseFee) { + if (!DebtAllocatorFactory(factory).isCurrentBaseFeeAcceptable()) { return (false, bytes("Base Fee")); } @@ -466,23 +453,6 @@ contract GenericDebtAllocator { emit UpdateMinimumWait(_minimumWait); } - /** - * @notice Set the max acceptable base fee. - * @dev This defaults to max uint256 and will need to - * be set for it to be used. - * - * Is denominated in gwei. So 50gwei would be set as 50e9. - * - * @param _maxAcceptableBaseFee The new max base fee. - */ - function setMaxAcceptableBaseFee( - uint256 _maxAcceptableBaseFee - ) external virtual onlyGovernance { - maxAcceptableBaseFee = _maxAcceptableBaseFee; - - emit UpdateMaxAcceptableBaseFee(_maxAcceptableBaseFee); - } - /** * @notice Set if a manager can update ratios. * @param _address The address to set mapping for. diff --git a/contracts/debtAllocators/DebtAllocatorFactory.sol b/contracts/debtAllocators/DebtAllocatorFactory.sol new file mode 100644 index 0000000..197ed66 --- /dev/null +++ b/contracts/debtAllocators/DebtAllocatorFactory.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity 0.8.18; + +import {DebtAllocator} from "./DebtAllocator.sol"; +import {Clonable} from "@periphery/utils/Clonable.sol"; +import {RoleManager} from "../Managers/RoleManager.sol"; +import {Governance} from "@periphery/utils/Governance.sol"; + +/** + * @title YearnV3 Debt Allocator Factory + * @author yearn.finance + * @notice + * Factory to deploy a debt allocator for a YearnV3 vault. + */ +contract DebtAllocatorFactory is Governance, Clonable { + /// @notice Revert message for when a debt allocator already exists. + error AlreadyDeployed(address _allocator); + + /// @notice An event emitted when a keeper is added or removed. + event UpdateKeeper(address indexed keeper, bool allowed); + + /// @notice An event emitted when the max base fee is updated. + event UpdateMaxAcceptableBaseFee(uint256 newMaxAcceptableBaseFee); + + /// @notice An event emitted when a new debt allocator is added or deployed. + event NewDebtAllocator(address indexed allocator, address indexed vault); + + /// @notice Max the chains base fee can be during debt update. + // Will default to max uint256 and need to be set to be used. + uint256 public maxAcceptableBaseFee; + + /// @notice Mapping of addresses that are allowed to update debt. + mapping(address => bool) public keepers; + + constructor(address _governance) Governance(_governance) { + // Deploy a dummy allocator as the original. + original = address(new DebtAllocator(address(1), 0)); + + // Default max base fee to uint max. + maxAcceptableBaseFee = type(uint256).max; + + // Default to allow governance to be a keeper. + keepers[_governance] = true; + emit UpdateKeeper(_governance, true); + } + + /** + * @notice Clones a new debt allocator. + * @dev defaults to msg.sender as the governance role and 0 + * for the `minimumChange`. + * + * @param _vault The vault for the allocator to be hooked to. + * @return Address of the new debt allocator + */ + function newDebtAllocator( + address _vault + ) external virtual returns (address) { + return newDebtAllocator(_vault, 0); + } + + /** + * @notice Clones a new debt allocator. + * @param _vault The vault for the allocator to be hooked to. + * @param _minimumChange The minimum amount needed to trigger debt update. + * @return newAllocator Address of the new debt allocator + */ + function newDebtAllocator( + address _vault, + uint256 _minimumChange + ) public virtual returns (address newAllocator) { + // Clone new allocator off the original. + newAllocator = _clone(); + + // Initialize the new allocator. + DebtAllocator(newAllocator).initialize(_vault, _minimumChange); + + // Emit event. + emit NewDebtAllocator(newAllocator, _vault); + } + + /** + * @notice Set the max acceptable base fee. + * @dev This defaults to max uint256 and will need to + * be set for it to be used. + * + * Is denominated in gwei. So 50gwei would be set as 50e9. + * + * @param _maxAcceptableBaseFee The new max base fee. + */ + function setMaxAcceptableBaseFee( + uint256 _maxAcceptableBaseFee + ) external virtual onlyGovernance { + maxAcceptableBaseFee = _maxAcceptableBaseFee; + + emit UpdateMaxAcceptableBaseFee(_maxAcceptableBaseFee); + } + + /** + * @notice Set if a keeper can update debt. + * @param _address The address to set mapping for. + * @param _allowed If the address can call {update_debt}. + */ + function setKeeper( + address _address, + bool _allowed + ) external virtual onlyGovernance { + keepers[_address] = _allowed; + + emit UpdateKeeper(_address, _allowed); + } + + /** + * @notice Returns wether or not the current base fee is acceptable + * based on the `maxAcceptableBaseFee`. + * @return . If the current base fee is acceptable. + */ + function isCurrentBaseFeeAcceptable() external view virtual returns (bool) { + return maxAcceptableBaseFee >= block.basefee; + } +} diff --git a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol b/contracts/debtAllocators/GenericDebtAllocatorFactory.sol deleted file mode 100644 index 3bc4c6d..0000000 --- a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity 0.8.18; - -import {Clonable} from "@periphery/utils/Clonable.sol"; -import {RoleManager} from "../Managers/RoleManager.sol"; -import {Governance} from "@periphery/utils/Governance.sol"; -import {GenericDebtAllocator} from "./GenericDebtAllocator.sol"; - -/** - * @title YearnV3 Generic Debt Allocator Factory - * @author yearn.finance - * @notice - * Factory to deploy a debt allocator for a YearnV3 vault. - */ -contract GenericDebtAllocatorFactory is Governance, Clonable { - /// @notice Revert message for when a debt allocator already exists. - error AlreadyDeployed(address _allocator); - - /// @notice An even emitted when a new `roleManager` is set. - event UpdateRoleManager(address indexed roleManager); - - /// @notice An event emitted when a keeper is added or removed. - event UpdateKeeper(address indexed keeper, bool allowed); - - /// @notice An event emitted when a new debt allocator is added or deployed. - event NewDebtAllocator(address indexed allocator, address indexed vault); - - /// @notice Only allow `governance` or the `roleManager`. - modifier onlyAuthorized() { - _isAuthorized(); - _; - } - - /// @notice Check is `governance` or the `roleManager`. - function _isAuthorized() internal view { - require( - msg.sender == roleManager || msg.sender == governance, - "!authorized" - ); - } - - /// @notice Address that is the roleManager for all vaults and can deploy allocators. - address public roleManager; - - /// @notice Mapping of addresses that are allowed to update debt. - mapping(address => bool) public keepers; - - constructor( - address _governance, - address _roleManager - ) Governance(_governance) { - // Deploy a dummy allocator as the original. - original = address(new GenericDebtAllocator(address(1), 0)); - - // Set the initial role manager. - roleManager = _roleManager; - emit UpdateRoleManager(_roleManager); - - // Default to allow governance to be a keeper. - keepers[_governance] = true; - emit UpdateKeeper(_governance, true); - } - - /** - * @notice Clones a new debt allocator. - * @dev defaults to msg.sender as the governance role and 0 - * for the `minimumChange`. - * - * @param _vault The vault for the allocator to be hooked to. - * @return Address of the new generic debt allocator - */ - function newGenericDebtAllocator( - address _vault - ) external virtual returns (address) { - return newGenericDebtAllocator(_vault, 0); - } - - /** - * @notice Clones a new debt allocator. - * @param _vault The vault for the allocator to be hooked to. - * @param _minimumChange The minimum amount needed to trigger debt update. - * @return newAllocator Address of the new generic debt allocator - */ - function newGenericDebtAllocator( - address _vault, - uint256 _minimumChange - ) public virtual returns (address newAllocator) { - // Clone new allocator off the original. - newAllocator = _clone(); - - // Initialize the new allocator. - GenericDebtAllocator(newAllocator).initialize(_vault, _minimumChange); - - // Emit event. - emit NewDebtAllocator(newAllocator, _vault); - } - - /** - * @notice Check if a strategy's debt should be updated. - * @dev This should be called by a keeper to decide if a strategies - * debt should be updated and if so by how much. - * - * @param _vault Address of the vault. - * @param _strategy Address of the strategy to check. - * @return . Bool representing if the debt should be updated. - * @return . Calldata if `true` or reason if `false`. - */ - function shouldUpdateDebt( - address _vault, - address _strategy - ) public view virtual returns (bool, bytes memory) { - return - GenericDebtAllocator(allocator(_vault)).shouldUpdateDebt(_strategy); - } - - /** - * @notice Helper function to easily get a vaults debt allocator from the role manager. - * @param _vault Address of the vault to get the allocator for. - * @return Address of the vaults debt allocator if one exists. - */ - function allocator(address _vault) public view virtual returns (address) { - return RoleManager(roleManager).getDebtAllocator(_vault); - } - - /** - * @notice Update the Role Manager address. - * @param _roleManager New role manager address. - */ - function setRoleManager( - address _roleManager - ) external virtual onlyGovernance { - roleManager = _roleManager; - - emit UpdateRoleManager(_roleManager); - } - - /** - * @notice Set if a keeper can update debt. - * @param _address The address to set mapping for. - * @param _allowed If the address can call {update_debt}. - */ - function setKeeper( - address _address, - bool _allowed - ) external virtual onlyGovernance { - keepers[_address] = _allowed; - - emit UpdateKeeper(_address, _allowed); - } -} diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index 0290b19..d26f61a 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -5,7 +5,7 @@ import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; import {AprOracle} from "@periphery/AprOracle/AprOracle.sol"; import {Keeper, Governance} from "./Keeper.sol"; -import {GenericDebtAllocator} from "../GenericDebtAllocator.sol"; +import {DebtAllocator} from "../DebtAllocator.sol"; /** * @title YearnV3 Yield Yield Based Debt Allocator @@ -176,12 +176,11 @@ contract YieldManager is Governance { // If different than the current target. if ( - GenericDebtAllocator(allocator).getStrategyTargetRatio( - _strategy - ) != _targetRatio + DebtAllocator(allocator).getStrategyTargetRatio(_strategy) != + _targetRatio ) { // Update allocation. - GenericDebtAllocator(allocator).setStrategyDebtRatio( + DebtAllocator(allocator).setStrategyDebtRatio( _strategy, _targetRatio ); @@ -374,12 +373,11 @@ contract YieldManager is Governance { : MAX_BPS; if ( - GenericDebtAllocator(allocator).getStrategyTargetRatio( - _strategy - ) != _targetRatio + DebtAllocator(allocator).getStrategyTargetRatio(_strategy) != + _targetRatio ) { // Update allocation. - GenericDebtAllocator(allocator).setStrategyDebtRatio( + DebtAllocator(allocator).setStrategyDebtRatio( _strategy, _targetRatio ); @@ -415,7 +413,7 @@ contract YieldManager is Governance { address _vault, address _allocator ) internal view virtual { - uint256 totalRatio = GenericDebtAllocator(_allocator).totalDebtRatio(); + uint256 totalRatio = DebtAllocator(_allocator).totalDebtRatio(); uint256 minIdle = IVault(_vault).minimum_total_idle(); // No need if minIdle is 0. diff --git a/tests/conftest.py b/tests/conftest.py index dd1eec5..2c225e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -395,33 +395,31 @@ def address_provider(deploy_address_provider): @pytest.fixture(scope="session") -def deploy_generic_debt_allocator_factory(project, daddy, brain): - def deploy_generic_debt_allocator_factory(gov=daddy): - generic_debt_allocator_factory = gov.deploy( - project.GenericDebtAllocatorFactory, brain, ZERO_ADDRESS - ) +def deploy_debt_allocator_factory(project, daddy, brain): + def deploy_debt_allocator_factory(gov=daddy): + debt_allocator_factory = gov.deploy(project.DebtAllocatorFactory, brain) - return generic_debt_allocator_factory + return debt_allocator_factory - yield deploy_generic_debt_allocator_factory + yield deploy_debt_allocator_factory @pytest.fixture(scope="session") -def generic_debt_allocator_factory(deploy_generic_debt_allocator_factory): - generic_debt_allocator_factory = deploy_generic_debt_allocator_factory() +def debt_allocator_factory(deploy_debt_allocator_factory): + debt_allocator_factory = deploy_debt_allocator_factory() - yield generic_debt_allocator_factory + yield debt_allocator_factory @pytest.fixture(scope="session") -def generic_debt_allocator(generic_debt_allocator_factory, project, vault, daddy): - tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=daddy) +def debt_allocator(debt_allocator_factory, project, vault, daddy): + tx = debt_allocator_factory.newDebtAllocator(vault, sender=daddy) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] - generic_debt_allocator = project.GenericDebtAllocator.at(event.allocator) + debt_allocator = project.DebtAllocator.at(event.allocator) - yield generic_debt_allocator + yield debt_allocator @pytest.fixture(scope="session") @@ -448,7 +446,7 @@ def role_manager( daddy, brain, healthcheck_accountant, - generic_debt_allocator_factory, + debt_allocator_factory, registry, ): role_manager = deploy_role_manager() @@ -458,9 +456,8 @@ def role_manager( ) role_manager.setPositionHolder(role_manager.REGISTRY(), registry, sender=daddy) role_manager.setPositionHolder( - role_manager.ALLOCATOR_FACTORY(), generic_debt_allocator_factory, sender=daddy + role_manager.ALLOCATOR_FACTORY(), debt_allocator_factory, sender=daddy ) - generic_debt_allocator_factory.setRoleManager(role_manager, sender=brain) return role_manager diff --git a/tests/debtAllocators/test_debt_allocator.py b/tests/debtAllocators/test_debt_allocator.py new file mode 100644 index 0000000..6c1ae16 --- /dev/null +++ b/tests/debtAllocators/test_debt_allocator.py @@ -0,0 +1,466 @@ +import ape +from ape import chain, project +from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES + + +def test_setup(debt_allocator_factory, brain, user, strategy, vault): + assert debt_allocator_factory.governance() == brain + assert debt_allocator_factory.keepers(brain) == True + assert debt_allocator_factory.maxAcceptableBaseFee() == MAX_INT + + tx = debt_allocator_factory.newDebtAllocator(vault, 0, sender=user) + + events = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator)) + + assert len(events) == 1 + assert events[0].vault == vault.address + + debt_allocator = project.DebtAllocator.at(events[0].allocator) + + assert debt_allocator.managers(brain) == False + assert debt_allocator.factory() == debt_allocator_factory.address + assert debt_allocator.vault() == vault.address + assert debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + assert debt_allocator.totalDebtRatio() == 0 + with ape.reverts("!active"): + debt_allocator.shouldUpdateDebt(strategy) + + +def test_set_keepers(debt_allocator_factory, debt_allocator, brain, user): + assert debt_allocator_factory.keepers(brain) == True + assert debt_allocator_factory.keepers(user) == False + + with ape.reverts("!governance"): + debt_allocator_factory.setKeeper(user, True, sender=user) + + tx = debt_allocator_factory.setKeeper(user, True, sender=brain) + + event = list(tx.decode_logs(debt_allocator_factory.UpdateKeeper))[0] + + assert event.keeper == user + assert event.allowed == True + assert debt_allocator_factory.keepers(user) == True + + tx = debt_allocator_factory.setKeeper(brain, False, sender=brain) + + event = list(tx.decode_logs(debt_allocator_factory.UpdateKeeper))[0] + + assert event.keeper == brain + assert event.allowed == False + assert debt_allocator_factory.keepers(brain) == False + + +def test_set_managers(debt_allocator, brain, user): + assert debt_allocator.managers(brain) == False + assert debt_allocator.managers(user) == False + + with ape.reverts("!governance"): + debt_allocator.setManager(user, True, sender=user) + + tx = debt_allocator.setManager(user, True, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateManager))[0] + + assert event.manager == user + assert event.allowed == True + assert debt_allocator.managers(user) == True + + tx = debt_allocator.setManager(user, False, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateManager))[0] + + assert event.manager == user + assert event.allowed == False + assert debt_allocator.managers(user) == False + + +def test_set_minimum_change(debt_allocator, brain, strategy, user): + assert debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + assert debt_allocator.minimumChange() == 0 + + minimum = int(1e17) + + with ape.reverts("!governance"): + debt_allocator.setMinimumChange(minimum, sender=user) + + with ape.reverts("zero"): + debt_allocator.setMinimumChange(0, sender=brain) + + tx = debt_allocator.setMinimumChange(minimum, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateMinimumChange))[0] + + assert event.newMinimumChange == minimum + assert debt_allocator.minimumChange() == minimum + + +def test_set_minimum_wait(debt_allocator, brain, strategy, user): + assert debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + assert debt_allocator.minimumWait() == 0 + + minimum = int(1e17) + + with ape.reverts("!governance"): + debt_allocator.setMinimumWait(minimum, sender=user) + + tx = debt_allocator.setMinimumWait(minimum, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateMinimumWait))[0] + + assert event.newMinimumWait == minimum + assert debt_allocator.minimumWait() == minimum + + +def test_set_max_debt_update_loss(debt_allocator, brain, strategy, user): + assert debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + assert debt_allocator.maxDebtUpdateLoss() == 1 + + max = int(69) + + with ape.reverts("!governance"): + debt_allocator.setMaxDebtUpdateLoss(max, sender=user) + + with ape.reverts("higher than max"): + debt_allocator.setMaxDebtUpdateLoss(10_001, sender=brain) + + tx = debt_allocator.setMaxDebtUpdateLoss(max, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateMaxDebtUpdateLoss))[0] + + assert event.newMaxDebtUpdateLoss == max + assert debt_allocator.maxDebtUpdateLoss() == max + + +def test_set_ratios( + debt_allocator, brain, daddy, vault, strategy, create_strategy, user +): + assert debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + + minimum = int(1e17) + max = int(6_000) + target = int(5_000) + + with ape.reverts("!manager"): + debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=user) + + vault.add_strategy(strategy.address, sender=daddy) + + with ape.reverts("!minimum"): + debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) + + debt_allocator.setMinimumChange(minimum, sender=brain) + + with ape.reverts("max too high"): + debt_allocator.setStrategyDebtRatio(strategy, target, int(10_001), sender=brain) + + with ape.reverts("max ratio"): + debt_allocator.setStrategyDebtRatio(strategy, int(max + 1), max, sender=brain) + + tx = debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert debt_allocator.totalDebtRatio() == target + assert debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + new_strategy = create_strategy() + vault.add_strategy(new_strategy, sender=daddy) + with ape.reverts("ratio too high"): + debt_allocator.setStrategyDebtRatio( + new_strategy, int(10_000), int(10_000), sender=brain + ) + + target = int(8_000) + tx = debt_allocator.setStrategyDebtRatio(strategy, target, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == target * 1.2 + assert event.newTotalDebtRatio == target + assert debt_allocator.totalDebtRatio() == target + assert debt_allocator.getConfig(strategy) == (target, target * 1.2, 0, 0) + + +def test_increase_debt_ratio( + debt_allocator, brain, daddy, vault, strategy, create_strategy, user +): + assert debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + + minimum = int(1e17) + target = int(5_000) + increase = int(5_000) + max = target * 1.2 + + with ape.reverts("!manager"): + debt_allocator.increaseStrategyDebtRatio(strategy, increase, sender=user) + + vault.add_strategy(strategy.address, sender=daddy) + + with ape.reverts("!minimum"): + debt_allocator.increaseStrategyDebtRatio(strategy, increase, sender=brain) + + debt_allocator.setMinimumChange(minimum, sender=brain) + + tx = debt_allocator.increaseStrategyDebtRatio(strategy, increase, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert debt_allocator.totalDebtRatio() == target + assert debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + new_strategy = create_strategy() + vault.add_strategy(new_strategy, sender=daddy) + + with ape.reverts("ratio too high"): + debt_allocator.increaseStrategyDebtRatio(new_strategy, int(5_001), sender=brain) + + target = int(8_000) + max = target * 1.2 + increase = int(3_000) + tx = debt_allocator.increaseStrategyDebtRatio(strategy, increase, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert debt_allocator.totalDebtRatio() == target + assert debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + target = int(10_000) + max = int(10_000) + increase = int(2_000) + tx = debt_allocator.increaseStrategyDebtRatio(strategy, increase, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert debt_allocator.totalDebtRatio() == target + assert debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + +def test_decrease_debt_ratio( + debt_allocator, brain, vault, strategy, daddy, create_strategy, user +): + assert debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + + minimum = int(1e17) + target = int(5_000) + max = target * 1.2 + + vault.add_strategy(strategy.address, sender=daddy) + debt_allocator.setMinimumChange(minimum, sender=brain) + + # Underflow + with ape.reverts(): + debt_allocator.decreaseStrategyDebtRatio(strategy, target, sender=brain) + + # Add the target + tx = debt_allocator.increaseStrategyDebtRatio(strategy, target, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert debt_allocator.totalDebtRatio() == target + assert debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + target = int(2_000) + max = target * 1.2 + decrease = int(3_000) + + with ape.reverts("!manager"): + debt_allocator.decreaseStrategyDebtRatio(strategy, decrease, sender=user) + + tx = debt_allocator.decreaseStrategyDebtRatio(strategy, decrease, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert debt_allocator.totalDebtRatio() == target + assert debt_allocator.getConfig(strategy) == (target, max, 0, 0) + + target = int(0) + max = int(0) + decrease = int(2_000) + tx = debt_allocator.decreaseStrategyDebtRatio(strategy, decrease, sender=brain) + + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert debt_allocator.totalDebtRatio() == target + assert debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + + +def test_should_update_debt( + debt_allocator, vault, strategy, brain, daddy, deposit_into_vault, amount +): + assert debt_allocator.getConfig(strategy.address) == (0, 0, 0, 0) + vault.add_role(debt_allocator, ROLES.DEBT_MANAGER, sender=daddy) + + with ape.reverts("!active"): + debt_allocator.shouldUpdateDebt(strategy.address) + + vault.add_strategy(strategy.address, sender=daddy) + + minimum = int(1) + target = int(5_000) + max = int(5_000) + + debt_allocator.setMinimumChange(minimum, sender=brain) + debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) + + # Vault has no assets so should be false. + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("Below Min").encode("utf-8") + + deposit_into_vault(vault, amount) + + # No max debt has been set so should be false. + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("Below Min").encode("utf-8") + + vault.update_max_debt_for_strategy(strategy, MAX_INT, sender=daddy) + + # Should now want to allocate 50% + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == True + print("got", bytes) + print("Made ", vault.update_debt.encode_input(strategy.address, amount // 2)) + assert bytes == vault.update_debt.encode_input(strategy.address, amount // 2) + + debt_allocator.update_debt(strategy, amount // 2, sender=brain) + chain.mine(1) + + # Should now be false again once allocated + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("Below Min").encode("utf-8") + + # Update the ratio to make true + debt_allocator.setStrategyDebtRatio( + strategy, int(target + 1), int(target + 1), sender=brain + ) + + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == True + assert bytes == vault.update_debt.encode_input( + strategy.address, int(amount * 5_001 // 10_000) + ) + + # Set a minimumWait time + debt_allocator.setMinimumWait(MAX_INT, sender=brain) + # Should now be false + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("min wait").encode("utf-8") + + debt_allocator.setMinimumWait(0, sender=brain) + + # Lower the max debt so its == to current debt + vault.update_max_debt_for_strategy(strategy, int(amount // 2), sender=daddy) + + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("Below Min").encode("utf-8") + + # Reset it. + vault.update_max_debt_for_strategy(strategy, MAX_INT, sender=daddy) + + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == True + assert bytes == vault.update_debt.encode_input( + strategy.address, int(amount * 5_001 // 10_000) + ) + + # Increase the minimum_total_idle + vault.set_minimum_total_idle(vault.totalIdle(), sender=daddy) + + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("No Idle").encode("utf-8") + + vault.set_minimum_total_idle(0, sender=daddy) + + # increase the minimum so its false again + debt_allocator.setMinimumChange(int(1e30), sender=brain) + + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("Below Min").encode("utf-8") + + # Lower the target and minimum + debt_allocator.setMinimumChange(int(1), sender=brain) + debt_allocator.setStrategyDebtRatio( + strategy, int(target // 2), int(target // 2), sender=brain + ) + + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy.address) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy.address, amount // 4) + + +def test_update_debt( + debt_allocator_factory, + debt_allocator, + vault, + strategy, + brain, + daddy, + user, + deposit_into_vault, + amount, +): + assert debt_allocator.getConfig(strategy) == (0, 0, 0, 0) + deposit_into_vault(vault, amount) + + assert vault.totalIdle() == amount + assert vault.totalDebt() == 0 + + vault.add_strategy(strategy, sender=daddy) + vault.update_max_debt_for_strategy(strategy, MAX_INT, sender=daddy) + + # This reverts by the allocator + with ape.reverts("!keeper"): + debt_allocator.update_debt(strategy, amount, sender=user) + + # This reverts by the vault + with ape.reverts("not allowed"): + debt_allocator.update_debt(strategy, amount, sender=brain) + + vault.add_role( + debt_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) + + debt_allocator.update_debt(strategy, amount, sender=brain) + + timestamp = debt_allocator.getConfig(strategy)[2] + assert timestamp != 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == amount + + debt_allocator_factory.setKeeper(user, True, sender=brain) + + debt_allocator.update_debt(strategy, 0, sender=user) + + assert debt_allocator.getConfig(strategy)[2] != timestamp + assert vault.totalIdle() == amount + assert vault.totalDebt() == 0 diff --git a/tests/debtAllocators/test_generic_debt_allocator.py b/tests/debtAllocators/test_generic_debt_allocator.py deleted file mode 100644 index 64d8671..0000000 --- a/tests/debtAllocators/test_generic_debt_allocator.py +++ /dev/null @@ -1,494 +0,0 @@ -import ape -from ape import chain, project -from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES - - -def test_setup(generic_debt_allocator_factory, brain, user, strategy, vault): - assert generic_debt_allocator_factory.governance() == brain - assert generic_debt_allocator_factory.keepers(brain) == True - assert generic_debt_allocator_factory.roleManager() == ZERO_ADDRESS - - tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, 0, sender=user) - - events = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator)) - - assert len(events) == 1 - assert events[0].vault == vault.address - - generic_debt_allocator = project.GenericDebtAllocator.at(events[0].allocator) - - assert generic_debt_allocator.managers(brain) == False - assert generic_debt_allocator.factory() == generic_debt_allocator_factory.address - assert generic_debt_allocator.vault() == vault.address - assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - assert generic_debt_allocator.totalDebtRatio() == 0 - with ape.reverts("!active"): - generic_debt_allocator.shouldUpdateDebt(strategy) - - -def test_set_keepers( - generic_debt_allocator_factory, generic_debt_allocator, brain, user -): - assert generic_debt_allocator_factory.keepers(brain) == True - assert generic_debt_allocator_factory.keepers(user) == False - - with ape.reverts("!governance"): - generic_debt_allocator_factory.setKeeper(user, True, sender=user) - - tx = generic_debt_allocator_factory.setKeeper(user, True, sender=brain) - - event = list(tx.decode_logs(generic_debt_allocator_factory.UpdateKeeper))[0] - - assert event.keeper == user - assert event.allowed == True - assert generic_debt_allocator_factory.keepers(user) == True - - tx = generic_debt_allocator_factory.setKeeper(brain, False, sender=brain) - - event = list(tx.decode_logs(generic_debt_allocator_factory.UpdateKeeper))[0] - - assert event.keeper == brain - assert event.allowed == False - assert generic_debt_allocator_factory.keepers(brain) == False - - -def test_set_managers(generic_debt_allocator, brain, user): - assert generic_debt_allocator.managers(brain) == False - assert generic_debt_allocator.managers(user) == False - - with ape.reverts("!governance"): - generic_debt_allocator.setManager(user, True, sender=user) - - tx = generic_debt_allocator.setManager(user, True, sender=brain) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateManager))[0] - - assert event.manager == user - assert event.allowed == True - assert generic_debt_allocator.managers(user) == True - - tx = generic_debt_allocator.setManager(user, False, sender=brain) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateManager))[0] - - assert event.manager == user - assert event.allowed == False - assert generic_debt_allocator.managers(user) == False - - -def test_set_minimum_change(generic_debt_allocator, brain, strategy, user): - assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - assert generic_debt_allocator.minimumChange() == 0 - - minimum = int(1e17) - - with ape.reverts("!governance"): - generic_debt_allocator.setMinimumChange(minimum, sender=user) - - with ape.reverts("zero"): - generic_debt_allocator.setMinimumChange(0, sender=brain) - - tx = generic_debt_allocator.setMinimumChange(minimum, sender=brain) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateMinimumChange))[0] - - assert event.newMinimumChange == minimum - assert generic_debt_allocator.minimumChange() == minimum - - -def test_set_minimum_wait(generic_debt_allocator, brain, strategy, user): - assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - assert generic_debt_allocator.minimumWait() == 0 - - minimum = int(1e17) - - with ape.reverts("!governance"): - generic_debt_allocator.setMinimumWait(minimum, sender=user) - - tx = generic_debt_allocator.setMinimumWait(minimum, sender=brain) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateMinimumWait))[0] - - assert event.newMinimumWait == minimum - assert generic_debt_allocator.minimumWait() == minimum - - -def test_set_max_debt_update_loss(generic_debt_allocator, brain, strategy, user): - assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - assert generic_debt_allocator.maxDebtUpdateLoss() == 1 - - max = int(69) - - with ape.reverts("!governance"): - generic_debt_allocator.setMaxDebtUpdateLoss(max, sender=user) - - with ape.reverts("higher than max"): - generic_debt_allocator.setMaxDebtUpdateLoss(10_001, sender=brain) - - tx = generic_debt_allocator.setMaxDebtUpdateLoss(max, sender=brain) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateMaxDebtUpdateLoss))[0] - - assert event.newMaxDebtUpdateLoss == max - assert generic_debt_allocator.maxDebtUpdateLoss() == max - - -def test_set_ratios( - generic_debt_allocator, brain, daddy, vault, strategy, create_strategy, user -): - assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - - minimum = int(1e17) - max = int(6_000) - target = int(5_000) - - with ape.reverts("!manager"): - generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=user) - - vault.add_strategy(strategy.address, sender=daddy) - - with ape.reverts("!minimum"): - generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) - - generic_debt_allocator.setMinimumChange(minimum, sender=brain) - - with ape.reverts("max too high"): - generic_debt_allocator.setStrategyDebtRatio( - strategy, target, int(10_001), sender=brain - ) - - with ape.reverts("max ratio"): - generic_debt_allocator.setStrategyDebtRatio( - strategy, int(max + 1), max, sender=brain - ) - - tx = generic_debt_allocator.setStrategyDebtRatio( - strategy, target, max, sender=brain - ) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] - - assert event.newTargetRatio == target - assert event.newMaxRatio == max - assert event.newTotalDebtRatio == target - assert generic_debt_allocator.totalDebtRatio() == target - assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) - - new_strategy = create_strategy() - vault.add_strategy(new_strategy, sender=daddy) - with ape.reverts("ratio too high"): - generic_debt_allocator.setStrategyDebtRatio( - new_strategy, int(10_000), int(10_000), sender=brain - ) - - target = int(8_000) - tx = generic_debt_allocator.setStrategyDebtRatio(strategy, target, sender=brain) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] - - assert event.newTargetRatio == target - assert event.newMaxRatio == target * 1.2 - assert event.newTotalDebtRatio == target - assert generic_debt_allocator.totalDebtRatio() == target - assert generic_debt_allocator.getConfig(strategy) == (target, target * 1.2, 0, 0) - - -def test_increase_debt_ratio( - generic_debt_allocator, brain, daddy, vault, strategy, create_strategy, user -): - assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - - minimum = int(1e17) - target = int(5_000) - increase = int(5_000) - max = target * 1.2 - - with ape.reverts("!manager"): - generic_debt_allocator.increaseStrategyDebtRatio( - strategy, increase, sender=user - ) - - vault.add_strategy(strategy.address, sender=daddy) - - with ape.reverts("!minimum"): - generic_debt_allocator.increaseStrategyDebtRatio( - strategy, increase, sender=brain - ) - - generic_debt_allocator.setMinimumChange(minimum, sender=brain) - - tx = generic_debt_allocator.increaseStrategyDebtRatio( - strategy, increase, sender=brain - ) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] - - assert event.newTargetRatio == target - assert event.newMaxRatio == max - assert event.newTotalDebtRatio == target - assert generic_debt_allocator.totalDebtRatio() == target - assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) - - new_strategy = create_strategy() - vault.add_strategy(new_strategy, sender=daddy) - - with ape.reverts("ratio too high"): - generic_debt_allocator.increaseStrategyDebtRatio( - new_strategy, int(5_001), sender=brain - ) - - target = int(8_000) - max = target * 1.2 - increase = int(3_000) - tx = generic_debt_allocator.increaseStrategyDebtRatio( - strategy, increase, sender=brain - ) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] - - assert event.newTargetRatio == target - assert event.newMaxRatio == max - assert event.newTotalDebtRatio == target - assert generic_debt_allocator.totalDebtRatio() == target - assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) - - target = int(10_000) - max = int(10_000) - increase = int(2_000) - tx = generic_debt_allocator.increaseStrategyDebtRatio( - strategy, increase, sender=brain - ) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] - - assert event.newTargetRatio == target - assert event.newMaxRatio == max - assert event.newTotalDebtRatio == target - assert generic_debt_allocator.totalDebtRatio() == target - assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) - - -def test_decrease_debt_ratio( - generic_debt_allocator, brain, vault, strategy, daddy, create_strategy, user -): - assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - - minimum = int(1e17) - target = int(5_000) - max = target * 1.2 - - vault.add_strategy(strategy.address, sender=daddy) - generic_debt_allocator.setMinimumChange(minimum, sender=brain) - - # Underflow - with ape.reverts(): - generic_debt_allocator.decreaseStrategyDebtRatio(strategy, target, sender=brain) - - # Add the target - tx = generic_debt_allocator.increaseStrategyDebtRatio( - strategy, target, sender=brain - ) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] - - assert event.newTargetRatio == target - assert event.newMaxRatio == max - assert event.newTotalDebtRatio == target - assert generic_debt_allocator.totalDebtRatio() == target - assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) - - target = int(2_000) - max = target * 1.2 - decrease = int(3_000) - - with ape.reverts("!manager"): - generic_debt_allocator.decreaseStrategyDebtRatio( - strategy, decrease, sender=user - ) - - tx = generic_debt_allocator.decreaseStrategyDebtRatio( - strategy, decrease, sender=brain - ) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] - - assert event.newTargetRatio == target - assert event.newMaxRatio == max - assert event.newTotalDebtRatio == target - assert generic_debt_allocator.totalDebtRatio() == target - assert generic_debt_allocator.getConfig(strategy) == (target, max, 0, 0) - - target = int(0) - max = int(0) - decrease = int(2_000) - tx = generic_debt_allocator.decreaseStrategyDebtRatio( - strategy, decrease, sender=brain - ) - - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] - - assert event.newTargetRatio == target - assert event.newMaxRatio == max - assert event.newTotalDebtRatio == target - assert generic_debt_allocator.totalDebtRatio() == target - assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - - -def test_should_update_debt( - generic_debt_allocator, vault, strategy, brain, daddy, deposit_into_vault, amount -): - assert generic_debt_allocator.getConfig(strategy.address) == (0, 0, 0, 0) - vault.add_role(generic_debt_allocator, ROLES.DEBT_MANAGER, sender=daddy) - - with ape.reverts("!active"): - generic_debt_allocator.shouldUpdateDebt(strategy.address) - - vault.add_strategy(strategy.address, sender=daddy) - - minimum = int(1) - target = int(5_000) - max = int(5_000) - - generic_debt_allocator.setMinimumChange(minimum, sender=brain) - generic_debt_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) - - # Vault has no assets so should be false. - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == False - assert bytes == ("Below Min").encode("utf-8") - - deposit_into_vault(vault, amount) - - # No max debt has been set so should be false. - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == False - assert bytes == ("Below Min").encode("utf-8") - - vault.update_max_debt_for_strategy(strategy, MAX_INT, sender=daddy) - - # Should now want to allocate 50% - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == True - print("got", bytes) - print("Made ", vault.update_debt.encode_input(strategy.address, amount // 2)) - assert bytes == vault.update_debt.encode_input(strategy.address, amount // 2) - - generic_debt_allocator.update_debt(strategy, amount // 2, sender=brain) - chain.mine(1) - - # Should now be false again once allocated - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == False - assert bytes == ("Below Min").encode("utf-8") - - # Update the ratio to make true - generic_debt_allocator.setStrategyDebtRatio( - strategy, int(target + 1), int(target + 1), sender=brain - ) - - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == True - assert bytes == vault.update_debt.encode_input( - strategy.address, int(amount * 5_001 // 10_000) - ) - - # Set a minimumWait time - generic_debt_allocator.setMinimumWait(MAX_INT, sender=brain) - # Should now be false - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == False - assert bytes == ("min wait").encode("utf-8") - - generic_debt_allocator.setMinimumWait(0, sender=brain) - - # Lower the max debt so its == to current debt - vault.update_max_debt_for_strategy(strategy, int(amount // 2), sender=daddy) - - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == False - assert bytes == ("Below Min").encode("utf-8") - - # Reset it. - vault.update_max_debt_for_strategy(strategy, MAX_INT, sender=daddy) - - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == True - assert bytes == vault.update_debt.encode_input( - strategy.address, int(amount * 5_001 // 10_000) - ) - - # Increase the minimum_total_idle - vault.set_minimum_total_idle(vault.totalIdle(), sender=daddy) - - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == False - assert bytes == ("No Idle").encode("utf-8") - - vault.set_minimum_total_idle(0, sender=daddy) - - # increase the minimum so its false again - generic_debt_allocator.setMinimumChange(int(1e30), sender=brain) - - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == False - assert bytes == ("Below Min").encode("utf-8") - - # Lower the target and minimum - generic_debt_allocator.setMinimumChange(int(1), sender=brain) - generic_debt_allocator.setStrategyDebtRatio( - strategy, int(target // 2), int(target // 2), sender=brain - ) - - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) - assert bool == True - assert bytes == vault.update_debt.encode_input(strategy.address, amount // 4) - - -def test_update_debt( - generic_debt_allocator_factory, - generic_debt_allocator, - vault, - strategy, - brain, - daddy, - user, - deposit_into_vault, - amount, -): - assert generic_debt_allocator.getConfig(strategy) == (0, 0, 0, 0) - deposit_into_vault(vault, amount) - - assert vault.totalIdle() == amount - assert vault.totalDebt() == 0 - - vault.add_strategy(strategy, sender=daddy) - vault.update_max_debt_for_strategy(strategy, MAX_INT, sender=daddy) - - # This reverts by the allocator - with ape.reverts("!keeper"): - generic_debt_allocator.update_debt(strategy, amount, sender=user) - - # This reverts by the vault - with ape.reverts("not allowed"): - generic_debt_allocator.update_debt(strategy, amount, sender=brain) - - vault.add_role( - generic_debt_allocator, - ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, - sender=daddy, - ) - - generic_debt_allocator.update_debt(strategy, amount, sender=brain) - - timestamp = generic_debt_allocator.getConfig(strategy)[2] - assert timestamp != 0 - assert vault.totalIdle() == 0 - assert vault.totalDebt() == amount - - generic_debt_allocator_factory.setKeeper(user, True, sender=brain) - - generic_debt_allocator.update_debt(strategy, 0, sender=user) - - assert generic_debt_allocator.getConfig(strategy)[2] != timestamp - assert vault.totalIdle() == amount - assert vault.totalDebt() == 0 diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 6d218ab..387a652 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -19,7 +19,7 @@ def full_setup( daddy, brain, yield_manager, - generic_debt_allocator, + debt_allocator, keeper, management, user, @@ -28,12 +28,12 @@ def full_setup( strategy_one = deploy_mock_tokenized("One", int(1e16)) strategy_two = deploy_mock_tokenized("two", int(1e17)) setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) - generic_debt_allocator.setManager(yield_manager, True, sender=brain) - generic_debt_allocator.setMinimumChange(1, sender=brain) + yield_manager.setVaultAllocator(vault, debt_allocator, sender=daddy) + debt_allocator.setManager(yield_manager, True, sender=brain) + debt_allocator.setMinimumChange(1, sender=brain) vault.add_role(yield_manager, ROLES.REPORTING_MANAGER, sender=daddy) vault.set_role( - generic_debt_allocator, + debt_allocator, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy, ) @@ -53,23 +53,21 @@ def test_yield_manager_setup(yield_manager, daddy, vault, management, keeper): assert yield_manager.vaultAllocator(vault) == ZERO_ADDRESS -def test_setters(yield_manager, daddy, vault, generic_debt_allocator, management): +def test_setters(yield_manager, daddy, vault, debt_allocator, management): assert yield_manager.vaultAllocator(vault) == ZERO_ADDRESS assert yield_manager.proposer(management) == False assert yield_manager.open() == False with ape.reverts("!governance"): - yield_manager.setVaultAllocator( - vault, generic_debt_allocator, sender=management - ) + yield_manager.setVaultAllocator(vault, debt_allocator, sender=management) - tx = yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + tx = yield_manager.setVaultAllocator(vault, debt_allocator, sender=daddy) event = list(tx.decode_logs(yield_manager.UpdateVaultAllocator))[0] assert event.vault == vault - assert event.allocator == generic_debt_allocator - assert yield_manager.vaultAllocator(vault) == generic_debt_allocator + assert event.allocator == debt_allocator + assert yield_manager.vaultAllocator(vault) == debt_allocator with ape.reverts("!governance"): yield_manager.setProposer(management, True, sender=management) @@ -113,7 +111,7 @@ def test_update_allocation( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): # Strategy two will have the higher apr strategy_one = deploy_mock_tokenized("One", int(1e16)) @@ -135,16 +133,16 @@ def test_update_allocation( with ape.reverts("vault not added"): yield_manager.updateAllocation(vault, allocation, sender=user) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + yield_manager.setVaultAllocator(vault, debt_allocator, sender=daddy) # Must give allocator the manager role with ape.reverts("!manager"): yield_manager.updateAllocation(vault, allocation, sender=user) - generic_debt_allocator.setManager(yield_manager, True, sender=brain) - generic_debt_allocator.setMinimumChange(1, sender=brain) + debt_allocator.setManager(yield_manager, True, sender=brain) + debt_allocator.setMinimumChange(1, sender=brain) vault.set_role( - generic_debt_allocator, + debt_allocator, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy, ) @@ -161,17 +159,17 @@ def test_update_allocation( (before, now) = tx.return_value - assert generic_debt_allocator.getConfig(strategy_two).targetRatio == MAX_BPS - assert generic_debt_allocator.getConfig(strategy_one).targetRatio == 0 - assert generic_debt_allocator.shouldUpdateDebt(strategy_one)[0] == False - assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == True - assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[ + assert debt_allocator.getConfig(strategy_two).targetRatio == MAX_BPS + assert debt_allocator.getConfig(strategy_one).targetRatio == 0 + assert debt_allocator.shouldUpdateDebt(strategy_one)[0] == False + assert debt_allocator.shouldUpdateDebt(strategy_two)[0] == True + assert debt_allocator.shouldUpdateDebt(strategy_two)[ 1 ] == vault.update_debt.encode_input(strategy_two.address, amount) assert before == 0 - generic_debt_allocator.update_debt(strategy_two, amount, sender=brain) - assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + debt_allocator.update_debt(strategy_two, amount, sender=brain) + assert debt_allocator.shouldUpdateDebt(strategy_two)[0] == False # assert now == int(1e17 * amount) assert vault.totalIdle() == 0 @@ -203,26 +201,26 @@ def test_update_allocation( allocation = [(strategy_two, amount - to_move), (strategy_one, to_move)] tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert generic_debt_allocator.getConfig(strategy_two).targetRatio != MAX_BPS - assert generic_debt_allocator.getConfig(strategy_one).targetRatio != 0 - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) - (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert debt_allocator.getConfig(strategy_two).targetRatio != MAX_BPS + assert debt_allocator.getConfig(strategy_one).targetRatio != 0 + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False assert bool_two == True assert bytes_two == vault.update_debt.encode_input( strategy_two.address, amount - to_move ) - generic_debt_allocator.update_debt(strategy_two, amount - to_move, sender=brain) + debt_allocator.update_debt(strategy_two, amount - to_move, sender=brain) - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, to_move) - generic_debt_allocator.update_debt(strategy_one, to_move, sender=brain) + debt_allocator.update_debt(strategy_one, to_move, sender=brain) - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) - (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False assert bool_two == False @@ -240,25 +238,25 @@ def test_update_allocation( tx = yield_manager.updateAllocation(vault, allocation, sender=user) - assert generic_debt_allocator.getConfig(strategy_two).targetRatio == 0 - assert generic_debt_allocator.getConfig(strategy_one).targetRatio == MAX_BPS + assert debt_allocator.getConfig(strategy_two).targetRatio == 0 + assert debt_allocator.getConfig(strategy_one).targetRatio == MAX_BPS - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) - (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False assert bool_two == True assert bytes_two == vault.update_debt.encode_input(strategy_two.address, 0) - generic_debt_allocator.update_debt(strategy_two, 0, sender=brain) + debt_allocator.update_debt(strategy_two, 0, sender=brain) - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, amount) - generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) + debt_allocator.update_debt(strategy_one, amount, sender=brain) - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) - (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False assert bool_two == False @@ -287,7 +285,7 @@ def test_update_allocation_pending_profit( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): strategy_one, strategy_two = full_setup( deploy_mock_tokenized=deploy_mock_tokenized, @@ -296,7 +294,7 @@ def test_update_allocation_pending_profit( daddy=daddy, brain=brain, yield_manager=yield_manager, - generic_debt_allocator=generic_debt_allocator, + debt_allocator=debt_allocator, keeper=keeper, management=management, user=user, @@ -308,7 +306,7 @@ def test_update_allocation_pending_profit( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) + debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -322,18 +320,18 @@ def test_update_allocation_pending_profit( assert len(list(tx.decode_logs(strategy_one.Reported))) == 1 - assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy_one) assert bool == True assert bytes == vault.update_debt.encode_input(strategy_one, 0) - tx = generic_debt_allocator.update_debt(strategy_one, 0, sender=brain) + tx = debt_allocator.update_debt(strategy_one, 0, sender=brain) event = list(tx.decode_logs(vault.StrategyReported)) assert len(event) == 1 assert event[0].gain == profit - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True assert bytes == vault.update_debt.encode_input(strategy_two, amount + profit) @@ -350,7 +348,7 @@ def test_update_allocation_pending_loss( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): strategy_one, strategy_two = full_setup( deploy_mock_tokenized=deploy_mock_tokenized, @@ -359,7 +357,7 @@ def test_update_allocation_pending_loss( daddy=daddy, brain=brain, yield_manager=yield_manager, - generic_debt_allocator=generic_debt_allocator, + debt_allocator=debt_allocator, keeper=keeper, management=management, user=user, @@ -370,7 +368,7 @@ def test_update_allocation_pending_loss( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) + debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -384,14 +382,14 @@ def test_update_allocation_pending_loss( assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 - assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy_one) assert bool == True assert bytes == vault.update_debt.encode_input(strategy_one, 0) - generic_debt_allocator.update_debt(strategy_one, 0, sender=brain) + debt_allocator.update_debt(strategy_one, 0, sender=brain) - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True assert bytes == vault.update_debt.encode_input(strategy_two, amount - loss) @@ -408,7 +406,7 @@ def test_update_allocation_pending_loss_move_half( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): strategy_one, strategy_two = full_setup( deploy_mock_tokenized=deploy_mock_tokenized, @@ -417,7 +415,7 @@ def test_update_allocation_pending_loss_move_half( daddy=daddy, brain=brain, yield_manager=yield_manager, - generic_debt_allocator=generic_debt_allocator, + debt_allocator=debt_allocator, keeper=keeper, management=management, user=user, @@ -428,7 +426,7 @@ def test_update_allocation_pending_loss_move_half( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) + debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -446,13 +444,13 @@ def test_update_allocation_pending_loss_move_half( assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 assert vault.totalAssets() < amount - assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy_one) assert bool == True - generic_debt_allocator.update_debt(strategy_one, amount - to_move, sender=brain) + debt_allocator.update_debt(strategy_one, amount - to_move, sender=brain) - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True assert bytes == vault.update_debt.encode_input(strategy_two, to_move - loss) @@ -469,7 +467,7 @@ def test_update_allocation_pending_loss_move_all( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): strategy_one, strategy_two = full_setup( deploy_mock_tokenized=deploy_mock_tokenized, @@ -478,7 +476,7 @@ def test_update_allocation_pending_loss_move_all( daddy=daddy, brain=brain, yield_manager=yield_manager, - generic_debt_allocator=generic_debt_allocator, + debt_allocator=debt_allocator, keeper=keeper, management=management, user=user, @@ -489,7 +487,7 @@ def test_update_allocation_pending_loss_move_all( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) + debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -507,13 +505,13 @@ def test_update_allocation_pending_loss_move_all( assert len(list(tx.decode_logs(vault.StrategyReported))) == 1 assert vault.totalAssets() < amount - assert generic_debt_allocator.shouldUpdateDebt(strategy_two)[0] == False - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + assert debt_allocator.shouldUpdateDebt(strategy_two)[0] == False + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy_one) assert bool == True - generic_debt_allocator.update_debt(strategy_one, amount - to_move, sender=brain) + debt_allocator.update_debt(strategy_one, amount - to_move, sender=brain) - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True assert bytes == vault.update_debt.encode_input(strategy_two, to_move - loss) @@ -528,14 +526,14 @@ def test_validate_allocation( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): strategy_one = deploy_mock_tokenized("One") strategy_two = deploy_mock_tokenized("two") setup_vault(vault, [strategy_one, strategy_two], apr_oracle, daddy) - yield_manager.setVaultAllocator(vault, generic_debt_allocator, sender=daddy) + yield_manager.setVaultAllocator(vault, debt_allocator, sender=daddy) vault.set_role( - generic_debt_allocator, + debt_allocator, ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, sender=daddy, ) @@ -552,7 +550,7 @@ def test_validate_allocation( vault, [(strategy_one, amount), (strategy_two, 0)] ) - generic_debt_allocator.update_debt(strategy_one, amount // 2, sender=brain) + debt_allocator.update_debt(strategy_one, amount // 2, sender=brain) assert yield_manager.validateAllocation(vault, []) == False assert yield_manager.validateAllocation(vault, [(strategy_one, amount)]) @@ -561,9 +559,7 @@ def test_validate_allocation( ) # Now will be false - generic_debt_allocator.update_debt( - strategy_two, vault.totalIdle() // 2, sender=brain - ) + debt_allocator.update_debt(strategy_two, vault.totalIdle() // 2, sender=brain) assert yield_manager.validateAllocation(vault, []) == False assert yield_manager.validateAllocation(vault, [(strategy_one, amount)]) == False @@ -635,7 +631,7 @@ def test_update_allocation_permissioned( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): strategy_one, strategy_two = full_setup( deploy_mock_tokenized=deploy_mock_tokenized, @@ -644,7 +640,7 @@ def test_update_allocation_permissioned( daddy=daddy, brain=brain, yield_manager=yield_manager, - generic_debt_allocator=generic_debt_allocator, + debt_allocator=debt_allocator, keeper=keeper, management=management, user=user, @@ -666,12 +662,12 @@ def test_update_allocation_permissioned( tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) - assert generic_debt_allocator.shouldUpdateDebt(strategy_one)[0] == False - (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert debt_allocator.shouldUpdateDebt(strategy_one)[0] == False + (bool, bytes) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool == True assert bytes == vault.update_debt.encode_input(strategy_two, amount) - generic_debt_allocator.update_debt(strategy_two, amount, sender=brain) + debt_allocator.update_debt(strategy_two, amount, sender=brain) allocation = [(strategy_one, amount)] with ape.reverts("ratio too high"): @@ -690,26 +686,26 @@ def test_update_allocation_permissioned( allocation = [(strategy_two, amount - to_move), (strategy_one, to_move)] tx = yield_manager.updateAllocationPermissioned(vault, allocation, sender=daddy) - assert generic_debt_allocator.getConfig(strategy_two).targetRatio != MAX_BPS - assert generic_debt_allocator.getConfig(strategy_one).targetRatio != 0 - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) - (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert debt_allocator.getConfig(strategy_two).targetRatio != MAX_BPS + assert debt_allocator.getConfig(strategy_one).targetRatio != 0 + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False assert bool_two == True assert bytes_two == vault.update_debt.encode_input( strategy_two.address, amount - to_move ) - generic_debt_allocator.update_debt(strategy_two, amount - to_move, sender=brain) + debt_allocator.update_debt(strategy_two, amount - to_move, sender=brain) - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, to_move) - generic_debt_allocator.update_debt(strategy_one, to_move, sender=brain) + debt_allocator.update_debt(strategy_one, to_move, sender=brain) - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) - (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False assert bool_two == False @@ -724,24 +720,24 @@ def test_update_allocation_permissioned( assert len(list(tx.decode_logs(strategy_two.Reported))) == 1 - assert generic_debt_allocator.getConfig(strategy_two).targetRatio == 0 - assert generic_debt_allocator.getConfig(strategy_one).targetRatio == MAX_BPS - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) - (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + assert debt_allocator.getConfig(strategy_two).targetRatio == 0 + assert debt_allocator.getConfig(strategy_one).targetRatio == MAX_BPS + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False assert bool_two == True assert bytes_two == vault.update_debt.encode_input(strategy_two.address, 0) - generic_debt_allocator.update_debt(strategy_two, 0, sender=brain) + debt_allocator.update_debt(strategy_two, 0, sender=brain) - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) assert bool_one == True assert bytes_one == vault.update_debt.encode_input(strategy_one, amount) - generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) + debt_allocator.update_debt(strategy_one, amount, sender=brain) - (bool_one, bytes_one) = generic_debt_allocator.shouldUpdateDebt(strategy_one) - (bool_two, bytes_two) = generic_debt_allocator.shouldUpdateDebt(strategy_two) + (bool_one, bytes_one) = debt_allocator.shouldUpdateDebt(strategy_one) + (bool_two, bytes_two) = debt_allocator.shouldUpdateDebt(strategy_two) assert bool_one == False assert bool_two == False @@ -764,7 +760,7 @@ def test_update_allocation__max_withdraw( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): strategy_one, strategy_two = full_setup( deploy_mock_tokenized=deploy_mock_tokenized, @@ -773,7 +769,7 @@ def test_update_allocation__max_withdraw( daddy=daddy, brain=brain, yield_manager=yield_manager, - generic_debt_allocator=generic_debt_allocator, + debt_allocator=debt_allocator, keeper=keeper, management=management, user=user, @@ -782,7 +778,7 @@ def test_update_allocation__max_withdraw( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) + debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -823,7 +819,7 @@ def test_update_allocation__max_deposit( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): strategy_one, strategy_two = full_setup( deploy_mock_tokenized=deploy_mock_tokenized, @@ -832,7 +828,7 @@ def test_update_allocation__max_deposit( daddy=daddy, brain=brain, yield_manager=yield_manager, - generic_debt_allocator=generic_debt_allocator, + debt_allocator=debt_allocator, keeper=keeper, management=management, user=user, @@ -841,7 +837,7 @@ def test_update_allocation__max_deposit( asset.approve(vault, amount, sender=user) vault.deposit(amount, user, sender=user) - generic_debt_allocator.update_debt(strategy_one, amount, sender=brain) + debt_allocator.update_debt(strategy_one, amount, sender=brain) assert vault.totalAssets() == amount assert vault.totalDebt() == amount @@ -882,7 +878,7 @@ def test_update_allocation__min_idle( amount, asset, deploy_mock_tokenized, - generic_debt_allocator, + debt_allocator, ): strategy_one, strategy_two = full_setup( deploy_mock_tokenized=deploy_mock_tokenized, @@ -891,7 +887,7 @@ def test_update_allocation__min_idle( daddy=daddy, brain=brain, yield_manager=yield_manager, - generic_debt_allocator=generic_debt_allocator, + debt_allocator=debt_allocator, keeper=keeper, management=management, user=user, @@ -924,7 +920,7 @@ def test_update_allocation__min_idle( allocation = [(strategy_one, amount - min_idle)] tx = yield_manager.updateAllocation(vault, allocation, sender=user) - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio))[0] + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio))[0] assert event.newTargetRatio == 5_000 # lower the min idle to 0 @@ -934,7 +930,7 @@ def test_update_allocation__min_idle( allocation = [(strategy_one, 0), (strategy_two, amount)] tx = yield_manager.updateAllocation(vault, allocation, sender=user) - event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatio)) + event = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio)) assert len(event) == 2 assert event[0].newTargetRatio == 0 diff --git a/tests/manager/test_role_manager.py b/tests/manager/test_role_manager.py index 5a6d74f..1ac246c 100644 --- a/tests/manager/test_role_manager.py +++ b/tests/manager/test_role_manager.py @@ -21,7 +21,7 @@ def test_role_manager_setup( strategy_manager, healthcheck_accountant, registry, - generic_debt_allocator_factory, + debt_allocator_factory, release_registry, vault, asset, @@ -44,7 +44,7 @@ def test_role_manager_setup( assert role_manager.getStrategyManager() == strategy_manager assert role_manager.getRegistry() == registry assert role_manager.getAccountant() == healthcheck_accountant - assert role_manager.getAllocatorFactory() == generic_debt_allocator_factory + assert role_manager.getAllocatorFactory() == debt_allocator_factory assert role_manager.isVaultsRoleManager(vault) == False assert role_manager.getDebtAllocator(vault) == ZERO_ADDRESS assert role_manager.getRating(vault) == 0 @@ -64,7 +64,7 @@ def test__positions( strategy_manager, healthcheck_accountant, registry, - generic_debt_allocator_factory, + debt_allocator_factory, user, ): assert role_manager.getDaddy() == daddy @@ -74,7 +74,7 @@ def test__positions( assert role_manager.getStrategyManager() == strategy_manager assert role_manager.getRegistry() == registry assert role_manager.getAccountant() == healthcheck_accountant - assert role_manager.getAllocatorFactory() == generic_debt_allocator_factory + assert role_manager.getAllocatorFactory() == debt_allocator_factory assert role_manager.getPositionHolder(role_manager.DADDY()) == daddy assert role_manager.getPositionHolder(role_manager.BRAIN()) == brain assert role_manager.getPositionHolder(role_manager.SECURITY()) == security @@ -90,7 +90,7 @@ def test__positions( ) assert ( role_manager.getPositionHolder(role_manager.ALLOCATOR_FACTORY()) - == generic_debt_allocator_factory + == debt_allocator_factory ) # Check roles assert role_manager.getDaddyRoles() == daddy_roles @@ -128,7 +128,7 @@ def test__positions( "Strategy Manager": (strategy_manager, strategy_manager_roles), "Registry": (registry, 0), "Accountant": (healthcheck_accountant, 0), - "Allocator Factory": (generic_debt_allocator_factory, 0), + "Allocator Factory": (debt_allocator_factory, 0), } new_role = int(420_69) @@ -272,7 +272,7 @@ def test_deploy_new_vault( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): rating = int(1) deposit_limit = int(100e18) @@ -323,10 +323,10 @@ def test_deploy_new_vault( assert event.rating == rating allocator = event.debtAllocator - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + debt_allocator = project.DebtAllocator.at(event.allocator) assert allocator == debt_allocator (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( @@ -380,7 +380,7 @@ def test_deploy_new_vault__duplicate_reverts( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): rating = int(1) deposit_limit = int(100e18) @@ -408,10 +408,10 @@ def test_deploy_new_vault__duplicate_reverts( vault = project.dependencies["yearn-vaults"]["v3.0.1"].VaultV3.at(event.vault) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + debt_allocator = project.DebtAllocator.at(event.allocator) (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault @@ -436,7 +436,9 @@ def test_deploy_new_vault__duplicate_reverts( role_manager.newVault(asset, rating, deposit_limit, profit_unlock, sender=daddy) # can with a different rating. - role_manager.newVault(asset, rating + 1, deposit_limit, profit_unlock, sender=daddy) + role_manager.newVault( + asset, rating + 1, deposit_limit, profit_unlock, max_fee="1", sender=daddy + ) def test_deploy_new_vault__default_values( @@ -452,7 +454,7 @@ def test_deploy_new_vault__default_values( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): rating = int(2) @@ -499,10 +501,10 @@ def test_deploy_new_vault__default_values( assert event.vault == vault assert event.rating == rating - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + debt_allocator = project.DebtAllocator.at(event.allocator) (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault @@ -570,7 +572,7 @@ def test_add_new_vault__endorsed( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): setup_role_manager( role_manager=role_manager, @@ -623,10 +625,10 @@ def test_add_new_vault__endorsed( assert event.vault == vault assert event.rating == rating - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + debt_allocator = project.DebtAllocator.at(event.allocator) (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault @@ -680,7 +682,7 @@ def test_add_new_vault__not_endorsed( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): setup_role_manager( role_manager=role_manager, @@ -731,10 +733,10 @@ def test_add_new_vault__not_endorsed( assert event.vault == vault assert event.rating == rating - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + debt_allocator = project.DebtAllocator.at(event.allocator) (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault @@ -791,7 +793,7 @@ def test_add_new_vault__with_debt_allocator( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): setup_role_manager( role_manager=role_manager, @@ -820,10 +822,10 @@ def test_add_new_vault__with_debt_allocator( assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=brain) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + tx = debt_allocator_factory.newDebtAllocator(vault, sender=brain) + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + debt_allocator = project.DebtAllocator.at(event.allocator) with ape.reverts("!allowed"): role_manager.addNewVault(vault, rating, debt_allocator, sender=user) @@ -903,7 +905,7 @@ def test_add_new_vault__with_accountant( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): setup_role_manager( role_manager=role_manager, @@ -931,10 +933,10 @@ def test_add_new_vault__with_accountant( assert registry.numAssets() == 0 assert registry.numEndorsedVaults(asset) == 0 - tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=brain) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + tx = debt_allocator_factory.newDebtAllocator(vault, sender=brain) + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + debt_allocator = project.DebtAllocator.at(event.allocator) vault.add_role(daddy, ROLES.ACCOUNTANT_MANAGER, sender=daddy) vault.set_accountant(user, sender=daddy) @@ -1013,7 +1015,7 @@ def test_add_new_vault__duplicate_reverts( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): setup_role_manager( role_manager=role_manager, @@ -1043,10 +1045,10 @@ def test_add_new_vault__duplicate_reverts( vault = project.dependencies["yearn-vaults"]["v3.0.1"].VaultV3.at(event.vault) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] assert event.vault == vault - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + debt_allocator = project.DebtAllocator.at(event.allocator) (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault @@ -1094,7 +1096,7 @@ def test_new_debt_allocator__deploys_one( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): setup_role_manager( role_manager=role_manager, @@ -1117,8 +1119,8 @@ def test_new_debt_allocator__deploys_one( event = list(tx.decode_logs(registry.NewEndorsedVault))[0] vault = project.dependencies["yearn-vaults"]["v3.0.1"].VaultV3.at(event.vault) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] + debt_allocator = project.DebtAllocator.at(event.allocator) (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault @@ -1160,12 +1162,16 @@ def test_new_debt_allocator__deploys_one( tx = role_manager.updateDebtAllocator(vault, sender=brain) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] - new_debt_allocator = project.GenericDebtAllocator.at(event.allocator) + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] + new_debt_allocator = project.DebtAllocator.at(event.allocator) + + event = list(tx.decode_logs(role_manager.UpdateDebtAllocator))[0] + + assert event.vault == vault + assert event.debtAllocator == new_debt_allocator assert new_debt_allocator != debt_allocator assert new_debt_allocator.vault() == vault - assert new_debt_allocator.maxAcceptableBaseFee() == MAX_INT (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault @@ -1209,7 +1215,7 @@ def test_new_debt_allocator__already_deployed( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): setup_role_manager( role_manager=role_manager, @@ -1232,8 +1238,8 @@ def test_new_debt_allocator__already_deployed( event = list(tx.decode_logs(registry.NewEndorsedVault))[0] vault = project.dependencies["yearn-vaults"]["v3.0.1"].VaultV3.at(event.vault) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] + debt_allocator = project.DebtAllocator.at(event.allocator) (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault @@ -1265,9 +1271,9 @@ def test_new_debt_allocator__already_deployed( # Check debt allocator assert debt_allocator.vault() == vault - tx = generic_debt_allocator_factory.newGenericDebtAllocator(vault, sender=brain) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] - new_debt_allocator = project.GenericDebtAllocator.at(event.allocator) + tx = debt_allocator_factory.newDebtAllocator(vault, sender=brain) + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] + new_debt_allocator = project.DebtAllocator.at(event.allocator) # Update to a new debt allocator with ape.reverts("!allowed"): @@ -1278,9 +1284,13 @@ def test_new_debt_allocator__already_deployed( tx = role_manager.updateDebtAllocator(vault, new_debt_allocator, sender=brain) + event = list(tx.decode_logs(role_manager.UpdateDebtAllocator))[0] + + assert event.vault == vault + assert event.debtAllocator == new_debt_allocator + assert new_debt_allocator != debt_allocator assert new_debt_allocator.vault() == vault - assert new_debt_allocator.maxAcceptableBaseFee() == MAX_INT (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault @@ -1324,7 +1334,7 @@ def test_remove_vault( registry, release_registry, vault_factory, - generic_debt_allocator_factory, + debt_allocator_factory, ): setup_role_manager( role_manager=role_manager, @@ -1350,8 +1360,8 @@ def test_remove_vault( event = list(tx.decode_logs(registry.NewEndorsedVault))[0] vault = project.dependencies["yearn-vaults"]["v3.0.1"].VaultV3.at(event.vault) - event = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator))[0] - debt_allocator = project.GenericDebtAllocator.at(event.allocator) + event = list(tx.decode_logs(debt_allocator_factory.NewDebtAllocator))[0] + debt_allocator = project.DebtAllocator.at(event.allocator) (vault_asset, vault_rating, vault_debt_allocator, index) = role_manager.vaultConfig( vault From a390e09f5f6e9cc2e19877e10aa80bf2422ff9cc Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 8 Jan 2024 16:58:45 -0700 Subject: [PATCH 34/35] chore: comments --- .../YieldManager/YieldManager.sol | 22 ++++++++++++++----- scripts/deploy_allocator_factory.py | 13 ++++++++--- .../yield/test_yield_manager.py | 10 ++++----- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/contracts/debtAllocators/YieldManager/YieldManager.sol b/contracts/debtAllocators/YieldManager/YieldManager.sol index d26f61a..61ec991 100644 --- a/contracts/debtAllocators/YieldManager/YieldManager.sol +++ b/contracts/debtAllocators/YieldManager/YieldManager.sol @@ -79,7 +79,7 @@ contract YieldManager is Governance { * and debt increases at the end. * - Account for all limiting values such as the vaults max_debt and min_total_idle * as well as the strategies maxDeposit/maxRedeem that are enforced on debt updates. - * - Account for the expected differences in amounts caused unrealised losses or profits. + * - Account for the expected differences in amounts caused by unrealised losses or profits. * * @param _vault The address of the vault to propose an allocation for. * @param _newAllocations Array of strategies and their new proposed allocation. @@ -110,6 +110,8 @@ contract YieldManager is Governance { address _strategy; uint256 _currentDebt; uint256 _newDebt; + uint256 _strategyRate; + uint256 _targetRatio; for (uint256 i = 0; i < _newAllocations.length; ++i) { _strategy = _newAllocations[i].strategy; _newDebt = uint256(_newAllocations[i].newDebt); @@ -119,7 +121,7 @@ contract YieldManager is Governance { _accountedFor += _currentDebt; // Get the current weighted rate the strategy is earning - uint256 _strategyRate = (aprOracle.getStrategyApr(_strategy, 0) * + _strategyRate = (aprOracle.getStrategyApr(_strategy, 0) * _currentDebt); // Add to the amount currently being earned. @@ -170,7 +172,7 @@ contract YieldManager is Governance { } // Get the target based on the new debt. - uint256 _targetRatio = _newDebt < _totalAssets + _targetRatio = _newDebt < _totalAssets ? (_newDebt * MAX_BPS) / _totalAssets : MAX_BPS; @@ -249,7 +251,7 @@ contract YieldManager is Governance { * @return _currentRate The current weighted rate that the collective strategies are earning. * @return _expectedRate The expected weighted rate that the collective strategies would earn. */ - function getCurrentAndExpectedYield( + function getCurrentAndExpectedRate( address _vault, Allocation[] memory _newAllocations ) @@ -267,13 +269,14 @@ contract YieldManager is Governance { uint256 _newDebt; address _strategy; uint256 _currentDebt; + uint256 _strategyRate; for (uint256 i = 0; i < _newAllocations.length; ++i) { _newDebt = uint256(_newAllocations[i].newDebt); _strategy = _newAllocations[i].strategy; _currentDebt = IVault(_vault).strategies(_strategy).current_debt; // Get the current weighted rate the strategy is earning - uint256 _strategyRate = (aprOracle.getStrategyApr(_strategy, 0) * + _strategyRate = (aprOracle.getStrategyApr(_strategy, 0) * _currentDebt); // Add to the amount currently being earned. @@ -299,8 +302,14 @@ contract YieldManager is Governance { * its strategies and their specific allocation. * * The `_newAllocations` array should: + * - Contain all strategies that hold any amount of debt from the vault + * even if the debt wont be adjusted in order to get the correct + * on chain rate. * - Be ordered so that all debt decreases are at the beginning of the array * and debt increases at the end. + * - Account for all limiting values such as the vaults max_debt and min_total_idle + * as well as the strategies maxDeposit/maxRedeem that are enforced on debt updates. + * - Account for the expected differences in amounts caused by unrealised losses or profits. * * This will not do any APR checks and assumes the sender has completed * any and all necessary checks before sending. @@ -317,6 +326,7 @@ contract YieldManager is Governance { address _strategy; uint256 _newDebt; uint256 _currentDebt; + uint256 _targetRatio; uint256 _totalAssets = IVault(_vault).totalAssets(); for (uint256 i = 0; i < _newAllocations.length; ++i) { _strategy = _newAllocations[i].strategy; @@ -368,7 +378,7 @@ contract YieldManager is Governance { } // Get the target based on the new debt. - uint256 _targetRatio = _newDebt < _totalAssets + _targetRatio = _newDebt < _totalAssets ? (_newDebt * MAX_BPS) / _totalAssets : MAX_BPS; diff --git a/scripts/deploy_allocator_factory.py b/scripts/deploy_allocator_factory.py index 80d958e..99b80e1 100644 --- a/scripts/deploy_allocator_factory.py +++ b/scripts/deploy_allocator_factory.py @@ -10,17 +10,17 @@ def deploy_allocator_factory(): - print("Deploying Generic Debt Allocator Factory on ChainID", chain.chain_id) + print("Deploying Debt Allocator Factory on ChainID", chain.chain_id) if input("Do you want to continue? ") == "n": return - allocator_factory = project.GenericDebtAllocatorFactory + allocator_factory = project.DebtAllocatorFactory deployer_contract = project.Deployer.at( "0x8D85e7c9A4e369E53Acc8d5426aE1568198b0112" ) - salt_string = "Generic Debt Allocator Factory" + salt_string = "Debt Allocator Factory" # Create a SHA-256 hash object hash_object = hashlib.sha256() @@ -34,9 +34,14 @@ def deploy_allocator_factory(): print(f"Salt we are using {salt}") print("Init balance:", deployer.balance / 1e18) + gov = input("Governance? ") + + allocator_constructor = allocator_factory.constructor.encode_input(gov) + # generate and deploy deploy_bytecode = HexBytes( HexBytes(allocator_factory.contract_type.deployment_bytecode.bytecode) + + allocator_constructor ) print(f"Deploying the Factory...") @@ -50,6 +55,8 @@ def deploy_allocator_factory(): print("------------------") print(f"Deployed the Factory to {address}") print("------------------") + print(f"Encoded Constructor to use for verifaction {allocator_constructor.hex()[2:]}") + def main(): diff --git a/tests/debtAllocators/yield/test_yield_manager.py b/tests/debtAllocators/yield/test_yield_manager.py index 387a652..dbac718 100644 --- a/tests/debtAllocators/yield/test_yield_manager.py +++ b/tests/debtAllocators/yield/test_yield_manager.py @@ -589,27 +589,25 @@ def test_get_current_and_expected( vault.deposit(amount, user, sender=user) allocation = [] - (current, expected) = yield_manager.getCurrentAndExpectedYield(vault, allocation) + (current, expected) = yield_manager.getCurrentAndExpectedRate(vault, allocation) assert current == 0 assert expected == 0 allocation = [(strategy_one, 0), (strategy_two, 0)] - (current, expected) = yield_manager.getCurrentAndExpectedYield(vault, allocation) + (current, expected) = yield_manager.getCurrentAndExpectedRate(vault, allocation) assert current == 0 assert expected == 0 allocation = [(strategy_one, amount), (strategy_two, 0)] - (current, expected) = yield_manager.getCurrentAndExpectedYield(vault, allocation) + (current, expected) = yield_manager.getCurrentAndExpectedRate(vault, allocation) assert current == 0 assert expected != 0 vault.update_debt(strategy_one, amount, sender=daddy) allocation = [(strategy_one, 0), (strategy_two, amount)] - (current, new_expected) = yield_manager.getCurrentAndExpectedYield( - vault, allocation - ) + (current, new_expected) = yield_manager.getCurrentAndExpectedRate(vault, allocation) assert current == expected assert expected != 0 assert expected > 0 From 9c01b1e1ba430498e57e971a848a846e426c9a85 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 8 Jan 2024 17:04:34 -0700 Subject: [PATCH 35/35] fix: black --- scripts/deploy_allocator_factory.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/deploy_allocator_factory.py b/scripts/deploy_allocator_factory.py index 99b80e1..eb56809 100644 --- a/scripts/deploy_allocator_factory.py +++ b/scripts/deploy_allocator_factory.py @@ -55,8 +55,9 @@ def deploy_allocator_factory(): print("------------------") print(f"Deployed the Factory to {address}") print("------------------") - print(f"Encoded Constructor to use for verifaction {allocator_constructor.hex()[2:]}") - + print( + f"Encoded Constructor to use for verifaction {allocator_constructor.hex()[2:]}" + ) def main():