Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/Strategy.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.24;
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {TroveOps} from "./libraries/TroveOps.sol";
import {LenderOps} from "./libraries/LenderOps.sol";
import {LiquityMath} from "./libraries/LiquityMath.sol";
import {IExchange} from "./interfaces/IExchange.sol";
import {AggregatorInterface} from "./interfaces/AggregatorInterface.sol";
import {
IAddressesRegistry,
IBorrowerOperations,
ICollSurplusPool,
ITroveManager
} from "./interfaces/IAddressesRegistry.sol";
import {BaseLenderBorrower, ERC20, Math} from "./BaseLenderBorrower.sol";
contract LiquityV2LBStrategy is BaseLenderBorrower {
using SafeERC20 for ERC20;
// ===============================================================
// Storage
// ===============================================================
/// @notice Trove ID
uint256 public troveId;
/// @notice Absolute surplus required (in BOLD) before tending is considered
uint256 public minSurplusAbsolute;
/// @notice Relative surplus required (in basis points) of current debt, before tending is considered
uint256 public minSurplusRelative;
/// @notice Addresses allowed to deposit
mapping(address => bool) public allowed;
// ===============================================================
// Constants
// ===============================================================
/// @notice The difference in decimals between our price oracle (1e8) and Liquity's price oracle (1e18)
uint256 private constant _DECIMALS_DIFF = 1e10;
/// @notice Maximum relative surplus required (in basis points) before tending is considered
uint256 private constant _MAX_RELATIVE_SURPLUS = 1000; // 10%
/// @notice WETH token
ERC20 private constant _WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
/// @notice Same Chainlink price feed as used by the Liquity branch
AggregatorInterface public immutable PRICE_FEED;
/// @notice Liquity's borrower operations contract
IBorrowerOperations public immutable BORROWER_OPERATIONS;
/// @notice Liquity's trove manager contract
ITroveManager public immutable TROVE_MANAGER;
/// @notice Liquity's collateral surplus pool contract
ICollSurplusPool public immutable COLL_SURPLUS_POOL;
/// @notice The exchange contract for buying/selling the borrow token
IExchange public immutable EXCHANGE;
// ===============================================================
// Constructor
// ===============================================================
/// @param _addressesRegistry The Liquity addresses registry contract
/// @param _priceFeed The price feed contract for the `asset`
/// @param _exchange The exchange contract for buying/selling borrow token
/// @param _name The name of the strategy
constructor(
IAddressesRegistry _addressesRegistry,
AggregatorInterface _priceFeed,
IExchange _exchange,
string memory _name
)
BaseLenderBorrower(
_addressesRegistry.collToken(), // asset
_name,
_addressesRegistry.boldToken(), // borrowToken
address(0x89E93172AEF8261Db8437b90c3dCb61545a05317) // lenderVault (sUSDaf)
)
{
require(_exchange.BORROW() == borrowToken && _exchange.COLLATERAL() == address(asset), "!exchange");
minSurplusAbsolute = 50e18; // 50 BOLD
minSurplusRelative = 100; // 1%
BORROWER_OPERATIONS = _addressesRegistry.borrowerOperations();
TROVE_MANAGER = _addressesRegistry.troveManager();
COLL_SURPLUS_POOL = _addressesRegistry.collSurplusPool();
PRICE_FEED =
address(_priceFeed) == address(0) ? _addressesRegistry.priceFeed().ethUsdOracle().aggregator : _priceFeed;
require(PRICE_FEED.decimals() == 8, "!priceFeed");
EXCHANGE = _exchange;
ERC20(borrowToken).forceApprove(address(EXCHANGE), type(uint256).max);
asset.forceApprove(address(EXCHANGE), type(uint256).max);
asset.forceApprove(address(BORROWER_OPERATIONS), type(uint256).max);
if (asset != _WETH) _WETH.forceApprove(address(BORROWER_OPERATIONS), TroveOps.ETH_GAS_COMPENSATION);
}
// ===============================================================
// Privileged functions
// ===============================================================
/// @notice Open a trove
/// @dev Callable only once. If the position gets liquidated, we'll need to shutdown the strategy
/// @dev `asset` balance must be large enough to open a trove with `MIN_DEBT`
/// @dev Requires the caller to pay the gas compensation in WETH
/// @dev Should be called through a private RPC to avoid fee slippage
/// @param _upperHint Upper hint
/// @param _lowerHint Lower hint
function openTrove(uint256 _upperHint, uint256 _lowerHint) external onlyEmergencyAuthorized {
require(troveId == 0, "troveId");
// Mint `MIN_DEBT` and use all the collateral we have
troveId = TroveOps.openTrove(BORROWER_OPERATIONS, balanceOfAsset(), _upperHint, _lowerHint);
// Lend everything we have
_lendBorrowToken(balanceOfBorrowToken());
}
/// @notice Adjust the interest rate of the trove
/// @dev Will fail if the trove is not active
/// @dev Should be called through a private RPC to avoid fee slippage
/// @dev Would incur an upfront fee if the adjustment is considered premature (i.e. within 7 days of last adjustment)
/// @param _newAnnualInterestRate New annual interest rate
/// @param _upperHint Upper hint
/// @param _lowerHint Lower hint
function adjustTroveInterestRate(
uint256 _newAnnualInterestRate,
uint256 _upperHint,
uint256 _lowerHint
) external onlyEmergencyAuthorized {
TroveOps.adjustTroveInterestRate(BORROWER_OPERATIONS, troveId, _newAnnualInterestRate, _upperHint, _lowerHint);
}
/// @notice Manually buy borrow token
/// @dev Potentially can never reach `_buyBorrowToken()` in `_liquidatePosition()`
/// because of lender vault accounting (i.e. `balanceOfLentAssets() == 0` is never true)
function buyBorrowToken(
uint256 _amount
) external onlyEmergencyAuthorized {
if (_amount == type(uint256).max) _amount = balanceOfAsset();
_buyBorrowToken(_amount);
}
/// @notice Adjust zombie trove
/// @dev Might need to be called after a redemption if our debt is below `MIN_DEBT`
/// @dev Will fail if the trove is not in zombie mode
/// @dev Should be called through a private RPC to avoid fee slippage
/// @param _upperHint Upper hint
/// @param _lowerHint Lower hint
function adjustZombieTrove(uint256 _upperHint, uint256 _lowerHint) external onlyEmergencyAuthorized {
// Mint just enough to get the trove out of zombie mode, using all the collateral we have
TroveOps.adjustZombieTrove(
BORROWER_OPERATIONS, troveId, balanceOfAsset(), balanceOfDebt(), _upperHint, _lowerHint
);
// Lend everything we have
_lendBorrowToken(balanceOfBorrowToken());
}
/// @notice Set the surplus detection floors used by `hasBorrowTokenSurplus()`
/// @param _minSurplusAbsolute Absolute minimum surplus required, in borrow token units
/// @param _minSurplusRelative Relative minimum surplus required, as basis points of current debt
function setSurplusFloors(uint256 _minSurplusAbsolute, uint256 _minSurplusRelative) external onlyManagement {
require(_minSurplusRelative <= _MAX_RELATIVE_SURPLUS, "!relativeSurplus");
minSurplusAbsolute = _minSurplusAbsolute;
minSurplusRelative = _minSurplusRelative;
}
/// @notice Allow (or disallow) a specific address to deposit
/// @dev Deposits can trigger new borrows, and Liquity charges an upfront fee on
/// every debt increase. If deposits were permissionless, a malicious actor
/// could repeatedly deposit/withdraw small amounts to force the strategy
/// to borrow and pay the fee many times, socializing those costs across
/// existing depositors. To prevent this griefing vector, deposits are gated
/// @param _address Address to allow or disallow
/// @param _allowed True to allow deposits from `_address`, false to block
function setAllowed(address _address, bool _allowed) external onlyManagement {
allowed[_address] = _allowed;
}
/// @notice Sweep of non-asset ERC20 tokens
/// @param _token The ERC20 token to sweep
function sweep(
ERC20 _token
) external onlyManagement {
require(_token != asset, "!asset");
_token.safeTransfer(TokenizedStrategy.management(), _token.balanceOf(address(this)));
}
// ===============================================================
// Write functions
// ===============================================================
/// @inheritdoc BaseLenderBorrower
function _leveragePosition(
uint256 _amount
) internal override {
// Do nothing if the trove is not active
if (TROVE_MANAGER.getTroveStatus(troveId) != ITroveManager.Status.active) return;
// Otherwise, business as usual
BaseLenderBorrower._leveragePosition(_amount);
}
/// @inheritdoc BaseLenderBorrower
function _supplyCollateral(
uint256 _amount
) internal override {
if (_amount > 0) BORROWER_OPERATIONS.addColl(troveId, _amount);
}
/// @inheritdoc BaseLenderBorrower
function _withdrawCollateral(
uint256 _amount
) internal override {
if (_amount > 0) BORROWER_OPERATIONS.withdrawColl(troveId, _amount);
}
/// @inheritdoc BaseLenderBorrower
function _borrow(
uint256 _amount
) internal override {
if (_amount > 0) BORROWER_OPERATIONS.withdrawBold(troveId, _amount, type(uint256).max);
}
/// @inheritdoc BaseLenderBorrower
function _repay(
uint256 _amount
) internal override {
// `repayBold()` makes sure we don't go below `MIN_DEBT`
if (_amount > 0) BORROWER_OPERATIONS.repayBold(troveId, _amount);
}
// ===============================================================
// View functions
// ===============================================================
/// @inheritdoc BaseLenderBorrower
function availableDepositLimit(
address _owner
) public view override returns (uint256) {
// We check `_owner == address(this)` because BaseLenderBorrower uses `availableDepositLimit(address(this))`
return allowed[_owner] || _owner == address(this) ? BaseLenderBorrower.availableDepositLimit(_owner) : 0;
}
/// @inheritdoc BaseLenderBorrower
/// @dev Returns the maximum collateral that can be withdrawn without reducing
/// the branch’s total collateral ratio (TCR) below the critical collateral ratio (CCR).
/// Withdrawal is limited by both:
/// - the CCR constraint at the branch level, and
/// - the base withdrawal constraints in BaseLenderBorrower
function _maxWithdrawal() internal view override returns (uint256) {
// Cache values for later use
uint256 _price = _getPrice(address(asset)) * _DECIMALS_DIFF;
uint256 _branchDebt = BORROWER_OPERATIONS.getEntireBranchDebt();
uint256 _branchCollateral = BORROWER_OPERATIONS.getEntireBranchColl();
// Collateral required to keep TCR >= CCR
uint256 _requiredColl = Math.ceilDiv(BORROWER_OPERATIONS.CCR() * _branchDebt, _price);
// Max collateral removable while staying >= CCR
uint256 _headroomByCCR = _branchCollateral > _requiredColl ? _branchCollateral - _requiredColl : 0;
// Final cap is the tighter of CCR constraint and base withdrawal cap
return Math.min(_headroomByCCR, BaseLenderBorrower._maxWithdrawal());
}
/// @inheritdoc BaseLenderBorrower
function _getPrice(
address _asset
) internal view override returns (uint256) {
// Not bothering with price feed checks because it's the same one Liquity uses
(, int256 _answer,,,) = PRICE_FEED.latestRoundData();
return _asset == borrowToken ? WAD / _DECIMALS_DIFF : uint256(_answer);
}
/// @inheritdoc BaseLenderBorrower
function _isSupplyPaused() internal pure override returns (bool) {
return false;
}
/// @inheritdoc BaseLenderBorrower
function _isBorrowPaused() internal view override returns (bool) {
// When the branch TCR falls below the CCR, BorrowerOperations blocks `withdrawBold()`,
// in that case we should not attempt to borrow.
// When TCR >= CCR, our own target (<= 90% of MCR) is stricter than the CCR requirement,
// so don’t need to impose an additional cap in `_maxBorrowAmount()`
return _isTCRBelowCCR();
}
/// @inheritdoc BaseLenderBorrower
function _isLiquidatable() internal view override returns (bool) {
// `getCurrentICR()` expects the price to be in 1e18 format
return
TROVE_MANAGER.getCurrentICR(troveId, _getPrice(address(asset)) * _DECIMALS_DIFF) < BORROWER_OPERATIONS.MCR();
}
/// @inheritdoc BaseLenderBorrower
function _maxCollateralDeposit() internal pure override returns (uint256) {
return type(uint256).max;
}
/// @inheritdoc BaseLenderBorrower
function _maxBorrowAmount() internal pure override returns (uint256) {
return type(uint256).max;
}
/// @inheritdoc BaseLenderBorrower
function getNetBorrowApr(
uint256 /*_newAmount*/
) public pure override returns (uint256) {
return 0; // Assumes always profitable to borrow
}
/// @inheritdoc BaseLenderBorrower
function getNetRewardApr(
uint256 /*_newAmount*/
) public pure override returns (uint256) {
return 1; // Assumes always profitable to borrow
}
/// @inheritdoc BaseLenderBorrower
function getLiquidateCollateralFactor() public view override returns (uint256) {
return WAD * WAD / BORROWER_OPERATIONS.MCR();
}
/// @inheritdoc BaseLenderBorrower
function balanceOfCollateral() public view override returns (uint256) {
return TROVE_MANAGER.getLatestTroveData(troveId).entireColl;
}
/// @inheritdoc BaseLenderBorrower
function balanceOfDebt() public view override returns (uint256) {
return TROVE_MANAGER.getLatestTroveData(troveId).entireDebt;
}
/// @notice Returns true when we hold more borrow token than we owe by a _meaningful_ margin
/// @dev Purpose is to detect redemptions/liquidations while avoiding action on normal profit
/// @return True if `surplus > max(absoluteFloor, relativeFloor)`
function hasBorrowTokenSurplus() public view returns (bool) {
uint256 _loose = balanceOfBorrowToken();
uint256 _have = balanceOfLentAssets() + _loose;
uint256 _owe = balanceOfDebt();
if (_have <= _owe) return false;
// Positive surplus we could realize by selling borrow token back to collateral
uint256 _surplus = _have - _owe;
// Use the stricter of the two floors (absolute or relative)
uint256 _floor = Math.max(
minSurplusAbsolute, // Absolute floor
_owe * minSurplusRelative / MAX_BPS // Relative floor (some percentage of current debt)
);
// Consider surplus only when higher than the higher floor
return _surplus > _floor;
}
/// @notice Check if the branch Total Collateral Ratio (TCR) has fallen below the Critical Collateral Ratio (CCR)
/// @return True if TCR < CCR, false otherwise
function _isTCRBelowCCR() internal view returns (bool) {
return LiquityMath._computeCR(
BORROWER_OPERATIONS.getEntireBranchColl(),
BORROWER_OPERATIONS.getEntireBranchDebt(),
_getPrice(address(asset)) * _DECIMALS_DIFF // LiquityMath expects 1e18 format
) < BORROWER_OPERATIONS.CCR();
}
// ===============================================================
// Lender vault
// ===============================================================
/// @inheritdoc BaseLenderBorrower
function _lendBorrowToken(
uint256 _amount
) internal override {
LenderOps.lend(lenderVault, _amount);
}
/// @inheritdoc BaseLenderBorrower
function _withdrawBorrowToken(
uint256 _amount
) internal override {
LenderOps.withdraw(lenderVault, _amount);
}
/// @inheritdoc BaseLenderBorrower
function _lenderMaxDeposit() internal view override returns (uint256) {
return LenderOps.maxDeposit(lenderVault);
}
/// @inheritdoc BaseLenderBorrower
function _lenderMaxWithdraw() internal view override returns (uint256) {
return LenderOps.maxWithdraw(lenderVault);
}
/// @inheritdoc BaseLenderBorrower
function balanceOfLentAssets() public view override returns (uint256) {
return LenderOps.balanceOfAssets(lenderVault);
}
// ===============================================================
// Harvest / Token conversions
// ===============================================================
/// @inheritdoc BaseLenderBorrower
function _tendTrigger() internal view override returns (bool) {
// If base fee is acceptable and we have a borrow token surplus (likely from redemption/liquidation),
// tend to (1) minimize exchange rate exposure and (2) minimize the risk of someone using our borrowing capacity
// before we manage to borrow again, such that any new debt we take will lead to TCR < CCR
//
// (2) chain of events: [1] we are redeemed [2] we have no debt but some collateral
// [3] someone hops in and uses our collateral to borrow above CCR [4] we cannot take new debt because it will lead to TCR < CCR
if (_isBaseFeeAcceptable() && hasBorrowTokenSurplus()) return true;
// If the trove is not active, do nothing
if (TROVE_MANAGER.getTroveStatus(troveId) != ITroveManager.Status.active) return false;
// Finally, business as usual
return BaseLenderBorrower._tendTrigger();
}
/// @inheritdoc BaseLenderBorrower
function _tend(
uint256 /*_totalIdle*/
) internal override {
// Sell any surplus borrow token
_claimAndSellRewards();
// Using `balanceOfAsset()` because `_totalIdle` may increase after selling borrow token surplus
return BaseLenderBorrower._tend(balanceOfAsset());
}
/// @inheritdoc BaseLenderBorrower
function _claimRewards() internal pure override {
return;
}
/// @inheritdoc BaseLenderBorrower
function _claimAndSellRewards() internal override {
uint256 _loose = balanceOfBorrowToken();
uint256 _have = balanceOfLentAssets() + _loose;
uint256 _owe = balanceOfDebt();
if (_owe >= _have) return;
uint256 _toSell = _have - _owe;
if (_toSell > _loose) _withdrawBorrowToken(_toSell - _loose);
_loose = balanceOfBorrowToken();
_sellBorrowToken(Math.min(_toSell, _loose));
}
/// @inheritdoc BaseLenderBorrower
function _sellBorrowToken(
uint256 _amount
) internal override {
EXCHANGE.swap(
_amount,
0, // minAmount
true // fromBorrow
);
}
/// @inheritdoc BaseLenderBorrower
function _buyBorrowToken() internal virtual override {
uint256 _borrowTokenStillOwed = borrowTokenOwedBalance();
uint256 _maxAssetBalance = _fromUsd(_toUsd(_borrowTokenStillOwed, borrowToken), address(asset));
_buyBorrowToken(_maxAssetBalance);
}
/// @notice Buy borrow token
/// @param _amount The amount of asset to sale
function _buyBorrowToken(
uint256 _amount
) internal {
EXCHANGE.swap(
_amount,
0, // minAmount
false // fromBorrow
);
}
/// @inheritdoc BaseLenderBorrower
function _emergencyWithdraw(
uint256 _amount
) internal override {
if (_amount > 0) _withdrawBorrowToken(Math.min(_amount, _lenderMaxWithdraw()));
TroveOps.onEmergencyWithdraw(
TROVE_MANAGER, BORROWER_OPERATIONS, COLL_SURPLUS_POOL, TokenizedStrategy.management(), troveId
);
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.3) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.0;
import "../IERC20.sol";
import "../extensions/IERC20Permit.sol";
import "../../../utils/Address.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
using Address for address;
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
/**
* @dev Deprecated. This function has issues similar to the ones found in
* {IERC20-approve}, and its usage is discouraged.
*
* Whenever possible, use {safeIncreaseAllowance} and
* {safeDecreaseAllowance} instead.
*/
function safeApprove(IERC20 token, address spender, uint256 value) internal {
// safeApprove should only be called when setting an initial allowance,
// or when resetting it to zero. To increase and decrease it, use
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value));
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value));
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value);
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`.
* Revert on invalid signature.
*/
function safePermit(
IERC20Permit token,
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
uint256 nonceBefore = token.nonces(owner);
token.permit(owner, spender, value, deadline, v, r, s);
uint256 nonceAfter = token.nonces(owner);
require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
// and not revert is the subcall reverts.
(bool success, bytes memory returndata) = address(token).call(data);
return
success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token));
}
}
"
},
"src/libraries/TroveOps.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.24;
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ITroveManager} from "../interfaces/ITroveManager.sol";
import {ICollSurplusPool} from "../interfaces/ICollSurplusPool.sol";
import {IBorrowerOperations} from "../interfaces/IBorrowerOperations.sol";
library TroveOps {
using SafeERC20 for IERC20;
// ===============================================================
// Constants
// ===============================================================
/// @notice Liquity's minimum amount of net Bold debt a trove must have
/// If a trove is redeeemed and the debt is less than this, it will be considered a zombie trove
uint256 public constant MIN_DEBT = 2_000 * 1e18;
/// @notice Liquity's amount of WETH to be locked in gas pool when opening a trove
/// Will be pulled from the contract on `_openTrove`
uint256 public constant ETH_GAS_COMPENSATION = 0.0375 ether;
/// @notice Minimum annual interest rate
uint256 public constant MIN_ANNUAL_INTEREST_RATE = 1e18 / 100 / 2; // 0.5%
/// @notice WETH token
IERC20 private constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
// ===============================================================
// Write functions
// ===============================================================
/// @notice Opens a trove with the given parameters
/// @dev Requires the caller to pay the gas compensation in WETH
/// @dev Should be called through a private RPC to avoid fee slippage
/// @param _borrowerOperations The borrower operations contract
/// @param _collAmount The amount of collateral to deposit
/// @param _upperHint The upper hint for the trove
/// @param _lowerHint The lower hint for the trove
/// @return The ID of the newly opened trove
function openTrove(
IBorrowerOperations _borrowerOperations,
uint256 _collAmount,
uint256 _upperHint,
uint256 _lowerHint
) external returns (uint256) {
WETH.safeTransferFrom(msg.sender, address(this), ETH_GAS_COMPENSATION);
return _borrowerOperations.openTrove(
address(this), // owner
block.timestamp, // ownerIndex
_collAmount,
MIN_DEBT, // boldAmount
_upperHint,
_lowerHint,
MIN_ANNUAL_INTEREST_RATE, // annualInterestRate
type(uint256).max, // maxUpfrontFee
address(0), // addManager
address(0), // removeManager
address(0) // receiver
);
}
/// @notice Adjust the interest rate of the trove
/// @dev Will fail if the trove is not active
/// @dev Should be called through a private RPC to avoid fee slippage
/// @param _borrowerOperations The borrower operations contract
/// @param _troveId The ID of the trove to adjust
/// @param _newAnnualInterestRate New annual interest rate
/// @param _upperHint Upper hint
/// @param _lowerHint Lower hint
function adjustTroveInterestRate(
IBorrowerOperations _borrowerOperations,
uint256 _troveId,
uint256 _newAnnualInterestRate,
uint256 _upperHint,
uint256 _lowerHint
) external {
_borrowerOperations.adjustTroveInterestRate(
_troveId,
_newAnnualInterestRate,
_upperHint,
_lowerHint,
type(uint256).max // maxUpfrontFee
);
}
/// @notice Adjust zombie trove
/// @dev Might need to be called after a redemption, if our debt is below `MIN_DEBT`
/// @dev Will fail if the trove is not in zombie mode
/// @dev Should be called through a private RPC to avoid fee slippage
/// @param _borrowerOperations The borrower operations contract
/// @param _troveId The ID of the trove to adjust
/// @param _balanceOfAsset Balance of asset
/// @param _balanceOfDebt Balance of debt
/// @param _upperHint Upper hint
/// @param _lowerHint Lower hint
function adjustZombieTrove(
IBorrowerOperations _borrowerOperations,
uint256 _troveId,
uint256 _balanceOfAsset,
uint256 _balanceOfDebt,
uint256 _upperHint,
uint256 _lowerHint
) external {
_borrowerOperations.adjustZombieTrove(
_troveId,
_balanceOfAsset, // collChange
true, // isCollIncrease
MIN_DEBT - _balanceOfDebt, // boldChange
true, // isDebtIncrease
_upperHint,
_lowerHint,
type(uint256).max // maxUpfrontFee
);
}
/// @notice Close the trove if it's active or try to claim leftover collateral if we were liquidated
/// @dev `_management` will get back the ETH gas compensation if we're closing the trove
/// @param _troveManager The trove manager contract
/// @param _borrowerOperations The borrower operations contract
/// @param _borrowerOperations The collateral surplus pool contract
/// @param _management The management address
/// @param _troveId The ID of the trove
function onEmergencyWithdraw(
ITroveManager _troveManager,
IBorrowerOperations _borrowerOperations,
ICollSurplusPool _collSurplusPool,
address _management,
uint256 _troveId
) external {
if (_troveManager.getTroveStatus(_troveId) == ITroveManager.Status.active) {
_borrowerOperations.closeTrove(_troveId);
if (WETH.balanceOf(address(this)) >= ETH_GAS_COMPENSATION) {
WETH.safeTransfer(_management, ETH_GAS_COMPENSATION);
}
} else if (_troveManager.getTroveStatus(_troveId) == ITroveManager.Status.closedByLiquidation) {
if (_collSurplusPool.getCollateral(address(this)) > 0) _borrowerOperations.claimCollateral();
}
}
}
"
},
"src/libraries/LenderOps.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.24;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
library LenderOps {
// ===============================================================
// View functions
// ===============================================================
/// @notice Returns the maximum amount of borrowed token we can lend
/// @param _vault The vault contract (i.e. sUSDaf)
/// @return The maximum amount of borrowed token we can lend
function maxDeposit(IERC4626 _vault) external view returns (uint256) {
return _vault.maxDeposit(address(this));
}
/// @notice Returns the maximum amount of assets we can withdraw from the vault
/// @param _vault The vault contract (i.e. sUSDaf)
/// @return The maximum amount of borrowed token we can withdraw from the vault
function maxWithdraw(IERC4626 _vault) external view returns (uint256) {
return _vault.maxWithdraw(address(this));
}
/// @notice Returns the amount of borrow token we have lent
/// @param _vault The vault contract (i.e. sUSDaf)
/// @return The amount of borrow token we have lent
function balanceOfAssets(IERC4626 _vault) external view returns (uint256) {
return _vault.convertToAssets(_vault.balanceOf(address(this)));
}
// ===============================================================
// Write functions
// ===============================================================
/// @notice Deposits borrowed tokens into the vault
/// @param _vault The vault contract (i.e. sUSDaf)
/// @param _amount The amount of tokens to deposit
function lend(IERC4626 _vault, uint256 _amount) external {
_vault.deposit(_amount, address(this));
}
/// @notice Withdraws tokens from the vault
/// @param _vault The vault contract (i.e. sUSDaf)
/// @param _amount The amount of tokens to withdraw
function withdraw(IERC4626 _vault, uint256 _amount) external {
if (_amount > 0) {
// How much sUSDaf
uint256 _shares = Math.min(_vault.previewWithdraw(_amount), _vault.balanceOf(address(this)));
// Redeem sUSDaf to USDaf
_vault.redeem(_shares, address(this), address(this));
}
}
}
"
},
"src/libraries/LiquityMath.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
library LiquityMath {
uint256 constant DECIMAL_PRECISION = 1e18;
function _min(uint256 _a, uint256 _b) internal pure returns (uint256) {
return (_a < _b) ? _a : _b;
}
function _max(uint256 _a, uint256 _b) internal pure returns (uint256) {
return (_a >= _b) ? _a : _b;
}
function _sub_min_0(uint256 _a, uint256 _b) internal pure returns (uint256) {
return (_a > _b) ? _a - _b : 0;
}
/*
* Multiply two decimal numbers and use normal rounding rules:
* -round product up if 19'th mantissa digit >= 5
* -round product down if 19'th mantissa digit < 5
*
* Used only inside the exponentiation, _decPow().
*/
function decMul(uint256 x, uint256 y) internal pure returns (uint256 decProd) {
uint256 prod_xy = x * y;
decProd = (prod_xy + DECIMAL_PRECISION / 2) / DECIMAL_PRECISION;
}
/*
* _decPow: Exponentiation function for 18-digit decimal base, and integer exponent n.
*
* Uses the efficient "exponentiation by squaring" algorithm. O(log(n)) complexity.
*
* Called by function CollateralRegistry._calcDecayedBaseRate, that represent time in units of minutes
*
* The exponent is capped to avoid reverting due to overflow. The cap 525600000 equals
* "minutes in 1000 years": 60 * 24 * 365 * 1000
*
* If a period of > 1000 years is ever used as an exponent in either of the above functions, the result will be
* negligibly different from just passing the cap, since:
*
* In function 1), the decayed base rate will be 0 for 1000 years or > 1000 years
* In function 2), the difference in tokens issued at 1000 years and any time > 1000 years, will be negligible
*/
function _decPow(uint256 _base, uint256 _minutes) internal pure returns (uint256) {
if (_minutes > 525600000) _minutes = 525600000; // cap to avoid overflow
if (_minutes == 0) return DECIMAL_PRECISION;
uint256 y = DECIMAL_PRECISION;
uint256 x = _base;
uint256 n = _minutes;
// Exponentiation-by-squaring
while (n > 1) {
if (n % 2 == 0) {
x = decMul(x, x);
n = n / 2;
} else {
// if (n % 2 != 0)
y = decMul(x, y);
x = decMul(x, x);
n = (n - 1) / 2;
}
}
return decMul(x, y);
}
function _getAbsoluteDifference(uint256 _a, uint256 _b) internal pure returns (uint256) {
return (_a >= _b) ? _a - _b : _b - _a;
}
function _computeCR(uint256 _coll, uint256 _debt, uint256 _price) internal pure returns (uint256) {
if (_debt > 0) {
uint256 newCollRatio = _coll * _price / _debt;
return newCollRatio;
}
// Return the maximal value for uint256 if the debt is 0. Represents "infinite" CR.
else {
// if (_debt == 0)
return 2 ** 256 - 1;
}
}
}
"
},
"src/interfaces/IExchange.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IExchange {
function SMS() external view returns (address);
function BORROW() external view returns (address);
function COLLATERAL() external view returns (address);
function swap(uint256 _amount, uint256 _minAmount, bool _fromBorrow) external returns (uint256);
function sweep(
IERC20 _token
) external;
}
"
},
"src/interfaces/AggregatorInterface.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.24;
interface AggregatorInterface {
function decimals() external view returns (uint8);
function latestAnswer() external view returns (int256);
function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}
"
},
"src/interfaces/IAddressesRegistry.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.24;
import {IPriceFeed} from "./IPriceFeed.sol";
import {ICollSurplusPool} from "./ICollSurplusPool.sol";
import {IBorrowerOperations} from "./IBorrowerOperations.sol";
import {ITroveManager} from "./ITroveManager.sol";
interface IAddressesRegistry {
function collToken() external view returns (address);
function boldToken() external view returns (address);
function priceFeed() external view returns (IPriceFeed);
function collSurplusPool() external view returns (ICollSurplusPool);
function borrowerOperations() external view returns (IBorrowerOperations);
function troveManager() external view returns (ITroveManager);
}
"
},
"src/BaseLenderBorrower.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {BaseHealthCheck, ERC20} from "@periphery/Bases/HealthCheck/BaseHealthCheck.sol";
/**
* @title Base Lender Borrower
*/
abstract contract BaseLenderBorrower is BaseHealthCheck {
using SafeERC20 for ERC20;
uint256 internal constant WAD = 1e18;
/// The token we will be borrowing/supplying.
address public immutable borrowToken;
/// If set to true, the strategy will not try to repay debt by selling rewards or asset.
bool public leaveDebtBehind;
/// @notice Target Loan-To-Value (LTV) multiplier in Basis Points
/// @dev Represents the ratio up to which we will borrow, relative to the liquidation threshold.
/// LTV is the debt-to-collateral ratio. Default is set to 70% of the liquidation LTV.
uint16 public targetLTVMultiplier;
/// @notice Warning Loan-To-Value (LTV) multiplier in Basis Points
/// @dev Represents the ratio at which we will start repaying the debt to avoid liquidation
/// Default is set to 80% of the liquidation LTV
uint16 public warningLTVMultiplier; // 80% of liquidation LTV
/// @notice Slippage tolerance (in basis points) for swaps
uint64 public slippage;
/// @notice Deposit limit for the strategy.
uint256 public depositLimit;
/// The max the base fee (in gwei) will be for a tend
uint256 public maxGasPriceToTend;
/// Thresholds: lower limit on how much base token can be borrowed at a time.
uint256 internal minAmountToBorrow;
/// The lender vault that will be used to lend and borrow.
IERC4626 public immutable lenderVault;
/**
* @param _asset The address of the asset we are lending/borrowing.
* @param _name The name of the strategy.
* @param _borrowToken The address of the borrow token.
*/
constructor(
address _asset,
string memory _name,
address _borrowToken,
address _lenderVault
) BaseHealthCheck(_asset, _name) {
borrowToken = _borrowToken;
// Set default variables
depositLimit = type(uint256).max;
targetLTVMultiplier = 7_000;
warningLTVMultiplier = 8_000;
leaveDebtBehind = false;
maxGasPriceToTend = 200 * 1e9;
slippage = 500;
// Allow for address(0) for versions that don't use 4626 vault.
if (_lenderVault != address(0)) {
lenderVault = IERC4626(_lenderVault);
require(lenderVault.asset() == _borrowToken, "!lenderVault");
ERC20(_borrowToken).safeApprove(_lenderVault, type(uint256).max);
}
}
/// ----------------- SETTERS -----------------
/**
* @notice Set the deposit limit for the strategy
* @param _depositLimit New deposit limit
*/
function setDepositLimit(
uint256 _depositLimit
) external onlyManagement {
depositLimit = _depositLimit;
}
/**
* @notice Set the target and warning LTV multipliers
* @param _targetLTVMultiplier New target LTV multiplier
* @param _warningLTVMultiplier New warning LTV multiplier
* @dev Target must be less than warning, warning must be <= 9000, target cannot be 0
*/
function setLtvMultipliers(uint16 _targetLTVMultiplier, uint16 _warningLTVMultiplier) external onlyManagement {
require(
_warningLTVMultiplier <= 9_000 && _targetLTVMultiplier < _warningLTVMultiplier && _targetLTVMultiplier != 0,
"invalid LTV"
);
targetLTVMultiplier = _targetLTVMultiplier;
warningLTVMultiplier = _warningLTVMultiplier;
}
/**
* @notice Set whether to leave debt behind
* @param _leaveDebtBehind New leave debt behind setting
*/
function setLeaveDebtBehind(
bool _leaveDebtBehind
) external onlyManagement {
leaveDebtBehind = _leaveDebtBehind;
}
/**
* @notice Set the maximum gas price for tending
* @param _maxGasPriceToTend New maximum gas price
*/
function setMaxGasPriceToTend(
uint256 _maxGasPriceToTend
) external onlyManagement {
maxGasPriceToTend = _maxGasPriceToTend;
}
/**
* @notice Set the slippage tolerance
* @param _slippage New slippage tolerance in basis points
*/
function setSlippage(
uint256 _slippage
) external onlyManagement {
require(_slippage < MAX_BPS, "slippage");
slippage = uint64(_slippage);
}
/*//////////////////////////////////////////////////////////////
NEEDED TO BE OVERRIDDEN BY STRATEGIST
//////////////////////////////////////////////////////////////*/
/**
* @dev Should deploy up to '_amount' of 'asset' in the yield source.
*
* This function is called at the end of a {deposit} or {mint}
* call. Meaning that unless a whitelist is implemented it will
* be entirely permissionless and thus can be sandwiched or otherwise
* manipulated.
*
* @param _amount The amount of 'asset' that the strategy should attempt
* to deposit in the yield source.
*/
function _deployFunds(
uint256 _amount
) internal virtual override {
_leveragePosition(_amount);
}
/**
* @dev Will attempt to free the '_amount' of 'asset'.
*
* The amount of 'asset' that is already loose has already
* been accounted for.
*
* This function is called during {withdraw} and {redeem} calls.
* Meaning that unless a whitelist is implemented it will be
* entirely permissionless and thus can be sandwiched or otherwise
* manipulated.
*
* Should not rely on asset.balanceOf(address(this)) calls other than
* for diff accounting purposes.
*
* Any difference between `_amount` and what is actually freed will be
* counted as a loss and passed on to the withdrawer. This means
* care should be taken in times of illiquidity. It may be better to revert
* if withdraws are simply illiquid so not to realize incorrect losses.
*
* @param _amount, The amount of 'asset' to be freed.
*/
function _freeFunds(
uint256 _amount
) internal virtual override {
_liquidatePosition(_amount);
}
/**
* @dev Internal function to harvest all rewards, redeploy any idle
* funds and return an accurate accounting of all funds currently
* held by the Strategy.
*
* This should do any needed harvesting, rewards selling, accrual,
* redepositing etc. to get the most accurate view of current assets.
*
* NOTE: All applicable assets including loose assets should be
* accounted for in this function.
*
* Care should be taken when relying on oracles or swap values rather
* than actual amounts as all Strategy profit/loss accounting will
* be done based on this returned value.
*
* This can still be called post a shutdown, a strategist can check
* `TokenizedStrategy.isShutdown()` to decide if funds should be
* redeployed or simply realize any profits/losses.
*
* @return _totalAssets A trusted and accurate account for the total
* amount of 'asset' the strategy currently holds including idle funds.
*/
function _harvestAndReport() internal virtual override returns (uint256 _totalAssets) {
/// 1. claim rewards, 2. even borrowToken deposits and borrows 3. sell remainder of rewards to asset.
_claimAndSellRewards();
/// Leverage all the asset we have or up to the supply cap.
/// We want check our leverage even if balance of asset is 0.
_leveragePosition(Math.min(balanceOfAsset(), availableDepositLimit(address(this))));
/// Base token owed should be 0 here but we count it just in case
_totalAssets = balanceOfAsset() + balanceOfCollateral() - _borrowTokenOwedInAsset();
}
/*//////////////////////////////////////////////////////////////
OPTIONAL TO OVERRIDE BY STRATEGIST
//////////////////////////////////////////////////////////////*/
/**
* @dev Optional function for strategist to override that can
* be called in between reports.
*
* If '_tend' is used tendTrigger() will also need to be overridden.
*
* This call can only be called by a permissioned role so may be
* through protected relays.
*
* This can be used to harvest and compound rewards, deposit idle funds,
* perform needed position maintenance or anything else that doesn't need
* a full report for.
*
* EX: A strategy that can not deposit funds without getting
* sandwiched can use the tend when a certain threshold
* of idle to totalAssets has been reached.
*
* The TokenizedStrategy contract will do all needed debt and idle updates
* after this has finished and will have no effect on PPS of the strategy
* till report() is called.
*
* @param _totalIdle The current amount of idle funds that are available to deploy.
*/
function _tend(
uint256 _totalIdle
) internal virtual override {
/// If the cost to borrow > rewards rate we will pull out all funds to not report a loss
if (getNetBorrowApr(0) > getNetRewardApr(0)) {
/// Liquidate everything so not to report a loss
_liquidatePosition(balanceOfCollateral());
/// Return since we don't asset to do anything else
return;
}
/// Else we need to either adjust LTV up or down.
_leveragePosition(Math.min(_totalIdle, availableDepositLimit(address(this))));
}
/**
* @dev Optional trigger to override if tend() will be used by the strategy.
* This must be implemented if the strategy hopes to invoke _tend().
*
* @return . Should return true if tend() should be called by keeper or false if not.
*/
function _tendTrigger() internal view virtual override returns (bool) {
/// If we are in danger of being liquidated tend no matter what
if (_isLiquidatable()) return true;
if (TokenizedStrategy.totalAssets() == 0) return false;
/// We adjust position if:
/// 1. LTV ratios are not in the HEALTHY range (either we take on more debt or repay debt)
/// 2. costs are acceptable
uint256 collateralInUsd = _toUsd(balanceOfCollateral(), address(asset));
uint256 debtInUsd = _toUsd(balanceOfDebt(), borrowToken);
uint256 currentLTV = collateralInUsd > 0 ? (debtInUsd * WAD) / collateralInUsd : 0;
/// Check if we are over our warning LTV
if (currentLTV > _getWarningLTV()) return true;
if (_isSupplyPaused() || _isBorrowPaused()) return false;
uint256 targetLTV = _getTargetLTV();
/// If we are still levered and Borrowing costs are too high.
if (currentLTV != 0 && getNetBorrowApr(0) > getNetRewardApr(0)) {
/// Tend if base fee is acceptable.
return _isBaseFeeAcceptable();
/// IF we are lower than our target. (we need a 10% (1000bps) difference)
} else if ((currentLTV < targetLTV && targetLTV - currentLTV > 1e17)) {
/// Make sure the increase in debt would keep borrowing costs healthy.
uint256 targetDebtUsd = (collateralInUsd * targetLTV) / WAD;
uint256 amountToBorrowUsd;
unchecked {
amountToBorrowUsd = targetDebtUsd - debtInUsd; // safe bc we checked ratios
}
/// Convert to borrowToken
uint256 amountToBorrowBT =
Math.min(_fromUsd(amountToBorrowUsd, borrowToken), Math.min(_lenderMaxDeposit(), _maxBorrowAmount()));
if (amountToBorrowBT == 0) return false;
/// We want to make sure that the reward apr > borrow apr so we don't report a loss
/// Borrowing will cause the borrow apr to go up and the rewards apr to go down
if (getNetBorrowApr(amountToBorrowBT) < getNetRewardApr(amountToBorrowBT)) {
/// Borrowing costs are healthy and WE NEED TO TAKE ON MORE DEBT
return _isBaseFeeAcceptable();
}
}
return false;
}
/**
* @notice Gets the max amount of `asset` that an address can deposit.
* @dev Defaults to an unlimited amount for any address. But can
* be overridden by strategists.
*
* This function will be called before any deposit or mints to enforce
* any limits desired by the strategist. This can be used for either a
* traditional deposit limit or for implementing a whitelist etc.
*
* EX:
* if(isAllowed[_owner]) return super.availableDepositLimit(_owner);
*
* This does not need to take into account any conversion rates
* from shares to assets. But should know that any non max uint256
* amounts may be converted to shares. So it is recommended to keep
* custom amounts low enough as not to cause overflow when multiplied
* by `totalSupply`.
*
* @param . The address that is depositing into the strategy.
* @return . The available amount the `_owner` can deposit in terms of `asset`
*/
function availableDepositLimit(
address /*_owner*/
) public view virtual override returns (uint256) {
/// We need to be able to both supply and withdraw on deposits.
if (_isSupplyPaused() || _isBorrowPaused()) return 0;
uint256 currentAssets = TokenizedStrategy.totalAssets();
uint256 limit = depositLimit > currentAssets ? depositLimit - currentAssets : 0;
uint256 maxDeposit = Math.min(_maxCollateralDeposit(), limit);
uint256 maxBorrow = Math.min(_lenderMaxDeposit(), _maxBorrowAmount());
// Either the max supply or the max we could borrow / targetLTV.
return Math.min(maxDeposit, _fromUsd((_toUsd(maxBorrow, borrowToken) * WAD) / _getTargetLTV(), address(asset)));
}
/**
* @notice Gets the max amount of `asset` that can be withdrawn.
* @dev Defaults to an unlimited amount for any address. But can
* be overridden by strategists.
*
* This function will be called before any withdraw or redeem to enforce
* any limits desired by the strategist. This can be used for illiquid
* or sandwichable strategies. It should never be lower than `totalIdle`.
*
* EX:
* return TokenIzedStrategy.totalIdle();
*
* This does not need to take into account the `_owner`'s share balance
* or conversion rates from shares to assets.
*
* @param . The address that is withdrawing from the strategy.
* @return . The available amount that can be withdrawn in terms of `asset`
*/
function availableWithdrawLimit(
address /*_owner*/
) public view virtual override returns (uint256) {
/// Default liquidity is the balance of collateral + 1 for rounding.
uint256 liquidity = balanceOfCollateral() + 1;
uint256 lenderLiquidity = _lenderMaxWithdraw();
/// If we can't withdraw or supply, set liquidity = 0.
if (lenderLiquidity < balanceOfLentAssets()) {
/// Adjust liquidity based on withdrawing the full amount of debt.
unchecked {
liquidity = ((_fromUsd(_toUsd(lenderLiquidity, borrowToken), address(asset)) * WAD) / _getTargetLTV());
}
}
return balanceOfAsset() + liquidity;
}
/// ----------------- INTERNAL FUNCTIONS SUPPORT ----------------- \\
/**
* @notice Adjusts the leverage position of the strategy based on current and target Loan-to-Value (LTV) ratios.
* @dev All debt and collateral calculations are done in USD terms. LTV values are represented in 1e18 format.
* @param _amount The amount to be supplied to adjust the leverage position,
*/
function _leveragePosition(
uint256 _amount
) internal virtual {
/// Supply the given amount to the strategy.
// This function internally checks for zero amounts.
_supplyCollateral(_amount);
uint256 collateralInUsd = _toUsd(balanceOfCollateral(), address(asset));
/// Convert debt to USD
uint256 debtInUsd = _toUsd(balanceOfDebt(), borrowToken);
/// LTV numbers are always in WAD
uint256 currentLTV = collateralInUsd > 0 ? (debtInUsd * WAD) / collateralInUsd : 0;
uint256 targetLTV = _getTargetLTV(); // 70% under default liquidation Threshold
/// decide in which range we are and act accordingly:
/// SUBOPTIMAL(borrow) (e.g. from 0 to 70% liqLTV)
/// HEALTHY(do nothing) (e.g. from 70% to 80% liqLTV)
/// UNHEALTHY(repay) (e.g. from 80% to 100% liqLTV)
if (targetLTV > currentLTV) {
/// SUBOPTIMAL RATIO: our current Loan-to-Value is lower than what we want
/// we need to take on more debt
uint256 targetDebtUsd = (collateralInUsd * targetLTV) / WAD;
uint256 amountToBorrowUsd;
unchecked {
amountToBorrowUsd = targetDebtUsd - debtInUsd; // safe bc we checked ratios
}
/// convert to borrowToken
uint256 amountToBorrowBT =
Math.min(_fromUsd(amountToBorrowUsd, borrowToken), Math.min(_lenderMaxDeposit(), _maxBorrowAmount()));
/// We want to make sure that the reward apr > borrow apr so we don't report a loss
/// Borrowing will cause the borrow apr to go up and the rewards apr to go down
if (getNetBorrowApr(amountToBorrowBT) > getNetRewardApr(amountToBorrowBT)) {
/// If we would push it over the limit don't borrow anything
amountToBorrowBT = 0;
}
/// Need to have at least the min threshold
if (amountToBorrowBT > minAmountToBorrow) _borrow(amountToBorrowBT);
} else if (currentLTV > _getWarningLTV()) {
/// UNHEALTHY RATIO
/// we repay debt to set it to targetLTV
uint256 targetDebtUsd = (targetLTV * collateralInUsd) / WAD;
/// Withdraw the difference from the Depositor
_withdrawFromLender(_fromUsd(debtInUsd - targetDebtUsd, borrowToken));
/// Repay the borrowToken debt.
_repayTokenDebt();
}
// Deposit any loose base token that was borrowed.
uint256 borrowTokenBalance = balanceOfBorrowToken();
if (borrowTokenBalance > 0) _lendBorrowToken(borrowTokenBalance);
}
/**
* @notice Liquidates the position to ensure the needed amount while maintaining healthy ratios.
* @dev All debt, collateral, and needed amounts are calculated in USD. The needed amount is represented in the asset.
* @param _needed The amount required in the asset.
*/
function _liquidatePosition(
uint256 _needed
) internal virtual {
/// Cache balance for withdraw checks
uint256 balance = balanceOfAsset();
/// We first repay whatever we need to repay to keep healthy ratios
_withdrawFromLender(_calculateAmountToRepay(_needed));
/// we repay the borrowToken debt with the amount withdrawn from the vault
_repayTokenDebt();
// Withdraw as much as we can up to the amount needed while maintaining a health ltv
_withdrawCollateral(Math.min(_needed, _maxWithdrawal()));
/// We check if we withdrew less than expected, and we do have not more borrowToken
/// left AND should harvest or buy borrowToken with asset (potentially realising losses)
if (
/// if we didn't get enough
/// still some debt remaining
/// but no capital to repay
/// And the leave debt flag is false.
_needed > balanceOfAsset() - balance && balanceOfDebt() > 0 && balanceOfLentAssets() == 0
&& !leaveDebtBehind
) {
/// using this part of code may result in losses but it is necessary to unlock full collateral
/// in case of wind down. This should only occur when depleting the strategy so we buy the full
/// amount of our remaining debt. We buy borrowToken first with available rewards then with asset.
_buyBorrowToken();
/// we repay debt to actually unlock collateral
/// after this, balanceOfDebt should be 0
_repayTokenDebt();
/// then we try withdraw once more
/// still withdraw with target LTV since management can potentially save any left over manually
_withdrawCollateral(_maxWithdrawal());
}
}
/**
* @notice Calculates max amount that can be withdrawn while maintaining healthy LTV ratio
* @dev Considers current collateral and debt amounts
* @return The max amount of collateral available for withdrawal
*/
function _maxWithdrawal() internal view virtual returns (uint256) {
uint256 collateral = balanceOfCollateral();
uint256 debt = balanceOfDebt();
/// If there is no debt we can withdraw everything
if (debt == 0) return collateral;
uint256 debtInUsd = _toUsd(debt, borrowToken);
/// What we need to maintain a health LTV
uint256 neededCollateral = _fromUsd((debtInUsd * WAD) / _getTargetLTV(), address(asset));
/// We need more collateral so we cant withdraw anything
if (neededCollateral > collateral) return 0;
/// Return the difference in terms of asset
unchecked {
return collateral - neededCollateral;
}
}
/**
* @notice Calculates amount of debt to repay to maintain healthy LTV ratio
* @dev Considers target LTV, amount being withdrawn, and current collateral/debt
* @param amount The withdrawal amount
* @return The amount of debt to repay
*/
function _calculateAmountToRepay(
uint256 amount
) internal view virtual returns (uint256) {
if (amount == 0) return 0;
uint256 collateral = balanceOfCollateral();
/// To unlock all collateral we must repay all the debt
if (amount >= collateral) return balanceOfDebt();
/// We check if the collateral that we are withdrawing leaves us in a risky range, we then take action
uint256 newCollateralUsd = _toUsd(collateral - amount, address(asset));
uint256 targetD
Submitted on: 2025-11-07 16:44:16
Comments
Log in to comment.
No comments yet.