Skip to content

Commit

Permalink
build: max debt in allocator (#26)
Browse files Browse the repository at this point in the history
* build: max debt in allocator

* test: test new allocator

* test: debt update

* feat: virtualize

* chore: update comments

* fix: ci vyper version

* feat: check debt loss

* fix: vyper ci

* feat: use max redeem
  • Loading branch information
Schlagonia authored Nov 17, 2023
1 parent 13101c0 commit 52b1d06
Show file tree
Hide file tree
Showing 8 changed files with 391 additions and 139 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:

- name: install vyper
run: pip install git+https://github.com/vyperlang/vyper

- run: ape compile --force --size
- run: npm install hardhat

Expand Down
230 changes: 186 additions & 44 deletions contracts/debtAllocators/GenericDebtAllocator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,81 +16,169 @@ 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(
/// @notice An event emitted when a strategies debt ratios are Updated.
event UpdateStrategyDebtRatios(
address indexed strategy,
uint256 targetRatio,
uint256 totalDebtRatio
uint256 newTargetRatio,
uint256 newMaxRatio,
uint256 newTotalDebtRatio
);

event SetMinimumChange(address indexed strategy, 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 UpdateMaxAcceptableBaseFee(uint256 newMaxAcceptableBaseFee);

/// @notice An event emitted when the max debt update loss is updated.
event UpdateMaxDebtUpdateLoss(uint256 newMaxDebtUpdateLoss);

event SetMaxAcceptableBaseFee(uint256 maxAcceptableBaseFee);
/// @notice An event emitted when the minimum time to wait is updated.
event UpdateMinimumWait(uint256 newMinimumWait);

// Struct for each strategies info.
/// @notice 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;

// Mapping of strategy => its config.
/// @notice Vaults DEBT_MANAGER enumerator.
uint256 internal constant DEBT_MANAGER = 64;

/// @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;

// Max the chains base fee can be during debt update.
/// @notice The minimum amount denominated in asset that will
// need to be moved to trigger a debt update.
uint256 public minimumChange;

/// @notice Time to wait between debt updates.
uint256 public minimumWait;

/// @notice Max loss to accept on debt updates in basis points.
uint256 public maxDebtUpdateLoss;

/// @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;

constructor(address _vault, address _governance) Governance(_governance) {
initialize(_vault, _governance);
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) public {
function initialize(
address _vault,
address _governance,
uint256 _minimumChange
) public virtual {
require(address(vault) == address(0), "!initialized");
vault = _vault;
governance = _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;
}

/**
* @notice Debt update wrapper for the vault.
* @dev This can be used if a minimum time between debt updates
* 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.
*
* 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
) external virtual {
IVault _vault = IVault(vault);
require(
(_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();

// If a loss was realized.
if (afterAssets < initialAssets) {
// Make sure its within the range.
require(
initialAssets - afterAssets <=
(initialDebt * maxDebtUpdateLoss) / MAX_BPS,
"too much loss"
);
}

// Update the last time the strategies debt was updated.
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.
* @return . Calldata if `true` or reason if `false`.
*/
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"));
Expand All @@ -108,9 +196,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, bytes("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
);
Expand All @@ -125,8 +226,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,
Expand All @@ -135,7 +237,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,
Expand All @@ -145,23 +247,26 @@ contract GenericDebtAllocator is Governance {
)
);
}
// If target debt is lower than the current.
} else if (targetDebt < params.current_debt) {
// Find out by how much.
// If current debt is greater than our max.
} else if (maxDebt < params.current_debt) {
// 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.
if (toPull > config.minimumChange) {
if (toPull > minimumChange) {
// Can't lower debt if there is unrealised losses.
if (
_vault.assess_share_of_unrealised_losses(
_strategy,
params.current_debt
) > 0
) != 0
) {
return (false, bytes("unrealised loss"));
}
Expand All @@ -188,15 +293,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
) external onlyGovernance {
uint256 _targetRatio,
uint256 _maxRatio
) 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.
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 -
Expand All @@ -208,30 +319,61 @@ contract GenericDebtAllocator is Governance {

// Write to storage.
configs[_strategy].targetRatio = _targetRatio;
configs[_strategy].maxRatio = _maxRatio;

debtRatio = newDebtRatio;

emit SetTargetDebtRatio(_strategy, _targetRatio, newDebtRatio);
emit UpdateStrategyDebtRatios(
_strategy,
_targetRatio,
_maxRatio,
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 _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");

) external virtual onlyGovernance {
require(_minimumChange > 0, "zero");
// Set the new minimum.
configs[_strategy].minimumChange = _minimumChange;
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 SetMinimumChange(_strategy, _minimumChange);
emit UpdateMinimumWait(_minimumWait);
}

/**
Expand All @@ -245,9 +387,9 @@ contract GenericDebtAllocator is Governance {
*/
function setMaxAcceptableBaseFee(
uint256 _maxAcceptableBaseFee
) external onlyGovernance {
) external virtual onlyGovernance {
maxAcceptableBaseFee = _maxAcceptableBaseFee;

emit SetMaxAcceptableBaseFee(_maxAcceptableBaseFee);
emit UpdateMaxAcceptableBaseFee(_maxAcceptableBaseFee);
}
}
Loading

0 comments on commit 52b1d06

Please sign in to comment.