diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 1767a323..391eb224 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -113,6 +113,7 @@ library RepoTokenList { /** * @notice This function calculates the cumulative weighted time to maturity and cumulative amount of all repoTokens in the list. * @param listData The list data + * @param 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 @@ -127,6 +128,7 @@ library RepoTokenList { */ function getCumulativeRepoTokenData( RepoTokenListData storage listData, + ITermDiscountRateAdapter discountRateAdapter, address repoToken, uint256 repoTokenAmount, uint256 purchaseTokenPrecision @@ -148,12 +150,10 @@ library RepoTokenList { } // Convert the repo token balance to base asset precision - uint256 redemptionValue = ITermRepoToken(current).redemptionValue(); - uint256 repoTokenPrecision = 10**ERC20(current).decimals(); - uint256 repoTokenBalanceInBaseAssetPrecision = - (redemptionValue * repoTokenBalance * purchaseTokenPrecision) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + RepoTokenUtils.getNormalizedRepoTokenAmount( + current, repoTokenBalance, purchaseTokenPrecision, discountRateAdapter.repoRedemptionHaircut(current) + ); // Calculate the weighted time to maturity uint256 weightedTimeToMaturity = getRepoTokenWeightedTimeToMaturity( @@ -173,45 +173,31 @@ library RepoTokenList { /** * @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 - * @param repoTokenToMatch The address of the repoToken to match (optional) * @return totalPresentValue The total present value of the repoTokens - * @dev If the `repoTokenToMatch` parameter is provided (non-zero address), the function will filter - * the calculations to include only the specified repoToken. If `repoTokenToMatch` is not provided - * (zero address), it will aggregate the present value of all repoTokens in the list. - * - * Example usage: - * - To get the present value of all repoTokens: call with `repoTokenToMatch` set to `address(0)`. - * - To get the present value of a specific repoToken: call with `repoTokenToMatch` set to the address of the desired repoToken. + * @dev Aggregates the present value of all repoTokens in the list. */ function getPresentValue( RepoTokenListData storage listData, - uint256 purchaseTokenPrecision, - address repoTokenToMatch + 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) { - // Filter by a specific repoToken, address(0) bypasses this filter - if (repoTokenToMatch != address(0) && current != repoTokenToMatch) { - // Not a match, do not add to totalPresentValue - // Move to the next token in the list - current = _getNext(listData, current); - continue; - } - uint256 currentMaturity = getRepoTokenMaturity(current); uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - uint256 repoTokenPrecision = 10**ERC20(current).decimals(); - uint256 discountRate = listData.discountRates[current]; + uint256 discountRate = discountRateAdapter.getDiscountRate(current); // Convert repo token balance to base asset precision // (ratePrecision * repoPrecision * purchasePrecision) / (repoPrecision * ratePrecision) = purchasePrecision uint256 repoTokenBalanceInBaseAssetPrecision = - (ITermRepoToken(current).redemptionValue() * repoTokenBalance * purchaseTokenPrecision) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + RepoTokenUtils.getNormalizedRepoTokenAmount( + current, repoTokenBalance, purchaseTokenPrecision, discountRateAdapter.repoRedemptionHaircut(current) + ); // Calculate present value based on maturity if (currentMaturity > block.timestamp) { @@ -222,12 +208,6 @@ library RepoTokenList { totalPresentValue += repoTokenBalanceInBaseAssetPrecision; } - // Filter by a specific repo token, address(0) bypasses this condition - if (repoTokenToMatch != address(0) && current == repoTokenToMatch) { - // Found a match, terminate early - break; - } - // Move to the next token in the list current = _getNext(listData, current); } @@ -324,7 +304,7 @@ library RepoTokenList { (redemptionTimestamp, purchaseToken, , collateralManager) = repoToken.config(); // Validate purchase token - if (purchaseToken != address(asset)) { + if (purchaseToken != asset) { revert InvalidRepoToken(address(repoToken)); } diff --git a/src/RepoTokenUtils.sol b/src/RepoTokenUtils.sol index 1376f403..47fef655 100644 --- a/src/RepoTokenUtils.sol +++ b/src/RepoTokenUtils.sol @@ -12,40 +12,6 @@ library RepoTokenUtils { uint256 public constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; uint256 public constant RATE_PRECISION = 1e18; - /*////////////////////////////////////////////////////////////// - PURE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Convert repoToken amount to purchase token precision - * @param repoTokenPrecision The precision of the repoToken - * @param purchaseTokenPrecision The precision of the purchase token - * @param purchaseTokenAmountInRepoPrecision The amount of purchase token in repoToken precision - * @return The amount in purchase token precision - */ - function repoToPurchasePrecision( - uint256 repoTokenPrecision, - uint256 purchaseTokenPrecision, - uint256 purchaseTokenAmountInRepoPrecision - ) internal pure returns (uint256) { - return (purchaseTokenAmountInRepoPrecision * purchaseTokenPrecision) / repoTokenPrecision; - } - - /** - * @notice Convert purchase token amount to repoToken precision - * @param repoTokenPrecision The precision of the repoToken - * @param purchaseTokenPrecision The precision of the purchase token - * @param repoTokenAmount The amount of repoToken - * @return The amount in repoToken precision - */ - function purchaseToRepoPrecision( - uint256 repoTokenPrecision, - uint256 purchaseTokenPrecision, - uint256 repoTokenAmount - ) internal pure returns (uint256) { - return (repoTokenAmount * repoTokenPrecision) / purchaseTokenPrecision; - } - /*////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -78,17 +44,21 @@ library RepoTokenUtils { * @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 purchaseTokenPrecision, + uint256 repoRedemptionHaircut ) internal view returns (uint256 repoTokenAmountInBaseAssetPrecision) { uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals(); uint256 redemptionValue = ITermRepoToken(repoToken).redemptionValue(); repoTokenAmountInBaseAssetPrecision = - (redemptionValue * repoTokenAmount * purchaseTokenPrecision) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + repoRedemptionHaircut != 0 ? + (redemptionValue * repoRedemptionHaircut * repoTokenAmount * purchaseTokenPrecision) / + (repoTokenPrecision * RATE_PRECISION * 1e18) + : (redemptionValue * repoTokenAmount * purchaseTokenPrecision) / (repoTokenPrecision * RATE_PRECISION); } } \ No newline at end of file diff --git a/src/Strategy.sol b/src/Strategy.sol index 57e41094..2607f6a4 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -53,6 +53,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { IERC4626 public immutable YEARN_VAULT; /// @notice State variables + bool public depositLock; /// @dev Previous term controller ITermController public prevTermController; /// @dev Current term controller @@ -62,10 +63,9 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { TermAuctionListData internal termAuctionListData; uint256 public timeToMaturityThreshold; // seconds uint256 public requiredReserveRatio; // 1e18 - uint256 public discountRateMarkup; // 1e18 (TODO: check this) + uint256 public discountRateMarkup; // 1e18 uint256 public repoTokenConcentrationLimit; // 1e18 mapping(address => bool) public repoTokenBlacklist; - bool public depositLock; modifier notBlacklisted(address repoToken) { if (repoTokenBlacklist[repoToken]) { @@ -344,22 +344,21 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 proceeds; if (repoToken != address(0)) { if (!_isTermDeployed(repoToken)) { - revert RepoTokenList.InvalidRepoToken(address(repoToken)); + revert RepoTokenList.InvalidRepoToken(repoToken); } uint256 redemptionTimestamp = repoTokenListData.validateRepoToken( ITermRepoToken(repoToken), address(asset) ); - + uint256 discountRate = discountRateAdapter.getDiscountRate(repoToken); - uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); - repoTokenAmountInBaseAssetPrecision = (ITermRepoToken( - repoToken - ).redemptionValue() * - amount * - PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( + repoToken, + amount, + PURCHASE_TOKEN_PRECISION, + discountRateAdapter.repoRedemptionHaircut(repoToken) + ); proceeds = RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, PURCHASE_TOKEN_PRECISION, @@ -398,15 +397,16 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { address repoToken, uint256 discountRate, uint256 amount - ) external view returns (uint256) { + ) public view returns (uint256) { (uint256 redemptionTimestamp, , , ) = ITermRepoToken(repoToken) .config(); - uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); - uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) - .redemptionValue() * - amount * - PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + repoToken, + amount, + PURCHASE_TOKEN_PRECISION, + discountRateAdapter.repoRedemptionHaircut(repoToken) + ); return RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, @@ -428,11 +428,16 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function getRepoTokenHoldingValue( address repoToken ) public view returns (uint256) { - return - repoTokenListData.getPresentValue( - PURCHASE_TOKEN_PRECISION, - repoToken - ) + + uint256 repoTokenHoldingPV; + if (repoTokenListData.discountRates[repoToken] != 0) { + repoTokenHoldingPV = calculateRepoTokenPresentValue( + repoToken, + discountRateAdapter.getDiscountRate(repoToken), + ITermRepoToken(repoToken).balanceOf(address(this)) + ); + } + return + repoTokenHoldingPV + termAuctionListData.getPresentValue( repoTokenListData, discountRateAdapter, @@ -489,8 +494,8 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { return liquidBalance + repoTokenListData.getPresentValue( - PURCHASE_TOKEN_PRECISION, - address(0) + discountRateAdapter, + PURCHASE_TOKEN_PRECISION ) + termAuctionListData.getPresentValue( repoTokenListData, @@ -596,6 +601,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 cumulativeRepoTokenAmount, bool foundInRepoTokenList ) = repoTokenListData.getCumulativeRepoTokenData( + discountRateAdapter, repoToken, repoTokenAmount, PURCHASE_TOKEN_PRECISION @@ -611,6 +617,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { bool foundInOfferList ) = termAuctionListData.getCumulativeOfferData( repoTokenListData, + discountRateAdapter, repoToken, repoTokenAmount, PURCHASE_TOKEN_PRECISION @@ -625,11 +632,13 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { !foundInOfferList && repoToken != address(0) ) { + uint256 repoRedemptionHaircut = discountRateAdapter.repoRedemptionHaircut(repoToken); uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils .getNormalizedRepoTokenAmount( repoToken, repoTokenAmount, - PURCHASE_TOKEN_PRECISION + PURCHASE_TOKEN_PRECISION, + repoRedemptionHaircut ); cumulativeAmount += repoTokenAmountInBaseAssetPrecision; @@ -652,16 +661,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { (cumulativeAmount + liquidBalance); } - /** - * @notice Deposits all available asset tokens into the liquid vault - * - * @dev This function transfers the entire balance of the asset token held by this contract - * into the associated liquid vault. - */ - function _sweepAsset() private { - YEARN_VAULT.deposit(IERC20(asset).balanceOf(address(this)), address(this)); - } - /** * @notice Checks if a term contract is marked as deployed in either the current or previous term controller * @param termContract The address of the term contract to check @@ -689,36 +688,26 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * optimizing asset allocation. */ function _redeemRepoTokens(uint256 liquidAmountRequired) private { - uint256 liquidityBefore = IERC20(asset).balanceOf(address(this)); - // Remove completed auction offers - termAuctionListData.removeCompleted( - repoTokenListData, - discountRateAdapter, - address(asset) - ); + termAuctionListData.removeCompleted(repoTokenListData, discountRateAdapter, address(asset)); // Remove and redeem matured repoTokens repoTokenListData.removeAndRedeemMaturedTokens(); - uint256 liquidityAfter = IERC20(asset).balanceOf(address(this)); - uint256 liquidityDiff = liquidityAfter - liquidityBefore; + uint256 liquidity = IERC20(asset).balanceOf(address(this)); // Deposit excess underlying balance into Yearn Vault - if (liquidityDiff > liquidAmountRequired) { + if (liquidity > liquidAmountRequired) { unchecked { - YEARN_VAULT.deposit( - liquidityDiff - liquidAmountRequired, - address(this) - ); + YEARN_VAULT.deposit(liquidity - liquidAmountRequired, address(this)); } - // Withdraw shortfall from Yearn Vault to meet required liquidity - } else if (liquidityDiff < liquidAmountRequired) { + // Withdraw shortfall from Yearn Vault to meet required liquidity + } else if (liquidity < liquidAmountRequired) { unchecked { - _withdrawAsset(liquidAmountRequired - liquidityDiff); + _withdrawAsset(liquidAmountRequired - liquidity); } } - } +} /*////////////////////////////////////////////////////////////// STRATEGIST FUNCTIONS @@ -743,7 +732,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { revert InvalidTermAuction(address(termAuction)); } if (!_isTermDeployed(repoToken)) { - revert RepoTokenList.InvalidRepoToken(address(repoToken)); + revert RepoTokenList.InvalidRepoToken(repoToken); } require(termAuction.termRepoId() == ITermRepoToken(repoToken).termRepoId(), "repoToken does not match term repo ID"); @@ -801,7 +790,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); // Sweep assets, redeem matured repoTokens and ensure liquid balances up to date - _sweepAsset(); _redeemRepoTokens(0); bytes32 offerId = _generateOfferId(idHash, address(offerLocker)); @@ -830,9 +818,12 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Retrieve the total liquid balance uint256 liquidBalance = _totalLiquidBalance(); + uint256 totalAssetValue = _totalAssetValue(liquidBalance); + require(totalAssetValue > 0); + uint256 liquidReserveRatio = liquidBalance * 1e18 / totalAssetValue; // NOTE: we require totalAssetValue > 0 above // Check that new offer does not violate reserve ratio constraint - if (_liquidReserveRatio(liquidBalance) < requiredReserveRatio) { + if (liquidReserveRatio < requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } @@ -850,7 +841,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Passing in 0 amount and 0 liquid balance adjustment because offer and balance already updated - _validateRepoTokenConcentration(repoToken, 0, _totalAssetValue(liquidBalance), 0); + _validateRepoTokenConcentration(repoToken, 0, totalAssetValue, 0); } /** @@ -925,6 +916,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { PendingOffer storage pendingOffer = termAuctionListData.offers[offerIds[0]]; pendingOffer.offerAmount = offer.amount; } + + if (newOfferAmount < currentOfferAmount) { + YEARN_VAULT.deposit(IERC20(asset).balanceOf(address(this)), address(this)); + } + } /** @@ -958,7 +954,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); // Sweep any remaining assets and redeem repoTokens - _sweepAsset(); _redeemRepoTokens(0); } @@ -976,10 +971,9 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } /** - * @notice Close the auction + * @notice Required for post-processing after auction clos */ function auctionClosed() external { - _sweepAsset(); _redeemRepoTokens(0); } @@ -1001,11 +995,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Make sure repo token is valid and deployed by Term if (!_isTermDeployed(repoToken)) { - revert RepoTokenList.InvalidRepoToken(address(repoToken)); + revert RepoTokenList.InvalidRepoToken(repoToken); } // Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp - (uint256 discountRate, uint256 redemptionTimestamp) = repoTokenListData + (, uint256 redemptionTimestamp) = repoTokenListData .validateAndInsertRepoToken( ITermRepoToken(repoToken), discountRateAdapter, @@ -1013,20 +1007,23 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); // Sweep assets and redeem repoTokens, if needed - _sweepAsset(); _redeemRepoTokens(0); - // Retrieve total liquid balance and ensure it's greater than zero + // Retrieve total asset value and liquid balance and ensure they are greater than zero uint256 liquidBalance = _totalLiquidBalance(); require(liquidBalance > 0); + uint256 totalAssetValue = _totalAssetValue(liquidBalance); + require(totalAssetValue > 0); - // Calculate the repoToken amount in base asset precision - uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); - uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) - .redemptionValue() * - repoTokenAmount * - PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + uint256 discountRate = discountRateAdapter.getDiscountRate(repoToken); + + // Calculate the repoToken amount in base asset precision + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( + repoToken, + repoTokenAmount, + PURCHASE_TOKEN_PRECISION, + discountRateAdapter.repoRedemptionHaircut(repoToken) + ); // Calculate the proceeds from selling the repoToken uint256 proceeds = RepoTokenUtils.calculatePresentValue( @@ -1052,8 +1049,8 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Ensure the remaining liquid balance is above the liquidity threshold - uint256 newLiquidBalance = liquidBalance - proceeds; - if (_liquidReserveRatio(newLiquidBalance) < requiredReserveRatio) { + uint256 newLiquidReserveRatio = ( liquidBalance - proceeds ) * 1e18 / totalAssetValue; // NOTE: we require totalAssetValue > 0 above + if (newLiquidReserveRatio < requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } @@ -1061,7 +1058,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { _validateRepoTokenConcentration( repoToken, repoTokenAmountInBaseAssetPrecision, - _totalAssetValue(liquidBalance), + totalAssetValue, proceeds ); @@ -1101,6 +1098,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { discountRateAdapter = ITermDiscountRateAdapter(_discountRateAdapter); IERC20(_asset).safeApprove(_yearnVault, type(uint256).max); + + timeToMaturityThreshold = 45 days; + requiredReserveRatio = 0.2e18; + discountRateMarkup = 0.005e18; + repoTokenConcentrationLimit = 0.1e18; } /*////////////////////////////////////////////////////////////// @@ -1123,7 +1125,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { revert DepositPaused(); } - _sweepAsset(); _redeemRepoTokens(0); } @@ -1180,7 +1181,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { whenNotPaused returns (uint256 _totalAssets) { - _sweepAsset(); _redeemRepoTokens(0); return _totalAssetValue(_totalLiquidBalance()); } @@ -1213,38 +1213,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { return _totalLiquidBalance(); } - /** - * @notice Gets the max amount of `asset` that an address can deposit. - * @dev Defaults to an unlimited amount for any address. But can - * be overridden by strategists. - * - * This function will be called before any deposit or mints to enforce - * any limits desired by the strategist. This can be used for either a - * traditional deposit limit or for implementing a whitelist etc. - * - * EX: - * if(isAllowed[_owner]) return super.availableDepositLimit(_owner); - * - * This does not need to take into account any conversion rates - * from shares to assets. But should know that any non max uint256 - * amounts may be converted to shares. So it is recommended to keep - * custom amounts low enough as not to cause overflow when multiplied - * by `totalSupply`. - * - * @param . The address that is depositing into the strategy. - * @return . The available amount the `_owner` can deposit in terms of `asset` - * - function availableDepositLimit( - address _owner - ) public view override returns (uint256) { - TODO: If desired Implement deposit limit logic and any needed state variables . - - EX: - uint256 totalAssets = TokenizedStrategy.totalAssets(); - return totalAssets >= depositLimit ? 0 : depositLimit - totalAssets; - } - */ - /** * @dev Optional function for strategist to override that can * be called in between reports. @@ -1300,12 +1268,9 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * @param _amount The amount of asset to attempt to free. * function _emergencyWithdraw(uint256 _amount) internal override { - TODO: If desired implement simple logic to free deployed funds. - EX: _amount = min(_amount, aToken.balanceOf(address(this))); _freeFunds(_amount); } - */ } diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 672b3b75..c3d0f9ca 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -16,16 +16,6 @@ struct PendingOffer { ITermAuctionOfferLocker offerLocker; } -// In-memory representation of an offer object -struct PendingOfferMemory { - bytes32 offerId; - address repoToken; - uint256 offerAmount; - ITermAuction termAuction; - ITermAuctionOfferLocker offerLocker; - bool isRepoTokenSeen; -} - struct TermAuctionListNode { bytes32 next; } @@ -59,51 +49,6 @@ library TermAuctionList { return listData.nodes[current].next; } - /** - * @notice Loads all pending offers into an array of `PendingOfferMemory` structs - * @param listData The list data - * @return offers An array of structs containing details of all pending offers - * - * @dev This function iterates through the list of offers and gathers their details into an array of `PendingOfferMemory` structs. - * This makes it easier to process and analyze the pending offers. - */ - function _loadOffers(TermAuctionListData storage listData) private view returns (PendingOfferMemory[] memory offers) { - uint256 len = _count(listData); - offers = new PendingOfferMemory[](len); - - uint256 i; - bytes32 current = listData.head; - while (current != NULL_NODE) { - PendingOffer memory currentOffer = listData.offers[current]; - PendingOfferMemory memory newOffer = offers[i]; - - newOffer.offerId = current; - newOffer.repoToken = currentOffer.repoToken; - newOffer.offerAmount = currentOffer.offerAmount; - newOffer.termAuction = currentOffer.termAuction; - newOffer.offerLocker = currentOffer.offerLocker; - - i++; - current = _getNext(listData, current); - } - } - - /** - * @notice Marks a specific repoToken as seen within an array of `PendingOfferMemory` structs - * @param offers The array of `PendingOfferMemory` structs representing the pending offers - * @param repoToken The address of the repoToken to be marked as seen - * - * @dev This function iterates through the `offers` array and sets the `isRepoTokenSeen` flag to `true` - * for the specified `repoToken`. This helps to avoid double-counting or reprocessing the same repoToken. - */ - function _markRepoTokenAsSeen(PendingOfferMemory[] memory offers, address repoToken) private pure { - for (uint256 i; i < offers.length; i++) { - if (repoToken == offers[i].repoToken) { - offers[i].isRepoTokenSeen = true; - } - } - } - /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -149,17 +94,53 @@ library TermAuctionList { * @param offerId The ID of the offer to be inserted * @param pendingOffer The `PendingOffer` struct containing details of the offer to be inserted * - * @dev This function inserts a new pending offer at the beginning of the linked list in the `TermAuctionListData` structure. - * It updates the `next` pointers and the head of the list to ensure the new offer is correctly linked. + * @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; - if (current != NULL_NODE) { - listData.nodes[offerId].next = current; + // If the list is empty, set the new repoToken as the head + if (current == NULL_NODE) { + listData.head = offerId; + listData.offers[offerId] = pendingOffer; + return; } - listData.head = offerId; + 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 repoToken before current if its maturity is less than or equal + 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; + break; + } + + prev = current; + current = next; + } listData.offers[offerId] = pendingOffer; } @@ -210,7 +191,7 @@ library TermAuctionList { removeNode = true; bytes32[] memory offerIds = new bytes32[](1); offerIds[0] = current; - offer.offerLocker.unlockOffers(offerIds); // unlocking offer in this scenario withdraws offer ammount + offer.offerLocker.unlockOffers(offerIds); // unlocking offer in this scenario withdraws offer amount } } @@ -226,7 +207,6 @@ library TermAuctionList { } if (insertRepoToken) { - // TODO: do we need to validate termDeployed(repoToken) here? // Auction still open => include offerAmount in totalValue // (otherwise locked purchaseToken will be missing from TV) @@ -268,12 +248,11 @@ library TermAuctionList { ) 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 - // Load pending offers - PendingOfferMemory[] memory offers = _loadOffers(listData); - - for (uint256 i; i < offers.length; i++) { - PendingOfferMemory memory offer = offers[i]; + 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) { @@ -281,17 +260,18 @@ library TermAuctionList { continue; } - uint256 offerAmount = offer.offerLocker.lockedOffer(offer.offerId).amount; + 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 (!offer.isRepoTokenSeen) { + if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) { + if (edgeCaseAuction != address(offer.termAuction)) { uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); totalValue += RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, @@ -300,15 +280,18 @@ library TermAuctionList { discountRateAdapter.getDiscountRate(offer.repoToken) ); - // Mark the repo token as seen to avoid double counting - // since multiple offers can be tied to the same repoToken, we need to mark - // the repoTokens we've seen to avoid double counting - _markRepoTokenAsSeen(offers, offer.repoToken); + // 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); } } @@ -316,6 +299,7 @@ library TermAuctionList { * @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 @@ -331,18 +315,19 @@ library TermAuctionList { 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 - // Load pending offers from the list data - PendingOfferMemory[] memory offers = _loadOffers(listData); - for (uint256 i; i < offers.length; i++) { - PendingOfferMemory memory offer = offers[i]; + bytes32 current = listData.head; + while (current != NULL_NODE) { + PendingOffer storage offer =listData.offers[current]; uint256 offerAmount; if (offer.repoToken == repoToken) { @@ -350,21 +335,26 @@ library TermAuctionList { found = true; } else { // Retrieve the current offer amount from the offer locker - offerAmount = offer.offerLocker.lockedOffer(offer.offerId).amount; + 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) { + if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) { // use normalized repoToken amount if repoToken is not in the list - if (!offer.isRepoTokenSeen) { + if (edgeCaseAuction != address(offer.termAuction)) { offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); - _markRepoTokenAsSeen(offers, 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); } } } @@ -378,6 +368,9 @@ library TermAuctionList { cumulativeWeightedTimeToMaturity += weightedTimeToMaturity; cumulativeOfferAmount += offerAmount; } + + // Move to the next token in the list + current = _getNext(listData, current); } } } \ No newline at end of file diff --git a/src/TermDiscountRateAdapter.sol b/src/TermDiscountRateAdapter.sol index e6610ee7..ac8f5a48 100644 --- a/src/TermDiscountRateAdapter.sol +++ b/src/TermDiscountRateAdapter.sol @@ -4,22 +4,28 @@ pragma solidity ^0.8.18; import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.sol"; import {ITermController, AuctionMetadata} from "./interfaces/term/ITermController.sol"; import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; +import "@openzeppelin/contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; /** * @title TermDiscountRateAdapter * @notice Adapter contract to retrieve discount rates for Term repo tokens * @dev This contract implements the ITermDiscountRateAdapter interface and interacts with the Term Controller */ -contract TermDiscountRateAdapter is ITermDiscountRateAdapter { +contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControlUpgradeable { + + bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); /// @notice The Term Controller contract ITermController public immutable TERM_CONTROLLER; + mapping(address => uint256) public repoRedemptionHaircut; /** * @notice Constructor to initialize the TermDiscountRateAdapter * @param termController_ The address of the Term Controller contract + * @param oracleWallet_ The address of the oracle wallet */ - constructor(address termController_) { + constructor(address termController_, address oracleWallet_) { TERM_CONTROLLER = ITermController(termController_); + _grantRole(ORACLE_ROLE, oracleWallet_); } /** @@ -37,4 +43,13 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter { return auctionMetadata[len - 1].auctionClearingRate; } -} + + /** + * @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; + } +} \ No newline at end of file diff --git a/src/TermVaultEventEmitter.sol b/src/TermVaultEventEmitter.sol index 84240332..d5716c36 100644 --- a/src/TermVaultEventEmitter.sol +++ b/src/TermVaultEventEmitter.sol @@ -33,6 +33,7 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU 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) { diff --git a/src/interfaces/term/ITermDiscountRateAdapter.sol b/src/interfaces/term/ITermDiscountRateAdapter.sol index 97fe4019..feba49c3 100644 --- a/src/interfaces/term/ITermDiscountRateAdapter.sol +++ b/src/interfaces/term/ITermDiscountRateAdapter.sol @@ -2,5 +2,6 @@ pragma solidity ^0.8.18; interface ITermDiscountRateAdapter { + function repoRedemptionHaircut(address) external view returns (uint256); function getDiscountRate(address repoToken) external view returns (uint256); } diff --git a/src/interfaces/term/ITermVaultEvents.sol b/src/interfaces/term/ITermVaultEvents.sol index 5ebb1076..c7ec9e79 100644 --- a/src/interfaces/term/ITermVaultEvents.sol +++ b/src/interfaces/term/ITermVaultEvents.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.18; interface ITermVaultEvents { + event VaultContractPaired(address vault); + event TermControllerUpdated(address oldController, address newController); event TimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold); diff --git a/src/periphery/StrategyAprOracle.sol b/src/periphery/StrategyAprOracle.sol index 1c0bfb8f..96d0f8ab 100644 --- a/src/periphery/StrategyAprOracle.sol +++ b/src/periphery/StrategyAprOracle.sol @@ -28,7 +28,7 @@ contract StrategyAprOracle is AprOracleBase { function aprAfterDebtChange( address _strategy, int256 _delta - ) external view override returns (uint256) { + ) external pure override returns (uint256) { // TODO: Implement any necessary logic to return the most accurate // APR estimation for the strategy. return 1e17; diff --git a/src/test/TestUSDCOffers.t.sol b/src/test/TestUSDCOffers.t.sol index 5dfccf4a..ea30441a 100644 --- a/src/test/TestUSDCOffers.t.sol +++ b/src/test/TestUSDCOffers.t.sol @@ -37,6 +37,8 @@ contract TestUSDCSubmitOffer is Setup { 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 diff --git a/src/test/TestUSDCSellRepoToken.t.sol b/src/test/TestUSDCSellRepoToken.t.sol index 1181d154..c6c6ee23 100644 --- a/src/test/TestUSDCSellRepoToken.t.sol +++ b/src/test/TestUSDCSellRepoToken.t.sol @@ -49,6 +49,8 @@ contract TestUSDCSellRepoToken is Setup { termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); termStrategy.setTimeToMaturityThreshold(10 weeks); termStrategy.setRepoTokenConcentrationLimit(1e18); + termStrategy.setRequiredReserveRatio(0); + termStrategy.setdiscountRateMarkup(0); vm.stopPrank(); } diff --git a/src/test/TestUSDCSubmitOffer.t.sol b/src/test/TestUSDCSubmitOffer.t.sol index 0550c844..07bffcca 100644 --- a/src/test/TestUSDCSubmitOffer.t.sol +++ b/src/test/TestUSDCSubmitOffer.t.sol @@ -37,6 +37,8 @@ contract TestUSDCSubmitOffer is Setup { 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 diff --git a/src/test/mocks/MockTermAuctionOfferLocker.sol b/src/test/mocks/MockTermAuctionOfferLocker.sol index 2c69173d..7748ea37 100644 --- a/src/test/mocks/MockTermAuctionOfferLocker.sol +++ b/src/test/mocks/MockTermAuctionOfferLocker.sol @@ -25,7 +25,7 @@ contract MockTermAuctionOfferLocker is ITermAuctionOfferLocker { purchaseToken = _purchaseToken; termRepoServicer = _repoServicer; repoLocker = MockTermRepoLocker(_repoLocker); - auctionStartTime = block.timestamp; + auctionStartTime = 0; } function termRepoId() external view returns (bytes32) { @@ -41,7 +41,7 @@ contract MockTermAuctionOfferLocker is ITermAuctionOfferLocker { } function revealTime() external view returns (uint256) { - + return auction.auctionEndTime(); } function lockedOffer(bytes32 id) external view returns (TermAuctionOffer memory) { diff --git a/src/test/utils/Setup.sol b/src/test/utils/Setup.sol index a2dc0fe4..fea7c67a 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -50,6 +50,7 @@ contract Setup is ExtendedTest, IEvents { address public performanceFeeRecipient = address(3); address public adminWallet = address(111); address public devopsWallet = address(222); + address public oracleWallet = address(333); // Address of the real deployed Factory address public factory; @@ -95,7 +96,7 @@ contract Setup is ExtendedTest, IEvents { vm.etch(0xBB51273D6c746910C7C06fe718f30c936170feD0, address(tokenizedStrategy).code); termController = new MockTermController(); - discountRateAdapter = new TermDiscountRateAdapter(address(termController)); + discountRateAdapter = new TermDiscountRateAdapter(address(termController), oracleWallet); termVaultEventEmitterImpl = new TermVaultEventEmitter(); termVaultEventEmitter = TermVaultEventEmitter(address(new ERC1967Proxy(address(termVaultEventEmitterImpl), ""))); mockYearnVault = new ERC4626Mock(address(asset));