@@ -170,16 +170,20 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
170170 /// @notice All requests indexed by request ID
171171 mapping (uint256 => Request) public requests;
172172
173- /// @notice Array of pending request IDs awaiting processing (FIFO order)
174- uint256 [] public pendingRequestIds;
175-
176- /// @notice Index of request ID in global pending array (for O(1) lookup)
177- mapping (uint256 => uint256 ) private _requestIndexInGlobalArray;
178-
179173 /// @notice Index of yieldVaultId in user's yieldVaultsByUser array (for O(1) removal)
180174 /// @dev Internal visibility allows test helpers to properly initialize state
181175 mapping (address => mapping (uint64 => uint256 )) internal _yieldVaultIndexInUserArray;
182176
177+ /// @notice Mapping of queued request IDs awaiting processing (FIFO order)
178+ mapping (uint256 => uint256 ) private _requestsQueue;
179+
180+ /// @notice Pointer to the current head in _requestsQueue. Denotes the next request to be processed
181+ uint256 private _requestsQueueHead = 1 ;
182+
183+ /// @notice Pointer to the current tail in _requestsQueue. Points to the next available
184+ /// slot — i.e., one past the last enqueued request.
185+ uint256 private _requestsQueueTail = 1 ;
186+
183187 // ============================================
184188 // Errors
185189 // ============================================
@@ -282,6 +286,12 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
282286 /// @notice No refund available for the specified token
283287 error NoRefundAvailable (address token );
284288
289+ /// @notice Invalid dequeue operation on an empty requests queue
290+ error EmptyRequestsQueue ();
291+
292+ /// @notice Processed request does not match the head of requestsQueue
293+ error RequestProcessOutOfOrder (uint256 expectedId , uint256 processedId );
294+
285295 // ============================================
286296 // Events
287297 // ============================================
@@ -842,7 +852,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
842852 if (userPendingRequestCount[request.user] > 0 ) {
843853 userPendingRequestCount[request.user]-- ;
844854 }
845- _removePendingRequest (requestId);
855+ _removeUserPendingRequest (requestId);
856+ _dropQueuedRequest (requestId);
846857
847858 // === REFUND HANDLING (pull pattern) ===
848859 // For CREATE/DEPOSIT requests, move funds from pendingUserBalances to claimableRefunds
@@ -911,6 +922,9 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
911922 * @notice Processes a batch of PENDING requests.
912923 * @dev For successful requests, marks them as PROCESSING.
913924 * For rejected requests, marks them as FAILED.
925+ * Requests are classified as successful/rejected based on validation
926+ * logic that is performed on Cadence side, and not on the authorized
927+ * COA's discretion.
914928 * Single-request processing is supported by passing one request id in
915929 * successfulRequestIds and an empty rejectedRequestIds array.
916930 * @param successfulRequestIds The request ids to start processing (PENDING -> PROCESSING)
@@ -920,6 +934,38 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
920934 uint256 [] calldata successfulRequestIds ,
921935 uint256 [] calldata rejectedRequestIds
922936 ) external onlyAuthorizedCOA nonReentrant {
937+ uint256 totalRequests = successfulRequestIds.length + rejectedRequestIds.length ;
938+
939+ uint256 j = 0 ;
940+ uint256 k = 0 ;
941+ for (uint256 i = 0 ; i < totalRequests; i++ ) {
942+ uint256 requestId = _requestsQueue[_requestsQueueHead+ i];
943+ uint256 reqId;
944+ if (j < successfulRequestIds.length ) {
945+ reqId = successfulRequestIds[j];
946+ Request storage request = requests[reqId];
947+
948+ // === VALIDATION ===
949+ if (request.id != reqId) revert RequestNotFound ();
950+ if (request.status != RequestStatus.PENDING)
951+ revert InvalidRequestState ();
952+
953+ if (reqId == requestId) {
954+ j++ ;
955+ continue ;
956+ }
957+ }
958+
959+ if (k < rejectedRequestIds.length ) {
960+ reqId = rejectedRequestIds[k];
961+ if (reqId == requestId) {
962+ k++ ;
963+ continue ;
964+ }
965+ }
966+
967+ revert RequestProcessOutOfOrder (requestId, reqId);
968+ }
923969
924970 // === REJECTED REQUESTS ===
925971 _dropRequestsInternal (rejectedRequestIds);
@@ -1071,12 +1117,21 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
10711117 /// @notice Gets the count of pending requests
10721118 /// @return Number of pending requests
10731119 function getPendingRequestCount () external view returns (uint256 ) {
1074- return pendingRequestIds. length ;
1120+ return _requestsQueueLength () ;
10751121 }
10761122
10771123 /// @notice Gets all pending request IDs
10781124 /// @return Array of pending request IDs
10791125 function getPendingRequestIds () external view returns (uint256 [] memory ) {
1126+ uint256 [] memory pendingRequestIds = new uint256 [](_requestsQueueLength ());
1127+ uint256 arrayIndex = 0 ;
1128+ for (uint256 i = _requestsQueueHead; i < _requestsQueueTail;) {
1129+ pendingRequestIds[arrayIndex] = _requestsQueue[i];
1130+ unchecked {
1131+ ++ arrayIndex;
1132+ ++ i;
1133+ }
1134+ }
10801135 return pendingRequestIds;
10811136 }
10821137
@@ -1115,7 +1170,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
11151170 string [] memory strategyIdentifiers
11161171 )
11171172 {
1118- if (startIndex >= pendingRequestIds. length ) {
1173+ if (startIndex >= _requestsQueueLength () ) {
11191174 return (
11201175 new uint256 [](0 ),
11211176 new address [](0 ),
@@ -1131,7 +1186,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
11311186 );
11321187 }
11331188
1134- uint256 remaining = pendingRequestIds. length - startIndex;
1189+ uint256 remaining = _requestsQueueLength () - startIndex;
11351190 uint256 size = count == 0
11361191 ? remaining
11371192 : (count < remaining ? count : remaining);
@@ -1148,8 +1203,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
11481203 vaultIdentifiers = new string [](size);
11491204 strategyIdentifiers = new string [](size);
11501205
1151- for (uint256 i = 0 ; i < size; ) {
1152- Request memory req = requests[pendingRequestIds[ startIndex + i]];
1206+ for (uint256 i = 0 ; i < size;) {
1207+ Request memory req = requests[_requestsQueue[_requestsQueueHead + startIndex + i]];
11531208 ids[i] = req.id;
11541209 users[i] = req.user;
11551210 requestTypes[i] = uint8 (req.requestType);
@@ -1412,7 +1467,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
14121467 }
14131468
14141469 // Remove from pending queues (both global and user-specific)
1415- _removePendingRequest (requestId);
1470+ _removeUserPendingRequest (requestId);
1471+ _dropQueuedRequest (requestId);
14161472
14171473 emit RequestProcessed (
14181474 requestId,
@@ -1457,11 +1513,6 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
14571513 function _startProcessingInternal (uint256 requestId ) internal {
14581514 Request storage request = requests[requestId];
14591515
1460- // === VALIDATION ===
1461- if (request.id != requestId) revert RequestNotFound ();
1462- if (request.status != RequestStatus.PENDING)
1463- revert InvalidRequestState ();
1464-
14651516 // === TRANSITION TO PROCESSING ===
14661517 // This prevents cancellation and ensures atomicity with completeProcessing
14671518 request.status = RequestStatus.PROCESSING;
@@ -1517,7 +1568,9 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
15171568 if (userPendingRequestCount[request.user] > 0 ) {
15181569 userPendingRequestCount[request.user]-- ;
15191570 }
1520- _removePendingRequest (requestId);
1571+ _removeUserPendingRequest (requestId);
1572+ uint256 reqId = _dequeueRequest ();
1573+ if (reqId != requestId) revert RequestProcessOutOfOrder (reqId, requestId);
15211574
15221575 emit RequestProcessed (
15231576 requestId,
@@ -1758,8 +1811,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
17581811 });
17591812
17601813 // Add to global pending queue with index tracking for O(1) lookup
1761- _requestIndexInGlobalArray[requestId] = pendingRequestIds.length ;
1762- pendingRequestIds.push (requestId);
1814+ _enqueueRequest (requestId);
17631815 userPendingRequestCount[msg .sender ]++ ;
17641816
17651817 // Add to user's pending array with index tracking for O(1) removal
@@ -1795,40 +1847,16 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
17951847 }
17961848
17971849 /**
1798- * @dev Removes a request from all pending queues while preserving request history.
1799- * Uses two different removal strategies:
1800- * - Global array: Shift elements to maintain FIFO order (O(n) but necessary for fair processing)
1801- * - User array: Swap-and-pop for O(1) removal (order doesn't affect processing)
1850+ * @dev Removes a request from the user pending requests mapping while preserving request history.
1851+ * Uses the following removal strategy:
1852+ * - Swap-and-pop for O(1) removal (order doesn't affect processing)
18021853 *
18031854 * The request data remains in the `requests` mapping for historical queries;
1804- * this function only removes it from the pending queues .
1805- * @param requestId The request ID to remove from pending queues .
1855+ * This function only removes it from the user pending requests mapping .
1856+ * @param requestId The request ID to remove from the user pending requests mapping .
18061857 */
1807- function _removePendingRequest (uint256 requestId ) internal {
1808- // === GLOBAL PENDING ARRAY REMOVAL ===
1809- // Uses O(1) lookup + O(n) shift to maintain FIFO order
1810- // FIFO order is critical for DeFi fairness - requests must be processed in submission order
1811- uint256 indexInGlobal = _requestIndexInGlobalArray[requestId];
1812- uint256 globalLength = pendingRequestIds.length ;
1813-
1814- // Safety check: verify element exists at expected index
1815- if (globalLength > 0 && indexInGlobal < globalLength && pendingRequestIds[indexInGlobal] == requestId) {
1816- // Shift all subsequent elements left to maintain FIFO order
1817- for (uint256 j = indexInGlobal; j < globalLength - 1 ; ) {
1818- pendingRequestIds[j] = pendingRequestIds[j + 1 ];
1819- // Update index mapping for each shifted element
1820- _requestIndexInGlobalArray[pendingRequestIds[j]] = j;
1821- unchecked {
1822- ++ j;
1823- }
1824- }
1825- // Remove the last element (now duplicated or the one to remove)
1826- pendingRequestIds.pop ();
1827- // Clean up index mapping
1828- delete _requestIndexInGlobalArray[requestId];
1829- }
1830-
1831- // === USER PENDING ARRAY REMOVAL ===
1858+ function _removeUserPendingRequest (uint256 requestId ) internal {
1859+ // === USER PENDING REQUESTS ARRAY REMOVAL ===
18321860 // Uses swap-and-pop for O(1) removal (order doesn't affect FIFO processing)
18331861 address user = requests[requestId].user;
18341862 uint256 [] storage userPendingIds = pendingRequestIdsByUser[user];
@@ -1850,4 +1878,70 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
18501878 delete _requestIndexInUserArray[requestId];
18511879 }
18521880 }
1881+
1882+ /**
1883+ * @dev Enqueues a request in the requestsQueue and shifts the queue's tail pointer.
1884+ *
1885+ * @param requestId The request ID to enqueue in the pending requests queue.
1886+ */
1887+ function _enqueueRequest (uint256 requestId ) internal {
1888+ _requestsQueue[_requestsQueueTail] = requestId;
1889+ _requestsQueueTail += 1 ;
1890+ }
1891+
1892+ /**
1893+ * @dev Dequeues the head of requestsQueue and shifts the queue's head pointer.
1894+ *
1895+ * @return The request ID that was dequeued.
1896+ */
1897+ function _dequeueRequest () internal returns (uint256 ) {
1898+ if (_requestsQueueLength () == 0 ) revert EmptyRequestsQueue ();
1899+
1900+ uint256 requestId = _requestsQueue[_requestsQueueHead];
1901+
1902+ delete _requestsQueue[_requestsQueueHead];
1903+ _requestsQueueHead += 1 ;
1904+
1905+ return requestId;
1906+ }
1907+
1908+ /**
1909+ * @dev Drops a request from the requestsQueue.
1910+ * O(n) operation — scans from the removed element to the tail and shifts
1911+ * the queue to all subsequent elements left to maintain FIFO order.
1912+ *
1913+ * @param requestId The request ID to remove from the pending requests queue.
1914+ */
1915+ function _dropQueuedRequest (uint256 requestId ) internal {
1916+ bool requestFound = false ;
1917+ for (uint256 i = _requestsQueueHead; i < _requestsQueueTail;) {
1918+ if (_requestsQueue[i] == requestId) {
1919+ requestFound = true ;
1920+ }
1921+
1922+ // Shift the matching request to the queue's tail, then delete it
1923+ if (requestFound && (i + 1 < _requestsQueueTail)) {
1924+ _requestsQueue[i] = _requestsQueue[i + 1 ];
1925+ } else if (requestFound) {
1926+ delete _requestsQueue[i];
1927+ }
1928+
1929+ unchecked {
1930+ ++ i;
1931+ }
1932+ }
1933+
1934+ // Decrement the queue tail only if the given requestId was found
1935+ if (! requestFound) revert RequestNotFound ();
1936+ _requestsQueueTail -= 1 ;
1937+ }
1938+
1939+ /**
1940+ * @dev Counts the total number of pending requests in the requestsQueue.
1941+ *
1942+ * @return The current requestsQueue length.
1943+ */
1944+ function _requestsQueueLength () internal view returns (uint256 ) {
1945+ return _requestsQueueTail - _requestsQueueHead;
1946+ }
18531947}
0 commit comments