From 934d375f09238a21d0ac1417e0aac773ac161b33 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 14 Nov 2023 09:42:13 -0700 Subject: [PATCH] feat: check debt loss --- .../debtAllocators/GenericDebtAllocator.sol | 73 ++++++++++++++----- tests/test_generic_debt_allocator.py | 51 +++++++++++-- 2 files changed, 100 insertions(+), 24 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index 4209beb..f950100 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -29,22 +29,25 @@ import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; * more than its `maxRatio`. */ contract GenericDebtAllocator is Governance { - /// @notice An event emitted when a strategies debt ratios are updated. - event UpdatedStrategyDebtRatios( + /// @notice An event emitted when a strategies debt ratios are Updated. + event UpdateStrategyDebtRatios( address indexed strategy, - uint256 targetRatio, - uint256 maxRatio, - uint256 totalDebtRatio + uint256 newTargetRatio, + uint256 newMaxRatio, + uint256 newTotalDebtRatio ); - /// @notice An event emitted when the minimum change is updated. - event UpdatedMinimumChange(uint256 minimumChange); + /// @notice An event emitted when the minimum change is Updated. + event UpdateMinimumChange(uint256 newMinimumChange); - /// @notice An event emitted when the max base fee is updated. - event UpdatedMaxAcceptableBaseFee(uint256 maxAcceptableBaseFee); + /// @notice An event emitted when the max base fee is Updated. + event UpdateMaxAcceptableBaseFee(uint256 newMaxAcceptableBaseFee); - /// @notice An event emitted when the minimum time to wait is updated. - event UpdatedMinimumWait(uint256 newMinimumWait); + /// @notice An event emitted when the max debt update loss is Updated. + event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss); + + /// @notice An event emitted when the minimum time to wait is Updated. + event UpdateMinimumWait(uint256 newMinimumWait); /// @notice Struct for each strategies info. struct Config { @@ -80,6 +83,9 @@ contract GenericDebtAllocator is Governance { /// @notice Time to wait between debt updates. uint256 public minimumWait; + /// @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; @@ -110,6 +116,8 @@ contract GenericDebtAllocator is Governance { minimumChange = _minimumChange; // Default max base fee to uint256 max maxAcceptableBaseFee = type(uint256).max; + // Default max loss on debt updates to 1 BP. + maxDebtUpdateLoss = 1; } /** @@ -122,6 +130,10 @@ contract GenericDebtAllocator is Governance { * * 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, @@ -129,10 +141,22 @@ contract GenericDebtAllocator is Governance { ) external virtual { IVault _vault = IVault(vault); require( - (IVault(_vault).roles(msg.sender) & DEBT_MANAGER) == DEBT_MANAGER, + (_vault.roles(msg.sender) & DEBT_MANAGER) == DEBT_MANAGER, "not allowed" ); - IVault(_vault).update_debt(_strategy, _targetDebt); + uint256 initialAssets = _vault.totalAssets(); + _vault.update_debt(_strategy, _targetDebt); + uint256 afterAssets = _vault.totalAssets(); + + // If a loss was realized. + if (afterAssets < initialAssets) { + // Make sure its within the range. + require( + initialAssets - afterAssets <= + (initialAssets * maxDebtUpdateLoss) / MAX_BPS, + "too much loss" + ); + } configs[_strategy].lastUpdate = block.timestamp; } @@ -291,7 +315,7 @@ contract GenericDebtAllocator is Governance { debtRatio = newDebtRatio; - emit UpdatedStrategyDebtRatios( + emit UpdateStrategyDebtRatios( _strategy, _targetRatio, _maxRatio, @@ -313,7 +337,22 @@ contract GenericDebtAllocator is Governance { // Set the new minimum. minimumChange = _minimumChange; - emit UpdatedMinimumChange(_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); } /** @@ -326,7 +365,7 @@ contract GenericDebtAllocator is Governance { ) external virtual onlyGovernance { minimumWait = _minimumWait; - emit UpdatedMinimumWait(_minimumWait); + emit UpdateMinimumWait(_minimumWait); } /** @@ -343,6 +382,6 @@ contract GenericDebtAllocator is Governance { ) external virtual onlyGovernance { maxAcceptableBaseFee = _maxAcceptableBaseFee; - emit UpdatedMaxAcceptableBaseFee(_maxAcceptableBaseFee); + emit UpdateMaxAcceptableBaseFee(_maxAcceptableBaseFee); } } diff --git a/tests/test_generic_debt_allocator.py b/tests/test_generic_debt_allocator.py index d421245..dc7a6a9 100644 --- a/tests/test_generic_debt_allocator.py +++ b/tests/test_generic_debt_allocator.py @@ -23,7 +23,7 @@ def test_setup(generic_debt_allocator_factory, user, strategy, vault): generic_debt_allocator.shouldUpdateDebt(strategy) -def test_set_minimum(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.minimumChange() == 0 @@ -37,12 +37,49 @@ def test_set_minimum(generic_debt_allocator, daddy, vault, strategy, user): tx = generic_debt_allocator.setMinimumChange(minimum, sender=daddy) - event = list(tx.decode_logs(generic_debt_allocator.UpdatedMinimumChange))[0] + event = list(tx.decode_logs(generic_debt_allocator.UpdateMinimumChange))[0] - assert event.minimumChange == minimum + assert event.newMinimumChange == minimum assert generic_debt_allocator.minimumChange() == minimum +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.minimumWait() == 0 + + minimum = int(1e17) + + with ape.reverts("!governance"): + generic_debt_allocator.setMinimumWait(minimum, sender=user) + + tx = generic_debt_allocator.setMinimumWait(minimum, sender=daddy) + + 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, daddy, vault, strategy, user): + assert generic_debt_allocator.configs(strategy) == (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=daddy) + + tx = generic_debt_allocator.setMaxDebtUpdateLoss(max, sender=daddy) + + 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, daddy, vault, strategy, create_strategy, user ): @@ -83,11 +120,11 @@ def test_set_ratios( strategy, target, max, sender=daddy ) - event = list(tx.decode_logs(generic_debt_allocator.UpdatedStrategyDebtRatios))[0] + event = list(tx.decode_logs(generic_debt_allocator.UpdateStrategyDebtRatios))[0] - assert event.targetRatio == target - assert event.maxRatio == max - assert event.totalDebtRatio == target + 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)