From f4ba3917a96b07c8938252978351d78e311a1f34 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 3 Nov 2023 13:26:44 -0600 Subject: [PATCH 1/9] build: max debt in allocator --- contracts/debtAllocators/Allocator.sol | 300 +++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 contracts/debtAllocators/Allocator.sol diff --git a/contracts/debtAllocators/Allocator.sol b/contracts/debtAllocators/Allocator.sol new file mode 100644 index 0000000..5a374f0 --- /dev/null +++ b/contracts/debtAllocators/Allocator.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: GNU AGPLv3 +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"; + +/** + * @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 `minimumChange` and a `targetRatio`. + * + * 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. + */ +contract GenericDebtAllocator is Governance { + event SetTargetDebtRatio( + address indexed strategy, + uint256 targetRatio, + uint256 totalDebtRatio + ); + + event SetMinimumChange(uint256 minimumChange); + + event SetMaxAcceptableBaseFee(uint256 maxAcceptableBaseFee); + + event SetMinWait(uint256 newMinWait); + + // Struct for each strategies info. + struct Config { + // The percent in Basis Points the strategy should have. + uint256 targetRatio; + uint256 maxRatio; + uint256 lastUpdate; + } + + uint256 internal constant MAX_BPS = 10_000; + + // Mapping of strategy => its config. + mapping(address => Config) public configs; + + address public keeper; + + // Address of the vault this serves as allocator for. + address public vault; + + // Total debt ratio currently allocated in basis points. + // Can't be more than 10_000. + uint256 public debtRatio; + + // The minimum amount denominated in asset that will + // need to be moved to trigger a debt update. + uint256 public minimumChange; + + // 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; + + uint256 public minWait; + + constructor(address _vault, address _governance) Governance(_governance) { + initialize(_vault, _governance); + } + + /** + * @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. + */ + function initialize(address _vault, address _governance) public { + require(address(vault) == address(0), "!initialized"); + vault = _vault; + governance = _governance; + // Default max base fee to uint256 max + maxAcceptableBaseFee = type(uint256).max; + } + + function update_debt(address _strategy, uint256 _targetDebt) external { + require(msg.sender == keeper, "!keeper"); + IVault(vault).update_debt(_strategy, _targetDebt); + configs[_strategy].lastUpdate = 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. + * + * 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`. + */ + function shouldUpdateDebt( + address _strategy + ) external view returns (bool, bytes memory) { + // Check the base fee isn't too high. + if (block.basefee > maxAcceptableBaseFee) { + 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"); + + // Get the strategy specific debt config. + Config memory config = configs[_strategy]; + // Make sure we have a target debt. + require(config.targetRatio != 0, "no targetRatio"); + + if (block.timestamp - config.lastUpdate <= minWait) { + return (false, "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.encodeCall( + _vault.update_debt, + (_strategy, params.current_debt + toAdd) + ) + ); + } + // If target debt is lower than the current. + } else if (maxDebt < params.current_debt) { + // Find out by how much. + uint256 toPull = Math.min( + params.current_debt - targetDebt, + // Account for the current liquidity constraints. + IVault(_strategy).maxWithdraw(address(_vault)) + ); + + // Check if it's over the threshold. + if (toPull > minimumChange) { + // Can't lower debt if there is 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.encodeCall( + _vault.update_debt, + (_strategy, params.current_debt - toPull) + ) + ); + } + } + + // Either no change or below our minimumChange. + return (false, bytes("Below Min")); + } + + /** + * @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 setStrategyDebtRatios( + address _strategy, + uint256 _targetRatio, + uint256 _maxRatio + ) external onlyGovernance { + // Make sure the strategy is added to the vault. + require(IVault(vault).strategies(_strategy).activation != 0, "!active"); + // Make sure a minimumChange has been set. + require(minimumChange != 0, "!minimum"); + // Max can not be lower than the target. + require(_maxRatio >= _targetRatio, "max ratio"); + + // Get what will be the new total debt ratio. + uint256 newDebtRatio = debtRatio - + configs[_strategy].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; + debtRatio = newDebtRatio; + + emit SetTargetDebtRatio(_strategy, _targetRatio, newDebtRatio); + } + + /** + * @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 onlyGovernance { + require(minimumChange > 0, "zero"); + // Set the new minimum. + minimumChange = _minimumChange; + + emit SetMinimumChange(_minimumChange); + } + + function setKeeper(address _keeper) external onlyGovernance { + keeper = _keeper; + } + + /** + * @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 onlyGovernance { + maxAcceptableBaseFee = _maxAcceptableBaseFee; + + emit SetMaxAcceptableBaseFee(_maxAcceptableBaseFee); + } + + /** + * @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 _minWait The new max base fee. + */ + function setMinTime(uint256 _minWait) external onlyGovernance { + minWait = _minWait; + + emit SetMinWait(_minWait); + } +} From cdc35cb58d11123c8183b2dcc10c9fe736a69b5b Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 6 Nov 2023 20:14:50 -0700 Subject: [PATCH 2/9] test: test new allocator --- contracts/debtAllocators/Allocator.sol | 300 ------------------ .../debtAllocators/GenericDebtAllocator.sol | 148 +++++++-- .../GenericDebtAllocatorFactory.sol | 31 +- tests/test_generic_debt_allocator.py | 88 +++-- 4 files changed, 199 insertions(+), 368 deletions(-) delete mode 100644 contracts/debtAllocators/Allocator.sol diff --git a/contracts/debtAllocators/Allocator.sol b/contracts/debtAllocators/Allocator.sol deleted file mode 100644 index 5a374f0..0000000 --- a/contracts/debtAllocators/Allocator.sol +++ /dev/null @@ -1,300 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -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"; - -/** - * @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 `minimumChange` and a `targetRatio`. - * - * 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. - */ -contract GenericDebtAllocator is Governance { - event SetTargetDebtRatio( - address indexed strategy, - uint256 targetRatio, - uint256 totalDebtRatio - ); - - event SetMinimumChange(uint256 minimumChange); - - event SetMaxAcceptableBaseFee(uint256 maxAcceptableBaseFee); - - event SetMinWait(uint256 newMinWait); - - // Struct for each strategies info. - struct Config { - // The percent in Basis Points the strategy should have. - uint256 targetRatio; - uint256 maxRatio; - uint256 lastUpdate; - } - - uint256 internal constant MAX_BPS = 10_000; - - // Mapping of strategy => its config. - mapping(address => Config) public configs; - - address public keeper; - - // Address of the vault this serves as allocator for. - address public vault; - - // Total debt ratio currently allocated in basis points. - // Can't be more than 10_000. - uint256 public debtRatio; - - // The minimum amount denominated in asset that will - // need to be moved to trigger a debt update. - uint256 public minimumChange; - - // 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; - - uint256 public minWait; - - constructor(address _vault, address _governance) Governance(_governance) { - initialize(_vault, _governance); - } - - /** - * @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. - */ - function initialize(address _vault, address _governance) public { - require(address(vault) == address(0), "!initialized"); - vault = _vault; - governance = _governance; - // Default max base fee to uint256 max - maxAcceptableBaseFee = type(uint256).max; - } - - function update_debt(address _strategy, uint256 _targetDebt) external { - require(msg.sender == keeper, "!keeper"); - IVault(vault).update_debt(_strategy, _targetDebt); - configs[_strategy].lastUpdate = 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. - * - * 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`. - */ - function shouldUpdateDebt( - address _strategy - ) external view returns (bool, bytes memory) { - // Check the base fee isn't too high. - if (block.basefee > maxAcceptableBaseFee) { - 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"); - - // Get the strategy specific debt config. - Config memory config = configs[_strategy]; - // Make sure we have a target debt. - require(config.targetRatio != 0, "no targetRatio"); - - if (block.timestamp - config.lastUpdate <= minWait) { - return (false, "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.encodeCall( - _vault.update_debt, - (_strategy, params.current_debt + toAdd) - ) - ); - } - // If target debt is lower than the current. - } else if (maxDebt < params.current_debt) { - // Find out by how much. - uint256 toPull = Math.min( - params.current_debt - targetDebt, - // Account for the current liquidity constraints. - IVault(_strategy).maxWithdraw(address(_vault)) - ); - - // Check if it's over the threshold. - if (toPull > minimumChange) { - // Can't lower debt if there is 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.encodeCall( - _vault.update_debt, - (_strategy, params.current_debt - toPull) - ) - ); - } - } - - // Either no change or below our minimumChange. - return (false, bytes("Below Min")); - } - - /** - * @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 setStrategyDebtRatios( - address _strategy, - uint256 _targetRatio, - uint256 _maxRatio - ) external onlyGovernance { - // Make sure the strategy is added to the vault. - require(IVault(vault).strategies(_strategy).activation != 0, "!active"); - // Make sure a minimumChange has been set. - require(minimumChange != 0, "!minimum"); - // Max can not be lower than the target. - require(_maxRatio >= _targetRatio, "max ratio"); - - // Get what will be the new total debt ratio. - uint256 newDebtRatio = debtRatio - - configs[_strategy].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; - debtRatio = newDebtRatio; - - emit SetTargetDebtRatio(_strategy, _targetRatio, newDebtRatio); - } - - /** - * @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 onlyGovernance { - require(minimumChange > 0, "zero"); - // Set the new minimum. - minimumChange = _minimumChange; - - emit SetMinimumChange(_minimumChange); - } - - function setKeeper(address _keeper) external onlyGovernance { - keeper = _keeper; - } - - /** - * @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 onlyGovernance { - maxAcceptableBaseFee = _maxAcceptableBaseFee; - - emit SetMaxAcceptableBaseFee(_maxAcceptableBaseFee); - } - - /** - * @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 _minWait The new max base fee. - */ - function setMinTime(uint256 _minWait) external onlyGovernance { - minWait = _minWait; - - emit SetMinWait(_minWait); - } -} diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index 741c20d..b19cef7 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -16,35 +16,49 @@ import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; * * 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 `minimumChange` and a `targetRatio`. + * 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 { - event SetTargetDebtRatio( + event UpdatedStrategyDebtRatios( address indexed strategy, uint256 targetRatio, + uint256 maxRatio, uint256 totalDebtRatio ); - event SetMinimumChange(address indexed strategy, uint256 minimumChange); + event UpdatedMinimumChange(uint256 minimumChange); + + event UpdatedMaxAcceptableBaseFee(uint256 maxAcceptableBaseFee); - event SetMaxAcceptableBaseFee(uint256 maxAcceptableBaseFee); + event UpdatedMinimumWait(uint256 newMinimumWait); // Struct for each strategies info. struct Config { - // The percent in Basis Points the strategy should have. + // The ideal percent in Basis Points the strategy should have. uint256 targetRatio; - // The minimum amount denominated in asset that will - // need to be moved to trigger a debt update. - uint256 minimumChange; + // The max percent of assets the strategy should hold. + uint256 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; } uint256 internal constant MAX_BPS = 10_000; + // Vaults DEBT_MANAGER enumerator. + uint256 internal constant DEBT_MANAGER = 64; + // Mapping of strategy => its config. mapping(address => Config) public configs; @@ -55,12 +69,23 @@ contract GenericDebtAllocator is Governance { // Can't be more than 10_000. uint256 public debtRatio; + // The minimum amount denominated in asset that will + // need to be moved to trigger a debt update. + uint256 public minimumChange; + + // Time to wait between debt updates. + uint256 public minimumWait; + // 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; - constructor(address _vault, address _governance) Governance(_governance) { - initialize(_vault, _governance); + constructor( + address _vault, + address _governance, + uint256 _minimumChange + ) Governance(_governance) { + initialize(_vault, _governance, _minimumChange); } /** @@ -68,21 +93,48 @@ contract GenericDebtAllocator is Governance { * @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) public { + function initialize( + address _vault, + address _governance, + uint256 _minimumChange + ) public { require(address(vault) == address(0), "!initialized"); vault = _vault; governance = _governance; + minimumChange = _minimumChange; // 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 enforced. + * + * This contract and the msg.sender must have the DEBT_MANAGER + * role assigned to them. + * + * The function signature matches the vault so no update to the + * call data is required. + */ + function update_debt(address _strategy, uint256 _targetDebt) external { + IVault _vault = IVault(vault); + require( + (IVault(_vault).roles(msg.sender) & DEBT_MANAGER) == DEBT_MANAGER, + "!authorized" + ); + IVault(_vault).update_debt(_strategy, _targetDebt); + configs[_strategy].lastUpdate = 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. * - * This cannot be used to withdraw down to 0 debt. + * 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. @@ -108,9 +160,22 @@ contract GenericDebtAllocator is Governance { // Make sure we have a target debt. require(config.targetRatio != 0, "no targetRatio"); + if (block.timestamp - config.lastUpdate <= minimumWait) { + return (false, "min wait"); + } + + uint256 vaultAssets = _vault.totalAssets(); + // Get the target debt for the strategy based on vault assets. uint256 targetDebt = Math.min( - (_vault.totalAssets() * config.targetRatio) / MAX_BPS, + (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 ); @@ -125,8 +190,9 @@ contract GenericDebtAllocator is Governance { return (false, bytes("No Idle")); } + // Add up to the max if possible uint256 toAdd = Math.min( - targetDebt - params.current_debt, + maxDebt - params.current_debt, // Can't take more than is available. Math.min( currentIdle - minIdle, @@ -135,7 +201,7 @@ contract GenericDebtAllocator is Governance { ); // If the amount to add is over our threshold. - if (toAdd > config.minimumChange) { + if (toAdd > minimumChange) { // Return true and the calldata. return ( true, @@ -146,7 +212,7 @@ contract GenericDebtAllocator is Governance { ); } // If target debt is lower than the current. - } else if (targetDebt < params.current_debt) { + } else if (maxDebt < params.current_debt) { // Find out by how much. uint256 toPull = Math.min( params.current_debt - targetDebt, @@ -155,7 +221,7 @@ contract GenericDebtAllocator is Governance { ); // Check if it's over the threshold. - if (toPull > config.minimumChange) { + if (toPull > minimumChange) { // Can't lower debt if there is unrealised losses. if ( _vault.assess_share_of_unrealised_losses( @@ -188,15 +254,21 @@ contract GenericDebtAllocator is Governance { * * @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 setTargetDebtRatio( + function setStrategyDebtRatios( address _strategy, - uint256 _targetRatio + uint256 _targetRatio, + uint256 _maxRatio ) external onlyGovernance { // Make sure the strategy is added to the vault. require(IVault(vault).strategies(_strategy).activation != 0, "!active"); // Make sure a minimumChange has been set. - require(configs[_strategy].minimumChange != 0, "!minimum"); + 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 what will be the new total debt ratio. uint256 newDebtRatio = debtRatio - @@ -208,9 +280,16 @@ contract GenericDebtAllocator is Governance { // Write to storage. configs[_strategy].targetRatio = _targetRatio; + configs[_strategy].maxRatio = _maxRatio; + debtRatio = newDebtRatio; - emit SetTargetDebtRatio(_strategy, _targetRatio, newDebtRatio); + emit UpdatedStrategyDebtRatios( + _strategy, + _targetRatio, + _maxRatio, + newDebtRatio + ); } /** @@ -218,20 +297,25 @@ contract GenericDebtAllocator is Governance { * @dev This is the amount of debt that will needed to be * added or pulled for it to trigger an update. * - * @param _strategy The address of the strategy to update. * @param _minimumChange The new minimum to set for the strategy. */ - function setMinimumChange( - address _strategy, - uint256 _minimumChange - ) external onlyGovernance { - // Make sure the strategy is added to the vault. - require(IVault(vault).strategies(_strategy).activation != 0, "!active"); - + function setMinimumChange(uint256 _minimumChange) external onlyGovernance { + require(_minimumChange > 0, "zero"); // Set the new minimum. - configs[_strategy].minimumChange = _minimumChange; + minimumChange = _minimumChange; + + emit UpdatedMinimumChange(_minimumChange); + } + + /** + * @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 setMinimumTime(uint256 _minimumWait) external onlyGovernance { + minimumWait = _minimumWait; - emit SetMinimumChange(_strategy, _minimumChange); + emit UpdatedMinimumWait(_minimumWait); } /** @@ -248,6 +332,6 @@ contract GenericDebtAllocator is Governance { ) external onlyGovernance { maxAcceptableBaseFee = _maxAcceptableBaseFee; - emit SetMaxAcceptableBaseFee(_maxAcceptableBaseFee); + emit UpdatedMaxAcceptableBaseFee(_maxAcceptableBaseFee); } } diff --git a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol b/contracts/debtAllocators/GenericDebtAllocatorFactory.sol index 0727128..aa26a77 100644 --- a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol +++ b/contracts/debtAllocators/GenericDebtAllocatorFactory.sol @@ -17,12 +17,13 @@ contract GenericDebtAllocatorFactory { address public immutable original; constructor() { - original = address(new GenericDebtAllocator(address(1), address(2))); + original = address(new GenericDebtAllocator(address(1), address(2), 0)); } /** * @notice Clones a new debt allocator. - * @dev defaults to msg.sender as the governance role. + * @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 @@ -30,18 +31,34 @@ contract GenericDebtAllocatorFactory { function newGenericDebtAllocator( address _vault ) external returns (address) { - return newGenericDebtAllocator(_vault, msg.sender); + 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 newAllocator Address of the new generic debt allocator + * @return Address of the new generic debt allocator */ function newGenericDebtAllocator( address _vault, address _governance + ) external returns (address) { + return newGenericDebtAllocator(_vault, _governance, 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 returns (address newAllocator) { // Copied from https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol bytes20 addressBytes = bytes20(original); @@ -60,7 +77,11 @@ contract GenericDebtAllocatorFactory { newAllocator := create(0, clone_code, 0x37) } - GenericDebtAllocator(newAllocator).initialize(_vault, _governance); + GenericDebtAllocator(newAllocator).initialize( + _vault, + _governance, + _minimumChange + ); emit NewDebtAllocator(newAllocator, _vault); } diff --git a/tests/test_generic_debt_allocator.py b/tests/test_generic_debt_allocator.py index 912348b..bb39bc0 100644 --- a/tests/test_generic_debt_allocator.py +++ b/tests/test_generic_debt_allocator.py @@ -5,7 +5,7 @@ def test_setup(generic_debt_allocator_factory, user, strategy, vault): tx = generic_debt_allocator_factory.newGenericDebtAllocator( - vault, user, sender=user + vault, user, 0, sender=user ) events = list(tx.decode_logs(generic_debt_allocator_factory.NewDebtAllocator)) @@ -17,71 +17,92 @@ def test_setup(generic_debt_allocator_factory, user, strategy, vault): assert generic_debt_allocator.governance() == user assert generic_debt_allocator.vault() == vault.address - assert generic_debt_allocator.configs(strategy) == (0, 0) + assert generic_debt_allocator.configs(strategy) == (0, 0, 0) assert generic_debt_allocator.debtRatio() == 0 with ape.reverts("!active"): generic_debt_allocator.shouldUpdateDebt(strategy) def test_set_minimum(generic_debt_allocator, daddy, vault, strategy, user): - assert generic_debt_allocator.configs(strategy) == (0, 0) + assert generic_debt_allocator.configs(strategy) == (0, 0, 0) + assert generic_debt_allocator.minimumChange() == 0 minimum = int(1e17) with ape.reverts("!governance"): - generic_debt_allocator.setMinimumChange(strategy, minimum, sender=user) + generic_debt_allocator.setMinimumChange(minimum, sender=user) - with ape.reverts("!active"): - generic_debt_allocator.setMinimumChange(strategy, minimum, sender=daddy) - - vault.add_strategy(strategy.address, sender=daddy) + with ape.reverts("zero"): + generic_debt_allocator.setMinimumChange(0, sender=daddy) - tx = generic_debt_allocator.setMinimumChange(strategy, minimum, sender=daddy) + tx = generic_debt_allocator.setMinimumChange(minimum, sender=daddy) - event = list(tx.decode_logs(generic_debt_allocator.SetMinimumChange))[0] + event = list(tx.decode_logs(generic_debt_allocator.UpdatedMinimumChange))[0] - assert event.strategy == strategy.address assert event.minimumChange == minimum - assert generic_debt_allocator.configs(strategy) == (0, minimum) + assert generic_debt_allocator.minimumChange() == minimum -def test_set_minimum(generic_debt_allocator, daddy, vault, strategy, user): - assert generic_debt_allocator.configs(strategy) == (0, 0) +def test_set_ratios( + generic_debt_allocator, daddy, vault, strategy, create_strategy, user +): + assert generic_debt_allocator.configs(strategy) == (0, 0, 0) minimum = int(1e17) + max = int(6_000) target = int(5_000) with ape.reverts("!governance"): - generic_debt_allocator.setTargetDebtRatio(strategy, target, sender=user) + generic_debt_allocator.setStrategyDebtRatios(strategy, target, max, sender=user) with ape.reverts("!active"): - generic_debt_allocator.setTargetDebtRatio(strategy, target, sender=daddy) + generic_debt_allocator.setStrategyDebtRatios( + strategy, target, max, sender=daddy + ) vault.add_strategy(strategy.address, sender=daddy) with ape.reverts("!minimum"): - generic_debt_allocator.setTargetDebtRatio(strategy, target, sender=daddy) + generic_debt_allocator.setStrategyDebtRatios( + strategy, target, max, sender=daddy + ) - generic_debt_allocator.setMinimumChange(strategy, minimum, sender=daddy) + generic_debt_allocator.setMinimumChange(minimum, sender=daddy) - with ape.reverts("ratio too high"): - generic_debt_allocator.setTargetDebtRatio(strategy, int(10_001), sender=daddy) + with ape.reverts("max too high"): + generic_debt_allocator.setStrategyDebtRatios( + strategy, target, int(10_001), sender=daddy + ) - tx = generic_debt_allocator.setTargetDebtRatio(strategy, target, sender=daddy) + with ape.reverts("max ratio"): + generic_debt_allocator.setStrategyDebtRatios( + strategy, int(max + 1), max, sender=daddy + ) - event = list(tx.decode_logs(generic_debt_allocator.SetTargetDebtRatio))[0] + tx = generic_debt_allocator.setStrategyDebtRatios( + strategy, target, max, sender=daddy + ) + + event = list(tx.decode_logs(generic_debt_allocator.UpdatedStrategyDebtRatios))[0] - assert event.strategy == strategy.address assert event.targetRatio == target + assert event.maxRatio == max assert event.totalDebtRatio == target assert generic_debt_allocator.debtRatio() == target - assert generic_debt_allocator.configs(strategy) == (target, minimum) + assert generic_debt_allocator.configs(strategy) == (target, max, 0) + + new_strategy = create_strategy() + vault.add_strategy(new_strategy, sender=daddy) + with ape.reverts("ratio too high"): + generic_debt_allocator.setStrategyDebtRatios( + new_strategy, int(10_000), int(10_000), sender=daddy + ) def test_should_update_debt( generic_debt_allocator, vault, strategy, daddy, deposit_into_vault, amount ): - assert generic_debt_allocator.configs(strategy.address) == (0, 0) + assert generic_debt_allocator.configs(strategy.address) == (0, 0, 0) with ape.reverts("!active"): generic_debt_allocator.shouldUpdateDebt(strategy.address) @@ -93,9 +114,10 @@ def test_should_update_debt( minimum = int(1) target = int(5_000) + max = int(5_000) - generic_debt_allocator.setMinimumChange(strategy, minimum, sender=daddy) - generic_debt_allocator.setTargetDebtRatio(strategy, target, sender=daddy) + generic_debt_allocator.setMinimumChange(minimum, sender=daddy) + generic_debt_allocator.setStrategyDebtRatios(strategy, target, max, sender=daddy) # Vault has no assets so should be false. (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) @@ -126,7 +148,9 @@ def test_should_update_debt( assert bytes == ("Below Min").encode("utf-8") # Update the ratio to make true - generic_debt_allocator.setTargetDebtRatio(strategy, int(target + 1), sender=daddy) + generic_debt_allocator.setStrategyDebtRatios( + strategy, int(target + 1), int(target + 1), sender=daddy + ) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) assert bool == True @@ -160,15 +184,17 @@ def test_should_update_debt( vault.set_minimum_total_idle(0, sender=daddy) # increase the minimum so its false again - generic_debt_allocator.setMinimumChange(strategy, int(1e30), sender=daddy) + generic_debt_allocator.setMinimumChange(int(1e30), sender=daddy) (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(strategy, int(1), sender=daddy) - generic_debt_allocator.setTargetDebtRatio(strategy, int(target // 2), sender=daddy) + generic_debt_allocator.setMinimumChange(int(1), sender=daddy) + generic_debt_allocator.setStrategyDebtRatios( + strategy, int(target // 2), int(target // 2), sender=daddy + ) (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) assert bool == True From 094b26f0cadaa14ec6c043b28c4c15d102d84311 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 7 Nov 2023 09:42:53 -0700 Subject: [PATCH 3/9] test: debt update --- .../debtAllocators/GenericDebtAllocator.sol | 4 +- tests/conftest.py | 14 +---- tests/test_generic_debt_allocator.py | 53 ++++++++++++++++++- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index b19cef7..5d03651 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -123,7 +123,7 @@ contract GenericDebtAllocator is Governance { IVault _vault = IVault(vault); require( (IVault(_vault).roles(msg.sender) & DEBT_MANAGER) == DEBT_MANAGER, - "!authorized" + "not allowed" ); IVault(_vault).update_debt(_strategy, _targetDebt); configs[_strategy].lastUpdate = block.timestamp; @@ -312,7 +312,7 @@ contract GenericDebtAllocator is Governance { * @dev This is only enforced per strategy. * @param _minimumWait The minimum time in seconds to wait. */ - function setMinimumTime(uint256 _minimumWait) external onlyGovernance { + function setMinimumWait(uint256 _minimumWait) external onlyGovernance { minimumWait = _minimumWait; emit UpdatedMinimumWait(_minimumWait); diff --git a/tests/conftest.py b/tests/conftest.py index 16736f6..d644f46 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -147,19 +147,7 @@ def create_vault( vault.set_role( daddy.address, - ROLES.ADD_STRATEGY_MANAGER - | ROLES.REVOKE_STRATEGY_MANAGER - | ROLES.FORCE_REVOKE_MANAGER - | ROLES.ACCOUNTANT_MANAGER - | ROLES.QUEUE_MANAGER - | ROLES.REPORTING_MANAGER - | ROLES.DEBT_MANAGER - | ROLES.MAX_DEBT_MANAGER - | ROLES.DEPOSIT_LIMIT_MANAGER - | ROLES.WITHDRAW_LIMIT_MANAGER - | ROLES.MINIMUM_IDLE_MANAGER - | ROLES.PROFIT_UNLOCK_MANAGER - | ROLES.EMERGENCY_MANAGER, + ROLES.ALL, sender=daddy, ) diff --git a/tests/test_generic_debt_allocator.py b/tests/test_generic_debt_allocator.py index bb39bc0..d421245 100644 --- a/tests/test_generic_debt_allocator.py +++ b/tests/test_generic_debt_allocator.py @@ -1,6 +1,6 @@ import ape from ape import chain, project -from utils.constants import ZERO_ADDRESS, MAX_INT +from utils.constants import ZERO_ADDRESS, MAX_INT, ROLES def test_setup(generic_debt_allocator_factory, user, strategy, vault): @@ -103,6 +103,7 @@ 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) + vault.add_role(generic_debt_allocator, ROLES.DEBT_MANAGER, sender=daddy) with ape.reverts("!active"): generic_debt_allocator.shouldUpdateDebt(strategy.address) @@ -140,7 +141,8 @@ 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) - vault.update_debt(strategy, amount // 2, sender=daddy) + generic_debt_allocator.update_debt(strategy, amount // 2, sender=daddy) + chain.mine(1) # Should now be false again once allocated (bool, bytes) = generic_debt_allocator.shouldUpdateDebt(strategy.address) @@ -158,6 +160,15 @@ def test_should_update_debt( strategy.address, int(amount * 5_001 // 10_000) ) + # Set a minimumWait time + generic_debt_allocator.setMinimumWait(MAX_INT, sender=daddy) + # 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) + # Lower the max debt so its == to current debt vault.update_max_debt_for_strategy(strategy, int(amount // 2), sender=daddy) @@ -199,3 +210,41 @@ def test_should_update_debt( (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, vault, strategy, daddy, user, deposit_into_vault, amount +): + assert generic_debt_allocator.configs(strategy) == (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("not allowed"): + 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=daddy) + + vault.add_role(generic_debt_allocator, ROLES.DEBT_MANAGER, sender=daddy) + + generic_debt_allocator.update_debt(strategy, amount, sender=daddy) + + timestamp = generic_debt_allocator.configs(strategy)[2] + assert timestamp != 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == amount + + vault.add_role(user, ROLES.DEBT_MANAGER, sender=daddy) + + generic_debt_allocator.update_debt(strategy, 0, sender=user) + + assert generic_debt_allocator.configs(strategy)[2] != timestamp + assert vault.totalIdle() == amount + assert vault.totalDebt() == 0 From 2460df6ff1383f431d41f845aa08793b2c7cbb54 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 10 Nov 2023 16:58:32 -0700 Subject: [PATCH 4/9] feat: virtualize --- .../debtAllocators/GenericDebtAllocator.sol | 21 +++++++++----- .../GenericDebtAllocatorFactory.sol | 6 ++-- contracts/registry/Registry.sol | 29 ++++++++++--------- contracts/registry/RegistryFactory.sol | 8 +++-- contracts/registry/ReleaseRegistry.sol | 6 ++-- 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index 5d03651..42529dc 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -99,7 +99,7 @@ contract GenericDebtAllocator is Governance { address _vault, address _governance, uint256 _minimumChange - ) public { + ) public virtual { require(address(vault) == address(0), "!initialized"); vault = _vault; governance = _governance; @@ -119,7 +119,10 @@ contract GenericDebtAllocator is Governance { * The function signature matches the vault so no update to the * call data is required. */ - function update_debt(address _strategy, uint256 _targetDebt) external { + function update_debt( + address _strategy, + uint256 _targetDebt + ) external virtual { IVault _vault = IVault(vault); require( (IVault(_vault).roles(msg.sender) & DEBT_MANAGER) == DEBT_MANAGER, @@ -142,7 +145,7 @@ contract GenericDebtAllocator is Governance { */ function shouldUpdateDebt( address _strategy - ) external view returns (bool, bytes memory) { + ) external view virtual returns (bool, bytes memory) { // Check the base fee isn't too high. if (block.basefee > maxAcceptableBaseFee) { return (false, bytes("Base Fee")); @@ -260,7 +263,7 @@ contract GenericDebtAllocator is Governance { address _strategy, uint256 _targetRatio, uint256 _maxRatio - ) external onlyGovernance { + ) external virtual onlyGovernance { // Make sure the strategy is added to the vault. require(IVault(vault).strategies(_strategy).activation != 0, "!active"); // Make sure a minimumChange has been set. @@ -299,7 +302,9 @@ contract GenericDebtAllocator is Governance { * * @param _minimumChange The new minimum to set for the strategy. */ - function setMinimumChange(uint256 _minimumChange) external onlyGovernance { + function setMinimumChange( + uint256 _minimumChange + ) external virtual onlyGovernance { require(_minimumChange > 0, "zero"); // Set the new minimum. minimumChange = _minimumChange; @@ -312,7 +317,9 @@ contract GenericDebtAllocator is Governance { * @dev This is only enforced per strategy. * @param _minimumWait The minimum time in seconds to wait. */ - function setMinimumWait(uint256 _minimumWait) external onlyGovernance { + function setMinimumWait( + uint256 _minimumWait + ) external virtual onlyGovernance { minimumWait = _minimumWait; emit UpdatedMinimumWait(_minimumWait); @@ -329,7 +336,7 @@ contract GenericDebtAllocator is Governance { */ function setMaxAcceptableBaseFee( uint256 _maxAcceptableBaseFee - ) external onlyGovernance { + ) external virtual onlyGovernance { maxAcceptableBaseFee = _maxAcceptableBaseFee; emit UpdatedMaxAcceptableBaseFee(_maxAcceptableBaseFee); diff --git a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol b/contracts/debtAllocators/GenericDebtAllocatorFactory.sol index aa26a77..16845c9 100644 --- a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol +++ b/contracts/debtAllocators/GenericDebtAllocatorFactory.sol @@ -30,7 +30,7 @@ contract GenericDebtAllocatorFactory { */ function newGenericDebtAllocator( address _vault - ) external returns (address) { + ) external virtual returns (address) { return newGenericDebtAllocator(_vault, msg.sender, 0); } @@ -45,7 +45,7 @@ contract GenericDebtAllocatorFactory { function newGenericDebtAllocator( address _vault, address _governance - ) external returns (address) { + ) external virtual returns (address) { return newGenericDebtAllocator(_vault, _governance, 0); } @@ -59,7 +59,7 @@ contract GenericDebtAllocatorFactory { address _vault, address _governance, uint256 _minimumChange - ) public returns (address newAllocator) { + ) public virtual returns (address newAllocator) { // Copied from https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol bytes20 addressBytes = bytes20(original); assembly { diff --git a/contracts/registry/Registry.sol b/contracts/registry/Registry.sol index 78b0f6b..15e27e1 100644 --- a/contracts/registry/Registry.sol +++ b/contracts/registry/Registry.sol @@ -94,7 +94,7 @@ contract Registry is Governance { * @notice Returns the total number of assets being used as the underlying. * @return The amount of assets. */ - function numAssets() external view returns (uint256) { + function numAssets() external view virtual returns (uint256) { return assets.length; } @@ -102,7 +102,7 @@ contract Registry is Governance { * @notice Get the full array of tokens being used. * @return The full array of underlying tokens being used/. */ - function getAssets() external view returns (address[] memory) { + function getAssets() external view virtual returns (address[] memory) { return assets; } @@ -110,7 +110,9 @@ contract Registry is Governance { * @notice The amount of endorsed vaults for a specific token. * @return The amount of endorsed vaults. */ - function numEndorsedVaults(address _asset) public view returns (uint256) { + function numEndorsedVaults( + address _asset + ) public view virtual returns (uint256) { return _endorsedVaults[_asset].length; } @@ -121,7 +123,7 @@ contract Registry is Governance { */ function getEndorsedVaults( address _asset - ) external view returns (address[] memory) { + ) external view virtual returns (address[] memory) { return _endorsedVaults[_asset]; } @@ -138,6 +140,7 @@ contract Registry is Governance { function getAllEndorsedVaults() external view + virtual returns (address[][] memory allEndorsedVaults) { address[] memory allAssets = assets; @@ -170,7 +173,7 @@ contract Registry is Governance { string memory _symbol, address _roleManager, uint256 _profitMaxUnlockTime - ) public returns (address _vault) { + ) public virtual returns (address _vault) { return newEndorsedVault( _asset, @@ -205,7 +208,7 @@ contract Registry is Governance { address _roleManager, uint256 _profitMaxUnlockTime, uint256 _releaseDelta - ) public onlyGovernance returns (address _vault) { + ) public virtual onlyGovernance returns (address _vault) { // Get the target release based on the delta given. uint256 _releaseTarget = ReleaseRegistry(releaseRegistry) .numReleases() - @@ -246,7 +249,7 @@ contract Registry is Governance { * @param _vault Address of the vault to endorse. */ - function endorseMultiStrategyVault(address _vault) external { + function endorseMultiStrategyVault(address _vault) external virtual { endorseVault(_vault, 0, MULTI_STRATEGY_TYPE, 0); } @@ -257,7 +260,7 @@ contract Registry is Governance { * * @param _vault Address of the vault to endorse. */ - function endorseSingleStrategyVault(address _vault) external { + function endorseSingleStrategyVault(address _vault) external virtual { endorseVault(_vault, 0, SINGLE_STRATEGY_TYPE, 0); } @@ -279,7 +282,7 @@ contract Registry is Governance { uint256 _releaseDelta, uint256 _vaultType, uint256 _deploymentTimestamp - ) public onlyGovernance { + ) public virtual onlyGovernance { // Cannot endorse twice. require(vaultInfo[_vault].asset == address(0), "endorsed"); require(_vaultType != 0, "no 0 type"); @@ -319,7 +322,7 @@ contract Registry is Governance { uint256 _releaseTarget, uint256 _vaultType, uint256 _deploymentTimestamp - ) internal { + ) internal virtual { // Add to the endorsed vaults array. _endorsedVaults[_asset].push(_vault); @@ -353,7 +356,7 @@ contract Registry is Governance { function tagVault( address _vault, string memory _tag - ) external onlyGovernance { + ) external virtual onlyGovernance { require(vaultInfo[_vault].asset != address(0), "!Endorsed"); vaultInfo[_vault].tag = _tag; } @@ -372,7 +375,7 @@ contract Registry is Governance { function removeVault( address _vault, uint256 _index - ) external onlyGovernance { + ) external virtual onlyGovernance { require(vaultInfo[_vault].asset != address(0), "!endorsed"); // Get the asset the vault is using. @@ -415,7 +418,7 @@ contract Registry is Governance { function removeAsset( address _asset, uint256 _index - ) external onlyGovernance { + ) external virtual onlyGovernance { require(assetIsUsed[_asset], "!in use"); require(_endorsedVaults[_asset].length == 0, "still in use"); require(assets[_index] == _asset, "wrong asset"); diff --git a/contracts/registry/RegistryFactory.sol b/contracts/registry/RegistryFactory.sol index 0ee6682..d30b31f 100644 --- a/contracts/registry/RegistryFactory.sol +++ b/contracts/registry/RegistryFactory.sol @@ -23,7 +23,7 @@ contract RegistryFactory { releaseRegistry = _releaseRegistry; } - function name() external pure returns (string memory) { + function name() external pure virtual returns (string memory) { return "Custom Vault Registry Factory"; } @@ -33,7 +33,9 @@ contract RegistryFactory { * @param _name The name of the new registry. * @return Address of the new Registry. */ - function createNewRegistry(string memory _name) external returns (address) { + function createNewRegistry( + string memory _name + ) external virtual returns (address) { return createNewRegistry(_name, msg.sender); } @@ -46,7 +48,7 @@ contract RegistryFactory { function createNewRegistry( string memory _name, address _governance - ) public returns (address) { + ) public virtual returns (address) { Registry newRegistry = new Registry( _governance, _name, diff --git a/contracts/registry/ReleaseRegistry.sol b/contracts/registry/ReleaseRegistry.sol index bd113e5..86289dc 100644 --- a/contracts/registry/ReleaseRegistry.sol +++ b/contracts/registry/ReleaseRegistry.sol @@ -41,7 +41,7 @@ contract ReleaseRegistry is Governance { * @dev Throws if no releases are registered yet. * @return The address of the factory for the latest release. */ - function latestFactory() external view returns (address) { + function latestFactory() external view virtual returns (address) { return factories[numReleases - 1]; } @@ -50,7 +50,7 @@ contract ReleaseRegistry is Governance { * @dev Throws if no releases are registered yet. * @return The api version of the latest release. */ - function latestRelease() external view returns (string memory) { + function latestRelease() external view virtual returns (string memory) { return IFactory(factories[numReleases - 1]).apiVersion(); // dev: no release } @@ -65,7 +65,7 @@ contract ReleaseRegistry is Governance { * * @param _factory The factory that will be used create new vaults. */ - function newRelease(address _factory) external onlyGovernance { + function newRelease(address _factory) external virtual onlyGovernance { // Check if the release is different from the current one uint256 releaseId = numReleases; From 9a9e94703eb79537e3d5a8a6d816e297057e2d36 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 13 Nov 2023 17:54:18 -0700 Subject: [PATCH 5/9] chore: update comments --- .../debtAllocators/GenericDebtAllocator.sol | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index 42529dc..4209beb 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -29,6 +29,7 @@ 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( address indexed strategy, uint256 targetRatio, @@ -36,13 +37,16 @@ contract GenericDebtAllocator is Governance { uint256 totalDebtRatio ); + /// @notice An event emitted when the minimum change is updated. event UpdatedMinimumChange(uint256 minimumChange); + /// @notice An event emitted when the max base fee is updated. event UpdatedMaxAcceptableBaseFee(uint256 maxAcceptableBaseFee); + /// @notice An event emitted when the minimum time to wait is updated. event UpdatedMinimumWait(uint256 newMinimumWait); - // Struct for each strategies info. + /// @notice Struct for each strategies info. struct Config { // The ideal percent in Basis Points the strategy should have. uint256 targetRatio; @@ -56,27 +60,27 @@ contract GenericDebtAllocator is Governance { uint256 internal constant MAX_BPS = 10_000; - // Vaults DEBT_MANAGER enumerator. + /// @notice Vaults DEBT_MANAGER enumerator. uint256 internal constant DEBT_MANAGER = 64; - // Mapping of strategy => its config. + /// @notice Mapping of strategy => its config. mapping(address => Config) public configs; - // Address of the vault this serves as allocator for. + /// @notice Address of the vault this serves as allocator for. address public vault; - // Total debt ratio currently allocated in basis points. + /// @notice Total debt ratio currently allocated in basis points. // Can't be more than 10_000. uint256 public debtRatio; - // The minimum amount denominated in asset that will + /// @notice The minimum amount denominated in asset that will // need to be moved to trigger a debt update. uint256 public minimumChange; - // Time to wait between debt updates. + /// @notice Time to wait between debt updates. uint256 public minimumWait; - // Max the chains base fee can be during debt update. + /// @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; From 1a15619869a06db3c41430679096d376652b2c4a Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 13 Nov 2023 18:13:45 -0700 Subject: [PATCH 6/9] fix: ci vyper version --- .github/workflows/test.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 35854bb..40b4fde 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,7 +21,8 @@ jobs: python-version: '3.10' - name: install vyper - run: pip install git+https://github.com/vyperlang/vyper + run: pip install vyper==0.3.7 + - run: ape compile --force --size - run: npm install hardhat From 934d375f09238a21d0ac1417e0aac773ac161b33 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 14 Nov 2023 09:42:13 -0700 Subject: [PATCH 7/9] 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) From d8915eb76899d6bf66740b3582fd170f385b6ed9 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 14 Nov 2023 09:43:30 -0700 Subject: [PATCH 8/9] fix: vyper ci --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 40b4fde..0ed8c8a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,7 +21,7 @@ jobs: python-version: '3.10' - name: install vyper - run: pip install vyper==0.3.7 + run: pip install git+https://github.com/vyperlang/vyper - run: ape compile --force --size - run: npm install hardhat From 19bad7e0777fdeca933c2230cbc2e0dbad15428a Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 14 Nov 2023 14:14:36 -0700 Subject: [PATCH 9/9] feat: use max redeem --- .../debtAllocators/GenericDebtAllocator.sol | 30 ++++++++++++------- .../GenericDebtAllocatorFactory.sol | 23 ++------------ 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/contracts/debtAllocators/GenericDebtAllocator.sol b/contracts/debtAllocators/GenericDebtAllocator.sol index f950100..db56955 100644 --- a/contracts/debtAllocators/GenericDebtAllocator.sol +++ b/contracts/debtAllocators/GenericDebtAllocator.sol @@ -37,16 +37,16 @@ contract GenericDebtAllocator is Governance { uint256 newTotalDebtRatio ); - /// @notice An event emitted when the minimum change is Updated. + /// @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. + /// @notice An event emitted when the max base fee is updated. event UpdateMaxAcceptableBaseFee(uint256 newMaxAcceptableBaseFee); - /// @notice An event emitted when the max debt update loss is Updated. + /// @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. + /// @notice An event emitted when the minimum time to wait is updated. event UpdateMinimumWait(uint256 newMinimumWait); /// @notice Struct for each strategies info. @@ -123,7 +123,7 @@ contract GenericDebtAllocator is Governance { /** * @notice Debt update wrapper for the vault. * @dev This can be used if a minimum time between debt updates - * is desired to be enforced. + * is desired to be enforced and to enforce a max loss. * * This contract and the msg.sender must have the DEBT_MANAGER * role assigned to them. @@ -144,6 +144,9 @@ contract GenericDebtAllocator is Governance { (_vault.roles(msg.sender) & DEBT_MANAGER) == DEBT_MANAGER, "not allowed" ); + + // Cache initial values in case of loss. + uint256 initialDebt = _vault.strategies(_strategy).current_debt; uint256 initialAssets = _vault.totalAssets(); _vault.update_debt(_strategy, _targetDebt); uint256 afterAssets = _vault.totalAssets(); @@ -153,10 +156,12 @@ contract GenericDebtAllocator is Governance { // Make sure its within the range. require( initialAssets - afterAssets <= - (initialAssets * maxDebtUpdateLoss) / MAX_BPS, + (initialDebt * maxDebtUpdateLoss) / MAX_BPS, "too much loss" ); } + + // Update the last time the strategies debt was updated. configs[_strategy].lastUpdate = block.timestamp; } @@ -192,7 +197,7 @@ contract GenericDebtAllocator is Governance { require(config.targetRatio != 0, "no targetRatio"); if (block.timestamp - config.lastUpdate <= minimumWait) { - return (false, "min wait"); + return (false, bytes("min wait")); } uint256 vaultAssets = _vault.totalAssets(); @@ -242,13 +247,16 @@ contract GenericDebtAllocator is Governance { ) ); } - // If target debt is lower than the current. + // If current debt is greater than our max. } else if (maxDebt < params.current_debt) { - // Find out by how much. + // Find out by how much. Aim for the target. uint256 toPull = Math.min( params.current_debt - targetDebt, // Account for the current liquidity constraints. - IVault(_strategy).maxWithdraw(address(_vault)) + // Use max redeem to match vault logic. + IVault(_strategy).convertToAssets( + IVault(_strategy).maxRedeem(address(_vault)) + ) ); // Check if it's over the threshold. @@ -258,7 +266,7 @@ contract GenericDebtAllocator is Governance { _vault.assess_share_of_unrealised_losses( _strategy, params.current_debt - ) > 0 + ) != 0 ) { return (false, bytes("unrealised loss")); } diff --git a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol b/contracts/debtAllocators/GenericDebtAllocatorFactory.sol index 16845c9..be5759c 100644 --- a/contracts/debtAllocators/GenericDebtAllocatorFactory.sol +++ b/contracts/debtAllocators/GenericDebtAllocatorFactory.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.18; import {GenericDebtAllocator} from "./GenericDebtAllocator.sol"; +import {Clonable} from "@periphery/utils/Clonable.sol"; /** * @title YearnV3 Generic Debt Allocator Factory @@ -10,12 +11,9 @@ import {GenericDebtAllocator} from "./GenericDebtAllocator.sol"; * Factory for anyone to easily deploy their own generic * debt allocator for a Yearn V3 Vault. */ -contract GenericDebtAllocatorFactory { +contract GenericDebtAllocatorFactory is Clonable { event NewDebtAllocator(address indexed allocator, address indexed vault); - // Original allocator to use for cloning. - address public immutable original; - constructor() { original = address(new GenericDebtAllocator(address(1), address(2), 0)); } @@ -60,22 +58,7 @@ contract GenericDebtAllocatorFactory { address _governance, uint256 _minimumChange ) public virtual returns (address newAllocator) { - // Copied from https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol - bytes20 addressBytes = bytes20(original); - assembly { - // EIP-1167 bytecode - let clone_code := mload(0x40) - mstore( - clone_code, - 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 - ) - mstore(add(clone_code, 0x14), addressBytes) - mstore( - add(clone_code, 0x28), - 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 - ) - newAllocator := create(0, clone_code, 0x37) - } + newAllocator = _clone(); GenericDebtAllocator(newAllocator).initialize( _vault,