Skip to content

Commit 76c7bec

Browse files
committed
Enforce fair FIFO queue ordering on-chain
1 parent c6bace8 commit 76c7bec

2 files changed

Lines changed: 425 additions & 97 deletions

File tree

solidity/src/FlowYieldVaultsRequests.sol

Lines changed: 146 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)