Skip to content

Commit 8ad764e

Browse files
committed
Enforce fair FIFO queue ordering on-chain
1 parent c59ef47 commit 8ad764e

File tree

2 files changed

+186
-150
lines changed

2 files changed

+186
-150
lines changed

solidity/src/FlowYieldVaultsRequests.sol

Lines changed: 80 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -167,16 +167,69 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
167167
/// @notice All requests indexed by request ID
168168
mapping(uint256 => Request) public requests;
169169

170-
/// @notice Array of pending request IDs awaiting processing (FIFO order)
171-
uint256[] public pendingRequestIds;
172-
173-
/// @notice Index of request ID in global pending array (for O(1) lookup)
174-
mapping(uint256 => uint256) private _requestIndexInGlobalArray;
175-
176170
/// @notice Index of yieldVaultId in user's yieldVaultsByUser array (for O(1) removal)
177171
/// @dev Internal visibility allows test helpers to properly initialize state
178172
mapping(address => mapping(uint64 => uint256)) internal _yieldVaultIndexInUserArray;
179173

174+
mapping(uint256 => uint256) queue;
175+
uint256 first = 1;
176+
uint256 last = 1;
177+
178+
function _enqueue(uint256 requestId) internal {
179+
// empty queue
180+
if (first == last) {
181+
queue[first] = requestId;
182+
} else {
183+
queue[last] = requestId;
184+
}
185+
last += 1;
186+
}
187+
188+
function _dequeue() internal returns (uint256) {
189+
require(last > first);
190+
191+
uint256 requestId;
192+
requestId = queue[first];
193+
194+
delete queue[first];
195+
first += 1;
196+
197+
return requestId;
198+
}
199+
200+
function _drop(uint256 requestId) internal {
201+
bool slide = false;
202+
for (uint256 i = first; i <= last;) {
203+
if (queue[i] == requestId) {
204+
delete queue[i];
205+
slide = true;
206+
}
207+
208+
if (slide) {
209+
queue[i] = queue[i+1];
210+
}
211+
212+
unchecked {
213+
++i;
214+
}
215+
}
216+
217+
last -= 1;
218+
}
219+
220+
function _peek() internal view returns (uint256) {
221+
require(last > first);
222+
223+
uint256 requestId;
224+
requestId = queue[first];
225+
226+
return requestId;
227+
}
228+
229+
function _queueLength() internal view returns (uint256) {
230+
return last - first;
231+
}
232+
180233
// ============================================
181234
// Errors
182235
// ============================================
@@ -728,6 +781,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
728781

729782
// Remove from pending queues (both global and user-specific)
730783
_removePendingRequest(requestId);
784+
_drop(requestId);
731785

732786
emit RequestProcessed(
733787
requestId,
@@ -929,6 +983,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
929983
userPendingRequestCount[request.user]--;
930984
}
931985
_removePendingRequest(requestId);
986+
_drop(requestId);
932987

