Skip to content

Commit

Permalink
build: max debt in allocator
Browse files Browse the repository at this point in the history
  • Loading branch information
Schlagonia committed Nov 3, 2023
1 parent 13101c0 commit f4ba391
Showing 1 changed file with 300 additions and 0 deletions.
300 changes: 300 additions & 0 deletions contracts/debtAllocators/Allocator.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit f4ba391

Please sign in to comment.