From ea792f153178d72e0d0d97cab65b606d10801f33 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 17:07:52 -0700 Subject: [PATCH 01/11] get rid of unneccessary looping for present value of single token --- src/RepoTokenList.sol | 26 ++------------------------ src/Strategy.sol | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 1c586604..31029176 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -175,35 +175,19 @@ library RepoTokenList { * @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 Aaggregates the present value of all repoTokens in the list. */ function getPresentValue( RepoTokenListData storage listData, ITermDiscountRateAdapter discountRateAdapter, - uint256 purchaseTokenPrecision, - address repoTokenToMatch + 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 discountRate = discountRateAdapter.getDiscountRate(current); @@ -224,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); } diff --git a/src/Strategy.sol b/src/Strategy.sol index fd7ccf78..2607f6a4 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -428,12 +428,16 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function getRepoTokenHoldingValue( address repoToken ) public view returns (uint256) { - return - repoTokenListData.getPresentValue( - discountRateAdapter, - 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, @@ -491,8 +495,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { liquidBalance + repoTokenListData.getPresentValue( discountRateAdapter, - PURCHASE_TOKEN_PRECISION, - address(0) + PURCHASE_TOKEN_PRECISION ) + termAuctionListData.getPresentValue( repoTokenListData, From 706bef2228c72d50f1fc1a863a298374bbb1bf10 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 18:48:45 -0700 Subject: [PATCH 02/11] remove memory load --- src/TermAuctionList.sol | 138 +++++++++++++---------------- src/test/TestUSDCSubmitOffer.t.sol | 2 +- 2 files changed, 61 insertions(+), 79 deletions(-) diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 7a4554b8..4822fe77 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,52 @@ 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; + 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; } @@ -267,12 +247,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) { @@ -280,13 +259,13 @@ 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 (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) { - if (!offer.isRepoTokenSeen) { + if (edgeCaseAuction != address(offer.termAuction)) { uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, ITermRepoToken(offer.repoToken).balanceOf(address(this)), @@ -300,15 +279,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); } } @@ -339,12 +321,12 @@ library TermAuctionList { ) 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) { @@ -352,14 +334,14 @@ 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 (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)), @@ -367,7 +349,7 @@ library TermAuctionList { discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); - _markRepoTokenAsSeen(offers, offer.repoToken); + edgeCaseAuction = address(offer.termAuction); } } } diff --git a/src/test/TestUSDCSubmitOffer.t.sol b/src/test/TestUSDCSubmitOffer.t.sol index 07bffcca..5abd318c 100644 --- a/src/test/TestUSDCSubmitOffer.t.sol +++ b/src/test/TestUSDCSubmitOffer.t.sol @@ -8,7 +8,7 @@ import {MockUSDC} from "./mocks/MockUSDC.sol"; import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol"; import {Strategy} from "../Strategy.sol"; -contract TestUSDCSubmitOffer is Setup { +contract TestUSDCSubmitOf1er1 is Setup { uint256 internal constant TEST_REPO_TOKEN_RATE = 0.05e18; MockUSDC internal mockUSDC; From 3c59187910b456250984ebb89a74a2ebd0cff4f5 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 19:20:15 -0700 Subject: [PATCH 03/11] fix test name --- src/test/TestUSDCSubmitOffer.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/TestUSDCSubmitOffer.t.sol b/src/test/TestUSDCSubmitOffer.t.sol index 5abd318c..07bffcca 100644 --- a/src/test/TestUSDCSubmitOffer.t.sol +++ b/src/test/TestUSDCSubmitOffer.t.sol @@ -8,7 +8,7 @@ import {MockUSDC} from "./mocks/MockUSDC.sol"; import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol"; import {Strategy} from "../Strategy.sol"; -contract TestUSDCSubmitOf1er1 is Setup { +contract TestUSDCSubmitOffer is Setup { uint256 internal constant TEST_REPO_TOKEN_RATE = 0.05e18; MockUSDC internal mockUSDC; From 7ab2aaaf013d9aecad03e2bcfb2d2682aa28caaf Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 19:24:48 -0700 Subject: [PATCH 04/11] increment linked list counter --- src/TermAuctionList.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 4822fe77..2b11cd9b 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -363,6 +363,8 @@ library TermAuctionList { cumulativeWeightedTimeToMaturity += weightedTimeToMaturity; cumulativeOfferAmount += offerAmount; } + // Move to the next token in the list + current = _getNext(listData, current); } } } \ No newline at end of file From 6d1ea56bba907f6114c48446b116f7083ace9c9f Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 20:40:32 -0700 Subject: [PATCH 05/11] load offer into memory on first insert in auction list --- src/TermAuctionList.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 2b11cd9b..b3d623f3 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -8,6 +8,9 @@ import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapt import {RepoTokenList, RepoTokenListData} from "./RepoTokenList.sol"; import {RepoTokenUtils} from "./RepoTokenUtils.sol"; +import "forge-std/console.sol"; + + // In-storage representation of an offer object struct PendingOffer { address repoToken; @@ -103,6 +106,7 @@ library TermAuctionList { // If the list is empty, set the new repoToken as the head if (current == NULL_NODE) { listData.head = offerId; + listData.offers[offerId] = pendingOffer; return; } @@ -363,6 +367,7 @@ library TermAuctionList { cumulativeWeightedTimeToMaturity += weightedTimeToMaturity; cumulativeOfferAmount += offerAmount; } + // Move to the next token in the list current = _getNext(listData, current); } From a23c0d987a6f73ecbfc174069be6b38dbfd9d52d Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 20:42:26 -0700 Subject: [PATCH 06/11] cleanup --- src/TermAuctionList.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index b3d623f3..c3d0f9ca 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -8,9 +8,6 @@ import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapt import {RepoTokenList, RepoTokenListData} from "./RepoTokenList.sol"; import {RepoTokenUtils} from "./RepoTokenUtils.sol"; -import "forge-std/console.sol"; - - // In-storage representation of an offer object struct PendingOffer { address repoToken; @@ -353,6 +350,10 @@ library TermAuctionList { 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); } } From c36a2be1e798ffb90bf111f891aea196448a1f98 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Thu, 12 Sep 2024 11:35:39 -0700 Subject: [PATCH 07/11] fix mispelling in docs --- src/RepoTokenList.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 31029176..391eb224 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -176,7 +176,7 @@ library RepoTokenList { * @param discountRateAdapter The discount rate adapter * @param purchaseTokenPrecision The precision of the purchase token * @return totalPresentValue The total present value of the repoTokens - * @dev Aaggregates the present value of all repoTokens in the list. + * @dev Aggregates the present value of all repoTokens in the list. */ function getPresentValue( RepoTokenListData storage listData, From 8efc7785a9c72c9e9f4ed05434a6826c1128e97f Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Thu, 12 Sep 2024 13:42:43 -0700 Subject: [PATCH 08/11] sanity checks on setting new controllers and discount rate adapters --- src/Strategy.sol | 5 ++++- src/interfaces/term/ITermController.sol | 2 ++ src/interfaces/term/ITermDiscountRateAdapter.sol | 2 ++ src/test/mocks/MockTermController.sol | 4 ++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 2607f6a4..6dcc3788 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -120,6 +120,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { address newTermController ) external onlyManagement { require(newTermController != address(0)); + require(ITermController(newTermController).getProtocolReserveAddress() != address(0)); address current = address(currTermController); TERM_VAULT_EVENT_EMITTER.emitTermControllerUpdated( current, @@ -136,11 +137,13 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function setDiscountRateAdapter( address newAdapter ) external onlyManagement { + ITermDiscountRateAdapter newDiscountRateAdapter = ITermDiscountRateAdapter(newAdapter); + require(address(newDiscountRateAdapter.TERM_CONTROLLER()) != address(0)); TERM_VAULT_EVENT_EMITTER.emitDiscountRateAdapterUpdated( address(discountRateAdapter), newAdapter ); - discountRateAdapter = ITermDiscountRateAdapter(newAdapter); + discountRateAdapter = newDiscountRateAdapter; } /** diff --git a/src/interfaces/term/ITermController.sol b/src/interfaces/term/ITermController.sol index c8f3d2d7..e121fd11 100644 --- a/src/interfaces/term/ITermController.sol +++ b/src/interfaces/term/ITermController.sol @@ -10,5 +10,7 @@ struct AuctionMetadata { 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 feba49c3..c441b436 100644 --- a/src/interfaces/term/ITermDiscountRateAdapter.sol +++ b/src/interfaces/term/ITermDiscountRateAdapter.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.18; +import {ITermController} from "./ITermController.sol"; interface ITermDiscountRateAdapter { + function TERM_CONTROLLER() external view returns (ITermController); function repoRedemptionHaircut(address) external view returns (uint256); function getDiscountRate(address repoToken) external view returns (uint256); } diff --git a/src/test/mocks/MockTermController.sol b/src/test/mocks/MockTermController.sol index 08fa6ed5..f953a53f 100644 --- a/src/test/mocks/MockTermController.sol +++ b/src/test/mocks/MockTermController.sol @@ -15,6 +15,10 @@ contract MockTermController is ITermController { return true; } + function getProtocolReserveAddress() external view returns (address) { + return address(100); + } + function setOracleRate(bytes32 termRepoId, uint256 oracleRate) external { AuctionMetadata memory metadata; From 32490f6b4026cc8f855e201f1c2b6f13b7b9a7e8 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Thu, 12 Sep 2024 15:01:30 -0700 Subject: [PATCH 09/11] delete StrategyAprOracle --- src/periphery/StrategyAprOracle.sol | 36 ----------------------------- 1 file changed, 36 deletions(-) delete mode 100644 src/periphery/StrategyAprOracle.sol diff --git a/src/periphery/StrategyAprOracle.sol b/src/periphery/StrategyAprOracle.sol deleted file mode 100644 index 96d0f8ab..00000000 --- a/src/periphery/StrategyAprOracle.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; - -import {AprOracleBase} from "@periphery/AprOracle/AprOracleBase.sol"; - -contract StrategyAprOracle is AprOracleBase { - constructor() AprOracleBase("Strategy Apr Oracle Example", msg.sender) {} - - /** - * @notice Will return the expected Apr of a strategy post a debt change. - * @dev _delta is a signed integer so that it can also represent a debt - * decrease. - * - * This should return the annual expected return at the current timestamp - * represented as 1e18. - * - * ie. 10% == 1e17 - * - * _delta will be == 0 to get the current apr. - * - * This will potentially be called during non-view functions so gas - * efficiency should be taken into account. - * - * @param _strategy The token to get the apr for. - * @param _delta The difference in debt. - * @return . The expected apr for the strategy represented as 1e18. - */ - function aprAfterDebtChange( - address _strategy, - int256 _delta - ) external pure override returns (uint256) { - // TODO: Implement any necessary logic to return the most accurate - // APR estimation for the strategy. - return 1e17; - } -} From 39cf09cd702a24d3d7c047bfe9cc2721f13de1c2 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Thu, 12 Sep 2024 15:17:33 -0700 Subject: [PATCH 10/11] sync rate adapter with listing --- src/TermDiscountRateAdapter.sol | 90 +++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/src/TermDiscountRateAdapter.sol b/src/TermDiscountRateAdapter.sol index ac8f5a48..f0a0d466 100644 --- a/src/TermDiscountRateAdapter.sol +++ b/src/TermDiscountRateAdapter.sol @@ -1,31 +1,21 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.18; +// 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 "@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, AccessControlUpgradeable { +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl { bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); - /// @notice The Term Controller contract + ITermController public immutable TERM_CONTROLLER; + mapping(address => mapping (bytes32 => bool)) public rateInvalid; 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_, address oracleWallet_) { TERM_CONTROLLER = ITermController(termController_); - _grantRole(ORACLE_ROLE, oracleWallet_); + _grantRole(ORACLE_ROLE, oracleWallet_); } /** @@ -35,13 +25,60 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControlUpgra * @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) external view returns (uint256) { + function getDiscountRate(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); + 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; + } - return auctionMetadata[len - 1].auctionClearingRate; + /** + * @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) { + // Fetch the auction metadata for the given repo token + (AuctionMetadata[] memory auctionMetadata, ) = TERM_CONTROLLER.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId()); + + // Check if the termAuctionId exists in the metadata + bool auctionExists = _validateAuctionExistence(auctionMetadata, termAuctionId); + + // Revert if the auction doesn't exist + require(auctionExists, "Auction ID not found in metadata"); + + // Update the rate invalidation status + rateInvalid[repoToken][termAuctionId] = isInvalid; } /** @@ -52,4 +89,15 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControlUpgra function setRepoRedemptionHaircut(address repoToken, uint256 haircut) external onlyRole(ORACLE_ROLE) { repoRedemptionHaircut[repoToken] = haircut; } -} \ No newline at end of file + + function _validateAuctionExistence(AuctionMetadata[] memory auctionMetadata, bytes32 termAuctionId) private view returns(bool auctionExists) { + // Check if the termAuctionId exists in the metadata + bool auctionExists; + for (uint256 i = 0; i < auctionMetadata.length; i++) { + if (auctionMetadata[i].termAuctionId == termAuctionId) { + auctionExists = true; + break; + } + } + } +} From ba30a203b25c0e8cc98874afc70bf85b98585272 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Thu, 12 Sep 2024 15:20:01 -0700 Subject: [PATCH 11/11] remove apr oracle tests --- src/test/Oracle.t.sol | 63 ------------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 src/test/Oracle.t.sol diff --git a/src/test/Oracle.t.sol b/src/test/Oracle.t.sol deleted file mode 100644 index f7c349c4..00000000 --- a/src/test/Oracle.t.sol +++ /dev/null @@ -1,63 +0,0 @@ -pragma solidity ^0.8.18; - -import "forge-std/console2.sol"; -import {Setup} from "./utils/Setup.sol"; - -import {StrategyAprOracle} from "../periphery/StrategyAprOracle.sol"; - -contract OracleTest is Setup { - StrategyAprOracle public oracle; - - function setUp() public override { - super.setUp(); - oracle = new StrategyAprOracle(); - } - - function checkOracle(address _strategy, uint256 _delta) public { - // Check set up - // TODO: Add checks for the setup - - uint256 currentApr = oracle.aprAfterDebtChange(_strategy, 0); - - // Should be greater than 0 but likely less than 100% - assertGt(currentApr, 0, "ZERO"); - assertLt(currentApr, 1e18, "+100%"); - - // TODO: Uncomment to test the apr goes up and down based on debt changes - /** - uint256 negativeDebtChangeApr = oracle.aprAfterDebtChange(_strategy, -int256(_delta)); - - // The apr should go up if deposits go down - assertLt(currentApr, negativeDebtChangeApr, "negative change"); - - uint256 positiveDebtChangeApr = oracle.aprAfterDebtChange(_strategy, int256(_delta)); - - assertGt(currentApr, positiveDebtChangeApr, "positive change"); - */ - - // TODO: Uncomment if there are setter functions to test. - /** - vm.expectRevert("!governance"); - vm.prank(user); - oracle.setterFunction(setterVariable); - - vm.prank(management); - oracle.setterFunction(setterVariable); - - assertEq(oracle.setterVariable(), setterVariable); - */ - } - - function test_oracle(uint256 _amount, uint16 _percentChange) public { - vm.assume(_amount > minFuzzAmount && _amount < maxFuzzAmount); - _percentChange = uint16(bound(uint256(_percentChange), 10, MAX_BPS)); - - mintAndDepositIntoStrategy(strategy, user, _amount); - - uint256 _delta = (_amount * _percentChange) / MAX_BPS; - - checkOracle(address(strategy), _delta); - } - - // TODO: Deploy multiple strategies with different tokens as `asset` to test against the oracle. -}