933988
// === REFUND HANDLING (pull pattern) ===
934989
// For CREATE/DEPOSIT requests, move funds from pendingUserBalances to claimableRefunds
@@ -1008,9 +1063,10 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
10081063
* - Funds will be bridged back from Cadence in completeProcessing
10091064
*
10101065
* The PROCESSING status prevents request cancellation and double-processing.
1011-
* @param requestId The unique identifier of the request to start processing.
10121066
*/
1013-
function startProcessing(uint256 requestId) external onlyAuthorizedCOA nonReentrant {
1067+
function startProcessing() external onlyAuthorizedCOA nonReentrant {
1068+
// Pick the request at the front of the queue, for processing
1069+
uint256 requestId = _peek();
10141070
Request storage request = requests[requestId];
10151071

10161072
// === VALIDATION ===
@@ -1094,17 +1150,17 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
10941150
* - Successful CLOSE: Unregisters YieldVault ownership
10951151
*
10961152
* For all other cases (success, WITHDRAW/CLOSE), msg.value must be 0.
1097-
* @param requestId The unique identifier of the request to complete.
10981153
* @param success True if the Cadence operation succeeded, false otherwise.
10991154
* @param yieldVaultId The YieldVault Id from Cadence (for CREATE: newly assigned; for others: existing).
11001155
* @param message Human-readable status message or error description.
11011156
*/
11021157
function completeProcessing(
1103-
uint256 requestId,
11041158
bool success,
11051159
uint64 yieldVaultId,
11061160
string calldata message
11071161
) external payable onlyAuthorizedCOA nonReentrant {
1162+
// Pick the request at the front of the queue, for processing
1163+
uint256 requestId = _peek();
11081164
Request storage request = requests[requestId];
11091165

11101166
// === VALIDATION ===
@@ -1181,6 +1237,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
11811237
userPendingRequestCount[request.user]--;
11821238
}
11831239
_removePendingRequest(requestId);
1240+
_dequeue();
11841241

11851242
emit RequestProcessed(requestId, request.user, request.requestType, newStatus, yieldVaultId, message);
11861243
}
@@ -1222,12 +1279,19 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
12221279
/// @notice Gets the count of pending requests
12231280
/// @return Number of pending requests
12241281
function getPendingRequestCount() external view returns (uint256) {
1225-
return pendingRequestIds.length;
1282+
return _queueLength();
12261283
}
12271284

12281285
/// @notice Gets all pending request IDs
12291286
/// @return Array of pending request IDs
12301287
function getPendingRequestIds() external view returns (uint256[] memory) {
1288+
uint256[] memory pendingRequestIds = new uint256[](_queueLength());
1289+
for (uint256 i = first; i <= last;) {
1290+
pendingRequestIds[i] = queue[i];
1291+
unchecked {
1292+
++i;
1293+
}
1294+
}
12311295
return pendingRequestIds;
12321296
}
12331297

@@ -1266,7 +1330,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
12661330
string[] memory strategyIdentifiers
12671331
)
12681332
{
1269-
if (startIndex >= pendingRequestIds.length) {
1333+
if (startIndex >= _queueLength()) {
12701334
return (
12711335
new uint256[](0),
12721336
new address[](0),
@@ -1282,7 +1346,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
12821346
);
12831347
}
12841348

1285-
uint256 remaining = pendingRequestIds.length - startIndex;
1349+
uint256 remaining = _queueLength() - startIndex;
12861350
uint256 size = count == 0
12871351
? remaining
12881352
: (count < remaining ? count : remaining);
@@ -1299,8 +1363,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
12991363
vaultIdentifiers = new string[](size);
13001364
strategyIdentifiers = new string[](size);
13011365

1302-
for (uint256 i = 0; i < size; ) {
1303-
Request memory req = requests[pendingRequestIds[startIndex + i]];
1366+
for (uint256 i = 0; i < size;) {
1367+
Request memory req = requests[queue[first + startIndex + i]];
13041368
ids[i] = req.id;
13051369
users[i] = req.user;
13061370
requestTypes[i] = uint8(req.requestType);
@@ -1672,8 +1736,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
16721736
});
16731737

16741738
// Add to global pending queue with index tracking for O(1) lookup
1675-
_requestIndexInGlobalArray[requestId] = pendingRequestIds.length;
1676-
pendingRequestIds.push(requestId);
1739+
_enqueue(requestId);
16771740
userPendingRequestCount[msg.sender]++;
16781741

16791742
// Add to user's pending array with index tracking for O(1) removal
@@ -1719,29 +1782,6 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
17191782
* @param requestId The request ID to remove from pending queues.
17201783
*/
17211784
function _removePendingRequest(uint256 requestId) internal {
1722-
// === GLOBAL PENDING ARRAY REMOVAL ===
1723-
// Uses O(1) lookup + O(n) shift to maintain FIFO order
1724-
// FIFO order is critical for DeFi fairness - requests must be processed in submission order
1725-
uint256 indexInGlobal = _requestIndexInGlobalArray[requestId];
1726-
uint256 globalLength = pendingRequestIds.length;
1727-
1728-
// Safety check: verify element exists at expected index
1729-
if (globalLength > 0 && indexInGlobal < globalLength && pendingRequestIds[indexInGlobal] == requestId) {
1730-
// Shift all subsequent elements left to maintain FIFO order
1731-
for (uint256 j = indexInGlobal; j < globalLength - 1; ) {
1732-
pendingRequestIds[j] = pendingRequestIds[j + 1];
1733-
// Update index mapping for each shifted element
1734-
_requestIndexInGlobalArray[pendingRequestIds[j]] = j;
1735-
unchecked {
1736-
++j;
1737-
}
1738-
}
1739-
// Remove the last element (now duplicated or the one to remove)
1740-
pendingRequestIds.pop();
1741-
// Clean up index mapping
1742-
delete _requestIndexInGlobalArray[requestId];
1743-
}
1744-
17451785
// === USER PENDING ARRAY REMOVAL ===
17461786
// Uses swap-and-pop for O(1) removal (order doesn't affect FIFO processing)
17471787
address user = requests[requestId].user;

0 commit comments

Comments
 (0)