From 8fa3be710af8ba657bd83f3a63bf0c04e14f372e Mon Sep 17 00:00:00 2001 From: aazhou1 Date: Thu, 31 Oct 2024 14:21:13 -0700 Subject: [PATCH 1/5] modify zero-discount-rates to prevent confusion with invalid discount rates --- src/RepoTokenList.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 65b15aae..c9237342 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -27,6 +27,7 @@ struct RepoTokenListData { library RepoTokenList { address public constant NULL_NODE = address(0); uint256 internal constant INVALID_AUCTION_RATE = 0; + uint256 internal constant ZERO_AUCTION_RATE = 0; error InvalidRepoToken(address token); @@ -357,15 +358,14 @@ library RepoTokenList { } uint256 oracleRate = discountRateAdapter.getDiscountRate(address(repoToken)); - if (oracleRate != INVALID_AUCTION_RATE) { + if (oracleRate != 0) { if (discountRate != oracleRate) { listData.discountRates[address(repoToken)] = oracleRate; } } } else { try discountRateAdapter.getDiscountRate(address(repoToken)) returns (uint256 rate) { - discountRate = rate; - + discountRate = rate == 0 ? ZERO_AUCTION_RATE : rate; } catch { discountRate = INVALID_AUCTION_RATE; return (false, discountRate, redemptionTimestamp); From dbb7cb7eb1cd4693d767ab82cc76da7c21b8fddc Mon Sep 17 00:00:00 2001 From: aazhou1 Date: Thu, 31 Oct 2024 14:25:15 -0700 Subject: [PATCH 2/5] zero auction rate is 1 --- src/RepoTokenList.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index c9237342..8e69e8a9 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -27,7 +27,7 @@ struct RepoTokenListData { library RepoTokenList { address public constant NULL_NODE = address(0); uint256 internal constant INVALID_AUCTION_RATE = 0; - uint256 internal constant ZERO_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); From 65e452baf1a4f020218be803571fac2b2e28b263 Mon Sep 17 00:00:00 2001 From: aazhou1 Date: Wed, 6 Nov 2024 22:54:19 -0800 Subject: [PATCH 3/5] safeguards on setting new controllers --- src/Strategy.sol | 22 ++-- src/TermDiscountRateAdapter.sol | 107 +++++++++++++----- .../term/ITermDiscountRateAdapter.sol | 2 +- src/test/kontrol/TermDiscountRateAdapter.sol | 2 +- 4 files changed, 97 insertions(+), 36 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 97b5735d..4a7d4938 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -117,20 +117,28 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard { /** * @notice Set the term controller - * @param newTermController The address of the new term controller + * @param newTermControllerAddr The address of the new term controller */ function setTermController( - address newTermController + address newTermControllerAddr ) external onlyRole(GOVERNOR_ROLE) { - require(newTermController != address(0)); - require(ITermController(newTermController).getProtocolReserveAddress() != address(0)); + require(newTermControllerAddr != address(0)); + require(ITermController(newTermControllerAddr).getProtocolReserveAddress() != address(0)); + ITermController newTermController = ITermController(newTermControllerAddr); + address currentIteration = repoTokenListData.head; + while (currentIteration != address(0)) { + if (!currTermController.isTermDeployed(currentIteration) && !newTermController.isTermDeployed(currentIteration)) { + revert("repoToken not in controllers"); + } + currentIteration = repoTokenListData.nodes[currentIteration].next; + } address current = address(currTermController); TERM_VAULT_EVENT_EMITTER.emitTermControllerUpdated( current, - newTermController + newTermControllerAddr ); prevTermController = ITermController(current); - currTermController = ITermController(newTermController); + currTermController = newTermController; } /** @@ -141,7 +149,7 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard { address newAdapter ) external onlyRole(GOVERNOR_ROLE) { ITermDiscountRateAdapter newDiscountRateAdapter = ITermDiscountRateAdapter(newAdapter); - require(address(newDiscountRateAdapter.TERM_CONTROLLER()) != address(0)); + require(address(newDiscountRateAdapter.currTermController()) != address(0)); TERM_VAULT_EVENT_EMITTER.emitDiscountRateAdapterUpdated( address(discountRateAdapter), newAdapter diff --git a/src/TermDiscountRateAdapter.sol b/src/TermDiscountRateAdapter.sol index f0a0d466..8ac310d8 100644 --- a/src/TermDiscountRateAdapter.sol +++ b/src/TermDiscountRateAdapter.sol @@ -9,50 +9,52 @@ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl { bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); - ITermController public immutable TERM_CONTROLLER; + /// @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_) { - TERM_CONTROLLER = ITermController(termController_); + 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 repoToken) public view virtual returns (uint256) { + function getDiscountRate(address termController, address repoToken) public view virtual returns (uint256) { + if (repoToken == address(0)) return 0; - (AuctionMetadata[] memory auctionMetadata, ) = TERM_CONTROLLER.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"); + ITermController tokenTermController; + if (termController == address(prevTermController)) { + tokenTermController = prevTermController; + } else if (termController == address(currTermController)) { + tokenTermController = currTermController; + } else { + revert("Invalid term controller"); } + return _getDiscountRate(tokenTermController, repoToken); + } - // If there is only 1 result (not a re-opening) then always return result - return auctionMetadata[0].auctionClearingRate; + /** + * @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); } /** @@ -68,12 +70,14 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl { bytes32 termAuctionId, bool isInvalid ) external onlyRole(ORACLE_ROLE) { + ITermController tokenTermController = _identifyTermController(repoToken); // Fetch the auction metadata for the given repo token - (AuctionMetadata[] memory auctionMetadata, ) = TERM_CONTROLLER.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId()); + (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"); @@ -81,6 +85,15 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl { 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 @@ -90,6 +103,46 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl { 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 view returns(bool auctionExists) { // Check if the termAuctionId exists in the metadata bool auctionExists; diff --git a/src/interfaces/term/ITermDiscountRateAdapter.sol b/src/interfaces/term/ITermDiscountRateAdapter.sol index c441b436..caf24200 100644 --- a/src/interfaces/term/ITermDiscountRateAdapter.sol +++ b/src/interfaces/term/ITermDiscountRateAdapter.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.18; import {ITermController} from "./ITermController.sol"; interface ITermDiscountRateAdapter { - function TERM_CONTROLLER() external view returns (ITermController); + function currTermController() external view returns (ITermController); function repoRedemptionHaircut(address) external view returns (uint256); function getDiscountRate(address repoToken) external view returns (uint256); } diff --git a/src/test/kontrol/TermDiscountRateAdapter.sol b/src/test/kontrol/TermDiscountRateAdapter.sol index 6226df6b..ab79ec48 100644 --- a/src/test/kontrol/TermDiscountRateAdapter.sol +++ b/src/test/kontrol/TermDiscountRateAdapter.sol @@ -31,7 +31,7 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, KontrolTest { return _discountRate[repoToken]; } - function TERM_CONTROLLER() external view returns (ITermController) { + function currTermController() external view returns (ITermController) { return ITermController(kevm.freshAddress()); } } From 46d35c452bb33e2aeecaad0a0d1545b518882d78 Mon Sep 17 00:00:00 2001 From: aazhou1 Date: Wed, 6 Nov 2024 23:18:36 -0800 Subject: [PATCH 4/5] integrate controlllers into discount rate adapter calls in value calculations --- src/RepoTokenList.sol | 15 +++++++++++++-- src/Strategy.sol | 16 ++++++++++++++-- src/TermAuctionList.sol | 11 +++++++++++ src/interfaces/term/ITermDiscountRateAdapter.sol | 1 + src/test/kontrol/TermDiscountRateAdapter.sol | 6 +++++- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 8e69e8a9..56762564 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -1,6 +1,7 @@ // 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"; @@ -176,22 +177,32 @@ library RepoTokenList { * @param listData The list data * @param discountRateAdapter The discount rate adapter * @param purchaseTokenPrecision The precision of the purchase token + * @param prevTermController The previous term controller + * @param currTermController The current term controller * @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 + uint256 purchaseTokenPrecision, + ITermController prevTermController, + ITermController currTermController ) internal view returns (uint256 totalPresentValue) { // If the list is empty, return 0 if (listData.head == NULL_NODE) return 0; address current = listData.head; + address tokenTermController; while (current != NULL_NODE) { uint256 currentMaturity = getRepoTokenMaturity(current); uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - uint256 discountRate = discountRateAdapter.getDiscountRate(current); + if (currTermController.isTermDeployed(current)){ + tokenTermController = address(currTermController); + } else if (prevTermController.isTermDeployed(current)){ + tokenTermController = address(prevTermController); + } + uint256 discountRate = discountRateAdapter.getDiscountRate(tokenTermController, current); // Convert repo token balance to base asset precision // (ratePrecision * repoPrecision * purchasePrecision) / (repoPrecision * ratePrecision) = purchasePrecision diff --git a/src/Strategy.sol b/src/Strategy.sol index 4a7d4938..08ac59bb 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -455,9 +455,15 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard { ) public view returns (uint256) { uint256 repoTokenHoldingPV; if (repoTokenListData.discountRates[repoToken] != 0) { + address tokenTermController; + if (currTermController.isTermDeployed(repoToken)){ + tokenTermController = address(currTermController); + } else if (prevTermController.isTermDeployed(repoToken)){ + tokenTermController = address(prevTermController); + } repoTokenHoldingPV = calculateRepoTokenPresentValue( repoToken, - discountRateAdapter.getDiscountRate(repoToken), + discountRateAdapter.getDiscountRate(tokenTermController, repoToken), ITermRepoToken(repoToken).balanceOf(address(this)) ); } @@ -467,6 +473,8 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard { repoTokenListData, discountRateAdapter, PURCHASE_TOKEN_PRECISION, + prevTermController, + currTermController, repoToken ); } @@ -520,12 +528,16 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard { liquidBalance + repoTokenListData.getPresentValue( discountRateAdapter, - PURCHASE_TOKEN_PRECISION + PURCHASE_TOKEN_PRECISION, + prevTermController, + currTermController ) + termAuctionListData.getPresentValue( repoTokenListData, discountRateAdapter, PURCHASE_TOKEN_PRECISION, + prevTermController, + currTermController, address(0) ); } diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index a3e49e3c..bdc61e4a 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -1,6 +1,7 @@ // 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"; @@ -241,6 +242,8 @@ library TermAuctionList { * @param repoTokenListData The repoToken list data * @param discountRateAdapter The discount rate adapter * @param purchaseTokenPrecision The precision of the purchase token + * @param prevTermController The previous term controller + * @param currTermController The current term controller * @param repoTokenToMatch The address of the repoToken to match (optional) * @return totalValue The total present value of the offers * @@ -254,6 +257,8 @@ library TermAuctionList { RepoTokenListData storage repoTokenListData, ITermDiscountRateAdapter discountRateAdapter, uint256 purchaseTokenPrecision, + ITermController prevTermController, + ITermController currTermController, address repoTokenToMatch ) internal view returns (uint256 totalValue) { // Return 0 if the list is empty @@ -273,6 +278,7 @@ library TermAuctionList { } uint256 offerAmount = offer.offerLocker.lockedOffer(current).amount; + address tokenTermController; // 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 @@ -285,6 +291,11 @@ library TermAuctionList { purchaseTokenPrecision, discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); + if (currTermController.isTermDeployed(offer.repoToken)){ + tokenTermController = address(currTermController); + } else if (prevTermController.isTermDeployed(offer.repoToken)){ + tokenTermController = address(prevTermController); + } totalValue += RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, purchaseTokenPrecision, diff --git a/src/interfaces/term/ITermDiscountRateAdapter.sol b/src/interfaces/term/ITermDiscountRateAdapter.sol index caf24200..be35f005 100644 --- a/src/interfaces/term/ITermDiscountRateAdapter.sol +++ b/src/interfaces/term/ITermDiscountRateAdapter.sol @@ -6,4 +6,5 @@ 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/test/kontrol/TermDiscountRateAdapter.sol b/src/test/kontrol/TermDiscountRateAdapter.sol index ab79ec48..ed1036c0 100644 --- a/src/test/kontrol/TermDiscountRateAdapter.sol +++ b/src/test/kontrol/TermDiscountRateAdapter.sol @@ -27,7 +27,11 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, KontrolTest { return _repoRedemptionHaircut[repoToken]; } - function getDiscountRate(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) { return _discountRate[repoToken]; } From 6bae8c24f72fcc5adcf426ec079fa53e8f47c6e5 Mon Sep 17 00:00:00 2001 From: aazhou1 Date: Thu, 7 Nov 2024 11:22:09 -0800 Subject: [PATCH 5/5] try/catch on insertion on discount rate oracle relookup --- src/RepoTokenList.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 56762564..85ced15e 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -368,7 +368,12 @@ library RepoTokenList { return (false, discountRate, redemptionTimestamp); //revert InvalidRepoToken(address(repoToken)); } - uint256 oracleRate = discountRateAdapter.getDiscountRate(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;