diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index e5d56354..81690607 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -1,449 +1,522 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {ITermController} from "./interfaces/term/ITermController.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 {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {RepoTokenUtils} from "./RepoTokenUtils.sol"; - -struct RepoTokenListNode { - address next; -} - -struct RepoTokenListData { - address head; - mapping(address => RepoTokenListNode) nodes; - mapping(address => uint256) discountRates; - /// @notice keyed by collateral token - mapping(address => uint256) collateralTokenParams; -} - -/*////////////////////////////////////////////////////////////// - LIBRARY: RepoTokenList -//////////////////////////////////////////////////////////////*/ - -library RepoTokenList { - address internal constant NULL_NODE = address(0); - uint256 internal constant INVALID_AUCTION_RATE = 0; - uint256 internal constant ZERO_AUCTION_RATE = 1; //Set to lowest nonzero number so that it is not confused with INVALID_AUCTION_RATe but still calculates as if 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(); - } - - /** - * @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; - while (current != NULL_NODE) { - count++; - current = _getNext(listData, current); - } - } - - /** - * @notice Returns an array of addresses representing the repoTokens currently held in the list data - * @param listData The list data - * @return holdingsArray 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 holdingsArray) { - uint256 count = _count(listData); - if (count > 0) { - holdingsArray = new address[](count); - uint256 i; - address current = listData.head; - while (current != NULL_NODE) { - holdingsArray[i++] = current; - current = _getNext(listData, current); - } - } - } - - /** - * @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) { - uint256 currentMaturity = getRepoTokenMaturity(repoToken); - - if (currentMaturity > block.timestamp) { - uint256 timeToMaturity = _getRepoTokenTimeToMaturity(currentMaturity); - // Not matured yet - weightedTimeToMaturity = timeToMaturity * repoTokenBalanceInBaseAssetPrecision; - } - } - - /** - * @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 discountRateAdapter The discount rate adapter - * @param repoToken The address of the repoToken (optional) - * @param repoTokenAmount The amount of the repoToken (optional) - * @param purchaseTokenPrecision The precision of the purchase token - * @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, - ITermDiscountRateAdapter discountRateAdapter, - address repoToken, - uint256 repoTokenAmount, - uint256 purchaseTokenPrecision - ) 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 repoTokenBalanceInBaseAssetPrecision = - RepoTokenUtils.getNormalizedRepoTokenAmount( - current, repoTokenBalance, purchaseTokenPrecision, discountRateAdapter.repoRedemptionHaircut(current) - ); - - // 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 discountRateAdapter The discount rate adapter - * @param purchaseTokenPrecision The precision of the purchase token - * @return totalPresentValue The total present value of the repoTokens - * @dev Aggregates the present value of all repoTokens in the list. - */ - function getPresentValue( - RepoTokenListData storage listData, - ITermDiscountRateAdapter discountRateAdapter, - uint256 purchaseTokenPrecision - ) 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 discountRate = discountRateAdapter.getDiscountRate(current); - - // Convert repo token balance to base asset precision - // (ratePrecision * repoPrecision * purchasePrecision) / (repoPrecision * ratePrecision) = purchasePrecision - uint256 repoTokenBalanceInBaseAssetPrecision = - RepoTokenUtils.getNormalizedRepoTokenAmount( - current, repoTokenBalance, purchaseTokenPrecision, discountRateAdapter.repoRedemptionHaircut(current) - ); - - // Calculate present value based on maturity - if (currentMaturity > block.timestamp) { - totalPresentValue += RepoTokenUtils.calculatePresentValue( - repoTokenBalanceInBaseAssetPrecision, purchaseTokenPrecision, currentMaturity, discountRate - ); - } else { - totalPresentValue += repoTokenBalanceInBaseAssetPrecision; - } - - // 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 - * @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) 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; - - address current = listData.head; - address prev = current; - while (current != NULL_NODE) { - address next; - if (getRepoTokenMaturity(current) <= block.timestamp) { - bool removeMaturedToken; - uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - - if (repoTokenBalance > 0) { - (, , address termRepoServicer,) = ITermRepoToken(current).config(); - try ITermRepoServicer(termRepoServicer).redeemTermRepoTokens( - address(this), - repoTokenBalance - ) { - removeMaturedToken = true; - } catch { - // redemption failed, do not remove token from the list - } - } else { - // already redeemed - removeMaturedToken = true; - } - - next = _getNext(listData, current); - - if (removeMaturedToken) { - if (current == listData.head) { - listData.head = next; - } - - listData.nodes[prev].next = next; - delete listData.nodes[current]; - delete listData.discountRates[current]; - } - } else { - /// @dev early exit because list is sorted - break; - } - - prev = current; - current = next; - } - } - - /** - * @notice Validates a repoToken against specific criteria - * @param listData The list data - * @param repoToken The repoToken to validate - * @param asset The address of the base asset - * @return isRepoTokenValid Whether the repoToken is valid - * @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, - address asset - ) internal view returns (bool isRepoTokenValid, uint256 redemptionTimestamp) { - // Retrieve repo token configuration - address purchaseToken; - address collateralManager; - (redemptionTimestamp, purchaseToken, , collateralManager) = repoToken.config(); - - // Validate purchase token - if (purchaseToken != asset) { - return (false, redemptionTimestamp); - } - - // Check if repo token has matured - if (redemptionTimestamp < block.timestamp) { - return (false, redemptionTimestamp); - } - - // 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]; - - if (minCollateralRatio == 0) { - return (false, redemptionTimestamp); - } else if ( - ITermRepoCollateralManager(collateralManager).maintenanceCollateralRatios(currentToken) < minCollateralRatio - ) { - return (false, redemptionTimestamp); - } - } - return (true, redemptionTimestamp); - } - - /** - * @notice Validate and insert a repoToken into the list data - * @param listData The list data - * @param repoToken The repoToken to validate and insert - * @param discountRateAdapter The discount rate adapter - * @param asset The address of the base asset - * @return validRepoToken Whether the repoToken is valid - * @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, - ITermDiscountRateAdapter discountRateAdapter, - address asset - ) internal returns (bool validRepoToken, uint256 discountRate, uint256 redemptionTimestamp) { - discountRate = listData.discountRates[address(repoToken)]; - if (discountRate != INVALID_AUCTION_RATE) { - (redemptionTimestamp, , ,) = repoToken.config(); - - // skip matured repoTokens - if (redemptionTimestamp < block.timestamp) { - return (false, discountRate, redemptionTimestamp); //revert InvalidRepoToken(address(repoToken)); - } - - uint256 oracleRate; - try discountRateAdapter.getDiscountRate(address(repoToken)) returns (uint256 rate) { - oracleRate = rate; - } catch { - } - - if (oracleRate != 0) { - if (discountRate != oracleRate) { - listData.discountRates[address(repoToken)] = oracleRate; - } - } - } else { - try discountRateAdapter.getDiscountRate(address(repoToken)) returns (uint256 rate) { - discountRate = rate == 0 ? ZERO_AUCTION_RATE : rate; - } catch { - discountRate = INVALID_AUCTION_RATE; - return (false, discountRate, redemptionTimestamp); - } - - bool isRepoTokenValid; - - (isRepoTokenValid, redemptionTimestamp) = validateRepoToken(listData, repoToken, asset); - if (!isRepoTokenValid) { - return (false, discountRate, redemptionTimestamp); - } - insertSorted(listData, address(repoToken)); - listData.discountRates[address(repoToken)] = discountRate; - } - - return (true, discountRate, redemptionTimestamp); - } - - /** - * @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; - listData.nodes[repoToken].next = NULL_NODE; - return; - } - - uint256 maturityToInsert = getRepoTokenMaturity(repoToken); - - address prev; - while (current != NULL_NODE) { - - // If the repoToken is already in the list, exit - if (current == repoToken) { - break; - } - - uint256 currentMaturity = getRepoTokenMaturity(current); - - // Insert repoToken before current if its maturity is less than current maturity - if (maturityToInsert < currentMaturity) { - if (prev == NULL_NODE) { - listData.head = repoToken; - } else { - listData.nodes[prev].next = repoToken; - } - listData.nodes[repoToken].next = current; - 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; - listData.nodes[repoToken].next = NULL_NODE; - break; - } - - prev = current; - current = next; - } - } -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ITermController} from "./interfaces/term/ITermController.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 {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {RepoTokenUtils} from "./RepoTokenUtils.sol"; + +struct RepoTokenListNode { + address next; +} + +struct RepoTokenListData { + address head; + mapping(address => RepoTokenListNode) nodes; + mapping(address => uint256) discountRates; + /// @notice keyed by collateral token + mapping(address => uint256) collateralTokenParams; +} + +/*////////////////////////////////////////////////////////////// + LIBRARY: RepoTokenList +//////////////////////////////////////////////////////////////*/ + +library RepoTokenList { + address internal constant NULL_NODE = address(0); + uint256 internal constant INVALID_AUCTION_RATE = 0; + uint256 internal constant ZERO_AUCTION_RATE = 1; //Set to lowest nonzero number so that it is not confused with INVALID_AUCTION_RATe but still calculates as if 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(); + } + + /** + * @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; + while (current != NULL_NODE) { + count++; + current = _getNext(listData, current); + } + } + + /** + * @notice Returns an array of addresses representing the repoTokens currently held in the list data + * @param listData The list data + * @return holdingsArray 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 holdingsArray) { + uint256 count = _count(listData); + if (count > 0) { + holdingsArray = new address[](count); + uint256 i; + address current = listData.head; + while (current != NULL_NODE) { + holdingsArray[i++] = current; + current = _getNext(listData, current); + } + } + } + + /** + * @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) { + uint256 currentMaturity = getRepoTokenMaturity(repoToken); + + if (currentMaturity > block.timestamp) { + uint256 timeToMaturity = _getRepoTokenTimeToMaturity( + currentMaturity + ); + // Not matured yet + weightedTimeToMaturity = + timeToMaturity * + repoTokenBalanceInBaseAssetPrecision; + } + } + + /** + * @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 discountRateAdapter The discount rate adapter + * @param repoToken The address of the repoToken (optional) + * @param repoTokenAmount The amount of the repoToken (optional) + * @param purchaseTokenPrecision The precision of the purchase token + * @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, + ITermDiscountRateAdapter discountRateAdapter, + address repoToken, + uint256 repoTokenAmount, + uint256 purchaseTokenPrecision + ) + 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 repoTokenBalanceInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + current, + repoTokenBalance, + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(current) + ); + + // 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 discountRateAdapter The discount rate adapter + * @param purchaseTokenPrecision The precision of the purchase token + * @return totalPresentValue The total present value of the repoTokens + * @dev Aggregates the present value of all repoTokens in the list. + */ + function getPresentValue( + RepoTokenListData storage listData, + ITermDiscountRateAdapter discountRateAdapter, + uint256 purchaseTokenPrecision + ) 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 discountRate = discountRateAdapter.getDiscountRate(current); + + // Convert repo token balance to base asset precision + // (ratePrecision * repoPrecision * purchasePrecision) / (repoPrecision * ratePrecision) = purchasePrecision + uint256 repoTokenBalanceInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + current, + repoTokenBalance, + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(current) + ); + + // Calculate present value based on maturity + if (currentMaturity > block.timestamp) { + totalPresentValue += RepoTokenUtils.calculatePresentValue( + repoTokenBalanceInBaseAssetPrecision, + purchaseTokenPrecision, + currentMaturity, + discountRate + ); + } else { + totalPresentValue += repoTokenBalanceInBaseAssetPrecision; + } + + // 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 + * @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 + ) 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; + + address current = listData.head; + address prev = current; + while (current != NULL_NODE) { + address next; + if (getRepoTokenMaturity(current) <= block.timestamp) { + bool removeMaturedToken; + uint256 repoTokenBalance = ITermRepoToken(current).balanceOf( + address(this) + ); + + if (repoTokenBalance > 0) { + (, , address termRepoServicer, ) = ITermRepoToken(current) + .config(); + try + ITermRepoServicer(termRepoServicer) + .redeemTermRepoTokens( + address(this), + repoTokenBalance + ) + { + removeMaturedToken = true; + } catch { + // redemption failed, do not remove token from the list + } + } else { + // already redeemed + removeMaturedToken = true; + } + + next = _getNext(listData, current); + + if (removeMaturedToken) { + if (current == listData.head) { + listData.head = next; + } + + listData.nodes[prev].next = next; + delete listData.nodes[current]; + delete listData.discountRates[current]; + } + } else { + /// @dev early exit because list is sorted + break; + } + + prev = current; + current = next; + } + } + + /** + * @notice Validates a repoToken against specific criteria + * @param listData The list data + * @param repoToken The repoToken to validate + * @param asset The address of the base asset + * @return isRepoTokenValid Whether the repoToken is valid + * @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, + address asset + ) + internal + view + returns (bool isRepoTokenValid, uint256 redemptionTimestamp) + { + // Retrieve repo token configuration + address purchaseToken; + address collateralManager; + (redemptionTimestamp, purchaseToken, , collateralManager) = repoToken + .config(); + + // Validate purchase token + if (purchaseToken != asset) { + return (false, redemptionTimestamp); + } + + // Check if repo token has matured + if (redemptionTimestamp < block.timestamp) { + return (false, redemptionTimestamp); + } + + // 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 + ]; + + if (minCollateralRatio == 0) { + return (false, redemptionTimestamp); + } else if ( + ITermRepoCollateralManager(collateralManager) + .maintenanceCollateralRatios(currentToken) < + minCollateralRatio + ) { + return (false, redemptionTimestamp); + } + } + return (true, redemptionTimestamp); + } + + /** + * @notice Validate and insert a repoToken into the list data + * @param listData The list data + * @param repoToken The repoToken to validate and insert + * @param discountRateAdapter The discount rate adapter + * @param asset The address of the base asset + * @return validRepoToken Whether the repoToken is valid + * @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, + ITermDiscountRateAdapter discountRateAdapter, + address asset + ) + internal + returns ( + bool validRepoToken, + uint256 discountRate, + uint256 redemptionTimestamp + ) + { + discountRate = listData.discountRates[address(repoToken)]; + if (discountRate != INVALID_AUCTION_RATE) { + (redemptionTimestamp, , , ) = repoToken.config(); + + // skip matured repoTokens + if (redemptionTimestamp < block.timestamp) { + return (false, discountRate, redemptionTimestamp); //revert InvalidRepoToken(address(repoToken)); + } + + uint256 oracleRate; + try + discountRateAdapter.getDiscountRate(address(repoToken)) + returns (uint256 rate) { + oracleRate = rate; + } catch {} + + if (oracleRate != 0) { + if (discountRate != oracleRate) { + listData.discountRates[address(repoToken)] = oracleRate; + } + } + } else { + try + discountRateAdapter.getDiscountRate(address(repoToken)) + returns (uint256 rate) { + discountRate = rate == 0 ? ZERO_AUCTION_RATE : rate; + } catch { + discountRate = INVALID_AUCTION_RATE; + return (false, discountRate, redemptionTimestamp); + } + + bool isRepoTokenValid; + + (isRepoTokenValid, redemptionTimestamp) = validateRepoToken( + listData, + repoToken, + asset + ); + if (!isRepoTokenValid) { + return (false, discountRate, redemptionTimestamp); + } + insertSorted(listData, address(repoToken)); + listData.discountRates[address(repoToken)] = discountRate; + } + + return (true, discountRate, redemptionTimestamp); + } + + /** + * @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; + listData.nodes[repoToken].next = NULL_NODE; + return; + } + + uint256 maturityToInsert = getRepoTokenMaturity(repoToken); + + address prev; + while (current != NULL_NODE) { + // If the repoToken is already in the list, exit + if (current == repoToken) { + break; + } + + uint256 currentMaturity = getRepoTokenMaturity(current); + + // Insert repoToken before current if its maturity is less than current maturity + if (maturityToInsert < currentMaturity) { + if (prev == NULL_NODE) { + listData.head = repoToken; + } else { + listData.nodes[prev].next = repoToken; + } + listData.nodes[repoToken].next = current; + 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; + listData.nodes[repoToken].next = NULL_NODE; + break; + } + + prev = current; + current = next; + } + } +} diff --git a/src/RepoTokenUtils.sol b/src/RepoTokenUtils.sol index 32c41d3c..469ac631 100644 --- a/src/RepoTokenUtils.sol +++ b/src/RepoTokenUtils.sol @@ -1,66 +1,77 @@ -// SPDX-License-Identifier: AGPL-3.0 -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 internal constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; - uint256 internal constant RATE_PRECISION = 1e18; - - /*////////////////////////////////////////////////////////////// - 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 discountRate - ) internal view returns (uint256 presentValue) { - uint256 timeLeftToMaturityDayFraction = block.timestamp > redemptionTimestamp ? 0 : - ((redemptionTimestamp - block.timestamp) * purchaseTokenPrecision) / THREESIXTY_DAYCOUNT_SECONDS; - - // repoTokenAmountInBaseAssetPrecision / (1 + r * days / 360) - presentValue = - (repoTokenAmountInBaseAssetPrecision * purchaseTokenPrecision) / - (purchaseTokenPrecision + (discountRate * timeLeftToMaturityDayFraction / RATE_PRECISION)); - - return presentValue > repoTokenAmountInBaseAssetPrecision ? repoTokenAmountInBaseAssetPrecision : presentValue; - } - - /** - * @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 - * @param repoRedemptionHaircut The haircut to be applied to the repoToken for bad debt - * @return repoTokenAmountInBaseAssetPrecision The normalized amount of the repoToken in base asset precision - */ - function getNormalizedRepoTokenAmount( - address repoToken, - uint256 repoTokenAmount, - uint256 purchaseTokenPrecision, - uint256 repoRedemptionHaircut - ) internal view returns (uint256 repoTokenAmountInBaseAssetPrecision) { - uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals(); - uint256 redemptionValue = ITermRepoToken(repoToken).redemptionValue(); - repoTokenAmountInBaseAssetPrecision = - repoRedemptionHaircut != 0 ? - (redemptionValue * repoRedemptionHaircut * repoTokenAmount * purchaseTokenPrecision) / - (repoTokenPrecision * RATE_PRECISION * 1e18) - : (redemptionValue * repoTokenAmount * purchaseTokenPrecision) / (repoTokenPrecision * RATE_PRECISION); - } -} \ No newline at end of file +// SPDX-License-Identifier: AGPL-3.0 +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 internal constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; + uint256 internal constant RATE_PRECISION = 1e18; + + /*////////////////////////////////////////////////////////////// + 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 discountRate + ) internal view returns (uint256 presentValue) { + uint256 timeLeftToMaturityDayFraction = block.timestamp > + redemptionTimestamp + ? 0 + : ((redemptionTimestamp - block.timestamp) * + purchaseTokenPrecision) / THREESIXTY_DAYCOUNT_SECONDS; + + // repoTokenAmountInBaseAssetPrecision / (1 + r * days / 360) + presentValue = + (repoTokenAmountInBaseAssetPrecision * purchaseTokenPrecision) / + (purchaseTokenPrecision + + ((discountRate * timeLeftToMaturityDayFraction) / + RATE_PRECISION)); + + return + presentValue > repoTokenAmountInBaseAssetPrecision + ? repoTokenAmountInBaseAssetPrecision + : presentValue; + } + + /** + * @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 + * @param repoRedemptionHaircut The haircut to be applied to the repoToken for bad debt + * @return repoTokenAmountInBaseAssetPrecision The normalized amount of the repoToken in base asset precision + */ + function getNormalizedRepoTokenAmount( + address repoToken, + uint256 repoTokenAmount, + uint256 purchaseTokenPrecision, + uint256 repoRedemptionHaircut + ) internal view returns (uint256 repoTokenAmountInBaseAssetPrecision) { + uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); + uint256 redemptionValue = ITermRepoToken(repoToken).redemptionValue(); + repoTokenAmountInBaseAssetPrecision = repoRedemptionHaircut != 0 + ? (redemptionValue * + repoRedemptionHaircut * + repoTokenAmount * + purchaseTokenPrecision) / + (repoTokenPrecision * RATE_PRECISION * 1e18) + : (redemptionValue * repoTokenAmount * purchaseTokenPrecision) / + (repoTokenPrecision * RATE_PRECISION); + } +} diff --git a/src/Strategy.sol b/src/Strategy.sol index 2e164ba0..073edb31 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -104,8 +104,6 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { TermAuctionListData internal termAuctionListData; string internal tokenSymbol; - - StrategyState public strategyState; mapping(address => bool) public repoTokenBlacklist; @@ -154,7 +152,9 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { //TERM_VAULT_EVENT_EMITTER.emitStrategyUnpaused(); } - function setPendingGovernor(address newGovernor) external onlyRole(GOVERNOR_ROLE) { + function setPendingGovernor( + address newGovernor + ) external onlyRole(GOVERNOR_ROLE) { require(newGovernor != address(0)); pendingGovernor = newGovernor; } @@ -176,8 +176,13 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { address newTermControllerAddr ) external onlyRole(GOVERNOR_ROLE) { require(newTermControllerAddr != address(0)); - require(ITermController(newTermControllerAddr).getProtocolReserveAddress() != address(0)); - ITermController newTermController = ITermController(newTermControllerAddr); + require( + ITermController(newTermControllerAddr) + .getProtocolReserveAddress() != address(0) + ); + ITermController newTermController = ITermController( + newTermControllerAddr + ); address currentIteration = repoTokenListData.head; while (currentIteration != address(0)) { if (!_isTermDeployed(currentIteration)) { @@ -201,8 +206,12 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { function setDiscountRateAdapter( address newAdapter ) external onlyRole(GOVERNOR_ROLE) { - ITermDiscountRateAdapter newDiscountRateAdapter = ITermDiscountRateAdapter(newAdapter); - require(address(newDiscountRateAdapter.currTermController()) != address(0)); + ITermDiscountRateAdapter newDiscountRateAdapter = ITermDiscountRateAdapter( + newAdapter + ); + require( + address(newDiscountRateAdapter.currTermController()) != address(0) + ); TERM_VAULT_EVENT_EMITTER.emitDiscountRateAdapterUpdated( address(strategyState.discountRateAdapter), newAdapter @@ -225,10 +234,10 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { } /** - * @notice Set the required reserve ratio - * @dev This function can only be called by management - * @param newRequiredReserveRatio The new required reserve ratio (in 1e18 precision) - */ + * @notice Set the required reserve ratio + * @dev This function can only be called by management + * @param newRequiredReserveRatio The new required reserve ratio (in 1e18 precision) + */ function setRequiredReserveRatio( uint256 newRequiredReserveRatio ) external onlyRole(GOVERNOR_ROLE) { @@ -250,7 +259,8 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { strategyState.repoTokenConcentrationLimit, newRepoTokenConcentrationLimit ); - strategyState.repoTokenConcentrationLimit = newRepoTokenConcentrationLimit; + strategyState + .repoTokenConcentrationLimit = newRepoTokenConcentrationLimit; } /** @@ -282,8 +292,14 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { repoTokenListData.collateralTokenParams[tokenAddr] = minCollateralRatio; } - function setRepoTokenBlacklist(address repoToken, bool blacklisted) external onlyRole(GOVERNOR_ROLE) { - TERM_VAULT_EVENT_EMITTER.emitRepoTokenBlacklistUpdated(repoToken, blacklisted); + function setRepoTokenBlacklist( + address repoToken, + bool blacklisted + ) external onlyRole(GOVERNOR_ROLE) { + TERM_VAULT_EVENT_EMITTER.emitRepoTokenBlacklistUpdated( + repoToken, + blacklisted + ); repoTokenBlacklist[repoToken] = blacklisted; } @@ -299,7 +315,7 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { * @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. + * and the present value of all pending offers to calculate the total asset value. */ function totalAssetValue() external view returns (uint256) { return _totalAssetValue(_totalLiquidBalance()); @@ -310,7 +326,7 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { * @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. + * 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(); @@ -324,10 +340,12 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { * @dev This function calculates the ratio of liquid balance to total asset value. * It returns 0 if the total asset value is 0 to avoid division by zero. */ - function _liquidReserveRatio(uint256 liquidBalance) internal view returns (uint256) { + function _liquidReserveRatio( + uint256 liquidBalance + ) internal view returns (uint256) { uint256 assetValue = _totalAssetValue(liquidBalance); if (assetValue == 0) return 0; - return liquidBalance * 1e18 / assetValue; + return (liquidBalance * 1e18) / assetValue; } /** @@ -373,13 +391,19 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { * is based on the current total asset value and does not consider any additional purchases * or removals of the repoToken. */ - function getRepoTokenConcentrationRatio(address repoToken) external view returns (uint256) { + function getRepoTokenConcentrationRatio( + address repoToken + ) external view returns (uint256) { if (repoToken == address(0)) { revert RepoTokenList.InvalidRepoToken(address(0)); } - return _getRepoTokenConcentrationRatio( - repoToken, 0, _totalAssetValue(_totalLiquidBalance()), 0 - ); + return + _getRepoTokenConcentrationRatio( + repoToken, + 0, + _totalAssetValue(_totalLiquidBalance()), + 0 + ); } /** @@ -402,11 +426,15 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { function simulateTransaction( address repoToken, uint256 amount - ) external view returns ( - uint256 simulatedWeightedMaturity, - uint256 simulatedRepoTokenConcentrationRatio, - uint256 simulatedLiquidityRatio - ) { + ) + external + view + returns ( + uint256 simulatedWeightedMaturity, + uint256 simulatedRepoTokenConcentrationRatio, + uint256 simulatedLiquidityRatio + ) + { // do not validate if we are simulating with existing repoTokens uint256 liquidBalance = _totalLiquidBalance(); uint256 repoTokenAmountInBaseAssetPrecision; @@ -416,25 +444,31 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { revert RepoTokenList.InvalidRepoToken(repoToken); } - (bool isRepoTokenValid, uint256 redemptionTimestamp) = repoTokenListData.validateRepoToken( - ITermRepoToken(repoToken), - address(asset) - ); + ( + bool isRepoTokenValid, + uint256 redemptionTimestamp + ) = repoTokenListData.validateRepoToken( + ITermRepoToken(repoToken), + address(asset) + ); if (!isRepoTokenValid) { revert RepoTokenList.InvalidRepoToken(repoToken); } - - - uint256 discountRate = strategyState.discountRateAdapter.getDiscountRate(repoToken); - uint256 repoRedemptionHaircut = strategyState.discountRateAdapter.repoRedemptionHaircut(repoToken); - repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( - repoToken, - amount, - PURCHASE_TOKEN_PRECISION, - repoRedemptionHaircut - ); + uint256 discountRate = strategyState + .discountRateAdapter + .getDiscountRate(repoToken); + uint256 repoRedemptionHaircut = strategyState + .discountRateAdapter + .repoRedemptionHaircut(repoToken); + repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + repoToken, + amount, + PURCHASE_TOKEN_PRECISION, + repoRedemptionHaircut + ); proceeds = RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, PURCHASE_TOKEN_PRECISION, @@ -444,22 +478,28 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { } simulatedWeightedMaturity = _calculateWeightedMaturity( - repoToken, amount, liquidBalance - proceeds); + repoToken, + amount, + liquidBalance - proceeds + ); if (repoToken != address(0)) { simulatedRepoTokenConcentrationRatio = _getRepoTokenConcentrationRatio( - repoToken, - repoTokenAmountInBaseAssetPrecision, - _totalAssetValue(liquidBalance), + repoToken, + repoTokenAmountInBaseAssetPrecision, + _totalAssetValue(liquidBalance), proceeds ); } uint256 assetValue = _totalAssetValue(liquidBalance); - if (assetValue == 0) {simulatedLiquidityRatio = 0;} - else { - simulatedLiquidityRatio = (liquidBalance - proceeds) * 10 ** 18 / assetValue; + if (assetValue == 0) { + simulatedLiquidityRatio = 0; + } else { + simulatedLiquidityRatio = + ((liquidBalance - proceeds) * 10 ** 18) / + assetValue; } } @@ -486,7 +526,9 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { repoToken, amount, PURCHASE_TOKEN_PRECISION, - strategyState.discountRateAdapter.repoRedemptionHaircut(repoToken) + strategyState.discountRateAdapter.repoRedemptionHaircut( + repoToken + ) ); return RepoTokenUtils.calculatePresentValue( @@ -512,18 +554,23 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { uint256 repoTokenHoldingPV; if (repoTokenListData.discountRates[repoToken] != 0) { address tokenTermController; - if (strategyState.currTermController.isTermDeployed(repoToken)){ + if (strategyState.currTermController.isTermDeployed(repoToken)) { tokenTermController = address(strategyState.currTermController); - } else if (strategyState.prevTermController.isTermDeployed(repoToken)){ + } else if ( + strategyState.prevTermController.isTermDeployed(repoToken) + ) { tokenTermController = address(strategyState.prevTermController); } repoTokenHoldingPV = calculateRepoTokenPresentValue( repoToken, - strategyState.discountRateAdapter.getDiscountRate(tokenTermController, repoToken), + strategyState.discountRateAdapter.getDiscountRate( + tokenTermController, + repoToken + ), ITermRepoToken(repoToken).balanceOf(address(this)) ); - } - return + } + return repoTokenHoldingPV + termAuctionListData.getPresentValue( repoTokenListData, @@ -542,11 +589,7 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { * @param amount The amount to withdraw */ function _withdrawAsset(uint256 amount) private { - YEARN_VAULT.withdraw( - amount, - address(this), - address(this) - ); + YEARN_VAULT.withdraw(amount, address(this), address(this)); } /** @@ -577,10 +620,12 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { * @dev This function aggregates the total liquid balance, the present value of all repoTokens, * and the present value of all pending offers to calculate the total asset value. */ - function _totalAssetValue(uint256 liquidBalance) internal view returns (uint256 totalValue) { + function _totalAssetValue( + uint256 liquidBalance + ) internal view returns (uint256 totalValue) { ITermController prevTermController = strategyState.prevTermController; ITermController currTermController = strategyState.currTermController; - + return liquidBalance + repoTokenListData.getPresentValue( @@ -611,25 +656,28 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { address repoToken, uint256 repoTokenAmountInBaseAssetPrecision, uint256 assetValue, - uint256 liquidBalanceToRemove + uint256 liquidBalanceToRemove ) private view returns (uint256) { // Retrieve the current value of the repoToken held by the strategy and add the new repoToken amount uint256 repoTokenValue = getRepoTokenHoldingValue(repoToken) + repoTokenAmountInBaseAssetPrecision; - // Retrieve the total asset value of the strategy and adjust it for the new repoToken amount and liquid balance to be removed + // Retrieve the total asset value of the strategy and adjust it for the new repoToken amount and liquid balance to be removed uint256 adjustedTotalAssetValue = assetValue + repoTokenAmountInBaseAssetPrecision - liquidBalanceToRemove; // Normalize the repoToken value and total asset value to 1e18 precision repoTokenValue = (repoTokenValue * 1e18) / PURCHASE_TOKEN_PRECISION; - adjustedTotalAssetValue = (adjustedTotalAssetValue * 1e18) / PURCHASE_TOKEN_PRECISION; + adjustedTotalAssetValue = + (adjustedTotalAssetValue * 1e18) / + PURCHASE_TOKEN_PRECISION; // Calculate the repoToken concentration - return adjustedTotalAssetValue == 0 - ? 0 - : (repoTokenValue * 1e18) / adjustedTotalAssetValue; + return + adjustedTotalAssetValue == 0 + ? 0 + : (repoTokenValue * 1e18) / adjustedTotalAssetValue; } /** @@ -657,11 +705,13 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { ); // Check if the repoToken concentration exceeds the predefined limit - if (repoTokenConcentration > strategyState.repoTokenConcentrationLimit) { + if ( + repoTokenConcentration > strategyState.repoTokenConcentrationLimit + ) { revert RepoTokenConcentrationTooHigh(repoToken); } } - + /** * @notice Calculates the weighted time to maturity for the strategy's holdings, including the impact of a specified repoToken and amount * @param repoToken The address of the repoToken (optional) @@ -673,15 +723,14 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { * 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. + * the cumulative data without specific token adjustments. */ function _calculateWeightedMaturity( address repoToken, uint256 repoTokenAmount, uint256 liquidBalance ) private view returns (uint256) { - - // Initialize cumulative weighted time to maturity and cumulative amount + // Initialize cumulative weighted time to maturity and cumulative amount uint256 cumulativeWeightedTimeToMaturity; // in seconds uint256 cumulativeAmount; // in purchase token precision @@ -722,7 +771,9 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { !foundInOfferList && repoToken != address(0) ) { - uint256 repoRedemptionHaircut = strategyState.discountRateAdapter.repoRedemptionHaircut(repoToken); + uint256 repoRedemptionHaircut = strategyState + .discountRateAdapter + .repoRedemptionHaircut(repoToken); uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils .getNormalizedRepoTokenAmount( repoToken, @@ -762,10 +813,16 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { function _isTermDeployed(address termContract) private view returns (bool) { ITermController currTermController = strategyState.currTermController; ITermController prevTermController = strategyState.prevTermController; - if (address(currTermController) != address(0) && currTermController.isTermDeployed(termContract)) { + if ( + address(currTermController) != address(0) && + currTermController.isTermDeployed(termContract) + ) { return true; } - if (address(prevTermController) != address(0) && prevTermController.isTermDeployed(termContract)) { + if ( + address(prevTermController) != address(0) && + prevTermController.isTermDeployed(termContract) + ) { return true; } return false; @@ -777,11 +834,15 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { * * @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. + * optimizing asset allocation. */ function _redeemRepoTokens(uint256 liquidAmountRequired) private { // Remove completed auction offers - termAuctionListData.removeCompleted(repoTokenListData, strategyState.discountRateAdapter, address(asset)); + termAuctionListData.removeCompleted( + repoTokenListData, + strategyState.discountRateAdapter, + address(asset) + ); // Remove and redeem matured repoTokens repoTokenListData.removeAndRedeemMaturedTokens(); @@ -791,7 +852,10 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { // Deposit excess underlying balance into Yearn Vault if (liquidity > liquidAmountRequired) { unchecked { - YEARN_VAULT.deposit(liquidity - liquidAmountRequired, address(this)); + YEARN_VAULT.deposit( + liquidity - liquidAmountRequired, + address(this) + ); } // Withdraw shortfall from Yearn Vault to meet required liquidity } else if (liquidity < liquidAmountRequired) { @@ -827,10 +891,12 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { revert RepoTokenList.InvalidRepoToken(repoToken); } - if(termAuction.termRepoId() != ITermRepoToken(repoToken).termRepoId()) { + if ( + termAuction.termRepoId() != ITermRepoToken(repoToken).termRepoId() + ) { revert RepoTokenList.InvalidRepoToken(repoToken); - } - + } + // Validate purchase token, min collateral ratio and insert the repoToken if necessary (bool isValid, ) = repoTokenListData.validateRepoToken( ITermRepoToken(repoToken), @@ -845,9 +911,9 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { ITermAuctionOfferLocker offerLocker = ITermAuctionOfferLocker( termAuction.termAuctionOfferLocker() ); - if( + if ( block.timestamp <= offerLocker.auctionStartTime() || - block.timestamp >= offerLocker.revealTime() + block.timestamp >= offerLocker.revealTime() ) { revert AuctionNotOpen(); } @@ -865,7 +931,7 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { * @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. + * and rebalances liquidity to support the offer submission. It handles both new offers and edits to existing offers. */ function submitAuctionOffer( ITermAuction termAuction, @@ -880,7 +946,7 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { onlyManagement returns (bytes32[] memory offerIds) { - if(purchaseTokenAmount == 0) { + if (purchaseTokenAmount == 0) { revert ZeroPurchaseTokenAmount(); } @@ -919,14 +985,14 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { uint256 liquidBalance = _totalLiquidBalance(); uint256 totalAssetValue = _totalAssetValue(liquidBalance); require(totalAssetValue > 0); - uint256 liquidReserveRatio = liquidBalance * 1e18 / totalAssetValue; // NOTE: we require totalAssetValue > 0 above + uint256 liquidReserveRatio = (liquidBalance * 1e18) / totalAssetValue; // NOTE: we require totalAssetValue > 0 above // Check that new offer does not violate reserve ratio constraint if (liquidReserveRatio < strategyState.requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } - // Calculate the resulting weighted time to maturity + // Calculate the resulting weighted time to maturity // Passing in 0 adjustment because offer and balance already updated uint256 resultingWeightedTimeToMaturity = _calculateWeightedMaturity( address(0), @@ -935,7 +1001,10 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { ); // Check if the resulting weighted time to maturity exceeds the threshold - if (resultingWeightedTimeToMaturity > strategyState.timeToMaturityThreshold) { + if ( + resultingWeightedTimeToMaturity > + strategyState.timeToMaturityThreshold + ) { revert TimeToMaturityAboveThreshold(); } @@ -981,7 +1050,7 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { offerDebit = newOfferAmount - currentOfferAmount; } - uint256 liquidBalance = _totalLiquidBalance(); + uint256 liquidBalance = _totalLiquidBalance(); if (liquidBalance < offerDebit) { revert InsufficientLiquidBalance(liquidBalance, offerDebit); } @@ -996,7 +1065,7 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { // Submit the offer and get the offer IDs offerIds = offerLocker.lockOffers(offerSubmissions); - if(offerIds.length == 0) { + if (offerIds.length == 0) { revert OfferNotFound(); } @@ -1014,14 +1083,18 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { ); } else { // Edit offer, overwrite existing - PendingOffer storage pendingOffer = termAuctionListData.offers[offerIds[0]]; + PendingOffer storage pendingOffer = termAuctionListData.offers[ + offerIds[0] + ]; pendingOffer.offerAmount = offer.amount; } - if (newOfferAmount < currentOfferAmount) { - YEARN_VAULT.deposit(IERC20(asset).balanceOf(address(this)), address(this)); + if (newOfferAmount < currentOfferAmount) { + YEARN_VAULT.deposit( + IERC20(asset).balanceOf(address(this)), + address(this) + ); } - } /** @@ -1087,8 +1160,11 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { } // Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp - (bool isRepoTokenValid , , uint256 redemptionTimestamp) = repoTokenListData - .validateAndInsertRepoToken( + ( + bool isRepoTokenValid, + , + uint256 redemptionTimestamp + ) = repoTokenListData.validateAndInsertRepoToken( ITermRepoToken(repoToken), strategyState.discountRateAdapter, address(asset) @@ -1107,17 +1183,22 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { uint256 totalAssetValue = _totalAssetValue(liquidBalance); require(totalAssetValue > 0); - uint256 discountRate = strategyState.discountRateAdapter.getDiscountRate(repoToken); + uint256 discountRate = strategyState + .discountRateAdapter + .getDiscountRate(repoToken); - // Calculate the repoToken amount in base asset precision - uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( - repoToken, - repoTokenAmount, - PURCHASE_TOKEN_PRECISION, - strategyState.discountRateAdapter.repoRedemptionHaircut(repoToken) - ); + // Calculate the repoToken amount in base asset precision + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + repoToken, + repoTokenAmount, + PURCHASE_TOKEN_PRECISION, + strategyState.discountRateAdapter.repoRedemptionHaircut( + repoToken + ) + ); - // Calculate the proceeds from selling the repoToken + // Calculate the proceeds from selling the repoToken uint256 proceeds = RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, PURCHASE_TOKEN_PRECISION, @@ -1141,7 +1222,8 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { } // Ensure the remaining liquid balance is above the liquidity threshold - uint256 newLiquidReserveRatio = ( liquidBalance - proceeds ) * 1e18 / totalAssetValue; // NOTE: we require totalAssetValue > 0 above + uint256 newLiquidReserveRatio = ((liquidBalance - proceeds) * 1e18) / + totalAssetValue; // NOTE: we require totalAssetValue > 0 above if (newLiquidReserveRatio < strategyState.requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } @@ -1182,7 +1264,10 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { TERM_VAULT_EVENT_EMITTER = ITermVaultEvents(_params._eventEmitter); PURCHASE_TOKEN_PRECISION = 10 ** ERC20(asset).decimals(); - IERC20(_params._asset).safeApprove(_params._yearnVault, type(uint256).max); + IERC20(_params._asset).safeApprove( + _params._yearnVault, + type(uint256).max + ); tokenSymbol = _symbol; strategyState = StrategyState({ @@ -1191,7 +1276,9 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { governorAddress: _params._governorAddress, prevTermController: ITermController(address(0)), currTermController: ITermController(_params._termController), - discountRateAdapter: ITermDiscountRateAdapter(_params._discountRateAdapter), + discountRateAdapter: ITermDiscountRateAdapter( + _params._discountRateAdapter + ), timeToMaturityThreshold: _params._timeToMaturityThreshold, requiredReserveRatio: _params._requiredReserveRatio, discountRateMarkup: _params._discountRateMarkup, diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 7ae55a64..c98e9330 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -1,389 +1,440 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {ITermController} from "./interfaces/term/ITermController.sol"; -import {ITermAuction} from "./interfaces/term/ITermAuction.sol"; -import {ITermAuctionOfferLocker} from "./interfaces/term/ITermAuctionOfferLocker.sol"; -import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; -import {ITermRepoServicer} from "./interfaces/term/ITermRepoServicer.sol"; -import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.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; - ITermAuction termAuction; - ITermAuctionOfferLocker offerLocker; -} - -struct TermAuctionListNode { - bytes32 next; -} - -struct TermAuctionListData { - bytes32 head; - mapping(bytes32 => TermAuctionListNode) nodes; - mapping(bytes32 => PendingOffer) offers; -} - -/*////////////////////////////////////////////////////////////// - LIBRARY: TermAuctionList -//////////////////////////////////////////////////////////////*/ - -library TermAuctionList { - using RepoTokenList for RepoTokenListData; - - bytes32 internal 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; - } - - /*////////////////////////////////////////////////////////////// - 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; - while (current != NULL_NODE) { - count++; - current = _getNext(listData, current); - } - } - - /** - * @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) { - offers = new bytes32[](count); - uint256 i; - bytes32 current = listData.head; - while (current != NULL_NODE) { - offers[i++] = current; - current = _getNext(listData, current); - } - } - } - - /** - * @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 while maintaining the list sorted by auction address. - * The function iterates through the list to find the correct position for the new `offerId` and updates the pointers accordingly. - */ - function insertPending(TermAuctionListData storage listData, bytes32 offerId, PendingOffer memory pendingOffer) internal { - bytes32 current = listData.head; - require(!pendingOffer.termAuction.auctionCompleted()); - - // If the list is empty, set the new repoToken as the head - if (current == NULL_NODE) { - listData.head = offerId; - listData.nodes[offerId].next = NULL_NODE; - listData.offers[offerId] = pendingOffer; - return; - } - - bytes32 prev; - while (current != NULL_NODE) { - - // If the offerId is already in the list, exit - if (current == offerId) { - break; - } - - address currentAuction = address(listData.offers[current].termAuction); - address auctionToInsert = address(pendingOffer.termAuction); - - // Insert offer before current if the auction address to insert is less than current auction address - if (auctionToInsert < currentAuction) { - if (prev == NULL_NODE) { - listData.head = offerId; - } else { - listData.nodes[prev].next = offerId; - } - listData.nodes[offerId].next = current; - break; - } - - // Move to the next node - bytes32 next = _getNext(listData, current); - - // If at the end of the list, insert repoToken after current - if (next == NULL_NODE) { - listData.nodes[current].next = offerId; - listData.nodes[offerId].next = NULL_NODE; - break; - } - - prev = current; - current = next; - } - 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 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, - ITermDiscountRateAdapter discountRateAdapter, - address asset - ) internal { - // Return if the list is empty - if (listData.head == NULL_NODE) return; - - bytes32 current = listData.head; - bytes32 prev = current; - while (current != NULL_NODE) { - PendingOffer memory offer = listData.offers[current]; - bytes32 next = _getNext(listData, current); - - uint256 offerAmount = offer.offerLocker.lockedOffer(current).amount; - bool removeNode; - - if (offer.termAuction.auctionCompleted()) { - // If auction is completed and closed, mark for removal and prepare to insert repo token - removeNode = true; - // 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). - (bool isValidRepoToken, , uint256 redemptionTimestamp ) = repoTokenListData.validateAndInsertRepoToken( - ITermRepoToken(offer.repoToken), discountRateAdapter, asset - ); - if (!isValidRepoToken && block.timestamp > redemptionTimestamp) { - ITermRepoToken repoToken = ITermRepoToken(offer.repoToken); - (, , address repoServicerAddr, ) = repoToken.config(); - ITermRepoServicer repoServicer = ITermRepoServicer(repoServicerAddr); - try repoServicer.redeemTermRepoTokens(address(this), repoToken.balanceOf(address(this))) { - } catch { - - } - } - } else { - if (offer.termAuction.auctionCancelledForWithdrawal()) { - // If auction was canceled for withdrawal, remove the node and unlock offers manually - bytes32[] memory offerIds = new bytes32[](1); - offerIds[0] = current; - try offer.offerLocker.unlockOffers(offerIds) { // unlocking offer in this scenario withdraws offer amount - removeNode = true; - } catch { - removeNode = false; - } - } else { - if (offerAmount == 0) { - // If offer amount is zero, it indicates the auction was canceled or deleted - removeNode = true; - } - } - } - - if (removeNode) { - // Update the list to remove the current node - delete listData.nodes[current]; - delete listData.offers[current]; - if (current == listData.head) { - listData.head = next; - } - else { - listData.nodes[prev].next = next; - current = prev; - } - } - - // Move to the next node - prev = current; - current = next; - } - } - - /** - * @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, - 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; - address edgeCaseAuction; // NOTE: handle edge case, assumes that pendingOffer is properly sorted by auction address - - bytes32 current = listData.head; - while (current != NULL_NODE) { - PendingOffer storage offer = listData.offers[current]; - - // Filter by specific repo token if provided, address(0) bypasses this filter - if (repoTokenToMatch != address(0) && offer.repoToken != repoTokenToMatch) { - // Not a match, skip - // Move to the next token in the list - current = _getNext(listData, current); - continue; - } - - uint256 offerAmount = offer.offerLocker.lockedOffer(current).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 repoTokendiscountRates to make sure we are not double counting on re-openings - if (offer.termAuction.auctionCompleted() && repoTokenListData.discountRates[offer.repoToken] == 0) { - if (edgeCaseAuction != address(offer.termAuction)) { - uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( - offer.repoToken, - ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision, - discountRateAdapter.repoRedemptionHaircut(offer.repoToken) - ); - totalValue += RepoTokenUtils.calculatePresentValue( - repoTokenAmountInBaseAssetPrecision, - purchaseTokenPrecision, - RepoTokenList.getRepoTokenMaturity(offer.repoToken), - discountRateAdapter.getDiscountRate(offer.repoToken) - ); - - // Mark the edge case auction as processed to avoid double counting - // since multiple offers can be tied to the same auction, we need to mark - // the edge case auction as processed to avoid double counting - edgeCaseAuction = address(offer.termAuction); - } - } else { - // Add the offer amount to the total value - totalValue += offerAmount; - } - - // Move to the next token in the list - current = _getNext(listData, current); - } - } - - /** - * @notice Get cumulative offer data for a specified repoToken - * @param listData The list data - * @param repoTokenListData The repoToken list data - * @param discountRateAdapter The discount rate adapter - * @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, - ITermDiscountRateAdapter discountRateAdapter, - address repoToken, - 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); - address edgeCaseAuction; // NOTE: handle edge case, assumes that pendingOffer is properly sorted by auction address - - - bytes32 current = listData.head; - while (current != NULL_NODE) { - PendingOffer storage offer =listData.offers[current]; - - uint256 offerAmount; - if (offer.repoToken == repoToken) { - offerAmount = newOfferAmount; - found = true; - } else { - // Retrieve the current offer amount from the offer locker - offerAmount = offer.offerLocker.lockedOffer(current).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 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 (edgeCaseAuction != address(offer.termAuction)) { - offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( - offer.repoToken, - ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision, - discountRateAdapter.repoRedemptionHaircut(offer.repoToken) - ); - - - // Mark the edge case auction as processed to avoid double counting - // since multiple offers can be tied to the same auction, we need to mark - // the edge case auction as processed to avoid double counting - edgeCaseAuction = address(offer.termAuction); - } - } - } - - if (offerAmount > 0) { - // Calculate weighted time to maturity - uint256 weightedTimeToMaturity = RepoTokenList.getRepoTokenWeightedTimeToMaturity( - offer.repoToken, offerAmount - ); - - cumulativeWeightedTimeToMaturity += weightedTimeToMaturity; - cumulativeOfferAmount += offerAmount; - } - - // Move to the next token in the list - current = _getNext(listData, current); - } - } -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ITermController} from "./interfaces/term/ITermController.sol"; +import {ITermAuction} from "./interfaces/term/ITermAuction.sol"; +import {ITermAuctionOfferLocker} from "./interfaces/term/ITermAuctionOfferLocker.sol"; +import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; +import {ITermRepoServicer} from "./interfaces/term/ITermRepoServicer.sol"; +import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.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; + ITermAuction termAuction; + ITermAuctionOfferLocker offerLocker; +} + +struct TermAuctionListNode { + bytes32 next; +} + +struct TermAuctionListData { + bytes32 head; + mapping(bytes32 => TermAuctionListNode) nodes; + mapping(bytes32 => PendingOffer) offers; +} + +/*////////////////////////////////////////////////////////////// + LIBRARY: TermAuctionList +//////////////////////////////////////////////////////////////*/ + +library TermAuctionList { + using RepoTokenList for RepoTokenListData; + + bytes32 internal 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; + } + + /*////////////////////////////////////////////////////////////// + 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; + while (current != NULL_NODE) { + count++; + current = _getNext(listData, current); + } + } + + /** + * @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) { + offers = new bytes32[](count); + uint256 i; + bytes32 current = listData.head; + while (current != NULL_NODE) { + offers[i++] = current; + current = _getNext(listData, current); + } + } + } + + /** + * @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 while maintaining the list sorted by auction address. + * The function iterates through the list to find the correct position for the new `offerId` and updates the pointers accordingly. + */ + function insertPending( + TermAuctionListData storage listData, + bytes32 offerId, + PendingOffer memory pendingOffer + ) internal { + bytes32 current = listData.head; + require(!pendingOffer.termAuction.auctionCompleted()); + + // If the list is empty, set the new repoToken as the head + if (current == NULL_NODE) { + listData.head = offerId; + listData.nodes[offerId].next = NULL_NODE; + listData.offers[offerId] = pendingOffer; + return; + } + + bytes32 prev; + while (current != NULL_NODE) { + // If the offerId is already in the list, exit + if (current == offerId) { + break; + } + + address currentAuction = address( + listData.offers[current].termAuction + ); + address auctionToInsert = address(pendingOffer.termAuction); + + // Insert offer before current if the auction address to insert is less than current auction address + if (auctionToInsert < currentAuction) { + if (prev == NULL_NODE) { + listData.head = offerId; + } else { + listData.nodes[prev].next = offerId; + } + listData.nodes[offerId].next = current; + break; + } + + // Move to the next node + bytes32 next = _getNext(listData, current); + + // If at the end of the list, insert repoToken after current + if (next == NULL_NODE) { + listData.nodes[current].next = offerId; + listData.nodes[offerId].next = NULL_NODE; + break; + } + + prev = current; + current = next; + } + 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 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, + ITermDiscountRateAdapter discountRateAdapter, + address asset + ) internal { + // Return if the list is empty + if (listData.head == NULL_NODE) return; + + bytes32 current = listData.head; + bytes32 prev = current; + while (current != NULL_NODE) { + PendingOffer memory offer = listData.offers[current]; + bytes32 next = _getNext(listData, current); + + uint256 offerAmount = offer.offerLocker.lockedOffer(current).amount; + bool removeNode; + + if (offer.termAuction.auctionCompleted()) { + // If auction is completed and closed, mark for removal and prepare to insert repo token + removeNode = true; + // 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). + ( + bool isValidRepoToken, + , + uint256 redemptionTimestamp + ) = repoTokenListData.validateAndInsertRepoToken( + ITermRepoToken(offer.repoToken), + discountRateAdapter, + asset + ); + if ( + !isValidRepoToken && block.timestamp > redemptionTimestamp + ) { + ITermRepoToken repoToken = ITermRepoToken(offer.repoToken); + (, , address repoServicerAddr, ) = repoToken.config(); + ITermRepoServicer repoServicer = ITermRepoServicer( + repoServicerAddr + ); + try + repoServicer.redeemTermRepoTokens( + address(this), + repoToken.balanceOf(address(this)) + ) + {} catch {} + } + } else { + if (offer.termAuction.auctionCancelledForWithdrawal()) { + // If auction was canceled for withdrawal, remove the node and unlock offers manually + bytes32[] memory offerIds = new bytes32[](1); + offerIds[0] = current; + try offer.offerLocker.unlockOffers(offerIds) { + // unlocking offer in this scenario withdraws offer amount + removeNode = true; + } catch { + removeNode = false; + } + } else { + if (offerAmount == 0) { + // If offer amount is zero, it indicates the auction was canceled or deleted + removeNode = true; + } + } + } + + if (removeNode) { + // Update the list to remove the current node + delete listData.nodes[current]; + delete listData.offers[current]; + if (current == listData.head) { + listData.head = next; + } else { + listData.nodes[prev].next = next; + current = prev; + } + } + + // Move to the next node + prev = current; + current = next; + } + } + + /** + * @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, + 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; + address edgeCaseAuction; // NOTE: handle edge case, assumes that pendingOffer is properly sorted by auction address + + bytes32 current = listData.head; + while (current != NULL_NODE) { + PendingOffer storage offer = listData.offers[current]; + + // Filter by specific repo token if provided, address(0) bypasses this filter + if ( + repoTokenToMatch != address(0) && + offer.repoToken != repoTokenToMatch + ) { + // Not a match, skip + // Move to the next token in the list + current = _getNext(listData, current); + continue; + } + + uint256 offerAmount = offer.offerLocker.lockedOffer(current).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 repoTokendiscountRates to make sure we are not double counting on re-openings + if ( + offer.termAuction.auctionCompleted() && + repoTokenListData.discountRates[offer.repoToken] == 0 + ) { + if (edgeCaseAuction != address(offer.termAuction)) { + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + offer.repoToken, + ITermRepoToken(offer.repoToken).balanceOf( + address(this) + ), + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut( + offer.repoToken + ) + ); + totalValue += RepoTokenUtils.calculatePresentValue( + repoTokenAmountInBaseAssetPrecision, + purchaseTokenPrecision, + RepoTokenList.getRepoTokenMaturity(offer.repoToken), + discountRateAdapter.getDiscountRate(offer.repoToken) + ); + + // Mark the edge case auction as processed to avoid double counting + // since multiple offers can be tied to the same auction, we need to mark + // the edge case auction as processed to avoid double counting + edgeCaseAuction = address(offer.termAuction); + } + } else { + // Add the offer amount to the total value + totalValue += offerAmount; + } + + // Move to the next token in the list + current = _getNext(listData, current); + } + } + + /** + * @notice Get cumulative offer data for a specified repoToken + * @param listData The list data + * @param repoTokenListData The repoToken list data + * @param discountRateAdapter The discount rate adapter + * @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, + ITermDiscountRateAdapter discountRateAdapter, + address repoToken, + 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); + address edgeCaseAuction; // NOTE: handle edge case, assumes that pendingOffer is properly sorted by auction address + + bytes32 current = listData.head; + while (current != NULL_NODE) { + PendingOffer storage offer = listData.offers[current]; + + uint256 offerAmount; + if (offer.repoToken == repoToken) { + offerAmount = newOfferAmount; + found = true; + } else { + // Retrieve the current offer amount from the offer locker + offerAmount = offer.offerLocker.lockedOffer(current).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 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 (edgeCaseAuction != address(offer.termAuction)) { + offerAmount = RepoTokenUtils + .getNormalizedRepoTokenAmount( + offer.repoToken, + ITermRepoToken(offer.repoToken).balanceOf( + address(this) + ), + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut( + offer.repoToken + ) + ); + + // Mark the edge case auction as processed to avoid double counting + // since multiple offers can be tied to the same auction, we need to mark + // the edge case auction as processed to avoid double counting + edgeCaseAuction = address(offer.termAuction); + } + } + } + + if (offerAmount > 0) { + // Calculate weighted time to maturity + uint256 weightedTimeToMaturity = RepoTokenList + .getRepoTokenWeightedTimeToMaturity( + offer.repoToken, + offerAmount + ); + + cumulativeWeightedTimeToMaturity += weightedTimeToMaturity; + cumulativeOfferAmount += offerAmount; + } + + // Move to the next token in the list + current = _getNext(listData, current); + } + } +} diff --git a/src/TermDiscountRateAdapter.sol b/src/TermDiscountRateAdapter.sol index e91ccc35..011d5f32 100644 --- a/src/TermDiscountRateAdapter.sol +++ b/src/TermDiscountRateAdapter.sol @@ -1,155 +1,192 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.sol"; -import {ITermController, AuctionMetadata} from "./interfaces/term/ITermController.sol"; -import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; -import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; - -contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl { - bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); - - /// @dev Previous term controller - ITermController public prevTermController; - /// @dev Current term controller - ITermController public currTermController; - mapping(address => mapping (bytes32 => bool)) public rateInvalid; - mapping(address => uint256) public repoRedemptionHaircut; - - constructor(address termController_, address oracleWallet_) { - currTermController = ITermController(termController_); - _grantRole(ORACLE_ROLE, oracleWallet_); - } - - /** - * @notice Retrieves the discount rate for a given repo token - * @param termController The address of the term controller - * @param repoToken The address of the repo token - * @return The discount rate for the specified repo token - * @dev This function fetches the auction results for the repo token's term repo ID - * and returns the clearing rate of the most recent auction - */ - function getDiscountRate(address termController, address repoToken) public view virtual returns (uint256) { - - if (repoToken == address(0)) return 0; - - ITermController tokenTermController; - if (termController == address(prevTermController)) { - tokenTermController = prevTermController; - } else if (termController == address(currTermController)) { - tokenTermController = currTermController; - } else { - revert("Invalid term controller"); - } - return _getDiscountRate(tokenTermController, repoToken); - } - - /** - * @notice Retrieves the discount rate for a given repo token - * @param repoToken The address of the repo token - * @return The discount rate for the specified repo token - * @dev This function fetches the auction results for the repo token's term repo ID - * and returns the clearing rate of the most recent auction - */ - function getDiscountRate(address repoToken) public view virtual returns (uint256) { - if (repoToken == address(0)) return 0; - ITermController tokenTermController = _identifyTermController(repoToken); - return _getDiscountRate(tokenTermController, repoToken); - } - - /** - * @notice Sets the invalidity of the result of a specific auction for a given repo token - * @dev This function is used to mark auction results as invalid or not, typically in cases of suspected manipulation - * @param repoToken The address of the repo token associated with the auction - * @param termAuctionId The unique identifier of the term auction to be invalidated - * @param isInvalid The status of the rate invalidation - * @custom:access Restricted to accounts with the ORACLE_ROLE - */ - function setAuctionRateValidator( - address repoToken, - bytes32 termAuctionId, - bool isInvalid - ) external onlyRole(ORACLE_ROLE) { - ITermController tokenTermController = _identifyTermController(repoToken); - // Fetch the auction metadata for the given repo token - (AuctionMetadata[] memory auctionMetadata, ) = tokenTermController.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId()); - - // Check if the termAuctionId exists in the metadata - bool auctionExists = _validateAuctionExistence(auctionMetadata, termAuctionId); - - require(auctionMetadata.length > 1, "Cannot invalidate the only auction result"); - // Revert if the auction doesn't exist - require(auctionExists, "Auction ID not found in metadata"); - - // Update the rate invalidation status - rateInvalid[repoToken][termAuctionId] = isInvalid; - } - - /** - * @notice Sets the term controller - * @param termController The address of the term controller - */ - function setTermController(address termController) external onlyRole(ORACLE_ROLE) { - prevTermController = currTermController; - currTermController = ITermController(termController); - } - - /** - * @notice Set the repo redemption haircut - * @param repoToken The address of the repo token - * @param haircut The repo redemption haircut in 18 decimals - */ - function setRepoRedemptionHaircut(address repoToken, uint256 haircut) external onlyRole(ORACLE_ROLE) { - repoRedemptionHaircut[repoToken] = haircut; - } - - function _identifyTermController(address termRepoToken) internal view returns (ITermController) { - if (currTermController.isTermDeployed(termRepoToken)) { - return currTermController; - } else if (prevTermController.isTermDeployed(termRepoToken)) { - return prevTermController; - } else { - revert("Term controller not found"); - } - } - - function _getDiscountRate(ITermController termController, address repoToken) internal view returns (uint256) { - (AuctionMetadata[] memory auctionMetadata, ) = termController.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId()); - - uint256 len = auctionMetadata.length; - require(len > 0, "No auctions found"); - - // If there is a re-opening auction, e.g. 2 or more results for the same token - if (len > 1) { - uint256 latestAuctionTime = auctionMetadata[len - 1].auctionClearingBlockTimestamp; - if ((block.timestamp - latestAuctionTime) < 30 minutes) { - for (int256 i = int256(len) - 2; i >= 0; i--) { - if (!rateInvalid[repoToken][auctionMetadata[uint256(i)].termAuctionId]) { - return auctionMetadata[uint256(i)].auctionClearingRate; - } - } - } else { - for (int256 i = int256(len) - 1; i >= 0; i--) { - if (!rateInvalid[repoToken][auctionMetadata[uint256(i)].termAuctionId]) { - return auctionMetadata[uint256(i)].auctionClearingRate; - } - } - } - revert("No valid auction rate found"); - } - - // If there is only 1 result (not a re-opening) then always return result - return auctionMetadata[0].auctionClearingRate; - } - - - function _validateAuctionExistence(AuctionMetadata[] memory auctionMetadata, bytes32 termAuctionId) private pure returns(bool auctionExists) { - // Check if the termAuctionId exists in the metadata - for (uint256 i = 0; i < auctionMetadata.length; i++) { - if (auctionMetadata[i].termAuctionId == termAuctionId) { - auctionExists = true; - break; - } - } - } -} +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.sol"; +import {ITermController, AuctionMetadata} from "./interfaces/term/ITermController.sol"; +import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl { + bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); + + /// @dev Previous term controller + ITermController public prevTermController; + /// @dev Current term controller + ITermController public currTermController; + mapping(address => mapping(bytes32 => bool)) public rateInvalid; + mapping(address => uint256) public repoRedemptionHaircut; + + constructor(address termController_, address oracleWallet_) { + currTermController = ITermController(termController_); + _grantRole(ORACLE_ROLE, oracleWallet_); + } + + /** + * @notice Retrieves the discount rate for a given repo token + * @param termController The address of the term controller + * @param repoToken The address of the repo token + * @return The discount rate for the specified repo token + * @dev This function fetches the auction results for the repo token's term repo ID + * and returns the clearing rate of the most recent auction + */ + function getDiscountRate( + address termController, + address repoToken + ) public view virtual returns (uint256) { + if (repoToken == address(0)) return 0; + + ITermController tokenTermController; + if (termController == address(prevTermController)) { + tokenTermController = prevTermController; + } else if (termController == address(currTermController)) { + tokenTermController = currTermController; + } else { + revert("Invalid term controller"); + } + return _getDiscountRate(tokenTermController, repoToken); + } + + /** + * @notice Retrieves the discount rate for a given repo token + * @param repoToken The address of the repo token + * @return The discount rate for the specified repo token + * @dev This function fetches the auction results for the repo token's term repo ID + * and returns the clearing rate of the most recent auction + */ + function getDiscountRate( + address repoToken + ) public view virtual returns (uint256) { + if (repoToken == address(0)) return 0; + ITermController tokenTermController = _identifyTermController( + repoToken + ); + return _getDiscountRate(tokenTermController, repoToken); + } + + /** + * @notice Sets the invalidity of the result of a specific auction for a given repo token + * @dev This function is used to mark auction results as invalid or not, typically in cases of suspected manipulation + * @param repoToken The address of the repo token associated with the auction + * @param termAuctionId The unique identifier of the term auction to be invalidated + * @param isInvalid The status of the rate invalidation + * @custom:access Restricted to accounts with the ORACLE_ROLE + */ + function setAuctionRateValidator( + address repoToken, + bytes32 termAuctionId, + bool isInvalid + ) external onlyRole(ORACLE_ROLE) { + ITermController tokenTermController = _identifyTermController( + repoToken + ); + // Fetch the auction metadata for the given repo token + (AuctionMetadata[] memory auctionMetadata, ) = tokenTermController + .getTermAuctionResults(ITermRepoToken(repoToken).termRepoId()); + + // Check if the termAuctionId exists in the metadata + bool auctionExists = _validateAuctionExistence( + auctionMetadata, + termAuctionId + ); + + require( + auctionMetadata.length > 1, + "Cannot invalidate the only auction result" + ); + // Revert if the auction doesn't exist + require(auctionExists, "Auction ID not found in metadata"); + + // Update the rate invalidation status + rateInvalid[repoToken][termAuctionId] = isInvalid; + } + + /** + * @notice Sets the term controller + * @param termController The address of the term controller + */ + function setTermController( + address termController + ) external onlyRole(ORACLE_ROLE) { + prevTermController = currTermController; + currTermController = ITermController(termController); + } + + /** + * @notice Set the repo redemption haircut + * @param repoToken The address of the repo token + * @param haircut The repo redemption haircut in 18 decimals + */ + function setRepoRedemptionHaircut( + address repoToken, + uint256 haircut + ) external onlyRole(ORACLE_ROLE) { + repoRedemptionHaircut[repoToken] = haircut; + } + + function _identifyTermController( + address termRepoToken + ) internal view returns (ITermController) { + if (currTermController.isTermDeployed(termRepoToken)) { + return currTermController; + } else if (prevTermController.isTermDeployed(termRepoToken)) { + return prevTermController; + } else { + revert("Term controller not found"); + } + } + + function _getDiscountRate( + ITermController termController, + address repoToken + ) internal view returns (uint256) { + (AuctionMetadata[] memory auctionMetadata, ) = termController + .getTermAuctionResults(ITermRepoToken(repoToken).termRepoId()); + + uint256 len = auctionMetadata.length; + require(len > 0, "No auctions found"); + + // If there is a re-opening auction, e.g. 2 or more results for the same token + if (len > 1) { + uint256 latestAuctionTime = auctionMetadata[len - 1] + .auctionClearingBlockTimestamp; + if ((block.timestamp - latestAuctionTime) < 30 minutes) { + for (int256 i = int256(len) - 2; i >= 0; i--) { + if ( + !rateInvalid[repoToken][ + auctionMetadata[uint256(i)].termAuctionId + ] + ) { + return auctionMetadata[uint256(i)].auctionClearingRate; + } + } + } else { + for (int256 i = int256(len) - 1; i >= 0; i--) { + if ( + !rateInvalid[repoToken][ + auctionMetadata[uint256(i)].termAuctionId + ] + ) { + return auctionMetadata[uint256(i)].auctionClearingRate; + } + } + } + revert("No valid auction rate found"); + } + + // If there is only 1 result (not a re-opening) then always return result + return auctionMetadata[0].auctionClearingRate; + } + + function _validateAuctionExistence( + AuctionMetadata[] memory auctionMetadata, + bytes32 termAuctionId + ) private pure returns (bool auctionExists) { + // Check if the termAuctionId exists in the metadata + for (uint256 i = 0; i < auctionMetadata.length; i++) { + if (auctionMetadata[i].termAuctionId == termAuctionId) { + auctionExists = true; + break; + } + } + } +} diff --git a/src/TermVaultEventEmitter.sol b/src/TermVaultEventEmitter.sol index e74d3695..cd06bf73 100644 --- a/src/TermVaultEventEmitter.sol +++ b/src/TermVaultEventEmitter.sol @@ -6,8 +6,12 @@ import "@openzeppelin/contracts-upgradeable/contracts/access/AccessControlUpgrad import "@openzeppelin/contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; -contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlUpgradeable, ITermVaultEvents { - +contract TermVaultEventEmitter is + Initializable, + UUPSUpgradeable, + AccessControlUpgradeable, + ITermVaultEvents +{ bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant DEVOPS_ROLE = keccak256("DEVOPS_ROLE"); bytes32 public constant VAULT_CONTRACT = keccak256("VAULT_CONTRACT"); @@ -22,8 +26,7 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU function initialize( address adminWallet_, address devopsWallet_ - ) - external initializer { + ) external initializer { UUPSUpgradeable.__UUPSUpgradeable_init(); AccessControlUpgradeable.__AccessControl_init(); @@ -31,35 +34,55 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU _grantRole(DEVOPS_ROLE, devopsWallet_); } - function pairVaultContract(address vaultContract) external onlyRole(ADMIN_ROLE){ + function pairVaultContract( + address vaultContract + ) external onlyRole(ADMIN_ROLE) { _grantRole(VAULT_CONTRACT, vaultContract); emit VaultContractPaired(vaultContract); } - function emitTermControllerUpdated(address oldController, address newController) external onlyRole(VAULT_CONTRACT) { + function emitTermControllerUpdated( + address oldController, + address newController + ) external onlyRole(VAULT_CONTRACT) { emit TermControllerUpdated(oldController, newController); } - function emitTimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold) external onlyRole(VAULT_CONTRACT) { + function emitTimeToMaturityThresholdUpdated( + uint256 oldThreshold, + uint256 newThreshold + ) external onlyRole(VAULT_CONTRACT) { emit TimeToMaturityThresholdUpdated(oldThreshold, newThreshold); } - function emitRequiredReserveRatioUpdated(uint256 oldThreshold, uint256 newThreshold) external onlyRole(VAULT_CONTRACT) { + function emitRequiredReserveRatioUpdated( + uint256 oldThreshold, + uint256 newThreshold + ) external onlyRole(VAULT_CONTRACT) { emit RequiredReserveRatioUpdated(oldThreshold, newThreshold); } - function emitDiscountRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup) external onlyRole(VAULT_CONTRACT) { + function emitDiscountRateMarkupUpdated( + uint256 oldMarkup, + uint256 newMarkup + ) external onlyRole(VAULT_CONTRACT) { emit DiscountRateMarkupUpdated(oldMarkup, newMarkup); } - function emitMinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio) external onlyRole(VAULT_CONTRACT) { + function emitMinCollateralRatioUpdated( + address collateral, + uint256 minCollateralRatio + ) external onlyRole(VAULT_CONTRACT) { emit MinCollateralRatioUpdated(collateral, minCollateralRatio); } - function emitRepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit) external onlyRole(VAULT_CONTRACT) { + function emitRepoTokenConcentrationLimitUpdated( + uint256 oldLimit, + uint256 newLimit + ) external onlyRole(VAULT_CONTRACT) { emit RepoTokenConcentrationLimitUpdated(oldLimit, newLimit); } - + function emitDepositPaused() external onlyRole(VAULT_CONTRACT) { emit DepositPaused(); } @@ -83,11 +106,16 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU emit DiscountRateAdapterUpdated(oldAdapter, newAdapter); } - function emitRepoTokenBlacklistUpdated(address repoToken, bool blacklisted) external onlyRole(VAULT_CONTRACT) { + function emitRepoTokenBlacklistUpdated( + address repoToken, + bool blacklisted + ) external onlyRole(VAULT_CONTRACT) { emit RepoTokenBlacklistUpdated(repoToken, blacklisted); } - function emitNewGovernor(address newGovernor) external onlyRole(VAULT_CONTRACT) { + function emitNewGovernor( + address newGovernor + ) external onlyRole(VAULT_CONTRACT) { emit NewGovernor(newGovernor); } diff --git a/src/interfaces/term/ITermAuction.sol b/src/interfaces/term/ITermAuction.sol index 875a614a..e878b2d4 100644 --- a/src/interfaces/term/ITermAuction.sol +++ b/src/interfaces/term/ITermAuction.sol @@ -1,14 +1,14 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -interface ITermAuction { - function termAuctionOfferLocker() external view returns (address); - - function termRepoId() external view returns (bytes32); - - function auctionEndTime() external view returns (uint256); - - function auctionCompleted() external view returns (bool); - - function auctionCancelledForWithdrawal() external view returns (bool); -} \ No newline at end of file +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +interface ITermAuction { + function termAuctionOfferLocker() external view returns (address); + + function termRepoId() external view returns (bytes32); + + function auctionEndTime() external view returns (uint256); + + function auctionCompleted() external view returns (bool); + + function auctionCancelledForWithdrawal() external view returns (bool); +} diff --git a/src/interfaces/term/ITermAuctionOfferLocker.sol b/src/interfaces/term/ITermAuctionOfferLocker.sol index 300aeb21..49cc0355 100644 --- a/src/interfaces/term/ITermAuctionOfferLocker.sol +++ b/src/interfaces/term/ITermAuctionOfferLocker.sol @@ -62,7 +62,9 @@ interface ITermAuctionOfferLocker { function termRepoServicer() external view returns (address); - function lockedOffer(bytes32 id) external view returns (TermAuctionOffer memory); + function lockedOffer( + bytes32 id + ) external view returns (TermAuctionOffer memory); /// @param offerSubmissions An array of offer submissions /// @return A bytes32 array of unique on chain offer ids. diff --git a/src/interfaces/term/ITermController.sol b/src/interfaces/term/ITermController.sol index e121fd11..2194b41f 100644 --- a/src/interfaces/term/ITermController.sol +++ b/src/interfaces/term/ITermController.sol @@ -1,16 +1,23 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -struct AuctionMetadata { - bytes32 termAuctionId; - uint256 auctionClearingRate; - uint256 auctionClearingBlockTimestamp; -} - -interface ITermController { - function isTermDeployed(address contractAddress) external view returns (bool); - - function getProtocolReserveAddress() external view returns (address); - - function getTermAuctionResults(bytes32 termRepoId) external view returns (AuctionMetadata[] memory auctionMetadata, uint8 numOfAuctions); -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +struct AuctionMetadata { + bytes32 termAuctionId; + uint256 auctionClearingRate; + uint256 auctionClearingBlockTimestamp; +} + +interface ITermController { + function isTermDeployed( + address contractAddress + ) external view returns (bool); + + function getProtocolReserveAddress() external view returns (address); + + function getTermAuctionResults( + bytes32 termRepoId + ) + external + view + returns (AuctionMetadata[] memory auctionMetadata, uint8 numOfAuctions); +} diff --git a/src/interfaces/term/ITermDiscountRateAdapter.sol b/src/interfaces/term/ITermDiscountRateAdapter.sol index be35f005..688e52ad 100644 --- a/src/interfaces/term/ITermDiscountRateAdapter.sol +++ b/src/interfaces/term/ITermDiscountRateAdapter.sol @@ -1,10 +1,13 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {ITermController} from "./ITermController.sol"; -interface ITermDiscountRateAdapter { - function currTermController() external view returns (ITermController); - function repoRedemptionHaircut(address) external view returns (uint256); - function getDiscountRate(address repoToken) external view returns (uint256); - function getDiscountRate(address termController, address repoToken) external view returns (uint256); -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ITermController} from "./ITermController.sol"; +interface ITermDiscountRateAdapter { + function currTermController() external view returns (ITermController); + function repoRedemptionHaircut(address) external view returns (uint256); + function getDiscountRate(address repoToken) external view returns (uint256); + function getDiscountRate( + address termController, + address repoToken + ) external view returns (uint256); +} diff --git a/src/interfaces/term/ITermRepoCollateralManager.sol b/src/interfaces/term/ITermRepoCollateralManager.sol index 63af88a3..1a983d51 100644 --- a/src/interfaces/term/ITermRepoCollateralManager.sol +++ b/src/interfaces/term/ITermRepoCollateralManager.sol @@ -1,12 +1,12 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -interface ITermRepoCollateralManager { - function maintenanceCollateralRatios( - address - ) external view returns (uint256); - - function numOfAcceptedCollateralTokens() external view returns (uint8); - - function collateralTokens(uint256 index) external view returns (address); -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +interface ITermRepoCollateralManager { + function maintenanceCollateralRatios( + address + ) external view returns (uint256); + + function numOfAcceptedCollateralTokens() external view returns (uint8); + + function collateralTokens(uint256 index) external view returns (address); +} diff --git a/src/interfaces/term/ITermRepoServicer.sol b/src/interfaces/term/ITermRepoServicer.sol index 99c25804..09529a2e 100644 --- a/src/interfaces/term/ITermRepoServicer.sol +++ b/src/interfaces/term/ITermRepoServicer.sol @@ -1,15 +1,15 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -interface ITermRepoServicer { - function redeemTermRepoTokens( - address redeemer, - uint256 amountToRedeem - ) external; - - function termRepoToken() external view returns (address); - - function termRepoLocker() external view returns (address); - - function purchaseToken() external view returns (address); -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +interface ITermRepoServicer { + function redeemTermRepoTokens( + address redeemer, + uint256 amountToRedeem + ) external; + + function termRepoToken() external view returns (address); + + function termRepoLocker() external view returns (address); + + function purchaseToken() external view returns (address); +} diff --git a/src/interfaces/term/ITermRepoToken.sol b/src/interfaces/term/ITermRepoToken.sol index 531c995a..040b88ec 100644 --- a/src/interfaces/term/ITermRepoToken.sol +++ b/src/interfaces/term/ITermRepoToken.sol @@ -1,17 +1,20 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -interface ITermRepoToken is IERC20 { - function redemptionValue() external view returns (uint256); - - function config() external view returns ( - uint256 redemptionTimestamp, - address purchaseToken, - address termRepoServicer, - address termRepoCollateralManager - ); - - function termRepoId() external view returns (bytes32); -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface ITermRepoToken is IERC20 { + function redemptionValue() external view returns (uint256); + + function config() + external + view + returns ( + uint256 redemptionTimestamp, + address purchaseToken, + address termRepoServicer, + address termRepoCollateralManager + ); + + function termRepoId() external view returns (bytes32); +} diff --git a/src/interfaces/term/ITermVaultEvents.sol b/src/interfaces/term/ITermVaultEvents.sol index cbb4a7f5..851cf476 100644 --- a/src/interfaces/term/ITermVaultEvents.sol +++ b/src/interfaces/term/ITermVaultEvents.sol @@ -1,72 +1,103 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -interface ITermVaultEvents { - event VaultContractPaired(address vault); - - event TermControllerUpdated(address oldController, address newController); - - event TimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold); - - event RequiredReserveRatioUpdated(uint256 oldThreshold, uint256 newThreshold); - - event DiscountRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup); - - event MinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio); - - event RepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit); - - - event DepositPaused(); - - event DepositUnpaused(); - - /* - event StrategyPaused(); - - event StrategyUnpaused(); - */ - - event DiscountRateAdapterUpdated( - address indexed oldAdapter, - address indexed newAdapter - ); - - event RepoTokenBlacklistUpdated( - address indexed repoToken, - bool blacklisted - ); - - event NewGovernor(address newGovernor); - - function emitTermControllerUpdated(address oldController, address newController) external; - - function emitTimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold) external; - - function emitRequiredReserveRatioUpdated(uint256 oldThreshold, uint256 newThreshold) external; - - function emitDiscountRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup) external; - - function emitMinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio) external; - - function emitRepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit) external; - - - function emitDepositPaused() external; - - function emitDepositUnpaused() external; - /* - - function emitStrategyPaused() external; - - function emitStrategyUnpaused() external;*/ - - function emitDiscountRateAdapterUpdated( - address oldAdapter, - address newAdapter - ) external; - - function emitRepoTokenBlacklistUpdated(address repoToken, bool blacklisted) external; - - function emitNewGovernor(address newGovernor) external; -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +interface ITermVaultEvents { + event VaultContractPaired(address vault); + + event TermControllerUpdated(address oldController, address newController); + + event TimeToMaturityThresholdUpdated( + uint256 oldThreshold, + uint256 newThreshold + ); + + event RequiredReserveRatioUpdated( + uint256 oldThreshold, + uint256 newThreshold + ); + + event DiscountRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup); + + event MinCollateralRatioUpdated( + address collateral, + uint256 minCollateralRatio + ); + + event RepoTokenConcentrationLimitUpdated( + uint256 oldLimit, + uint256 newLimit + ); + + event DepositPaused(); + + event DepositUnpaused(); + + /* + event StrategyPaused(); + + event StrategyUnpaused(); + */ + + event DiscountRateAdapterUpdated( + address indexed oldAdapter, + address indexed newAdapter + ); + + event RepoTokenBlacklistUpdated( + address indexed repoToken, + bool blacklisted + ); + + event NewGovernor(address newGovernor); + + function emitTermControllerUpdated( + address oldController, + address newController + ) external; + + function emitTimeToMaturityThresholdUpdated( + uint256 oldThreshold, + uint256 newThreshold + ) external; + + function emitRequiredReserveRatioUpdated( + uint256 oldThreshold, + uint256 newThreshold + ) external; + + function emitDiscountRateMarkupUpdated( + uint256 oldMarkup, + uint256 newMarkup + ) external; + + function emitMinCollateralRatioUpdated( + address collateral, + uint256 minCollateralRatio + ) external; + + function emitRepoTokenConcentrationLimitUpdated( + uint256 oldLimit, + uint256 newLimit + ) external; + + function emitDepositPaused() external; + + function emitDepositUnpaused() external; + /* + + function emitStrategyPaused() external; + + function emitStrategyUnpaused() external;*/ + + function emitDiscountRateAdapterUpdated( + address oldAdapter, + address newAdapter + ) external; + + function emitRepoTokenBlacklistUpdated( + address repoToken, + bool blacklisted + ) external; + + function emitNewGovernor(address newGovernor) external; +} diff --git a/src/test/TestUSDCIntegration.t.sol b/src/test/TestUSDCIntegration.t.sol index f4f099ea..4522e2e1 100644 --- a/src/test/TestUSDCIntegration.t.sol +++ b/src/test/TestUSDCIntegration.t.sol @@ -14,14 +14,13 @@ import {TermDiscountRateAdapter} from "../TermDiscountRateAdapter.sol"; import {RepoTokenList} from "../RepoTokenList.sol"; import "../TermAuctionList.sol"; - contract TestUSDCIntegration is Setup { uint256 internal constant TEST_REPO_TOKEN_RATE = 0.05e18; uint256 public constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; uint256 public constant RATE_PRECISION = 1e18; MockUSDC internal mockUSDC; - ERC20Mock internal mockCollateral; + ERC20Mock internal mockCollateral; MockTermRepoToken internal repoToken1Week; MockTermRepoToken internal repoToken1Month; MockTermRepoToken internal repoToken1Year; @@ -39,21 +38,42 @@ contract TestUSDCIntegration is Setup { _setUp(ERC20(address(mockUSDC))); repoToken1Week = new MockTermRepoToken( - bytes32("test repo token 1"), address(mockUSDC), address(mockCollateral), 1e18, 1 weeks - ); + bytes32("test repo token 1"), + address(mockUSDC), + address(mockCollateral), + 1e18, + 1 weeks + ); repoToken1Month = new MockTermRepoToken( - bytes32("test repo token 2"), address(mockUSDC), address(mockCollateral), 1e18, 4 weeks - ); + bytes32("test repo token 2"), + address(mockUSDC), + address(mockCollateral), + 1e18, + 4 weeks + ); repoToken1Year = new MockTermRepoToken( - bytes32("test repo token 4"), address(mockUSDC), address(mockCollateral), 1e18, 48 weeks - ); + bytes32("test repo token 4"), + address(mockUSDC), + address(mockCollateral), + 1e18, + 48 weeks + ); repoTokenMatured = new MockTermRepoToken( - bytes32("test repo token 3"), address(mockUSDC), address(mockCollateral), 1e18, block.timestamp - 1 + bytes32("test repo token 3"), + address(mockUSDC), + address(mockCollateral), + 1e18, + block.timestamp - 1 ); - termController.setOracleRate(MockTermRepoToken(repoToken1Week).termRepoId(), TEST_REPO_TOKEN_RATE); - termController.setOracleRate(MockTermRepoToken(repoToken1Month).termRepoId(), TEST_REPO_TOKEN_RATE); - + termController.setOracleRate( + MockTermRepoToken(repoToken1Week).termRepoId(), + TEST_REPO_TOKEN_RATE + ); + termController.setOracleRate( + MockTermRepoToken(repoToken1Month).termRepoId(), + TEST_REPO_TOKEN_RATE + ); termStrategy = Strategy(address(strategy)); @@ -76,25 +96,38 @@ contract TestUSDCIntegration is Setup { initialState.totalLiquidBalance = termStrategy.totalLiquidBalance(); } - function _submitOffer(bytes32 idHash, uint256 offerAmount, MockTermAuction auction, MockTermRepoToken repoToken) private returns (bytes32) { + function _submitOffer( + bytes32 idHash, + uint256 offerAmount, + MockTermAuction auction, + MockTermRepoToken repoToken + ) private returns (bytes32) { // test: only management can submit offers vm.expectRevert("!management"); bytes32[] memory offerIds = termStrategy.submitAuctionOffer( - auction, address(repoToken), idHash, bytes32("test price"), offerAmount - ); + auction, + address(repoToken), + idHash, + bytes32("test price"), + offerAmount + ); vm.prank(management); offerIds = termStrategy.submitAuctionOffer( - auction, address(repoToken), idHash, bytes32("test price"), offerAmount - ); + auction, + address(repoToken), + idHash, + bytes32("test price"), + offerAmount + ); assertEq(offerIds.length, 1); return offerIds[0]; } - function testSellRepoTokenSubmitOfferAndCloseAuction() public { - address testUser = vm.addr(0x11111); + function testSellRepoTokenSubmitOfferAndCloseAuction() public { + address testUser = vm.addr(0x11111); mockUSDC.mint(testUser, 1e18); repoToken1Month.mint(testUser, 1000e18); @@ -110,19 +143,28 @@ contract TestUSDCIntegration is Setup { assertEq(holdings.length, 1); - - bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6, repoToken1WeekAuction, repoToken1Week); + bytes32 offerId1 = _submitOffer( + bytes32("offer id hash 1"), + 1e6, + repoToken1WeekAuction, + repoToken1Week + ); bytes32[] memory offerIds = new bytes32[](1); offerIds[0] = offerId1; uint256[] memory fillAmounts = new uint256[](1); fillAmounts[0] = 1e6; uint256[] memory repoTokenAmounts = new uint256[](1); repoTokenAmounts[0] = _getRepoTokenAmountGivenPurchaseTokenAmount( - 1e6, repoToken1Week, TEST_REPO_TOKEN_RATE + 1e6, + repoToken1Week, + TEST_REPO_TOKEN_RATE ); - - repoToken1WeekAuction.auctionSuccess(offerIds, fillAmounts, repoTokenAmounts); + repoToken1WeekAuction.auctionSuccess( + offerIds, + fillAmounts, + repoTokenAmounts + ); holdings = termStrategy.repoTokenHoldings(); @@ -132,20 +174,28 @@ contract TestUSDCIntegration is Setup { termStrategy.auctionClosed(); holdings = termStrategy.repoTokenHoldings(); assertEq(holdings.length, 2); - (uint256 holdings0Maturity, , ,) = MockTermRepoToken(holdings[0]).config(); - (uint256 holdings1Maturity, , ,) = MockTermRepoToken(holdings[1]).config(); + (uint256 holdings0Maturity, , , ) = MockTermRepoToken(holdings[0]) + .config(); + (uint256 holdings1Maturity, , , ) = MockTermRepoToken(holdings[1]) + .config(); assertTrue(holdings0Maturity <= holdings1Maturity); - bytes32[] memory offers = termStrategy.pendingOffers(); + bytes32[] memory offers = termStrategy.pendingOffers(); assertEq(offers.length, 0); - assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - 1e6); + assertEq( + termStrategy.totalLiquidBalance(), + initialState.totalLiquidBalance - 1e6 + ); // test: totalAssetValue = total liquid balance + pending offer amount - assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + 1e6); + assertEq( + termStrategy.totalAssetValue(), + termStrategy.totalLiquidBalance() + 1e6 + ); } - function testSubmittingOffersToMultipleAuctions() public { - address testUser = vm.addr(0x11111); + function testSubmittingOffersToMultipleAuctions() public { + address testUser = vm.addr(0x11111); mockUSDC.mint(testUser, 1e18); repoToken1Month.mint(testUser, 1000e18); @@ -157,11 +207,26 @@ contract TestUSDCIntegration is Setup { mockYearnVault.withdraw(1e18, testUser, testUser); vm.stopPrank(); - _submitOffer(bytes32("offer id hash 4"), 1e6, repoToken1YearAuction, repoToken1Year); + _submitOffer( + bytes32("offer id hash 4"), + 1e6, + repoToken1YearAuction, + repoToken1Year + ); - _submitOffer(bytes32("offer id hash 1"), 1e6, repoToken1WeekAuction, repoToken1Week); + _submitOffer( + bytes32("offer id hash 1"), + 1e6, + repoToken1WeekAuction, + repoToken1Week + ); - _submitOffer(bytes32("offer id hash 2"), 1e6, repoToken1MonthAuction, repoToken1Month); + _submitOffer( + bytes32("offer id hash 2"), + 1e6, + repoToken1MonthAuction, + repoToken1Month + ); bytes32[] memory offers = termStrategy.pendingOffers(); @@ -174,22 +239,22 @@ contract TestUSDCIntegration is Setup { for (uint256 i = 0; i < offers.length - 1; i++) { bytes32 offerSlot1 = keccak256(abi.encode(offers[i], 7)); - bytes32 offerSlot2 = keccak256(abi.encode(offers[i+1], 7)); + bytes32 offerSlot2 = keccak256(abi.encode(offers[i + 1], 7)); offer1 = vm.load(address(termStrategy), offerSlot1); offer2 = vm.load(address(termStrategy), offerSlot2); termAuction1 = address(uint160(uint256(offer1) >> 64)); termAuction2 = address(uint160(uint256(offer2) >> 64)); if (termAuction1 > termAuction2) { - isSorted=false; + isSorted = false; break; } } assertTrue(isSorted); } - function testRemovingMaturedTokensWithRedemptionAttempt() public { - address testUser = vm.addr(0x11111); + function testRemovingMaturedTokensWithRedemptionAttempt() public { + address testUser = vm.addr(0x11111); mockUSDC.mint(testUser, 1e18); repoToken1Month.mint(testUser, 1000e18); @@ -203,7 +268,6 @@ contract TestUSDCIntegration is Setup { address[] memory holdings = termStrategy.repoTokenHoldings(); assertEq(holdings.length, 1); - vm.warp(block.timestamp + 5 weeks); termStrategy.auctionClosed(); @@ -213,22 +277,32 @@ contract TestUSDCIntegration is Setup { } function testSimulateTransactionWithNonTermDeployedToken() public { - address testUser = vm.addr(0x11111); + address testUser = vm.addr(0x11111); vm.prank(management); termController.markNotTermDeployed(address(repoToken1Week)); vm.stopPrank(); - vm.prank(testUser); - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(repoToken1Week))); + vm.prank(testUser); + vm.expectRevert( + abi.encodeWithSelector( + RepoTokenList.InvalidRepoToken.selector, + address(repoToken1Week) + ) + ); termStrategy.simulateTransaction(address(repoToken1Week), 1e6); } function testSimulateTransactionWithInvalidToken() public { - address testUser = vm.addr(0x11111); + address testUser = vm.addr(0x11111); - vm.prank(testUser); - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(repoTokenMatured))); + vm.prank(testUser); + vm.expectRevert( + abi.encodeWithSelector( + RepoTokenList.InvalidRepoToken.selector, + address(repoTokenMatured) + ) + ); termStrategy.simulateTransaction(address(repoTokenMatured), 1e6); } @@ -253,10 +327,17 @@ contract TestUSDCIntegration is Setup { termStrategy.setTimeToMaturityThreshold(3 weeks); vm.stopPrank(); - vm.startPrank(testUser); + vm.startPrank(testUser); repoToken1Month.mint(testUser, 1000e18); repoToken1Month.approve(address(strategy), type(uint256).max); - (uint256 simulatedWeightedMaturity, uint256 simulatedRepoTokenConcentrationRatio, uint256 simulatedLiquidityRatio) = termStrategy.simulateTransaction(address(repoToken1Month), repoTokenSellAmount); + ( + uint256 simulatedWeightedMaturity, + uint256 simulatedRepoTokenConcentrationRatio, + uint256 simulatedLiquidityRatio + ) = termStrategy.simulateTransaction( + address(repoToken1Month), + repoTokenSellAmount + ); assertApproxEq(simulatedWeightedMaturity, 1.25 weeks, 1); assertEq(simulatedRepoTokenConcentrationRatio, 0.25e18); assertEq(simulatedLiquidityRatio, 0.5e18); @@ -268,10 +349,14 @@ contract TestUSDCIntegration is Setup { vm.prank(management); termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), bytes32("offer 1"), bytes32("test price"), 1e6 - ); + repoToken1WeekAuction, + address(repoToken1Week), + bytes32("offer 1"), + bytes32("test price"), + 1e6 + ); - repoToken1WeekAuction.auctionCancelForWithdrawal(); + repoToken1WeekAuction.auctionCancelForWithdrawal(); vm.startPrank(testUser); repoToken1Month.mint(testUser, 1000e18); @@ -286,22 +371,35 @@ contract TestUSDCIntegration is Setup { vm.prank(management); bytes32[] memory offerIds = termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), bytes32("offer 1"), bytes32("test price"), 1e6 - ); + repoToken1WeekAuction, + address(repoToken1Week), + bytes32("offer 1"), + bytes32("test price"), + 1e6 + ); - repoToken1WeekAuction.auctionCancelForWithdrawal(); + repoToken1WeekAuction.auctionCancelForWithdrawal(); vm.startPrank(testUser); repoToken1Month.mint(testUser, 1000e18); repoToken1Month.approve(address(strategy), type(uint256).max); - vm.mockCall(repoToken1WeekAuction.termAuctionOfferLocker(), abi.encodeWithSelector(MockTermAuctionOfferLocker.unlockOffers.selector, offerIds), abi.encodeWithSelector(MockTermAuctionOfferLocker.OfferUnlockingFailed.selector)); + vm.mockCall( + repoToken1WeekAuction.termAuctionOfferLocker(), + abi.encodeWithSelector( + MockTermAuctionOfferLocker.unlockOffers.selector, + offerIds + ), + abi.encodeWithSelector( + MockTermAuctionOfferLocker.OfferUnlockingFailed.selector + ) + ); termStrategy.sellRepoToken(address(repoToken1Month), 1e6); bytes32[] memory pendingOffers = termStrategy.pendingOffers(); assertEq(1, pendingOffers.length); } function testRepoTokenBlacklist() public { - address testUser = vm.addr(0x11111); + address testUser = vm.addr(0x11111); vm.prank(testUser); vm.expectRevert(); termStrategy.setRepoTokenBlacklist(address(repoToken1Week), true); @@ -311,13 +409,18 @@ contract TestUSDCIntegration is Setup { termStrategy.setRepoTokenBlacklist(address(repoToken1Week), true); vm.stopPrank(); - vm.prank(testUser); - vm.expectRevert(abi.encodeWithSelector(Strategy.RepoTokenBlacklisted.selector, address(repoToken1Week))); + vm.prank(testUser); + vm.expectRevert( + abi.encodeWithSelector( + Strategy.RepoTokenBlacklisted.selector, + address(repoToken1Week) + ) + ); termStrategy.sellRepoToken(address(repoToken1Week), 1e6); } function testPauses() public { - address testUser = vm.addr(0x11111); + address testUser = vm.addr(0x11111); mockUSDC.mint(testUser, 1e18); vm.prank(testUser); vm.expectRevert(); @@ -334,7 +437,9 @@ contract TestUSDCIntegration is Setup { mockUSDC.approve(address(termStrategy), 1e6); vm.prank(testUser); - vm.expectRevert(abi.encodeWithSelector(Strategy.DepositPaused.selector)); + vm.expectRevert( + abi.encodeWithSelector(Strategy.DepositPaused.selector) + ); IERC4626(address(termStrategy)).deposit(1e6, testUser); vm.stopPrank(); @@ -348,10 +453,16 @@ contract TestUSDCIntegration is Setup { } function testSetDiscountRateAdapter() public { - address testUser = vm.addr(0x11111); + address testUser = vm.addr(0x11111); - TermDiscountRateAdapter invalid = new TermDiscountRateAdapter(address(0), adminWallet); - TermDiscountRateAdapter valid = new TermDiscountRateAdapter(address(termController), adminWallet); + TermDiscountRateAdapter invalid = new TermDiscountRateAdapter( + address(0), + adminWallet + ); + TermDiscountRateAdapter valid = new TermDiscountRateAdapter( + address(termController), + adminWallet + ); vm.prank(testUser); vm.expectRevert(); @@ -367,26 +478,31 @@ contract TestUSDCIntegration is Setup { ( address assetVault, - address eventEmitter, - address governor, - ITermController prevTermController, - ITermController currTermController, - ITermDiscountRateAdapter discountRateAdapter, - uint256 timeToMaturityThreshold, - uint256 requiredReserveRatio, - uint256 discountRateMarkup, - uint256 repoTokenConcentrationLimit + address eventEmitter, + address governor, + ITermController prevTermController, + ITermController currTermController, + ITermDiscountRateAdapter discountRateAdapter, + uint256 timeToMaturityThreshold, + uint256 requiredReserveRatio, + uint256 discountRateMarkup, + uint256 repoTokenConcentrationLimit ) = termStrategy.strategyState(); - assertEq(address(valid), address(discountRateAdapter)); } function testSettingNewGovernor() public { - address testUser = vm.addr(0x11111); + address testUser = vm.addr(0x11111); - TermDiscountRateAdapter invalid = new TermDiscountRateAdapter(address(0), adminWallet); - TermDiscountRateAdapter valid = new TermDiscountRateAdapter(address(termController), adminWallet); + TermDiscountRateAdapter invalid = new TermDiscountRateAdapter( + address(0), + adminWallet + ); + TermDiscountRateAdapter valid = new TermDiscountRateAdapter( + address(termController), + adminWallet + ); vm.prank(testUser); vm.expectRevert(); @@ -418,18 +534,17 @@ contract TestUSDCIntegration is Setup { ( address assetVault, - address eventEmitter, - address governor, - ITermController prevTermController, - ITermController currTermController, - ITermDiscountRateAdapter discountRateAdapter, - uint256 timeToMaturityThreshold, - uint256 requiredReserveRatio, - uint256 discountRateMarkup, - uint256 repoTokenConcentrationLimit + address eventEmitter, + address governor, + ITermController prevTermController, + ITermController currTermController, + ITermDiscountRateAdapter discountRateAdapter, + uint256 timeToMaturityThreshold, + uint256 requiredReserveRatio, + uint256 discountRateMarkup, + uint256 repoTokenConcentrationLimit ) = termStrategy.strategyState(); - assertEq(address(valid), address(discountRateAdapter)); } @@ -438,19 +553,25 @@ contract TestUSDCIntegration is Setup { MockTermRepoToken termRepoToken, uint256 discountRate ) private view returns (uint256) { - (uint256 redemptionTimestamp, address purchaseToken, ,) = termRepoToken.config(); + (uint256 redemptionTimestamp, address purchaseToken, , ) = termRepoToken + .config(); - uint256 purchaseTokenPrecision = 10**ERC20(purchaseToken).decimals(); - uint256 repoTokenPrecision = 10**ERC20(address(termRepoToken)).decimals(); + uint256 purchaseTokenPrecision = 10 ** ERC20(purchaseToken).decimals(); + uint256 repoTokenPrecision = 10 ** + ERC20(address(termRepoToken)).decimals(); - uint256 timeLeftToMaturityDayFraction = - ((redemptionTimestamp - block.timestamp) * purchaseTokenPrecision) / THREESIXTY_DAYCOUNT_SECONDS; + 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; + uint256 repoTokenAmountInBaseAssetPrecision = (purchaseTokenAmount * + (purchaseTokenPrecision + + ((discountRate * timeLeftToMaturityDayFraction) / + RATE_PRECISION))) / purchaseTokenPrecision; + + return + (repoTokenAmountInBaseAssetPrecision * repoTokenPrecision) / + purchaseTokenPrecision; } - } diff --git a/src/test/TestUSDCOffers.t.sol b/src/test/TestUSDCOffers.t.sol index e717d2a3..010c454c 100644 --- a/src/test/TestUSDCOffers.t.sol +++ b/src/test/TestUSDCOffers.t.sol @@ -1,444 +1,630 @@ -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"; -import {RepoTokenList} from "../RepoTokenList.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; - MockTermRepoToken internal repoToken100Week; - MockTermAuction internal repoToken100WeekAuction; - 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 - ); - repoToken100Week = new MockTermRepoToken( - bytes32("test repo token 100"), address(mockUSDC), address(mockCollateral), 1e18, 100 weeks - ); - termController.setOracleRate(MockTermRepoToken(repoToken1Week).termRepoId(), TEST_REPO_TOKEN_RATE); - - termStrategy = Strategy(address(strategy)); - - repoToken1WeekAuction = new MockTermAuction(repoToken1Week); - repoToken100WeekAuction = new MockTermAuction(repoToken100Week); - - vm.startPrank(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); - termStrategy.setTimeToMaturityThreshold(3 weeks); - termStrategy.setRepoTokenConcentrationLimit(1e18); - termStrategy.setRequiredReserveRatio(0); - termStrategy.setDiscountRateMarkup(0); - vm.stopPrank(); - - // start with some initial funds - mockUSDC.mint(address(strategy), 100e6); - - assertEq(termStrategy.liquidReserveRatio(), 1e18); - - 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( - repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount - ); - - vm.prank(management); - offerIds = termStrategy.submitAuctionOffer( - 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); - - assertEq(termStrategy.liquidReserveRatio(), 0.99e18); - - uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 1e6); - } - - function testSubmitOfferBelowLiquidReserveRatio() public { - vm.startPrank(governor); - termStrategy.setRequiredReserveRatio(0.5e18); - vm.stopPrank(); - - vm.startPrank(management); - vm.expectRevert(abi.encodeWithSelector(Strategy.BalanceBelowRequiredReserveRatio.selector)); - termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), bytes32("offer id hash 1"), bytes32("test price"), 51e6 - ); - } - - function testSubmitOfferToInvalidAuction() public { - vm.startPrank(management); - termController.markNotTermDeployed(address(repoToken1WeekAuction)); - vm.expectRevert(abi.encodeWithSelector(Strategy.InvalidTermAuction.selector, address(repoToken1WeekAuction))); - termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), bytes32("offer id hash 1"), bytes32("test price"), 1e6 - ); - } - - function testSubmitOfferToAuctionWithInvalidRepoToken() public { - vm.startPrank(management); - termController.markNotTermDeployed(address(repoToken1Week)); - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(repoToken1Week))); - termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), bytes32("offer id hash 1"), bytes32("test price"), 1e6 - ); - } - - function testSubmitOfferWithoutEnoughLiquidity() public { - vm.prank(management); - vm.expectRevert(abi.encodeWithSelector(Strategy.InsufficientLiquidBalance.selector, 100e6, 101e6)); - termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), bytes32("offer id hash 1"), bytes32("test price"), 101e6 - ); - } - - function testSubmitOfferWithExcessiveWeightedTimeMaturity() public { - vm.prank(management); - vm.expectRevert(abi.encodeWithSelector(Strategy.TimeToMaturityAboveThreshold.selector)); - termStrategy.submitAuctionOffer( - repoToken100WeekAuction, address(repoToken100Week), bytes32("offer id hash 1"), bytes32("test price"), 10e6 - ); - } - - function testSubmitOfferFailsIfMinCollatRatioisZero() public { - vm.startPrank(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0); - vm.stopPrank(); - - vm.startPrank(management); - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(repoToken1Week))); - termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), bytes32("offer id hash 1"), bytes32("test price"), 1e6 - ); - } - - function testEditOffer() public { - bytes32 idHash1 = bytes32("offer id hash 1"); - bytes32 offerId1 = _submitOffer(idHash1, 1e6); - - // TODO: fuzz this - uint256 offerAmount = 4e6; - - assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - 1e6); - uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 1e6); - assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + 1e6); - assertEq(termStrategy.liquidReserveRatio(), 0.99e18); - - vm.prank(management); - bytes32[] memory offerIds = termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), offerId1, bytes32("test price"), offerAmount - ); - - assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - offerAmount); - repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 4e6); - assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + offerAmount); - assertEq(termStrategy.liquidReserveRatio(), 0.96e18); - } - - 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 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 0); - assertEq(termStrategy.liquidReserveRatio(), 1e18); - } - - function testDeleteOffersFromInvalidAuction() 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.startPrank(management); - termController.markNotTermDeployed(address(repoToken1WeekAuction)); - vm.expectRevert(abi.encodeWithSelector(Strategy.InvalidTermAuction.selector, address(repoToken1WeekAuction))); - termStrategy.deleteAuctionOffers(address(repoToken1WeekAuction), offerIds); - } - - uint256 public constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; - uint256 public constant RATE_PRECISION = 1e18; - - function _getRepoTokenAmountGivenPurchaseTokenAmount( - uint256 purchaseTokenAmount, - MockTermRepoToken termRepoToken, - uint256 auctionRate - ) 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 + (auctionRate * 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); - assertEq(termStrategy.liquidReserveRatio(), 0.99e18); - - uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 1e6); - - //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(); - assertEq(termStrategy.liquidReserveRatio(), 0.99e18); - - repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 1e6); - - // 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); - assertEq(termStrategy.liquidReserveRatio(), 0.99e18); - 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 - ); - - uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 1e6); - - repoToken1WeekAuction.auctionSuccess(offerIds, fillAmounts, repoTokenAmounts); - - assertEq(termStrategy.liquidReserveRatio(), 0.995e18); - - repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 0.5e6); - - // 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(); - - repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 0.5e6); - - // 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 testAuctionCancelForWithdrawal() public { - bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); - - uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 1e6); - - repoToken1WeekAuction.auctionCancelForWithdrawal(); - - repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 1e6); - - - // test: check value before calling complete auction - termStrategy.auctionClosed(); - - repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 0); - - bytes32[] memory offers = termStrategy.pendingOffers(); - - assertEq(offers.length, 0); - } - - function testAuctionCancel() public { - bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); - - uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 1e6); - - bytes32[] memory offerIds = new bytes32[](1); - offerIds[0] = offerId1; - - repoToken1WeekAuction.auctionCancel(offerIds); - - repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 0); - - // test: check value before calling complete auction - termStrategy.auctionClosed(); - - repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - assertEq(repoTokenHoldingValue, 0); - - 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], offerId1); - assertEq(offers[1], offerId2); - } - - 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(offerId1, 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(offerId1, 50e6); - - assertEq(termStrategy.totalLiquidBalance(), 50e6); - } -} +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"; +import {RepoTokenList} from "../RepoTokenList.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; + MockTermRepoToken internal repoToken100Week; + MockTermAuction internal repoToken100WeekAuction; + 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 + ); + repoToken100Week = new MockTermRepoToken( + bytes32("test repo token 100"), + address(mockUSDC), + address(mockCollateral), + 1e18, + 100 weeks + ); + termController.setOracleRate( + MockTermRepoToken(repoToken1Week).termRepoId(), + TEST_REPO_TOKEN_RATE + ); + + termStrategy = Strategy(address(strategy)); + + repoToken1WeekAuction = new MockTermAuction(repoToken1Week); + repoToken100WeekAuction = new MockTermAuction(repoToken100Week); + + vm.startPrank(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); + termStrategy.setTimeToMaturityThreshold(3 weeks); + termStrategy.setRepoTokenConcentrationLimit(1e18); + termStrategy.setRequiredReserveRatio(0); + termStrategy.setDiscountRateMarkup(0); + vm.stopPrank(); + + // start with some initial funds + mockUSDC.mint(address(strategy), 100e6); + + assertEq(termStrategy.liquidReserveRatio(), 1e18); + + 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( + repoToken1WeekAuction, + address(repoToken1Week), + idHash, + bytes32("test price"), + offerAmount + ); + + vm.prank(management); + offerIds = termStrategy.submitAuctionOffer( + 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 + ); + + assertEq(termStrategy.liquidReserveRatio(), 0.99e18); + + uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 1e6); + } + + function testSubmitOfferBelowLiquidReserveRatio() public { + vm.startPrank(governor); + termStrategy.setRequiredReserveRatio(0.5e18); + vm.stopPrank(); + + vm.startPrank(management); + vm.expectRevert( + abi.encodeWithSelector( + Strategy.BalanceBelowRequiredReserveRatio.selector + ) + ); + termStrategy.submitAuctionOffer( + repoToken1WeekAuction, + address(repoToken1Week), + bytes32("offer id hash 1"), + bytes32("test price"), + 51e6 + ); + } + + function testSubmitOfferToInvalidAuction() public { + vm.startPrank(management); + termController.markNotTermDeployed(address(repoToken1WeekAuction)); + vm.expectRevert( + abi.encodeWithSelector( + Strategy.InvalidTermAuction.selector, + address(repoToken1WeekAuction) + ) + ); + termStrategy.submitAuctionOffer( + repoToken1WeekAuction, + address(repoToken1Week), + bytes32("offer id hash 1"), + bytes32("test price"), + 1e6 + ); + } + + function testSubmitOfferToAuctionWithInvalidRepoToken() public { + vm.startPrank(management); + termController.markNotTermDeployed(address(repoToken1Week)); + vm.expectRevert( + abi.encodeWithSelector( + RepoTokenList.InvalidRepoToken.selector, + address(repoToken1Week) + ) + ); + termStrategy.submitAuctionOffer( + repoToken1WeekAuction, + address(repoToken1Week), + bytes32("offer id hash 1"), + bytes32("test price"), + 1e6 + ); + } + + function testSubmitOfferWithoutEnoughLiquidity() public { + vm.prank(management); + vm.expectRevert( + abi.encodeWithSelector( + Strategy.InsufficientLiquidBalance.selector, + 100e6, + 101e6 + ) + ); + termStrategy.submitAuctionOffer( + repoToken1WeekAuction, + address(repoToken1Week), + bytes32("offer id hash 1"), + bytes32("test price"), + 101e6 + ); + } + + function testSubmitOfferWithExcessiveWeightedTimeMaturity() public { + vm.prank(management); + vm.expectRevert( + abi.encodeWithSelector( + Strategy.TimeToMaturityAboveThreshold.selector + ) + ); + termStrategy.submitAuctionOffer( + repoToken100WeekAuction, + address(repoToken100Week), + bytes32("offer id hash 1"), + bytes32("test price"), + 10e6 + ); + } + + function testSubmitOfferFailsIfMinCollatRatioisZero() public { + vm.startPrank(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0); + vm.stopPrank(); + + vm.startPrank(management); + vm.expectRevert( + abi.encodeWithSelector( + RepoTokenList.InvalidRepoToken.selector, + address(repoToken1Week) + ) + ); + termStrategy.submitAuctionOffer( + repoToken1WeekAuction, + address(repoToken1Week), + bytes32("offer id hash 1"), + bytes32("test price"), + 1e6 + ); + } + + function testEditOffer() public { + bytes32 idHash1 = bytes32("offer id hash 1"); + bytes32 offerId1 = _submitOffer(idHash1, 1e6); + + // TODO: fuzz this + uint256 offerAmount = 4e6; + + assertEq( + termStrategy.totalLiquidBalance(), + initialState.totalLiquidBalance - 1e6 + ); + uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 1e6); + assertEq( + termStrategy.totalAssetValue(), + termStrategy.totalLiquidBalance() + 1e6 + ); + assertEq(termStrategy.liquidReserveRatio(), 0.99e18); + + vm.prank(management); + bytes32[] memory offerIds = termStrategy.submitAuctionOffer( + repoToken1WeekAuction, + address(repoToken1Week), + offerId1, + bytes32("test price"), + offerAmount + ); + + assertEq( + termStrategy.totalLiquidBalance(), + initialState.totalLiquidBalance - offerAmount + ); + repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 4e6); + assertEq( + termStrategy.totalAssetValue(), + termStrategy.totalLiquidBalance() + offerAmount + ); + assertEq(termStrategy.liquidReserveRatio(), 0.96e18); + } + + 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 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 0); + assertEq(termStrategy.liquidReserveRatio(), 1e18); + } + + function testDeleteOffersFromInvalidAuction() 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.startPrank(management); + termController.markNotTermDeployed(address(repoToken1WeekAuction)); + vm.expectRevert( + abi.encodeWithSelector( + Strategy.InvalidTermAuction.selector, + address(repoToken1WeekAuction) + ) + ); + termStrategy.deleteAuctionOffers( + address(repoToken1WeekAuction), + offerIds + ); + } + + uint256 public constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; + uint256 public constant RATE_PRECISION = 1e18; + + function _getRepoTokenAmountGivenPurchaseTokenAmount( + uint256 purchaseTokenAmount, + MockTermRepoToken termRepoToken, + uint256 auctionRate + ) 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 + + ((auctionRate * 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 + ); + assertEq(termStrategy.liquidReserveRatio(), 0.99e18); + + uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 1e6); + + //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(); + assertEq(termStrategy.liquidReserveRatio(), 0.99e18); + + repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 1e6); + + // 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); + assertEq(termStrategy.liquidReserveRatio(), 0.99e18); + 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 + ); + + uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 1e6); + + repoToken1WeekAuction.auctionSuccess( + offerIds, + fillAmounts, + repoTokenAmounts + ); + + assertEq(termStrategy.liquidReserveRatio(), 0.995e18); + + repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 0.5e6); + + // 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(); + + repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 0.5e6); + + // 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 testAuctionCancelForWithdrawal() public { + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); + + uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 1e6); + + repoToken1WeekAuction.auctionCancelForWithdrawal(); + + repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 1e6); + + // test: check value before calling complete auction + termStrategy.auctionClosed(); + + repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 0); + + bytes32[] memory offers = termStrategy.pendingOffers(); + + assertEq(offers.length, 0); + } + + function testAuctionCancel() public { + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); + + uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 1e6); + + bytes32[] memory offerIds = new bytes32[](1); + offerIds[0] = offerId1; + + repoToken1WeekAuction.auctionCancel(offerIds); + + repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 0); + + // test: check value before calling complete auction + termStrategy.auctionClosed(); + + repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + assertEq(repoTokenHoldingValue, 0); + + 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], offerId1); + assertEq(offers[1], offerId2); + } + + 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(offerId1, 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(offerId1, 50e6); + + assertEq(termStrategy.totalLiquidBalance(), 50e6); + } +} diff --git a/src/test/TestUSDCSellRepoToken.t.sol b/src/test/TestUSDCSellRepoToken.t.sol index 93606a64..1815cc59 100644 --- a/src/test/TestUSDCSellRepoToken.t.sol +++ b/src/test/TestUSDCSellRepoToken.t.sol @@ -1,762 +1,958 @@ -pragma solidity ^0.8.18; - -import "forge-std/console.sol"; -import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {ITokenizedStrategy} from "@tokenized-strategy/interfaces/ITokenizedStrategy.sol"; -import {MockTermRepoToken} from "./mocks/MockTermRepoToken.sol"; -import {MockTermController} from "./mocks/MockTermController.sol"; -import {MockTermAuction} from "./mocks/MockTermAuction.sol"; -import {MockUSDC} from "./mocks/MockUSDC.sol"; -import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol"; -import {ITermRepoToken} from "../interfaces/term/ITermRepoToken.sol"; -import {RepoTokenList} from "../RepoTokenList.sol"; -import {Strategy} from "../Strategy.sol"; -import {ITermController} from "../interfaces/term/ITermController.sol"; - -contract TestUSDCSellRepoToken is Setup { - - MockUSDC internal mockUSDC; - ERC20Mock internal mockCollateral; - MockTermRepoToken internal repoToken1Week; - MockTermRepoToken internal repoToken2Week; - MockTermRepoToken internal repoToken4Week; - MockTermRepoToken internal repoTokenMatured; - 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, block.timestamp + 1 weeks - ); - repoToken2Week = new MockTermRepoToken( - bytes32("test repo token 2"), address(mockUSDC), address(mockCollateral), 1e18, block.timestamp + 2 weeks - ); - repoToken4Week = new MockTermRepoToken( - bytes32("test repo token 3"), address(mockUSDC), address(mockCollateral), 1e18, block.timestamp + 4 weeks - ); - repoTokenMatured = new MockTermRepoToken( - bytes32("test repo token 4"), address(mockUSDC), address(mockCollateral), 1e18, block.timestamp - 1 - ); - - termStrategy = Strategy(address(strategy)); - - vm.startPrank(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); - termStrategy.setTimeToMaturityThreshold(10 weeks); - termStrategy.setRepoTokenConcentrationLimit(1e18); - termStrategy.setRequiredReserveRatio(0); - termStrategy.setDiscountRateMarkup(0); - vm.stopPrank(); - - } - - function _initState() private { - initialState.totalAssetValue = termStrategy.totalAssetValue(); - initialState.totalLiquidBalance = termStrategy.totalLiquidBalance(); - } - - function testSellSingleRepoToken() public { - // start with some initial funds - mockUSDC.mint(address(strategy), 100e6); - _initState(); - - // TODO: fuzz this - uint256 repoTokenSellAmount = 1e18; - - address testUser = vm.addr(0x11111); - - repoToken1Week.mint(testUser, 1000e18); - - vm.prank(testUser); - repoToken1Week.approve(address(strategy), type(uint256).max); - - termController.setOracleRate(repoToken1Week.termRepoId(), 0.05e18); - - vm.startPrank(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); - termStrategy.setTimeToMaturityThreshold(3 weeks); - vm.stopPrank(); - - vm.prank(testUser); - termStrategy.sellRepoToken(address(repoToken1Week), repoTokenSellAmount); - - uint256 expectedProceeds = termStrategy.calculateRepoTokenPresentValue( - address(repoToken1Week), 0.05e18, repoTokenSellAmount - ); - - uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue(address(repoToken1Week)); - - assertEq(repoTokenHoldingValue, expectedProceeds); - - assertEq(mockUSDC.balanceOf(testUser), expectedProceeds); - assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - expectedProceeds); - assertEq(termStrategy.totalAssetValue(), initialState.totalAssetValue); - - (uint256 weightedTimeToMaturity, ,) = termStrategy.simulateTransaction(address(0), 0); - - (uint256 redemptionTimestamp, , ,) = ITermRepoToken(repoToken1Week).config(); - - // TODO: validate this math (weighted time to maturity) - uint256 repoTokenBalanceInBaseAssetPrecision = - (ITermRepoToken(repoToken1Week).redemptionValue() * repoTokenSellAmount * 1e6) / (1e18 * 1e18); - uint256 cumulativeWeightedTimeToMaturity = - (redemptionTimestamp - block.timestamp) * repoTokenBalanceInBaseAssetPrecision; - uint256 expectedWeightedTimeToMaturity = - cumulativeWeightedTimeToMaturity / (repoTokenBalanceInBaseAssetPrecision + termStrategy.totalLiquidBalance()); - - // assertEq(weightedTimeToMaturity, expectedWeightedTimeToMaturity); - } - - function testSellInvalidRepoToken() public { - // start with some initial funds - mockUSDC.mint(address(strategy), 100e6); - _initState(); - - uint256 repoTokenSellAmount = 1e18; - - address testUser = vm.addr(0x11111); - - repoToken1Week.mint(testUser, 1000e18); - - vm.prank(testUser); - repoToken1Week.approve(address(strategy), type(uint256).max); - - termController.setOracleRate(repoToken1Week.termRepoId(), 0.05e18); - termController.markNotTermDeployed(address(repoToken1Week)); - - vm.startPrank(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); - termStrategy.setTimeToMaturityThreshold(3 weeks); - vm.stopPrank(); - - vm.prank(testUser); - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(repoToken1Week))); - termStrategy.sellRepoToken(address(repoToken1Week), repoTokenSellAmount); - } - - function testSellRepoTokenInvalidLiquidBalance() public { - // start with some initial funds - mockUSDC.mint(address(strategy), 5e6); - _initState(); - - uint256 repoTokenSellAmount = 5.1e18; - - address testUser = vm.addr(0x11111); - - repoToken1Week.mint(testUser, 1000e18); - - vm.prank(testUser); - repoToken1Week.approve(address(strategy), type(uint256).max); - - termController.setOracleRate(repoToken1Week.termRepoId(), 0.00001e18); - - vm.startPrank(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); - termStrategy.setTimeToMaturityThreshold(3 weeks); - vm.stopPrank(); - - vm.prank(testUser); - vm.expectRevert(abi.encodeWithSelector(Strategy.InsufficientLiquidBalance.selector, 5e6, 5.1e6)); - termStrategy.sellRepoToken(address(repoToken1Week), repoTokenSellAmount); - } - - function testSellRepoTokenBalanceBelowRequireReserveRatio() public { - // start with some initial funds - mockUSDC.mint(address(strategy), 5e6); - _initState(); - - uint256 repoTokenSellAmount = 2.7e18; - - address testUser = vm.addr(0x11111); - - repoToken1Week.mint(testUser, 1000e18); - - vm.prank(testUser); - repoToken1Week.approve(address(strategy), type(uint256).max); - - termController.setOracleRate(repoToken1Week.termRepoId(), 0.05e18); - - vm.startPrank(governor); - termStrategy.setRequiredReserveRatio(0.5e18); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); - termStrategy.setTimeToMaturityThreshold(3 weeks); - vm.stopPrank(); - - vm.prank(testUser); - vm.expectRevert(abi.encodeWithSelector(Strategy.BalanceBelowRequiredReserveRatio.selector)); - termStrategy.sellRepoToken(address(repoToken1Week), repoTokenSellAmount); - } - - // Test with different precisions - function testCalculateRepoTokenPresentValue() public { - // 0.05 0.075 0.1687 - // 7 999028 998544 996730 - // 14 998059 997092 993482 - // 28 996127 994200 987049 - - // 7 days, 0.5 = 999028 - assertEq(termStrategy.calculateRepoTokenPresentValue(address(repoToken1Week), 0.05e18, 1e18), 999028); - // 7 days, 0.075 = 99854 - assertEq(termStrategy.calculateRepoTokenPresentValue(address(repoToken1Week), 0.075e18, 1e18), 998544); - // 7 days, 0.1687 = 996730 - assertEq(termStrategy.calculateRepoTokenPresentValue(address(repoToken1Week), 0.1687e18, 1e18), 996730); - - // 14 days, 0.5 = 999028 - assertEq(termStrategy.calculateRepoTokenPresentValue(address(repoToken2Week), 0.05e18, 1e18), 998059); - // 14 days, 0.075 = 99854 - assertEq(termStrategy.calculateRepoTokenPresentValue(address(repoToken2Week), 0.075e18, 1e18), 997092); - // 14 days, 0.1687 = 996730 - assertEq(termStrategy.calculateRepoTokenPresentValue(address(repoToken2Week), 0.1687e18, 1e18), 993482); - - // 28 days, 0.5 = 999028 - assertEq(termStrategy.calculateRepoTokenPresentValue(address(repoToken4Week), 0.05e18, 1e18), 996127); - // 28 days, 0.075 = 99854 - assertEq(termStrategy.calculateRepoTokenPresentValue(address(repoToken4Week), 0.075e18, 1e18), 994200); - // 28 days, 0.1687 = 996730 - assertEq(termStrategy.calculateRepoTokenPresentValue(address(repoToken4Week), 0.1687e18, 1e18), 987049); - } - - function _sell1RepoToken(MockTermRepoToken rt1, uint256 amount1) private { - address[] memory tokens = new address[](1); - tokens[0] = address(rt1); - uint256[] memory amounts = new uint256[](1); - amounts[0] = amount1; - - _sellRepoTokens(tokens, amounts, false, true, ""); - } - - function _sell1RepoTokenNoMint(MockTermRepoToken rt1, uint256 amount1) private { - address[] memory tokens = new address[](1); - tokens[0] = address(rt1); - uint256[] memory amounts = new uint256[](1); - amounts[0] = amount1; - - _sellRepoTokens(tokens, amounts, false, false, ""); - } - - function _sell1RepoTokenExpectRevert(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, 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, - MockTermRepoToken rt2, - uint256 amount2, - MockTermRepoToken rt3, - uint256 amount3 - ) private { - address[] memory tokens = new address[](3); - tokens[0] = address(rt1); - tokens[1] = address(rt2); - tokens[2] = address(rt3); - uint256[] memory amounts = new uint256[](3); - amounts[0] = amount1; - amounts[1] = amount2; - amounts[2] = amount3; - - _sellRepoTokens(tokens, amounts, false, true, ""); - } - - function _sell2RepoTokens( - MockTermRepoToken rt1, - uint256 amount1, - MockTermRepoToken rt2, - uint256 amount2 - ) private { - address[] memory tokens = new address[](2); - tokens[0] = address(rt1); - tokens[1] = address(rt2); - uint256[] memory amounts = new uint256[](2); - amounts[0] = amount1; - amounts[1] = amount2; - - _sellRepoTokens(tokens, amounts, false, true, ""); - } - - function _sell3RepoTokensCheckHoldings() private { - address[] memory holdings = termStrategy.repoTokenHoldings(); - - // 3 repo tokens - assertEq(holdings.length, 3); - - // sorted by time to maturity - assertEq(holdings[0], address(repoToken1Week)); - assertEq(holdings[1], address(repoToken2Week)); - assertEq(holdings[2], address(repoToken4Week)); - } - - function _sellRepoTokens(address[] memory tokens, uint256[] memory amounts, bool expectRevert, bool mintUnderlying, bytes memory err) private { - address testUser = vm.addr(0x11111); - - for (uint256 i; i < tokens.length; i++) { - address token = tokens[i]; - uint256 amount = amounts[i]; - - termController.setOracleRate(MockTermRepoToken(token).termRepoId(), 0.05e18); - - MockTermRepoToken(token).mint(testUser, amount); - if (mintUnderlying) { - mockUSDC.mint( - address(strategy), - termStrategy.calculateRepoTokenPresentValue(token, 0.05e18, amount) - ); - } - - vm.startPrank(testUser); - MockTermRepoToken(token).approve(address(strategy), type(uint256).max); - - if (expectRevert) { - vm.expectRevert(err); - termStrategy.sellRepoToken(token, amount); - } else { - termStrategy.sellRepoToken(token, amount); - } - vm.stopPrank(); - } - } - - // 7 days (3), 14 days (9), 28 days (3) - function testSellMultipleRepoTokens_7_14_28_3_9_3() public { - _sell3RepoTokens(repoToken1Week, 3e18, repoToken2Week, 9e18, repoToken4Week, 3e18); - _sell3RepoTokensCheckHoldings(); - (uint256 weightedTimeToMaturity, ,) = termStrategy.simulateTransaction(address(0), 0); - assertEq(weightedTimeToMaturity, 1330560); - } - - // 14 days (9), 7 days (3), 28 days (3) - function testSellMultipleRepoTokens_14_7_28_9_3_3() public { - _sell3RepoTokens(repoToken2Week, 9e18, repoToken1Week, 3e18, repoToken4Week, 3e18); - _sell3RepoTokensCheckHoldings(); - (uint256 weightedTimeToMaturity, ,) = termStrategy.simulateTransaction(address(0), 0); - assertEq(weightedTimeToMaturity, 1330560); - } - - // 28 days (3), 14 days (9), 7 days (3) - function testSellMultipleRepoTokens_28_14_7_3_9_3() public { - _sell3RepoTokens(repoToken4Week, 3e18, repoToken2Week, 9e18, repoToken1Week, 3e18); - _sell3RepoTokensCheckHoldings(); - (uint256 weightedTimeToMaturity, ,) = termStrategy.simulateTransaction(address(0), 0); - assertEq(weightedTimeToMaturity, 1330560); - } - - // 28 days (3), 7 days (3), 14 days (9) - function testSellMultipleRepoTokens_28_7_14_3_3_9() public { - _sell3RepoTokens(repoToken4Week, 3e18, repoToken1Week, 3e18, repoToken2Week, 9e18); - _sell3RepoTokensCheckHoldings(); - (uint256 weightedTimeToMaturity, ,) = termStrategy.simulateTransaction(address(0), 0); - assertEq(weightedTimeToMaturity, 1330560); - } - - // 7 days (6), 14 days (2), 28 days (8) - function testSellMultipleRepoTokens_7_14_28_6_2_8() public { - _sell3RepoTokens(repoToken1Week, 6e18, repoToken2Week, 2e18, repoToken4Week, 8e18); - _sell3RepoTokensCheckHoldings(); - (uint256 weightedTimeToMaturity, ,) = termStrategy.simulateTransaction(address(0), 0); - assertEq(weightedTimeToMaturity, 1587600); - } - - // 7 days (8), 14 days (1), 28 days (3) - function testSellMultipleRepoTokens_7_14_28_8_1_3() public { - _sell3RepoTokens(repoToken1Week, 8e18, repoToken2Week, 1e18, repoToken4Week, 3e18); - _sell3RepoTokensCheckHoldings(); - (uint256 weightedTimeToMaturity, ,) = termStrategy.simulateTransaction(address(0), 0); - assertEq(weightedTimeToMaturity, 1108800); - } - - // test: weighted maturity with both repo tokens and pending offers - function testSellMultipleRepoTokens_7_14_8_1_Offer_28_3() public { - _sell2RepoTokens(repoToken1Week, 8e18, repoToken2Week, 1e18); - - bytes32 idHash = bytes32("offer id hash 1"); - - MockTermAuction repoToken4WeekAuction = new MockTermAuction(repoToken4Week); - - mockUSDC.mint(address(termStrategy), 3e6); - - vm.prank(management); - termStrategy.submitAuctionOffer( - repoToken4WeekAuction, address(repoToken4Week), idHash, bytes32("test price"), 3e6 - ); - - (uint256 weightedTimeToMaturity, ,) = termStrategy.simulateTransaction(address(0), 0); - - assertEq(weightedTimeToMaturity, 1108800); - } - - function testSetTermController() public { - MockTermController newController = new MockTermController(); - - vm.expectRevert(); - termStrategy.setTermController(address(newController)); - - vm.expectRevert(); - vm.prank(governor); - termStrategy.setTermController(address(0)); - - ( - , - , - , - ITermController prevTermController, - ITermController currTermController, - , - , - , - , - ) = termStrategy.strategyState(); - - address currentController = address(currTermController); - vm.prank(governor); - termStrategy.setTermController(address(newController)); - ( - , - , - , - prevTermController, - currTermController, - , - , - , - , - ) = termStrategy.strategyState(); - assertEq(address(currTermController), address(newController)); - assertEq(address(prevTermController), currentController); - } - - function testSetTimeToMaturityThreshold() public { - vm.expectRevert(); - termStrategy.setTimeToMaturityThreshold(12345); - - vm.prank(governor); - termStrategy.setTimeToMaturityThreshold(12345); - ( - , - , - , - , - , - , - uint256 timeToMaturityThreshold, - , - , - ) = termStrategy.strategyState(); - assertEq(timeToMaturityThreshold, 12345); - } - - function testSetRequiredReserveRatio() public { - vm.expectRevert(); - termStrategy.setRequiredReserveRatio(12345); - - vm.prank(governor); - termStrategy.setRequiredReserveRatio(12345); - ( - , - , - , - , - , - , - , - uint256 requiredReserveRatio, - , - ) = termStrategy.strategyState(); - assertEq(requiredReserveRatio, 12345); - } - - function testSetDiscountRateMarkup() public { - vm.expectRevert(); - termStrategy.setDiscountRateMarkup(12345); - - vm.prank(governor); - termStrategy.setDiscountRateMarkup(12345); - ( - , - , - , - , - , - , - , - , - uint256 discountRateMarkup, - ) = termStrategy.strategyState(); - assertEq(discountRateMarkup, 12345); - } - - function testSetCollateralTokenParams() public { - vm.prank(governor); - termStrategy.setDiscountRateMarkup(12345); - - vm.expectRevert(); - termStrategy.setCollateralTokenParams(address(mockCollateral), 12345); - - vm.prank(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 12345); - ( - , - , - , - , - , - , - , - , - uint256 discountRateMarkup, - ) = termStrategy.strategyState(); - assertEq(discountRateMarkup, 12345); - } - - - function testRepoTokenValidationFailures() public { - // start with some initial funds - mockUSDC.mint(address(strategy), 100e6); - _initState(); - - address testUser = vm.addr(0x11111); - - repoToken1Week.mint(testUser, 1000e18); - repoTokenMatured.mint(testUser, 1000e18); - - // test: token has no auction clearing rate - vm.expectRevert(); - vm.prank(testUser); - termStrategy.sellRepoToken(address(repoToken1Week), 1e18); - - termController.setOracleRate(repoToken1Week.termRepoId(), 0.05e18); - termController.setOracleRate(repoTokenMatured.termRepoId(), 0.05e18); - - vm.prank(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0); - - // test: min collateral ratio not set - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(repoToken1Week))); - vm.prank(testUser); - termStrategy.sellRepoToken(address(repoToken1Week), 1e18); - - vm.startPrank(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); - termStrategy.setTimeToMaturityThreshold(3 weeks); - vm.stopPrank(); - - // test: matured repo token - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(repoTokenMatured))); - vm.prank(testUser); - termStrategy.sellRepoToken(address(repoTokenMatured), 1e18); - } - - function testAboveMaturityThresholdFailure() public { - _sell1RepoToken(repoToken2Week, 2e18); - - (uint256 timeToMat, ,) = termStrategy.simulateTransaction(address(0), 0); - - vm.prank(governor); - termStrategy.setTimeToMaturityThreshold(timeToMat); - - // test: can't sell 4 week repo token because of time to maturity threshold - _sell1RepoTokenExpectRevert(repoToken4Week, 4e18, abi.encodeWithSelector(Strategy.TimeToMaturityAboveThreshold.selector)); - - // test: can still sell 1 week repo token - _sell1RepoToken(repoToken1Week, 4e18); - } - - function testRedeemMaturedRepoTokensInternal() public { - // start with some initial funds - 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(); - - _sell1RepoTokenNoMint(repoToken2Week, 2e18); - - address[] memory holdings = termStrategy.repoTokenHoldings(); - - assertEq(holdings.length, 1); - - vm.warp(block.timestamp + 3 weeks); - - vm.prank(keeper); - ITokenizedStrategy(address(termStrategy)).report(); - - holdings = termStrategy.repoTokenHoldings(); - - assertEq(holdings.length, 0); - - vm.startPrank(testDepositor); - IERC4626(address(termStrategy)).withdraw( - IERC4626(address(termStrategy)).balanceOf(testDepositor), - testDepositor, - testDepositor - ); - vm.stopPrank(); - } - - function testRedeemMaturedRepoTokensExternal() public { - // start with some initial funds - 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(); - - console.log("totalLiquidBalance", termStrategy.totalLiquidBalance()); - - _sell1RepoTokenNoMint(repoToken2Week, 2e18); - - address[] memory holdings = termStrategy.repoTokenHoldings(); - - assertEq(holdings.length, 1); - - vm.warp(block.timestamp + 3 weeks); - - console.log("totalLiquidBalance", termStrategy.totalLiquidBalance()); - console.log("totalAssetValue", termStrategy.totalAssetValue()); - - // external redemption - repoToken2Week.mockServicer().redeemTermRepoTokens(address(termStrategy), repoToken2Week.balanceOf(address(termStrategy))); - - console.log("totalLiquidBalance", termStrategy.totalLiquidBalance()); - console.log("totalAssetValue", termStrategy.totalAssetValue()); - - vm.prank(keeper); - ITokenizedStrategy(address(termStrategy)).report(); - - holdings = termStrategy.repoTokenHoldings(); - - assertEq(holdings.length, 0); - - vm.startPrank(testDepositor); - IERC4626(address(termStrategy)).withdraw( - IERC4626(address(termStrategy)).balanceOf(testDepositor), - testDepositor, - testDepositor - ); - vm.stopPrank(); - } - - function testRedeemMaturedRepoTokensFailure() public { - // start with some initial funds - 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(); - - _sell1RepoTokenNoMint(repoToken2Week, 2e18); - - address[] memory holdings = termStrategy.repoTokenHoldings(); - - assertEq(holdings.length, 1); - - vm.warp(block.timestamp + 3 weeks); - - repoToken2Week.mockServicer().setRedemptionFailure(true); - - vm.prank(keeper); - ITokenizedStrategy(address(termStrategy)).report(); - - holdings = termStrategy.repoTokenHoldings(); - - // TEST: still has 1 repo token because redemption failure - assertEq(holdings.length, 1); - - 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(); - termStrategy.setRepoTokenConcentrationLimit(0.4e18); - - // Set to 40% - vm.prank(governor); - termStrategy.setRepoTokenConcentrationLimit(0.4e18); - - termController.setOracleRate(repoToken2Week.termRepoId(), 0.05e18); - - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(0))); - termStrategy.getRepoTokenConcentrationRatio(address(0)); - - - uint256 concentrationLimit = termStrategy.getRepoTokenConcentrationRatio(address(repoToken2Week)); - - _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(); - termStrategy.pauseStrategy(); - - vm.prank(governor); - termStrategy.pauseStrategy(); - - 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" - ); - - vm.prank(governor); - termStrategy.unpauseStrategy(); - vm.prank(testDepositor); - IERC4626(address(termStrategy)).deposit(depositAmount, testDepositor); - vm.stopPrank(); - } -} +pragma solidity ^0.8.18; + +import "forge-std/console.sol"; +import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {ITokenizedStrategy} from "@tokenized-strategy/interfaces/ITokenizedStrategy.sol"; +import {MockTermRepoToken} from "./mocks/MockTermRepoToken.sol"; +import {MockTermController} from "./mocks/MockTermController.sol"; +import {MockTermAuction} from "./mocks/MockTermAuction.sol"; +import {MockUSDC} from "./mocks/MockUSDC.sol"; +import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol"; +import {ITermRepoToken} from "../interfaces/term/ITermRepoToken.sol"; +import {RepoTokenList} from "../RepoTokenList.sol"; +import {Strategy} from "../Strategy.sol"; +import {ITermController} from "../interfaces/term/ITermController.sol"; + +contract TestUSDCSellRepoToken is Setup { + MockUSDC internal mockUSDC; + ERC20Mock internal mockCollateral; + MockTermRepoToken internal repoToken1Week; + MockTermRepoToken internal repoToken2Week; + MockTermRepoToken internal repoToken4Week; + MockTermRepoToken internal repoTokenMatured; + 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, + block.timestamp + 1 weeks + ); + repoToken2Week = new MockTermRepoToken( + bytes32("test repo token 2"), + address(mockUSDC), + address(mockCollateral), + 1e18, + block.timestamp + 2 weeks + ); + repoToken4Week = new MockTermRepoToken( + bytes32("test repo token 3"), + address(mockUSDC), + address(mockCollateral), + 1e18, + block.timestamp + 4 weeks + ); + repoTokenMatured = new MockTermRepoToken( + bytes32("test repo token 4"), + address(mockUSDC), + address(mockCollateral), + 1e18, + block.timestamp - 1 + ); + + termStrategy = Strategy(address(strategy)); + + vm.startPrank(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); + termStrategy.setTimeToMaturityThreshold(10 weeks); + termStrategy.setRepoTokenConcentrationLimit(1e18); + termStrategy.setRequiredReserveRatio(0); + termStrategy.setDiscountRateMarkup(0); + vm.stopPrank(); + } + + function _initState() private { + initialState.totalAssetValue = termStrategy.totalAssetValue(); + initialState.totalLiquidBalance = termStrategy.totalLiquidBalance(); + } + + function testSellSingleRepoToken() public { + // start with some initial funds + mockUSDC.mint(address(strategy), 100e6); + _initState(); + + // TODO: fuzz this + uint256 repoTokenSellAmount = 1e18; + + address testUser = vm.addr(0x11111); + + repoToken1Week.mint(testUser, 1000e18); + + vm.prank(testUser); + repoToken1Week.approve(address(strategy), type(uint256).max); + + termController.setOracleRate(repoToken1Week.termRepoId(), 0.05e18); + + vm.startPrank(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); + termStrategy.setTimeToMaturityThreshold(3 weeks); + vm.stopPrank(); + + vm.prank(testUser); + termStrategy.sellRepoToken( + address(repoToken1Week), + repoTokenSellAmount + ); + + uint256 expectedProceeds = termStrategy.calculateRepoTokenPresentValue( + address(repoToken1Week), + 0.05e18, + repoTokenSellAmount + ); + + uint256 repoTokenHoldingValue = termStrategy.getRepoTokenHoldingValue( + address(repoToken1Week) + ); + + assertEq(repoTokenHoldingValue, expectedProceeds); + + assertEq(mockUSDC.balanceOf(testUser), expectedProceeds); + assertEq( + termStrategy.totalLiquidBalance(), + initialState.totalLiquidBalance - expectedProceeds + ); + assertEq(termStrategy.totalAssetValue(), initialState.totalAssetValue); + + (uint256 weightedTimeToMaturity, , ) = termStrategy.simulateTransaction( + address(0), + 0 + ); + + (uint256 redemptionTimestamp, , , ) = ITermRepoToken(repoToken1Week) + .config(); + + // TODO: validate this math (weighted time to maturity) + uint256 repoTokenBalanceInBaseAssetPrecision = (ITermRepoToken( + repoToken1Week + ).redemptionValue() * + repoTokenSellAmount * + 1e6) / (1e18 * 1e18); + uint256 cumulativeWeightedTimeToMaturity = (redemptionTimestamp - + block.timestamp) * repoTokenBalanceInBaseAssetPrecision; + uint256 expectedWeightedTimeToMaturity = cumulativeWeightedTimeToMaturity / + (repoTokenBalanceInBaseAssetPrecision + + termStrategy.totalLiquidBalance()); + + // assertEq(weightedTimeToMaturity, expectedWeightedTimeToMaturity); + } + + function testSellInvalidRepoToken() public { + // start with some initial funds + mockUSDC.mint(address(strategy), 100e6); + _initState(); + + uint256 repoTokenSellAmount = 1e18; + + address testUser = vm.addr(0x11111); + + repoToken1Week.mint(testUser, 1000e18); + + vm.prank(testUser); + repoToken1Week.approve(address(strategy), type(uint256).max); + + termController.setOracleRate(repoToken1Week.termRepoId(), 0.05e18); + termController.markNotTermDeployed(address(repoToken1Week)); + + vm.startPrank(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); + termStrategy.setTimeToMaturityThreshold(3 weeks); + vm.stopPrank(); + + vm.prank(testUser); + vm.expectRevert( + abi.encodeWithSelector( + RepoTokenList.InvalidRepoToken.selector, + address(repoToken1Week) + ) + ); + termStrategy.sellRepoToken( + address(repoToken1Week), + repoTokenSellAmount + ); + } + + function testSellRepoTokenInvalidLiquidBalance() public { + // start with some initial funds + mockUSDC.mint(address(strategy), 5e6); + _initState(); + + uint256 repoTokenSellAmount = 5.1e18; + + address testUser = vm.addr(0x11111); + + repoToken1Week.mint(testUser, 1000e18); + + vm.prank(testUser); + repoToken1Week.approve(address(strategy), type(uint256).max); + + termController.setOracleRate(repoToken1Week.termRepoId(), 0.00001e18); + + vm.startPrank(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); + termStrategy.setTimeToMaturityThreshold(3 weeks); + vm.stopPrank(); + + vm.prank(testUser); + vm.expectRevert( + abi.encodeWithSelector( + Strategy.InsufficientLiquidBalance.selector, + 5e6, + 5.1e6 + ) + ); + termStrategy.sellRepoToken( + address(repoToken1Week), + repoTokenSellAmount + ); + } + + function testSellRepoTokenBalanceBelowRequireReserveRatio() public { + // start with some initial funds + mockUSDC.mint(address(strategy), 5e6); + _initState(); + + uint256 repoTokenSellAmount = 2.7e18; + + address testUser = vm.addr(0x11111); + + repoToken1Week.mint(testUser, 1000e18); + + vm.prank(testUser); + repoToken1Week.approve(address(strategy), type(uint256).max); + + termController.setOracleRate(repoToken1Week.termRepoId(), 0.05e18); + + vm.startPrank(governor); + termStrategy.setRequiredReserveRatio(0.5e18); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); + termStrategy.setTimeToMaturityThreshold(3 weeks); + vm.stopPrank(); + + vm.prank(testUser); + vm.expectRevert( + abi.encodeWithSelector( + Strategy.BalanceBelowRequiredReserveRatio.selector + ) + ); + termStrategy.sellRepoToken( + address(repoToken1Week), + repoTokenSellAmount + ); + } + + // Test with different precisions + function testCalculateRepoTokenPresentValue() public { + // 0.05 0.075 0.1687 + // 7 999028 998544 996730 + // 14 998059 997092 993482 + // 28 996127 994200 987049 + + // 7 days, 0.5 = 999028 + assertEq( + termStrategy.calculateRepoTokenPresentValue( + address(repoToken1Week), + 0.05e18, + 1e18 + ), + 999028 + ); + // 7 days, 0.075 = 99854 + assertEq( + termStrategy.calculateRepoTokenPresentValue( + address(repoToken1Week), + 0.075e18, + 1e18 + ), + 998544 + ); + // 7 days, 0.1687 = 996730 + assertEq( + termStrategy.calculateRepoTokenPresentValue( + address(repoToken1Week), + 0.1687e18, + 1e18 + ), + 996730 + ); + + // 14 days, 0.5 = 999028 + assertEq( + termStrategy.calculateRepoTokenPresentValue( + address(repoToken2Week), + 0.05e18, + 1e18 + ), + 998059 + ); + // 14 days, 0.075 = 99854 + assertEq( + termStrategy.calculateRepoTokenPresentValue( + address(repoToken2Week), + 0.075e18, + 1e18 + ), + 997092 + ); + // 14 days, 0.1687 = 996730 + assertEq( + termStrategy.calculateRepoTokenPresentValue( + address(repoToken2Week), + 0.1687e18, + 1e18 + ), + 993482 + ); + + // 28 days, 0.5 = 999028 + assertEq( + termStrategy.calculateRepoTokenPresentValue( + address(repoToken4Week), + 0.05e18, + 1e18 + ), + 996127 + ); + // 28 days, 0.075 = 99854 + assertEq( + termStrategy.calculateRepoTokenPresentValue( + address(repoToken4Week), + 0.075e18, + 1e18 + ), + 994200 + ); + // 28 days, 0.1687 = 996730 + assertEq( + termStrategy.calculateRepoTokenPresentValue( + address(repoToken4Week), + 0.1687e18, + 1e18 + ), + 987049 + ); + } + + function _sell1RepoToken(MockTermRepoToken rt1, uint256 amount1) private { + address[] memory tokens = new address[](1); + tokens[0] = address(rt1); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount1; + + _sellRepoTokens(tokens, amounts, false, true, ""); + } + + function _sell1RepoTokenNoMint( + MockTermRepoToken rt1, + uint256 amount1 + ) private { + address[] memory tokens = new address[](1); + tokens[0] = address(rt1); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount1; + + _sellRepoTokens(tokens, amounts, false, false, ""); + } + + function _sell1RepoTokenExpectRevert( + 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, 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, + MockTermRepoToken rt2, + uint256 amount2, + MockTermRepoToken rt3, + uint256 amount3 + ) private { + address[] memory tokens = new address[](3); + tokens[0] = address(rt1); + tokens[1] = address(rt2); + tokens[2] = address(rt3); + uint256[] memory amounts = new uint256[](3); + amounts[0] = amount1; + amounts[1] = amount2; + amounts[2] = amount3; + + _sellRepoTokens(tokens, amounts, false, true, ""); + } + + function _sell2RepoTokens( + MockTermRepoToken rt1, + uint256 amount1, + MockTermRepoToken rt2, + uint256 amount2 + ) private { + address[] memory tokens = new address[](2); + tokens[0] = address(rt1); + tokens[1] = address(rt2); + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount1; + amounts[1] = amount2; + + _sellRepoTokens(tokens, amounts, false, true, ""); + } + + function _sell3RepoTokensCheckHoldings() private { + address[] memory holdings = termStrategy.repoTokenHoldings(); + + // 3 repo tokens + assertEq(holdings.length, 3); + + // sorted by time to maturity + assertEq(holdings[0], address(repoToken1Week)); + assertEq(holdings[1], address(repoToken2Week)); + assertEq(holdings[2], address(repoToken4Week)); + } + + function _sellRepoTokens( + address[] memory tokens, + uint256[] memory amounts, + bool expectRevert, + bool mintUnderlying, + bytes memory err + ) private { + address testUser = vm.addr(0x11111); + + for (uint256 i; i < tokens.length; i++) { + address token = tokens[i]; + uint256 amount = amounts[i]; + + termController.setOracleRate( + MockTermRepoToken(token).termRepoId(), + 0.05e18 + ); + + MockTermRepoToken(token).mint(testUser, amount); + if (mintUnderlying) { + mockUSDC.mint( + address(strategy), + termStrategy.calculateRepoTokenPresentValue( + token, + 0.05e18, + amount + ) + ); + } + + vm.startPrank(testUser); + MockTermRepoToken(token).approve( + address(strategy), + type(uint256).max + ); + + if (expectRevert) { + vm.expectRevert(err); + termStrategy.sellRepoToken(token, amount); + } else { + termStrategy.sellRepoToken(token, amount); + } + vm.stopPrank(); + } + } + + // 7 days (3), 14 days (9), 28 days (3) + function testSellMultipleRepoTokens_7_14_28_3_9_3() public { + _sell3RepoTokens( + repoToken1Week, + 3e18, + repoToken2Week, + 9e18, + repoToken4Week, + 3e18 + ); + _sell3RepoTokensCheckHoldings(); + (uint256 weightedTimeToMaturity, , ) = termStrategy.simulateTransaction( + address(0), + 0 + ); + assertEq(weightedTimeToMaturity, 1330560); + } + + // 14 days (9), 7 days (3), 28 days (3) + function testSellMultipleRepoTokens_14_7_28_9_3_3() public { + _sell3RepoTokens( + repoToken2Week, + 9e18, + repoToken1Week, + 3e18, + repoToken4Week, + 3e18 + ); + _sell3RepoTokensCheckHoldings(); + (uint256 weightedTimeToMaturity, , ) = termStrategy.simulateTransaction( + address(0), + 0 + ); + assertEq(weightedTimeToMaturity, 1330560); + } + + // 28 days (3), 14 days (9), 7 days (3) + function testSellMultipleRepoTokens_28_14_7_3_9_3() public { + _sell3RepoTokens( + repoToken4Week, + 3e18, + repoToken2Week, + 9e18, + repoToken1Week, + 3e18 + ); + _sell3RepoTokensCheckHoldings(); + (uint256 weightedTimeToMaturity, , ) = termStrategy.simulateTransaction( + address(0), + 0 + ); + assertEq(weightedTimeToMaturity, 1330560); + } + + // 28 days (3), 7 days (3), 14 days (9) + function testSellMultipleRepoTokens_28_7_14_3_3_9() public { + _sell3RepoTokens( + repoToken4Week, + 3e18, + repoToken1Week, + 3e18, + repoToken2Week, + 9e18 + ); + _sell3RepoTokensCheckHoldings(); + (uint256 weightedTimeToMaturity, , ) = termStrategy.simulateTransaction( + address(0), + 0 + ); + assertEq(weightedTimeToMaturity, 1330560); + } + + // 7 days (6), 14 days (2), 28 days (8) + function testSellMultipleRepoTokens_7_14_28_6_2_8() public { + _sell3RepoTokens( + repoToken1Week, + 6e18, + repoToken2Week, + 2e18, + repoToken4Week, + 8e18 + ); + _sell3RepoTokensCheckHoldings(); + (uint256 weightedTimeToMaturity, , ) = termStrategy.simulateTransaction( + address(0), + 0 + ); + assertEq(weightedTimeToMaturity, 1587600); + } + + // 7 days (8), 14 days (1), 28 days (3) + function testSellMultipleRepoTokens_7_14_28_8_1_3() public { + _sell3RepoTokens( + repoToken1Week, + 8e18, + repoToken2Week, + 1e18, + repoToken4Week, + 3e18 + ); + _sell3RepoTokensCheckHoldings(); + (uint256 weightedTimeToMaturity, , ) = termStrategy.simulateTransaction( + address(0), + 0 + ); + assertEq(weightedTimeToMaturity, 1108800); + } + + // test: weighted maturity with both repo tokens and pending offers + function testSellMultipleRepoTokens_7_14_8_1_Offer_28_3() public { + _sell2RepoTokens(repoToken1Week, 8e18, repoToken2Week, 1e18); + + bytes32 idHash = bytes32("offer id hash 1"); + + MockTermAuction repoToken4WeekAuction = new MockTermAuction( + repoToken4Week + ); + + mockUSDC.mint(address(termStrategy), 3e6); + + vm.prank(management); + termStrategy.submitAuctionOffer( + repoToken4WeekAuction, + address(repoToken4Week), + idHash, + bytes32("test price"), + 3e6 + ); + + (uint256 weightedTimeToMaturity, , ) = termStrategy.simulateTransaction( + address(0), + 0 + ); + + assertEq(weightedTimeToMaturity, 1108800); + } + + function testSetTermController() public { + MockTermController newController = new MockTermController(); + + vm.expectRevert(); + termStrategy.setTermController(address(newController)); + + vm.expectRevert(); + vm.prank(governor); + termStrategy.setTermController(address(0)); + + ( + , + , + , + ITermController prevTermController, + ITermController currTermController, + , + , + , + , + + ) = termStrategy.strategyState(); + + address currentController = address(currTermController); + vm.prank(governor); + termStrategy.setTermController(address(newController)); + (, , , prevTermController, currTermController, , , , , ) = termStrategy + .strategyState(); + assertEq(address(currTermController), address(newController)); + assertEq(address(prevTermController), currentController); + } + + function testSetTimeToMaturityThreshold() public { + vm.expectRevert(); + termStrategy.setTimeToMaturityThreshold(12345); + + vm.prank(governor); + termStrategy.setTimeToMaturityThreshold(12345); + (, , , , , , uint256 timeToMaturityThreshold, , , ) = termStrategy + .strategyState(); + assertEq(timeToMaturityThreshold, 12345); + } + + function testSetRequiredReserveRatio() public { + vm.expectRevert(); + termStrategy.setRequiredReserveRatio(12345); + + vm.prank(governor); + termStrategy.setRequiredReserveRatio(12345); + (, , , , , , , uint256 requiredReserveRatio, , ) = termStrategy + .strategyState(); + assertEq(requiredReserveRatio, 12345); + } + + function testSetDiscountRateMarkup() public { + vm.expectRevert(); + termStrategy.setDiscountRateMarkup(12345); + + vm.prank(governor); + termStrategy.setDiscountRateMarkup(12345); + (, , , , , , , , uint256 discountRateMarkup, ) = termStrategy + .strategyState(); + assertEq(discountRateMarkup, 12345); + } + + function testSetCollateralTokenParams() public { + vm.prank(governor); + termStrategy.setDiscountRateMarkup(12345); + + vm.expectRevert(); + termStrategy.setCollateralTokenParams(address(mockCollateral), 12345); + + vm.prank(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 12345); + (, , , , , , , , uint256 discountRateMarkup, ) = termStrategy + .strategyState(); + assertEq(discountRateMarkup, 12345); + } + + function testRepoTokenValidationFailures() public { + // start with some initial funds + mockUSDC.mint(address(strategy), 100e6); + _initState(); + + address testUser = vm.addr(0x11111); + + repoToken1Week.mint(testUser, 1000e18); + repoTokenMatured.mint(testUser, 1000e18); + + // test: token has no auction clearing rate + vm.expectRevert(); + vm.prank(testUser); + termStrategy.sellRepoToken(address(repoToken1Week), 1e18); + + termController.setOracleRate(repoToken1Week.termRepoId(), 0.05e18); + termController.setOracleRate(repoTokenMatured.termRepoId(), 0.05e18); + + vm.prank(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0); + + // test: min collateral ratio not set + vm.expectRevert( + abi.encodeWithSelector( + RepoTokenList.InvalidRepoToken.selector, + address(repoToken1Week) + ) + ); + vm.prank(testUser); + termStrategy.sellRepoToken(address(repoToken1Week), 1e18); + + vm.startPrank(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); + termStrategy.setTimeToMaturityThreshold(3 weeks); + vm.stopPrank(); + + // test: matured repo token + vm.expectRevert( + abi.encodeWithSelector( + RepoTokenList.InvalidRepoToken.selector, + address(repoTokenMatured) + ) + ); + vm.prank(testUser); + termStrategy.sellRepoToken(address(repoTokenMatured), 1e18); + } + + function testAboveMaturityThresholdFailure() public { + _sell1RepoToken(repoToken2Week, 2e18); + + (uint256 timeToMat, , ) = termStrategy.simulateTransaction( + address(0), + 0 + ); + + vm.prank(governor); + termStrategy.setTimeToMaturityThreshold(timeToMat); + + // test: can't sell 4 week repo token because of time to maturity threshold + _sell1RepoTokenExpectRevert( + repoToken4Week, + 4e18, + abi.encodeWithSelector( + Strategy.TimeToMaturityAboveThreshold.selector + ) + ); + + // test: can still sell 1 week repo token + _sell1RepoToken(repoToken1Week, 4e18); + } + + function testRedeemMaturedRepoTokensInternal() public { + // start with some initial funds + 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(); + + _sell1RepoTokenNoMint(repoToken2Week, 2e18); + + address[] memory holdings = termStrategy.repoTokenHoldings(); + + assertEq(holdings.length, 1); + + vm.warp(block.timestamp + 3 weeks); + + vm.prank(keeper); + ITokenizedStrategy(address(termStrategy)).report(); + + holdings = termStrategy.repoTokenHoldings(); + + assertEq(holdings.length, 0); + + vm.startPrank(testDepositor); + IERC4626(address(termStrategy)).withdraw( + IERC4626(address(termStrategy)).balanceOf(testDepositor), + testDepositor, + testDepositor + ); + vm.stopPrank(); + } + + function testRedeemMaturedRepoTokensExternal() public { + // start with some initial funds + 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(); + + console.log("totalLiquidBalance", termStrategy.totalLiquidBalance()); + + _sell1RepoTokenNoMint(repoToken2Week, 2e18); + + address[] memory holdings = termStrategy.repoTokenHoldings(); + + assertEq(holdings.length, 1); + + vm.warp(block.timestamp + 3 weeks); + + console.log("totalLiquidBalance", termStrategy.totalLiquidBalance()); + console.log("totalAssetValue", termStrategy.totalAssetValue()); + + // external redemption + repoToken2Week.mockServicer().redeemTermRepoTokens( + address(termStrategy), + repoToken2Week.balanceOf(address(termStrategy)) + ); + + console.log("totalLiquidBalance", termStrategy.totalLiquidBalance()); + console.log("totalAssetValue", termStrategy.totalAssetValue()); + + vm.prank(keeper); + ITokenizedStrategy(address(termStrategy)).report(); + + holdings = termStrategy.repoTokenHoldings(); + + assertEq(holdings.length, 0); + + vm.startPrank(testDepositor); + IERC4626(address(termStrategy)).withdraw( + IERC4626(address(termStrategy)).balanceOf(testDepositor), + testDepositor, + testDepositor + ); + vm.stopPrank(); + } + + function testRedeemMaturedRepoTokensFailure() public { + // start with some initial funds + 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(); + + _sell1RepoTokenNoMint(repoToken2Week, 2e18); + + address[] memory holdings = termStrategy.repoTokenHoldings(); + + assertEq(holdings.length, 1); + + vm.warp(block.timestamp + 3 weeks); + + repoToken2Week.mockServicer().setRedemptionFailure(true); + + vm.prank(keeper); + ITokenizedStrategy(address(termStrategy)).report(); + + holdings = termStrategy.repoTokenHoldings(); + + // TEST: still has 1 repo token because redemption failure + assertEq(holdings.length, 1); + + 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(); + termStrategy.setRepoTokenConcentrationLimit(0.4e18); + + // Set to 40% + vm.prank(governor); + termStrategy.setRepoTokenConcentrationLimit(0.4e18); + + termController.setOracleRate(repoToken2Week.termRepoId(), 0.05e18); + + vm.expectRevert( + abi.encodeWithSelector( + RepoTokenList.InvalidRepoToken.selector, + address(0) + ) + ); + termStrategy.getRepoTokenConcentrationRatio(address(0)); + + uint256 concentrationLimit = termStrategy + .getRepoTokenConcentrationRatio(address(repoToken2Week)); + + _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(); + termStrategy.pauseStrategy(); + + vm.prank(governor); + termStrategy.pauseStrategy(); + + 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"); + + vm.prank(governor); + termStrategy.unpauseStrategy(); + vm.prank(testDepositor); + IERC4626(address(termStrategy)).deposit(depositAmount, testDepositor); + vm.stopPrank(); + } +} diff --git a/src/test/TestUSDCSubmitOffer.t.sol b/src/test/TestUSDCSubmitOffer.t.sol index 64733de1..467a4685 100644 --- a/src/test/TestUSDCSubmitOffer.t.sol +++ b/src/test/TestUSDCSubmitOffer.t.sol @@ -1,313 +1,409 @@ -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(governor); - termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); - termStrategy.setTimeToMaturityThreshold(3 weeks); - termStrategy.setRepoTokenConcentrationLimit(1e18); - termStrategy.setRequiredReserveRatio(0); - termStrategy.setDiscountRateMarkup(0); - 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( - repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount - ); - - vm.prank(management); - offerIds = termStrategy.submitAuctionOffer( - 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( - repoToken1WeekAuction, address(repoToken1Week), offerId1, 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 testEditOfferWithConcentrationLimit() public { - bytes32 idHash1 = bytes32("offer id hash 1"); - - vm.prank(governor); - termStrategy.setRepoTokenConcentrationLimit(0.5e18); - - // 50% concentration - bytes32 offerId1 = _submitOffer(idHash1, 50e6); - - // 60% concentration should fail (> 50%) - vm.expectRevert(abi.encodeWithSelector(Strategy.RepoTokenConcentrationTooHigh.selector, address(repoToken1Week))); - vm.prank(management); - bytes32[] memory offerIds = termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), offerId1, bytes32("test price"), 60e6 - ); - - // 40% concentration should pass - vm.prank(management); - offerIds = termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), offerId1, bytes32("test price"), 40e6 - ); - } - - 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 testAuctionCancelForWithdrawal() public { - bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); - - repoToken1WeekAuction.auctionCancelForWithdrawal(); - - // 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], offerId1); - assertEq(offers[1], offerId2); - } - - 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(offerId1, 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(offerId1, 50e6); - - assertEq(termStrategy.totalLiquidBalance(), 50e6); - } -} +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(governor); + termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); + termStrategy.setTimeToMaturityThreshold(3 weeks); + termStrategy.setRepoTokenConcentrationLimit(1e18); + termStrategy.setRequiredReserveRatio(0); + termStrategy.setDiscountRateMarkup(0); + 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( + repoToken1WeekAuction, + address(repoToken1Week), + idHash, + bytes32("test price"), + offerAmount + ); + + vm.prank(management); + offerIds = termStrategy.submitAuctionOffer( + 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( + repoToken1WeekAuction, + address(repoToken1Week), + offerId1, + 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 testEditOfferWithConcentrationLimit() public { + bytes32 idHash1 = bytes32("offer id hash 1"); + + vm.prank(governor); + termStrategy.setRepoTokenConcentrationLimit(0.5e18); + + // 50% concentration + bytes32 offerId1 = _submitOffer(idHash1, 50e6); + + // 60% concentration should fail (> 50%) + vm.expectRevert( + abi.encodeWithSelector( + Strategy.RepoTokenConcentrationTooHigh.selector, + address(repoToken1Week) + ) + ); + vm.prank(management); + bytes32[] memory offerIds = termStrategy.submitAuctionOffer( + repoToken1WeekAuction, + address(repoToken1Week), + offerId1, + bytes32("test price"), + 60e6 + ); + + // 40% concentration should pass + vm.prank(management); + offerIds = termStrategy.submitAuctionOffer( + repoToken1WeekAuction, + address(repoToken1Week), + offerId1, + bytes32("test price"), + 40e6 + ); + } + + 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 testAuctionCancelForWithdrawal() public { + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); + + repoToken1WeekAuction.auctionCancelForWithdrawal(); + + // 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], offerId1); + assertEq(offers[1], offerId2); + } + + 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(offerId1, 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(offerId1, 50e6); + + assertEq(termStrategy.totalLiquidBalance(), 50e6); + } +} diff --git a/src/test/kontrol/Counterexamples.t.sol b/src/test/kontrol/Counterexamples.t.sol index 070db6b3..cbeca53a 100644 --- a/src/test/kontrol/Counterexamples.t.sol +++ b/src/test/kontrol/Counterexamples.t.sol @@ -38,10 +38,18 @@ contract CounterexamplesTest is Test { // Deploy two repo tokens with the same maturity date MockTermRepoToken repoToken1 = new MockTermRepoToken( - bytes32("test repo token 1"), address(mockUSDC), address(mockCollateral), 1e18, block.timestamp + 1 weeks + bytes32("test repo token 1"), + address(mockUSDC), + address(mockCollateral), + 1e18, + block.timestamp + 1 weeks ); MockTermRepoToken repoToken2 = new MockTermRepoToken( - bytes32("test repo token 2"), address(mockUSDC), address(mockCollateral), 1e18, block.timestamp + 1 weeks + bytes32("test repo token 2"), + address(mockUSDC), + address(mockCollateral), + 1e18, + block.timestamp + 1 weeks ); // Initialize list to repoToken1 -> repoToken2 -> NULL_NODE diff --git a/src/test/kontrol/KontrolTest.sol b/src/test/kontrol/KontrolTest.sol index 83e0650f..62dad4c1 100644 --- a/src/test/kontrol/KontrolTest.sol +++ b/src/test/kontrol/KontrolTest.sol @@ -23,7 +23,10 @@ contract KontrolTest is Test, KontrolCheats { Assert } - function _establish(Mode mode, bool condition) internal pure returns (bool) { + function _establish( + Mode mode, + bool condition + ) internal pure returns (bool) { if (mode == Mode.Assume) { vm.assume(condition); return true; @@ -59,7 +62,13 @@ contract KontrolTest is Test, KontrolCheats { return mask & (slotValue >> shift); } - function _storeData(address contractAddress, uint256 slot, uint256 offset, uint256 width, uint256 value) internal { + function _storeData( + address contractAddress, + uint256 slot, + uint256 offset, + uint256 width, + uint256 value + ) internal { // `offset` and `width` must not overflow the slot assert(offset + width <= 32); // and `value` must fit into the designated part @@ -83,23 +92,41 @@ contract KontrolTest is Test, KontrolCheats { vm.store(contractAddress, bytes32(slot), bytes32(slotValue)); } - function _loadUInt256(address contractAddress, uint256 slot) internal view returns (uint256) { + function _loadUInt256( + address contractAddress, + uint256 slot + ) internal view returns (uint256) { return _loadData(contractAddress, slot, 0, 32); } - function _loadAddress(address contractAddress, uint256 slot) internal view returns (address) { + function _loadAddress( + address contractAddress, + uint256 slot + ) internal view returns (address) { return address(uint160(_loadData(contractAddress, slot, 0, 20))); } - function _storeUInt256(address contractAddress, uint256 slot, uint256 value) internal { + function _storeUInt256( + address contractAddress, + uint256 slot, + uint256 value + ) internal { _storeData(contractAddress, slot, 0, 32, value); } - function _storeAddress(address contractAddress, uint256 slot, address value) internal { + function _storeAddress( + address contractAddress, + uint256 slot, + address value + ) internal { _storeData(contractAddress, slot, 0, 20, uint160(value)); } - function _storeBytes32(address contractAddress, uint256 slot, bytes32 value) internal { + function _storeBytes32( + address contractAddress, + uint256 slot, + bytes32 value + ) internal { _storeUInt256(contractAddress, slot, uint256(value)); } diff --git a/src/test/kontrol/RepoToken.sol b/src/test/kontrol/RepoToken.sol index 84375a84..4cd8dc33 100644 --- a/src/test/kontrol/RepoToken.sol +++ b/src/test/kontrol/RepoToken.sol @@ -41,11 +41,19 @@ contract RepoToken is ITermRepoToken, KontrolTest { termRepoServicerSlot := _termRepoServicer.slot termRepoCollateralManagerSlot := _termRepoCollateralManager.slot } - _storeUInt256(address(this), termRepoServicerSlot, uint256(uint160(address(termRepoServicer)))); + _storeUInt256( + address(this), + termRepoServicerSlot, + uint256(uint160(address(termRepoServicer))) + ); TermRepoCollateralManager termRepoCollateralManager = new TermRepoCollateralManager(); termRepoCollateralManager.initializeSymbolic(); - _storeUInt256(address(this), termRepoCollateralManagerSlot, uint256(uint160(address(termRepoCollateralManager)))); + _storeUInt256( + address(this), + termRepoCollateralManagerSlot, + uint256(uint160(address(termRepoCollateralManager))) + ); } function decimals() public pure returns (uint8) { @@ -60,12 +68,16 @@ contract RepoToken is ITermRepoToken, KontrolTest { return _redemptionValue; } - function config() external view returns ( - uint256 redemptionTimestamp, - address purchaseToken, - address termRepoServicer, - address termRepoCollateralManager - ) { + function config() + external + view + returns ( + uint256 redemptionTimestamp, + address purchaseToken, + address termRepoServicer, + address termRepoCollateralManager + ) + { redemptionTimestamp = _redemptionTimestamp; purchaseToken = _purchaseToken; termRepoServicer = address(_termRepoServicer); @@ -76,7 +88,10 @@ contract RepoToken is ITermRepoToken, KontrolTest { return bytes32(freshUInt256()); } - function allowance(address owner, address spender) external view returns (uint256) { + function allowance( + address owner, + address spender + ) external view returns (uint256) { return freshUInt256(); } @@ -94,7 +109,11 @@ contract RepoToken is ITermRepoToken, KontrolTest { return kevm.freshBool() > 0; } - function transferFrom(address from, address to, uint256 amount) external returns (bool) { + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool) { kevm.symbolicStorage(address(this)); return kevm.freshBool() > 0; } diff --git a/src/test/kontrol/RepoTokenListInvariants.t.sol b/src/test/kontrol/RepoTokenListInvariants.t.sol index bef6aa32..fbfa4aa5 100644 --- a/src/test/kontrol/RepoTokenListInvariants.t.sol +++ b/src/test/kontrol/RepoTokenListInvariants.t.sol @@ -50,7 +50,6 @@ contract RepoTokenListInvariantsTest is RepoTokenListTest { // Call the function being tested _repoTokenList.insertSorted(repoToken); - // Assert that the size of the list increased by 1 assert(_countNodesInList() == count + 1); @@ -70,9 +69,7 @@ contract RepoTokenListInvariantsTest is RepoTokenListTest { * Test that insertSorted preserves the list invariants when trying to * insert a RepoToken that is already in the list. */ - function testInsertSortedDuplicateToken( - address repoToken - ) external { + function testInsertSortedDuplicateToken(address repoToken) external { // Our initialization procedure guarantees this invariant, // so we assert instead of assuming _establishNoDuplicateTokens(Mode.Assert); @@ -144,40 +141,35 @@ contract RepoTokenListInvariantsTest is RepoTokenListTest { _establishPositiveBalance(Mode.Assert); } - function testGetCumulativeDataEmpty( + function testGetCumulativeDataEmpty( address repoToken, uint256 repoTokenAmount, uint256 purchaseTokenPrecision ) external { _initializeRepoTokenListEmpty(); - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); ( uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeRepoTokenAmount, bool found ) = _repoTokenList.getCumulativeRepoTokenData( - ITermDiscountRateAdapter(address(discountRateAdapter)), - repoToken, - repoTokenAmount, - purchaseTokenPrecision - ); + ITermDiscountRateAdapter(address(discountRateAdapter)), + repoToken, + repoTokenAmount, + purchaseTokenPrecision + ); assert(cumulativeWeightedTimeToMaturity == 0); assert(cumulativeRepoTokenAmount == 0); assert(found == false); } - - function testGetPresentValueEmpty( - uint256 purchaseTokenPrecision - ) external { + function testGetPresentValueEmpty(uint256 purchaseTokenPrecision) external { _initializeRepoTokenListEmpty(); - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); uint256 totalPresentValue = _repoTokenList.getPresentValue( ITermDiscountRateAdapter(address(discountRateAdapter)), @@ -196,8 +188,7 @@ contract RepoTokenListInvariantsTest is RepoTokenListTest { _establishPositiveBalance(Mode.Assume); // Initialize a DiscountRateAdapter with symbolic storage - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); _initializeDiscountRateAdapter(discountRateAdapter); // Consider only the case where we are not trying to match a token @@ -209,48 +200,44 @@ contract RepoTokenListInvariantsTest is RepoTokenListTest { uint256 cumulativeRepoTokenAmount, bool found ) = _repoTokenList.getCumulativeRepoTokenData( - discountRateAdapter, - repoToken, - repoTokenAmount, - purchaseTokenPrecision - ); + discountRateAdapter, + repoToken, + repoTokenAmount, + purchaseTokenPrecision + ); assert(!found); // Removes matured tokens and returns their total value - uint256 cumulativeRepoTokenAmountMatured - = _filterMaturedTokensGetTotalValue( - discountRateAdapter, - purchaseTokenPrecision - ); + uint256 cumulativeRepoTokenAmountMatured = _filterMaturedTokensGetTotalValue( + discountRateAdapter, + purchaseTokenPrecision + ); // Simplified calculation for no matured tokens ( - uint256 cumulativeWeightedTimeToMaturityNotMatured, - uint256 cumulativeRepoTokenAmountNotMatured + uint256 cumulativeWeightedTimeToMaturityNotMatured, + uint256 cumulativeRepoTokenAmountNotMatured ) = _cumulativeRepoTokenDataNotMatured( - discountRateAdapter, - purchaseTokenPrecision - ); + discountRateAdapter, + purchaseTokenPrecision + ); assert( cumulativeWeightedTimeToMaturity == - cumulativeWeightedTimeToMaturityNotMatured + cumulativeWeightedTimeToMaturityNotMatured ); assert( cumulativeRepoTokenAmount == - cumulativeRepoTokenAmountMatured + cumulativeRepoTokenAmountNotMatured + cumulativeRepoTokenAmountMatured + + cumulativeRepoTokenAmountNotMatured ); } - - function testGetPresentTotalValue( - uint256 purchaseTokenPrecision - ) external { + function testGetPresentTotalValue(uint256 purchaseTokenPrecision) external { // Initialize a DiscountRateAdapter with symbolic storage - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); _initializeDiscountRateAdapter(discountRateAdapter); vm.assume(0 < purchaseTokenPrecision); @@ -272,6 +259,9 @@ contract RepoTokenListInvariantsTest is RepoTokenListTest { purchaseTokenPrecision ); - assert(totalPresentValue == totalPresentValueMatured + totalPresentValueNotMatured); + assert( + totalPresentValue == + totalPresentValueMatured + totalPresentValueNotMatured + ); } } diff --git a/src/test/kontrol/RepoTokenListTest.t.sol b/src/test/kontrol/RepoTokenListTest.t.sol index 4b67d985..651eb517 100644 --- a/src/test/kontrol/RepoTokenListTest.t.sol +++ b/src/test/kontrol/RepoTokenListTest.t.sol @@ -13,7 +13,6 @@ import "src/test/kontrol/TermAuction.sol"; import "src/test/kontrol/TermAuctionOfferLocker.sol"; import "src/test/kontrol/TermDiscountRateAdapter.sol"; - contract RepoTokenListTest is KontrolTest { using RepoTokenList for RepoTokenListData; @@ -32,14 +31,18 @@ contract RepoTokenListTest is KontrolTest { /** * Return the maturity timestamp of the given RepoToken. */ - function _getRepoTokenMaturity(address repoToken) internal view returns (uint256 redemptionTimestamp) { - (redemptionTimestamp, , ,) = ITermRepoToken(repoToken).config(); + function _getRepoTokenMaturity( + address repoToken + ) internal view returns (uint256 redemptionTimestamp) { + (redemptionTimestamp, , , ) = ITermRepoToken(repoToken).config(); } /** * Return the this contract's balance in the given RepoToken. */ - function _getRepoTokenBalance(address repoToken) internal view returns (uint256) { + function _getRepoTokenBalance( + address repoToken + ) internal view returns (uint256) { return ITermRepoToken(repoToken).balanceOf(address(this)); } @@ -109,7 +112,9 @@ contract RepoTokenListTest is KontrolTest { return false; } - function _repoTokensListToArray(uint256 length) internal view returns (address[] memory repoTokens) { + function _repoTokensListToArray( + uint256 length + ) internal view returns (address[] memory repoTokens) { address current = _repoTokenList.head; uint256 i; repoTokens = new address[](length); @@ -142,28 +147,35 @@ contract RepoTokenListTest is KontrolTest { function _cumulativeRepoTokenDataNotMatured( ITermDiscountRateAdapter discountRateAdapter, uint256 purchaseTokenPrecision - ) internal view returns ( - uint256 cumulativeWeightedTimeToMaturity, - uint256 cumulativeRepoTokenAmount - ) { + ) + internal + view + returns ( + uint256 cumulativeWeightedTimeToMaturity, + uint256 cumulativeRepoTokenAmount + ) + { address current = _repoTokenList.head; while (current != RepoTokenList.NULL_NODE) { - (uint256 currentMaturity, , ,) = ITermRepoToken(current).config(); - uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - uint256 repoRedemptionHaircut = discountRateAdapter.repoRedemptionHaircut(current); + (uint256 currentMaturity, , , ) = ITermRepoToken(current).config(); + uint256 repoTokenBalance = ITermRepoToken(current).balanceOf( + address(this) + ); + uint256 repoRedemptionHaircut = discountRateAdapter + .repoRedemptionHaircut(current); uint256 timeToMaturity = currentMaturity - block.timestamp; - uint256 repoTokenAmountInBaseAssetPrecision = - RepoTokenUtils.getNormalizedRepoTokenAmount( + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( current, repoTokenBalance, purchaseTokenPrecision, repoRedemptionHaircut ); - uint256 weightedTimeToMaturity = - timeToMaturity * repoTokenAmountInBaseAssetPrecision; + uint256 weightedTimeToMaturity = timeToMaturity * + repoTokenAmountInBaseAssetPrecision; cumulativeWeightedTimeToMaturity += weightedTimeToMaturity; cumulativeRepoTokenAmount += repoTokenAmountInBaseAssetPrecision; @@ -184,14 +196,17 @@ contract RepoTokenListTest is KontrolTest { while (current != RepoTokenList.NULL_NODE) { address next = _repoTokenList.nodes[current].next; - (uint256 currentMaturity, , ,) = ITermRepoToken(current).config(); + (uint256 currentMaturity, , , ) = ITermRepoToken(current).config(); if (currentMaturity <= block.timestamp) { - uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - uint256 repoRedemptionHaircut = discountRateAdapter.repoRedemptionHaircut(current); - - totalPresentValue += - RepoTokenUtils.getNormalizedRepoTokenAmount( + uint256 repoTokenBalance = ITermRepoToken(current).balanceOf( + address(this) + ); + uint256 repoRedemptionHaircut = discountRateAdapter + .repoRedemptionHaircut(current); + + totalPresentValue += RepoTokenUtils + .getNormalizedRepoTokenAmount( current, repoTokenBalance, purchaseTokenPrecision, @@ -200,13 +215,12 @@ contract RepoTokenListTest is KontrolTest { if (current == _repoTokenList.head) { _repoTokenList.head = next; - } - else { + } else { _repoTokenList.nodes[prev].next = next; current = prev; } } - + prev = current; current = next; } @@ -223,26 +237,30 @@ contract RepoTokenListTest is KontrolTest { uint256 totalPresentValue = 0; while (current != RepoTokenList.NULL_NODE) { - (uint256 currentMaturity, , ,) = ITermRepoToken(current).config(); - uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - uint256 repoRedemptionHaircut = discountRateAdapter.repoRedemptionHaircut(current); + (uint256 currentMaturity, , , ) = ITermRepoToken(current).config(); + uint256 repoTokenBalance = ITermRepoToken(current).balanceOf( + address(this) + ); + uint256 repoRedemptionHaircut = discountRateAdapter + .repoRedemptionHaircut(current); uint256 discountRate = discountRateAdapter.getDiscountRate(current); uint256 timeToMaturity = currentMaturity - block.timestamp; - uint256 repoTokenAmountInBaseAssetPrecision = - RepoTokenUtils.getNormalizedRepoTokenAmount( + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( current, repoTokenBalance, purchaseTokenPrecision, repoRedemptionHaircut ); - uint256 timeLeftToMaturityDayFraction = - (timeToMaturity * purchaseTokenPrecision) / 360 days; + uint256 timeLeftToMaturityDayFraction = (timeToMaturity * + purchaseTokenPrecision) / 360 days; - uint256 presentValue = - (repoTokenAmountInBaseAssetPrecision * purchaseTokenPrecision) / - (purchaseTokenPrecision + (discountRate * timeLeftToMaturityDayFraction / 1e18)); + uint256 presentValue = (repoTokenAmountInBaseAssetPrecision * + purchaseTokenPrecision) / + (purchaseTokenPrecision + + ((discountRate * timeLeftToMaturityDayFraction) / 1e18)); totalPresentValue += presentValue; @@ -252,15 +270,18 @@ contract RepoTokenListTest is KontrolTest { return totalPresentValue; } - function _establishInsertListPreservation(address insertedRepoToken, address[] memory repoTokens, uint256 repoTokensCount) internal view { + function _establishInsertListPreservation( + address insertedRepoToken, + address[] memory repoTokens, + uint256 repoTokensCount + ) internal view { address current = _repoTokenList.head; uint256 i = 0; - if(insertedRepoToken != address(0)) { - + if (insertedRepoToken != address(0)) { while (current != RepoTokenList.NULL_NODE && i < repoTokensCount) { - if(current != repoTokens[i]) { - assert (current == insertedRepoToken); + if (current != repoTokens[i]) { + assert(current == insertedRepoToken); current = _repoTokenList.nodes[current].next; break; } @@ -269,7 +290,7 @@ contract RepoTokenListTest is KontrolTest { } if (current != RepoTokenList.NULL_NODE && i == repoTokensCount) { - assert (current == insertedRepoToken); + assert(current == insertedRepoToken); } } @@ -279,12 +300,15 @@ contract RepoTokenListTest is KontrolTest { } } - function _establishRemoveListPreservation(address[] memory repoTokens, uint256 repoTokensCount) internal view { + function _establishRemoveListPreservation( + address[] memory repoTokens, + uint256 repoTokensCount + ) internal view { address current = _repoTokenList.head; uint256 i = 0; while (current != RepoTokenList.NULL_NODE && i < repoTokensCount) { - if(current == repoTokens[i++]) { + if (current == repoTokens[i++]) { current = _repoTokenList.nodes[current].next; } } @@ -367,7 +391,9 @@ contract RepoTokenListTest is KontrolTest { * Note: This is equivalent to the above invariant if the NoMaturedTokens * invariant also holds. */ - function _establishPositiveBalanceForNonMaturedTokens(Mode mode) internal view { + function _establishPositiveBalanceForNonMaturedTokens( + Mode mode + ) internal view { address current = _repoTokenList.head; while (current != RepoTokenList.NULL_NODE) { @@ -390,10 +416,10 @@ contract RepoTokenListTest is KontrolTest { address current = _repoTokenList.head; while (current != RepoTokenList.NULL_NODE) { - (, , address repoServicer,) = ITermRepoToken(current).config(); + (, , address repoServicer, ) = ITermRepoToken(current).config(); TermRepoServicer(repoServicer).guaranteeRedeemAlwaysSucceeds(); current = _repoTokenList.nodes[current].next; } } -} \ No newline at end of file +} diff --git a/src/test/kontrol/TermAuctionListInvariants.t.sol b/src/test/kontrol/TermAuctionListInvariants.t.sol index c8c8826f..409b748b 100644 --- a/src/test/kontrol/TermAuctionListInvariants.t.sol +++ b/src/test/kontrol/TermAuctionListInvariants.t.sol @@ -15,7 +15,10 @@ import "src/test/kontrol/TermAuctionListTest.t.sol"; import "src/test/kontrol/TermAuctionOfferLocker.sol"; import "src/test/kontrol/TermDiscountRateAdapter.sol"; -contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest { +contract TermAuctionListInvariantsTest is + RepoTokenListTest, + TermAuctionListTest +{ using TermAuctionList for TermAuctionListData; using RepoTokenList for RepoTokenListData; @@ -33,7 +36,10 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest * Test that insertPending preserves the list invariants when a new offer * is added (that was not present in the list before). */ - function testInsertPendingNewOffer(bytes32 offerId, address asset) external { + function testInsertPendingNewOffer( + bytes32 offerId, + address asset + ) external { // offerId must not equal zero, otherwise the linked list breaks vm.assume(offerId != TermAuctionList.NULL_NODE); @@ -62,13 +68,19 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest // Initialize RepoToken and OfferLocker, making sure that the addresses // also don't overlap with the symbolic auction - (RepoToken repoToken, TermAuctionOfferLocker offerLocker) = - this.newRepoTokenAndOfferLocker(); + (RepoToken repoToken, TermAuctionOfferLocker offerLocker) = this + .newRepoTokenAndOfferLocker(); offerLocker.initializeSymbolicLockedOfferFor(offerId); - (,, address termRepoServicer, address termRepoCollateralManager) = - repoToken.config(); + ( + , + , + address termRepoServicer, + address termRepoCollateralManager + ) = repoToken.config(); _assumeRepoTokenValidate(address(repoToken), asset, true); - vm.assume(0 < TermAuctionOfferLocker(offerLocker).lockedOfferAmount(offerId)); + vm.assume( + 0 < TermAuctionOfferLocker(offerLocker).lockedOfferAmount(offerId) + ); vm.assume(auction != address(repoToken)); vm.assume(auction != address(offerLocker)); vm.assume(auction != termRepoServicer); @@ -84,7 +96,8 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest // Build new PendingOffer PendingOffer memory pendingOffer; pendingOffer.repoToken = address(repoToken); - pendingOffer.offerAmount = TermAuctionOfferLocker(offerLocker).lockedOfferAmount(offerId); + pendingOffer.offerAmount = TermAuctionOfferLocker(offerLocker) + .lockedOfferAmount(offerId); pendingOffer.termAuction = ITermAuction(auction); pendingOffer.offerLocker = ITermAuctionOfferLocker(offerLocker); @@ -111,7 +124,6 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest _assertRepoTokensValidate(asset); } - /** * Test that insertPending preserves the list invariants when trying to * insert an offer that is already in the list. @@ -151,7 +163,11 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest vm.assume(offer.offerLocker == pendingOffer.offerLocker); // This is being checked by Strategy.submitAuctionOffer vm.assume(pendingOffer.offerAmount > 0); - vm.assume(pendingOffer.offerAmount == TermAuctionOfferLocker(address(pendingOffer.offerLocker)).lockedOfferAmount(offerId)); + vm.assume( + pendingOffer.offerAmount == + TermAuctionOfferLocker(address(pendingOffer.offerLocker)) + .lockedOfferAmount(offerId) + ); // Call the function being tested _termAuctionList.insertPending(offerId, pendingOffer); @@ -188,40 +204,57 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest } } - function _assumeRepoTokenValidate(address repoToken, address asset, bool assumeTimestamp) internal view { + function _assumeRepoTokenValidate( + address repoToken, + address asset, + bool assumeTimestamp + ) internal view { ( - uint256 redemptionTimestamp, - address purchaseToken, - , - address collateralManager + uint256 redemptionTimestamp, + address purchaseToken, + , + address collateralManager ) = ITermRepoToken(repoToken).config(); vm.assume(purchaseToken == asset); - if(assumeTimestamp) { + if (assumeTimestamp) { vm.assume(block.timestamp <= redemptionTimestamp); } - uint256 numTokens = ITermRepoCollateralManager(collateralManager).numOfAcceptedCollateralTokens(); + uint256 numTokens = ITermRepoCollateralManager(collateralManager) + .numOfAcceptedCollateralTokens(); for (uint256 i; i < numTokens; i++) { - address currentToken = ITermRepoCollateralManager(collateralManager).collateralTokens(i); - uint256 minCollateralRatio = _repoTokenList.collateralTokenParams[currentToken]; + address currentToken = ITermRepoCollateralManager(collateralManager) + .collateralTokens(i); + uint256 minCollateralRatio = _repoTokenList.collateralTokenParams[ + currentToken + ]; vm.assume(minCollateralRatio != 0); - vm.assume(ITermRepoCollateralManager(collateralManager).maintenanceCollateralRatios(currentToken) >= minCollateralRatio); + vm.assume( + ITermRepoCollateralManager(collateralManager) + .maintenanceCollateralRatios(currentToken) >= + minCollateralRatio + ); } } - function _assumeRepoTokensValidate(address asset, bool assumeTimestamp) internal view { + function _assumeRepoTokensValidate( + address asset, + bool assumeTimestamp + ) internal view { bytes32 current = _termAuctionList.head; while (current != TermAuctionList.NULL_NODE) { address repoToken = _termAuctionList.offers[current].repoToken; - if(assumeTimestamp) { + if (assumeTimestamp) { _assumeRepoTokenValidate(repoToken, asset, true); - } - else { - bool auctionCompleted = _termAuctionList.offers[current].termAuction.auctionCompleted(); + } else { + bool auctionCompleted = _termAuctionList + .offers[current] + .termAuction + .auctionCompleted(); _assumeRepoTokenValidate(repoToken, asset, !auctionCompleted); } @@ -234,7 +267,10 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest while (current != TermAuctionList.NULL_NODE) { address repoToken = _termAuctionList.offers[current].repoToken; - (bool isRepoTokenValid, ) = _repoTokenList.validateRepoToken(ITermRepoToken(repoToken), asset); + (bool isRepoTokenValid, ) = _repoTokenList.validateRepoToken( + ITermRepoToken(repoToken), + asset + ); assert(isRepoTokenValid); current = _termAuctionList.nodes[current].next; @@ -246,7 +282,7 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest while (current != TermAuctionList.NULL_NODE) { address repoToken = _termAuctionList.offers[current].repoToken; - (, , address repoServicer,) = ITermRepoToken(repoToken).config(); + (, , address repoServicer, ) = ITermRepoToken(repoToken).config(); TermRepoServicer(repoServicer).guaranteeRedeemAlwaysSucceeds(); current = _termAuctionList.nodes[current].next; @@ -260,8 +296,7 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest // For simplicity, assume that the RepoTokenList is empty _repoTokenList.head = RepoTokenList.NULL_NODE; // Initialize a DiscountRateAdapter with symbolic storage - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); _initializeDiscountRateAdapterOffers(discountRateAdapter); // Our initialization procedure guarantees these invariants, @@ -315,20 +350,19 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest ) external { _initializeTermAuctionListEmpty(); - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); ( uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeOfferAmount, bool found ) = _termAuctionList.getCumulativeOfferData( - _repoTokenList, - ITermDiscountRateAdapter(address(discountRateAdapter)), - repoToken, - newOfferAmount, - purchaseTokenPrecision - ); + _repoTokenList, + ITermDiscountRateAdapter(address(discountRateAdapter)), + repoToken, + newOfferAmount, + purchaseTokenPrecision + ); assert(cumulativeWeightedTimeToMaturity == 0); assert(cumulativeOfferAmount == 0); @@ -341,8 +375,7 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest ) external { _initializeTermAuctionListEmpty(); - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); uint256 totalPresentValue = _termAuctionList.getPresentValue( _repoTokenList, @@ -363,8 +396,7 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest uint256 purchaseTokenPrecision ) external { // Initialize a DiscountRateAdapter with symbolic storage - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); _initializeDiscountRateAdapterOffers(discountRateAdapter); _establishNoCompletedAuctions(Mode.Assume); @@ -378,25 +410,27 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest uint256 cumulativeOfferAmount, bool found ) = _termAuctionList.getCumulativeOfferData( - _repoTokenList, - ITermDiscountRateAdapter(address(discountRateAdapter)), - repoToken, - newOfferAmount, - purchaseTokenPrecision - ); + _repoTokenList, + ITermDiscountRateAdapter(address(discountRateAdapter)), + repoToken, + newOfferAmount, + purchaseTokenPrecision + ); ( uint256 cumulativeWeightedTimeToMaturityNoCompletedAuctions, uint256 cumulativeOfferAmountNoCompletedAuctions, bool foundNoCompletedAuctions - ) = _getCumulativeOfferTimeAndAmount( - repoToken, - newOfferAmount ); + ) = _getCumulativeOfferTimeAndAmount(repoToken, newOfferAmount); - assert(cumulativeWeightedTimeToMaturity == cumulativeWeightedTimeToMaturityNoCompletedAuctions); - assert(cumulativeOfferAmount == cumulativeOfferAmountNoCompletedAuctions); + assert( + cumulativeWeightedTimeToMaturity == + cumulativeWeightedTimeToMaturityNoCompletedAuctions + ); + assert( + cumulativeOfferAmount == cumulativeOfferAmountNoCompletedAuctions + ); assert(found == foundNoCompletedAuctions); - } /* If there are no completed auctions in the list then getCumulativeOfferData should return the sum @@ -408,8 +442,7 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest uint256 purchaseTokenPrecision ) external { // Initialize a DiscountRateAdapter with symbolic storage - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); _initializeDiscountRateAdapterOffers(discountRateAdapter); _establishCompletedAuctions(Mode.Assume); @@ -428,22 +461,25 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest uint256 cumulativeOfferAmount, bool found ) = _termAuctionList.getCumulativeOfferData( - _repoTokenList, - ITermDiscountRateAdapter(address(discountRateAdapter)), - repoToken, - newOfferAmount, - purchaseTokenPrecision - ); + _repoTokenList, + ITermDiscountRateAdapter(address(discountRateAdapter)), + repoToken, + newOfferAmount, + purchaseTokenPrecision + ); ( uint256 cumulativeWeightedTimeToMaturityCompletedAuctions, uint256 cumulativeOfferAmountCompletedAuctions ) = _getGroupedOfferTimeAndAmount( - ITermDiscountRateAdapter(address(discountRateAdapter)), - purchaseTokenPrecision - ); + ITermDiscountRateAdapter(address(discountRateAdapter)), + purchaseTokenPrecision + ); - assert(cumulativeWeightedTimeToMaturity == cumulativeWeightedTimeToMaturityCompletedAuctions); + assert( + cumulativeWeightedTimeToMaturity == + cumulativeWeightedTimeToMaturityCompletedAuctions + ); assert(cumulativeOfferAmount == cumulativeOfferAmountCompletedAuctions); } @@ -463,8 +499,7 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest delete _termAuctionList.offers[current]; if (current == _termAuctionList.head) { _termAuctionList.head = next; - } - else { + } else { _termAuctionList.nodes[prev].next = next; current = prev; } @@ -480,8 +515,7 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest uint256 purchaseTokenPrecision ) external { // Initialize a DiscountRateAdapter with symbolic storage - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); _initializeDiscountRateAdapterOffers(discountRateAdapter); _assumeNonMaturedRepoTokens(); @@ -499,12 +533,12 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest uint256 cumulativeOfferAmount, bool found ) = _termAuctionList.getCumulativeOfferData( - _repoTokenList, - ITermDiscountRateAdapter(address(discountRateAdapter)), - repoToken, - newOfferAmount, - purchaseTokenPrecision - ); + _repoTokenList, + ITermDiscountRateAdapter(address(discountRateAdapter)), + repoToken, + newOfferAmount, + purchaseTokenPrecision + ); assert(!found); @@ -519,10 +553,21 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest ( uint256 cumulativeWeightedTimeToMaturityCompletedAuctions, uint256 cumulativeOfferAmountCompletedAuctions - ) = _getCumulativeOfferDataCompletedAuctions(discountRateAdapter, purchaseTokenPrecision); - - assert(cumulativeWeightedTimeToMaturity == cumulativeWeightedTimeToMaturityIncompletedAuctions + cumulativeWeightedTimeToMaturityCompletedAuctions); - assert(cumulativeOfferAmount == cumulativeOfferAmountIncompletedAuctions + cumulativeOfferAmountCompletedAuctions); + ) = _getCumulativeOfferDataCompletedAuctions( + discountRateAdapter, + purchaseTokenPrecision + ); + + assert( + cumulativeWeightedTimeToMaturity == + cumulativeWeightedTimeToMaturityIncompletedAuctions + + cumulativeWeightedTimeToMaturityCompletedAuctions + ); + assert( + cumulativeOfferAmount == + cumulativeOfferAmountIncompletedAuctions + + cumulativeOfferAmountCompletedAuctions + ); } function testGetPresentTotalValue( @@ -530,8 +575,7 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest address repoTokenToMatch ) external { // Initialize a DiscountRateAdapter with symbolic storage - TermDiscountRateAdapter discountRateAdapter = - new TermDiscountRateAdapter(); + TermDiscountRateAdapter discountRateAdapter = new TermDiscountRateAdapter(); _initializeDiscountRateAdapterOffers(discountRateAdapter); _assumeOfferAmountLocked(); @@ -553,9 +597,14 @@ contract TermAuctionListInvariantsTest is RepoTokenListTest, TermAuctionListTest _filterDiscountRateSet(); _filterRepeatedAuctions(); - uint256 totalValueCompletedAuctions = _getTotalValueCompletedAuctions(ITermDiscountRateAdapter(address(discountRateAdapter)), purchaseTokenPrecision); + uint256 totalValueCompletedAuctions = _getTotalValueCompletedAuctions( + ITermDiscountRateAdapter(address(discountRateAdapter)), + purchaseTokenPrecision + ); - assert(totalPresentValue == totalValueNonCompletedAuctions + totalValueCompletedAuctions); + assert( + totalPresentValue == + totalValueNonCompletedAuctions + totalValueCompletedAuctions + ); } - } diff --git a/src/test/kontrol/TermAuctionListTest.t.sol b/src/test/kontrol/TermAuctionListTest.t.sol index 44661585..c4672864 100644 --- a/src/test/kontrol/TermAuctionListTest.t.sol +++ b/src/test/kontrol/TermAuctionListTest.t.sol @@ -13,7 +13,6 @@ import "src/test/kontrol/TermAuction.sol"; import "src/test/kontrol/TermAuctionOfferLocker.sol"; import "src/test/kontrol/TermDiscountRateAdapter.sol"; - contract TermAuctionListTest is KontrolTest { using TermAuctionList for TermAuctionListData; using RepoTokenList for RepoTokenListData; @@ -23,8 +22,18 @@ contract TermAuctionListTest is KontrolTest { uint256 private _auctionListSlot; - function _auctionListOfferSlot(bytes32 offerId) internal view returns (uint256) { - return uint256(keccak256(abi.encodePacked(uint256(offerId), uint256(_auctionListSlot + 2)))); + function _auctionListOfferSlot( + bytes32 offerId + ) internal view returns (uint256) { + return + uint256( + keccak256( + abi.encodePacked( + uint256(offerId), + uint256(_auctionListSlot + 2) + ) + ) + ); } function _setReferenceAuction() internal { @@ -35,37 +44,51 @@ contract TermAuctionListTest is KontrolTest { referenceAuctionSlot := _referenceAuction.slot sstore(_auctionListSlot.slot, _termAuctionList.slot) } - _storeUInt256(address(this), referenceAuctionSlot, uint256(uint160(address(new TermAuction())))); + _storeUInt256( + address(this), + referenceAuctionSlot, + uint256(uint160(address(new TermAuction()))) + ); } /** * Set pending offer using slot manipulation directly */ - function _setPendingOffer(bytes32 offerId, address repoToken, uint256 offerAmount, address auction, address offerLocker) internal { + function _setPendingOffer( + bytes32 offerId, + address repoToken, + uint256 offerAmount, + address auction, + address offerLocker + ) internal { uint256 offerSlot = _auctionListOfferSlot(offerId); _storeUInt256(address(this), offerSlot, uint256(uint160(repoToken))); _storeUInt256(address(this), offerSlot + 1, offerAmount); _storeUInt256(address(this), offerSlot + 2, uint256(uint160(auction))); - _storeUInt256(address(this), offerSlot + 3, uint256(uint160(offerLocker))); + _storeUInt256( + address(this), + offerSlot + 3, + uint256(uint160(offerLocker)) + ); } /** * Return the auction for a given offer in the list. */ - function _getAuction(bytes32 offerId) internal view returns(address) { + function _getAuction(bytes32 offerId) internal view returns (address) { return address(_termAuctionList.offers[offerId].termAuction); } /** * Deploy & initialize RepoToken and OfferLocker with the same RepoServicer */ - function newRepoTokenAndOfferLocker() public returns ( - RepoToken repoToken, - TermAuctionOfferLocker offerLocker - ) { + function newRepoTokenAndOfferLocker() + public + returns (RepoToken repoToken, TermAuctionOfferLocker offerLocker) + { repoToken = new RepoToken(); repoToken.initializeSymbolic(); - (, , address termRepoServicer,) = repoToken.config(); + (, , address termRepoServicer, ) = repoToken.config(); offerLocker = new TermAuctionOfferLocker(); offerLocker.initializeSymbolic(termRepoServicer); @@ -76,7 +99,7 @@ contract TermAuctionListTest is KontrolTest { * reducing memory consumption in the caller function */ function etch(address dest, address src) public { - vm.etch(dest, src.code); + vm.etch(dest, src.code); } function _initializeTermAuctionListEmpty() internal { @@ -96,17 +119,17 @@ contract TermAuctionListTest is KontrolTest { while (kevm.freshBool() != 0) { // Create a new auction - if(count == 0 || kevm.freshBool() != 0) { + if (count == 0 || kevm.freshBool() != 0) { // Create sequential addresses to ensure that list is sorted auction = address(uint160(1000 + 2 * count)); // Etch the code of the auction contract into this address this.etch(auction, _referenceAuction); - + TermAuction(auction).initializeSymbolic(); (repoToken, offerLocker) = this.newRepoTokenAndOfferLocker(); } // Else the aution is the same as the previous one on the list - + // Assign each offer an ID based on Strategy._generateOfferId() bytes32 current = keccak256( abi.encodePacked(count, address(this), address(offerLocker)) @@ -121,7 +144,13 @@ contract TermAuctionListTest is KontrolTest { } // Build PendingOffer - _setPendingOffer(current, address(repoToken), freshUInt256(), auction, address(offerLocker)); + _setPendingOffer( + current, + address(repoToken), + freshUInt256(), + auction, + address(offerLocker) + ); previous = current; ++count; @@ -188,7 +217,9 @@ contract TermAuctionListTest is KontrolTest { bytes32 current = _termAuctionList.head; while (current != TermAuctionList.NULL_NODE) { - (uint256 currentMaturity, , ,) = ITermRepoToken(_termAuctionList.offers[current].repoToken).config(); + (uint256 currentMaturity, , , ) = ITermRepoToken( + _termAuctionList.offers[current].repoToken + ).config(); vm.assume(currentMaturity > block.timestamp); current = _termAuctionList.nodes[current].next; @@ -204,14 +235,15 @@ contract TermAuctionListTest is KontrolTest { while (current != TermAuctionList.NULL_NODE) { PendingOffer storage offer = _termAuctionList.offers[current]; - uint256 offerAmount = TermAuctionOfferLocker(address(offer.offerLocker)).lockedOfferAmount(current); + uint256 offerAmount = TermAuctionOfferLocker( + address(offer.offerLocker) + ).lockedOfferAmount(current); if (offer.termAuction.auctionCompleted()) { vm.assume(offerAmount == 0); - } - else { + } else { vm.assume(offerAmount > 0); } - + current = _termAuctionList.nodes[current].next; } } @@ -221,8 +253,11 @@ contract TermAuctionListTest is KontrolTest { while (current != TermAuctionList.NULL_NODE) { address repoToken = _termAuctionList.offers[current].repoToken; - uint256 redemptionValue = ITermRepoToken(repoToken).redemptionValue(); - uint256 repoTokenBalance = ITermRepoToken(repoToken).balanceOf(address(this)); + uint256 redemptionValue = ITermRepoToken(repoToken) + .redemptionValue(); + uint256 repoTokenBalance = ITermRepoToken(repoToken).balanceOf( + address(this) + ); vm.assume(0 < redemptionValue); vm.assume(0 < repoTokenBalance); @@ -230,8 +265,13 @@ contract TermAuctionListTest is KontrolTest { } } - - function _filterCompletedAuctionsGetCumulativeOfferData() internal returns (uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeOfferAmount) { + function _filterCompletedAuctionsGetCumulativeOfferData() + internal + returns ( + uint256 cumulativeWeightedTimeToMaturity, + uint256 cumulativeOfferAmount + ) + { bytes32 current = _termAuctionList.head; bytes32 prev = current; @@ -239,9 +279,15 @@ contract TermAuctionListTest is KontrolTest { bytes32 next = _termAuctionList.nodes[current].next; PendingOffer storage offer = _termAuctionList.offers[current]; - uint256 offerAmount = TermAuctionOfferLocker(address(offer.offerLocker)).lockedOfferAmount(current); + uint256 offerAmount = TermAuctionOfferLocker( + address(offer.offerLocker) + ).lockedOfferAmount(current); if (!offer.termAuction.auctionCompleted()) { - cumulativeWeightedTimeToMaturity += RepoTokenList.getRepoTokenWeightedTimeToMaturity(offer.repoToken, offerAmount); + cumulativeWeightedTimeToMaturity += RepoTokenList + .getRepoTokenWeightedTimeToMaturity( + offer.repoToken, + offerAmount + ); cumulativeOfferAmount += offerAmount; // Update the list to remove the current node @@ -249,8 +295,7 @@ contract TermAuctionListTest is KontrolTest { delete _termAuctionList.offers[current]; if (current == _termAuctionList.head) { _termAuctionList.head = next; - } - else { + } else { _termAuctionList.nodes[prev].next = next; current = prev; } @@ -264,19 +309,20 @@ contract TermAuctionListTest is KontrolTest { bytes32 current = _termAuctionList.head; bytes32 prev = current; address prevAuction = address(0); - + while (current != TermAuctionList.NULL_NODE) { bytes32 next = _termAuctionList.nodes[current].next; - address offerAuction = address(_termAuctionList.offers[current].termAuction); + address offerAuction = address( + _termAuctionList.offers[current].termAuction + ); if (offerAuction == prevAuction) { // Update the list to remove the current node delete _termAuctionList.nodes[current]; delete _termAuctionList.offers[current]; if (current == _termAuctionList.head) { _termAuctionList.head = next; - } - else { + } else { _termAuctionList.nodes[prev].next = next; current = prev; } @@ -287,12 +333,18 @@ contract TermAuctionListTest is KontrolTest { } } - function _getCumulativeOfferTimeAndAmount( address repoToken, uint256 newOfferAmount - ) internal view returns (uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeOfferAmount, bool found) { - + ) + internal + view + returns ( + uint256 cumulativeWeightedTimeToMaturity, + uint256 cumulativeOfferAmount, + bool found + ) + { bytes32 current = _termAuctionList.head; while (current != TermAuctionList.NULL_NODE) { @@ -306,7 +358,11 @@ contract TermAuctionListTest is KontrolTest { offerAmount = offer.offerLocker.lockedOffer(current).amount; } - cumulativeWeightedTimeToMaturity += RepoTokenList.getRepoTokenWeightedTimeToMaturity(offer.repoToken, offerAmount); + cumulativeWeightedTimeToMaturity += RepoTokenList + .getRepoTokenWeightedTimeToMaturity( + offer.repoToken, + offerAmount + ); cumulativeOfferAmount += offerAmount; current = _termAuctionList.nodes[current].next; @@ -316,20 +372,30 @@ contract TermAuctionListTest is KontrolTest { function _getCumulativeOfferDataCompletedAuctions( ITermDiscountRateAdapter discountRateAdapter, uint256 purchaseTokenPrecision - ) internal view returns (uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeOfferAmount) { - + ) + internal + view + returns ( + uint256 cumulativeWeightedTimeToMaturity, + uint256 cumulativeOfferAmount + ) + { bytes32 current = _termAuctionList.head; while (current != TermAuctionList.NULL_NODE) { PendingOffer storage offer = _termAuctionList.offers[current]; uint256 offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( - offer.repoToken, - ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision, - discountRateAdapter.repoRedemptionHaircut(offer.repoToken) - ); + offer.repoToken, + ITermRepoToken(offer.repoToken).balanceOf(address(this)), + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(offer.repoToken) + ); if (offerAmount > 0) { - cumulativeWeightedTimeToMaturity += RepoTokenList.getRepoTokenWeightedTimeToMaturity(offer.repoToken, offerAmount); + cumulativeWeightedTimeToMaturity += RepoTokenList + .getRepoTokenWeightedTimeToMaturity( + offer.repoToken, + offerAmount + ); cumulativeOfferAmount += offerAmount; } @@ -340,8 +406,14 @@ contract TermAuctionListTest is KontrolTest { function _getGroupedOfferTimeAndAmount( ITermDiscountRateAdapter discountRateAdapter, uint256 purchaseTokenPrecision - ) internal view returns (uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeOfferAmount) { - + ) + internal + view + returns ( + uint256 cumulativeWeightedTimeToMaturity, + uint256 cumulativeOfferAmount + ) + { bytes32 current = _termAuctionList.head; address previous = address(0); @@ -352,23 +424,30 @@ contract TermAuctionListTest is KontrolTest { if (address(offer.termAuction) != previous) { offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( - offer.repoToken, - ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision, - discountRateAdapter.repoRedemptionHaircut(offer.repoToken) + offer.repoToken, + ITermRepoToken(offer.repoToken).balanceOf(address(this)), + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); if (offerAmount > 0) { - cumulativeWeightedTimeToMaturity += RepoTokenList.getRepoTokenWeightedTimeToMaturity(offer.repoToken, offerAmount); + cumulativeWeightedTimeToMaturity += RepoTokenList + .getRepoTokenWeightedTimeToMaturity( + offer.repoToken, + offerAmount + ); cumulativeOfferAmount += offerAmount; } } - + previous = address(offer.termAuction); current = _termAuctionList.nodes[current].next; } } - function _filterCompletedAuctionsGetTotalValue() internal returns (uint256 totalValue) { + function _filterCompletedAuctionsGetTotalValue() + internal + returns (uint256 totalValue) + { bytes32 current = _termAuctionList.head; bytes32 prev = current; @@ -378,15 +457,15 @@ contract TermAuctionListTest is KontrolTest { PendingOffer storage offer = _termAuctionList.offers[current]; if (!offer.termAuction.auctionCompleted()) { - totalValue += TermAuctionOfferLocker(address(offer.offerLocker)).lockedOfferAmount(current); + totalValue += TermAuctionOfferLocker(address(offer.offerLocker)) + .lockedOfferAmount(current); // Update the list to remove the current node delete _termAuctionList.nodes[current]; delete _termAuctionList.offers[current]; if (current == _termAuctionList.head) { _termAuctionList.head = next; - } - else { + } else { _termAuctionList.nodes[prev].next = next; current = prev; } @@ -400,17 +479,17 @@ contract TermAuctionListTest is KontrolTest { ITermDiscountRateAdapter discountRateAdapter, uint256 purchaseTokenPrecision ) internal view returns (uint256 totalValue) { - bytes32 current = _termAuctionList.head; while (current != TermAuctionList.NULL_NODE) { PendingOffer storage offer = _termAuctionList.offers[current]; - uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( - offer.repoToken, - ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision, - discountRateAdapter.repoRedemptionHaircut(offer.repoToken) - ); + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + offer.repoToken, + ITermRepoToken(offer.repoToken).balanceOf(address(this)), + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(offer.repoToken) + ); totalValue += RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, purchaseTokenPrecision, @@ -468,7 +547,10 @@ contract TermAuctionListTest is KontrolTest { while (current != TermAuctionList.NULL_NODE) { PendingOffer storage offer = _termAuctionList.offers[current]; _establish(mode, !offer.termAuction.auctionCompleted()); - _establish(mode, !offer.termAuction.auctionCancelledForWithdrawal()); + _establish( + mode, + !offer.termAuction.auctionCancelledForWithdrawal() + ); current = _termAuctionList.nodes[current].next; } @@ -492,13 +574,18 @@ contract TermAuctionListTest is KontrolTest { * Assume or assert that the offer amounts recorded in the list are the same * as the offer amounts in the offer locker. */ - function _establishOfferAmountMatchesAmountLocked(Mode mode, bytes32 offerId) internal view { + function _establishOfferAmountMatchesAmountLocked( + Mode mode, + bytes32 offerId + ) internal view { bytes32 current = _termAuctionList.head; while (current != TermAuctionList.NULL_NODE) { - if(offerId == 0 || offerId != current) { + if (offerId == 0 || offerId != current) { PendingOffer storage offer = _termAuctionList.offers[current]; - uint256 offerAmount = TermAuctionOfferLocker(address(offer.offerLocker)).lockedOfferAmount(current); + uint256 offerAmount = TermAuctionOfferLocker( + address(offer.offerLocker) + ).lockedOfferAmount(current); _establish(mode, offer.offerAmount == offerAmount); } @@ -560,8 +647,12 @@ contract TermAuctionListTest is KontrolTest { while (current != TermAuctionList.NULL_NODE) { PendingOffer storage offer = _termAuctionList.offers[current]; - (,, address termRepoServicer, address termRepoCollateralManager) = - ITermRepoToken(offer.repoToken).config(); + ( + , + , + address termRepoServicer, + address termRepoCollateralManager + ) = ITermRepoToken(offer.repoToken).config(); vm.assume(freshAddress != offer.repoToken); vm.assume(freshAddress != address(offer.termAuction)); @@ -573,8 +664,9 @@ contract TermAuctionListTest is KontrolTest { } } - - function _termAuctionListToArray(uint256 length) internal view returns (bytes32[] memory offerIds) { + function _termAuctionListToArray( + uint256 length + ) internal view returns (bytes32[] memory offerIds) { bytes32 current = _termAuctionList.head; uint256 i; offerIds = new bytes32[](length); @@ -585,15 +677,18 @@ contract TermAuctionListTest is KontrolTest { } } - function _establishInsertListPreservation(bytes32 newOfferId, bytes32[] memory offerIds, uint256 offerIdsCount) internal view { + function _establishInsertListPreservation( + bytes32 newOfferId, + bytes32[] memory offerIds, + uint256 offerIdsCount + ) internal view { bytes32 current = _termAuctionList.head; uint256 i = 0; - if(newOfferId != bytes32(0)) { - + if (newOfferId != bytes32(0)) { while (current != TermAuctionList.NULL_NODE && i < offerIdsCount) { - if(current != offerIds[i]) { - assert (current == newOfferId); + if (current != offerIds[i]) { + assert(current == newOfferId); current = _termAuctionList.nodes[current].next; break; } @@ -602,7 +697,7 @@ contract TermAuctionListTest is KontrolTest { } if (current != TermAuctionList.NULL_NODE && i == offerIdsCount) { - assert (current == newOfferId); + assert(current == newOfferId); } } @@ -612,12 +707,15 @@ contract TermAuctionListTest is KontrolTest { } } - function _establishRemoveListPreservation(bytes32[] memory offerIds, uint256 offerIdsCount) internal view { + function _establishRemoveListPreservation( + bytes32[] memory offerIds, + uint256 offerIdsCount + ) internal view { bytes32 current = _termAuctionList.head; uint256 i = 0; while (current != TermAuctionList.NULL_NODE && i < offerIdsCount) { - if(current == offerIds[i++]) { + if (current == offerIds[i++]) { current = _termAuctionList.nodes[current].next; } } @@ -634,8 +732,9 @@ contract TermAuctionListTest is KontrolTest { while (current != TermAuctionList.NULL_NODE) { PendingOffer storage offer = _termAuctionList.offers[current]; - TermAuctionOfferLocker offerLocker = - TermAuctionOfferLocker(address(offer.offerLocker)); + TermAuctionOfferLocker offerLocker = TermAuctionOfferLocker( + address(offer.offerLocker) + ); offerLocker.guaranteeUnlockAlwaysSucceeds(); @@ -643,4 +742,3 @@ contract TermAuctionListTest is KontrolTest { } } } - diff --git a/src/test/kontrol/TermAuctionOfferLocker.sol b/src/test/kontrol/TermAuctionOfferLocker.sol index 04bc52f9..6b6cfc08 100644 --- a/src/test/kontrol/TermAuctionOfferLocker.sol +++ b/src/test/kontrol/TermAuctionOfferLocker.sol @@ -13,7 +13,15 @@ contract TermAuctionOfferLocker is ITermAuctionOfferLocker, KontrolTest { uint256 private lockedOffersSlot; function lockedOfferSlot(bytes32 offerId) internal view returns (uint256) { - return uint256(keccak256(abi.encodePacked(uint256(offerId), uint256(lockedOffersSlot)))); + return + uint256( + keccak256( + abi.encodePacked( + uint256(offerId), + uint256(lockedOffersSlot) + ) + ) + ); } function initializeSymbolic(address termReposervicer) public { @@ -71,14 +79,15 @@ contract TermAuctionOfferLocker is ITermAuctionOfferLocker, KontrolTest { return _termRepoServicer; } - function lockedOffer(bytes32 id) external view returns (TermAuctionOffer memory) { + function lockedOffer( + bytes32 id + ) external view returns (TermAuctionOffer memory) { return _lockedOffers[id]; } function lockOffers( TermAuctionOfferSubmission[] calldata offerSubmissions ) external view returns (bytes32[] memory) { - uint256 length = offerSubmissions.length; bytes32[] memory offers = new bytes32[](length); @@ -96,7 +105,7 @@ contract TermAuctionOfferLocker is ITermAuctionOfferLocker, KontrolTest { } for (uint256 i = 0; i < offerIds.length; ++i) { - delete(_lockedOffers[offerIds[i]]); + delete (_lockedOffers[offerIds[i]]); } } } diff --git a/src/test/kontrol/TermDiscountRateAdapter.sol b/src/test/kontrol/TermDiscountRateAdapter.sol index 60831a83..a3984a34 100644 --- a/src/test/kontrol/TermDiscountRateAdapter.sol +++ b/src/test/kontrol/TermDiscountRateAdapter.sol @@ -23,15 +23,22 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, KontrolTest { _discountRate[repoToken] = discountRate; } - function repoRedemptionHaircut(address repoToken) external view returns (uint256) { + function repoRedemptionHaircut( + address repoToken + ) external view returns (uint256) { return _repoRedemptionHaircut[repoToken]; } - function getDiscountRate(address termController, address repoToken) external view returns (uint256) { + function getDiscountRate( + address termController, + address repoToken + ) external view returns (uint256) { return _discountRate[repoToken]; } - function getDiscountRate( address repoToken) external view returns (uint256) { + function getDiscountRate( + address repoToken + ) external view returns (uint256) { return _discountRate[repoToken]; } diff --git a/src/test/kontrol/TermRepoCollateralManager.sol b/src/test/kontrol/TermRepoCollateralManager.sol index 2713bd69..ab3a858f 100644 --- a/src/test/kontrol/TermRepoCollateralManager.sol +++ b/src/test/kontrol/TermRepoCollateralManager.sol @@ -9,7 +9,9 @@ contract TermRepoCollateralManager is ITermRepoCollateralManager, KontrolTest { uint256 private collateralTokensSlot; - function collateralTokensDataSlot(uint256 i) internal view returns (uint256) { + function collateralTokensDataSlot( + uint256 i + ) internal view returns (uint256) { return uint256(keccak256(abi.encodePacked(collateralTokensSlot))) + i; } @@ -30,7 +32,11 @@ contract TermRepoCollateralManager is ITermRepoCollateralManager, KontrolTest { uint160(uint256(keccak256(abi.encodePacked("collateral", i)))) ); - _storeUInt256(address(this), collateralTokensDataSlot(i), uint256(uint160(currentToken))); + _storeUInt256( + address(this), + collateralTokensDataSlot(i), + uint256(uint160(currentToken)) + ); _maintenanceCollateralRatios[currentToken] = freshUInt256(); } } diff --git a/src/test/mocks/MockTermAuction.sol b/src/test/mocks/MockTermAuction.sol index c63d0111..b3f6ac9f 100644 --- a/src/test/mocks/MockTermAuction.sol +++ b/src/test/mocks/MockTermAuction.sol @@ -1,69 +1,74 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {ITermAuction} from "../../interfaces/term/ITermAuction.sol"; -import {ITermAuctionOfferLocker} from "../../interfaces/term/ITermAuctionOfferLocker.sol"; -import {ITermRepoToken} from "../../interfaces/term/ITermRepoToken.sol"; -import {ITermRepoServicer} from "../../interfaces/term/ITermRepoServicer.sol"; -import {MockTermAuctionOfferLocker} from "./MockTermAuctionOfferLocker.sol"; -import {MockTermRepoToken} from "./MockTermRepoToken.sol"; - -contract MockTermAuction is ITermAuction { - - address public termAuctionOfferLocker; - bytes32 public termRepoId; - uint256 public auctionEndTime; - bool public auctionCompleted; - bool public auctionCancelledForWithdrawal; - ITermRepoToken internal repoToken; - - constructor(ITermRepoToken _repoToken) { - termRepoId = _repoToken.termRepoId(); - repoToken = _repoToken; - ( - uint256 redemptionTimestamp, - address purchaseToken, - address termRepoServicer, - address termRepoCollateralManager - ) = _repoToken.config(); - termAuctionOfferLocker = address(new MockTermAuctionOfferLocker( - ITermAuction(address(this)), - ITermRepoServicer(termRepoServicer).termRepoLocker(), - termRepoServicer, - purchaseToken - )); - auctionEndTime = block.timestamp + 1 weeks; - } - - function auctionSuccess(bytes32[] calldata offerIds, uint256[] calldata fillAmounts, uint256[] calldata repoTokenAmounts) external { - auctionCompleted = true; - auctionEndTime = block.timestamp; - - for (uint256 i; i < offerIds.length; i++) { - MockTermAuctionOfferLocker(termAuctionOfferLocker).processOffer( - MockTermRepoToken(address(repoToken)), offerIds[i], fillAmounts[i], repoTokenAmounts[i] - ); - } - } - - function auctionCancelForWithdrawal() external { - auctionCancelledForWithdrawal = true; - } - - function auctionCancel(bytes32[] calldata offerIds) external { - - uint256 i = 0; - // Return revealed offer funds. - for (i = 0; i < offerIds.length; ++i) { - ITermAuctionOfferLocker.TermAuctionOffer memory offer = MockTermAuctionOfferLocker(termAuctionOfferLocker).lockedOffer(offerIds[i]); - - MockTermAuctionOfferLocker(termAuctionOfferLocker).unlockOfferPartial( - offer.id, - offer.offeror, - offer.amount - ); - } - - - } -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ITermAuction} from "../../interfaces/term/ITermAuction.sol"; +import {ITermAuctionOfferLocker} from "../../interfaces/term/ITermAuctionOfferLocker.sol"; +import {ITermRepoToken} from "../../interfaces/term/ITermRepoToken.sol"; +import {ITermRepoServicer} from "../../interfaces/term/ITermRepoServicer.sol"; +import {MockTermAuctionOfferLocker} from "./MockTermAuctionOfferLocker.sol"; +import {MockTermRepoToken} from "./MockTermRepoToken.sol"; + +contract MockTermAuction is ITermAuction { + address public termAuctionOfferLocker; + bytes32 public termRepoId; + uint256 public auctionEndTime; + bool public auctionCompleted; + bool public auctionCancelledForWithdrawal; + ITermRepoToken internal repoToken; + + constructor(ITermRepoToken _repoToken) { + termRepoId = _repoToken.termRepoId(); + repoToken = _repoToken; + ( + uint256 redemptionTimestamp, + address purchaseToken, + address termRepoServicer, + address termRepoCollateralManager + ) = _repoToken.config(); + termAuctionOfferLocker = address( + new MockTermAuctionOfferLocker( + ITermAuction(address(this)), + ITermRepoServicer(termRepoServicer).termRepoLocker(), + termRepoServicer, + purchaseToken + ) + ); + auctionEndTime = block.timestamp + 1 weeks; + } + + function auctionSuccess( + bytes32[] calldata offerIds, + uint256[] calldata fillAmounts, + uint256[] calldata repoTokenAmounts + ) external { + auctionCompleted = true; + auctionEndTime = block.timestamp; + + for (uint256 i; i < offerIds.length; i++) { + MockTermAuctionOfferLocker(termAuctionOfferLocker).processOffer( + MockTermRepoToken(address(repoToken)), + offerIds[i], + fillAmounts[i], + repoTokenAmounts[i] + ); + } + } + + function auctionCancelForWithdrawal() external { + auctionCancelledForWithdrawal = true; + } + + function auctionCancel(bytes32[] calldata offerIds) external { + uint256 i = 0; + // Return revealed offer funds. + for (i = 0; i < offerIds.length; ++i) { + ITermAuctionOfferLocker.TermAuctionOffer + memory offer = MockTermAuctionOfferLocker( + termAuctionOfferLocker + ).lockedOffer(offerIds[i]); + + MockTermAuctionOfferLocker(termAuctionOfferLocker) + .unlockOfferPartial(offer.id, offer.offeror, offer.amount); + } + } +} diff --git a/src/test/mocks/MockTermAuctionOfferLocker.sol b/src/test/mocks/MockTermAuctionOfferLocker.sol index 55eeec14..77e4fdd3 100644 --- a/src/test/mocks/MockTermAuctionOfferLocker.sol +++ b/src/test/mocks/MockTermAuctionOfferLocker.sol @@ -1,109 +1,134 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {ITermAuctionOfferLocker} from "../../interfaces/term/ITermAuctionOfferLocker.sol"; -import {ITermAuction} from "../../interfaces/term/ITermAuction.sol"; -import {MockTermRepoLocker} from "./MockTermRepoLocker.sol"; -import {MockTermRepoToken} from "./MockTermRepoToken.sol"; - -contract MockTermAuctionOfferLocker is ITermAuctionOfferLocker { - error OfferUnlockingFailed(); - - address public purchaseToken; - address public termRepoServicer; - uint256 public auctionStartTime; - MockTermRepoLocker internal repoLocker; - ITermAuction internal auction; - mapping(bytes32 => TermAuctionOffer) internal lockedOffers; - - constructor( - ITermAuction _auction, - address _repoLocker, - address _repoServicer, - address _purchaseToken - ) { - auction = _auction; - purchaseToken = _purchaseToken; - termRepoServicer = _repoServicer; - repoLocker = MockTermRepoLocker(_repoLocker); - auctionStartTime = 0; - } - - function termRepoId() external view returns (bytes32) { - return auction.termRepoId(); - } - - function termAuctionId() external view returns (bytes32) { - return auction.termRepoId(); - } - - function auctionEndTime() external view returns (uint256) { - return auction.auctionEndTime(); - } - - function revealTime() external view returns (uint256) { - return auction.auctionEndTime(); - } - - function lockedOffer(bytes32 id) external view returns (TermAuctionOffer memory) { - return lockedOffers[id]; - } - - function lockOffers( - TermAuctionOfferSubmission[] calldata offerSubmissions - ) external returns (bytes32[] memory offerIds) { - offerIds = new bytes32[](offerSubmissions.length); - - for (uint256 i; i < offerSubmissions.length; i++) { - TermAuctionOfferSubmission memory submission = offerSubmissions[i]; - TermAuctionOffer memory offer = lockedOffers[submission.id]; - - // existing offer - if (offer.amount > 0) { - if (offer.amount > submission.amount) { - // current amount > new amount, release tokens - repoLocker.releasePurchaseTokens(msg.sender, offer.amount - submission.amount); - } else if (offer.amount < submission.amount) { - repoLocker.lockPurchaseTokens(msg.sender, submission.amount - offer.amount); - } - // update locked amount - offer.amount = submission.amount; - } else { - bytes32 offerId = keccak256(abi.encodePacked(submission.id,msg.sender,address(this))); - - offer.id = offerId; - offer.offeror = submission.offeror; - offer.offerPriceHash = submission.offerPriceHash; - offer.amount = submission.amount; - offer.purchaseToken = submission.purchaseToken; - - repoLocker.lockPurchaseTokens(msg.sender, offer.amount); - } - lockedOffers[offer.id] = offer; - offerIds[i] = offer.id; - } - } - - function processOffer(MockTermRepoToken mockRepoToken, bytes32 offerId, uint256 fillAmount, uint256 repoTokenAmount) external { - require(lockedOffers[offerId].amount >= fillAmount); - uint256 remainingAmount = lockedOffers[offerId].amount - fillAmount; - - lockedOffers[offerId].amount = remainingAmount; - - mockRepoToken.mint(lockedOffers[offerId].offeror, repoTokenAmount); - repoLocker.releasePurchaseTokens(lockedOffers[offerId].offeror, remainingAmount); - } - - function unlockOffers(bytes32[] calldata offerIds) external { - for (uint256 i; i < offerIds.length; i++) { - bytes32 offerId = offerIds[i]; - repoLocker.releasePurchaseTokens(msg.sender, lockedOffers[offerId].amount); - delete lockedOffers[offerId]; - } - } - - function unlockOfferPartial(bytes32 offerId, address offeror, uint256 amount) external { - delete lockedOffers[offerId]; - repoLocker.releasePurchaseTokens(offeror, amount); - } -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ITermAuctionOfferLocker} from "../../interfaces/term/ITermAuctionOfferLocker.sol"; +import {ITermAuction} from "../../interfaces/term/ITermAuction.sol"; +import {MockTermRepoLocker} from "./MockTermRepoLocker.sol"; +import {MockTermRepoToken} from "./MockTermRepoToken.sol"; + +contract MockTermAuctionOfferLocker is ITermAuctionOfferLocker { + error OfferUnlockingFailed(); + + address public purchaseToken; + address public termRepoServicer; + uint256 public auctionStartTime; + MockTermRepoLocker internal repoLocker; + ITermAuction internal auction; + mapping(bytes32 => TermAuctionOffer) internal lockedOffers; + + constructor( + ITermAuction _auction, + address _repoLocker, + address _repoServicer, + address _purchaseToken + ) { + auction = _auction; + purchaseToken = _purchaseToken; + termRepoServicer = _repoServicer; + repoLocker = MockTermRepoLocker(_repoLocker); + auctionStartTime = 0; + } + + function termRepoId() external view returns (bytes32) { + return auction.termRepoId(); + } + + function termAuctionId() external view returns (bytes32) { + return auction.termRepoId(); + } + + function auctionEndTime() external view returns (uint256) { + return auction.auctionEndTime(); + } + + function revealTime() external view returns (uint256) { + return auction.auctionEndTime(); + } + + function lockedOffer( + bytes32 id + ) external view returns (TermAuctionOffer memory) { + return lockedOffers[id]; + } + + function lockOffers( + TermAuctionOfferSubmission[] calldata offerSubmissions + ) external returns (bytes32[] memory offerIds) { + offerIds = new bytes32[](offerSubmissions.length); + + for (uint256 i; i < offerSubmissions.length; i++) { + TermAuctionOfferSubmission memory submission = offerSubmissions[i]; + TermAuctionOffer memory offer = lockedOffers[submission.id]; + + // existing offer + if (offer.amount > 0) { + if (offer.amount > submission.amount) { + // current amount > new amount, release tokens + repoLocker.releasePurchaseTokens( + msg.sender, + offer.amount - submission.amount + ); + } else if (offer.amount < submission.amount) { + repoLocker.lockPurchaseTokens( + msg.sender, + submission.amount - offer.amount + ); + } + // update locked amount + offer.amount = submission.amount; + } else { + bytes32 offerId = keccak256( + abi.encodePacked(submission.id, msg.sender, address(this)) + ); + + offer.id = offerId; + offer.offeror = submission.offeror; + offer.offerPriceHash = submission.offerPriceHash; + offer.amount = submission.amount; + offer.purchaseToken = submission.purchaseToken; + + repoLocker.lockPurchaseTokens(msg.sender, offer.amount); + } + lockedOffers[offer.id] = offer; + offerIds[i] = offer.id; + } + } + + function processOffer( + MockTermRepoToken mockRepoToken, + bytes32 offerId, + uint256 fillAmount, + uint256 repoTokenAmount + ) external { + require(lockedOffers[offerId].amount >= fillAmount); + uint256 remainingAmount = lockedOffers[offerId].amount - fillAmount; + + lockedOffers[offerId].amount = remainingAmount; + + mockRepoToken.mint(lockedOffers[offerId].offeror, repoTokenAmount); + repoLocker.releasePurchaseTokens( + lockedOffers[offerId].offeror, + remainingAmount + ); + } + + function unlockOffers(bytes32[] calldata offerIds) external { + for (uint256 i; i < offerIds.length; i++) { + bytes32 offerId = offerIds[i]; + repoLocker.releasePurchaseTokens( + msg.sender, + lockedOffers[offerId].amount + ); + delete lockedOffers[offerId]; + } + } + + function unlockOfferPartial( + bytes32 offerId, + address offeror, + uint256 amount + ) external { + delete lockedOffers[offerId]; + repoLocker.releasePurchaseTokens(offeror, amount); + } +} diff --git a/src/test/mocks/MockTermController.sol b/src/test/mocks/MockTermController.sol index 129f33d9..b953a8cc 100644 --- a/src/test/mocks/MockTermController.sol +++ b/src/test/mocks/MockTermController.sol @@ -1,40 +1,51 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {ITermController, AuctionMetadata} from "../../interfaces/term/ITermController.sol"; - -struct TermAuctionResults { - AuctionMetadata[] auctionMetadata; - uint8 numOfAuctions; -} - -contract MockTermController is ITermController { - mapping(bytes32 => TermAuctionResults) internal auctionResults; - mapping(address => bool) internal notTermDeployedContracts; - - function isTermDeployed(address contractAddress) external view returns (bool) { - return !notTermDeployedContracts[contractAddress]; - } - - function markNotTermDeployed(address contractAddress) external { - notTermDeployedContracts[contractAddress] = true; - } - - function getProtocolReserveAddress() external view returns (address) { - return address(100); - } - - function setOracleRate(bytes32 termRepoId, uint256 oracleRate) external { - AuctionMetadata memory metadata; - - metadata.auctionClearingRate = oracleRate; - - delete auctionResults[termRepoId]; - auctionResults[termRepoId].auctionMetadata.push(metadata); - auctionResults[termRepoId].numOfAuctions = 1; - } - - function getTermAuctionResults(bytes32 termRepoId) external view returns (AuctionMetadata[] memory auctionMetadata, uint8 numOfAuctions) { - return (auctionResults[termRepoId].auctionMetadata, auctionResults[termRepoId].numOfAuctions); - } -} \ No newline at end of file +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ITermController, AuctionMetadata} from "../../interfaces/term/ITermController.sol"; + +struct TermAuctionResults { + AuctionMetadata[] auctionMetadata; + uint8 numOfAuctions; +} + +contract MockTermController is ITermController { + mapping(bytes32 => TermAuctionResults) internal auctionResults; + mapping(address => bool) internal notTermDeployedContracts; + + function isTermDeployed( + address contractAddress + ) external view returns (bool) { + return !notTermDeployedContracts[contractAddress]; + } + + function markNotTermDeployed(address contractAddress) external { + notTermDeployedContracts[contractAddress] = true; + } + + function getProtocolReserveAddress() external view returns (address) { + return address(100); + } + + function setOracleRate(bytes32 termRepoId, uint256 oracleRate) external { + AuctionMetadata memory metadata; + + metadata.auctionClearingRate = oracleRate; + + delete auctionResults[termRepoId]; + auctionResults[termRepoId].auctionMetadata.push(metadata); + auctionResults[termRepoId].numOfAuctions = 1; + } + + function getTermAuctionResults( + bytes32 termRepoId + ) + external + view + returns (AuctionMetadata[] memory auctionMetadata, uint8 numOfAuctions) + { + return ( + auctionResults[termRepoId].auctionMetadata, + auctionResults[termRepoId].numOfAuctions + ); + } +} diff --git a/src/test/mocks/MockTermRepoCollateralManager.sol b/src/test/mocks/MockTermRepoCollateralManager.sol index 95d83c0f..17625d9d 100644 --- a/src/test/mocks/MockTermRepoCollateralManager.sol +++ b/src/test/mocks/MockTermRepoCollateralManager.sol @@ -1,29 +1,36 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {ITermRepoCollateralManager} from "../../interfaces/term/ITermRepoCollateralManager.sol"; -import {ITermRepoToken} from "../../interfaces/term/ITermRepoToken.sol"; - -contract MockTermRepoCollateralManager is ITermRepoCollateralManager { - ITermRepoToken internal repoToken; - mapping(address => uint256) public maintenanceCollateralRatios; - address[] internal collateralTokenList; - - constructor(ITermRepoToken _repoToken, address _collateral, uint256 _maintenanceRatio) { - repoToken = _repoToken; - addCollateralToken(_collateral, _maintenanceRatio); - } - - function addCollateralToken(address _collateral, uint256 _maintenanceRatio) public { - collateralTokenList.push(_collateral); - maintenanceCollateralRatios[_collateral] = _maintenanceRatio; - } - - function numOfAcceptedCollateralTokens() external view returns (uint8) { - return uint8(collateralTokenList.length); - } - - function collateralTokens(uint256 index) external view returns (address) { - return collateralTokenList[index]; - } -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ITermRepoCollateralManager} from "../../interfaces/term/ITermRepoCollateralManager.sol"; +import {ITermRepoToken} from "../../interfaces/term/ITermRepoToken.sol"; + +contract MockTermRepoCollateralManager is ITermRepoCollateralManager { + ITermRepoToken internal repoToken; + mapping(address => uint256) public maintenanceCollateralRatios; + address[] internal collateralTokenList; + + constructor( + ITermRepoToken _repoToken, + address _collateral, + uint256 _maintenanceRatio + ) { + repoToken = _repoToken; + addCollateralToken(_collateral, _maintenanceRatio); + } + + function addCollateralToken( + address _collateral, + uint256 _maintenanceRatio + ) public { + collateralTokenList.push(_collateral); + maintenanceCollateralRatios[_collateral] = _maintenanceRatio; + } + + function numOfAcceptedCollateralTokens() external view returns (uint8) { + return uint8(collateralTokenList.length); + } + + function collateralTokens(uint256 index) external view returns (address) { + return collateralTokenList[index]; + } +} diff --git a/src/test/mocks/MockTermRepoLocker.sol b/src/test/mocks/MockTermRepoLocker.sol index 9b4dabba..be240559 100644 --- a/src/test/mocks/MockTermRepoLocker.sol +++ b/src/test/mocks/MockTermRepoLocker.sol @@ -1,20 +1,20 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -contract MockTermRepoLocker { - IERC20 internal purchaseToken; - - constructor(address _purchaseToken) { - purchaseToken = IERC20(_purchaseToken); - } - - function lockPurchaseTokens(address from, uint256 amount) external { - purchaseToken.transferFrom(from, address(this), amount); - } - - function releasePurchaseTokens(address to, uint256 amount) external { - purchaseToken.transfer(to, amount); - } -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MockTermRepoLocker { + IERC20 internal purchaseToken; + + constructor(address _purchaseToken) { + purchaseToken = IERC20(_purchaseToken); + } + + function lockPurchaseTokens(address from, uint256 amount) external { + purchaseToken.transferFrom(from, address(this), amount); + } + + function releasePurchaseTokens(address to, uint256 amount) external { + purchaseToken.transfer(to, amount); + } +} diff --git a/src/test/mocks/MockTermRepoServicer.sol b/src/test/mocks/MockTermRepoServicer.sol index 123c567f..5467dbe5 100644 --- a/src/test/mocks/MockTermRepoServicer.sol +++ b/src/test/mocks/MockTermRepoServicer.sol @@ -1,49 +1,52 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {ITermRepoServicer} from "../../interfaces/term/ITermRepoServicer.sol"; -import {ITermRepoToken} from "../../interfaces/term/ITermRepoToken.sol"; -import {MockTermRepoLocker} from "./MockTermRepoLocker.sol"; - -interface IMockERC20 { - function mint(address account, uint256 amount) external; - function burn(address account, uint256 amount) external; - function decimals() external view returns (uint256); -} - -contract MockTermRepoServicer is ITermRepoServicer { - ITermRepoToken internal repoToken; - MockTermRepoLocker internal repoLocker; - address public purchaseToken; - bool public redemptionFailure; - - constructor(ITermRepoToken _repoToken, address _purchaseToken) { - repoToken = _repoToken; - repoLocker = new MockTermRepoLocker(_purchaseToken); - purchaseToken = _purchaseToken; - } - - function setRedemptionFailure(bool hasFailure) external { - redemptionFailure = hasFailure; - } - - function redeemTermRepoTokens( - address redeemer, - uint256 amountToRedeem - ) external { - if (redemptionFailure) revert("redemption failured"); - uint256 amountToRedeemInAssetPrecision = - amountToRedeem * (10**IMockERC20(purchaseToken).decimals()) / - (10**IMockERC20(address(repoToken)).decimals()); - IMockERC20(purchaseToken).mint(redeemer, amountToRedeemInAssetPrecision); - IMockERC20(address(repoToken)).burn(redeemer, amountToRedeem); - } - - function termRepoToken() external view returns (address) { - return address(repoToken); - } - - function termRepoLocker() external view returns (address) { - return address(repoLocker); - } -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ITermRepoServicer} from "../../interfaces/term/ITermRepoServicer.sol"; +import {ITermRepoToken} from "../../interfaces/term/ITermRepoToken.sol"; +import {MockTermRepoLocker} from "./MockTermRepoLocker.sol"; + +interface IMockERC20 { + function mint(address account, uint256 amount) external; + function burn(address account, uint256 amount) external; + function decimals() external view returns (uint256); +} + +contract MockTermRepoServicer is ITermRepoServicer { + ITermRepoToken internal repoToken; + MockTermRepoLocker internal repoLocker; + address public purchaseToken; + bool public redemptionFailure; + + constructor(ITermRepoToken _repoToken, address _purchaseToken) { + repoToken = _repoToken; + repoLocker = new MockTermRepoLocker(_purchaseToken); + purchaseToken = _purchaseToken; + } + + function setRedemptionFailure(bool hasFailure) external { + redemptionFailure = hasFailure; + } + + function redeemTermRepoTokens( + address redeemer, + uint256 amountToRedeem + ) external { + if (redemptionFailure) revert("redemption failured"); + uint256 amountToRedeemInAssetPrecision = (amountToRedeem * + (10 ** IMockERC20(purchaseToken).decimals())) / + (10 ** IMockERC20(address(repoToken)).decimals()); + IMockERC20(purchaseToken).mint( + redeemer, + amountToRedeemInAssetPrecision + ); + IMockERC20(address(repoToken)).burn(redeemer, amountToRedeem); + } + + function termRepoToken() external view returns (address) { + return address(repoToken); + } + + function termRepoLocker() external view returns (address) { + return address(repoLocker); + } +} diff --git a/src/test/mocks/MockTermRepoToken.sol b/src/test/mocks/MockTermRepoToken.sol index f5b3994d..c0540b1c 100644 --- a/src/test/mocks/MockTermRepoToken.sol +++ b/src/test/mocks/MockTermRepoToken.sol @@ -1,71 +1,77 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {ITermRepoToken} from "../../interfaces/term/ITermRepoToken.sol"; -import {ITermRepoServicer} from "../../interfaces/term/ITermRepoServicer.sol"; -import {ITermRepoCollateralManager} from "../../interfaces/term/ITermRepoCollateralManager.sol"; -import {MockTermRepoServicer} from "./MockTermRepoServicer.sol"; -import {MockTermRepoCollateralManager} from "./MockTermRepoCollateralManager.sol"; - -contract MockTermRepoToken is ERC20, ITermRepoToken { - struct RepoTokenContext { - uint256 redemptionTimestamp; - address purchaseToken; - ITermRepoServicer termRepoServicer; - ITermRepoCollateralManager termRepoCollateralManager; - } - - bytes32 public termRepoId; - RepoTokenContext internal repoTokenContext; - - constructor( - bytes32 _termRepoId, - address _purchaseToken, - address _collateral, - uint256 _maintenanceRatio, - uint256 _redemptionTimestamp - ) ERC20("MockRepo", "MockRepo") { - termRepoId = _termRepoId; - repoTokenContext.redemptionTimestamp = _redemptionTimestamp; - repoTokenContext.purchaseToken = _purchaseToken; - repoTokenContext.termRepoServicer = new MockTermRepoServicer(ITermRepoToken(address(this)), _purchaseToken); - repoTokenContext.termRepoCollateralManager = new MockTermRepoCollateralManager( - ITermRepoToken(address(this)), _collateral, _maintenanceRatio - ); - } - - function redemptionValue() external view returns (uint256) { - return 1e18; - } - - function mint(address account, uint256 amount) external { - _mint(account, amount); - } - - function burn(address account, uint256 amount) external { - _burn(account, amount); - } - - function mockServicer() external returns (MockTermRepoServicer) { - return MockTermRepoServicer(address(repoTokenContext.termRepoServicer)); - } - - function config() - external - view - returns ( - uint256 redemptionTimestamp, - address purchaseToken, - address termRepoServicer, - address termRepoCollateralManager - ) - { - return ( - repoTokenContext.redemptionTimestamp, - repoTokenContext.purchaseToken, - address(repoTokenContext.termRepoServicer), - address(repoTokenContext.termRepoCollateralManager) - ); - } -} +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.18; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ITermRepoToken} from "../../interfaces/term/ITermRepoToken.sol"; +import {ITermRepoServicer} from "../../interfaces/term/ITermRepoServicer.sol"; +import {ITermRepoCollateralManager} from "../../interfaces/term/ITermRepoCollateralManager.sol"; +import {MockTermRepoServicer} from "./MockTermRepoServicer.sol"; +import {MockTermRepoCollateralManager} from "./MockTermRepoCollateralManager.sol"; + +contract MockTermRepoToken is ERC20, ITermRepoToken { + struct RepoTokenContext { + uint256 redemptionTimestamp; + address purchaseToken; + ITermRepoServicer termRepoServicer; + ITermRepoCollateralManager termRepoCollateralManager; + } + + bytes32 public termRepoId; + RepoTokenContext internal repoTokenContext; + + constructor( + bytes32 _termRepoId, + address _purchaseToken, + address _collateral, + uint256 _maintenanceRatio, + uint256 _redemptionTimestamp + ) ERC20("MockRepo", "MockRepo") { + termRepoId = _termRepoId; + repoTokenContext.redemptionTimestamp = _redemptionTimestamp; + repoTokenContext.purchaseToken = _purchaseToken; + repoTokenContext.termRepoServicer = new MockTermRepoServicer( + ITermRepoToken(address(this)), + _purchaseToken + ); + repoTokenContext + .termRepoCollateralManager = new MockTermRepoCollateralManager( + ITermRepoToken(address(this)), + _collateral, + _maintenanceRatio + ); + } + + function redemptionValue() external view returns (uint256) { + return 1e18; + } + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } + + function mockServicer() external returns (MockTermRepoServicer) { + return MockTermRepoServicer(address(repoTokenContext.termRepoServicer)); + } + + function config() + external + view + returns ( + uint256 redemptionTimestamp, + address purchaseToken, + address termRepoServicer, + address termRepoCollateralManager + ) + { + return ( + repoTokenContext.redemptionTimestamp, + repoTokenContext.purchaseToken, + address(repoTokenContext.termRepoServicer), + address(repoTokenContext.termRepoCollateralManager) + ); + } +} diff --git a/src/test/mocks/MockUSDC.sol b/src/test/mocks/MockUSDC.sol index a6cfbf78..b8058d47 100644 --- a/src/test/mocks/MockUSDC.sol +++ b/src/test/mocks/MockUSDC.sol @@ -1,19 +1,19 @@ -pragma solidity ^0.8.18; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract MockUSDC is ERC20 { - constructor() ERC20("USDC", "USDC") {} - - function mint(address account, uint256 amount) external { - _mint(account, amount); - } - - function burn(address account, uint256 amount) external { - _burn(account, amount); - } - - function decimals() public view override returns (uint8) { - return 6; - } -} +pragma solidity ^0.8.18; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockUSDC is ERC20 { + constructor() ERC20("USDC", "USDC") {} + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } + + function decimals() public view override returns (uint8) { + return 6; + } +} diff --git a/src/test/utils/ExtendedTest.sol b/src/test/utils/ExtendedTest.sol index e840e4b9..5428abf1 100644 --- a/src/test/utils/ExtendedTest.sol +++ b/src/test/utils/ExtendedTest.sol @@ -85,12 +85,14 @@ contract ExtendedTest is Test { } function expectRoleRevert(bytes32 role) internal { - string memory revertMessage = string(abi.encodePacked( - "AccessControl: account ", - address(this), - " is missing role ", - role - )); + string memory revertMessage = string( + abi.encodePacked( + "AccessControl: account ", + address(this), + " is missing role ", + role + ) + ); vm.expectRevert(bytes(revertMessage)); } } diff --git a/src/test/utils/Setup.sol b/src/test/utils/Setup.sol index 7666f2ca..3e9f9f4d 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -85,12 +85,20 @@ contract Setup is ExtendedTest, IEvents { // Factory from mainnet, tokenized strategy needs to be hardcoded to 0xBB51273D6c746910C7C06fe718f30c936170feD0 tokenizedStrategy = new TokenizedStrategy(address(mockFactory)); - vm.etch(0xBB51273D6c746910C7C06fe718f30c936170feD0, address(tokenizedStrategy).code); + vm.etch( + 0xBB51273D6c746910C7C06fe718f30c936170feD0, + address(tokenizedStrategy).code + ); termController = new MockTermController(); - discountRateAdapter = new TermDiscountRateAdapter(address(termController), adminWallet); + discountRateAdapter = new TermDiscountRateAdapter( + address(termController), + adminWallet + ); termVaultEventEmitterImpl = new TermVaultEventEmitter(); - termVaultEventEmitter = TermVaultEventEmitter(address(new ERC1967Proxy(address(termVaultEventEmitterImpl), ""))); + termVaultEventEmitter = TermVaultEventEmitter( + address(new ERC1967Proxy(address(termVaultEventEmitterImpl), "")) + ); mockYearnVault = new ERC4626Mock(address(asset)); termVaultEventEmitter.initialize(adminWallet, devopsWallet); @@ -98,7 +106,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"); @@ -113,8 +121,8 @@ 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 = constructStrategy( - address(asset), - address(mockYearnVault), + address(asset), + address(mockYearnVault), address(discountRateAdapter), address(termVaultEventEmitter), governor, @@ -139,7 +147,14 @@ contract Setup is ExtendedTest, IEvents { return address(_strategy); } - function constructStrategy(address asset, address mockYearnVault, address discountRateAdapter, address termVaultEventEmitter, address governor, address termController) internal returns (IStrategyInterface) { + function constructStrategy( + address asset, + address mockYearnVault, + address discountRateAdapter, + address termVaultEventEmitter, + address governor, + address termController + ) internal returns (IStrategyInterface) { Strategy.StrategyParams memory params = Strategy.StrategyParams( asset, mockYearnVault, @@ -152,12 +167,8 @@ contract Setup is ExtendedTest, IEvents { 0.2e18, 0.005e18 ); - Strategy strat = new Strategy( - "Tokenized Strategy", - "tS", - params - ); - + Strategy strat = new Strategy("Tokenized Strategy", "tS", params); + return IStrategyInterface(address(strat)); } diff --git a/src/util/TermFinanceVaultWrappedVotesToken.sol b/src/util/TermFinanceVaultWrappedVotesToken.sol index 25861caa..a2486f80 100644 --- a/src/util/TermFinanceVaultWrappedVotesToken.sol +++ b/src/util/TermFinanceVaultWrappedVotesToken.sol @@ -30,7 +30,10 @@ contract TermFinanceVaultWrappedVotesToken is ERC20Votes, Ownable { require(amount > 0, "Amount must be greater than zero"); // Transfer the underlying tokens from the user to the contract - require(underlyingToken.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + require( + underlyingToken.transferFrom(msg.sender, address(this), amount), + "Transfer failed" + ); // Track the deposit deposits[msg.sender] += amount; @@ -53,7 +56,10 @@ contract TermFinanceVaultWrappedVotesToken is ERC20Votes, Ownable { deposits[msg.sender] -= amount; // Transfer the underlying tokens back to the user - require(underlyingToken.transfer(msg.sender, amount), "Transfer failed"); + require( + underlyingToken.transfer(msg.sender, amount), + "Transfer failed" + ); emit Unwrapped(msg.sender, amount); }