fix(FLOW-10): Enforce fair FIFO queue ordering on-chain#46
fix(FLOW-10): Enforce fair FIFO queue ordering on-chain#46
Conversation
ebe63d0 to
8ad764e
Compare
9a20c8f to
5717ad2
Compare
74c948d to
371f132
Compare
Code Review: Enforce Fair FIFO Queue Ordering On-ChainSummaryThis PR addresses FLOW-10 by replacing the O(n)-shift pendingRequestIds array with a mapping-based queue (head/tail pointers), and enforcing FIFO ordering on-chain via _dequeueRequest() inside _startProcessingInternal. The COA can no longer skip a successful request — any out-of-order attempt reverts with RequestProcessOutOfOrder. The overall approach is sound and the implementation is correct. Strengths
Important Issues1. Cadence worker not updated — will cause production reverts The Solidity contract now requires successful requests to be submitted to startProcessingBatch in strict FIFO queue order. However, neither FlowYieldVaultsEVM.cdc nor FlowYieldVaultsEVMWorkerOps.cdc has been updated in this PR. If the Cadence worker currently selects or orders requests by any logic other than queue-head-first, every call to startProcessingBatch will revert with RequestProcessOutOfOrder. This is the highest-priority gap to close — either update the Cadence code in this PR, or document the required Cadence changes and block merging on their completion. 2. FIFO bypass for rejections remains possible startProcessingBatch enforces FIFO for successfulRequestIds (each validated against the queue head), but rejectedRequestIds are removed via _dropQueuedRequest which can target any queue position. A COA operator could achieve non-FIFO outcomes by classifying early requests as rejected — e.g., rejecting req1-req3 and processing req4-req5 skips ahead in the queue. This may be intentional — genuinely invalid requests should not block valid ones — but the NatDoc comment added in this PR is a trust assumption, not an on-chain guarantee. Consider making the intended threat model explicit. Minor Issues3. requestsQueue visibility could be private requestsQueue is declared internal, making it accessible to inheriting contracts and allowing queue invariants to be bypassed. If no inheritance is intended, private would be safer. 4. _dropQueuedRequest silently no-ops for unknown IDs If requestId is not found in the queue, shiftQueue stays false and the function returns without any revert or event. Callers validate state first, so this is safe in practice, but a comment explaining why silent failure is acceptable here would improve maintainability. 5. Performance claim slightly overstates improvement _dequeueRequest is now O(1), which is a genuine win. However _dropQueuedRequest (used for cancellations and batch rejections) is still O(n) — the same asymptotic cost as the old array-shift approach, though with fewer SSTORE operations. This is unavoidable for arbitrary-position removal in a FIFO queue, but worth noting to set correct expectations. Test Observations
Generated with Claude Code |
71b9aeb to
9d7d107
Compare
a677e40 to
1760973
Compare
1760973 to
95d1302
Compare
Closes #25
FLOW-10: FIFO Queue Is Not Enforced on-Chain yet Costs O(n) to Maintain
Introduce an optimized queue data structure (mapping-based with head & tail pointers), to avoid the high gas costs of maintaining the FIFO order on-chain. Both
enqueue&dequeueoperations are now O(1).The previous queue data structure required an array and a mapping, and had O(n) performance when removing a request from the pending queue:
In additionn
_startProcessingInternalnow has a check which verifies that the request being processed is the head of therequestsQueueFIFO queue:If that is not the case, the function call reverts with
RequestProcessOutOfOrder.The Cadence
Schedulerthat schedules & processes the requests, is fetching the request IDs with:which returns a given count of pending EVM requests from the queue, in FIFO order.
These are fed to
preprocessRequests(), and after the validation checks, they are classified as successful/rejected, and they are then passed in tostartProcessingBatch(), which drops the rejected request IDs, and calls_startProcessingInternal()for each individual successful request ID.