Skip to content

Commit ac78f2e

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

File tree

1 file changed

+83
-40
lines changed

1 file changed

+83
-40
lines changed

solidity/src/FlowYieldVaultsRequests.sol

Lines changed: 83 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,11 @@ 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.
1066+
* @param a The unique identifier of the request to start processing.
10121067
*/
1013-
function startProcessing(uint256 requestId) external onlyAuthorizedCOA nonReentrant {
1068+
function startProcessing(uint256 a) external onlyAuthorizedCOA nonReentrant {
1069+
// Pick the request at the front of the queue, for processing
1070+
uint256 requestId = _peek();
10141071
Request storage request = requests[requestId];
10151072

10161073
// === VALIDATION ===
@@ -1094,17 +1151,19 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
10941151
* - Successful CLOSE: Unregisters YieldVault ownership
10951152
*
10961153
* For all other cases (success, WITHDRAW/CLOSE), msg.value must be 0.
1097-
* @param requestId The unique identifier of the request to complete.
1154+
* @param a The unique identifier of the request to complete.
10981155
* @param success True if the Cadence operation succeeded, false otherwise.
10991156
* @param yieldVaultId The YieldVault Id from Cadence (for CREATE: newly assigned; for others: existing).
11001157
* @param message Human-readable status message or error description.
11011158
*/
11021159
function completeProcessing(
1103-
uint256 requestId,
1160+
uint256 a,
11041161
bool success,
11051162
uint64 yieldVaultId,
11061163
string calldata message
11071164
) external payable onlyAuthorizedCOA nonReentrant {
1165+
// Pick the request at the front of the queue, for processing
1166+
uint256 requestId = _peek();
11081167
Request storage request = requests[requestId];
11091168

11101169
// === VALIDATION ===
@@ -1181,6 +1240,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
11811240
userPendingRequestCount[request.user]--;
11821241
}
11831242
_removePendingRequest(requestId);
1243+
_dequeue();
11841244

11851245
emit RequestProcessed(requestId, request.user, request.requestType, newStatus, yieldVaultId, message);
11861246
}
@@ -1222,12 +1282,19 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
12221282
/// @notice Gets the count of pending requests
12231283
/// @return Number of pending requests
12241284
function getPendingRequestCount() external view returns (uint256) {
1225-
return pendingRequestIds.length;
1285+
return _queueLength();
12261286
}
12271287

12281288
/// @notice Gets all pending request IDs
12291289
/// @return Array of pending request IDs
12301290
function getPendingRequestIds() external view returns (uint256[] memory) {
1291+
uint256[] memory pendingRequestIds = new uint256[](_queueLength());
1292+
for (uint256 i = first; i <= last;) {
1293+
pendingRequestIds[i] = queue[i];
1294+
unchecked {
1295+
++i;
1296+
}
1297+
}
12311298
return pendingRequestIds;
12321299
}
12331300

@@ -1266,7 +1333,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
12661333
string[] memory strategyIdentifiers
12671334
)
12681335
{
1269-
if (startIndex >= pendingRequestIds.length) {
1336+
if (startIndex >= _queueLength()) {
12701337
return (
12711338
new uint256[](0),
12721339
new address[](0),
@@ -1282,7 +1349,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
12821349
);
12831350
}
12841351

1285-
uint256 remaining = pendingRequestIds.length - startIndex;
1352+
uint256 remaining = _queueLength() - startIndex;
12861353
uint256 size = count == 0
12871354
? remaining
12881355
: (count < remaining ? count : remaining);
@@ -1299,8 +1366,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
12991366
vaultIdentifiers = new string[](size);
13001367
strategyIdentifiers = new string[](size);
13011368

1302-
for (uint256 i = 0; i < size; ) {
1303-
Request memory req = requests[pendingRequestIds[startIndex + i]];
1369+
for (uint256 i = 0; i < size;) {
1370+
Request memory req = requests[queue[first + startIndex + i]];
13041371
ids[i] = req.id;
13051372
users[i] = req.user;
13061373
requestTypes[i] = uint8(req.requestType);
@@ -1672,8 +1739,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
16721739
});
16731740

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

16791745
// Add to user's pending array with index tracking for O(1) removal
@@ -1719,29 +1785,6 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
17191785
* @param requestId The request ID to remove from pending queues.
17201786
*/
17211787
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-
17451788
// === USER PENDING ARRAY REMOVAL ===
17461789
// Uses swap-and-pop for O(1) removal (order doesn't affect FIFO processing)
17471790
address user = requests[requestId].user;

0 commit comments

Comments
 (0)