From 706bef2228c72d50f1fc1a863a298374bbb1bf10 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 18:48:45 -0700 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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,