From 2bf699197daf945a7bd437f1f9862e3dc7e5da68 Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:22:36 -0600 Subject: [PATCH] build: stand alone allocators (#45) * build: stand alone allocators * test: generic allocator * build: pause allocator --- .../Generic/GenericDebtAllocator.sol | 606 ++++++++++++++++++ .../Generic/GenericDebtAllocatorFactory.sol | 43 ++ tests/conftest.py | 30 + tests/debtAllocators/test_generic.py | 573 +++++++++++++++++ 4 files changed, 1252 insertions(+) create mode 100644 contracts/debtAllocators/Generic/GenericDebtAllocator.sol create mode 100644 contracts/debtAllocators/Generic/GenericDebtAllocatorFactory.sol create mode 100644 tests/debtAllocators/test_generic.py diff --git a/contracts/debtAllocators/Generic/GenericDebtAllocator.sol b/contracts/debtAllocators/Generic/GenericDebtAllocator.sol new file mode 100644 index 0000000..d0ba99b --- /dev/null +++ b/contracts/debtAllocators/Generic/GenericDebtAllocator.sol @@ -0,0 +1,606 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity >=0.8.18; + +import {Governance} from "@periphery/utils/Governance.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; + +/** + * @title YearnV3 Generic Debt Allocator + * @author yearn.finance + * @notice + * This Generic 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. + * + * Each allocator contract will serve one Vault and each strategy + * that should be managed by this allocator will need to be added + * manually by setting a `targetRatio` and `maxRatio`. + * + * The allocator aims to allocate debt between the strategies + * based on their set target ratios. Which are denominated in basis + * points and represent the percent of total assets that specific + * strategy should hold. + * + * The trigger will attempt to allocate up to the `maxRatio` when + * the strategy has `minimumChange` amount less than the `targetRatio`. + * And will pull funds from the strategy when it has `minimumChange` + * more than its `maxRatio`. + */ +contract GenericDebtAllocator is Governance { + /// @notice An event emitted when a strategies debt ratios are Updated. + event UpdateStrategyDebtRatio( + address indexed strategy, + uint256 newTargetRatio, + uint256 newMaxRatio, + uint256 newTotalDebtRatio + ); + + /// @notice An even emitted when the paused status is updated. + event UpdatePaused(bool indexed status); + + /// @notice An event emitted when the minimum time to wait is updated. + event UpdateMinimumWait(uint256 newMinimumWait); + + /// @notice An event emitted when the minimum change is updated. + event UpdateMinimumChange(uint256 newMinimumChange); + + /// @notice An event emitted when a keeper is added or removed. + event UpdateKeeper(address indexed keeper, bool allowed); + + /// @notice An event emitted when a keeper is added or removed. + event UpdateManager(address indexed manager, bool allowed); + + /// @notice An event emitted when the max debt update loss is updated. + event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); + + /// @notice An event emitted when a strategy is added or removed. + event StrategyChanged(address indexed strategy, Status status); + + /// @notice An event emitted when the max base fee is updated. + event UpdateMaxAcceptableBaseFee(uint256 newMaxAcceptableBaseFee); + + /// @notice Status when a strategy is added or removed from the allocator. + enum Status { + NULL, + ADDED, + REMOVED + } + + /// @notice Struct for each strategies info. + struct Config { + // Flag to set when a strategy is added. + bool added; + // The ideal percent in Basis Points the strategy should have. + uint16 targetRatio; + // The max percent of assets the strategy should hold. + uint16 maxRatio; + // Timestamp of the last time debt was updated. + // The debt updates must be done through this allocator + // for this to be used. + uint96 lastUpdate; + // We have an extra 120 bits in the slot. + // So we declare the variable in the struct so it can be + // used if this contract is inherited. + uint120 open; + } + + /// @notice Make sure the caller is governance or a manager. + modifier onlyManagers() { + _isManager(); + _; + } + + modifier onlyKeepers() { + _isKeeper(); + _; + } + + /// @notice Check is either factories governance or local manager. + function _isManager() internal view virtual { + require(managers[msg.sender] || msg.sender == governance, "!manager"); + } + + function _isKeeper() internal view virtual { + require(keepers[msg.sender], "!keeper"); + } + + uint256 internal constant MAX_BPS = 10_000; + + /// @notice If the allocator is currently in paused state. + bool public paused; + + /// @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 totalDebtRatio; + + /// @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 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 keepers; + + /// @notice Mapping of addresses that are allowed to update debt ratios. + 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); + } + + /** + * @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 { + 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; + // Default max base fee to uint256 max + maxAcceptableBaseFee = type(uint256).max; + } + + /** + * @notice Debt update wrapper for the vault. + * @dev This can be used if a minimum time between debt updates + * is desired to be used for the trigger and to enforce a max loss. + * + * This contract must have the DEBT_MANAGER role assigned to them. + * + * The function signature matches the vault so no update to the + * call data is required. + * + * This will also run checks on losses realized during debt + * updates to assure decreases did not realize profits outside + * of the allowed range. + */ + function update_debt( + address _strategy, + uint256 _targetDebt + ) public virtual onlyKeepers { + IVault _vault = IVault(vault); + + // If going to 0 record full balance first. + if (_targetDebt == 0) { + _vault.process_report(_strategy); + } + + // Update debt with the default max loss. + _vault.update_debt(_strategy, _targetDebt, maxDebtUpdateLoss); + + // Update the last time the strategies debt was updated. + _configs[_strategy].lastUpdate = uint96(block.timestamp); + } + + /** + * @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 _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 _strategy + ) public view virtual returns (bool, bytes memory) { + if (paused) return (false, bytes("paused")); + + // Get the strategy specific debt config. + Config memory config = getConfig(_strategy); + + // Make sure the strategy has been added to the allocator. + if (!config.added) return (false, bytes("!added")); + + // Check the base fee isn't too high. + if (!isCurrentBaseFeeAcceptable()) { + return (false, bytes("Base Fee")); + } + + // Cache the vault variable. + IVault _vault = IVault(vault); + // Retrieve the strategy specific parameters. + IVault.StrategyParams memory params = _vault.strategies(_strategy); + // Make sure its an active strategy. + require(params.activation != 0, "!active"); + + if (block.timestamp - config.lastUpdate <= minimumWait) { + return (false, bytes("min wait")); + } + + uint256 vaultAssets = _vault.totalAssets(); + + // Get the target debt for the strategy based on vault assets. + uint256 targetDebt = Math.min( + (vaultAssets * config.targetRatio) / MAX_BPS, + // Make sure it is not more than the max allowed. + params.max_debt + ); + + // Get the max debt we would want the strategy to have. + uint256 maxDebt = Math.min( + (vaultAssets * config.maxRatio) / MAX_BPS, + // Make sure it is not more than the max allowed. + params.max_debt + ); + + // If we need to add more. + if (targetDebt > params.current_debt) { + uint256 currentIdle = _vault.totalIdle(); + uint256 minIdle = _vault.minimum_total_idle(); + + // We can't add more than the available idle. + if (minIdle >= currentIdle) { + return (false, bytes("No Idle")); + } + + // Add up to the max if possible + uint256 toAdd = Math.min( + maxDebt - params.current_debt, + // Can't take more than is available. + Math.min( + currentIdle - minIdle, + IVault(_strategy).maxDeposit(vault) + ) + ); + + // If the amount to add is over our threshold. + if (toAdd > minimumChange) { + // Return true and the calldata. + return ( + true, + abi.encodeWithSignature( + "update_debt(address,uint256)", + _strategy, + params.current_debt + toAdd + ) + ); + } + // If current debt is greater than our max. + } else if (maxDebt < params.current_debt) { + uint256 toPull = params.current_debt - targetDebt; + + uint256 currentIdle = _vault.totalIdle(); + uint256 minIdle = _vault.minimum_total_idle(); + if (minIdle > currentIdle) { + // Pull at least the amount needed for minIdle. + toPull = Math.max(toPull, minIdle - currentIdle); + } + + // Find out by how much. Aim for the target. + toPull = Math.min( + toPull, + // Account for the current liquidity constraints. + // Use max redeem to match vault logic. + IVault(_strategy).convertToAssets( + IVault(_strategy).maxRedeem(address(_vault)) + ) + ); + + // Check if it's over the threshold. + if (toPull > minimumChange) { + // Can't lower debt if there are unrealised losses. + if ( + _vault.assess_share_of_unrealised_losses( + _strategy, + params.current_debt + ) != 0 + ) { + return (false, bytes("unrealised loss")); + } + + // If so return true and the calldata. + return ( + true, + abi.encodeWithSignature( + "update_debt(address,uint256)", + _strategy, + params.current_debt - toPull + ) + ); + } + } + + // Either no change or below our minimumChange. + 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 20% increase for max debt. + * + * @param _strategy Address of the strategy to set. + * @param _targetRatio Amount in Basis points to allocate. + */ + function setStrategyDebtRatio( + address _strategy, + uint256 _targetRatio + ) public virtual { + uint256 maxRatio = Math.min((_targetRatio * 12_000) / MAX_BPS, MAX_BPS); + setStrategyDebtRatio(_strategy, _targetRatio, maxRatio); + } + + /** + * @notice Sets a new target debt ratio for a strategy. + * @dev A `minimumChange` for that strategy must be set first. + * This is to prevent debt from being updated too frequently. + * + * @param _strategy Address of the strategy to set. + * @param _targetRatio Amount in Basis points to allocate. + * @param _maxRatio Max ratio to give on debt increases. + */ + function setStrategyDebtRatio( + address _strategy, + uint256 _targetRatio, + uint256 _maxRatio + ) public virtual onlyManagers { + // Make sure a minimumChange has been set. + require(minimumChange != 0, "!minimum"); + // Cannot be more than 100%. + require(_maxRatio <= MAX_BPS, "max too high"); + // Max cannot be lower than the target. + require(_maxRatio >= _targetRatio, "max ratio"); + + // Get the current config. + Config memory config = getConfig(_strategy); + + // Set added flag if not set yet. + if (!config.added) { + config.added = true; + emit StrategyChanged(_strategy, Status.ADDED); + } + + // Get what will be the new total debt ratio. + uint256 newTotalDebtRatio = totalDebtRatio - + config.targetRatio + + _targetRatio; + + // Make sure it is under 100% allocated + require(newTotalDebtRatio <= MAX_BPS, "ratio too high"); + + // Update local config. + config.targetRatio = uint16(_targetRatio); + config.maxRatio = uint16(_maxRatio); + + // Write to storage. + _configs[_strategy] = config; + totalDebtRatio = newTotalDebtRatio; + + emit UpdateStrategyDebtRatio( + _strategy, + _targetRatio, + _maxRatio, + newTotalDebtRatio + ); + } + + /** + * @notice Remove a strategy from this debt allocator. + * @dev Will delete the full config for the strategy + * @param _strategy Address of the address ro remove. + */ + function removeStrategy(address _strategy) external virtual onlyManagers { + Config memory config = getConfig(_strategy); + require(config.added, "!added"); + + uint256 target = config.targetRatio; + + // Remove any debt ratio the strategy holds. + if (target != 0) { + totalDebtRatio -= target; + emit UpdateStrategyDebtRatio(_strategy, 0, 0, totalDebtRatio); + } + + // Remove the full config including the `added` flag. + delete _configs[_strategy]; + + // Emit Event. + emit StrategyChanged(_strategy, Status.REMOVED); + } + + /** + * @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 + * added or pulled for it to trigger an update. + * + * @param _minimumChange The new minimum to set for the strategy. + */ + function setMinimumChange( + uint256 _minimumChange + ) external virtual onlyGovernance { + require(_minimumChange > 0, "zero"); + // Set the new minimum. + minimumChange = _minimumChange; + + emit UpdateMinimumChange(_minimumChange); + } + + /** + * @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); + } + + /** + * @notice Set the minimum time to wait before re-updating a strategies debt. + * @dev This is only enforced per strategy. + * @param _minimumWait The minimum time in seconds to wait. + */ + function setMinimumWait( + uint256 _minimumWait + ) external virtual onlyGovernance { + minimumWait = _minimumWait; + + 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. + * @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 Allows governance to pause the triggers. + * @param _status Status to set the `paused` bool to. + */ + function setPaused(bool _status) external virtual onlyGovernance { + require(_status != paused, "already set"); + paused = _status; + + emit UpdatePaused(_status); + } + + /** + * @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. + * @return The strategies current targetRatio. + */ + function getStrategyTargetRatio( + address _strategy + ) external view virtual returns (uint256) { + return getConfig(_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) { + return getConfig(_strategy).maxRatio; + } + + /** + * @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() public view virtual returns (bool) { + return maxAcceptableBaseFee >= block.basefee; + } +} diff --git a/contracts/debtAllocators/Generic/GenericDebtAllocatorFactory.sol b/contracts/debtAllocators/Generic/GenericDebtAllocatorFactory.sol new file mode 100644 index 0000000..c1f172c --- /dev/null +++ b/contracts/debtAllocators/Generic/GenericDebtAllocatorFactory.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity >=0.8.18; + +import {GenericDebtAllocator} from "./GenericDebtAllocator.sol"; +import {Clonable} from "@periphery/utils/Clonable.sol"; +import {Governance} from "@periphery/utils/Governance.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. + */ +contract GenericDebtAllocatorFactory is Clonable { + event NewDebtAllocator(address indexed allocator, address indexed vault); + + constructor() { + original = address(new GenericDebtAllocator(address(1), address(2), 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. + * @return newAllocator Address of the new generic debt allocator + */ + function newGenericDebtAllocator( + address _vault, + address _governance, + uint256 _minimumChange + ) public virtual returns (address newAllocator) { + newAllocator = _clone(); + + GenericDebtAllocator(newAllocator).initialize( + _vault, + _governance, + _minimumChange + ); + + emit NewDebtAllocator(newAllocator, _vault); + } +} diff --git a/tests/conftest.py b/tests/conftest.py index 7a190ec..16ed48f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -485,3 +485,33 @@ def splitter(daddy, management, brain, splitter_factory): splitter = project.Splitter.at(event.splitter) yield splitter + + +@pytest.fixture(scope="session") +def deploy_generic_allocator_factory(project, daddy, brain): + def deploy_generic_allocator_factory(gov=daddy): + generic_allocator_factory = gov.deploy(project.GenericDebtAllocatorFactory) + + return generic_allocator_factory + + yield deploy_generic_allocator_factory + + +@pytest.fixture(scope="session") +def generic_allocator_factory(deploy_generic_allocator_factory): + generic_allocator_factory = deploy_generic_allocator_factory() + + yield generic_allocator_factory + + +@pytest.fixture(scope="session") +def generic_allocator(generic_allocator_factory, project, vault, daddy, brain): + tx = generic_allocator_factory.newGenericDebtAllocator( + vault, brain, int(1e18), sender=daddy + ) + + event = list(tx.decode_logs(generic_allocator_factory.NewDebtAllocator))[0] + + generic_allocator = project.GenericDebtAllocator.at(event.allocator) + + yield generic_allocator diff --git a/tests/debtAllocators/test_generic.py b/tests/debtAllocators/test_generic.py new file mode 100644 index 0000000..f35db58 --- /dev/null +++ b/tests/debtAllocators/test_generic.py @@ -0,0 +1,573 @@ +import ape +from ape import chain, project +from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES + + +def test_setup(generic_allocator_factory, brain, user, strategy, vault): + tx = generic_allocator_factory.newGenericDebtAllocator(vault, brain, 0, sender=user) + + events = list(tx.decode_logs(generic_allocator_factory.NewDebtAllocator)) + + assert len(events) == 1 + assert events[0].vault == vault.address + + generic_allocator = project.GenericDebtAllocator.at(events[0].allocator) + + assert generic_allocator.maxAcceptableBaseFee() == MAX_INT + assert generic_allocator.keepers(brain) == True + assert generic_allocator.managers(brain) == False + assert generic_allocator.vault() == vault.address + assert generic_allocator.getConfig(strategy) == (False, 0, 0, 0, 0) + assert generic_allocator.totalDebtRatio() == 0 + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("!added").encode("utf-8") + + +def test_set_keepers(generic_allocator, brain, user): + assert generic_allocator.keepers(brain) == True + assert generic_allocator.keepers(user) == False + + with ape.reverts("!governance"): + generic_allocator.setKeeper(user, True, sender=user) + + tx = generic_allocator.setKeeper(user, True, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateKeeper))[0] + + assert event.keeper == user + assert event.allowed == True + assert generic_allocator.keepers(user) == True + + tx = generic_allocator.setKeeper(brain, False, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateKeeper))[0] + + assert event.keeper == brain + assert event.allowed == False + assert generic_allocator.keepers(brain) == False + + +def test_set_managers(generic_allocator, brain, user): + assert generic_allocator.managers(brain) == False + assert generic_allocator.managers(user) == False + + with ape.reverts("!governance"): + generic_allocator.setManager(user, True, sender=user) + + tx = generic_allocator.setManager(user, True, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateManager))[0] + + assert event.manager == user + assert event.allowed == True + assert generic_allocator.managers(user) == True + + tx = generic_allocator.setManager(user, False, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateManager))[0] + + assert event.manager == user + assert event.allowed == False + assert generic_allocator.managers(user) == False + + +def test_set_minimum_change(generic_allocator, brain, strategy, user): + assert generic_allocator.getConfig(strategy) == (False, 0, 0, 0, 0) + assert generic_allocator.minimumChange() != 0 + + minimum = int(1e17) + + with ape.reverts("!governance"): + generic_allocator.setMinimumChange(minimum, sender=user) + + with ape.reverts("zero"): + generic_allocator.setMinimumChange(0, sender=brain) + + tx = generic_allocator.setMinimumChange(minimum, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateMinimumChange))[0] + + assert event.newMinimumChange == minimum + assert generic_allocator.minimumChange() == minimum + + +def test_set_minimum_wait(generic_allocator, brain, strategy, user): + assert generic_allocator.getConfig(strategy) == (False, 0, 0, 0, 0) + assert generic_allocator.minimumWait() == 0 + + minimum = int(1e17) + + with ape.reverts("!governance"): + generic_allocator.setMinimumWait(minimum, sender=user) + + tx = generic_allocator.setMinimumWait(minimum, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateMinimumWait))[0] + + assert event.newMinimumWait == minimum + assert generic_allocator.minimumWait() == minimum + + +def test_set_max_debt_update_loss(generic_allocator, brain, strategy, user): + assert generic_allocator.getConfig(strategy) == (False, 0, 0, 0, 0) + assert generic_allocator.maxDebtUpdateLoss() == 1 + + max = int(69) + + with ape.reverts("!governance"): + generic_allocator.setMaxDebtUpdateLoss(max, sender=user) + + with ape.reverts("higher than max"): + generic_allocator.setMaxDebtUpdateLoss(10_001, sender=brain) + + tx = generic_allocator.setMaxDebtUpdateLoss(max, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateMaxDebtUpdateLoss))[0] + + assert event.newMaxDebtUpdateLoss == max + assert generic_allocator.maxDebtUpdateLoss() == max + + +def test_set_ratios( + generic_allocator, brain, daddy, vault, strategy, create_strategy, user +): + assert generic_allocator.getConfig(strategy) == (False, 0, 0, 0, 0) + + minimum = int(1e17) + max = int(6_000) + target = int(5_000) + + with ape.reverts("!manager"): + generic_allocator.setStrategyDebtRatio(strategy, target, max, sender=user) + + vault.add_strategy(strategy.address, sender=daddy) + + with ape.reverts("max too high"): + generic_allocator.setStrategyDebtRatio( + strategy, target, int(10_001), sender=brain + ) + + with ape.reverts("max ratio"): + generic_allocator.setStrategyDebtRatio( + strategy, int(max + 1), max, sender=brain + ) + + tx = generic_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) + + event = list(tx.decode_logs(generic_allocator.StrategyChanged))[0] + + assert event.strategy == strategy + assert event.status == 1 + + event = list(tx.decode_logs(generic_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_allocator.totalDebtRatio() == target + assert generic_allocator.getConfig(strategy) == (True, target, max, 0, 0) + + new_strategy = create_strategy() + vault.add_strategy(new_strategy, sender=daddy) + with ape.reverts("ratio too high"): + generic_allocator.setStrategyDebtRatio( + new_strategy, int(10_000), int(10_000), sender=brain + ) + + target = int(8_000) + tx = generic_allocator.setStrategyDebtRatio(strategy, target, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == target * 1.2 + assert event.newTotalDebtRatio == target + assert generic_allocator.totalDebtRatio() == target + assert generic_allocator.getConfig(strategy) == (True, target, target * 1.2, 0, 0) + + +def test_increase_debt_ratio( + generic_allocator, brain, daddy, vault, strategy, create_strategy, user +): + assert generic_allocator.getConfig(strategy) == (False, 0, 0, 0, 0) + + minimum = int(1e17) + target = int(5_000) + increase = int(5_000) + max = target * 1.2 + + with ape.reverts("!manager"): + generic_allocator.increaseStrategyDebtRatio(strategy, increase, sender=user) + + vault.add_strategy(strategy.address, sender=daddy) + + tx = generic_allocator.increaseStrategyDebtRatio(strategy, increase, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_allocator.totalDebtRatio() == target + assert generic_allocator.getConfig(strategy) == (True, target, max, 0, 0) + + new_strategy = create_strategy() + vault.add_strategy(new_strategy, sender=daddy) + + with ape.reverts("ratio too high"): + generic_allocator.increaseStrategyDebtRatio( + new_strategy, int(5_001), sender=brain + ) + + target = int(8_000) + max = target * 1.2 + increase = int(3_000) + tx = generic_allocator.increaseStrategyDebtRatio(strategy, increase, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_allocator.totalDebtRatio() == target + assert generic_allocator.getConfig(strategy) == (True, target, max, 0, 0) + + target = int(10_000) + max = int(10_000) + increase = int(2_000) + tx = generic_allocator.increaseStrategyDebtRatio(strategy, increase, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_allocator.totalDebtRatio() == target + assert generic_allocator.getConfig(strategy) == (True, target, max, 0, 0) + + +def test_decrease_debt_ratio( + generic_allocator, brain, vault, strategy, daddy, create_strategy, user +): + assert generic_allocator.getConfig(strategy) == (False, 0, 0, 0, 0) + + minimum = int(1e17) + target = int(5_000) + max = target * 1.2 + + vault.add_strategy(strategy.address, sender=daddy) + generic_allocator.setMinimumChange(minimum, sender=brain) + + # Underflow + with ape.reverts(): + generic_allocator.decreaseStrategyDebtRatio(strategy, target, sender=brain) + + # Add the target + tx = generic_allocator.increaseStrategyDebtRatio(strategy, target, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_allocator.totalDebtRatio() == target + assert generic_allocator.getConfig(strategy) == (True, target, max, 0, 0) + + target = int(2_000) + max = target * 1.2 + decrease = int(3_000) + + with ape.reverts("!manager"): + generic_allocator.decreaseStrategyDebtRatio(strategy, decrease, sender=user) + + tx = generic_allocator.decreaseStrategyDebtRatio(strategy, decrease, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_allocator.totalDebtRatio() == target + assert generic_allocator.getConfig(strategy) == (True, target, max, 0, 0) + + target = int(0) + max = int(0) + decrease = int(2_000) + tx = generic_allocator.decreaseStrategyDebtRatio(strategy, decrease, sender=brain) + + event = list(tx.decode_logs(generic_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_allocator.totalDebtRatio() == target + assert generic_allocator.getConfig(strategy) == (True, 0, 0, 0, 0) + + +def test_remove_strategy( + generic_allocator, brain, vault, strategy, daddy, user, deposit_into_vault, amount +): + assert generic_allocator.getConfig(strategy) == (False, 0, 0, 0, 0) + + minimum = int(1) + max = int(6_000) + target = int(5_000) + + vault.add_strategy(strategy.address, sender=daddy) + + generic_allocator.setMinimumChange(minimum, sender=brain) + + tx = generic_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) + + event = list(tx.decode_logs(generic_allocator.StrategyChanged))[0] + + assert event.strategy == strategy + assert event.status == 1 + + event = list(tx.decode_logs(generic_allocator.UpdateStrategyDebtRatio))[0] + + assert event.newTargetRatio == target + assert event.newMaxRatio == max + assert event.newTotalDebtRatio == target + assert generic_allocator.totalDebtRatio() == target + assert generic_allocator.getConfig(strategy) == (True, target, max, 0, 0) + + deposit_into_vault(vault, amount) + vault.update_max_debt_for_strategy(strategy, MAX_INT, sender=daddy) + + print(generic_allocator.shouldUpdateDebt(strategy)) + assert generic_allocator.shouldUpdateDebt(strategy)[0] == True + + with ape.reverts("!manager"): + generic_allocator.removeStrategy(strategy, sender=user) + + tx = generic_allocator.removeStrategy(strategy, sender=brain) + + event = list(tx.decode_logs(generic_allocator.StrategyChanged)) + + assert len(event) == 1 + assert event[0].strategy == strategy + assert event[0].status == 2 + assert generic_allocator.totalDebtRatio() == 0 + assert generic_allocator.getConfig(strategy) == (False, 0, 0, 0, 0) + assert generic_allocator.shouldUpdateDebt(strategy)[0] == False + + +def test_should_update_debt( + generic_allocator, vault, strategy, brain, daddy, deposit_into_vault, amount +): + assert generic_allocator.getConfig(strategy.address) == (False, 0, 0, 0, 0) + vault.add_role(generic_allocator, ROLES.DEBT_MANAGER, sender=daddy) + + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("!added").encode("utf-8") + + vault.add_strategy(strategy.address, sender=daddy) + + minimum = int(1) + target = int(5_000) + max = int(5_000) + + generic_allocator.setMinimumChange(minimum, sender=brain) + generic_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) + + # Vault has no assets so should be false. + (bool, bytes) = generic_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_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_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_allocator.update_debt(strategy, amount // 2, sender=brain) + chain.mine(1) + + # Should now be false again once allocated + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("Below Min").encode("utf-8") + + # Update the ratio to make true + generic_allocator.setStrategyDebtRatio( + strategy, int(target + 1), int(target + 1), sender=brain + ) + + (bool, bytes) = generic_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_allocator.setMinimumWait(MAX_INT, sender=brain) + # Should now be false + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("min wait").encode("utf-8") + + generic_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_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_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_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_allocator.setMinimumChange(int(1e30), sender=brain) + + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("Below Min").encode("utf-8") + + # Lower the target and minimum + generic_allocator.setMinimumChange(int(1), sender=brain) + generic_allocator.setStrategyDebtRatio( + strategy, int(target // 2), int(target // 2), sender=brain + ) + + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy.address, amount // 4) + + +def test_update_debt( + generic_allocator, + vault, + strategy, + brain, + daddy, + user, + deposit_into_vault, + amount, +): + assert generic_allocator.getConfig(strategy) == (False, 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_allocator.update_debt(strategy, amount, sender=user) + + # This reverts by the vault + with ape.reverts("not allowed"): + generic_allocator.update_debt(strategy, amount, sender=brain) + + vault.add_role( + generic_allocator, + ROLES.DEBT_MANAGER | ROLES.REPORTING_MANAGER, + sender=daddy, + ) + + generic_allocator.update_debt(strategy, amount, sender=brain) + + timestamp = generic_allocator.getConfig(strategy)[3] + assert timestamp != 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == amount + + generic_allocator.setKeeper(user, True, sender=brain) + + generic_allocator.update_debt(strategy, 0, sender=user) + + assert generic_allocator.getConfig(strategy)[2] != timestamp + assert vault.totalIdle() == amount + assert vault.totalDebt() == 0 + + +def test_pause( + generic_allocator, vault, strategy, brain, daddy, user, deposit_into_vault, amount +): + assert generic_allocator.paused() == False + assert generic_allocator.getConfig(strategy.address) == (False, 0, 0, 0, 0) + vault.add_role(generic_allocator, ROLES.DEBT_MANAGER, sender=daddy) + + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("!added").encode("utf-8") + + vault.add_strategy(strategy.address, sender=daddy) + + minimum = int(1) + target = int(5_000) + max = int(5_000) + + generic_allocator.setMinimumChange(minimum, sender=brain) + generic_allocator.setStrategyDebtRatio(strategy, target, max, sender=brain) + deposit_into_vault(vault, amount) + vault.update_max_debt_for_strategy(strategy, MAX_INT, sender=daddy) + + # Should now want to allocate 50% + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy.address, amount // 2) + + # Pause the allocator + + # with ape.reverts("!governance"): + # generic_allocator.setPaused(True, sender=user) + + tx = generic_allocator.setPaused(True, sender=brain) + + assert generic_allocator.paused() == True + + events = list(tx.decode_logs(generic_allocator.UpdatePaused)) + + assert len(events) == 1 + assert events[0].status == True + + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == False + assert bytes == ("paused").encode("utf-8") + + # Unpause + tx = generic_allocator.setPaused(False, sender=brain) + + assert generic_allocator.paused() == False + events = list(tx.decode_logs(generic_allocator.UpdatePaused)) + + assert len(events) == 1 + assert events[0].status == False + + (bool, bytes) = generic_allocator.shouldUpdateDebt(strategy.address) + assert bool == True + assert bytes == vault.update_debt.encode_input(strategy.address, amount // 2)