diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 29bff868..9075f683 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.18; -import "forge-std/console.sol"; import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; import {ITermRepoServicer} from "./interfaces/term/ITermRepoServicer.sol"; import {ITermRepoCollateralManager} from "./interfaces/term/ITermRepoCollateralManager.sol"; +import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.sol"; import {ITermController, AuctionMetadata} from "./interfaces/term/ITermController.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {RepoTokenUtils} from "./RepoTokenUtils.sol"; @@ -16,29 +16,52 @@ struct RepoTokenListNode { struct RepoTokenListData { address head; mapping(address => RepoTokenListNode) nodes; - mapping(address => uint256) auctionRates; + mapping(address => uint256) discountRates; /// @notice keyed by collateral token mapping(address => uint256) collateralTokenParams; } +/*////////////////////////////////////////////////////////////// + LIBRARY: RepoTokenList +//////////////////////////////////////////////////////////////*/ + library RepoTokenList { address public constant NULL_NODE = address(0); uint256 internal constant INVALID_AUCTION_RATE = 0; error InvalidRepoToken(address token); + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Retrieves the redemption (maturity) timestamp of a repoToken + * @param repoToken The address of the repoToken + * @return redemptionTimestamp The timestamp indicating when the repoToken matures + * + * @dev This function calls the `config()` method on the repoToken to retrieve its configuration details, + * including the redemption timestamp, which it then returns. + */ function getRepoTokenMaturity(address repoToken) internal view returns (uint256 redemptionTimestamp) { (redemptionTimestamp, , ,) = ITermRepoToken(repoToken).config(); } - function _getRepoTokenTimeToMaturity(uint256 redemptionTimestamp, address repoToken) private view returns (uint256) { - return redemptionTimestamp - block.timestamp; - } - + /** + * @notice Get the next node in the list + * @param listData The list data + * @param current The current node + * @return The next node + */ function _getNext(RepoTokenListData storage listData, address current) private view returns (address) { return listData.nodes[current].next; } + /** + * @notice Count the number of nodes in the list + * @param listData The list data + * @return count The number of nodes in the list + */ function _count(RepoTokenListData storage listData) private view returns (uint256 count) { if (listData.head == NULL_NODE) return 0; address current = listData.head; @@ -48,6 +71,15 @@ library RepoTokenList { } } + /** + * @notice Returns an array of addresses representing the repoTokens currently held in the list data + * @param listData The list data + * @return holdings An array of addresses of the repoTokens held in the list + * + * @dev This function iterates through the list of repoTokens and returns their addresses in an array. + * It first counts the number of repoTokens, initializes an array of that size, and then populates the array + * with the addresses of the repoTokens. + */ function holdings(RepoTokenListData storage listData) internal view returns (address[] memory holdings) { uint256 count = _count(listData); if (count > 0) { @@ -61,6 +93,12 @@ library RepoTokenList { } } + /** + * @notice Get the weighted time to maturity of the strategy's holdings of a specified repoToken + * @param repoToken The address of the repoToken + * @param repoTokenBalanceInBaseAssetPrecision The balance of the repoToken in base asset precision + * @return weightedTimeToMaturity The weighted time to maturity in seconds x repoToken balance in base asset precision + */ function getRepoTokenWeightedTimeToMaturity( address repoToken, uint256 repoTokenBalanceInBaseAssetPrecision ) internal view returns (uint256 weightedTimeToMaturity) { @@ -73,6 +111,22 @@ library RepoTokenList { } } + /** + * @notice This function calculates the cumulative weighted time to maturity and cumulative amount of all repoTokens in the list. + * @param listData The list data + * @param repoToken The address of the repoToken (optional) + * @param repoTokenAmount The amount of the repoToken (optional) + * @param purchaseTokenPrecision The precision of the purchase token + * @param liquidBalance The liquid balance + * @return cumulativeWeightedTimeToMaturity The cumulative weighted time to maturity for all repoTokens + * @return cumulativeRepoTokenAmount The cumulative repoToken amount across all repoTokens + * @return found Whether the specified repoToken was found in the list + * + * @dev The `repoToken` and `repoTokenAmount` parameters are optional and provide flexibility + * to adjust the calculations to include the provided repoToken and amount. If `repoToken` is + * set to `address(0)` or `repoTokenAmount` is `0`, the function calculates the cumulative + * data without specific token adjustments. + */ function getCumulativeRepoTokenData( RepoTokenListData storage listData, address repoToken, @@ -80,18 +134,23 @@ library RepoTokenList { uint256 purchaseTokenPrecision, uint256 liquidBalance ) internal view returns (uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeRepoTokenAmount, bool found) { + // Return early if the list is empty if (listData.head == NULL_NODE) return (0, 0, false); + // Initialize the current pointer to the head of the list address current = listData.head; while (current != NULL_NODE) { uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); + // Process if the repo token has a positive balance if (repoTokenBalance > 0) { + // Add repoTokenAmount if the current token matches the specified repoToken if (repoToken == current) { repoTokenBalance += repoTokenAmount; found = true; } + // Convert the repo token balance to base asset precision uint256 redemptionValue = ITermRepoToken(current).redemptionValue(); uint256 repoTokenPrecision = 10**ERC20(current).decimals(); @@ -99,18 +158,101 @@ library RepoTokenList { (redemptionValue * repoTokenBalance * purchaseTokenPrecision) / (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + // Calculate the weighted time to maturity uint256 weightedTimeToMaturity = getRepoTokenWeightedTimeToMaturity( current, repoTokenBalanceInBaseAssetPrecision ); + // Accumulate the results cumulativeWeightedTimeToMaturity += weightedTimeToMaturity; cumulativeRepoTokenAmount += repoTokenBalanceInBaseAssetPrecision; } + // Move to the next repo token in the list current = _getNext(listData, current); } } + /** + * @notice Get the present value of repoTokens + * @param listData The list data + * @param purchaseTokenPrecision The precision of the purchase token + * @param repoTokenToMatch The address of the repoToken to match (optional) + * @return totalPresentValue The total present value of the repoTokens + * @dev If the `repoTokenToMatch` parameter is provided (non-zero address), the function will filter + * the calculations to include only the specified repoToken. If `repoTokenToMatch` is not provided + * (zero address), it will aggregate the present value of all repoTokens in the list. + * + * Example usage: + * - To get the present value of all repoTokens: call with `repoTokenToMatch` set to `address(0)`. + * - To get the present value of a specific repoToken: call with `repoTokenToMatch` set to the address of the desired repoToken. + */ + function getPresentValue( + RepoTokenListData storage listData, + uint256 purchaseTokenPrecision, + address repoTokenToMatch + ) internal view returns (uint256 totalPresentValue) { + // If the list is empty, return 0 + if (listData.head == NULL_NODE) return 0; + + address current = listData.head; + while (current != NULL_NODE) { + uint256 currentMaturity = getRepoTokenMaturity(current); + uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); + uint256 repoTokenPrecision = 10**ERC20(current).decimals(); + uint256 discountRate = listData.discountRates[current]; + + // Convert repo token balance to base asset precision + // (ratePrecision * repoPrecision * purchasePrecision) / (repoPrecision * ratePrecision) = purchasePrecision + uint256 repoTokenBalanceInBaseAssetPrecision = + (ITermRepoToken(current).redemptionValue() * repoTokenBalance * purchaseTokenPrecision) / + (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + + // Calculate present value based on maturity + if (currentMaturity > block.timestamp) { + totalPresentValue += RepoTokenUtils.calculatePresentValue( + repoTokenBalanceInBaseAssetPrecision, purchaseTokenPrecision, currentMaturity, discountRate + ); + } else { + totalPresentValue += repoTokenBalanceInBaseAssetPrecision; + } + + // If filtering by a specific repo token, stop early if matched + if (repoTokenToMatch != address(0) && current == repoTokenToMatch) { + // matching a specific repoToken and terminate early because the list is sorted + // with no duplicates + break; + } + + // Move to the next token in the list + current = _getNext(listData, current); + } + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Calculates the time remaining until a repoToken matures + * @param redemptionTimestamp The redemption timestamp of the repoToken + * @param repoToken The address of the repoToken + * @return uint256 The time remaining (in seconds) until the repoToken matures + * + * @dev This function calculates the difference between the redemption timestamp and the current block timestamp + * to determine how many seconds are left until the repoToken reaches its maturity. + */ + function _getRepoTokenTimeToMaturity(uint256 redemptionTimestamp, address repoToken) private view returns (uint256) { + return redemptionTimestamp - block.timestamp; + } + + /** + * @notice Removes and redeems matured repoTokens from the list data + * @param listData The list data + * + * @dev Iterates through the list of repoTokens and removes those that have matured. If a matured repoToken has a balance, + * the function attempts to redeem it. This helps maintain the list by clearing out matured repoTokens and redeeming their balances. + */ function removeAndRedeemMaturedTokens(RepoTokenListData storage listData) internal { if (listData.head == NULL_NODE) return; @@ -146,7 +288,7 @@ library RepoTokenList { listData.nodes[prev].next = next; delete listData.nodes[current]; - delete listData.auctionRates[current]; + delete listData.discountRates[current]; } } else { /// @dev early exit because list is sorted @@ -156,44 +298,47 @@ library RepoTokenList { prev = current; current = next; } - } - - function getAuctionRate(ITermController termController, ITermRepoToken repoToken) internal view returns (uint256) { - (AuctionMetadata[] memory auctionMetadata, ) = termController.getTermAuctionResults(repoToken.termRepoId()); - - uint256 len = auctionMetadata.length; - - if (len == 0) { - revert InvalidRepoToken(address(repoToken)); - } - - return auctionMetadata[len - 1].auctionClearingRate; - } - + } + + /** + * @notice Validates a repoToken against specific criteria + * @param listData The list data + * @param repoToken The repoToken to validate + * @param termController The term controller + * @param asset The address of the base asset + * @return redemptionTimestamp The redemption timestamp of the validated repoToken + * + * @dev Ensures the repoToken is deployed, matches the purchase token, is not matured, and meets collateral requirements. + * Reverts with `InvalidRepoToken` if any validation check fails. + */ function validateRepoToken( RepoTokenListData storage listData, ITermRepoToken repoToken, ITermController termController, address asset ) internal view returns (uint256 redemptionTimestamp) { + // Ensure the repo token is deployed by term if (!termController.isTermDeployed(address(repoToken))) { revert InvalidRepoToken(address(repoToken)); } + // Retrieve repo token configuration address purchaseToken; address collateralManager; (redemptionTimestamp, purchaseToken, , collateralManager) = repoToken.config(); + + // Validate purchase token if (purchaseToken != address(asset)) { revert InvalidRepoToken(address(repoToken)); } - // skip matured repo tokens + // Check if repo token has matured if (redemptionTimestamp < block.timestamp) { revert InvalidRepoToken(address(repoToken)); } + // Validate collateral token ratios uint256 numTokens = ITermRepoCollateralManager(collateralManager).numOfAcceptedCollateralTokens(); - for (uint256 i; i < numTokens; i++) { address currentToken = ITermRepoCollateralManager(collateralManager).collateralTokens(i); uint256 minCollateralRatio = listData.collateralTokenParams[currentToken]; @@ -208,41 +353,61 @@ library RepoTokenList { } } + /** + * @notice Validate and insert a repoToken into the list data + * @param listData The list data + * @param repoToken The repoToken to validate and insert + * @param termController The term controller + * @param discountRateAdapter The discount rate adapter + * @param asset The address of the base asset + * @return discountRate The discount rate to be applied to the validated repoToken + * @return redemptionTimestamp The redemption timestamp of the validated repoToken + */ function validateAndInsertRepoToken( RepoTokenListData storage listData, ITermRepoToken repoToken, ITermController termController, + ITermDiscountRateAdapter discountRateAdapter, address asset - ) internal returns (uint256 auctionRate, uint256 redemptionTimestamp) - { - auctionRate = listData.auctionRates[address(repoToken)]; - if (auctionRate != INVALID_AUCTION_RATE) { + ) internal returns (uint256 discountRate, uint256 redemptionTimestamp) { + discountRate = listData.discountRates[address(repoToken)]; + if (discountRate != INVALID_AUCTION_RATE) { (redemptionTimestamp, , ,) = repoToken.config(); - // skip matured repo tokens + // skip matured repoTokens if (redemptionTimestamp < block.timestamp) { revert InvalidRepoToken(address(repoToken)); } - uint256 oracleRate = getAuctionRate(termController, repoToken); + uint256 oracleRate = discountRateAdapter.getDiscountRate(address(repoToken)); if (oracleRate != INVALID_AUCTION_RATE) { - if (auctionRate != oracleRate) { - listData.auctionRates[address(repoToken)] = oracleRate; + if (discountRate != oracleRate) { + listData.discountRates[address(repoToken)] = oracleRate; } } } else { - auctionRate = getAuctionRate(termController, repoToken); + discountRate = discountRateAdapter.getDiscountRate(address(repoToken)); redemptionTimestamp = validateRepoToken(listData, repoToken, termController, asset); insertSorted(listData, address(repoToken)); - listData.auctionRates[address(repoToken)] = auctionRate; + listData.discountRates[address(repoToken)] = discountRate; } } + /** + * @notice Insert a repoToken into the list in a sorted manner + * @param listData The list data + * @param repoToken The address of the repoToken to be inserted + * + * @dev Inserts the `repoToken` into the `listData` while maintaining the list sorted by the repoTokens' maturity timestamps. + * The function iterates through the list to find the correct position for the new `repoToken` and updates the pointers accordingly. + */ function insertSorted(RepoTokenListData storage listData, address repoToken) internal { + // Start at the head of the list address current = listData.head; + // If the list is empty, set the new repoToken as the head if (current == NULL_NODE) { listData.head = repoToken; return; @@ -251,7 +416,7 @@ library RepoTokenList { address prev; while (current != NULL_NODE) { - // already in list + // If the repoToken is already in the list, exit if (current == repoToken) { break; } @@ -259,6 +424,7 @@ library RepoTokenList { uint256 currentMaturity = getRepoTokenMaturity(current); uint256 maturityToInsert = getRepoTokenMaturity(repoToken); + // Insert repoToken before current if its maturity is less than or equal if (maturityToInsert <= currentMaturity) { if (prev == NULL_NODE) { listData.head = repoToken; @@ -269,8 +435,10 @@ library RepoTokenList { break; } + // Move to the next node address next = _getNext(listData, current); + // If at the end of the list, insert repoToken after current if (next == NULL_NODE) { listData.nodes[current].next = repoToken; break; @@ -280,34 +448,4 @@ library RepoTokenList { current = next; } } - - function getPresentValue( - RepoTokenListData storage listData, - uint256 purchaseTokenPrecision - ) internal view returns (uint256 totalPresentValue) { - if (listData.head == NULL_NODE) return 0; - - address current = listData.head; - while (current != NULL_NODE) { - uint256 currentMaturity = getRepoTokenMaturity(current); - uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - uint256 repoTokenPrecision = 10**ERC20(current).decimals(); - uint256 auctionRate = listData.auctionRates[current]; - - // (ratePrecision * repoPrecision * purchasePrecision) / (repoPrecision * ratePrecision) = purchasePrecision - uint256 repoTokenBalanceInBaseAssetPrecision = - (ITermRepoToken(current).redemptionValue() * repoTokenBalance * purchaseTokenPrecision) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); - - if (currentMaturity > block.timestamp) { - totalPresentValue += RepoTokenUtils.calculatePresentValue( - repoTokenBalanceInBaseAssetPrecision, purchaseTokenPrecision, currentMaturity, auctionRate - ); - } else { - totalPresentValue += repoTokenBalanceInBaseAssetPrecision; - } - - current = _getNext(listData, current); - } - } } diff --git a/src/RepoTokenUtils.sol b/src/RepoTokenUtils.sol index 52dcf76d..1376f403 100644 --- a/src/RepoTokenUtils.sol +++ b/src/RepoTokenUtils.sol @@ -4,10 +4,25 @@ pragma solidity ^0.8.18; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; +/*////////////////////////////////////////////////////////////// + LIBRARY: RepoTokenUtils +//////////////////////////////////////////////////////////////*/ + library RepoTokenUtils { uint256 public constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; uint256 public constant RATE_PRECISION = 1e18; + /*////////////////////////////////////////////////////////////// + PURE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Convert repoToken amount to purchase token precision + * @param repoTokenPrecision The precision of the repoToken + * @param purchaseTokenPrecision The precision of the purchase token + * @param purchaseTokenAmountInRepoPrecision The amount of purchase token in repoToken precision + * @return The amount in purchase token precision + */ function repoToPurchasePrecision( uint256 repoTokenPrecision, uint256 purchaseTokenPrecision, @@ -16,6 +31,13 @@ library RepoTokenUtils { return (purchaseTokenAmountInRepoPrecision * purchaseTokenPrecision) / repoTokenPrecision; } + /** + * @notice Convert purchase token amount to repoToken precision + * @param repoTokenPrecision The precision of the repoToken + * @param purchaseTokenPrecision The precision of the purchase token + * @param repoTokenAmount The amount of repoToken + * @return The amount in repoToken precision + */ function purchaseToRepoPrecision( uint256 repoTokenPrecision, uint256 purchaseTokenPrecision, @@ -24,11 +46,23 @@ library RepoTokenUtils { return (repoTokenAmount * repoTokenPrecision) / purchaseTokenPrecision; } + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Calculate the present value of a repoToken + * @param repoTokenAmountInBaseAssetPrecision The amount of repoToken in base asset precision + * @param purchaseTokenPrecision The precision of the purchase token + * @param redemptionTimestamp The redemption timestamp of the repoToken + * @param discountRate The auction rate + * @return presentValue The present value of the repoToken + */ function calculatePresentValue( uint256 repoTokenAmountInBaseAssetPrecision, uint256 purchaseTokenPrecision, uint256 redemptionTimestamp, - uint256 auctionRate + uint256 discountRate ) internal view returns (uint256 presentValue) { uint256 timeLeftToMaturityDayFraction = ((redemptionTimestamp - block.timestamp) * purchaseTokenPrecision) / THREESIXTY_DAYCOUNT_SECONDS; @@ -36,10 +70,16 @@ library RepoTokenUtils { // repoTokenAmountInBaseAssetPrecision / (1 + r * days / 360) presentValue = (repoTokenAmountInBaseAssetPrecision * purchaseTokenPrecision) / - (purchaseTokenPrecision + (auctionRate * timeLeftToMaturityDayFraction / RATE_PRECISION)); + (purchaseTokenPrecision + (discountRate * timeLeftToMaturityDayFraction / RATE_PRECISION)); } - // returns repo token amount in base asset precision + /** + * @notice Get the normalized amount of a repoToken in base asset precision + * @param repoToken The address of the repoToken + * @param repoTokenAmount The amount of the repoToken + * @param purchaseTokenPrecision The precision of the purchase token + * @return repoTokenAmountInBaseAssetPrecision The normalized amount of the repoToken in base asset precision + */ function getNormalizedRepoTokenAmount( address repoToken, uint256 repoTokenAmount, @@ -51,4 +91,4 @@ library RepoTokenUtils { (redemptionValue * repoTokenAmount * purchaseTokenPrecision) / (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); } -} +} \ No newline at end of file diff --git a/src/Strategy.sol b/src/Strategy.sol index bc37ec7d..01fe2d7e 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -4,17 +4,23 @@ 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 {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.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 {ITermRepoCollateralManager} from "./interfaces/term/ITermRepoCollateralManager.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 interfaces for many popular DeFi projects, or add your own! +//import "../interfaces//.sol"; + /** * The `TokenizedStrategy` variable can be used to retrieve the strategies * specific storage data your contract. @@ -28,296 +34,657 @@ import {RepoTokenUtils} from "./RepoTokenUtils.sol"; // NOTE: To implement permissioned functions you can use the onlyManagement, onlyEmergencyAuthorized and onlyKeepers modifiers -contract Strategy is BaseStrategy { +contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { using SafeERC20 for IERC20; using RepoTokenList for RepoTokenListData; using TermAuctionList for TermAuctionListData; + // Errors error InvalidTermAuction(address auction); error TimeToMaturityAboveThreshold(); - error BalanceBelowLiquidityThreshold(); + error BalanceBelowliquidityReserveRatio(); error InsufficientLiquidBalance(uint256 have, uint256 want); - + error RepoTokenConcentrationTooHigh(address repoToken); + + // Immutable state variables ITermVaultEvents public immutable TERM_VAULT_EVENT_EMITTER; uint256 public immutable PURCHASE_TOKEN_PRECISION; IERC4626 public immutable YEARN_VAULT; + // State variables ITermController public termController; + ITermDiscountRateAdapter public discountRateAdapter; RepoTokenListData internal repoTokenListData; TermAuctionListData internal termAuctionListData; uint256 public timeToMaturityThreshold; // seconds - uint256 public liquidityThreshold; // purchase token precision (underlying) - uint256 public auctionRateMarkup; // 1e18 (TODO: check this) + uint256 public liquidityReserveRatio; // purchase token precision (underlying) + uint256 public discountRateMarkup; // 1e18 (TODO: check this) + uint256 public repoTokenConcentrationLimit; - // These governance functions should have a different role - function setTermController(address newTermController) external onlyManagement { - require(newTermController != address(0)); - TERM_VAULT_EVENT_EMITTER.emitTermControllerUpdated(address(termController), newTermController); - termController = ITermController(newTermController); + /*////////////////////////////////////////////////////////////// + MANAGEMENT FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Rescue tokens from the contract + * @param token The address of the token to rescue + * @param amount The amount of tokens to rescue + */ + function rescueToken( + address token, + uint256 amount + ) external onlyManagement { + if (amount > 0) { + IERC20(token).safeTransfer(msg.sender, amount); + } } - function setTimeToMaturityThreshold(uint256 newTimeToMaturityThreshold) external onlyManagement { - TERM_VAULT_EVENT_EMITTER.emitTimeToMaturityThresholdUpdated(timeToMaturityThreshold, newTimeToMaturityThreshold); - timeToMaturityThreshold = newTimeToMaturityThreshold; + /** + * @notice Pause the contract + */ + function pause() external onlyManagement { + _pause(); + TERM_VAULT_EVENT_EMITTER.emitPaused(); } - function setLiquidityThreshold(uint256 newLiquidityThreshold) external onlyManagement { - TERM_VAULT_EVENT_EMITTER.emitLiquidityThresholdUpdated(liquidityThreshold, newLiquidityThreshold); - liquidityThreshold = newLiquidityThreshold; + /** + * @notice Unpause the contract + */ + function unpause() external onlyManagement { + _unpause(); + TERM_VAULT_EVENT_EMITTER.emitUnpaused(); } - function setAuctionRateMarkup(uint256 newAuctionRateMarkup) external onlyManagement { - TERM_VAULT_EVENT_EMITTER.emitAuctionRateMarkupUpdated(auctionRateMarkup, newAuctionRateMarkup); - auctionRateMarkup = newAuctionRateMarkup; + /** + * @notice Set the term controller + * @param newTermController The address of the new term controller + */ + function setTermController( + address newTermController + ) external onlyManagement { + require(newTermController != address(0)); + TERM_VAULT_EVENT_EMITTER.emitTermControllerUpdated( + address(termController), + newTermController + ); + termController = ITermController(newTermController); } - function setCollateralTokenParams(address tokenAddr, uint256 minCollateralRatio) external onlyManagement { - TERM_VAULT_EVENT_EMITTER.emitMinCollateralRatioUpdated(tokenAddr, minCollateralRatio); - repoTokenListData.collateralTokenParams[tokenAddr] = minCollateralRatio; + /** + * @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 onlyManagement { + TERM_VAULT_EVENT_EMITTER.emitDiscountRateAdapterUpdated( + address(discountRateAdapter), + newAdapter + ); + discountRateAdapter = ITermDiscountRateAdapter(newAdapter); } - function repoTokenHoldings() external view returns (address[] memory) { - return repoTokenListData.holdings(); + /** + * @notice Set the weighted time to maturity cap + * @param newTimeToMaturityThreshold The new weighted time to maturity cap + */ + function setTimeToMaturityThreshold( + uint256 newTimeToMaturityThreshold + ) external onlyManagement { + TERM_VAULT_EVENT_EMITTER.emitTimeToMaturityThresholdUpdated( + timeToMaturityThreshold, + newTimeToMaturityThreshold + ); + timeToMaturityThreshold = newTimeToMaturityThreshold; } - function pendingOffers() external view returns (bytes32[] memory) { - return termAuctionListData.pendingOffers(); + /** + * @notice Set the liquidity reserve factor + * @param newLiquidityReserveRatio The new liquidity reserve factor + */ + function setLiquidityReserveRatio( + uint256 newLiquidityReserveRatio + ) external onlyManagement { + TERM_VAULT_EVENT_EMITTER.emitLiquidityReserveRatioUpdated( + liquidityReserveRatio, + newLiquidityReserveRatio + ); + liquidityReserveRatio = newLiquidityReserveRatio; } - function _calculateWeightedMaturity( - address repoToken, - uint256 repoTokenAmount, - uint256 liquidBalance - ) private view returns (uint256) { - uint256 cumulativeWeightedTimeToMaturity; // in seconds - uint256 cumulativeAmount; // in purchase token precision + /** + * @notice Set the repoToken concentration limit + * @param newRepoTokenConcentrationLimit The new repoToken concentration limit + */ + function setRepoTokenConcentrationLimit( + uint256 newRepoTokenConcentrationLimit + ) external onlyManagement { + TERM_VAULT_EVENT_EMITTER.emitRepoTokenConcentrationLimitUpdated( + repoTokenConcentrationLimit, + newRepoTokenConcentrationLimit + ); + repoTokenConcentrationLimit = newRepoTokenConcentrationLimit; + } - ( - uint256 cumulativeRepoTokenWeightedTimeToMaturity, - uint256 cumulativeRepoTokenAmount, - bool foundInRepoTokenList - ) = repoTokenListData.getCumulativeRepoTokenData( - repoToken, repoTokenAmount, PURCHASE_TOKEN_PRECISION, liquidBalance + /** + * @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 onlyManagement { + TERM_VAULT_EVENT_EMITTER.emitDiscountRateMarkupUpdated( + discountRateMarkup, + newDiscountRateMarkup ); + discountRateMarkup = newDiscountRateMarkup; + } - cumulativeWeightedTimeToMaturity += cumulativeRepoTokenWeightedTimeToMaturity; - cumulativeAmount += cumulativeRepoTokenAmount; + /** + * @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 onlyManagement { + TERM_VAULT_EVENT_EMITTER.emitMinCollateralRatioUpdated( + tokenAddr, + minCollateralRatio + ); + repoTokenListData.collateralTokenParams[tokenAddr] = minCollateralRatio; + } - ( - uint256 cumulativeOfferWeightedTimeToMaturity, - uint256 cumulativeOfferAmount, - bool foundInOfferList - ) = termAuctionListData.getCumulativeOfferData( - repoTokenListData, termController, repoToken, repoTokenAmount, PURCHASE_TOKEN_PRECISION - ); + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ - cumulativeWeightedTimeToMaturity += cumulativeOfferWeightedTimeToMaturity; - cumulativeAmount += cumulativeOfferAmount; - if (!foundInRepoTokenList && !foundInOfferList && repoToken != address(0)) { - uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( - repoToken, - repoTokenAmount, - PURCHASE_TOKEN_PRECISION - ); + /** + * @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(); + } - cumulativeAmount += repoTokenAmountInBaseAssetPrecision; - cumulativeWeightedTimeToMaturity += RepoTokenList.getRepoTokenWeightedTimeToMaturity( - repoToken, - repoTokenAmountInBaseAssetPrecision - ); - } + /** + * @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(address(this)); + } - /// @dev avoid div by 0 - if (cumulativeAmount == 0 && liquidBalance == 0) { - return 0; - } + /** + * @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(); + } - // time * purchaseTokenPrecision / purchaseTokenPrecision - return cumulativeWeightedTimeToMaturity / (cumulativeAmount + liquidBalance); + /** + * @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(); } - function simulateWeightedTimeToMaturity(address repoToken, uint256 amount) external view returns (uint256) { - // do not validate if we are simulating with existing repo tokens + /** + * @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 uint256 The simulated weighted time to maturity for the entire strategy + * + * @dev This function validates the repoToken, normalizes its amount, checks concentration limits, + * and calculates the weighted time to maturity for the specified repoToken and amount. The result + * reflects the new weighted time to maturity for the entire strategy, including the new repoToken position. + */ + function simulateWeightedTimeToMaturity( + address repoToken, + uint256 amount + ) external view returns (uint256) { + // do not validate if we are simulating with existing repoTokens if (repoToken != address(0)) { - repoTokenListData.validateRepoToken(ITermRepoToken(repoToken), termController, address(asset)); + repoTokenListData.validateRepoToken( + ITermRepoToken(repoToken), + termController, + address(asset) + ); + + uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); + uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken( + repoToken + ).redemptionValue() * + amount * + PURCHASE_TOKEN_PRECISION) / + (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + _validateRepoTokenConcentration( + repoToken, + repoTokenAmountInBaseAssetPrecision, + 0 + ); } - return _calculateWeightedMaturity(repoToken, amount, _totalLiquidBalance(address(this))); + + return + _calculateWeightedMaturity( + repoToken, + amount, + _totalLiquidBalance(address(this)) + ); } + /** + * @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 auctionRate, + address repoToken, + uint256 discountRate, uint256 amount ) external view returns (uint256) { - (uint256 redemptionTimestamp, , ,) = ITermRepoToken(repoToken).config(); - uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals(); - uint256 repoTokenAmountInBaseAssetPrecision = - (ITermRepoToken(repoToken).redemptionValue() * amount * PURCHASE_TOKEN_PRECISION) / + (uint256 redemptionTimestamp, , , ) = ITermRepoToken(repoToken) + .config(); + uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); + uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) + .redemptionValue() * + amount * + PURCHASE_TOKEN_PRECISION) / (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); - return RepoTokenUtils.calculatePresentValue( - repoTokenAmountInBaseAssetPrecision, - PURCHASE_TOKEN_PRECISION, - redemptionTimestamp, - auctionRate - ); + return + RepoTokenUtils.calculatePresentValue( + repoTokenAmountInBaseAssetPrecision, + PURCHASE_TOKEN_PRECISION, + redemptionTimestamp, + discountRate + ); } - function _totalLiquidBalance(address addr) private view returns (uint256) { - uint256 underlyingBalance = IERC20(asset).balanceOf(address(this)); - return _assetBalance() + underlyingBalance; + /** + * @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) { + return + repoTokenListData.getPresentValue( + PURCHASE_TOKEN_PRECISION, + repoToken + ) + + termAuctionListData.getPresentValue( + repoTokenListData, + discountRateAdapter, + PURCHASE_TOKEN_PRECISION, + repoToken + ); } - function _sweepAssetAndRedeemRepoTokens(uint256 liquidAmountRequired) private { - termAuctionListData.removeCompleted(repoTokenListData, termController, address(asset)); - repoTokenListData.removeAndRedeemMaturedTokens(); - - uint256 underlyingBalance = IERC20(asset).balanceOf(address(this)); - if (underlyingBalance > liquidAmountRequired) { - unchecked { - YEARN_VAULT.deposit(underlyingBalance - liquidAmountRequired, address(this)); - } - } else if (underlyingBalance < liquidAmountRequired) { - unchecked { - _withdrawAsset(liquidAmountRequired - underlyingBalance); - } - } - } + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /** + * @dev Withdraw assets from the Yearn vault + * @param amount The amount to withdraw + */ function _withdrawAsset(uint256 amount) private { - YEARN_VAULT.withdraw(YEARN_VAULT.convertToShares(amount), address(this), address(this)); + YEARN_VAULT.withdraw( + YEARN_VAULT.convertToShares(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))); + return + YEARN_VAULT.convertToAssets(YEARN_VAULT.balanceOf(address(this))); } - // TODO: reentrancy check - function sellRepoToken(address repoToken, uint256 repoTokenAmount) external { - require(repoTokenAmount > 0); + /** + * @notice Calculates the total liquid balance of the assets managed by the strategy + * @param addr The address of the strategy or contract that holds the assets + * @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(address addr) private view returns (uint256) { + uint256 underlyingBalance = IERC20(asset).balanceOf(address(this)); + return _assetBalance() + underlyingBalance; + } - (uint256 auctionRate, uint256 redemptionTimestamp) = repoTokenListData.validateAndInsertRepoToken( - ITermRepoToken(repoToken), - termController, - address(asset) - ); + /** + * @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() internal view returns (uint256 totalValue) { + return + _totalLiquidBalance(address(this)) + + repoTokenListData.getPresentValue( + PURCHASE_TOKEN_PRECISION, + address(0) + ) + + termAuctionListData.getPresentValue( + repoTokenListData, + discountRateAdapter, + PURCHASE_TOKEN_PRECISION, + address(0) + ); + } - _sweepAssetAndRedeemRepoTokens(0); + /** + * @dev Validate the concentration of repoTokens + * @param repoToken The address of the repoToken + * @param repoTokenAmountInBaseAssetPrecision The amount of the repoToken in base asset precision + * @param liquidBalanceToRemove The liquid balance to remove + */ + function _validateRepoTokenConcentration( + address repoToken, + uint256 repoTokenAmountInBaseAssetPrecision, + uint256 liquidBalanceToRemove + ) private view { + // 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 totalAsseValue = _totalAssetValue() + + repoTokenAmountInBaseAssetPrecision - + liquidBalanceToRemove; + + // Normalize the repoToken value and total asset value to 1e18 precision + repoTokenValue = (repoTokenValue * 1e18) / PURCHASE_TOKEN_PRECISION; + totalAsseValue = (totalAsseValue * 1e18) / PURCHASE_TOKEN_PRECISION; + + // Calculate the repoToken concentration + uint256 repoTokenConcentration = totalAsseValue == 0 + ? 0 + : (repoTokenValue * 1e18) / totalAsseValue; + + // Check if the repoToken concentration exceeds the predefined limit + if (repoTokenConcentration > repoTokenConcentrationLimit) { + revert RepoTokenConcentrationTooHigh(repoToken); + } + } - uint256 liquidBalance = _totalLiquidBalance(address(this)); - require(liquidBalance > 0); + /** + * @notice Validates the resulting weighted time to maturity when submitting a new offer + * @param repoToken The address of the repoToken associated with the Term auction offer + * @param newOfferAmount The amount associated with the Term auction offer + * @param newLiquidBalance The new liquid balance of the strategy after accounting for the new offer + * + * @dev This function calculates the resulting weighted time to maturity assuming that a submitted offer is accepted + * and checks if it exceeds the predefined threshold (`timeToMaturityThreshold`). If the threshold is exceeded, + * the function reverts the transaction with an error. + */ + function _validateWeightedMaturity( + address repoToken, + uint256 newOfferAmount, + uint256 newLiquidBalance + ) private { + // Calculate the precision of the repoToken + uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); + + // Convert the new offer amount to repoToken precision + uint256 offerAmountInRepoPrecision = RepoTokenUtils + .purchaseToRepoPrecision( + repoTokenPrecision, + PURCHASE_TOKEN_PRECISION, + newOfferAmount + ); - uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals(); - uint256 repoTokenAmountInBaseAssetPrecision = - (ITermRepoToken(repoToken).redemptionValue() * repoTokenAmount * PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); - uint256 proceeds = RepoTokenUtils.calculatePresentValue( - repoTokenAmountInBaseAssetPrecision, - PURCHASE_TOKEN_PRECISION, - redemptionTimestamp, - auctionRate + auctionRateMarkup + // Calculate the resulting weighted time to maturity + uint256 resultingWeightedTimeToMaturity = _calculateWeightedMaturity( + repoToken, + offerAmountInRepoPrecision, + newLiquidBalance ); - if (liquidBalance < proceeds) { - revert InsufficientLiquidBalance(liquidBalance, proceeds); + // Check if the resulting weighted time to maturity exceeds the threshold + if (resultingWeightedTimeToMaturity > timeToMaturityThreshold) { + revert TimeToMaturityAboveThreshold(); } + } + + /** + * @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) { - uint256 resultingTimeToMaturity = _calculateWeightedMaturity( - repoToken, repoTokenAmount, liquidBalance - proceeds - ); + // Initialize cumulative weighted time to maturity and cumulative amount + uint256 cumulativeWeightedTimeToMaturity; // in seconds + uint256 cumulativeAmount; // in purchase token precision - if (resultingTimeToMaturity > timeToMaturityThreshold) { - revert TimeToMaturityAboveThreshold(); - } + // Get cumulative data from repoToken list + ( + uint256 cumulativeRepoTokenWeightedTimeToMaturity, + uint256 cumulativeRepoTokenAmount, + bool foundInRepoTokenList + ) = repoTokenListData.getCumulativeRepoTokenData( + repoToken, + repoTokenAmount, + PURCHASE_TOKEN_PRECISION, + liquidBalance + ); - liquidBalance -= proceeds; + // Accumulate repoToken data + cumulativeWeightedTimeToMaturity += cumulativeRepoTokenWeightedTimeToMaturity; + cumulativeAmount += cumulativeRepoTokenAmount; - if (liquidBalance < liquidityThreshold) { - revert BalanceBelowLiquidityThreshold(); - } + ( + uint256 cumulativeOfferWeightedTimeToMaturity, + uint256 cumulativeOfferAmount, + bool foundInOfferList + ) = termAuctionListData.getCumulativeOfferData( + repoTokenListData, + termController, + repoToken, + repoTokenAmount, + PURCHASE_TOKEN_PRECISION + ); - // withdraw from underlying vault - _withdrawAsset(proceeds); - - IERC20(repoToken).safeTransferFrom(msg.sender, address(this), repoTokenAmount); - IERC20(asset).safeTransfer(msg.sender, proceeds); - } - - function deleteAuctionOffers(address termAuction, bytes32[] calldata offerIds) external onlyManagement { - if (!termController.isTermDeployed(termAuction)) { - revert InvalidTermAuction(termAuction); - } + // Accumulate offer data + cumulativeWeightedTimeToMaturity += cumulativeOfferWeightedTimeToMaturity; + cumulativeAmount += cumulativeOfferAmount; - ITermAuction auction = ITermAuction(termAuction); - ITermAuctionOfferLocker offerLocker = ITermAuctionOfferLocker(auction.termAuctionOfferLocker()); + if ( + !foundInRepoTokenList && + !foundInOfferList && + repoToken != address(0) + ) { + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + repoToken, + repoTokenAmount, + PURCHASE_TOKEN_PRECISION + ); - offerLocker.unlockOffers(offerIds); + cumulativeAmount += repoTokenAmountInBaseAssetPrecision; + cumulativeWeightedTimeToMaturity += RepoTokenList + .getRepoTokenWeightedTimeToMaturity( + repoToken, + repoTokenAmountInBaseAssetPrecision + ); + } - termAuctionListData.removeCompleted(repoTokenListData, termController, address(asset)); + // Avoid division by zero + if (cumulativeAmount == 0 && liquidBalance == 0) { + return 0; + } - _sweepAssetAndRedeemRepoTokens(0); + // Calculate and return weighted time to maturity + // time * purchaseTokenPrecision / purchaseTokenPrecision + return + cumulativeWeightedTimeToMaturity / + (cumulativeAmount + liquidBalance); } - function _generateOfferId( - bytes32 id, - address offerLocker - ) internal view returns (bytes32) { - return keccak256( - abi.encodePacked(id, address(this), offerLocker) - ); + function _sweepAsset() private { + YEARN_VAULT.deposit(IERC20(asset).balanceOf(address(this)), address(this)); } - function _validateWeightedMaturity( - address repoToken, - uint256 newOfferAmount, - uint256 newLiquidBalance - ) private { - uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals(); - uint256 offerAmountInRepoPrecision = RepoTokenUtils.purchaseToRepoPrecision( - repoTokenPrecision, PURCHASE_TOKEN_PRECISION, newOfferAmount - ); - uint256 resultingWeightedTimeToMaturity = _calculateWeightedMaturity( - repoToken, offerAmountInRepoPrecision, newLiquidBalance + /** + * @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 { + uint256 liquidityBefore = IERC20(asset).balanceOf(address(this)); + + // Remove completed auction offers + termAuctionListData.removeCompleted( + repoTokenListData, + termController, + discountRateAdapter, + address(asset) ); - if (resultingWeightedTimeToMaturity > timeToMaturityThreshold) { - revert TimeToMaturityAboveThreshold(); + // Remove and redeem matured repoTokens + repoTokenListData.removeAndRedeemMaturedTokens(); + + uint256 liquidityAfter = IERC20(asset).balanceOf(address(this)); + uint256 liquidityDiff = liquidityAfter - liquidityBefore; + + // Deposit excess underlying balance into Yearn Vault + if (liquidityDiff > liquidAmountRequired) { + unchecked { + YEARN_VAULT.deposit( + liquidityDiff - liquidAmountRequired, + address(this) + ); + } + // Withdraw shortfall from Yearn Vault to meet required liquidity + } else if (liquidityDiff < liquidAmountRequired) { + unchecked { + _withdrawAsset(liquidAmountRequired - liquidityDiff); + } } } + /*////////////////////////////////////////////////////////////// + STRATEGIST FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @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( address termAuction, address repoToken, bytes32 idHash, bytes32 offerPriceHash, uint256 purchaseTokenAmount - ) external onlyManagement returns (bytes32[] memory offerIds) { - require(purchaseTokenAmount > 0); + ) + external + whenNotPaused + nonReentrant + onlyManagement + returns (bytes32[] memory offerIds) + { + require(purchaseTokenAmount > 0, "Purchase token amount must be greater than zero"); + // Verify that the term auction is valid and deployed by term if (!termController.isTermDeployed(termAuction)) { revert InvalidTermAuction(termAuction); } ITermAuction auction = ITermAuction(termAuction); + require(auction.termRepoId() == ITermRepoToken(repoToken).termRepoId(), "repoToken does not match term repo ID"); - require(auction.termRepoId() == ITermRepoToken(repoToken).termRepoId()); + // Validate purchase token, min collateral ratio and insert the repoToken if necessary + repoTokenListData.validateRepoToken( + ITermRepoToken(repoToken), + termController, + address(asset) + ); - // validate purchase token and min collateral ratio - repoTokenListData.validateRepoToken(ITermRepoToken(repoToken), termController, address(asset)); + _validateRepoTokenConcentration(repoToken, purchaseTokenAmount, 0); - ITermAuctionOfferLocker offerLocker = ITermAuctionOfferLocker(auction.termAuctionOfferLocker()); + // Prepare and submit the offer + ITermAuctionOfferLocker offerLocker = ITermAuctionOfferLocker( + auction.termAuctionOfferLocker() + ); require( - block.timestamp > offerLocker.auctionStartTime() - || block.timestamp < auction.auctionEndTime(), + block.timestamp > offerLocker.auctionStartTime() || + block.timestamp < auction.auctionEndTime(), "Auction not open" ); - _sweepAssetAndRedeemRepoTokens(0); //@dev sweep to ensure liquid balances up to date + // Sweep assets, redeem matured repoTokens and ensure liquid balances up to date + _sweepAsset(); + _redeemRepoTokens(0); + // Retrieve the total liquid balance uint256 liquidBalance = _totalLiquidBalance(address(this)); + uint256 newOfferAmount = purchaseTokenAmount; bytes32 offerId = _generateOfferId(idHash, address(offerLocker)); - uint256 currentOfferAmount = termAuctionListData.offers[offerId].offerAmount; + uint256 currentOfferAmount = termAuctionListData + .offers[offerId] + .offerAmount; + + // Handle adjustments if editing an existing offer if (newOfferAmount > currentOfferAmount) { // increasing offer amount uint256 offerDebit; @@ -329,10 +696,14 @@ contract Strategy is BaseStrategy { revert InsufficientLiquidBalance(liquidBalance, offerDebit); } uint256 newLiquidBalance = liquidBalance - offerDebit; - if (newLiquidBalance < liquidityThreshold) { - revert BalanceBelowLiquidityThreshold(); + if (newLiquidBalance < liquidityReserveRatio) { + revert BalanceBelowliquidityReserveRatio(); } - _validateWeightedMaturity(repoToken, newOfferAmount, newLiquidBalance); + _validateWeightedMaturity( + repoToken, + newOfferAmount, + newLiquidBalance + ); } else if (currentOfferAmount > newOfferAmount) { // decreasing offer amount uint256 offerCredit; @@ -340,16 +711,20 @@ contract Strategy is BaseStrategy { offerCredit = currentOfferAmount - newOfferAmount; } uint256 newLiquidBalance = liquidBalance + offerCredit; - if (newLiquidBalance < liquidityThreshold) { - revert BalanceBelowLiquidityThreshold(); + if (newLiquidBalance < liquidityReserveRatio) { + revert BalanceBelowliquidityReserveRatio(); } - _validateWeightedMaturity(repoToken, newOfferAmount, newLiquidBalance); + _validateWeightedMaturity( + repoToken, + newOfferAmount, + newLiquidBalance + ); } else { // no change in offer amount, do nothing } + // Submit the offer and lock it in the auction ITermAuctionOfferLocker.TermAuctionOfferSubmission memory offer; - offer.id = currentOfferAmount > 0 ? offerId : idHash; offer.offeror = address(this); offer.offerPriceHash = offerPriceHash; @@ -357,15 +732,25 @@ contract Strategy is BaseStrategy { offer.purchaseToken = address(asset); offerIds = _submitOffer( - auction, - offerLocker, + auction, + offerLocker, offer, - repoToken, + repoToken, newOfferAmount, currentOfferAmount ); } + /** + * @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, @@ -374,12 +759,19 @@ contract Strategy is BaseStrategy { uint256 newOfferAmount, uint256 currentOfferAmount ) private returns (bytes32[] memory offerIds) { - ITermRepoServicer repoServicer = ITermRepoServicer(offerLocker.termRepoServicer()); + // Retrieve the repo servicer contract + ITermRepoServicer repoServicer = ITermRepoServicer( + offerLocker.termRepoServicer() + ); - ITermAuctionOfferLocker.TermAuctionOfferSubmission[] memory offerSubmissions = - new ITermAuctionOfferLocker.TermAuctionOfferSubmission[](1); + // 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 { @@ -387,23 +779,31 @@ contract Strategy is BaseStrategy { offerDebit = newOfferAmount - currentOfferAmount; } _withdrawAsset(offerDebit); - IERC20(asset).safeApprove(address(repoServicer.termRepoLocker()), offerDebit); + IERC20(asset).safeApprove( + address(repoServicer.termRepoLocker()), + offerDebit + ); } + // Submit the offer and get the offer IDs offerIds = offerLocker.lockOffers(offerSubmissions); - require(offerIds.length > 0); + require(offerIds.length > 0, "No offer IDs returned"); + // Update the pending offers list if (currentOfferAmount == 0) { // new offer - termAuctionListData.insertPending(offerIds[0], PendingOffer({ - repoToken: repoToken, - offerAmount: offer.amount, - termAuction: auction, - offerLocker: offerLocker - })); + termAuctionListData.insertPending( + offerIds[0], + PendingOffer({ + repoToken: repoToken, + offerAmount: offer.amount, + termAuction: auction, + offerLocker: offerLocker + }) + ); } else { - // edit offer, overwrite existing + // Edit offer, overwrite existing termAuctionListData.offers[offerIds[0]] = PendingOffer({ repoToken: repoToken, offerAmount: offer.amount, @@ -413,33 +813,173 @@ contract Strategy is BaseStrategy { } } - function auctionClosed() external { - _sweepAssetAndRedeemRepoTokens(0); + /** + * @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 (!termController.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, + termController, + discountRateAdapter, + address(asset) + ); + + // Sweep any remaining assets and redeem repoTokens + _sweepAsset(); + _redeemRepoTokens(0); } - function totalAssetValue() external view returns (uint256) { - return _totalAssetValue(); + /** + * @dev Generate a term offer ID + * @param id The term offer ID hash + * @param offerLocker The address of the term offer locker + * @return The generated term offer ID + */ + function _generateOfferId( + bytes32 id, + address offerLocker + ) internal view returns (bytes32) { + return keccak256(abi.encodePacked(id, address(this), offerLocker)); } - function totalLiquidBalance() external view returns (uint256) { - return _totalLiquidBalance(address(this)); + /** + * @notice Close the auction + */ + function auctionClosed() external { + _sweepAsset(); + _redeemRepoTokens(0); } - function _totalAssetValue() internal view returns (uint256 totalValue) { - return _totalLiquidBalance(address(this)) + - repoTokenListData.getPresentValue(PURCHASE_TOKEN_PRECISION) + - termAuctionListData.getPresentValue(repoTokenListData, termController, PURCHASE_TOKEN_PRECISION); + /*////////////////////////////////////////////////////////////// + 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 nonReentrant { + // Ensure the amount of repoTokens to sell is greater than zero + require(repoTokenAmount > 0); + + // Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp + (uint256 discountRate, uint256 redemptionTimestamp) = repoTokenListData + .validateAndInsertRepoToken( + ITermRepoToken(repoToken), + termController, + discountRateAdapter, + address(asset) + ); + + // Sweep assets and redeem repoTokens, if needed + _sweepAsset(); + _redeemRepoTokens(0); + + // Retrieve total liquid balance and ensure it's greater than zero + uint256 liquidBalance = _totalLiquidBalance(address(this)); + require(liquidBalance > 0); + + // Calculate the repoToken amount in base asset precision + uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); + uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) + .redemptionValue() * + repoTokenAmount * + PURCHASE_TOKEN_PRECISION) / + (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + + // Calculate the proceeds from selling the repoToken + uint256 proceeds = RepoTokenUtils.calculatePresentValue( + repoTokenAmountInBaseAssetPrecision, + PURCHASE_TOKEN_PRECISION, + redemptionTimestamp, + discountRate + 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 > timeToMaturityThreshold) { + revert TimeToMaturityAboveThreshold(); + } + + // Ensure the remaining liquid balance is above the liquidity threshold + if ((liquidBalance - proceeds) < liquidityReserveRatio) { + revert BalanceBelowliquidityReserveRatio(); + } + + // Validate resulting repoToken concentration to ensure it meets requirements + _validateRepoTokenConcentration( + repoToken, + repoTokenAmountInBaseAssetPrecision, + 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 _asset The address of the asset + * @param _name The name of the strategy + * @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 + */ constructor( address _asset, string memory _name, address _yearnVault, + address _discountRateAdapter, address _eventEmitter ) BaseStrategy(_asset, _name) { YEARN_VAULT = IERC4626(_yearnVault); TERM_VAULT_EVENT_EMITTER = ITermVaultEvents(_eventEmitter); - PURCHASE_TOKEN_PRECISION = 10**ERC20(asset).decimals(); + PURCHASE_TOKEN_PRECISION = 10 ** ERC20(asset).decimals(); + + discountRateAdapter = ITermDiscountRateAdapter(_discountRateAdapter); IERC20(_asset).safeApprove(_yearnVault, type(uint256).max); } @@ -459,8 +999,9 @@ contract Strategy is BaseStrategy { * @param _amount The amount of 'asset' that the strategy can attempt * to deposit in the yield source. */ - function _deployFunds(uint256 _amount) internal override { - _sweepAssetAndRedeemRepoTokens(0); + function _deployFunds(uint256 _amount) internal override whenNotPaused { + _sweepAsset(); + _redeemRepoTokens(0); } /** @@ -484,8 +1025,8 @@ contract Strategy is BaseStrategy { * * @param _amount, The amount of 'asset' to be freed. */ - function _freeFunds(uint256 _amount) internal override { - _sweepAssetAndRedeemRepoTokens(_amount); + function _freeFunds(uint256 _amount) internal override whenNotPaused { + _redeemRepoTokens(_amount); } /** @@ -513,9 +1054,11 @@ contract Strategy is BaseStrategy { function _harvestAndReport() internal override + whenNotPaused returns (uint256 _totalAssets) { - _sweepAssetAndRedeemRepoTokens(0); + _sweepAsset(); + _redeemRepoTokens(0); return _totalAssetValue(); } @@ -553,7 +1096,7 @@ contract Strategy is BaseStrategy { // if(yieldSource.notShutdown()) { // return asset.balanceOf(address(this)) + asset.balanceOf(yieldSource); // } - return asset.balanceOf(address(this)); + return type(uint256).max; } /** diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 2a43d592..aa7d1f25 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -5,10 +5,12 @@ import {ITermAuction} from "./interfaces/term/ITermAuction.sol"; import {ITermAuctionOfferLocker} from "./interfaces/term/ITermAuctionOfferLocker.sol"; import {ITermController} from "./interfaces/term/ITermController.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 {RepoTokenList, RepoTokenListData} from "./RepoTokenList.sol"; import {RepoTokenUtils} from "./RepoTokenUtils.sol"; +// In-storage representation of an offer object struct PendingOffer { address repoToken; uint256 offerAmount; @@ -16,6 +18,7 @@ struct PendingOffer { ITermAuctionOfferLocker offerLocker; } +// In-memory representation of an offer object struct PendingOfferMemory { bytes32 offerId; address repoToken; @@ -35,15 +38,83 @@ struct TermAuctionListData { mapping(bytes32 => PendingOffer) offers; } +/*////////////////////////////////////////////////////////////// + LIBRARY: TermAuctionList +//////////////////////////////////////////////////////////////*/ + library TermAuctionList { using RepoTokenList for RepoTokenListData; bytes32 public constant NULL_NODE = bytes32(0); + /*////////////////////////////////////////////////////////////// + PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Get the next node in the list + * @param listData The list data + * @param current The current node + * @return The next node + */ function _getNext(TermAuctionListData storage listData, bytes32 current) private view returns (bytes32) { return listData.nodes[current].next; } + /** + * @notice Loads all pending offers into an array of `PendingOfferMemory` structs + * @param listData The list data + * @return offers An array of structs containing details of all pending offers + * + * @dev This function iterates through the list of offers and gathers their details into an array of `PendingOfferMemory` structs. + * This makes it easier to process and analyze the pending offers. + */ + function _loadOffers(TermAuctionListData storage listData) private view returns (PendingOfferMemory[] memory offers) { + uint256 len = _count(listData); + offers = new PendingOfferMemory[](len); + + uint256 i; + bytes32 current = listData.head; + while (current != NULL_NODE) { + PendingOffer memory currentOffer = listData.offers[current]; + PendingOfferMemory memory newOffer = offers[i]; + + newOffer.offerId = current; + newOffer.repoToken = currentOffer.repoToken; + newOffer.offerAmount = currentOffer.offerAmount; + newOffer.termAuction = currentOffer.termAuction; + newOffer.offerLocker = currentOffer.offerLocker; + + i++; + current = _getNext(listData, current); + } + } + + /** + * @notice Marks a specific repoToken as seen within an array of `PendingOfferMemory` structs + * @param offers The array of `PendingOfferMemory` structs representing the pending offers + * @param repoToken The address of the repoToken to be marked as seen + * + * @dev This function iterates through the `offers` array and sets the `isRepoTokenSeen` flag to `true` + * for the specified `repoToken`. This helps to avoid double-counting or reprocessing the same repoToken. + */ + function _markRepoTokenAsSeen(PendingOfferMemory[] memory offers, address repoToken) private view { + for (uint256 i; i < offers.length; i++) { + if (repoToken == offers[i].repoToken) { + offers[i].isRepoTokenSeen = true; + } + } + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Count the number of nodes in the list + * @param listData The list data + * @return count The number of nodes in the list + */ function _count(TermAuctionListData storage listData) internal view returns (uint256 count) { if (listData.head == NULL_NODE) return 0; bytes32 current = listData.head; @@ -53,6 +124,14 @@ library TermAuctionList { } } + /** + * @notice Retrieves an array of offer IDs representing the pending offers + * @param listData The list data + * @return offers An array of offer IDs representing the pending offers + * + * @dev This function iterates through the list of offers and gathers their IDs into an array of `bytes32`. + * This makes it easier to process and manage the pending offers. + */ function pendingOffers(TermAuctionListData storage listData) internal view returns (bytes32[] memory offers) { uint256 count = _count(listData); if (count > 0) { @@ -66,6 +145,15 @@ library TermAuctionList { } } + /** + * @notice Inserts a new pending offer into the list data + * @param listData The list data + * @param offerId The ID of the offer to be inserted + * @param pendingOffer The `PendingOffer` struct containing details of the offer to be inserted + * + * @dev This function inserts a new pending offer at the beginning of the linked list in the `TermAuctionListData` structure. + * It updates the `next` pointers and the head of the list to ensure the new offer is correctly linked. + */ function insertPending(TermAuctionListData storage listData, bytes32 offerId, PendingOffer memory pendingOffer) internal { bytes32 current = listData.head; @@ -77,19 +165,26 @@ library TermAuctionList { listData.offers[offerId] = pendingOffer; } + /** + * @notice Removes completed or cancelled offers from the list data and processes the corresponding repoTokens + * @param listData The list data + * @param repoTokenListData The repoToken list data + * @param termController The term controller + * @param discountRateAdapter The discount rate adapter + * @param asset The address of the asset + * + * @dev This function iterates through the list of offers and removes those that are completed or cancelled. + * It processes the corresponding repoTokens by validating and inserting them if necessary. This helps maintain + * the list by clearing out inactive offers and ensuring repoTokens are correctly processed. + */ function removeCompleted( TermAuctionListData storage listData, RepoTokenListData storage repoTokenListData, ITermController termController, + ITermDiscountRateAdapter discountRateAdapter, address asset ) internal { - /* - offer submitted; auction still open => include offerAmount in totalValue (otherwise locked purchaseToken will be missing from TV) - offer submitted; auction completed; !auctionClosed() => include offer.offerAmount in totalValue (because the offerLocker will have already deleted offer on completeAuction) - + even though repoToken has been transferred it hasn't been added to the repoTokenList - BUT only if it is new not a reopening - offer submitted; auction completed; auctionClosed() => repoToken has been added to the repoTokenList - */ + // Return if the list is empty if (listData.head == NULL_NODE) return; bytes32 current = listData.head; @@ -103,27 +198,28 @@ library TermAuctionList { bool insertRepoToken; if (offer.termAuction.auctionCompleted()) { + // If auction is completed and closed, mark for removal and prepare to insert repo token removeNode = true; insertRepoToken = true; } else { if (offerAmount == 0) { - // auction canceled or deleted + // If offer amount is zero, it indicates the auction was canceled or deleted removeNode = true; } else { - // offer pending, do nothing + // Otherwise, do nothing if the offer is still pending } if (offer.termAuction.auctionCancelledForWithdrawal()) { - removeNode = true; - - // withdraw manually + // If auction was canceled for withdrawal, remove the node and unlock offers manually + removeNode = true; bytes32[] memory offerIds = new bytes32[](1); offerIds[0] = current; - offer.offerLocker.unlockOffers(offerIds); + offer.offerLocker.unlockOffers(offerIds); // unlocking offer in this scenario withdraws offer ammount } } if (removeNode) { + // Update the list to remove the current node if (current == listData.head) { listData.head = next; } @@ -134,61 +230,64 @@ library TermAuctionList { } if (insertRepoToken) { - repoTokenListData.validateAndInsertRepoToken(ITermRepoToken(offer.repoToken), termController, asset); + // Auction still open => include offerAmount in totalValue + // (otherwise locked purchaseToken will be missing from TV) + // Auction completed but not closed => include offer.offerAmount in totalValue + // because the offerLocker will have already removed the offer. + // This applies if the repoToken hasn't been added to the repoTokenList + // (only for new auctions, not reopenings). + repoTokenListData.validateAndInsertRepoToken( + ITermRepoToken(offer.repoToken), termController, discountRateAdapter, asset + ); } + // Move to the next node prev = current; current = next; } } - function _loadOffers(TermAuctionListData storage listData) private view returns (PendingOfferMemory[] memory offers) { - uint256 len = _count(listData); - offers = new PendingOfferMemory[](len); - - uint256 i; - bytes32 current = listData.head; - while (current != NULL_NODE) { - PendingOffer memory currentOffer = listData.offers[current]; - PendingOfferMemory memory newOffer = offers[i]; - - newOffer.offerId = current; - newOffer.repoToken = currentOffer.repoToken; - newOffer.offerAmount = currentOffer.offerAmount; - newOffer.termAuction = currentOffer.termAuction; - newOffer.offerLocker = currentOffer.offerLocker; - - i++; - current = _getNext(listData, current); - } - } - - function _markRepoTokenAsSeen(PendingOfferMemory[] memory offers, address repoToken) private view { - for (uint256 i; i < offers.length; i++) { - if (repoToken == offers[i].repoToken) { - offers[i].isRepoTokenSeen = true; - } - } - } - + /** + * @notice Calculates the total present value of all relevant offers related to a specified repoToken + * @param listData The list data + * @param repoTokenListData The repoToken list data + * @param discountRateAdapter The discount rate adapter + * @param purchaseTokenPrecision The precision of the purchase token + * @param repoTokenToMatch The address of the repoToken to match (optional) + * @return totalValue The total present value of the offers + * + * @dev This function calculates the present value of offers in the list. If `repoTokenToMatch` is provided, + * it will filter the calculations to include only the specified repoToken. If `repoTokenToMatch` is not provided, + * it will aggregate the present value of all repoTokens in the list. This provides flexibility for both aggregate + * and specific token evaluations. + */ function getPresentValue( TermAuctionListData storage listData, RepoTokenListData storage repoTokenListData, - ITermController termController, - uint256 purchaseTokenPrecision + ITermDiscountRateAdapter discountRateAdapter, + uint256 purchaseTokenPrecision, + address repoTokenToMatch ) internal view returns (uint256 totalValue) { + // Return 0 if the list is empty if (listData.head == NULL_NODE) return 0; + // Load pending offers PendingOfferMemory[] memory offers = _loadOffers(listData); for (uint256 i; i < offers.length; i++) { PendingOfferMemory memory offer = offers[i]; + // Filter by specific repo token if provided + if (repoTokenToMatch != address(0) && offer.repoToken != repoTokenToMatch) { + continue; + } + uint256 offerAmount = offer.offerLocker.lockedOffer(offer.offerId).amount; + // Handle new or unseen repo tokens /// @dev offer processed, but auctionClosed not yet called and auction is new so repoToken not on List and wont be picked up - /// checking repoTokenAuctionRates to make sure we are not double counting on re-openings - if (offer.termAuction.auctionCompleted() && repoTokenListData.auctionRates[offer.repoToken] == 0) { + /// checking repoTokendiscountRates to make sure we are not double counting on re-openings + if (offer.termAuction.auctionCompleted() && repoTokenListData.discountRates[offer.repoToken] == 0) { if (!offer.isRepoTokenSeen) { uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, @@ -199,19 +298,38 @@ library TermAuctionList { repoTokenAmountInBaseAssetPrecision, purchaseTokenPrecision, RepoTokenList.getRepoTokenMaturity(offer.repoToken), - RepoTokenList.getAuctionRate(termController, ITermRepoToken(offer.repoToken)) + discountRateAdapter.getDiscountRate(offer.repoToken) ); - // since multiple offers can be tied to the same repo token, we need to mark - // the repo tokens we've seen to avoid double counting + // Mark the repo token as seen to avoid double counting + // since multiple offers can be tied to the same repoToken, we need to mark + // the repoTokens we've seen to avoid double counting _markRepoTokenAsSeen(offers, offer.repoToken); } } else { + // Add the offer amount to the total value totalValue += offerAmount; } } } + /** + * @notice Get cumulative offer data for a specified repoToken + * @param listData The list data + * @param repoTokenListData The repoToken list data + * @param termController The term controller + * @param repoToken The address of the repoToken (optional) + * @param newOfferAmount The new offer amount for the specified repoToken + * @param purchaseTokenPrecision The precision of the purchase token + * @return cumulativeWeightedTimeToMaturity The cumulative weighted time to maturity + * @return cumulativeOfferAmount The cumulative repoToken amount + * @return found Whether the specified repoToken was found in the list + * + * @dev This function calculates cumulative data for all offers in the list. The `repoToken` and `newOfferAmount` + * parameters are optional and provide flexibility to include the newOfferAmount for a specified repoToken in the calculation. + * If `repoToken` is set to `address(0)` or `newOfferAmount` is `0`, the function calculates the cumulative data + * without adjustments. + */ function getCumulativeOfferData( TermAuctionListData storage listData, RepoTokenListData storage repoTokenListData, @@ -220,8 +338,10 @@ library TermAuctionList { uint256 newOfferAmount, uint256 purchaseTokenPrecision ) internal view returns (uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeOfferAmount, bool found) { + // If the list is empty, return 0s and false if (listData.head == NULL_NODE) return (0, 0, false); + // Load pending offers from the list data PendingOfferMemory[] memory offers = _loadOffers(listData); for (uint256 i; i < offers.length; i++) { @@ -232,12 +352,14 @@ library TermAuctionList { offerAmount = newOfferAmount; found = true; } else { + // Retrieve the current offer amount from the offer locker offerAmount = offer.offerLocker.lockedOffer(offer.offerId).amount; + // Handle new repo tokens or reopening auctions /// @dev offer processed, but auctionClosed not yet called and auction is new so repoToken not on List and wont be picked up - /// checking repoTokenAuctionRates to make sure we are not double counting on re-openings - if (offer.termAuction.auctionCompleted() && repoTokenListData.auctionRates[offer.repoToken] == 0) { - // use normalized repo token amount if repo token is not in the list + /// checking repoTokendiscountRates to make sure we are not double counting on re-openings + if (offer.termAuction.auctionCompleted() && repoTokenListData.discountRates[offer.repoToken] == 0) { + // use normalized repoToken amount if repoToken is not in the list if (!offer.isRepoTokenSeen) { offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, @@ -251,6 +373,7 @@ library TermAuctionList { } if (offerAmount > 0) { + // Calculate weighted time to maturity uint256 weightedTimeToMaturity = RepoTokenList.getRepoTokenWeightedTimeToMaturity( offer.repoToken, offerAmount ); @@ -260,4 +383,4 @@ library TermAuctionList { } } } -} +} \ No newline at end of file diff --git a/src/TermDiscountRateAdapter.sol b/src/TermDiscountRateAdapter.sol new file mode 100644 index 00000000..dcb95d31 --- /dev/null +++ b/src/TermDiscountRateAdapter.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.sol"; +import {ITermController, AuctionMetadata} from "./interfaces/term/ITermController.sol"; +import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; + +contract TermDiscountRateAdapter is ITermDiscountRateAdapter { + ITermController public immutable TERM_CONTROLLER; + + constructor(address termController_) { + TERM_CONTROLLER = ITermController(termController_); + } + + function getDiscountRate(address repoToken) external view returns (uint256) { + (AuctionMetadata[] memory auctionMetadata, ) = TERM_CONTROLLER.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId()); + + uint256 len = auctionMetadata.length; + require(len > 0); + + return auctionMetadata[len - 1].auctionClearingRate; + } +} diff --git a/src/TermVaultEventEmitter.sol b/src/TermVaultEventEmitter.sol index bb427eb6..b749408e 100644 --- a/src/TermVaultEventEmitter.sol +++ b/src/TermVaultEventEmitter.sol @@ -43,18 +43,37 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU emit TimeToMaturityThresholdUpdated(oldThreshold, newThreshold); } - function emitLiquidityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold) external onlyRole(VAULT_CONTRACT) { - emit LiquidityThresholdUpdated(oldThreshold, newThreshold); + function emitLiquidityReserveRatioUpdated(uint256 oldThreshold, uint256 newThreshold) external onlyRole(VAULT_CONTRACT) { + emit LiquidityReserveRatioUpdated(oldThreshold, newThreshold); } - function emitAuctionRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup) external onlyRole(VAULT_CONTRACT) { - emit AuctionRateMarkupUpdated(oldMarkup, newMarkup); + function emitDiscountRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup) external onlyRole(VAULT_CONTRACT) { + emit DiscountRateMarkupUpdated(oldMarkup, newMarkup); } function emitMinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio) external onlyRole(VAULT_CONTRACT) { emit MinCollateralRatioUpdated(collateral, minCollateralRatio); } + function emitRepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit) external onlyRole(VAULT_CONTRACT) { + emit RepoTokenConcentrationLimitUpdated(oldLimit, newLimit); + } + + function emitPaused() external onlyRole(VAULT_CONTRACT) { + emit Paused(); + } + + function emitUnpaused() external onlyRole(VAULT_CONTRACT) { + emit Unpaused(); + } + + function emitDiscountRateAdapterUpdated( + address oldAdapter, + address newAdapter + ) external onlyRole(VAULT_CONTRACT) { + emit DiscountRateAdapterUpdated(oldAdapter, newAdapter); + } + // ======================================================================== // = Admin =============================================================== // ======================================================================== @@ -65,4 +84,4 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU address ) internal view override onlyRole(DEVOPS_ROLE) {} // solhint-enable no-empty-blocks -} \ No newline at end of file +} diff --git a/src/interfaces/term/ITermDiscountRateAdapter.sol b/src/interfaces/term/ITermDiscountRateAdapter.sol new file mode 100644 index 00000000..97fe4019 --- /dev/null +++ b/src/interfaces/term/ITermDiscountRateAdapter.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +interface ITermDiscountRateAdapter { + function getDiscountRate(address repoToken) external view returns (uint256); +} diff --git a/src/interfaces/term/ITermVaultEvents.sol b/src/interfaces/term/ITermVaultEvents.sol index 181b0d61..0181b09b 100644 --- a/src/interfaces/term/ITermVaultEvents.sol +++ b/src/interfaces/term/ITermVaultEvents.sol @@ -6,19 +6,41 @@ interface ITermVaultEvents { event TimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold); - event LiquidityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold); + event LiquidityReserveRatioUpdated(uint256 oldThreshold, uint256 newThreshold); - event AuctionRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup); + event DiscountRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup); event MinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio); + event RepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit); + + event Paused(); + + event Unpaused(); + + event DiscountRateAdapterUpdated( + address indexed oldAdapter, + address indexed newAdapter + ); + function emitTermControllerUpdated(address oldController, address newController) external; function emitTimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold) external; - function emitLiquidityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold) external; + function emitLiquidityReserveRatioUpdated(uint256 oldThreshold, uint256 newThreshold) external; - function emitAuctionRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup) external; + function emitDiscountRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup) external; function emitMinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio) external; + + function emitRepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit) external; + + function emitPaused() external; + + function emitUnpaused() external; + + function emitDiscountRateAdapterUpdated( + address oldAdapter, + address newAdapter + ) external; } diff --git a/src/test/TestUSDCOffers.t.sol b/src/test/TestUSDCOffers.t.sol index 1a11bfe7..8b35261a 100644 --- a/src/test/TestUSDCOffers.t.sol +++ b/src/test/TestUSDCOffers.t.sol @@ -36,6 +36,7 @@ contract TestUSDCSubmitOffer is Setup { vm.startPrank(management); termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); termStrategy.setTimeToMaturityThreshold(3 weeks); + termStrategy.setRepoTokenConcentrationLimit(1e18); vm.stopPrank(); // start with some initial funds diff --git a/src/test/TestUSDCSellRepoToken.t.sol b/src/test/TestUSDCSellRepoToken.t.sol index c18635f4..898aa5cc 100644 --- a/src/test/TestUSDCSellRepoToken.t.sol +++ b/src/test/TestUSDCSellRepoToken.t.sol @@ -48,6 +48,7 @@ contract TestUSDCSellRepoToken is Setup { vm.startPrank(management); termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); termStrategy.setTimeToMaturityThreshold(10 weeks); + termStrategy.setRepoTokenConcentrationLimit(1e18); vm.stopPrank(); } @@ -161,6 +162,15 @@ contract TestUSDCSellRepoToken is Setup { _sellRepoTokens(tokens, amounts, true, true, err); } + function _sell1RepoTokenNoMintExpectRevert(MockTermRepoToken rt1, uint256 amount1, bytes memory err) private { + address[] memory tokens = new address[](1); + tokens[0] = address(rt1); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount1; + + _sellRepoTokens(tokens, amounts, true, false, err); + } + function _sell3RepoTokens( MockTermRepoToken rt1, uint256 amount1, @@ -321,25 +331,25 @@ contract TestUSDCSellRepoToken is Setup { assertEq(termStrategy.timeToMaturityThreshold(), 12345); vm.expectRevert("!management"); - termStrategy.setLiquidityThreshold(12345); + termStrategy.setLiquidityReserveRatio(12345); vm.prank(management); - termStrategy.setLiquidityThreshold(12345); - assertEq(termStrategy.liquidityThreshold(), 12345); + termStrategy.setLiquidityReserveRatio(12345); + assertEq(termStrategy.liquidityReserveRatio(), 12345); vm.expectRevert("!management"); - termStrategy.setAuctionRateMarkup(12345); + termStrategy.setdiscountRateMarkup(12345); vm.prank(management); - termStrategy.setAuctionRateMarkup(12345); - assertEq(termStrategy.auctionRateMarkup(), 12345); + termStrategy.setdiscountRateMarkup(12345); + assertEq(termStrategy.discountRateMarkup(), 12345); vm.expectRevert("!management"); termStrategy.setCollateralTokenParams(address(mockCollateral), 12345); vm.prank(management); termStrategy.setCollateralTokenParams(address(mockCollateral), 12345); - assertEq(termStrategy.auctionRateMarkup(), 12345); + assertEq(termStrategy.discountRateMarkup(), 12345); } function testRepoTokenValidationFailures() public { @@ -353,7 +363,7 @@ contract TestUSDCSellRepoToken is Setup { repoTokenMatured.mint(testUser, 1000e18); // test: token has no auction clearing rate - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(repoToken1Week))); + vm.expectRevert(); vm.prank(testUser); termStrategy.sellRepoToken(address(repoToken1Week), 1e18); @@ -509,4 +519,54 @@ contract TestUSDCSellRepoToken is Setup { console.log("totalAssetValue", termStrategy.totalAssetValue()); } + + function testConcentrationLimitFailure() public { + address testDepositor = vm.addr(0x111111); + uint256 depositAmount = 1000e6; + + mockUSDC.mint(testDepositor, depositAmount); + + vm.startPrank(testDepositor); + mockUSDC.approve(address(termStrategy), type(uint256).max); + IERC4626(address(termStrategy)).deposit(depositAmount, testDepositor); + vm.stopPrank(); + + vm.expectRevert("!management"); + termStrategy.setRepoTokenConcentrationLimit(0.4e18); + + // Set to 40% + vm.prank(management); + termStrategy.setRepoTokenConcentrationLimit(0.4e18); + + _sell1RepoTokenNoMintExpectRevert( + repoToken2Week, + 500e18, + abi.encodeWithSelector(Strategy.RepoTokenConcentrationTooHigh.selector, address(repoToken2Week)) + ); + } + + function testPausing() public { + address testDepositor = vm.addr(0x111111); + uint256 depositAmount = 1000e6; + + mockUSDC.mint(testDepositor, depositAmount); + + vm.expectRevert("!management"); + termStrategy.pause(); + + vm.prank(management); + termStrategy.pause(); + + vm.startPrank(testDepositor); + mockUSDC.approve(address(termStrategy), type(uint256).max); + vm.expectRevert("Pausable: paused"); + IERC4626(address(termStrategy)).deposit(depositAmount, testDepositor); + vm.stopPrank(); + + _sell1RepoTokenExpectRevert( + repoToken2Week, + 2e18, + "Pausable: paused" + ); + } } diff --git a/src/test/TestUSDCSubmitOffer.t.sol b/src/test/TestUSDCSubmitOffer.t.sol new file mode 100644 index 00000000..26d4c70d --- /dev/null +++ b/src/test/TestUSDCSubmitOffer.t.sol @@ -0,0 +1,288 @@ +pragma solidity ^0.8.18; + +import "forge-std/console2.sol"; +import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import {MockTermRepoToken} from "./mocks/MockTermRepoToken.sol"; +import {MockTermAuction} from "./mocks/MockTermAuction.sol"; +import {MockUSDC} from "./mocks/MockUSDC.sol"; +import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol"; +import {Strategy} from "../Strategy.sol"; + +contract TestUSDCSubmitOffer is Setup { + uint256 internal constant TEST_REPO_TOKEN_RATE = 0.05e18; + + MockUSDC internal mockUSDC; + ERC20Mock internal mockCollateral; + MockTermRepoToken internal repoToken1Week; + MockTermAuction internal repoToken1WeekAuction; + Strategy internal termStrategy; + StrategySnapshot internal initialState; + + function setUp() public override { + mockUSDC = new MockUSDC(); + mockCollateral = new ERC20Mock(); + + _setUp(ERC20(address(mockUSDC))); + + repoToken1Week = new MockTermRepoToken( + bytes32("test repo token 1"), address(mockUSDC), address(mockCollateral), 1e18, 1 weeks + ); + termController.setOracleRate(MockTermRepoToken(repoToken1Week).termRepoId(), TEST_REPO_TOKEN_RATE); + + termStrategy = Strategy(address(strategy)); + + repoToken1WeekAuction = new MockTermAuction(repoToken1Week); + + vm.startPrank(management); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); + termStrategy.setTimeToMaturityThreshold(3 weeks); + termStrategy.setRepoTokenConcentrationLimit(1e18); + vm.stopPrank(); + + // start with some initial funds + mockUSDC.mint(address(strategy), 100e6); + + initialState.totalAssetValue = termStrategy.totalAssetValue(); + initialState.totalLiquidBalance = termStrategy.totalLiquidBalance(); + } + + function _submitOffer(bytes32 idHash, uint256 offerAmount) private returns (bytes32) { + // test: only management can submit offers + vm.expectRevert("!management"); + bytes32[] memory offerIds = termStrategy.submitAuctionOffer( + address(repoToken1WeekAuction), address(repoToken1Week), idHash, bytes32("test price"), offerAmount + ); + + vm.prank(management); + offerIds = termStrategy.submitAuctionOffer( + address(repoToken1WeekAuction), address(repoToken1Week), idHash, bytes32("test price"), offerAmount + ); + + assertEq(offerIds.length, 1); + + return offerIds[0]; + } + + function testSubmitOffer() public { + _submitOffer(bytes32("offer id hash 1"), 1e6); + + assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - 1e6); + // test: totalAssetValue = total liquid balance + pending offer amount + assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + 1e6); + } + + function testEditOffer() public { + bytes32 idHash1 = bytes32("offer id hash 1"); + bytes32 offerId1 = _submitOffer(idHash1, 1e6); + + // TODO: fuzz this + uint256 offerAmount = 4e6; + + vm.prank(management); + bytes32[] memory offerIds = termStrategy.submitAuctionOffer( + address(repoToken1WeekAuction), address(repoToken1Week), idHash1, bytes32("test price"), offerAmount + ); + + assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - offerAmount); + // test: totalAssetValue = total liquid balance + pending offer amount + assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + offerAmount); + } + + function testDeleteOffers() public { + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); + + bytes32[] memory offerIds = new bytes32[](1); + + offerIds[0] = offerId1; + + vm.expectRevert("!management"); + termStrategy.deleteAuctionOffers(address(repoToken1WeekAuction), offerIds); + + vm.prank(management); + termStrategy.deleteAuctionOffers(address(repoToken1WeekAuction), offerIds); + + assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance); + assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance()); + } + + uint256 public constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; + uint256 public constant RATE_PRECISION = 1e18; + + function _getRepoTokenAmountGivenPurchaseTokenAmount( + uint256 purchaseTokenAmount, + MockTermRepoToken termRepoToken, + uint256 discountRate + ) private view returns (uint256) { + (uint256 redemptionTimestamp, address purchaseToken, ,) = termRepoToken.config(); + + uint256 purchaseTokenPrecision = 10**ERC20(purchaseToken).decimals(); + uint256 repoTokenPrecision = 10**ERC20(address(termRepoToken)).decimals(); + + uint256 timeLeftToMaturityDayFraction = + ((redemptionTimestamp - block.timestamp) * purchaseTokenPrecision) / THREESIXTY_DAYCOUNT_SECONDS; + + // purchaseTokenAmount * (1 + r * days / 360) = repoTokenAmountInBaseAssetPrecision + uint256 repoTokenAmountInBaseAssetPrecision = + purchaseTokenAmount * (purchaseTokenPrecision + (discountRate * timeLeftToMaturityDayFraction / RATE_PRECISION)) / purchaseTokenPrecision; + + return repoTokenAmountInBaseAssetPrecision * repoTokenPrecision / purchaseTokenPrecision; + } + + function testCompleteAuctionSuccessFull() public { + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); + uint256 fillAmount = 1e6; + + bytes32[] memory offerIds = new bytes32[](1); + offerIds[0] = offerId1; + uint256[] memory fillAmounts = new uint256[](1); + fillAmounts[0] = fillAmount; + uint256[] memory repoTokenAmounts = new uint256[](1); + repoTokenAmounts[0] = _getRepoTokenAmountGivenPurchaseTokenAmount( + fillAmount, repoToken1Week, TEST_REPO_TOKEN_RATE + ); + + repoToken1WeekAuction.auctionSuccess(offerIds, fillAmounts, repoTokenAmounts); + + //console2.log("repoTokenAmounts[0]", repoTokenAmounts[0]); + + // test: asset value should equal to initial asset value (liquid + pending offers) + assertEq(termStrategy.totalAssetValue(), initialState.totalAssetValue); + + address[] memory holdings = termStrategy.repoTokenHoldings(); + + // test: 0 holding because auctionClosed not yet called + assertEq(holdings.length, 0); + + termStrategy.auctionClosed(); + + // test: asset value should equal to initial asset value (liquid + repo tokens) + assertEq(termStrategy.totalAssetValue(), initialState.totalAssetValue); + + holdings = termStrategy.repoTokenHoldings(); + + // test: check repo token holdings + assertEq(holdings.length, 1); + assertEq(holdings[0], address(repoToken1Week)); + + bytes32[] memory offers = termStrategy.pendingOffers(); + + assertEq(offers.length, 0); + } + + function testCompleteAuctionSuccessPartial() public { + bytes32 offerId1 = _submitOffer(bytes32("offer id 1"), 1e6); + uint256 fillAmount = 0.5e6; + + bytes32[] memory offerIds = new bytes32[](1); + offerIds[0] = offerId1; + uint256[] memory fillAmounts = new uint256[](1); + + // test: 50% filled + fillAmounts[0] = fillAmount; + uint256[] memory repoTokenAmounts = new uint256[](1); + repoTokenAmounts[0] = _getRepoTokenAmountGivenPurchaseTokenAmount( + fillAmount, repoToken1Week, TEST_REPO_TOKEN_RATE + ); + + repoToken1WeekAuction.auctionSuccess(offerIds, fillAmounts, repoTokenAmounts); + + // test: asset value should equal to initial asset value (liquid + pending offers) + assertEq(termStrategy.totalAssetValue(), initialState.totalAssetValue); + + address[] memory holdings = termStrategy.repoTokenHoldings(); + + // test: 0 holding because auctionClosed not yet called + assertEq(holdings.length, 0); + + termStrategy.auctionClosed(); + + // test: asset value should equal to initial asset value (liquid + repo tokens) + assertEq(termStrategy.totalAssetValue(), initialState.totalAssetValue); + + holdings = termStrategy.repoTokenHoldings(); + + // test: check repo token holdings + assertEq(holdings.length, 1); + assertEq(holdings[0], address(repoToken1Week)); + + bytes32[] memory offers = termStrategy.pendingOffers(); + + assertEq(offers.length, 0); + } + + function testCompleteAuctionCanceled() public { + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); + + repoToken1WeekAuction.auctionCanceled(); + + // test: check value before calling complete auction + termStrategy.auctionClosed(); + + bytes32[] memory offers = termStrategy.pendingOffers(); + + assertEq(offers.length, 0); + } + + function testMultipleOffers() public { + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); + bytes32 offerId2 = _submitOffer(bytes32("offer id hash 2"), 5e6); + + assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - 6e6); + // test: totalAssetValue = total liquid balance + pending offer amount + assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + 6e6); + + bytes32[] memory offers = termStrategy.pendingOffers(); + + assertEq(offers.length, 2); + assertEq(offers[0], offerId2); + assertEq(offers[1], offerId1); + } + + function testMultipleOffersFillAndNoFill() public { + uint256 offer1Amount = 1e6; + uint256 offer2Amount = 5e6; + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), offer1Amount); + bytes32 offerId2 = _submitOffer(bytes32("offer id hash 2"), offer2Amount); + + bytes32[] memory offerIds = new bytes32[](2); + offerIds[0] = offerId1; + offerIds[1] = offerId2; + uint256[] memory fillAmounts = new uint256[](2); + + // test: offer 1 filled, offer 2 not filled + fillAmounts[0] = offer1Amount; + fillAmounts[1] = 0; + uint256[] memory repoTokenAmounts = new uint256[](2); + repoTokenAmounts[0] = _getRepoTokenAmountGivenPurchaseTokenAmount( + offer1Amount, repoToken1Week, TEST_REPO_TOKEN_RATE + ); + repoTokenAmounts[1] = 0; + + repoToken1WeekAuction.auctionSuccess(offerIds, fillAmounts, repoTokenAmounts); + + // test: asset value should equal to initial asset value (liquid + pending offers) + assertEq(termStrategy.totalAssetValue(), initialState.totalAssetValue); + } + + function testEditOfferTotalGreaterThanCurrentLiquidity() public { + bytes32 idHash1 = bytes32("offer id hash 1"); + bytes32 offerId1 = _submitOffer(idHash1, 50e6); + + assertEq(termStrategy.totalLiquidBalance(), 50e6); + + _submitOffer(idHash1, 100e6); + + assertEq(termStrategy.totalLiquidBalance(), 0); + } + + function testEditOfferTotalLessThanCurrentLiquidity() public { + bytes32 idHash1 = bytes32("offer id hash 1"); + bytes32 offerId1 = _submitOffer(idHash1, 100e6); + + assertEq(termStrategy.totalLiquidBalance(), 0); + + _submitOffer(idHash1, 50e6); + + assertEq(termStrategy.totalLiquidBalance(), 50e6); + } +} diff --git a/src/test/utils/Setup.sol b/src/test/utils/Setup.sol index ac4f8a75..a2dc0fe4 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -5,6 +5,7 @@ import "forge-std/console2.sol"; import {ExtendedTest} from "./ExtendedTest.sol"; import {Strategy, ERC20} from "../../Strategy.sol"; +import {TermDiscountRateAdapter} from "../../TermDiscountRateAdapter.sol"; import {IStrategyInterface} from "../../interfaces/IStrategyInterface.sol"; // Inherit the events so they can be checked if desired. @@ -68,6 +69,7 @@ contract Setup is ExtendedTest, IEvents { // Term finance mocks MockTermController internal termController; + TermDiscountRateAdapter internal discountRateAdapter; TermVaultEventEmitter internal termVaultEventEmitterImpl; TermVaultEventEmitter internal termVaultEventEmitter; ERC4626Mock internal mockYearnVault; @@ -90,9 +92,10 @@ contract Setup is ExtendedTest, IEvents { // Factory from mainnet, tokenized strategy needs to be hardcoded to 0xBB51273D6c746910C7C06fe718f30c936170feD0 tokenizedStrategy = new TokenizedStrategy(address(mockFactory)); - vm.etch(0x2e234DAe75C793f67A35089C9d99245E1C58470b, address(tokenizedStrategy).code); + vm.etch(0xBB51273D6c746910C7C06fe718f30c936170feD0, address(tokenizedStrategy).code); termController = new MockTermController(); + discountRateAdapter = new TermDiscountRateAdapter(address(termController)); termVaultEventEmitterImpl = new TermVaultEventEmitter(); termVaultEventEmitter = TermVaultEventEmitter(address(new ERC1967Proxy(address(termVaultEventEmitterImpl), ""))); mockYearnVault = new ERC4626Mock(address(asset)); @@ -102,7 +105,7 @@ contract Setup is ExtendedTest, IEvents { // Deploy strategy and set variables strategy = IStrategyInterface(setUpStrategy()); - factory = strategy.FACTORY(); +// factory = strategy.FACTORY(); // label all the used addresses for traces vm.label(keeper, "keeper"); @@ -116,7 +119,15 @@ contract Setup is ExtendedTest, IEvents { function setUpStrategy() public returns (address) { // we save the strategy as a IStrategyInterface to give it the needed interface IStrategyInterface _strategy = IStrategyInterface( - address(new Strategy(address(asset), "Tokenized Strategy", address(mockYearnVault), address(termVaultEventEmitter))) + address( + new Strategy( + address(asset), + "Tokenized Strategy", + address(mockYearnVault), + address(discountRateAdapter), + address(termVaultEventEmitter) + ) + ) ); vm.prank(adminWallet);