diff --git a/contracts/debtAllocators/DebtOptimizerApplicator.sol b/contracts/debtAllocators/DebtOptimizerApplicator.sol new file mode 100644 index 0000000..85eccff --- /dev/null +++ b/contracts/debtAllocators/DebtOptimizerApplicator.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity >=0.8.18; + +import {DebtAllocator, DebtAllocatorFactory} from "./DebtAllocator.sol"; + +contract DebtOptimizerApplicator { + /// @notice An event emitted when a keeper is added or removed. + event UpdateManager(address indexed manager, bool allowed); + + /// @notice struct for debt ratio changes + struct StrategyDebtRatio { + address strategy; + uint256 targetRatio; + uint256 maxRatio; + } + + /// @notice Make sure the caller is governance. + modifier onlyGovernance() { + _isGovernance(); + _; + } + + /// @notice Make sure the caller is governance or a manager. + modifier onlyManagers() { + _isManager(); + _; + } + + /// @notice Check the Factories governance address. + function _isGovernance() internal view virtual { + require( + msg.sender == + DebtAllocatorFactory(debtAllocatorFactory).governance(), + "!governance" + ); + } + + /// @notice Check is either factories governance or local manager. + function _isManager() internal view virtual { + require( + managers[msg.sender] || + msg.sender == + DebtAllocatorFactory(debtAllocatorFactory).governance(), + "!manager" + ); + } + + /// @notice The address of the debt allocator factory to use for some role checks. + address public immutable debtAllocatorFactory; + + /// @notice Mapping of addresses that are allowed to update debt ratios. + mapping(address => bool) public managers; + + constructor(address _debtAllocatorFactory) { + debtAllocatorFactory = _debtAllocatorFactory; + } + + /** + * @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); + } + + function setStrategyDebtRatios( + address _debtAllocator, + StrategyDebtRatio[] calldata _strategyDebtRatios + ) public onlyManagers { + for (uint8 i; i < _strategyDebtRatios.length; ++i) { + if (_strategyDebtRatios[i].maxRatio == 0) { + DebtAllocator(_debtAllocator).setStrategyDebtRatio( + _strategyDebtRatios[i].strategy, + _strategyDebtRatios[i].targetRatio + ); + } else { + DebtAllocator(_debtAllocator).setStrategyDebtRatio( + _strategyDebtRatios[i].strategy, + _strategyDebtRatios[i].targetRatio, + _strategyDebtRatios[i].maxRatio + ); + } + } + } +} diff --git a/tests/conftest.py b/tests/conftest.py index f3561d8..7a190ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -414,6 +414,12 @@ def debt_allocator(debt_allocator_factory, project, vault, daddy): yield debt_allocator +@pytest.fixture(scope="session") +def debt_optimizer_applicator(debt_allocator_factory, project, brain): + + yield brain.deploy(project.DebtOptimizerApplicator, debt_allocator_factory.address) + + @pytest.fixture(scope="session") def deploy_role_manager( project, daddy, brain, security, keeper, strategy_manager, registry diff --git a/tests/debtAllocators/test_debt_optimizer_applicator.py b/tests/debtAllocators/test_debt_optimizer_applicator.py new file mode 100644 index 0000000..440d39c --- /dev/null +++ b/tests/debtAllocators/test_debt_optimizer_applicator.py @@ -0,0 +1,113 @@ +import ape +from ape import chain, project +from utils.constants import MAX_INT, ROLES + + +def test_setup(debt_optimizer_applicator, debt_allocator_factory, brain): + + assert debt_optimizer_applicator.managers(brain) == False + assert ( + debt_optimizer_applicator.debtAllocatorFactory() + == debt_allocator_factory.address + ) + + +def test_set_managers(debt_optimizer_applicator, brain, user): + assert debt_optimizer_applicator.managers(brain) == False + assert debt_optimizer_applicator.managers(user) == False + + with ape.reverts("!governance"): + debt_optimizer_applicator.setManager(user, True, sender=user) + + tx = debt_optimizer_applicator.setManager(user, True, sender=brain) + + event = list(tx.decode_logs(debt_optimizer_applicator.UpdateManager))[0] + + assert event.manager == user + assert event.allowed == True + assert debt_optimizer_applicator.managers(user) == True + + tx = debt_optimizer_applicator.setManager(user, False, sender=brain) + + event = list(tx.decode_logs(debt_optimizer_applicator.UpdateManager))[0] + + assert event.manager == user + assert event.allowed == False + assert debt_optimizer_applicator.managers(user) == False + + +def test_set_ratios( + debt_optimizer_applicator, + debt_allocator, + brain, + daddy, + vault, + strategy, + create_strategy, + user, +): + max = int(6_000) + target = int(5_000) + strategy_debt_ratio = (strategy.address, target, max) + + debt_allocator.setManager(debt_optimizer_applicator, True, sender=brain) + debt_allocator.setMinimumChange(1, sender=brain) + + with ape.reverts("!manager"): + debt_optimizer_applicator.setStrategyDebtRatios( + debt_allocator, [strategy_debt_ratio], sender=user + ) + + vault.add_strategy(strategy.address, sender=daddy) + + tx = debt_optimizer_applicator.setStrategyDebtRatios( + debt_allocator, [strategy_debt_ratio], sender=brain + ) + + event = list(tx.decode_logs(debt_allocator.StrategyChanged))[0] + + assert event.strategy == strategy + assert event.status == 1 + + 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) == (True, target, max, 0, 0) + + new_strategy = create_strategy() + vault.add_strategy(new_strategy, sender=daddy) + + with ape.reverts("ratio too high"): + debt_optimizer_applicator.setStrategyDebtRatios( + debt_allocator, + [(new_strategy.address, int(10_000), int(10_000))], + sender=brain, + ) + + tx = debt_optimizer_applicator.setStrategyDebtRatios( + debt_allocator, + [ + (strategy.address, int(8_000), int(9_000)), + (new_strategy.address, int(2_000), int(0)), + ], + sender=brain, + ) + + events = list(tx.decode_logs(debt_allocator.UpdateStrategyDebtRatio)) + + assert len(events) == 2 + for event in events: + assert event.strategy in [strategy, new_strategy] + if event.strategy == strategy: + assert event.newTargetRatio == 8_000 + assert event.newMaxRatio == 9_000 + else: + assert event.newTargetRatio == 2_000 + assert event.newMaxRatio == 2_000 * 1.2 + + assert debt_allocator.totalDebtRatio() == 10_000 + assert debt_allocator.getConfig(strategy) == (True, 8_000, 9_000, 0, 0) + assert debt_allocator.getConfig(new_strategy) == (True, 2_000, 2_000 * 1.2, 0, 0)