From 801f4390789d60f9417665153b183c161fde937e Mon Sep 17 00:00:00 2001 From: 0xddong Date: Tue, 6 Aug 2024 20:24:55 -0700 Subject: [PATCH] fixing tests --- src/RepoTokenList.sol | 256 ++++++++++++++++++----- src/RepoTokenUtils.sol | 48 ++++- src/TermAuctionList.sol | 215 ++++++++++++++----- src/TermVaultEventEmitter.sol | 4 +- src/interfaces/term/ITermVaultEvents.sol | 2 +- 5 files changed, 410 insertions(+), 115 deletions(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index f5336986..9075f683 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.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; @@ -118,7 +260,6 @@ library RepoTokenList { address prev = current; while (current != NULL_NODE) { address next; - if (getRepoTokenMaturity(current) < block.timestamp) { bool removeMaturedToken; uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); @@ -147,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 @@ -157,32 +298,47 @@ library RepoTokenList { prev = current; current = next; } - } - + } + + /** + * @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]; @@ -197,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 = 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 = discountRateAdapter.getDiscountRate(address(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; @@ -240,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; } @@ -248,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; @@ -258,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; @@ -269,41 +448,4 @@ library RepoTokenList { current = next; } } - - function getPresentValue( - RepoTokenListData storage listData, - uint256 purchaseTokenPrecision, - address repoTokenToMatch - ) 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; - } - - if (repoTokenToMatch != address(0) && current == repoTokenToMatch) { - // matching a specific repo token and terminate early because the list is sorted - // with no duplicates - break; - } - - 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/TermAuctionList.sol b/src/TermAuctionList.sol index 9e579d3d..aa7d1f25 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -10,6 +10,7 @@ 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; @@ -17,6 +18,7 @@ struct PendingOffer { ITermAuctionOfferLocker offerLocker; } +// In-memory representation of an offer object struct PendingOfferMemory { bytes32 offerId; address repoToken; @@ -36,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; @@ -54,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) { @@ -67,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; @@ -78,6 +165,18 @@ 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, @@ -85,13 +184,7 @@ library TermAuctionList { 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; @@ -105,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; } @@ -136,45 +230,37 @@ library TermAuctionList { } if (insertRepoToken) { + // 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, @@ -182,23 +268,26 @@ library TermAuctionList { 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 repoTokenToMatch if necessary + // 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, @@ -212,16 +301,35 @@ library TermAuctionList { 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, @@ -230,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++) { @@ -242,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, @@ -261,6 +373,7 @@ library TermAuctionList { } if (offerAmount > 0) { + // Calculate weighted time to maturity uint256 weightedTimeToMaturity = RepoTokenList.getRepoTokenWeightedTimeToMaturity( offer.repoToken, offerAmount ); @@ -270,4 +383,4 @@ library TermAuctionList { } } } -} +} \ No newline at end of file diff --git a/src/TermVaultEventEmitter.sol b/src/TermVaultEventEmitter.sol index 90d65043..b749408e 100644 --- a/src/TermVaultEventEmitter.sol +++ b/src/TermVaultEventEmitter.sol @@ -48,7 +48,7 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU } function emitDiscountRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup) external onlyRole(VAULT_CONTRACT) { - emit AuctionRateMarkupUpdated(oldMarkup, newMarkup); + emit DiscountRateMarkupUpdated(oldMarkup, newMarkup); } function emitMinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio) external onlyRole(VAULT_CONTRACT) { @@ -84,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/ITermVaultEvents.sol b/src/interfaces/term/ITermVaultEvents.sol index 6bb4349a..0181b09b 100644 --- a/src/interfaces/term/ITermVaultEvents.sol +++ b/src/interfaces/term/ITermVaultEvents.sol @@ -8,7 +8,7 @@ interface ITermVaultEvents { event LiquidityReserveRatioUpdated(uint256 oldThreshold, uint256 newThreshold); - event AuctionRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup); + event DiscountRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup); event MinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio);