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/helper/TermStrategyAprOracle.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {AprOracleBase} from "@periphery/AprOracle/AprOracleBase.sol";
import {Strategy} from "../Strategy.sol";
import {ITermRepoToken} from "../interfaces/term/ITermRepoToken.sol";
import {ITermDiscountRateAdapter} from "../interfaces/term/ITermDiscountRateAdapter.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ITermAuction} from "../interfaces/term/ITermAuction.sol";
// =============================================================
// TYPES
// =============================================================
enum VaultType {
STRATEGY,
MULTISTRAT
}
struct VaultMapping {
address vaultAddress;
VaultType vaultType;
}
interface IAprOracleGlobal {
function getWeightedAverageApr(
address _vault,
int256 _delta
) external view returns (uint256);
function getStrategyApr(
address _strategy,
int256 _debtChange
) external view returns (uint256 apr);
}
/**
* @title TermStrategyAprOracle
* @dev Oracle contract for providing APR data for Term Strategy
* @author Term Finance
*/
contract TermStrategyAprOracle is
AprOracleBase
{
// =============================================================
// ERRORS
// =============================================================
error UnknownUnderlyingVaultAprOracle();
error ZeroAddress();
// =============================================================
// EVENTS
// =============================================================
event ExternalAprOracleSet(address indexed underlyingVault, address indexed aprOracle);
event ExternalAprOracleRemoved(address indexed underlyingVault);
// =============================================================
// STATE VARIABLES
// =============================================================
IAprOracleGlobal public immutable globalOracle;
mapping(address => VaultMapping) public idleVaultRemappings;
// =============================================================
// CONSTRUCTOR
// =============================================================
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address admin_, address globalOracle_, address[] memory underlyingVaults_, VaultMapping[] memory vaultMappings_) AprOracleBase("TermStrategyAprOracle", admin_) {
require(underlyingVaults_.length == vaultMappings_.length, "Mismatched input lengths");
require(globalOracle_ != address(0), "ZERO ADDRESS");
globalOracle = IAprOracleGlobal(globalOracle_);
for (uint256 i = 0; i < underlyingVaults_.length; i++) {
idleVaultRemappings[underlyingVaults_[i]] = vaultMappings_[i];
}
}
// =============================================================
// VIEW FUNCTIONS
// =============================================================
function aprAfterDebtChange(
address _strategy,
int256 _debtChange
) public view override returns (uint256) {
Strategy termStrategy = Strategy(_strategy);
// Get strategy state fields individually since public struct getter returns tuple
(
address assetVault,
, // eventEmitter
, // governorAddress
, // prevTermController
, // currTermController
ITermDiscountRateAdapter discountRateAdapter,
, // timeToMaturityThreshold
, // requiredReserveRatio
, // discountRateMarkup
// repoTokenConcentrationLimit
) = termStrategy.strategyState();
uint256 totalWeightedRepoTokenApr = _calculateTotalWeightedRepoTokenApr(termStrategy, discountRateAdapter);
uint256 liquidBalanceWeightedApr = _calculateTotalWeightedLiquidBalanceApr(termStrategy, assetVault, _debtChange);
uint256 totalAssetValue = termStrategy.totalAssetValue();
// Handle signed debt change safely
uint256 adjustedAssetValue;
if (_debtChange >= 0) {
adjustedAssetValue = totalAssetValue + uint256(_debtChange);
} else {
uint256 absDebtChange = uint256(-_debtChange);
require(totalAssetValue >= absDebtChange, "Asset value insufficient for debt change");
adjustedAssetValue = totalAssetValue - absDebtChange;
}
require(adjustedAssetValue > 0, "Adjusted asset value is zero");
return (totalWeightedRepoTokenApr + liquidBalanceWeightedApr) / adjustedAssetValue;
}
// =============================================================
// INTERNAL FUNCTIONS
// =============================================================
/**
* @dev Calculates the total weighted APR for all repo token holdings
* @param termStrategy The Term strategy contract
* @param discountRateAdapter The discount rate adapter from strategy state
* @return totalWeightedApr The weighted APR based on repo token holdings
*/
function _calculateTotalWeightedRepoTokenApr(
Strategy termStrategy,
ITermDiscountRateAdapter discountRateAdapter
) internal view returns (uint256 totalWeightedApr) {
address[] memory repoTokens = termStrategy.repoTokenHoldings();
uint256 repoTokenTokenHoldingValue;
uint256 repoTokenApr;
for (uint256 i = 0; i < repoTokens.length; i++) {
// Get present value (current holding value)
repoTokenTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(repoTokens[i]);
// Skip if no holdings
if (repoTokenTokenHoldingValue == 0) continue;
// Get APR directly from discount rate adapter
repoTokenApr = discountRateAdapter.getDiscountRate(repoTokens[i]);
totalWeightedApr += repoTokenApr * repoTokenTokenHoldingValue;
}
}
/**
* @dev Calculates the total weighted APR for liquid balance using external oracle
* @param termStrategy The Term strategy contract
* @param strategyUnderlyingVault The asset vault address from strategy state
* @param _debtChange The debt change parameter to pass to external oracle
* @return liquidBalanceWeightedApr The weighted APR based on liquid balance
*/
function _calculateTotalWeightedLiquidBalanceApr(
Strategy termStrategy,
address strategyUnderlyingVault,
int256 _debtChange
) internal view returns (uint256 liquidBalanceWeightedApr) {
uint256 liquidBalance = termStrategy.totalLiquidBalance();
// Handle signed debt change safely for liquid balance
uint256 adjustedLiquidBalance;
if (_debtChange >= 0) {
adjustedLiquidBalance = liquidBalance + uint256(_debtChange);
} else {
uint256 absDebtChange = uint256(-_debtChange);
require(liquidBalance >= absDebtChange, "Liquid balance insufficient for debt change");
adjustedLiquidBalance = liquidBalance - absDebtChange;
}
// Check if there is an external APR oracle for the underlying asset
VaultMapping memory vaultMapping = idleVaultRemappings[strategyUnderlyingVault];
if (vaultMapping.vaultAddress == address(0)) {
revert UnknownUnderlyingVaultAprOracle();
}
uint256 externalApr;
if (vaultMapping.vaultType == VaultType.MULTISTRAT) {
externalApr = globalOracle.getWeightedAverageApr(strategyUnderlyingVault, _debtChange);
} else {
externalApr = globalOracle.getStrategyApr(strategyUnderlyingVault, _debtChange);
}
liquidBalanceWeightedApr = (externalApr * adjustedLiquidBalance);
}
// =============================================================
// ADMIN FUNCTIONS
// =============================================================
/**
* @dev Batch sets or removes idle vault remappings for underlying vaults
* @param _underlyingVaults Array of underlying vault addresses
* @param _vaultMappings Array of corresponding vault mappings (use address(0) in vaultAddress to remove)
*/
function batchSetIdleVaultRemappings(
address[] calldata _underlyingVaults,
VaultMapping[] calldata _vaultMappings
) external onlyGovernance {
require(_underlyingVaults.length == _vaultMappings.length, "Mismatched input lengths");
for (uint256 i = 0; i < _underlyingVaults.length; i++) {
if (_underlyingVaults[i] == address(0)) revert ZeroAddress();
if (_vaultMappings[i].vaultAddress == address(0)) {
// Remove mapping
delete idleVaultRemappings[_underlyingVaults[i]];
emit ExternalAprOracleRemoved(_underlyingVaults[i]);
} else {
// Set mapping
idleVaultRemappings[_underlyingVaults[i]] = _vaultMappings[i];
emit ExternalAprOracleSet(_underlyingVaults[i], _vaultMappings[i].vaultAddress);
}
}
}
}
"
},
"lib/tokenized-strategy-periphery/src/AprOracle/AprOracleBase.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity >=0.8.18;
import {Governance} from "../utils/Governance.sol";
abstract contract AprOracleBase is Governance {
string public name;
constructor(
string memory _name,
address _governance
) Governance(_governance) {
name = _name;
}
/**
* @notice Will return the expected Apr of a strategy post a debt change.
* @dev _delta is a signed integer so that it can also represent a debt
* decrease.
*
* _delta will be == 0 to get the current apr.
*
* This will potentially be called during non-view functions so gas
* efficiency should be taken into account.
*
* @param _strategy The strategy to get the apr for.
* @param _delta The difference in debt.
* @return . The expected apr for the strategy.
*/
function aprAfterDebtChange(
address _strategy,
int256 _delta
) external view virtual returns (uint256);
}
"
},
"src/Strategy.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.18;
import {BaseStrategy, ERC20} from "@tokenized-strategy/BaseStrategy.sol";
import {
SafeERC20,
IERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol";
import {ITermRepoServicer} from "./interfaces/term/ITermRepoServicer.sol";
import {ITermController} from "./interfaces/term/ITermController.sol";
import {ITermVaultEvents} from "./interfaces/term/ITermVaultEvents.sol";
import {
ITermAuctionOfferLocker
} from "./interfaces/term/ITermAuctionOfferLocker.sol";
import {
ITermDiscountRateAdapter
} from "./interfaces/term/ITermDiscountRateAdapter.sol";
import {ITermAuction} from "./interfaces/term/ITermAuction.sol";
import {RepoTokenList, RepoTokenListData} from "./RepoTokenList.sol";
import {
TermAuctionList,
TermAuctionListData,
PendingOffer
} from "./TermAuctionList.sol";
import {RepoTokenUtils} from "./RepoTokenUtils.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
// Import interfaces for many popular DeFi projects, or add your own!
//import "../interfaces/<protocol>/<Interface>.sol";
/**
* The `TokenizedStrategy` variable can be used to retrieve the strategies
* specific storage data your contract.
*
* i.e. uint256 totalAssets = TokenizedStrategy.totalAssets()
*
* This can not be used for write functions. Any TokenizedStrategy
* variables that need to be updated post deployment will need to
* come from an external call from the strategies specific `management`.
*/
// NOTE: To implement permissioned functions you can use the onlyManagement, onlyEmergencyAuthorized and onlyKeepers modifiers
contract Strategy is BaseStrategy, Pausable, AccessControl {
using SafeERC20 for IERC20;
using RepoTokenList for RepoTokenListData;
using TermAuctionList for TermAuctionListData;
/**
* @notice Constructor to initialize the Strategy contract
* @param _asset The address of the asset
* @param _yearnVault The address of the Yearn vault
* @param _discountRateAdapter The address of the discount rate adapter
* @param _eventEmitter The address of the event emitter
* @param _governorAddress The address of the governor
* @param _termController The address of the term controller
* @param _repoTokenConcentrationLimit The concentration limit for repoTokens
* @param _timeToMaturityThreshold The time to maturity threshold
* @param _requiredReserveRatio The required reserve ratio
* @param _discountRateMarkup The discount rate markup
*/
struct StrategyParams {
address _asset;
address _yearnVault;
address _discountRateAdapter;
address _eventEmitter;
address _governorAddress;
address _termController;
uint256 _repoTokenConcentrationLimit;
uint256 _timeToMaturityThreshold;
uint256 _requiredReserveRatio;
uint256 _discountRateMarkup;
}
struct StrategyState {
address assetVault;
address eventEmitter;
address governorAddress;
ITermController prevTermController;
ITermController currTermController;
ITermDiscountRateAdapter discountRateAdapter;
uint256 timeToMaturityThreshold;
uint256 requiredReserveRatio;
uint256 discountRateMarkup;
uint256 repoTokenConcentrationLimit;
}
// Custom errors
error InvalidTermAuction(address auction);
error TimeToMaturityAboveThreshold();
error BalanceBelowRequiredReserveRatio();
error InsufficientLiquidBalance(uint256 have, uint256 want);
error RepoTokenConcentrationTooHigh(address repoToken);
error RepoTokenBlacklisted(address repoToken);
error DepositPaused();
error AuctionNotOpen();
error ZeroPurchaseTokenAmount();
error OfferNotFound();
bytes32 internal constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE");
// Immutable state variables
ITermVaultEvents internal immutable TERM_VAULT_EVENT_EMITTER;
uint256 internal immutable PURCHASE_TOKEN_PRECISION;
IERC4626 internal immutable YEARN_VAULT;
/// @notice State variables
bool internal depositLock;
address internal pendingGovernor;
RepoTokenListData internal repoTokenListData;
TermAuctionListData internal termAuctionListData;
string internal tokenSymbol;
StrategyState public strategyState;
mapping(address => bool) public repoTokenBlacklist;
modifier notBlacklisted(address repoToken) {
if (repoTokenBlacklist[repoToken]) {
revert RepoTokenBlacklisted(repoToken);
}
_;
}
/*//////////////////////////////////////////////////////////////
MANAGEMENT FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
* @notice Pause the contract
*/
function pauseDeposit() external onlyRole(GOVERNOR_ROLE) {
depositLock = true;
TERM_VAULT_EVENT_EMITTER.emitDepositPaused();
}
/**
* @notice Unpause the contract
*/
function unpauseDeposit() external onlyRole(GOVERNOR_ROLE) {
depositLock = false;
TERM_VAULT_EVENT_EMITTER.emitDepositUnpaused();
}
/**
* @notice Pause the contract
*/
function pauseStrategy() external onlyRole(GOVERNOR_ROLE) {
_pause();
depositLock = true;
//TERM_VAULT_EVENT_EMITTER.emitStrategyPaused();
}
/**
* @notice Unpause the contract
*/
function unpauseStrategy() external onlyRole(GOVERNOR_ROLE) {
_unpause();
depositLock = false;
//TERM_VAULT_EVENT_EMITTER.emitStrategyUnpaused();
}
function setPendingGovernor(
address newGovernor
) external onlyRole(GOVERNOR_ROLE) {
require(newGovernor != address(0));
pendingGovernor = newGovernor;
}
function acceptGovernor() external {
require(msg.sender == pendingGovernor, "!pendingGovernor");
_revokeRole(GOVERNOR_ROLE, strategyState.governorAddress);
_grantRole(GOVERNOR_ROLE, pendingGovernor);
strategyState.governorAddress = pendingGovernor;
TERM_VAULT_EVENT_EMITTER.emitNewGovernor(pendingGovernor);
pendingGovernor = address(0);
}
/**
* @notice Set the term controller
* @param newTermControllerAddr The address of the new term controller
*/
function setTermController(
address newTermControllerAddr
) external onlyRole(GOVERNOR_ROLE) {
require(newTermControllerAddr != address(0));
require(
ITermController(newTermControllerAddr)
.getProtocolReserveAddress() != address(0)
);
address currentIteration = repoTokenListData.head;
while (currentIteration != address(0)) {
if (!_isTermDeployed(currentIteration)) {
revert RepoTokenList.InvalidRepoToken(currentIteration);
}
currentIteration = repoTokenListData.nodes[currentIteration].next;
}
address current = address(strategyState.currTermController);
TERM_VAULT_EVENT_EMITTER.emitTermControllerUpdated(
current,
newTermControllerAddr
);
strategyState.prevTermController = ITermController(current);
strategyState.currTermController = ITermController(
newTermControllerAddr
);
}
/**
* @notice Set the discount rate adapter used to price repoTokens
* @param newAdapter The address of the new discount rate adapter
*/
function setDiscountRateAdapter(
address newAdapter
) external onlyRole(GOVERNOR_ROLE) {
ITermDiscountRateAdapter newDiscountRateAdapter = ITermDiscountRateAdapter(
newAdapter
);
require(
address(newDiscountRateAdapter.currTermController()) != address(0)
);
TERM_VAULT_EVENT_EMITTER.emitDiscountRateAdapterUpdated(
address(strategyState.discountRateAdapter),
newAdapter
);
strategyState.discountRateAdapter = newDiscountRateAdapter;
}
/**
* @notice Set the weighted time to maturity cap
* @param newTimeToMaturityThreshold The new weighted time to maturity cap
*/
function setTimeToMaturityThreshold(
uint256 newTimeToMaturityThreshold
) external onlyRole(GOVERNOR_ROLE) {
TERM_VAULT_EVENT_EMITTER.emitTimeToMaturityThresholdUpdated(
strategyState.timeToMaturityThreshold,
newTimeToMaturityThreshold
);
strategyState.timeToMaturityThreshold = newTimeToMaturityThreshold;
}
/**
* @notice Set the required reserve ratio
* @dev This function can only be called by management
* @param newRequiredReserveRatio The new required reserve ratio (in 1e18 precision)
*/
function setRequiredReserveRatio(
uint256 newRequiredReserveRatio
) external onlyRole(GOVERNOR_ROLE) {
TERM_VAULT_EVENT_EMITTER.emitRequiredReserveRatioUpdated(
strategyState.requiredReserveRatio,
newRequiredReserveRatio
);
strategyState.requiredReserveRatio = newRequiredReserveRatio;
}
/**
* @notice Set the repoToken concentration limit
* @param newRepoTokenConcentrationLimit The new repoToken concentration limit
*/
function setRepoTokenConcentrationLimit(
uint256 newRepoTokenConcentrationLimit
) external onlyRole(GOVERNOR_ROLE) {
TERM_VAULT_EVENT_EMITTER.emitRepoTokenConcentrationLimitUpdated(
strategyState.repoTokenConcentrationLimit,
newRepoTokenConcentrationLimit
);
strategyState
.repoTokenConcentrationLimit = newRepoTokenConcentrationLimit;
}
/**
* @notice Set the markup that the vault will receive in excess of the oracle rate
* @param newDiscountRateMarkup The new auction rate markup
*/
function setDiscountRateMarkup(
uint256 newDiscountRateMarkup
) external onlyRole(GOVERNOR_ROLE) {
TERM_VAULT_EVENT_EMITTER.emitDiscountRateMarkupUpdated(
strategyState.discountRateMarkup,
newDiscountRateMarkup
);
strategyState.discountRateMarkup = newDiscountRateMarkup;
}
/**
* @notice Set the collateral token parameters
* @param tokenAddr The address of the collateral token to be accepted
* @param minCollateralRatio The minimum collateral ratio accepted by the strategy
*/
function setCollateralTokenParams(
address tokenAddr,
uint256 minCollateralRatio
) external onlyRole(GOVERNOR_ROLE) {
TERM_VAULT_EVENT_EMITTER.emitMinCollateralRatioUpdated(
tokenAddr,
minCollateralRatio
);
repoTokenListData.collateralTokenParams[tokenAddr] = minCollateralRatio;
}
function setRepoTokenBlacklist(
address repoToken,
bool blacklisted
) external onlyRole(GOVERNOR_ROLE) {
TERM_VAULT_EVENT_EMITTER.emitRepoTokenBlacklistUpdated(
repoToken,
blacklisted
);
repoTokenBlacklist[repoToken] = blacklisted;
}
/*//////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////*/
function symbol() external view returns (string memory) {
return tokenSymbol;
}
/**
* @notice Calculates the total value of all assets managed by the strategy
* @return The total asset value in the purchase token precision
*
* @dev This function aggregates the total liquid balance, the present value of all repoTokens,
* and the present value of all pending offers to calculate the total asset value.
*/
function totalAssetValue() external view returns (uint256) {
return _totalAssetValue(_totalLiquidBalance());
}
/**
* @notice Get the total liquid balance of the assets managed by the strategy
* @return The total liquid balance in the purchase token precision
*
* @dev This function aggregates the balance of the underlying asset held directly by the strategy
* and the balance of the asset held in the Yearn Vault to calculate the total liquid balance.
*/
function totalLiquidBalance() external view returns (uint256) {
return _totalLiquidBalance();
}
/**
* @notice Calculate the liquid reserve ratio
* @param liquidBalance The current liquid balance of the strategy
* @return The liquid reserve ratio in 1e18 precision
*
* @dev This function calculates the ratio of liquid balance to total asset value.
* It returns 0 if the total asset value is 0 to avoid division by zero.
*/
function _liquidReserveRatio(
uint256 liquidBalance
) internal view returns (uint256) {
uint256 assetValue = _totalAssetValue(liquidBalance);
if (assetValue == 0) return 0;
return (liquidBalance * 1e18) / assetValue;
}
/**
* @notice Get the current liquid reserve ratio of the strategy
* @return The current liquid reserve ratio in 1e18 precision
*
* @dev This function calculates the liquid reserve ratio based on the current
* total liquid balance of the strategy.
*/
function liquidReserveRatio() external view returns (uint256) {
return _liquidReserveRatio(_totalLiquidBalance());
}
/**
* @notice Returns an array of addresses representing the repoTokens currently held by the strategy
* @return address[] An array of addresses of the repoTokens held by the strategy
*
* @dev This function calls the `holdings` function from the `RepoTokenList` library to get the list
* of repoTokens currently held in the `RepoTokenListData` structure.
*/
function repoTokenHoldings() external view returns (address[] memory) {
return repoTokenListData.holdings();
}
/**
* @notice Get an array of pending offers submitted into Term auctions
* @return bytes32[] An array of `bytes32` values representing the IDs of the pending offers
*
* @dev This function calls the `pendingOffers` function from the `TermAuctionList` library to get the list
* of pending offers currently submitted into Term auctions from the `TermAuctionListData` structure.
*/
function pendingOffers() external view returns (bytes32[] memory) {
return termAuctionListData.pendingOffers();
}
/**
* @notice Calculate the concentration ratio of a specific repoToken in the strategy
* @param repoToken The address of the repoToken to calculate the concentration for
* @return The concentration ratio of the repoToken in the strategy (in 1e18 precision)
*
* @dev This function computes the current concentration ratio of a specific repoToken
* in the strategy's portfolio. It reverts if the repoToken address is zero. The calculation
* is based on the current total asset value and does not consider any additional purchases
* or removals of the repoToken.
*/
function getRepoTokenConcentrationRatio(
address repoToken
) external view returns (uint256) {
if (repoToken == address(0)) {
revert RepoTokenList.InvalidRepoToken(address(0));
}
return
_getRepoTokenConcentrationRatio(
repoToken,
0,
_totalAssetValue(_totalLiquidBalance()),
0
);
}
/**
* @notice Simulates the weighted time to maturity for a specified repoToken and amount, including the impact on the entire strategy's holdings
* @param repoToken The address of the repoToken to be simulated
* @param amount The amount of the repoToken to be simulated
* @return simulatedWeightedMaturity The simulated weighted time to maturity for the entire strategy
* @return simulatedRepoTokenConcentrationRatio The concentration ratio of the repoToken in the strategy (in 1e18 precision)
* @return simulatedLiquidityRatio The simulated liquidity ratio after the transaction
*
* @dev This function simulates the effects of a potential transaction on the strategy's key metrics.
* It calculates the new weighted time to maturity and liquidity ratio, considering the specified
* repoToken and amount. For existing repoTokens, use address(0) as the repoToken parameter.
* The function performs various checks and calculations, including:
* - Validating the repoToken (if not address(0))
* - Calculating the present value of the transaction
* - Estimating the impact on the strategy's liquid balance
* - Computing the new weighted maturity and liquidity ratio
*/
function simulateTransaction(
address repoToken,
uint256 amount
)
external
view
returns (
uint256 simulatedWeightedMaturity,
uint256 simulatedRepoTokenConcentrationRatio,
uint256 simulatedLiquidityRatio
)
{
// do not validate if we are simulating with existing repoTokens
uint256 liquidBalance = _totalLiquidBalance();
uint256 repoTokenAmountInBaseAssetPrecision;
uint256 proceeds;
if (repoToken != address(0)) {
if (!_isTermDeployed(repoToken)) {
revert RepoTokenList.InvalidRepoToken(repoToken);
}
(
bool isRepoTokenValid,
uint256 redemptionTimestamp
) = repoTokenListData.validateRepoToken(
ITermRepoToken(repoToken),
address(asset)
);
if (!isRepoTokenValid) {
revert RepoTokenList.InvalidRepoToken(repoToken);
}
uint256 discountRate = strategyState
.discountRateAdapter
.getDiscountRate(repoToken);
uint256 repoRedemptionHaircut = strategyState
.discountRateAdapter
.repoRedemptionHaircut(repoToken);
repoTokenAmountInBaseAssetPrecision = RepoTokenUtils
.getNormalizedRepoTokenAmount(
repoToken,
amount,
PURCHASE_TOKEN_PRECISION,
repoRedemptionHaircut
);
proceeds = RepoTokenUtils.calculatePresentValue(
repoTokenAmountInBaseAssetPrecision,
PURCHASE_TOKEN_PRECISION,
redemptionTimestamp,
discountRate + strategyState.discountRateMarkup
);
}
simulatedWeightedMaturity = _calculateWeightedMaturity(
repoToken,
amount,
liquidBalance - proceeds
);
if (repoToken != address(0)) {
simulatedRepoTokenConcentrationRatio = _getRepoTokenConcentrationRatio(
repoToken,
repoTokenAmountInBaseAssetPrecision,
_totalAssetValue(liquidBalance),
proceeds
);
}
uint256 assetValue = _totalAssetValue(liquidBalance);
if (assetValue == 0) {
simulatedLiquidityRatio = 0;
} else {
simulatedLiquidityRatio =
((liquidBalance - proceeds) * 10 ** 18) /
assetValue;
}
}
/**
* @notice Calculates the present value of a specified repoToken based on its discount rate, redemption timestamp, and amount
* @param repoToken The address of the repoToken
* @param discountRate The discount rate to be used in the present value calculation
* @param amount The amount of the repoToken to be discounted
* @return uint256 The present value of the specified repoToken and amount
*
* @dev This function retrieves the redemption timestamp, calculates the repoToken precision,
* normalizes the repoToken amount to base asset precision, and calculates the present value
* using the provided discount rate and redemption timestamp.
*/
function calculateRepoTokenPresentValue(
address repoToken,
uint256 discountRate,
uint256 amount
) public view returns (uint256) {
(uint256 redemptionTimestamp, , , ) = ITermRepoToken(repoToken)
.config();
uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils
.getNormalizedRepoTokenAmount(
repoToken,
amount,
PURCHASE_TOKEN_PRECISION,
strategyState.discountRateAdapter.repoRedemptionHaircut(
repoToken
)
);
return
RepoTokenUtils.calculatePresentValue(
repoTokenAmountInBaseAssetPrecision,
PURCHASE_TOKEN_PRECISION,
redemptionTimestamp,
discountRate
);
}
/**
* @notice Calculates the present value of a specified repoToken held by the strategy
* @param repoToken The address of the repoToken to value
* @return uint256 The present value of the specified repoToken
*
* @dev This function calculates the present value of the specified repoToken from both
* the `repoTokenListData` and `termAuctionListData` structures, then sums these values
* to provide a comprehensive valuation.
*/
function getRepoTokenHoldingValue(
address repoToken
) public view returns (uint256) {
uint256 repoTokenHoldingPV;
if (repoTokenListData.discountRates[repoToken] != 0) {
address tokenTermController;
if (strategyState.currTermController.isTermDeployed(repoToken)) {
tokenTermController = address(strategyState.currTermController);
} else if (
strategyState.prevTermController !=
ITermController(address(0)) &&
strategyState.prevTermController.isTermDeployed(repoToken)
) {
tokenTermController = address(strategyState.prevTermController);
}
repoTokenHoldingPV = calculateRepoTokenPresentValue(
repoToken,
strategyState.discountRateAdapter.getDiscountRate(
tokenTermController,
repoToken
),
ITermRepoToken(repoToken).balanceOf(address(this))
);
}
return
repoTokenHoldingPV +
termAuctionListData.getPresentValue(
repoTokenListData,
strategyState.discountRateAdapter,
PURCHASE_TOKEN_PRECISION,
repoToken
);
}
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
* @dev Withdraw assets from the Yearn vault
* @param amount The amount to withdraw
*/
function _withdrawAsset(uint256 amount) private {
YEARN_VAULT.withdraw(amount, address(this), address(this));
}
/**
* @dev Retrieves the asset balance from the Yearn Vault
* @return The balance of assets in the purchase token precision
*/
function _assetBalance() private view returns (uint256) {
return
YEARN_VAULT.convertToAssets(YEARN_VAULT.balanceOf(address(this)));
}
/**
* @notice Calculates the total liquid balance of the assets managed by the strategy
* @return uint256 The total liquid balance of the assets
*
* @dev This function aggregates the balance of the underlying asset held directly by the strategy
* and the balance of the asset held in the Yearn Vault to calculate the total liquid balance.
*/
function _totalLiquidBalance() private view returns (uint256) {
uint256 underlyingBalance = IERC20(asset).balanceOf(address(this));
return _assetBalance() + underlyingBalance;
}
/**
* @notice Calculates the total value of all assets managed by the strategy (internal function)
* @return totalValue The total value of all assets
*
* @dev This function aggregates the total liquid balance, the present value of all repoTokens,
* and the present value of all pending offers to calculate the total asset value.
*/
function _totalAssetValue(
uint256 liquidBalance
) internal view returns (uint256 totalValue) {
return
liquidBalance +
repoTokenListData.getPresentValue(
strategyState.discountRateAdapter,
PURCHASE_TOKEN_PRECISION
) +
termAuctionListData.getPresentValue(
repoTokenListData,
strategyState.discountRateAdapter,
PURCHASE_TOKEN_PRECISION,
address(0)
);
}
/**
* @notice Calculates the concentration ratio of a specific repoToken in the strategy
* @param repoToken The address of the repoToken to calculate the concentration for
* @param repoTokenAmountInBaseAssetPrecision The amount of the repoToken in base asset precision to be added
* @param assetValue The current total asset value of the strategy
* @param liquidBalanceToRemove The amount of liquid balance to be removed from the strategy
* @return The concentration ratio of the repoToken in the strategy (in 1e18 precision)
*
* @dev This function computes the concentration ratio of a specific repoToken, considering both
* existing holdings and a potential new addition. It adjusts the total asset value, normalizes
* values to 1e18 precision, and handles the case where total asset value might be zero.
*/
function _getRepoTokenConcentrationRatio(
address repoToken,
uint256 repoTokenAmountInBaseAssetPrecision,
uint256 assetValue,
uint256 liquidBalanceToRemove
) private view returns (uint256) {
// Retrieve the current value of the repoToken held by the strategy and add the new repoToken amount
uint256 repoTokenValue = getRepoTokenHoldingValue(repoToken) +
repoTokenAmountInBaseAssetPrecision;
// Retrieve the total asset value of the strategy and adjust it for the new repoToken amount and liquid balance to be removed
uint256 adjustedTotalAssetValue = assetValue +
repoTokenAmountInBaseAssetPrecision -
liquidBalanceToRemove;
// Normalize the repoToken value and total asset value to 1e18 precision
repoTokenValue = (repoTokenValue * 1e18) / PURCHASE_TOKEN_PRECISION;
adjustedTotalAssetValue =
(adjustedTotalAssetValue * 1e18) /
PURCHASE_TOKEN_PRECISION;
// Calculate the repoToken concentration
return
adjustedTotalAssetValue == 0
? 0
: (repoTokenValue * 1e18) / adjustedTotalAssetValue;
}
/**
* @notice Validate the concentration of a repoToken against the strategy's limit
* @param repoToken The address of the repoToken to validate
* @param repoTokenAmountInBaseAssetPrecision The amount of the repoToken in base asset precision
* @param assetValue The current total asset value of the strategy
* @param liquidBalanceToRemove The amount of liquid balance to be removed from the strategy
*
* @dev This function calculates the concentration ratio of the specified repoToken
* and compares it against the predefined concentration limit. It reverts with a
* RepoTokenConcentrationTooHigh error if the concentration exceeds the limit.
*/
function _validateRepoTokenConcentration(
address repoToken,
uint256 repoTokenAmountInBaseAssetPrecision,
uint256 assetValue,
uint256 liquidBalanceToRemove
) private view {
uint256 repoTokenConcentration = _getRepoTokenConcentrationRatio(
repoToken,
repoTokenAmountInBaseAssetPrecision,
assetValue,
liquidBalanceToRemove
);
// Check if the repoToken concentration exceeds the predefined limit
if (
repoTokenConcentration > strategyState.repoTokenConcentrationLimit
) {
revert RepoTokenConcentrationTooHigh(repoToken);
}
}
/**
* @notice Calculates the weighted time to maturity for the strategy's holdings, including the impact of a specified repoToken and amount
* @param repoToken The address of the repoToken (optional)
* @param repoTokenAmount The amount of the repoToken to be included in the calculation
* @param liquidBalance The liquid balance of the strategy
* @return uint256 The weighted time to maturity in seconds for the entire strategy, including the specified repoToken and amount
*
* @dev This function aggregates the cumulative weighted time to maturity and the cumulative amount of both existing repoTokens
* and offers, then calculates the weighted time to maturity for the entire strategy. It considers both repoTokens and auction offers.
* The `repoToken` and `repoTokenAmount` parameters are optional and provide flexibility to adjust the calculations to include
* the provided repoToken amount. If `repoToken` is set to `address(0)` or `repoTokenAmount` is `0`, the function calculates
* the cumulative data without specific token adjustments.
*/
function _calculateWeightedMaturity(
address repoToken,
uint256 repoTokenAmount,
uint256 liquidBalance
) private view returns (uint256) {
// Initialize cumulative weighted time to maturity and cumulative amount
uint256 cumulativeWeightedTimeToMaturity; // in seconds
uint256 cumulativeAmount; // in purchase token precision
// Get cumulative data from repoToken list
(
uint256 cumulativeRepoTokenWeightedTimeToMaturity,
uint256 cumulativeRepoTokenAmount,
bool foundInRepoTokenList
) = repoTokenListData.getCumulativeRepoTokenData(
strategyState.discountRateAdapter,
repoToken,
repoTokenAmount,
PURCHASE_TOKEN_PRECISION
);
// Accumulate repoToken data
cumulativeWeightedTimeToMaturity += cumulativeRepoTokenWeightedTimeToMaturity;
cumulativeAmount += cumulativeRepoTokenAmount;
(
uint256 cumulativeOfferWeightedTimeToMaturity,
uint256 cumulativeOfferAmount,
bool foundInOfferList
) = termAuctionListData.getCumulativeOfferData(
repoTokenListData,
strategyState.discountRateAdapter,
repoToken,
repoTokenAmount,
PURCHASE_TOKEN_PRECISION
);
// Accumulate offer data
cumulativeWeightedTimeToMaturity += cumulativeOfferWeightedTimeToMaturity;
cumulativeAmount += cumulativeOfferAmount;
if (
!foundInRepoTokenList &&
!foundInOfferList &&
repoToken != address(0)
) {
uint256 repoRedemptionHaircut = strategyState
.discountRateAdapter
.repoRedemptionHaircut(repoToken);
uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils
.getNormalizedRepoTokenAmount(
repoToken,
repoTokenAmount,
PURCHASE_TOKEN_PRECISION,
repoRedemptionHaircut
);
cumulativeAmount += repoTokenAmountInBaseAssetPrecision;
cumulativeWeightedTimeToMaturity += RepoTokenList
.getRepoTokenWeightedTimeToMaturity(
repoToken,
repoTokenAmountInBaseAssetPrecision
);
}
// Avoid division by zero
if (cumulativeAmount == 0 && liquidBalance == 0) {
return 0;
}
// Calculate and return weighted time to maturity
// time * purchaseTokenPrecision / purchaseTokenPrecision
return
cumulativeWeightedTimeToMaturity /
(cumulativeAmount + liquidBalance);
}
/**
* @notice Checks if a term contract is marked as deployed in either the current or previous term controller
* @param termContract The address of the term contract to check
* @return bool True if the term contract is deployed, false otherwise
*
* @dev This function first checks the current term controller, then the previous one if necessary.
* It handles cases where either controller might be unset (address(0)).
*/
function _isTermDeployed(address termContract) private view returns (bool) {
ITermController currTermController = strategyState.currTermController;
ITermController prevTermController = strategyState.prevTermController;
if (
address(currTermController) != address(0) &&
currTermController.isTermDeployed(termContract)
) {
return true;
}
if (
address(prevTermController) != address(0) &&
prevTermController.isTermDeployed(termContract)
) {
return true;
}
return false;
}
/**
* @notice Rebalances the strategy's assets by sweeping assets and redeeming matured repoTokens
* @param liquidAmountRequired The amount of liquid assets required to be maintained by the strategy
*
* @dev This function removes completed auction offers, redeems matured repoTokens, and adjusts the underlying
* balance to maintain the required liquidity. It ensures that the strategy has sufficient liquid assets while
* optimizing asset allocation.
*/
function _redeemRepoTokens(uint256 liquidAmountRequired) private {
// Remove completed auction offers
termAuctionListData.removeCompleted(
repoTokenListData,
strategyState.discountRateAdapter,
address(asset)
);
// Remove and redeem matured repoTokens
repoTokenListData.removeAndRedeemMaturedTokens();
uint256 liquidity = IERC20(asset).balanceOf(address(this));
// Deposit excess underlying balance into Yearn Vault
if (liquidity > liquidAmountRequired) {
unchecked {
YEARN_VAULT.deposit(
liquidity - liquidAmountRequired,
address(this)
);
}
// Withdraw shortfall from Yearn Vault to meet required liquidity
} else if (liquidity < liquidAmountRequired) {
unchecked {
_withdrawAsset(liquidAmountRequired - liquidity);
}
}
}
/*//////////////////////////////////////////////////////////////
STRATEGIST FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
* @notice Validates a term auction and repo token, and retrieves the associated offer locker
* @param termAuction The term auction contract to validate
* @param repoToken The repo token address to validate
* @return ITermAuctionOfferLocker The offer locker associated with the validated term auction
*
* @dev This function performs several validation steps: verifying term auction and repo token deployment,
* matching repo token to auction's term repo ID, validating repo token against strategy requirements,
* and ensuring the auction is open. It reverts with specific error messages on validation failures.
*/
function _validateAndGetOfferLocker(
ITermAuction termAuction,
address repoToken
) private view returns (ITermAuctionOfferLocker) {
// Verify that the term auction and repo token are valid and deployed by term
if (!_isTermDeployed(address(termAuction))) {
revert InvalidTermAuction(address(termAuction));
}
if (!_isTermDeployed(repoToken)) {
revert RepoTokenList.InvalidRepoToken(repoToken);
}
if (
termAuction.termRepoId() != ITermRepoToken(repoToken).termRepoId()
) {
revert RepoTokenList.InvalidRepoToken(repoToken);
}
// Validate purchase token, min collateral ratio and insert the repoToken if necessary
(bool isValid, ) = repoTokenListData.validateRepoToken(
ITermRepoToken(repoToken),
address(asset)
);
if (!isValid) {
revert RepoTokenList.InvalidRepoToken(repoToken);
}
// Prepare and submit the offer
ITermAuctionOfferLocker offerLocker = ITermAuctionOfferLocker(
termAuction.termAuctionOfferLocker()
);
if (
block.timestamp <= offerLocker.auctionStartTime() ||
block.timestamp >= offerLocker.revealTime()
) {
revert AuctionNotOpen();
}
return offerLocker;
}
/**
* @notice Submits an offer into a term auction for a specified repoToken
* @param termAuction The address of the term auction
* @param repoToken The address of the repoToken
* @param idHash The hash of the offer ID
* @param offerPriceHash The hash of the offer price
* @param purchaseTokenAmount The amount of purchase tokens being offered
* @return offerIds An array of offer IDs for the submitted offers
*
* @dev This function validates the underlying repoToken, checks concentration limits, ensures the auction is open,
* and rebalances liquidity to support the offer submission. It handles both new offers and edits to existing offers.
*/
function submitAuctionOffer(
ITermAuction termAuction,
address repoToken,
bytes32 idHash,
bytes32 offerPriceHash,
uint256 purchaseTokenAmount
)
external
whenNotPaused
notBlacklisted(repoToken)
onlyManagement
returns (bytes32[] memory offerIds)
{
if (purchaseTokenAmount == 0) {
revert ZeroPurchaseTokenAmount();
}
ITermAuctionOfferLocker offerLocker = _validateAndGetOfferLocker(
termAuction,
repoToken
);
// Sweep assets, redeem matured repoTokens and ensure liquid balances up to date
_redeemRepoTokens(0);
uint256 newOfferAmount = purchaseTokenAmount;
uint256 currentOfferAmount = termAuctionListData
.offers[idHash]
.offerAmount;
// Submit the offer and lock it in the auction
ITermAuctionOfferLocker.TermAuctionOfferSubmission memory offer;
offer.id = idHash;
offer.offeror = address(this);
offer.offerPriceHash = offerPriceHash;
offer.amount = purchaseTokenAmount;
offer.purchaseToken = address(asset);
// InsufficientLiquidBalance checked inside _submitOffer
offerIds = _submitOffer(
termAuction,
offerLocker,
offer,
repoToken,
newOfferAmount,
currentOfferAmount
);
// Retrieve the total liquid balance
uint256 liquidBalance = _totalLiquidBalance();
uint256 totalAssetValue = _totalAssetValue(liquidBalance);
require(totalAssetValue > 0);
uint256 liquidReserveRatio = (liquidBalance * 1e18) / totalAssetValue; // NOTE: we require totalAssetValue > 0 above
// Check that new offer does not violate reserve ratio constraint
if (liquidReserveRatio < strategyState.requiredReserveRatio) {
revert BalanceBelowRequiredReserveRatio();
}
// Calculate the resulting weighted time to maturity
// Passing in 0 adjustment because offer and balance already updated
uint256 resultingWeightedTimeToMaturity = _calculateWeightedMaturity(
address(0),
0,
liquidBalance
);
// Check if the resulting weighted time to maturity exceeds the threshold
if (
resultingWeightedTimeToMaturity >
strategyState.timeToMaturityThreshold
) {
revert TimeToMaturityAboveThreshold();
}
// Passing in 0 amount and 0 liquid balance adjustment because offer and balance already updated
_validateRepoTokenConcentration(repoToken, 0, totalAssetValue, 0);
}
/**
* @dev Submits an offer to a term auction and locks it using the offer locker.
* @param auction The term auction contract
* @param offerLocker The offer locker contract
* @param offer The offer details
* @param repoToken The address of the repoToken
* @param newOfferAmount The amount of the new offer
* @param currentOfferAmount The amount of the current offer, if it exists
* @return offerIds An array of offer IDs for the submitted offers
*/
function _submitOffer(
ITermAuction auction,
ITermAuctionOfferLocker offerLocker,
ITermAuctionOfferLocker.TermAuctionOfferSubmission memory offer,
address repoToken,
uint256 newOfferAmount,
uint256 currentOfferAmount
) private returns (bytes32[] memory offerIds) {
// Retrieve the repo servicer contract
ITermRepoServicer repoServicer = ITermRepoServicer(
offerLocker.termRepoServicer()
);
// Prepare the offer submission details
ITermAuctionOfferLocker.TermAuctionOfferSubmission[]
memory offerSubmissions = new ITermAuctionOfferLocker.TermAuctionOfferSubmission[](
1
);
offerSubmissions[0] = offer;
// Handle additional asset withdrawal if the new offer amount is greater than the current amount
if (newOfferAmount > currentOfferAmount) {
uint256 offerDebit;
unchecked {
// checked above
offerDebit = newOfferAmount - currentOfferAmount;
}
uint256 liquidBalance = _totalLiquidBalance();
if (liquidBalance < offerDebit) {
revert InsufficientLiquidBalance(liquidBalance, offerDebit);
}
_withdrawAsset(offerDebit);
IERC20(asset).safeApprove(
address(repoServicer.termRepoLocker()),
offerDebit
);
}
// Submit the offer and get the offer IDs
offerIds = offerLocker.lockOffers(offerSubmissions);
if (offerIds.length == 0) {
revert OfferNotFound();
}
// Update the pending offers list
if (currentOfferAmount == 0) {
// new offer
termAuctionListData.insertPending(
offerIds[0],
PendingOffer({
repoToken: repoToken,
offerAmount: offer.amount,
termAuction: auction,
offerLocker: offerLocker
})
);
} else {
// Edit offer, overwrite existing
PendingOffer storage pendingOffer = termAuctionListData.offers[
offerIds[0]
];
pendingOffer.offerAmount = offer.amount;
}
if (newOfferAmount < currentOfferAmount) {
YEARN_VAULT.deposit(
IERC20(asset).balanceOf(address(this)),
address(this)
);
}
}
/**
* @dev Removes specified offers from a term auction and performs related cleanup.
* @param termAuction The address of the term auction from which offers will be deleted.
* @param offerIds An array of offer IDs to be deleted.
*/
function deleteAuctionOffers(
address termAuction,
bytes32[] calldata offerIds
) external onlyManagement {
// Validate if the term auction is deployed by term
if (!_isTermDeployed(termAuction)) {
revert InvalidTermAuction(termAuction);
}
// Retrieve the auction and offer locker contracts
ITermAuction auction = ITermAuction(termAuction);
ITermAuctionOfferLocker offerLocker = ITermAuctionOfferLocker(
auction.termAuctionOfferLocker()
);
// Unlock the specified offers
offerLocker.unlockOffers(offerIds);
// Update the term auction list data and remove completed offers
termAuctionListData.removeCompleted(
repoTokenListData,
strategyState.discountRateAdapter,
address(asset)
);
// Sweep any remaining assets and redeem repoTokens
_redeemRepoTokens(0);
}
/**
* @notice Required for post-processing after auction clos
*/
function auctionClosed() external {
_redeemRepoTokens(0);
}
/*//////////////////////////////////////////////////////////////
PUBLIC FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
* @notice Allows the sale of a specified amount of a repoToken in exchange for assets.
* @param repoToken The address of the repoToken to be sold.
* @param repoTokenAmount The amount of the repoToken to be sold.
*/
function sellRepoToken(
address repoToken,
uint256 repoTokenAmount
) external whenNotPaused notBlacklisted(repoToken) {
// Ensure the amount of repoTokens to sell is greater than zero
require(repoTokenAmount > 0);
// Make sure repo token is valid and deployed by Term
if (!_isTermDeployed(repoToken)) {
revert RepoTokenList.InvalidRepoToken(repoToken);
}
// Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp
(bool isRepoTokenValid, uint256 redemptionTimestamp) = repoTokenListData
.validateAndInsertRepoToken(
ITermRepoToken(repoToken),
strategyState.discountRateAdapter,
address(asset)
);
if (!isRepoTokenValid) {
revert RepoTokenList.InvalidRepoToken(repoToken);
}
// Sweep assets and redeem repoTokens, if needed
_redeemRepoTokens(0);
// Retrieve total asset value and liquid balance and ensure they are greater than zero
uint256 liquidBalance = _totalLiquidBalance();
require(liquidBalance > 0);
uint256 totalAssetValue = _totalAssetValue(liquidBalance);
require(totalAssetValue > 0);
uint256 discountRate = strategyState
.discountRateAdapter
.getDiscountRate(repoToken);
// Calculate the repoToken amount in base asset precision
uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils
.getNormalizedRepoTokenAmount(
repoToken,
repoTokenAmount,
PURCHASE_TOKEN_PRECISION,
strategyState.discountRateAdapter.repoRedemptionHaircut(
repoToken
)
);
// Calculate the proceeds from selling the repoToken
uint256 proceeds = RepoTokenUtils.calculatePresentValue(
repoTokenAmountInBaseAssetPrecision,
PURCHASE_TOKEN_PRECISION,
redemptionTimestamp,
discountRate + strategyState.discountRateMarkup
);
// Ensure the liquid balance is sufficient to cover the proceeds
if (liquidBalance < proceeds) {
revert InsufficientLiquidBalance(liquidBalance, proceeds);
}
// Calculate resulting time to maturity after the sale and ensure it doesn't exceed the threshold
uint256 resultingTimeToMaturity = _calculateWeightedMaturity(
repoToken,
repoTokenAmount,
liquidBalance - proceeds
);
if (resultingTimeToMaturity > strategyState.timeToMaturityThreshold) {
revert TimeToMaturityAboveThreshold();
}
// Ensure the remaining liquid balance is above the liquidity threshold
uint256 newLiquidReserveRatio = ((liquidBalance - proceeds) * 1e18) /
totalAssetValue; // NOTE: we require totalAssetValue > 0 above
if (newLiquidReserveRatio < strategyState.requiredReserveRatio) {
revert BalanceBelowRequiredReserveRatio();
}
// Validate resulting repoToken concentration to ensure it meets requirements
_validateRepoTokenConcentration(
repoToken,
repoTokenAmountInBaseAssetPrecision,
totalAssetValue,
proceeds
);
// withdraw from underlying vault
_withdrawAsset(proceeds);
// Transfer repoTokens from the sender to the contract
IERC20(repoToken).safeTransferFrom(
msg.sender,
address(this),
repoTokenAmount
);
// Transfer the proceeds in assets to the sender
IERC20(asset).safeTransfer(msg.sender, proceeds);
}
/**
* @notice Constructor to initialize the Strategy contract
* @param _name The name of the strategy
*/
constructor(
string memory _name,
string memory _symbol,
StrategyParams memory _params
) BaseStrategy(_params._asset, _name) {
YEARN_VAULT = IERC4626(_params._yearnVault);
TERM_VAULT_EVENT_EMITTER = ITermVaultEvents(_params._eventEmitter);
PURCHASE_TOKEN_PRECISION = 10 ** ERC20(asset).decimals();
IERC20(_params._asset).safeApprove(
_params._yearnVault,
type(uint256).max
);
tokenSymbol = _symbol;
strategyState = StrategyState({
assetVault: address(YEARN_VAULT),
eventEmitter: address(TERM_VAULT_EVENT_EMITTER),
governorAddress: _params._governorAddress,
prevTermController: ITermController(address(0)),
currTermController: ITermController(_params._termController),
discountRateAdapter: ITermDiscountRateAdapter(
_params._discountRateAdapter
),
timeToMaturityThreshold: _params._timeToMaturityThreshold,
requiredReserveRatio: _params._requiredReserveRatio,
discountRateMarkup: _params._discountRateMarkup,
repoTokenConcentrationLimit: _params._repoTokenConcentrationLimit
});
_grantRole(GOVERNOR_ROLE, _params._governorAddress);
}
/*//////////////////////////////////////////////////////////////
NEEDED TO BE OVERRIDDEN BY STRATEGIST
//////////////////////////////////////////////////////////////*/
/**
* @dev Can 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 can attempt
* to deposit in the yield source.
*/
function _deployFunds(uint256 _amount) internal override whenNotPaused {
if (depositLock) {
revert DepositPaused();
}
_redeemRepoTokens(0);
}
/**
* @dev Should attempt to free the '_amount' of 'asset'.
*
* NOTE: 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 override whenNotPaused {
_redeemRepoTokens(_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.
*
*
Submitted on: 2025-10-28 11:37:00
Comments
Log in to comment.
No comments yet.