Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: max debt in allocator #26

Merged
merged 9 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
214 changes: 174 additions & 40 deletions contracts/debtAllocators/GenericDebtAllocator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,81 +16,164 @@ 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make "updated" lowercase

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
*
* 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"
);
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;
}

/**
* @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 +191,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");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to cast the string to bytes() like we do above?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

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 +221,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 +232,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 @@ -146,7 +243,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,
Expand All @@ -155,7 +252,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(
Expand Down Expand Up @@ -188,15 +285,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 +311,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 +379,9 @@ contract GenericDebtAllocator is Governance {
*/
function setMaxAcceptableBaseFee(
uint256 _maxAcceptableBaseFee
) external onlyGovernance {
) external virtual onlyGovernance {
maxAcceptableBaseFee = _maxAcceptableBaseFee;

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